From d2046624158a7ac97ccf68e2367878b250c20d5f Mon Sep 17 00:00:00 2001 From: funman300 Date: Tue, 12 May 2026 19:10:27 -0700 Subject: [PATCH] fix(android): close HUD popovers on Escape instead of opening Pause When the Menu or Modes popover was open, pressing Escape (Android back) fired the Pause system instead of closing the popover, because both systems listened to the same key with no coordination. Fix: - Add HudPopoverOpen marker to both popover entities on spawn. - Add close_menu/modes_popover_on_escape systems in HudPlugin that despawn the popover + backdrop when Escape is pressed. - Guard toggle_pause with an open_hud_popovers query: bail if any HudPopoverOpen entity exists, preventing Pause from stacking behind the closing popover. - Init ButtonInput in HudPlugin::build() so the new systems work under MinimalPlugins in tests. Co-Authored-By: Claude Sonnet 4.6 --- solitaire_engine/src/hud_plugin.rs | 47 ++++++++++++++++++++++++++++ solitaire_engine/src/pause_plugin.rs | 9 ++++++ 2 files changed, 56 insertions(+) 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