#include "window.hpp" #include #include #include #include #include #include namespace { std::string format_time(int secs) { char buf[16]; snprintf(buf, sizeof(buf), "%02d:%02d", secs / 60, secs % 60); return buf; } } // namespace MainWindow::MainWindow() { set_title("NoMines"); set_default_size(800, 600); setup_header_bar(); setup_board(); // Popovers: some GTK themes (and newer GTK versions) leave the popover // surface transparent unless the content opts in. Guarantee an opaque, // theme-aware background. auto popover_css = Gtk::CssProvider::create(); popover_css->load_from_data( "popover.background > contents { background-color: @theme_bg_color; }"); Gtk::StyleContext::add_provider_for_display( get_display(), popover_css, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); // Keyboard shortcuts auto key_controller = Gtk::EventControllerKey::create(); key_controller->signal_key_pressed().connect([this](guint keyval, guint /*keycode*/, Gdk::ModifierType mods) { const Gdk::ModifierType none = Gdk::ModifierType(0); bool plain = (mods & Gdk::ModifierType::SHIFT_MASK) == none && (mods & Gdk::ModifierType::CONTROL_MASK) == none && (mods & Gdk::ModifierType::ALT_MASK) == none && (mods & Gdk::ModifierType::META_MASK) == none; switch (keyval) { case GDK_KEY_r: if (plain) { start_new_game(current_difficulty_); return true; } break; case GDK_KEY_n: if ((mods & Gdk::ModifierType::CONTROL_MASK) != none) { start_new_game(current_difficulty_); return true; } break; case GDK_KEY_1: if (plain) { start_new_game(Minefield::DifficultyEasy); return true; } break; case GDK_KEY_2: if (plain) { start_new_game(Minefield::DifficultyMedium); return true; } break; case GDK_KEY_3: if (plain) { start_new_game(Minefield::DifficultyHard); return true; } break; case GDK_KEY_4: if (plain) { start_new_game(Minefield::DifficultyExpert); return true; } break; case GDK_KEY_l: if (plain) { if (menu_leaderboard_.get_visible()) menu_leaderboard_.popdown(); else menu_leaderboard_.popup(); return true; } break; } return false; }, false); add_controller(key_controller); // Start initial game start_new_game(Minefield::DifficultyEasy); } MainWindow::~MainWindow() { timer_conn_.disconnect(); dialog_timeout_conn_.disconnect(); } void MainWindow::setup_header_bar() { set_titlebar(header_bar_); // New Game Button btn_new_game_.set_label("New Game"); btn_new_game_.signal_clicked().connect([this]() { start_new_game(current_difficulty_); }); header_bar_.pack_start(btn_new_game_); // Difficulty Menu btn_difficulty_.set_label(Minefield::DifficultyEasy.name); box_difficulty_.set_orientation(Gtk::Orientation::VERTICAL); box_difficulty_.set_margin(10); box_difficulty_.set_spacing(5); auto group = Gtk::make_managed(Minefield::DifficultyEasy.name); group->add_css_class("radio"); group->signal_toggled().connect([this, group]() { if (!group->get_active()) return; start_new_game(Minefield::DifficultyEasy); btn_difficulty_.popdown(); }); box_difficulty_.append(*group); difficulty_buttons_.emplace_back(Minefield::DifficultyEasy, group); auto add_diff_btn = [&](const GameDifficulty& diff) { auto* btn = Gtk::make_managed(diff.name); btn->add_css_class("radio"); btn->set_group(*group); btn->signal_toggled().connect([this, btn, diff]() { if (!btn->get_active()) return; // Only react when this one is chosen start_new_game(diff); btn_difficulty_.popdown(); }); box_difficulty_.append(*btn); difficulty_buttons_.emplace_back(diff, btn); }; add_diff_btn(Minefield::DifficultyMedium); add_diff_btn(Minefield::DifficultyHard); add_diff_btn(Minefield::DifficultyExpert); group->set_active(true); btn_difficulty_.set_popover(menu_difficulty_); menu_difficulty_.set_child(box_difficulty_); menu_difficulty_.add_css_class("menu"); header_bar_.pack_start(btn_difficulty_); // Leaderboard btn_leaderboard_.set_label("Leaderboard"); box_leaderboard_.set_orientation(Gtk::Orientation::VERTICAL); box_leaderboard_.set_margin(10); box_leaderboard_.set_spacing(4); btn_leaderboard_.set_popover(menu_leaderboard_); menu_leaderboard_.set_child(box_leaderboard_); menu_leaderboard_.add_css_class("menu"); menu_leaderboard_.signal_show().connect(sigc::mem_fun(*this, &MainWindow::refresh_leaderboard)); header_bar_.pack_start(btn_leaderboard_); // Status Labels lbl_flags_.set_margin_end(10); lbl_time_.set_margin_end(10); // Monospaced and bold so the numbers don't jitter lbl_flags_.set_markup("Flags: 0"); lbl_time_.set_markup("Time: 00:00"); header_bar_.pack_end(lbl_time_); header_bar_.pack_end(lbl_flags_); } void MainWindow::refresh_leaderboard() { while (auto* child = box_leaderboard_.get_first_child()) { box_leaderboard_.remove(*child); } auto* title = Gtk::make_managed(); title->set_markup("Best Times"); box_leaderboard_.append(*title); for (const auto& diff : {Minefield::DifficultyEasy, Minefield::DifficultyMedium, Minefield::DifficultyHard, Minefield::DifficultyExpert}) { auto* row = Gtk::make_managed(); row->set_xalign(0.0f); auto best = leaderboard_.best_time(diff.name); std::string time_str = best ? format_time(*best) : "—"; row->set_text(diff.name + ": " + time_str); box_leaderboard_.append(*row); } } void MainWindow::setup_board() { // Minimum size so the largest board (30x20, Master) fits at the 16px // cell floor even when tiled very small. board_widget_.set_size_request(520, 360); set_child(board_widget_); board_widget_.signal_state_changed.connect(sigc::mem_fun(*this, &MainWindow::on_game_state_changed)); } void MainWindow::start_new_game(const GameDifficulty& difficulty) { current_difficulty_ = difficulty; // Reflect the active difficulty in the menu button and popover btn_difficulty_.set_label(difficulty.name); for (const auto& [diff, btn] : difficulty_buttons_) { btn->set_active(diff.name == difficulty.name); } // Cancel any pending game-over dialog from the finished game dialog_timeout_conn_.disconnect(); minefield_ = std::make_shared(difficulty.cols, difficulty.rows, difficulty.mines); board_widget_.set_minefield(minefield_); // Size the window to fit the board at a comfortable cell size. // Ignored once the user has resized the window (or under a tiling WM). int w = difficulty.cols * 36 + 160; int h = difficulty.rows * 36 + 200; // Extra height for the header bar set_default_size(std::clamp(w, 480, 1280), std::clamp(h, 420, 1000)); // Reset timer timer_conn_.disconnect(); timer_conn_ = Glib::signal_timeout().connect(sigc::mem_fun(*this, &MainWindow::on_timer_tick), 100); on_game_state_changed(); } void MainWindow::on_game_state_changed() { if (!minefield_) return; // Update Flags lbl_flags_.set_markup("Flags: " + std::to_string(minefield_->remaining_flags()) + ""); // Check Game Over GameState state = minefield_->state(); bool new_best = false; int secs = 0; if (state == GameState::Won || state == GameState::Lost) { timer_conn_.disconnect(); if (state == GameState::Won) { secs = std::chrono::duration_cast(minefield_->get_elapsed_time()).count(); new_best = leaderboard_.record(current_difficulty_.name, secs); } show_game_over_dialog(state == GameState::Won, new_best, secs); } // Update timer immediately to reflect end time if stopped on_timer_tick(); } bool MainWindow::on_timer_tick() { if (!minefield_) return true; auto elapsed = minefield_->get_elapsed_time(); auto secs = std::chrono::duration_cast(elapsed).count(); lbl_time_.set_markup("Time: " + format_time(secs) + ""); return true; // Keep calling } void MainWindow::show_game_over_dialog(bool won, bool new_best, int seconds) { // Delay so the win animation/board state is visible before the modal // dialog covers it. if (dialog_timeout_conn_.connected()) return; dialog_timeout_conn_ = Glib::signal_timeout().connect([this, won, new_best, seconds]() { dialog_timeout_conn_.disconnect(); // A new game may have started while the dialog was pending if (!minefield_ || minefield_->state() == GameState::Ready) return false; // Reuse a single dialog instance: creating a new make_managed dialog // per game over leaks (it's never added to a container, so it's never // freed). if (!game_over_dialog_) { game_over_dialog_ = Gtk::make_managed( *this, "", false, Gtk::MessageType::INFO, Gtk::ButtonsType::OK, true ); game_over_dialog_->signal_response().connect([this](int response) { game_over_dialog_->hide(); if (response == Gtk::ResponseType::OK) { start_new_game(current_difficulty_); } }); } game_over_dialog_->set_title(won ? "Congratulations!" : "Game Over"); game_over_dialog_->set_message(won ? "Congratulations!" : "Game Over"); std::string secondary = won ? "You cleared the minefield in " + format_time(seconds) + (new_best ? " — new best time!" : ".") : "Better luck next time."; game_over_dialog_->set_secondary_text(secondary); game_over_dialog_->show(); return false; // One-shot }, 1500); }