diff --git a/src/board_widget.cpp b/src/board_widget.cpp new file mode 100644 index 0000000..7e283db --- /dev/null +++ b/src/board_widget.cpp @@ -0,0 +1,332 @@ +#include "board_widget.hpp" +#include +#include +#include + +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 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& 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 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((x - offset_x_) / cell_size_); + int cy = static_cast((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& 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& 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& 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& 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& 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& 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(); + } +} diff --git a/src/board_widget.hpp b/src/board_widget.hpp new file mode 100644 index 0000000..364f7b2 --- /dev/null +++ b/src/board_widget.hpp @@ -0,0 +1,69 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#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 field); + + // Signal when state changes (for UI updates) + sigc::signal signal_state_changed; + + // Animation + void start_confetti(); + bool on_animation_tick(const Glib::RefPtr& clock); + bool process_animation(); // Internal helper + +private: + void on_draw(const Cairo::RefPtr& 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 get_cell_at(double x, double y); + void draw_cell(const Cairo::RefPtr& cr, int x, int y, double size, const Cell& cell); + void draw_digit(const Cairo::RefPtr& cr, int number, double x, double y, double size); + void draw_flag(const Cairo::RefPtr& cr, double x, double y, double size); + void draw_bomb(const Cairo::RefPtr& cr, double x, double y, double size, bool exploded); + void draw_particles(const Cairo::RefPtr& cr); + + std::shared_ptr 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 particles_; + guint tick_id_ = 0; + + // Controllers + Glib::RefPtr left_click_controller_; + Glib::RefPtr right_click_controller_; + Glib::RefPtr motion_controller_; +}; diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..1886f18 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,8 @@ +#include "window.hpp" +#include + +int main(int argc, char* argv[]) { + auto app = Gtk::Application::create("org.gtkmm.minesweeper"); + + return app->make_window_and_run(argc, argv); +}