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
This commit is contained in:
2026-08-01 16:40:55 +01:00
parent 9a9d9f3fea
commit cee916224a
4 changed files with 78 additions and 11 deletions
+52
View File
@@ -355,6 +355,58 @@ void BoardWidget::on_draw(const Cairo::RefPtr<Cairo::Context>& 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<Cairo::Context>& 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<Cairo::Context>& cr, int x, int y, double size, const Cell& cell, const Palette& pal) {
+1
View File
@@ -69,6 +69,7 @@ private:
void draw_flag(const Cairo::RefPtr<Cairo::Context>& cr, double x, double y, double size, const Palette& pal);
void draw_bomb(const Cairo::RefPtr<Cairo::Context>& cr, double x, double y, double size, bool exploded, const Palette& pal);
void draw_particles(const Cairo::RefPtr<Cairo::Context>& cr);
void draw_game_over_overlay(const Cairo::RefPtr<Cairo::Context>& cr, int width, int height);
std::shared_ptr<Minefield> field_;
+24 -10
View File
@@ -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<std::chrono::seconds>(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
+1 -1
View File
@@ -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_;