From 9a9d9f3fea2bdf82e9609e461ef9c35f85b72efb Mon Sep 17 00:00:00 2001 From: Bernardo Magri Date: Sat, 1 Aug 2026 16:30:46 +0100 Subject: [PATCH] Move flags/timer out of the header bar into a status bar above the grid Header bar now holds only Difficulty + Leaderboard. A status bar above the board shows the flag counter (left), the New Game button (center) and the timer (right) as LCD-style panels styled with theme-aware CSS. --- AGENTS.md | 2 +- src/window.cpp | 79 ++++++++++++++++++++++++++++++-------------------- src/window.hpp | 4 ++- 3 files changed, 52 insertions(+), 33 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 0b0b4a9..fd91bd6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,7 +20,7 @@ meson compile -C build - `src/minefield.*` — pure game logic, no GTK: `Minefield` (board state, flag counting, chording), `Cell` bitfield, `GameDifficulty` presets, `GameState` enum. - `src/board_widget.*` — `Gtk::DrawingArea`; all rendering is hand-drawn Cairo primitives (draw_cell/draw_digit/draw_flag/draw_bomb), input via `Gtk::GestureClick` + `EventControllerMotion`, confetti particle animation on win (frame-rate independent via `Gdk::FrameClock`). -- `src/window.*` — `Gtk::ApplicationWindow` with HeaderBar, difficulty popover (radio-style `CheckButton`s), leaderboard popover, timer; owns the `shared_ptr` and swaps it on new game. Keyboard shortcuts via `Gtk::EventControllerKey` (R/Ctrl+N new game, 1-4 difficulties, L leaderboard). +- `src/window.*` — `Gtk::ApplicationWindow` with HeaderBar (difficulty + leaderboard popovers, radio-style `CheckButton`s) and a status bar above the grid (LCD-style flags/timer panels via the `status-panel` CSS class, New Game button centered); owns the `shared_ptr` and swaps it on new game. Keyboard shortcuts via `Gtk::EventControllerKey` (R/Ctrl+N new game, 1-4 difficulties, L leaderboard). App-wide CSS (popover backgrounds, status bar) lives in a `Gtk::CssProvider` in the constructor. - `src/leaderboard.*` — persistent best times per difficulty via `Glib::KeyFile` at `~/.config/nomines/leaderboard.ini` (recorded on win, shown in a header-bar popover; falls back to the pre-1.0 `~/.config/minesweeper/` path for migration). NOTE: uses the glibmm 2.80 refcounted `KeyFile::create()` API and gtkmm 4.14 `CheckButton` (a `Widget`+`Actionable`, not a `Button` — use `signal_toggled()` + `get_active()`, and `set_group(CheckButton&)`). - glibmm 2.80 quirk: `SignalProxy::connect()` (non-void return signals like `signal_key_pressed`) requires the `after` bool explicitly — `connect(slot, false)`. Void signals default it to true. - `src/main.cpp` — single `Gtk::Application` (`io.github.bemagri.nomines`); `make_window_and_run`. diff --git a/src/window.cpp b/src/window.cpp index 7a1e2b7..59790cb 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -21,14 +21,27 @@ MainWindow::MainWindow() { 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; }"); + // App-wide CSS: opaque popovers (some themes/GTK versions leave them + // transparent), plus the status bar above the grid (flags/timer panels). + auto app_css = Gtk::CssProvider::create(); + app_css->load_from_data(R"CSS( +popover.background > contents { background-color: @theme_bg_color; } + +.status-bar { + padding: 8px 12px; + border-bottom: 1px solid alpha(@theme_fg_color, 0.12); +} +.status-panel { + background-color: alpha(@theme_fg_color, 0.85); + border-radius: 8px; + padding: 4px 14px; + color: @theme_bg_color; + font-family: monospace; + font-weight: bold; +} +)CSS"); Gtk::StyleContext::add_provider_for_display( - get_display(), popover_css, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + get_display(), app_css, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); // Keyboard shortcuts auto key_controller = Gtk::EventControllerKey::create(); @@ -81,13 +94,6 @@ MainWindow::~MainWindow() { 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); @@ -138,17 +144,6 @@ void MainWindow::setup_header_bar() { 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() { @@ -172,10 +167,27 @@ void MainWindow::refresh_leaderboard() { } 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_); + vbox_.set_orientation(Gtk::Orientation::VERTICAL); + + // Status bar above the grid: flags (left), new game (center), timer (right) + btn_new_game_.set_label("New Game"); + btn_new_game_.signal_clicked().connect([this]() { + start_new_game(current_difficulty_); + }); + lbl_flags_.set_text("Flags: 0"); + lbl_time_.set_text("Time: 00:00"); + lbl_flags_.add_css_class("status-panel"); + lbl_time_.add_css_class("status-panel"); + + status_bar_.add_css_class("status-bar"); + status_bar_.set_start_widget(lbl_flags_); + status_bar_.set_center_widget(btn_new_game_); + status_bar_.set_end_widget(lbl_time_); + + vbox_.append(status_bar_); + vbox_.append(board_widget_); + set_child(vbox_); + board_widget_.signal_state_changed.connect(sigc::mem_fun(*this, &MainWindow::on_game_state_changed)); } @@ -193,6 +205,11 @@ void MainWindow::start_new_game(const GameDifficulty& difficulty) { minefield_ = std::make_shared(difficulty.cols, difficulty.rows, difficulty.mines); board_widget_.set_minefield(minefield_); + + // Minimum size: let the window shrink down to what the current board + // needs at the 16px cell floor (30x20 Master requires 520x360, but a + // 9x9 Beginner board fits in ~184px). + board_widget_.set_size_request(difficulty.cols * 16 + 40, difficulty.rows * 16 + 40); // 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). @@ -211,7 +228,7 @@ void MainWindow::on_game_state_changed() { if (!minefield_) return; // Update Flags - lbl_flags_.set_markup("Flags: " + std::to_string(minefield_->remaining_flags()) + ""); + lbl_flags_.set_text("Flags: " + std::to_string(minefield_->remaining_flags())); // Check Game Over GameState state = minefield_->state(); @@ -236,7 +253,7 @@ bool MainWindow::on_timer_tick() { auto elapsed = minefield_->get_elapsed_time(); auto secs = std::chrono::duration_cast(elapsed).count(); - lbl_time_.set_markup("Time: " + format_time(secs) + ""); + lbl_time_.set_text("Time: " + format_time(secs)); return true; // Keep calling } diff --git a/src/window.hpp b/src/window.hpp index f38d0af..35aca3d 100644 --- a/src/window.hpp +++ b/src/window.hpp @@ -27,7 +27,6 @@ private: // Widgets Gtk::HeaderBar header_bar_; - Gtk::Button btn_new_game_; Gtk::MenuButton btn_difficulty_; Gtk::Popover menu_difficulty_; Gtk::Box box_difficulty_; // Content for popover @@ -37,6 +36,9 @@ private: Gtk::Popover menu_leaderboard_; Gtk::Box box_leaderboard_; // Content for popover + Gtk::Box vbox_; // Main vertical container + Gtk::CenterBox status_bar_; // Flags | New Game | Time + Gtk::Button btn_new_game_; Gtk::Label lbl_time_; Gtk::Label lbl_flags_;