This commit is contained in:
Bernardo Magri
2026-02-25 13:36:44 +00:00
parent 23316594f7
commit 2456814295
3 changed files with 409 additions and 0 deletions

332
src/board_widget.cpp Normal file
View File

@@ -0,0 +1,332 @@
#include "board_widget.hpp"
#include <iostream>
#include <cmath>
#include <random>
BoardWidget::BoardWidget() {
set_hexpand(true);
set_vexpand(true);
// Setup Draw Function
set_draw_func(sigc::mem_fun(*this, &BoardWidget::on_draw));
// Left Click
left_click_controller_ = Gtk::GestureClick::create();
left_click_controller_->set_button(GDK_BUTTON_PRIMARY);
left_click_controller_->signal_pressed().connect(sigc::mem_fun(*this, &BoardWidget::on_click_pressed));
add_controller(left_click_controller_);
// Right Click
right_click_controller_ = Gtk::GestureClick::create();
right_click_controller_->set_button(GDK_BUTTON_SECONDARY);
right_click_controller_->signal_pressed().connect(sigc::mem_fun(*this, &BoardWidget::on_right_click_pressed));
add_controller(right_click_controller_);
// Motion (Hover)
motion_controller_ = Gtk::EventControllerMotion::create();
motion_controller_->signal_enter().connect(sigc::mem_fun(*this, &BoardWidget::on_motion));
motion_controller_->signal_motion().connect(sigc::mem_fun(*this, &BoardWidget::on_motion));
motion_controller_->signal_leave().connect(sigc::mem_fun(*this, &BoardWidget::on_leave));
add_controller(motion_controller_);
}
BoardWidget::~BoardWidget() {
if (tick_id_ > 0) remove_tick_callback(tick_id_);
}
void BoardWidget::set_minefield(std::shared_ptr<Minefield> field) {
field_ = field;
particles_.clear();
if (tick_id_ > 0) {
remove_tick_callback(tick_id_);
tick_id_ = 0;
}
queue_draw();
}
void BoardWidget::start_confetti() {
particles_.clear();
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<> pos_dist(0.0, 1.0); // Relative to window
std::uniform_real_distribution<> vel_dist(-2.0, 2.0);
std::uniform_real_distribution<> color_dist(0.0, 1.0);
std::uniform_real_distribution<> size_dist(3.0, 8.0);
int w = get_width();
int h = get_height();
for (int i = 0; i < 200; ++i) {
particles_.push_back({
pos_dist(gen) * w,
pos_dist(gen) * h * 0.5, // Start from top half
vel_dist(gen),
vel_dist(gen) + 2.0, // Fall down
1.0, // Life
color_dist(gen), color_dist(gen), color_dist(gen),
size_dist(gen)
});
}
if (tick_id_ == 0) {
tick_id_ = add_tick_callback(
sigc::mem_fun(*this, &BoardWidget::on_animation_tick)
);
}
}
bool BoardWidget::on_animation_tick(const Glib::RefPtr<Gdk::FrameClock>& clock) {
return process_animation(); // Overload
}
bool BoardWidget::process_animation() {
if (particles_.empty()) return false;
bool alive = false;
for (auto& p : particles_) {
p.x += p.vx;
p.y += p.vy;
p.vy += 0.1; // Gravity
p.life -= 0.01;
if (p.life > 0) alive = true;
}
queue_draw();
return alive;
}
std::pair<int, int> BoardWidget::get_cell_at(double x, double y) {
if (!field_) return {-1, -1};
// Use stored metrics from last draw
if (x < offset_x_ || x >= offset_x_ + field_->cols() * cell_size_ ||
y < offset_y_ || y >= offset_y_ + field_->rows() * cell_size_) {
return {-1, -1};
}
int cx = static_cast<int>((x - offset_x_) / cell_size_);
int cy = static_cast<int>((y - offset_y_) / cell_size_);
return {cx, cy};
}
void BoardWidget::on_click_pressed(int n_press, double x, double y) {
if (!field_) return;
auto [cx, cy] = get_cell_at(x, y);
if (cx == -1) return;
bool changed = false;
if (field_->get_cell(cx, cy).is_revealed) {
changed = field_->chord_cell(cx, cy);
} else {
changed = field_->open_cell(cx, cy);
}
if (changed) {
signal_state_changed.emit();
if (field_->state() == GameState::Won) {
start_confetti();
}
queue_draw();
}
}
void BoardWidget::on_right_click_pressed(int n_press, double x, double y) {
if (!field_) return;
auto [cx, cy] = get_cell_at(x, y);
if (cx == -1) return;
if (field_->toggle_flag(cx, cy)) {
signal_state_changed.emit();
queue_draw();
}
}
void BoardWidget::on_motion(double x, double y) {
auto [cx, cy] = get_cell_at(x, y);
if (cx != hover_x_ || cy != hover_y_) {
hover_x_ = cx;
hover_y_ = cy;
queue_draw();
}
}
void BoardWidget::on_leave() {
hover_x_ = -1;
hover_y_ = -1;
queue_draw();
}
void BoardWidget::on_draw(const Cairo::RefPtr<Cairo::Context>& cr, int width, int height) {
// Background
cr->set_source_rgb(0.95, 0.95, 0.95);
cr->paint();
if (field_) {
// Calculate scaling
int cols = field_->cols();
int rows = field_->rows();
double pad = 20.0;
double avail_w = width - 2 * pad;
double avail_h = height - 2 * pad;
cell_size_ = std::min(avail_w / cols, avail_h / rows);
cell_size_ = std::min(cell_size_, 48.0);
cell_size_ = std::max(cell_size_, 16.0);
double board_w_px = cols * cell_size_;
double board_h_px = rows * cell_size_;
offset_x_ = (width - board_w_px) / 2.0;
offset_y_ = (height - board_h_px) / 2.0;
cr->save();
cr->translate(offset_x_, offset_y_);
// Shadow for the board
cr->set_source_rgba(0.0, 0.0, 0.0, 0.1);
cr->rectangle(5, 5, board_w_px, board_h_px);
cr->fill();
for (int y = 0; y < rows; ++y) {
for (int x = 0; x < cols; ++x) {
draw_cell(cr, x, y, cell_size_, field_->get_cell(x, y));
}
}
cr->restore();
}
if (!particles_.empty()) {
draw_particles(cr);
}
}
void BoardWidget::draw_cell(const Cairo::RefPtr<Cairo::Context>& cr, int x, int y, double size, const Cell& cell) {
double px = x * size;
double py = y * size;
// Base shape
cr->rectangle(px, py, size, size);
bool is_hover = (x == hover_x_ && y == hover_y_ && field_->state() == GameState::Playing);
bool revealed = cell.is_revealed;
if (revealed) {
if (cell.is_exploded) {
cr->set_source_rgb(0.9, 0.3, 0.3); // Red background
} else {
cr->set_source_rgb(0.92, 0.92, 0.92); // Revealed background (lighter gray)
}
} else {
if (is_hover) {
cr->set_source_rgb(0.75, 0.85, 1.0); // Hover light blue
} else {
// Gradient for unrevealed cells
auto pat = Cairo::LinearGradient::create(px, py, px + size, py + size);
pat->add_color_stop_rgba(0, 0.8, 0.8, 0.8, 1);
pat->add_color_stop_rgba(1, 0.7, 0.7, 0.7, 1);
cr->set_source(pat);
}
}
if (!revealed && !is_hover) cr->fill();
else cr->fill_preserve();
if (revealed || is_hover) {
// Border
cr->set_source_rgb(0.6, 0.6, 0.6);
cr->set_line_width(1.0);
cr->stroke();
}
// Content
if (cell.is_flagged) {
draw_flag(cr, px, py, size);
} else if (revealed) {
if (cell.is_bomb) {
draw_bomb(cr, px, py, size, cell.is_exploded);
} else if (cell.nearby_bombs > 0) {
draw_digit(cr, cell.nearby_bombs, px, py, size);
}
}
}
void BoardWidget::draw_flag(const Cairo::RefPtr<Cairo::Context>& cr, double x, double y, double size) {
double cx = x + size / 2.0;
double cy = y + size / 2.0;
// Pole
cr->set_source_rgb(0.2, 0.2, 0.2);
cr->set_line_width(2.0);
cr->move_to(cx - size * 0.1, cy + size * 0.3);
cr->line_to(cx - size * 0.1, cy - size * 0.3);
cr->stroke();
// Flag
cr->set_source_rgb(0.9, 0.2, 0.2);
cr->move_to(cx - size * 0.1, cy - size * 0.3);
cr->line_to(cx + size * 0.25, cy - size * 0.15);
cr->line_to(cx - size * 0.1, cy);
cr->close_path();
cr->fill();
}
void BoardWidget::draw_bomb(const Cairo::RefPtr<Cairo::Context>& cr, double x, double y, double size, bool exploded) {
double cx = x + size / 2.0;
double cy = y + size / 2.0;
double r = size * 0.25;
// Bomb body
auto pat = Cairo::RadialGradient::create(cx - r*0.3, cy - r*0.3, r*0.1, cx, cy, r);
pat->add_color_stop_rgba(0, 0.4, 0.4, 0.4, 1);
pat->add_color_stop_rgba(1, 0.1, 0.1, 0.1, 1);
cr->set_source(pat);
cr->arc(cx, cy, r, 0, 2 * M_PI);
cr->fill();
// Spikes/Fuse
cr->set_source_rgb(0.1, 0.1, 0.1);
cr->set_line_width(2.0);
cr->move_to(cx, cy - r);
cr->line_to(cx, cy - r - size * 0.1);
cr->stroke();
// Spark if exploded?
if (exploded) {
cr->set_source_rgb(1.0, 0.5, 0.0);
cr->arc(cx, cy - r - size * 0.1, size * 0.05, 0, 2 * M_PI);
cr->fill();
}
}
void BoardWidget::draw_digit(const Cairo::RefPtr<Cairo::Context>& cr, int number, double x, double y, double size) {
switch (number) {
case 1: cr->set_source_rgb(0.2, 0.4, 1.0); break; // Blue
case 2: cr->set_source_rgb(0.2, 0.6, 0.2); break; // Green
case 3: cr->set_source_rgb(0.9, 0.2, 0.2); break; // Red
case 4: cr->set_source_rgb(0.1, 0.1, 0.6); break; // Dark Blue
case 5: cr->set_source_rgb(0.6, 0.1, 0.1); break; // Maroon
case 6: cr->set_source_rgb(0.1, 0.6, 0.6); break; // Cyan
case 7: cr->set_source_rgb(0.1, 0.1, 0.1); break; // Black
case 8: cr->set_source_rgb(0.5, 0.5, 0.5); break; // Gray
}
cr->select_font_face("Sans", Cairo::ToyFontFace::Slant::NORMAL, Cairo::ToyFontFace::Weight::BOLD);
cr->set_font_size(size * 0.7); // Bigger font
Cairo::TextExtents extents;
std::string text = std::to_string(number);
cr->get_text_extents(text, extents);
cr->move_to(x + (size - extents.width) / 2 - extents.x_bearing,
y + (size - extents.height) / 2 - extents.y_bearing);
cr->show_text(text);
}
void BoardWidget::draw_particles(const Cairo::RefPtr<Cairo::Context>& cr) {
for (const auto& p : particles_) {
if (p.life <= 0) continue;
cr->set_source_rgba(p.r, p.g, p.b, p.life);
cr->rectangle(p.x, p.y, p.size, p.size);
cr->fill();
}
}

