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:
@@ -281,6 +281,13 @@ pub struct MenuButton;
|
|||||||
#[derive(Component, Debug)]
|
#[derive(Component, Debug)]
|
||||||
pub struct MenuPopover;
|
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`].
|
/// Fullscreen transparent backdrop spawned behind the [`MenuPopover`].
|
||||||
/// Pressing it (tap anywhere outside the popover) light-dismisses the menu.
|
/// Pressing it (tap anywhere outside the popover) light-dismisses the menu.
|
||||||
#[derive(Component, Debug)]
|
#[derive(Component, Debug)]
|
||||||
@@ -340,6 +347,9 @@ impl Plugin for HudPlugin {
|
|||||||
.add_message::<WinStreakMilestoneEvent>()
|
.add_message::<WinStreakMilestoneEvent>()
|
||||||
.init_resource::<PreviousScore>()
|
.init_resource::<PreviousScore>()
|
||||||
.init_resource::<HudActionFade>()
|
.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
|
// WindowResized is registered by table_plugin; re-register
|
||||||
// defensively so the HUD plugin works standalone in tests.
|
// defensively so the HUD plugin works standalone in tests.
|
||||||
.add_message::<WindowResized>()
|
.add_message::<WindowResized>()
|
||||||
@@ -376,9 +386,11 @@ impl Plugin for HudPlugin {
|
|||||||
handle_modes_button,
|
handle_modes_button,
|
||||||
handle_mode_option_click,
|
handle_mode_option_click,
|
||||||
handle_modes_backdrop_click,
|
handle_modes_backdrop_click,
|
||||||
|
close_modes_popover_on_escape,
|
||||||
handle_menu_button,
|
handle_menu_button,
|
||||||
handle_menu_option_click,
|
handle_menu_option_click,
|
||||||
handle_menu_backdrop_click,
|
handle_menu_backdrop_click,
|
||||||
|
close_menu_popover_on_escape,
|
||||||
paint_action_buttons,
|
paint_action_buttons,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -940,6 +952,7 @@ fn spawn_modes_popover(
|
|||||||
commands
|
commands
|
||||||
.spawn((
|
.spawn((
|
||||||
ModesPopover,
|
ModesPopover,
|
||||||
|
HudPopoverOpen,
|
||||||
Node {
|
Node {
|
||||||
position_type: PositionType::Absolute,
|
position_type: PositionType::Absolute,
|
||||||
right: VAL_SPACE_3,
|
right: VAL_SPACE_3,
|
||||||
@@ -1120,6 +1133,7 @@ fn spawn_menu_popover(commands: &mut Commands, font_res: Option<&FontResource>)
|
|||||||
commands
|
commands
|
||||||
.spawn((
|
.spawn((
|
||||||
MenuPopover,
|
MenuPopover,
|
||||||
|
HudPopoverOpen,
|
||||||
Node {
|
Node {
|
||||||
position_type: PositionType::Absolute,
|
position_type: PositionType::Absolute,
|
||||||
right: VAL_SPACE_3,
|
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
|
/// Despawns the [`ModesPopover`] and its backdrop when the player taps
|
||||||
/// anywhere outside the panel.
|
/// anywhere outside the panel.
|
||||||
fn handle_modes_backdrop_click(
|
fn handle_modes_backdrop_click(
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ use crate::resources::{DragState, GameStateResource};
|
|||||||
use crate::selection_plugin::{SelectionKeySet, SelectionState};
|
use crate::selection_plugin::{SelectionKeySet, SelectionState};
|
||||||
use crate::settings_plugin::{SettingsChangedEvent, SettingsResource, SettingsStoragePath};
|
use crate::settings_plugin::{SettingsChangedEvent, SettingsResource, SettingsStoragePath};
|
||||||
use crate::stats_plugin::StatsResource;
|
use crate::stats_plugin::StatsResource;
|
||||||
|
use crate::hud_plugin::HudPopoverOpen;
|
||||||
use crate::ui_modal::{
|
use crate::ui_modal::{
|
||||||
spawn_modal, spawn_modal_actions, spawn_modal_body_text, spawn_modal_button,
|
spawn_modal, spawn_modal_actions, spawn_modal_body_text, spawn_modal_button,
|
||||||
spawn_modal_header, ButtonVariant, ModalScrim,
|
spawn_modal_header, ButtonVariant, ModalScrim,
|
||||||
@@ -137,6 +138,7 @@ struct PauseModalQueries<'w, 's> {
|
|||||||
forfeit_screens: Query<'w, 's, Entity, With<ForfeitConfirmScreen>>,
|
forfeit_screens: Query<'w, 's, Entity, With<ForfeitConfirmScreen>>,
|
||||||
game_over_screens: Query<'w, 's, Entity, With<GameOverScreen>>,
|
game_over_screens: Query<'w, 's, Entity, With<GameOverScreen>>,
|
||||||
other_modal_scrims: Query<'w, 's, Entity, (With<ModalScrim>, Without<PauseScreen>)>,
|
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)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
@@ -162,6 +164,7 @@ fn toggle_pause(
|
|||||||
forfeit_screens,
|
forfeit_screens,
|
||||||
game_over_screens,
|
game_over_screens,
|
||||||
other_modal_scrims,
|
other_modal_scrims,
|
||||||
|
open_hud_popovers,
|
||||||
} = modal_queries;
|
} = modal_queries;
|
||||||
|
|
||||||
// Either Esc or a click on the HUD "Pause" button (which fires
|
// 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() {
|
if !other_modal_scrims.is_empty() {
|
||||||
return;
|
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`
|
// 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
|
// 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
|
// single Esc both stops the replay AND opens the pause modal on top of the
|
||||||
|
|||||||
Reference in New Issue
Block a user