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.
This commit is contained in:
@@ -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/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/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<Minefield>` 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<Minefield>` 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&)`).
|
- `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<R(T...)>::connect()` (non-void return signals like `signal_key_pressed`) requires the `after` bool explicitly — `connect(slot, false)`. Void signals default it to true.
|
- glibmm 2.80 quirk: `SignalProxy<R(T...)>::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<MainWindow>`.
|
- `src/main.cpp` — single `Gtk::Application` (`io.github.bemagri.nomines`); `make_window_and_run<MainWindow>`.
|
||||||
|
|||||||
+48
-31
@@ -21,14 +21,27 @@ MainWindow::MainWindow() {
|
|||||||
setup_header_bar();
|
setup_header_bar();
|
||||||
setup_board();
|
setup_board();
|
||||||
|
|
||||||
// Popovers: some GTK themes (and newer GTK versions) leave the popover
|
// App-wide CSS: opaque popovers (some themes/GTK versions leave them
|
||||||
// surface transparent unless the content opts in. Guarantee an opaque,
|
// transparent), plus the status bar above the grid (flags/timer panels).
|
||||||
// theme-aware background.
|
auto app_css = Gtk::CssProvider::create();
|
||||||
auto popover_css = Gtk::CssProvider::create();
|
app_css->load_from_data(R"CSS(
|
||||||
popover_css->load_from_data(
|
popover.background > contents { background-color: @theme_bg_color; }
|
||||||
"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(
|
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
|
// Keyboard shortcuts
|
||||||
auto key_controller = Gtk::EventControllerKey::create();
|
auto key_controller = Gtk::EventControllerKey::create();
|
||||||
@@ -81,13 +94,6 @@ MainWindow::~MainWindow() {
|
|||||||
void MainWindow::setup_header_bar() {
|
void MainWindow::setup_header_bar() {
|
||||||
set_titlebar(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
|
// Difficulty Menu
|
||||||
btn_difficulty_.set_label(Minefield::DifficultyEasy.name);
|
btn_difficulty_.set_label(Minefield::DifficultyEasy.name);
|
||||||
|
|
||||||
@@ -138,17 +144,6 @@ void MainWindow::setup_header_bar() {
|
|||||||
menu_leaderboard_.add_css_class("menu");
|
menu_leaderboard_.add_css_class("menu");
|
||||||
menu_leaderboard_.signal_show().connect(sigc::mem_fun(*this, &MainWindow::refresh_leaderboard));
|
menu_leaderboard_.signal_show().connect(sigc::mem_fun(*this, &MainWindow::refresh_leaderboard));
|
||||||
header_bar_.pack_start(btn_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("<b><tt>Flags: 0</tt></b>");
|
|
||||||
lbl_time_.set_markup("<b><tt>Time: 00:00</tt></b>");
|
|
||||||
|
|
||||||
header_bar_.pack_end(lbl_time_);
|
|
||||||
header_bar_.pack_end(lbl_flags_);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::refresh_leaderboard() {
|
void MainWindow::refresh_leaderboard() {
|
||||||
@@ -172,10 +167,27 @@ void MainWindow::refresh_leaderboard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::setup_board() {
|
void MainWindow::setup_board() {
|
||||||
// Minimum size so the largest board (30x20, Master) fits at the 16px
|
vbox_.set_orientation(Gtk::Orientation::VERTICAL);
|
||||||
// cell floor even when tiled very small.
|
|
||||||
board_widget_.set_size_request(520, 360);
|
// Status bar above the grid: flags (left), new game (center), timer (right)
|
||||||
set_child(board_widget_);
|
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));
|
board_widget_.signal_state_changed.connect(sigc::mem_fun(*this, &MainWindow::on_game_state_changed));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,6 +206,11 @@ void MainWindow::start_new_game(const GameDifficulty& difficulty) {
|
|||||||
minefield_ = std::make_shared<Minefield>(difficulty.cols, difficulty.rows, difficulty.mines);
|
minefield_ = std::make_shared<Minefield>(difficulty.cols, difficulty.rows, difficulty.mines);
|
||||||
board_widget_.set_minefield(minefield_);
|
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.
|
// 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).
|
// Ignored once the user has resized the window (or under a tiling WM).
|
||||||
int w = difficulty.cols * 36 + 160;
|
int w = difficulty.cols * 36 + 160;
|
||||||
@@ -211,7 +228,7 @@ void MainWindow::on_game_state_changed() {
|
|||||||
if (!minefield_) return;
|
if (!minefield_) return;
|
||||||
|
|
||||||
// Update Flags
|
// Update Flags
|
||||||
lbl_flags_.set_markup("<b><tt>Flags: " + std::to_string(minefield_->remaining_flags()) + "</tt></b>");
|
lbl_flags_.set_text("Flags: " + std::to_string(minefield_->remaining_flags()));
|
||||||
|
|
||||||
// Check Game Over
|
// Check Game Over
|
||||||
GameState state = minefield_->state();
|
GameState state = minefield_->state();
|
||||||
@@ -236,7 +253,7 @@ bool MainWindow::on_timer_tick() {
|
|||||||
auto elapsed = minefield_->get_elapsed_time();
|
auto elapsed = minefield_->get_elapsed_time();
|
||||||
auto secs = std::chrono::duration_cast<std::chrono::seconds>(elapsed).count();
|
auto secs = std::chrono::duration_cast<std::chrono::seconds>(elapsed).count();
|
||||||
|
|
||||||
lbl_time_.set_markup("<b><tt>Time: " + format_time(secs) + "</tt></b>");
|
lbl_time_.set_text("Time: " + format_time(secs));
|
||||||
|
|
||||||
return true; // Keep calling
|
return true; // Keep calling
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -27,7 +27,6 @@ private:
|
|||||||
|
|
||||||
// Widgets
|
// Widgets
|
||||||
Gtk::HeaderBar header_bar_;
|
Gtk::HeaderBar header_bar_;
|
||||||
Gtk::Button btn_new_game_;
|
|
||||||
Gtk::MenuButton btn_difficulty_;
|
Gtk::MenuButton btn_difficulty_;
|
||||||
Gtk::Popover menu_difficulty_;
|
Gtk::Popover menu_difficulty_;
|
||||||
Gtk::Box box_difficulty_; // Content for popover
|
Gtk::Box box_difficulty_; // Content for popover
|
||||||
@@ -37,6 +36,9 @@ private:
|
|||||||
Gtk::Popover menu_leaderboard_;
|
Gtk::Popover menu_leaderboard_;
|
||||||
Gtk::Box box_leaderboard_; // Content for popover
|
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_time_;
|
||||||
Gtk::Label lbl_flags_;
|
Gtk::Label lbl_flags_;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user