feat(engine): click-outside-to-dismiss for read-only modals
Adds a ScrimDismissible marker to ui_modal that opts a modal into the standard "click outside the card to close" gesture. The new dismiss_modal_on_scrim_click system fires on a left-mouse press whose cursor falls on the scrim and outside every ModalCard, then despawns the topmost dismissible scrim — Bevy's hierarchy despawn cascades to the card and its children. Marker design is opt-in per modal so destructive / state-mutating modals (Settings saves on close, Onboarding requires explicit acknowledgement, Pause / Forfeit / ConfirmNewGame need confirmed intent) don't lose work to an accidental scrim click. Three read-only modals opt in this round: - Stats — informational; press S or click outside to dismiss. - Achievements — read-only list. - Help — keyboard reference. Profile, Leaderboard, and Home will opt in the same way in a follow-up; they were left out to keep this commit's scope tight. The hit-test path uses each ModalCard's UiGlobalTransform + ComputedNode bounding box so stacked modals close cleanly: the topmost dismissible scrim is the only candidate per click. Tests spawn synthetic ComputedNodes (with bevy::sprite::BorderRect for the resolved-border slots Bevy's UI module re-exports) so the geometry hit-tests deterministically without running the full UI layout pipeline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,7 @@ use crate::settings_plugin::{SettingsResource, SettingsStoragePath};
|
||||
use crate::stats_plugin::{StatsResource, StatsUpdate};
|
||||
use crate::ui_modal::{
|
||||
spawn_modal, spawn_modal_actions, spawn_modal_button, spawn_modal_header, ButtonVariant,
|
||||
ScrimDismissible,
|
||||
};
|
||||
use crate::ui_theme::{
|
||||
ACCENT_PRIMARY, BORDER_SUBTLE, STATE_SUCCESS, TEXT_DISABLED, TEXT_PRIMARY, TEXT_SECONDARY,
|
||||
@@ -473,7 +474,7 @@ fn spawn_achievements_screen(
|
||||
..default()
|
||||
};
|
||||
|
||||
spawn_modal(commands, AchievementsScreen, Z_MODAL_PANEL, |card| {
|
||||
let scrim = spawn_modal(commands, AchievementsScreen, Z_MODAL_PANEL, |card| {
|
||||
spawn_modal_header(card, header, font_res);
|
||||
|
||||
// Scrollable body — the achievements list grows to ~19 rows which
|
||||
@@ -577,6 +578,9 @@ fn spawn_achievements_screen(
|
||||
);
|
||||
});
|
||||
});
|
||||
// Achievements is a read-only list — clicking the scrim outside
|
||||
// the card dismisses alongside the existing A / Done paths.
|
||||
commands.entity(scrim).insert(ScrimDismissible);
|
||||
}
|
||||
|
||||
fn format_reward(reward: Reward) -> String {
|
||||
|
||||
Reference in New Issue
Block a user