From cee916224a4b66e3edecf34e6370069b3462623b Mon Sep 17 00:00:00 2001 From: Bernardo Magri Date: Sat, 1 Aug 2026 16:40:55 +0100 Subject: [PATCH] Replace loss dialog with KABOOM! overlay; bevel the New Game button - Losing no longer pops a dialog: the board draws a tilted KABOOM! banner (theme-independent, auto-scaling, dark translucent panel) over the revealed mines; restart via the New Game button or R/Ctrl+N - Win dialog unchanged (time + new-record note, delayed for the confetti) - New Game button gets an explicit theme-aware bevel (gradient, border, hover/active) so it reads as a button even with themes that flatten widgets --- src/board_widget.cpp | 52 ++++++++++++++++++++++++++++++++++++++++++++ src/board_widget.hpp | 1 + src/window.cpp | 34 ++++++++++++++++++++--------- src/window.hpp | 2 +- 4 files changed, 78 insertions(+), 11 deletions(-) diff --git a/src/board_widget.cpp b/src/board_widget.cpp index f18ccd5..b6e17f7 100644 --- a/src/board_widget.cpp +++ b/src/board_widget.cpp @@ -355,6 +355,58 @@ void BoardWidget::on_draw(const Cairo::RefPtr& cr, int width, in if (!particles_.empty()) { draw_particles(cr); } + + if (field_ && field_->state() == GameState::Lost) { + draw_game_over_overlay(cr, width, height); + } +} + +void BoardWidget::draw_game_over_overlay(const Cairo::RefPtr& cr, int width, int height) { + const char* msg = "KABOOM!"; + + cr->save(); + + // Scale the font to fit both dimensions + double font_size = std::clamp(std::min(width, height) * 0.3, 24.0, 120.0); + cr->select_font_face("Sans", Cairo::ToyFontFace::Slant::NORMAL, Cairo::ToyFontFace::Weight::BOLD); + cr->set_font_size(font_size); + Cairo::TextExtents ext; + cr->get_text_extents(msg, ext); + if (ext.width > width * 0.8) { + font_size *= (width * 0.8) / ext.width; + cr->set_font_size(font_size); + cr->get_text_extents(msg, ext); + } + + double cx = (width - ext.width) / 2.0 - ext.x_bearing; + double cy = (height - ext.height) / 2.0 - ext.y_bearing; + + // Slight tilt for drama + cr->translate(width / 2.0, height / 2.0); + cr->rotate(-0.06); + cr->translate(-width / 2.0, -height / 2.0); + + // Translucent panel for readability over the revealed board + double pad = font_size * 0.35; + double px = cx - pad; + double py = cy - pad; + double pw = ext.width + 2 * pad; + double ph = ext.height + 2 * pad; + double radius = font_size * 0.12; + cr->set_source_rgba(0.0, 0.0, 0.0, 0.5); + cr->move_to(px + radius, py); + cr->arc(px + pw - radius, py + radius, radius, -M_PI / 2, 0); + cr->arc(px + pw - radius, py + ph - radius, radius, 0, M_PI / 2); + cr->arc(px + radius, py + ph - radius, radius, M_PI / 2, M_PI); + cr->arc(px + radius, py + radius, radius, M_PI, 3 * M_PI / 2); + cr->close_path(); + cr->fill(); + + cr->set_source_rgb(1.0, 0.42, 0.08); // Explosive orange + cr->move_to(cx, cy); + cr->show_text(msg); + + cr->restore(); } void BoardWidget::draw_cell(const Cairo::RefPtr& cr, int x, int y, double size, const Cell& cell, const Palette& pal) { diff --git a/src/board_widget.hpp b/src/board_widget.hpp index 0649a8f..e3ca458 100644 --- a/src/board_widget.hpp +++ b/src/board_widget.hpp @@ -69,6 +69,7 @@ private: void draw_flag(const Cairo::RefPtr& cr, double x, double y, double size, const Palette& pal); void draw_bomb(const Cairo::RefPtr& cr, double x, double y, double size, bool exploded, const Palette& pal); void draw_particles(const Cairo::RefPtr& cr); + void draw_game_over_overlay(const Cairo::RefPtr& cr, int width, int height); std::shared_ptr field_; diff --git a/src/window.cpp b/src/window.cpp index 59790cb..278ba6c 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -39,6 +39,20 @@ popover.background > contents { background-color: @theme_bg_color; } font-family: monospace; font-weight: bold; } +.status-bar button { + background-image: linear-gradient(to top, alpha(@theme_fg_color, 0.16), alpha(@theme_fg_color, 0.05)); + border: 1px solid alpha(@theme_fg_color, 0.3); + border-radius: 8px; + padding: 4px 18px; + color: @theme_fg_color; +} +.status-bar button:hover { + background-image: linear-gradient(to top, alpha(@theme_fg_color, 0.24), alpha(@theme_fg_color, 0.12)); +} +.status-bar button:active { + background-image: none; + background-color: alpha(@theme_fg_color, 0.2); +} )CSS"); Gtk::StyleContext::add_provider_for_display( get_display(), app_css, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); @@ -239,8 +253,10 @@ void MainWindow::on_game_state_changed() { 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(new_best, secs); } - show_game_over_dialog(state == GameState::Won, new_best, secs); + // Lost: no dialog — the board draws a KABOOM! overlay and the + // player restarts via the New Game button. } // Update timer immediately to reflect end time if stopped @@ -258,15 +274,15 @@ bool MainWindow::on_timer_tick() { return true; // Keep calling } -void MainWindow::show_game_over_dialog(bool won, bool new_best, int seconds) { +void MainWindow::show_game_over_dialog(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_ = Glib::signal_timeout().connect([this, 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; + if (!minefield_ || minefield_->state() != GameState::Won) 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 @@ -286,12 +302,10 @@ void MainWindow::show_game_over_dialog(bool won, bool new_best, int seconds) { }); } - 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_title("Congratulations!"); + game_over_dialog_->set_message("Congratulations!"); + std::string secondary = "You cleared the minefield in " + format_time(seconds) + + (new_best ? " — new best time!" : "."); game_over_dialog_->set_secondary_text(secondary); game_over_dialog_->show(); return false; // One-shot diff --git a/src/window.hpp b/src/window.hpp index 35aca3d..6b88221 100644 --- a/src/window.hpp +++ b/src/window.hpp @@ -23,7 +23,7 @@ private: void start_new_game(const GameDifficulty& difficulty); void on_game_state_changed(); bool on_timer_tick(); - void show_game_over_dialog(bool won, bool new_best, int seconds); + void show_game_over_dialog(bool new_best, int seconds); // Widgets Gtk::HeaderBar header_bar_;