refactor(engine): split hud_plugin runtime code into submodules
Continues #118 after settings_plugin (PR #127): the 2,725-line mod.rs becomes five focused submodules along existing system boundaries — mod.rs 553 markers, resources, popover enums, plugin build spawn.rs 552 band / columns / avatar / action-bar construction interaction.rs 616 button handlers, Modes/Menu popovers, tap gesture fx.rs 430 action fades, score pulses/floaters, streak flourish updates.rs 612 HUD text / typography / visibility updaters Moved items are pub(super) (fx re-exported pub for HudActionFade and format_time_limit consumers). Code moved verbatim; no behaviour change; all 44 hud tests pass unchanged. Refs #118 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,616 @@
|
||||
//! HUD interaction: action-button handlers, Modes/Menu popovers, and
|
||||
//! the chrome tap-to-toggle gesture.
|
||||
|
||||
use super::*;
|
||||
|
||||
|
||||
|
||||
/// `Changed<Interaction>` filter ensures we only react on the frame the
|
||||
/// interaction state transitions, avoiding repeat events while the button
|
||||
/// is held down. Each click handler fires the corresponding request event,
|
||||
/// which `pause_plugin` / `help_plugin` / `game_plugin` consume alongside
|
||||
/// their existing keyboard handlers.
|
||||
pub(super) fn handle_new_game_button(
|
||||
interaction_query: Query<&Interaction, (With<NewGameButton>, Changed<Interaction>)>,
|
||||
mut new_game: MessageWriter<NewGameRequestEvent>,
|
||||
) {
|
||||
for interaction in &interaction_query {
|
||||
if *interaction == Interaction::Pressed {
|
||||
new_game.write(NewGameRequestEvent::default());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn handle_undo_button(
|
||||
interaction_query: Query<&Interaction, (With<UndoButton>, Changed<Interaction>)>,
|
||||
mut undo: MessageWriter<UndoRequestEvent>,
|
||||
) {
|
||||
for interaction in &interaction_query {
|
||||
if *interaction == Interaction::Pressed {
|
||||
undo.write(UndoRequestEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn handle_pause_button(
|
||||
interaction_query: Query<&Interaction, (With<PauseButton>, Changed<Interaction>)>,
|
||||
mut pause: MessageWriter<PauseRequestEvent>,
|
||||
) {
|
||||
for interaction in &interaction_query {
|
||||
if *interaction == Interaction::Pressed {
|
||||
pause.write(PauseRequestEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn handle_help_button(
|
||||
interaction_query: Query<&Interaction, (With<HelpButton>, Changed<Interaction>)>,
|
||||
mut help: MessageWriter<HelpRequestEvent>,
|
||||
) {
|
||||
for interaction in &interaction_query {
|
||||
if *interaction == Interaction::Pressed {
|
||||
help.write(HelpRequestEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn handle_hint_button(
|
||||
interaction_query: Query<&Interaction, (With<HintButton>, Changed<Interaction>)>,
|
||||
paused: Option<Res<PausedResource>>,
|
||||
game: Option<Res<GameStateResource>>,
|
||||
solver_config: Option<Res<crate::input_plugin::HintSolverConfig>>,
|
||||
mut pending_hint: Option<ResMut<crate::pending_hint::PendingHintTask>>,
|
||||
mut info_toast: MessageWriter<InfoToastEvent>,
|
||||
) {
|
||||
for interaction in &interaction_query {
|
||||
if *interaction != Interaction::Pressed {
|
||||
continue;
|
||||
}
|
||||
if paused.as_ref().is_some_and(|p| p.0) {
|
||||
return;
|
||||
}
|
||||
let Some(ref g) = game else { return };
|
||||
if g.0.is_won() {
|
||||
info_toast.write(InfoToastEvent(HINT_WON_MSG.to_string()));
|
||||
return;
|
||||
}
|
||||
if let (Some(cfg), Some(hint)) = (solver_config.as_ref(), pending_hint.as_mut()) {
|
||||
hint.spawn(g.0.clone(), cfg.moves_budget, cfg.states_budget);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggles the [`ModesPopover`]: spawns it on first click, despawns it on
|
||||
/// second click. Mode rows are populated per the player's current level so
|
||||
/// only unlocked options appear.
|
||||
pub(super) fn handle_modes_button(
|
||||
interaction_query: Query<&Interaction, (With<ModesButton>, Changed<Interaction>)>,
|
||||
popovers: Query<Entity, With<ModesPopover>>,
|
||||
backdrops: Query<Entity, With<ModesPopoverBackdrop>>,
|
||||
progress: Option<Res<ProgressResource>>,
|
||||
daily: Option<Res<DailyChallengeResource>>,
|
||||
font_res: Option<Res<FontResource>>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
let pressed = interaction_query.iter().any(|i| *i == Interaction::Pressed);
|
||||
if !pressed {
|
||||
return;
|
||||
}
|
||||
if let Ok(entity) = popovers.single() {
|
||||
commands.entity(entity).despawn();
|
||||
for e in &backdrops {
|
||||
commands.entity(e).despawn();
|
||||
}
|
||||
} else {
|
||||
spawn_modes_popover(
|
||||
&mut commands,
|
||||
progress.as_deref(),
|
||||
daily.as_deref(),
|
||||
font_res.as_deref(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawns the modes popover anchored just below the action bar's right
|
||||
/// edge. Always includes Classic; includes Daily Challenge when a daily
|
||||
/// resource is loaded; includes Zen / Challenge / Time Attack once the
|
||||
/// player reaches the challenge unlock level.
|
||||
pub(super) fn spawn_modes_popover(
|
||||
commands: &mut Commands,
|
||||
progress: Option<&ProgressResource>,
|
||||
daily: Option<&DailyChallengeResource>,
|
||||
font_res: Option<&FontResource>,
|
||||
) {
|
||||
let level = progress.map_or(0, |p| p.0.level);
|
||||
let font = TextFont {
|
||||
font: font_res.map(|f| f.0.clone()).unwrap_or_default(),
|
||||
font_size: 15.0,
|
||||
..default()
|
||||
};
|
||||
|
||||
// Each row carries a tooltip alongside its label so hover reveals
|
||||
// a one-line description of what the mode does — mirroring the
|
||||
// tooltips on the action-bar buttons that opened this popover.
|
||||
let mut rows: Vec<(ModeOption, &'static str, &'static str)> = vec![(
|
||||
ModeOption::Classic,
|
||||
"Classic",
|
||||
"Standard Klondike. Score, timer, and full progression.",
|
||||
)];
|
||||
if daily.is_some() {
|
||||
rows.push((
|
||||
ModeOption::DailyChallenge,
|
||||
"Daily Challenge",
|
||||
"Today's seeded deal. Same for every player worldwide.",
|
||||
));
|
||||
}
|
||||
if level >= CHALLENGE_UNLOCK_LEVEL {
|
||||
rows.push((
|
||||
ModeOption::Zen,
|
||||
"Zen",
|
||||
"No timer, no score, no penalties. Just play.",
|
||||
));
|
||||
rows.push((
|
||||
ModeOption::Challenge,
|
||||
"Challenge",
|
||||
"Hand-picked hard seeds. No undo allowed.",
|
||||
));
|
||||
rows.push((
|
||||
ModeOption::TimeAttack,
|
||||
"Time Attack",
|
||||
"Win as many games as you can in ten minutes.",
|
||||
));
|
||||
}
|
||||
|
||||
// Popover opens upward from just above the bottom action bar.
|
||||
// Use a platform-aware offset that clears the bar height + safe-area
|
||||
// gesture zone on Android, and the flat bar height on desktop.
|
||||
let popover_bottom = Val::Px(ACTION_POPOVER_BOTTOM_PX);
|
||||
|
||||
commands
|
||||
.spawn((
|
||||
ModesPopover,
|
||||
HudPopoverOpen,
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
right: VAL_SPACE_3,
|
||||
bottom: popover_bottom,
|
||||
flex_direction: FlexDirection::Column,
|
||||
row_gap: VAL_SPACE_1,
|
||||
padding: UiRect::all(VAL_SPACE_2),
|
||||
border_radius: BorderRadius::all(Val::Px(RADIUS_MD)),
|
||||
..default()
|
||||
},
|
||||
BackgroundColor(BG_ELEVATED),
|
||||
ZIndex(Z_HUD_POPOVER),
|
||||
))
|
||||
.with_children(|panel| {
|
||||
for (option, label, tooltip) in rows {
|
||||
panel
|
||||
.spawn((
|
||||
option,
|
||||
ActionButton,
|
||||
PopoverRow,
|
||||
Button,
|
||||
Tooltip::new(tooltip),
|
||||
Node {
|
||||
padding: UiRect::axes(VAL_SPACE_3, Val::Px(6.0)),
|
||||
justify_content: JustifyContent::FlexStart,
|
||||
align_items: AlignItems::Center,
|
||||
min_width: Val::Px(150.0),
|
||||
border_radius: BorderRadius::all(Val::Px(RADIUS_SM)),
|
||||
..default()
|
||||
},
|
||||
BackgroundColor(ACTION_BTN_IDLE),
|
||||
))
|
||||
.with_children(|b| {
|
||||
b.spawn((Text::new(label), font.clone(), TextColor(TEXT_PRIMARY)));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Fullscreen transparent backdrop at Z_HUD_POPOVER_BACKDROP (below the
|
||||
// popover at Z_HUD_POPOVER) so tapping outside light-dismisses it.
|
||||
commands.spawn((
|
||||
ModesPopoverBackdrop,
|
||||
Button,
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
left: Val::Px(0.0),
|
||||
top: Val::Px(0.0),
|
||||
width: Val::Percent(100.0),
|
||||
height: Val::Percent(100.0),
|
||||
..default()
|
||||
},
|
||||
BackgroundColor(Color::NONE),
|
||||
ZIndex(Z_HUD_POPOVER_BACKDROP),
|
||||
));
|
||||
}
|
||||
|
||||
/// Dispatches the click on a popover row to the matching request event,
|
||||
/// then despawns the popover.
|
||||
///
|
||||
/// Classic uses [`NewGameRequestEvent`] directly; the other modes use
|
||||
/// their `Start*RequestEvent` so the existing keyboard handler runs
|
||||
/// (level gates, `TimeAttackResource` setup, daily seed lookup, etc.) —
|
||||
/// the popover stays a thin entry point and never duplicates that logic.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn handle_mode_option_click(
|
||||
interaction_query: Query<(&Interaction, &ModeOption), Changed<Interaction>>,
|
||||
popovers: Query<Entity, With<ModesPopover>>,
|
||||
backdrops: Query<Entity, With<ModesPopoverBackdrop>>,
|
||||
mut new_game: MessageWriter<NewGameRequestEvent>,
|
||||
mut zen: MessageWriter<StartZenRequestEvent>,
|
||||
mut challenge: MessageWriter<StartChallengeRequestEvent>,
|
||||
mut time_attack: MessageWriter<StartTimeAttackRequestEvent>,
|
||||
mut daily: MessageWriter<StartDailyChallengeRequestEvent>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
let mut clicked_any = false;
|
||||
for (interaction, option) in &interaction_query {
|
||||
if *interaction != Interaction::Pressed {
|
||||
continue;
|
||||
}
|
||||
clicked_any = true;
|
||||
match option {
|
||||
ModeOption::Classic => {
|
||||
new_game.write(NewGameRequestEvent::default());
|
||||
}
|
||||
ModeOption::DailyChallenge => {
|
||||
daily.write(StartDailyChallengeRequestEvent);
|
||||
}
|
||||
ModeOption::Zen => {
|
||||
zen.write(StartZenRequestEvent);
|
||||
}
|
||||
ModeOption::Challenge => {
|
||||
challenge.write(StartChallengeRequestEvent);
|
||||
}
|
||||
ModeOption::TimeAttack => {
|
||||
time_attack.write(StartTimeAttackRequestEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
if clicked_any && let Ok(entity) = popovers.single() {
|
||||
commands.entity(entity).despawn();
|
||||
for e in &backdrops {
|
||||
commands.entity(e).despawn();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggles the [`MenuPopover`]: spawns it on first click, despawns it on
|
||||
/// second click. The popover lists the five overlays previously only
|
||||
/// reachable via the S / A / P / O / L hotkeys.
|
||||
pub(super) fn handle_menu_button(
|
||||
interaction_query: Query<&Interaction, (With<MenuButton>, Changed<Interaction>)>,
|
||||
popovers: Query<Entity, With<MenuPopover>>,
|
||||
backdrops: Query<Entity, With<MenuPopoverBackdrop>>,
|
||||
scrims: Query<(), With<ModalScrim>>,
|
||||
font_res: Option<Res<FontResource>>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
let pressed = interaction_query.iter().any(|i| *i == Interaction::Pressed);
|
||||
if !pressed {
|
||||
return;
|
||||
}
|
||||
if let Ok(entity) = popovers.single() {
|
||||
commands.entity(entity).despawn();
|
||||
for e in &backdrops {
|
||||
commands.entity(e).despawn();
|
||||
}
|
||||
} else if scrims.is_empty() {
|
||||
spawn_menu_popover(&mut commands, font_res.as_deref());
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawns the menu popover anchored just below the action bar, with one
|
||||
/// row per overlay. Each row dispatches its corresponding
|
||||
/// `Toggle*RequestEvent` so the existing toggle handler runs (and the
|
||||
/// HUD never duplicates spawn / despawn / fetch logic).
|
||||
pub(super) fn spawn_menu_popover(commands: &mut Commands, font_res: Option<&FontResource>) {
|
||||
let font = TextFont {
|
||||
font: font_res.map(|f| f.0.clone()).unwrap_or_default(),
|
||||
font_size: 15.0,
|
||||
..default()
|
||||
};
|
||||
|
||||
// Each row carries a tooltip alongside its label so hover reveals
|
||||
// a one-line description of what each overlay shows — mirroring
|
||||
// the tooltips on the action-bar buttons that opened this popover.
|
||||
let rows: [(MenuOption, &'static str, &'static str); 7] = [
|
||||
(
|
||||
MenuOption::Help,
|
||||
"Help",
|
||||
"Show controls, rules, and keyboard shortcuts.",
|
||||
),
|
||||
(
|
||||
MenuOption::Modes,
|
||||
"Game Modes",
|
||||
"Switch modes: Classic, Daily, Zen, Challenge, Time Attack.",
|
||||
),
|
||||
(
|
||||
MenuOption::Stats,
|
||||
"Stats",
|
||||
"Lifetime totals: wins, streaks, fastest time, best score.",
|
||||
),
|
||||
(
|
||||
MenuOption::Achievements,
|
||||
"Achievements",
|
||||
"Browse unlocked achievements and the rewards still ahead.",
|
||||
),
|
||||
(
|
||||
MenuOption::Profile,
|
||||
"Profile",
|
||||
"Your level, XP progress, and sync status.",
|
||||
),
|
||||
(
|
||||
MenuOption::Settings,
|
||||
"Settings",
|
||||
"Audio, animations, theme, draw mode, and sync.",
|
||||
),
|
||||
(
|
||||
MenuOption::Leaderboard,
|
||||
"Leaderboard",
|
||||
"Top players from your sync server. Opt in from Profile.",
|
||||
),
|
||||
];
|
||||
|
||||
// Same upward-opening placement as ModesPopover.
|
||||
let popover_bottom = Val::Px(ACTION_POPOVER_BOTTOM_PX);
|
||||
|
||||
commands
|
||||
.spawn((
|
||||
MenuPopover,
|
||||
HudPopoverOpen,
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
right: VAL_SPACE_3,
|
||||
bottom: popover_bottom,
|
||||
flex_direction: FlexDirection::Column,
|
||||
row_gap: VAL_SPACE_1,
|
||||
padding: UiRect::all(VAL_SPACE_2),
|
||||
border_radius: BorderRadius::all(Val::Px(RADIUS_MD)),
|
||||
..default()
|
||||
},
|
||||
BackgroundColor(BG_ELEVATED),
|
||||
ZIndex(Z_HUD_POPOVER),
|
||||
))
|
||||
.with_children(|panel| {
|
||||
for (option, label, tooltip) in rows {
|
||||
panel
|
||||
.spawn((
|
||||
option,
|
||||
ActionButton,
|
||||
PopoverRow,
|
||||
Button,
|
||||
Tooltip::new(tooltip),
|
||||
Node {
|
||||
padding: UiRect::axes(VAL_SPACE_3, Val::Px(6.0)),
|
||||
justify_content: JustifyContent::FlexStart,
|
||||
align_items: AlignItems::Center,
|
||||
min_width: Val::Px(150.0),
|
||||
border_radius: BorderRadius::all(Val::Px(RADIUS_SM)),
|
||||
..default()
|
||||
},
|
||||
BackgroundColor(ACTION_BTN_IDLE),
|
||||
))
|
||||
.with_children(|b| {
|
||||
b.spawn((Text::new(label), font.clone(), TextColor(TEXT_PRIMARY)));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Transparent fullscreen backdrop behind the popover — tapping anywhere
|
||||
// outside the panel light-dismisses it via handle_menu_backdrop_click.
|
||||
commands.spawn((
|
||||
MenuPopoverBackdrop,
|
||||
Button,
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
left: Val::Px(0.0),
|
||||
top: Val::Px(0.0),
|
||||
width: Val::Percent(100.0),
|
||||
height: Val::Percent(100.0),
|
||||
..default()
|
||||
},
|
||||
BackgroundColor(Color::NONE),
|
||||
ZIndex(Z_HUD_POPOVER_BACKDROP),
|
||||
));
|
||||
}
|
||||
|
||||
/// Dispatches the click on a menu row to the matching toggle event,
|
||||
/// then despawns the popover.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn handle_menu_option_click(
|
||||
interaction_query: Query<(&Interaction, &MenuOption), Changed<Interaction>>,
|
||||
popovers: Query<Entity, With<MenuPopover>>,
|
||||
backdrops: Query<Entity, With<MenuPopoverBackdrop>>,
|
||||
mut stats: MessageWriter<ToggleStatsRequestEvent>,
|
||||
mut achievements: MessageWriter<ToggleAchievementsRequestEvent>,
|
||||
mut profile: MessageWriter<ToggleProfileRequestEvent>,
|
||||
mut settings: MessageWriter<ToggleSettingsRequestEvent>,
|
||||
mut leaderboard: MessageWriter<ToggleLeaderboardRequestEvent>,
|
||||
mut help: MessageWriter<HelpRequestEvent>,
|
||||
progress: Option<Res<ProgressResource>>,
|
||||
daily: Option<Res<DailyChallengeResource>>,
|
||||
font_res: Option<Res<FontResource>>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
let mut clicked_any = false;
|
||||
let mut open_modes = false;
|
||||
for (interaction, option) in &interaction_query {
|
||||
if *interaction != Interaction::Pressed {
|
||||
continue;
|
||||
}
|
||||
clicked_any = true;
|
||||
match option {
|
||||
MenuOption::Help => {
|
||||
help.write(HelpRequestEvent);
|
||||
}
|
||||
MenuOption::Modes => {
|
||||
open_modes = true;
|
||||
}
|
||||
MenuOption::Stats => {
|
||||
stats.write(ToggleStatsRequestEvent);
|
||||
}
|
||||
MenuOption::Achievements => {
|
||||
achievements.write(ToggleAchievementsRequestEvent);
|
||||
}
|
||||
MenuOption::Profile => {
|
||||
profile.write(ToggleProfileRequestEvent);
|
||||
}
|
||||
MenuOption::Settings => {
|
||||
settings.write(ToggleSettingsRequestEvent);
|
||||
}
|
||||
MenuOption::Leaderboard => {
|
||||
leaderboard.write(ToggleLeaderboardRequestEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
if clicked_any && let Ok(entity) = popovers.single() {
|
||||
commands.entity(entity).despawn();
|
||||
for e in &backdrops {
|
||||
commands.entity(e).despawn();
|
||||
}
|
||||
}
|
||||
if open_modes {
|
||||
spawn_modes_popover(
|
||||
&mut commands,
|
||||
progress.as_deref(),
|
||||
daily.as_deref(),
|
||||
font_res.as_deref(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub(super) 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.
|
||||
pub(super) 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.
|
||||
pub(super) fn handle_modes_backdrop_click(
|
||||
interaction_query: Query<&Interaction, (With<ModesPopoverBackdrop>, Changed<Interaction>)>,
|
||||
popovers: Query<Entity, With<ModesPopover>>,
|
||||
backdrops: Query<Entity, With<ModesPopoverBackdrop>>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
let pressed = interaction_query.iter().any(|i| *i == Interaction::Pressed);
|
||||
if !pressed {
|
||||
return;
|
||||
}
|
||||
for e in popovers.iter().chain(backdrops.iter()) {
|
||||
commands.entity(e).despawn();
|
||||
}
|
||||
}
|
||||
|
||||
/// Despawns the [`MenuPopover`] and its backdrop when the player taps
|
||||
/// anywhere outside the panel (i.e. the transparent backdrop is pressed).
|
||||
pub(super) fn handle_menu_backdrop_click(
|
||||
interaction_query: Query<&Interaction, (With<MenuPopoverBackdrop>, Changed<Interaction>)>,
|
||||
popovers: Query<Entity, With<MenuPopover>>,
|
||||
backdrops: Query<Entity, With<MenuPopoverBackdrop>>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
let pressed = interaction_query.iter().any(|i| *i == Interaction::Pressed);
|
||||
if !pressed {
|
||||
return;
|
||||
}
|
||||
for e in popovers.iter().chain(backdrops.iter()) {
|
||||
commands.entity(e).despawn();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn toggle_hud_on_tap(
|
||||
mut touch_events: MessageReader<TouchInput>,
|
||||
drag: Res<DragState>,
|
||||
scrims: Query<(), With<ModalScrim>>,
|
||||
paused: Option<Res<PausedResource>>,
|
||||
mut tracker: ResMut<HudTapTracker>,
|
||||
mut hud_vis: ResMut<HudVisibility>,
|
||||
buttons: Query<&Interaction, With<ActionButton>>,
|
||||
mut game_consumed: ResMut<GameInputConsumedResource>,
|
||||
) {
|
||||
use bevy::input::touch::TouchPhase;
|
||||
if !scrims.is_empty() || paused.is_some_and(|p| p.0) {
|
||||
// Drain buffered events so they don't replay in the frame after
|
||||
// the scrim despawns, which would trigger a spurious visibility
|
||||
// toggle as the resume/close button tap's Started+Ended pair
|
||||
// replays in the now-scrim-free frame.
|
||||
for _ in touch_events.read() {}
|
||||
tracker.start_pos = None;
|
||||
tracker.started_on_button = false;
|
||||
game_consumed.0 = false;
|
||||
return;
|
||||
}
|
||||
for event in touch_events.read() {
|
||||
match event.phase {
|
||||
TouchPhase::Started => {
|
||||
tracker.start_pos = Some(event.position);
|
||||
// Record whether the finger-down landed on a button so
|
||||
// the finger-up doesn't double-fire (toggle bar + press
|
||||
// button at the same time).
|
||||
tracker.started_on_button = buttons.iter().any(|i| *i != Interaction::None);
|
||||
}
|
||||
TouchPhase::Ended if drag.is_idle() => {
|
||||
// Also treat taps where game logic consumed the touch (e.g.
|
||||
// drawing from stock) as "on button" so they don't toggle
|
||||
// the HUD. The flag is set on TouchPhase::Started by the
|
||||
// input system that consumed the tap and must be cleared here
|
||||
// regardless of whether we toggle.
|
||||
let on_button = tracker.started_on_button || game_consumed.0;
|
||||
game_consumed.0 = false;
|
||||
if let Some(start) = tracker.start_pos.take()
|
||||
&& !on_button
|
||||
&& (event.position - start).length() < HUD_TAP_SLOP_PX
|
||||
{
|
||||
*hud_vis = match *hud_vis {
|
||||
HudVisibility::Visible => HudVisibility::Hidden,
|
||||
HudVisibility::Hidden => HudVisibility::Visible,
|
||||
};
|
||||
}
|
||||
tracker.started_on_button = false;
|
||||
}
|
||||
// Moved: don't clear start_pos — Android fires Moved for normal
|
||||
// tap jitter, and the distance check at Ended already rejects
|
||||
// real drags. Clearing here would silently swallow tap toggles.
|
||||
TouchPhase::Canceled => {
|
||||
tracker.start_pos = None;
|
||||
tracker.started_on_button = false;
|
||||
game_consumed.0 = false;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user