69
src/board_widget.hpp Normal file
View File

@@ -0,0 +1,69 @@
#pragma once
#include <gtkmm/drawingarea.h>
#include <gtkmm/gestureclick.h>
#include <gtkmm/eventcontrollermotion.h>
#include <gdkmm/texture.h>
#include <memory>
#include <vector>
#include <random>
#include "minefield.hpp"
struct Particle {
double x, y;
double vx, vy;
double life;
double r, g, b;
double size;
};
class BoardWidget : public Gtk::DrawingArea {
public:
BoardWidget();
virtual ~BoardWidget();
void set_minefield(std::shared_ptr<Minefield> field);
// Signal when state changes (for UI updates)
sigc::signal<void()> signal_state_changed;
// Animation
void start_confetti();
bool on_animation_tick(const Glib::RefPtr<Gdk::FrameClock>& clock);
bool process_animation(); // Internal helper
private:
void on_draw(const Cairo::RefPtr<Cairo::Context>& cr, int width, int height);
// Input handlers
void on_click_pressed(int n_press, double x, double y);
void on_right_click_pressed(int n_press, double x, double y);
void on_motion(double x, double y);
void on_leave();
// Helpers
std::pair<int, int> get_cell_at(double x, double y);
void draw_cell(const Cairo::RefPtr<Cairo::Context>& cr, int x, int y, double size, const Cell& cell);
void draw_digit(const Cairo::RefPtr<Cairo::Context>& cr, int number, double x, double y, double size);
void draw_flag(const Cairo::RefPtr<Cairo::Context>& cr, double x, double y, double size);
void draw_bomb(const Cairo::RefPtr<Cairo::Context>& cr, double x, double y, double size, bool exploded);
void draw_particles(const Cairo::RefPtr<Cairo::Context>& cr);
std::shared_ptr<Minefield> field_;
// State
double cell_size_ = 32.0;
double offset_x_ = 0;
double offset_y_ = 0;
int hover_x_ = -1;
int hover_y_ = -1;
// Particles
std::vector<Particle> particles_;
guint tick_id_ = 0;
// Controllers
Glib::RefPtr<Gtk::GestureClick> left_click_controller_;
Glib::RefPtr<Gtk::GestureClick> right_click_controller_;
Glib::RefPtr<Gtk::EventControllerMotion> motion_controller_;
};

8
src/main.cpp Normal file
View File

@@ -0,0 +1,8 @@
#include "window.hpp"
#include <gtkmm/application.h>
int main(int argc, char* argv[]) {
auto app = Gtk::Application::create("org.gtkmm.minesweeper");
return app->make_window_and_run<MainWindow>(argc, argv);
}