diff --git a/solitaire_engine/src/hud_plugin.rs b/solitaire_engine/src/hud_plugin.rs index cf81912..808cf18 100644 --- a/solitaire_engine/src/hud_plugin.rs +++ b/solitaire_engine/src/hud_plugin.rs @@ -281,6 +281,13 @@ pub struct MenuButton; #[derive(Component, Debug)] pub struct MenuPopover; +/// Shared marker placed on both [`MenuPopover`] and [`ModesPopover`] entities +/// while they are open. External systems (e.g. `PausePlugin`) query this to +/// determine whether a HUD popover is currently visible without importing the +/// individual popover types. +#[derive(Component, Debug)] +pub struct HudPopoverOpen; + /// Fullscreen transparent backdrop spawned behind the [`MenuPopover`]. /// Pressing it (tap anywhere outside the popover) light-dismisses the menu. #[derive(Component, Debug)] @@ -340,6 +347,9 @@ impl Plugin for HudPlugin { .add_message::() .init_resource::() .init_resource::() + // Escape-close handlers for popovers read this; init defensively + // so HudPlugin works under MinimalPlugins in tests. + .init_resource::>() // WindowResized is registered by table_plugin; re-register // defensively so the HUD plugin works standalone in tests. .add_message::() @@ -376,9 +386,11 @@ impl Plugin for HudPlugin { handle_modes_button, handle_mode_option_click, handle_modes_backdrop_click, + close_modes_popover_on_escape, handle_menu_button, handle_menu_option_click, handle_menu_backdrop_click, + close_menu_popover_on_escape, paint_action_buttons, ), ) @@ -940,6 +952,7 @@ fn spawn_modes_popover( commands .spawn(( ModesPopover, + HudPopoverOpen, Node { position_type: PositionType::Absolute, right: VAL_SPACE_3, @@ -1120,6 +1133,7 @@ fn spawn_menu_popover(commands: &mut Commands, font_res: Option<&FontResource>) commands .spawn(( MenuPopover, + HudPopoverOpen, Node { position_type: PositionType::Absolute, right: VAL_SPACE_3, @@ -1223,6 +1237,39 @@ fn handle_menu_option_click( } } +/// Despawns the [`ModesPopover`] and its backdrop when Escape / Android back +/// is pressed while the popover is open. Runs so `PausePlugin`'s guard (which +/// checks [`HudPopoverOpen`]) sees an empty world and stays idle. +fn close_modes_popover_on_escape( + keys: Res>, + popovers: Query>, + backdrops: Query>, + mut commands: Commands, +) { + if !keys.just_pressed(KeyCode::Escape) || popovers.is_empty() { + return; + } + for e in popovers.iter().chain(backdrops.iter()) { + commands.entity(e).despawn(); + } +} + +/// Despawns the [`MenuPopover`] and its backdrop when Escape / Android back +/// is pressed while the popover is open. +fn close_menu_popover_on_escape( + keys: Res>, + popovers: Query>, + backdrops: Query>, + mut commands: Commands, +) { + if !keys.just_pressed(KeyCode::Escape) || popovers.is_empty() { + return; + } + for e in popovers.iter().chain(backdrops.iter()) { + commands.entity(e).despawn(); + } +} + /// Despawns the [`ModesPopover`] and its backdrop when the player taps /// anywhere outside the panel. fn handle_modes_backdrop_click( diff --git a/solitaire_engine/src/pause_plugin.rs b/solitaire_engine/src/pause_plugin.rs index f84f373..14b5d2c 100644 --- a/solitaire_engine/src/pause_plugin.rs +++ b/solitaire_engine/src/pause_plugin.rs @@ -35,6 +35,7 @@ use crate::resources::{DragState, GameStateResource}; use crate::selection_plugin::{SelectionKeySet, SelectionState}; use crate::settings_plugin::{SettingsChangedEvent, SettingsResource, SettingsStoragePath}; use crate::stats_plugin::StatsResource; +use crate::hud_plugin::HudPopoverOpen; use crate::ui_modal::{ spawn_modal, spawn_modal_actions, spawn_modal_body_text, spawn_modal_button, spawn_modal_header, ButtonVariant, ModalScrim, @@ -137,6 +138,7 @@ struct PauseModalQueries<'w, 's> { forfeit_screens: Query<'w, 's, Entity, With>, game_over_screens: Query<'w, 's, Entity, With>, other_modal_scrims: Query<'w, 's, Entity, (With, Without)>, + open_hud_popovers: Query<'w, 's, Entity, With>, } #[allow(clippy::too_many_arguments)] @@ -162,6 +164,7 @@ fn toggle_pause( forfeit_screens, game_over_screens, other_modal_scrims, + open_hud_popovers, } = modal_queries; // Either Esc or a click on the HUD "Pause" button (which fires @@ -186,6 +189,12 @@ fn toggle_pause( if !other_modal_scrims.is_empty() { return; } + // A HUD popover (Menu or Modes dropdown) is open — the popover's own + // Escape handler (in HudPlugin) will close it this frame. Don't also + // spawn the pause overlay on top of the closing popover. + if !open_hud_popovers.is_empty() { + return; + } // If a replay is currently playing, let `replay_overlay::handle_stop_keyboard` // own the Esc press — that handler stops the replay. Without this guard a // single Esc both stops the replay AND opens the pause modal on top of the