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<KeyCode> in HudPlugin::build() so the new systems
  work under MinimalPlugins in tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-05-12 19:10:27 -07:00
parent 4f0080dfbc
commit d204662415
2 changed files with 56 additions and 0 deletions
+47
View File
@@ -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::<WinStreakMilestoneEvent>()
.init_resource::<PreviousScore>()
.init_resource::<HudActionFade>()
// Escape-close handlers for popovers read this; init defensively
// so HudPlugin works under MinimalPlugins in tests.
.init_resource::<ButtonInput<KeyCode>>()
// WindowResized is registered by table_plugin; re-register
// defensively so the HUD plugin works standalone in tests.
.add_message::<WindowResized>()
@@ -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<ButtonInput<KeyCode>>,
popovers: Query<Entity, With<ModesPopover>>,
backdrops: Query<Entity, With<ModesPopoverBackdrop>>,
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<ButtonInput<KeyCode>>,
popovers: Query<Entity, With<MenuPopover>>,
backdrops: Query<Entity, With<MenuPopoverBackdrop>>,
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(
+9
View File
@@ -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<ForfeitConfirmScreen>>,
game_over_screens: Query<'w, 's, Entity, With<GameOverScreen>>,
other_modal_scrims: Query<'w, 's, Entity, (With<ModalScrim>, Without<PauseScreen>)>,
open_hud_popovers: Query<'w, 's, Entity, With<HudPopoverOpen>>,
}
#[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