//! Persistent in-game HUD: score, move count, elapsed time, mode badge,
//! daily-challenge constraint, and undo count.
//!
//! The HUD spawns once at startup and lives for the app's lifetime. Text is
//! refreshed whenever `GameStateResource` changes (which happens on every move
//! and every elapsed-time tick), so score, moves, and timer all stay current
//! without a separate tick system.

use bevy::prelude::*;
use bevy::window::WindowResized;

mod fx;
mod interaction;
mod spawn;
mod updates;

pub use fx::*;
use interaction::*;
use spawn::*;
use updates::*;


// On wasm32 AvatarPlugin is gated out; define a placeholder type so the
// Option<Res<AvatarResource>> parameters below compile without changes.
// The resource is never inserted on wasm, so every call resolves to None.
#[cfg(target_arch = "wasm32")]
#[derive(bevy::prelude::Resource)]
struct AvatarResource(Option<bevy::prelude::Handle<bevy::prelude::Image>>);
use crate::challenge_plugin::CHALLENGE_UNLOCK_LEVEL;
use crate::daily_challenge_plugin::DailyChallengeResource;
use crate::events::{
    HelpRequestEvent, InfoToastEvent, NewGameRequestEvent, PauseRequestEvent,
    StartChallengeRequestEvent, StartDailyChallengeRequestEvent, StartTimeAttackRequestEvent,
    StartZenRequestEvent, ToggleAchievementsRequestEvent, ToggleLeaderboardRequestEvent,
    ToggleProfileRequestEvent, ToggleSettingsRequestEvent, ToggleStatsRequestEvent,
    UndoRequestEvent, WinStreakMilestoneEvent,
};
use crate::font_plugin::FontResource;
use crate::game_plugin::{GameMutation, NewGameRequestWriters};
#[cfg(target_os = "android")]
use crate::input_plugin::TouchDragSet;
use crate::layout::HUD_BAND_HEIGHT;
use crate::layout::LayoutSystem;
use crate::pause_plugin::PausedResource;
use crate::platform::{SHOW_KEYBOARD_ACCELERATORS, USE_TOUCH_UI_LAYOUT};
use crate::progress_plugin::ProgressResource;
use crate::resources::GameStateResource;
#[cfg(target_os = "android")]
use crate::resources::{DragState, GameInputConsumedResource};
use crate::safe_area::{SafeAreaAnchoredBottom, SafeAreaAnchoredTop};
use crate::selection_plugin::SelectionState;
use crate::settings_plugin::SettingsResource;
use crate::time_attack_plugin::TimeAttackResource;
use crate::ui_focus::{FocusGroup, Focusable};
use crate::ui_modal::ModalScrim;
use crate::ui_theme::SPACE_2;
use crate::ui_theme::UiTextFx;
use crate::ui_theme::{
    ACCENT_PRIMARY, ACCENT_SECONDARY, BG_ELEVATED, BG_ELEVATED_HI, BG_ELEVATED_PRESSED,
    BG_HUD_BAND, BORDER_SUBTLE, HighContrastBorder, MOTION_SCORE_PULSE_SECS,
    MOTION_STREAK_FLOURISH_SECS, RADIUS_MD, RADIUS_SM, STATE_DANGER, STATE_INFO, STATE_SUCCESS,
    STATE_WARNING, STREAK_FLOURISH_PEAK_SCALE, TEXT_PRIMARY, TEXT_SECONDARY, TYPE_BODY,
    TYPE_BODY_LG, TYPE_CAPTION, TYPE_HEADLINE, VAL_SPACE_1, VAL_SPACE_2, VAL_SPACE_3,
    scaled_duration,
};
use crate::ui_tooltip::Tooltip;
use solitaire_data::SyncBackend;

/// Marker on the score text node.
#[derive(Component, Debug)]
pub struct HudScore;

/// Marker on the move-count text node.
#[derive(Component, Debug)]
pub struct HudMoves;

/// Marker on the elapsed-time text node.
#[derive(Component, Debug)]
pub struct HudTime;

/// Marker on the mode badge text node.
#[derive(Component, Debug)]
pub struct HudMode;

/// Marker on the daily-challenge constraint text node.
///
/// Displays the active goal (time limit or score target) when a daily challenge
/// is in progress. Empty string when no challenge is active or the game is won.
#[derive(Component, Debug)]
pub struct HudChallenge;

/// Marker on the "won this deal before" indicator text node.
///
/// Displays `"✓ Won before"` when the current deal's seed + draw_mode +
/// mode triple matches one of the entries in `ReplayHistoryResource`.
/// Empty string otherwise (including won games — the score readout
/// already conveys the win on the active deal). Only meaningful for
/// Classic / Zen / Challenge — daily-challenge and time-attack seeds
/// are filtered out implicitly because their replay entries always
/// carry a different mode tag.
#[derive(Component, Debug)]
pub struct HudWonPreviously;

/// Marker on the undo-count text node.
///
/// Shows how many undos have been used this game. Displayed in amber when
/// `undo_count > 0` because using undo blocks the no-undo achievement.
#[derive(Component, Debug)]
pub struct HudUndos;

/// Marker on the auto-complete badge text node.
///
/// Displays `"AUTO"` in green while `AutoCompleteState.active` is true;
/// empty string otherwise.
#[derive(Component, Debug)]
pub struct HudAutoComplete;

/// Marker on the stock-recycle counter text node.
///
/// Displays `"Recycles: N"` whenever `recycle_count > 0`, regardless of draw
/// mode, so the player can track stock recycling in both Draw-One and
/// Draw-Three (relevant to the `comeback` achievement). Hidden (empty string)
/// until the first recycle occurs.
#[derive(Component, Debug)]
pub struct HudRecycles;

/// Marker on the draw-cycle indicator text node.
///
/// Only shown in Draw-Three mode. Displays `"Cycle: N/3"` where N is the
/// number of cards that will be drawn on the next stock click
/// (`min(stock_len, 3)`). Shows `"Cycle: 0/3"` when the stock is empty
/// (recycle available). Hidden (empty string) in Draw-One mode or after the
/// game is won.
#[derive(Component, Debug)]
pub struct HudDrawCycle;

/// Marker on the keyboard-selection indicator text node.
///
/// Displays `"▶ {pile_name}"` while a pile is selected via Tab, or an empty
/// string when no pile is selected. Uses a light-yellow colour so it stands
/// out from the other white HUD items.
#[derive(Component, Debug)]
pub struct HudSelection;

/// Marker on the HUD band background node (the translucent band behind buttons).
#[derive(Component, Debug)]
pub struct HudBand;

/// Marker on the HUD score/info column root node.
#[derive(Component, Debug)]
pub struct HudColumn;

/// Marker on the action button bar root node.
#[derive(Component, Debug)]
pub struct HudActionBar;

/// Set wrapping the chained HUD button/popover interaction systems. Other
/// keyboard consumers order themselves around it (e.g.
/// [`crate::ui_focus::FocusKeys`] runs after) so input-consumption order is
/// deterministic (#143).
#[derive(bevy::ecs::schedule::SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
pub struct HudButtons;

/// Marker on the text node inside each touch-layout action-bar button.
/// Used by `resize_action_bar_labels` to update font size on window resize.
#[derive(Component, Debug)]
struct ActionButtonLabel;

/// Marker on the circular profile-picture button anchored to the
/// top-right of the HUD band. Pressing it opens the Profile overlay.
/// Shows the server avatar image when loaded; falls back to the player's
/// initial on a filled disc when no image is available.
#[derive(Component, Debug)]
pub struct HudAvatar;

/// Controls whether the in-game HUD (band, score column, action buttons) is
/// visible. Toggled on Android by tapping empty board space; always `Visible`
/// on desktop. Resets to `Visible` whenever a modal opens.
#[derive(Resource, Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum HudVisibility {
    #[default]
    Visible,
    Hidden,
}

#[cfg(target_os = "android")]
#[derive(Resource, Debug, Default)]
struct HudTapTracker {
    start_pos: Option<Vec2>,
    /// Set `true` when the finger-down hit an action button so the
    /// finger-up never toggles bar visibility.
    started_on_button: bool,
}

#[cfg(target_os = "android")]
const HUD_TAP_SLOP_PX: f32 = 25.0;

/// Drives the score-readout pulse: scales the [`HudScore`] text from
/// 1.0 → 1.1 → 1.0 over [`MOTION_SCORE_PULSE_SECS`] (scaled by
/// [`AnimSpeed`](solitaire_data::AnimSpeed)). Inserted on the score
/// entity whenever the score increases; removed once `elapsed >=
/// duration`.
#[derive(Component, Debug, Clone, Copy)]
pub struct ScorePulse {
    /// Seconds elapsed since the pulse started.
    pub elapsed: f32,
    /// Total duration. Zero under `AnimSpeed::Instant` — the system
    /// snaps the scale back to 1.0 on first tick so no half-state
    /// is ever shown.
    pub duration: f32,
}

/// Marker on a transient floating "+N" text spawned next to the score
/// readout when the score jumps by [`SCORE_FLOATER_THRESHOLD`] or more.
/// Drifts upward and fades out over `MOTION_SCORE_PULSE_SECS * 2`,
/// then despawns. Kept rare/meaningful by the threshold gate.
#[derive(Component, Debug, Clone, Copy)]
pub struct ScoreFloater {
    /// Seconds elapsed since the floater spawned.
    pub elapsed: f32,
    /// Total lifetime. Zero under `AnimSpeed::Instant` — the system
    /// despawns it on first tick.
    pub duration: f32,
}

/// Drives the streak-milestone flourish: scales the [`HudScore`] text
/// from `1.0 → STREAK_FLOURISH_PEAK_SCALE → 1.0` over
/// [`MOTION_STREAK_FLOURISH_SECS`] (scaled by
/// [`AnimSpeed`](solitaire_data::AnimSpeed)) and tints it
/// [`ACCENT_SECONDARY`] for the same window before restoring the
/// original colour.
///
/// The streak readout currently lives in the Stats overlay (press
/// `S`) — there is no always-on HUD streak counter — so the flourish
/// piggybacks on the score readout, which is the most prominent
/// always-visible HUD number. Mirrors the `FoundationFlourish`
/// pattern: triangular scale curve, fixed duration, restores state
/// when the timer expires.
///
/// Inserted on `HudScore` entities by `start_streak_flourish` when a
/// `WinStreakMilestoneEvent` fires; removed once `elapsed >=
/// duration` so the readout returns to its rest state for the next
/// frame's transform sync.
///
/// Coexists with [`ScorePulse`]: the streak flourish lives on a
/// dedicated marker so a streak-crossing win that also ticks the
/// score (every win does) doesn't have the two animations stomp on
/// each other's `Transform.scale` writes — the streak flourish runs
/// in a `Without<ScorePulse>` query so only the loudest of the two
/// celebrations is active at a time.
#[derive(Component, Debug, Clone, Copy)]
pub struct StreakFlourish {
    /// The streak milestone that triggered this flourish (3, 5, 10).
    /// Carried for diagnostic logging only — the visual is identical
    /// for every threshold so play-testing can decide later whether
    /// to differentiate.
    pub streak: u32,
    /// Seconds elapsed since the flourish began.
    pub elapsed: f32,
    /// Total animation length in seconds. Zero under
    /// [`AnimSpeed::Instant`](solitaire_data::AnimSpeed) — the system
    /// snaps the scale back to 1.0 on the first tick so no half-state
    /// is ever shown.
    pub duration: f32,
    /// The score readout's colour before the flourish began —
    /// restored when the timer expires so the readout returns to its
    /// resting `TEXT_PRIMARY` (or whatever it was) tint.
    pub original_color: Color,
}

/// Tracks the score from the previous frame so the HUD can detect
/// changes without a `ScoreChangedEvent`. The plugin wires this to the
/// pulse + floater systems on every `Update`.
#[derive(Resource, Debug, Default, Clone, Copy)]
pub struct PreviousScore(pub i32);

/// Score increase (in points) below which no floating "+N" is spawned.
/// 50 keeps the feedback for foundation drops and tableau-to-foundation
/// promotions; single-card placements (which can earn as little as +5)
/// stay quiet so the floater feels like a reward instead of noise.
pub const SCORE_FLOATER_THRESHOLD: i32 = 50;

/// Marker shared by every clickable HUD action button so a single
/// `paint_action_buttons` system can recolour them on hover/press without
/// each button needing its own paint handler.
#[derive(Component, Debug)]
pub struct ActionButton;

/// Marker on rows inside a popover panel ([`ModesPopover`] or
/// [`MenuPopover`]). Popover rows already carry `ActionButton` so the
/// hover/press paint path applies to them, but the auto-fade applied
/// to the top-level action bar must NOT also fade these rows — the
/// popover only renders when the player has explicitly opened it, so
/// its content should always be at full opacity. `apply_action_fade`
/// excludes entities with this marker via `Without<PopoverRow>`.
#[derive(Component, Debug)]
pub struct PopoverRow;

/// Marker on the "New Game" action button anchored top-right of the play
/// area. Click fires [`NewGameRequestEvent`]; the existing
/// `ConfirmNewGameScreen` modal handles confirmation when a game is in
/// progress.
#[derive(Component, Debug)]
pub struct NewGameButton;

/// Marker on the "Undo" action button. Click fires [`UndoRequestEvent`],
/// mirroring the `U` keyboard accelerator.
#[derive(Component, Debug)]
pub struct UndoButton;

/// Marker on the "Pause" action button. Click fires [`PauseRequestEvent`],
/// mirroring the `Esc` keyboard accelerator. The pause overlay's own resume
/// affordance dismisses it from the paused state.
#[derive(Component, Debug)]
pub struct PauseButton;

/// Marker on the "Help" action button. Click fires [`HelpRequestEvent`],
/// mirroring the `F1` keyboard accelerator.
#[derive(Component, Debug)]
pub struct HelpButton;

/// Marker on the "Hint" action button. Click spawns an async solver task
/// (same as the `H` keyboard accelerator) and highlights the suggested card.
#[derive(Component, Debug)]
pub struct HintButton;

/// Android HUD label for the Hint button — shared with the help screen's
/// controls reference so both always agree.
#[cfg(target_os = "android")]
pub(crate) const ANDROID_HINT_LABEL: &str = "Hint";

#[cfg(target_os = "android")]
const ACTION_BAR_LABELS: [&str; 7] = [
    "Menu",
    "Undo",
    "Pause",
    "Help",
    ANDROID_HINT_LABEL,
    "Mode",
    "New",
];
#[cfg(not(target_os = "android"))]
const ACTION_BAR_LABELS: [&str; 7] = [
    "Menu \u{2193}",
    "Undo",
    "Pause",
    "Help",
    "Hint",
    "Modes \u{2193}",
    "New Game",
];
#[cfg(target_os = "android")]
const ACTION_BAR_COLUMN_GAP: Val = Val::Px(4.0);
#[cfg(not(target_os = "android"))]
const ACTION_BAR_COLUMN_GAP: Val = VAL_SPACE_2;
#[cfg(target_os = "android")]
const ACTION_POPOVER_BOTTOM_PX: f32 = 200.0;
#[cfg(not(target_os = "android"))]
const ACTION_POPOVER_BOTTOM_PX: f32 = 80.0;
#[cfg(target_os = "android")]
const HINT_WON_MSG: &str = "Game won! Tap New Game to play again";
#[cfg(not(target_os = "android"))]
const HINT_WON_MSG: &str = "Game won! Press N for a new game";

/// Marker on the "Modes" action button. Click toggles the [`ModesPopover`]
/// (a small dropdown panel) below the action bar. Each popover row starts
/// the corresponding game mode.
#[derive(Component, Debug)]
pub struct ModesButton;

/// Marker on the dropdown panel that opens below the [`ModesButton`].
/// Spawned on first click, despawned on second click or on mode select.
#[derive(Component, Debug)]
pub struct ModesPopover;

/// One row inside the [`ModesPopover`]. The variant carries which event
/// the click handler should fire — Classic uses `NewGameRequestEvent`
/// directly, the others go through their `Start*RequestEvent` so the
/// existing keyboard handler's level gate / resource setup runs.
#[derive(Component, Debug, Clone, Copy)]
pub enum ModeOption {
    Classic,
    DailyChallenge,
    Zen,
    Challenge,
    TimeAttack,
}

/// Marker on the "Menu" action button. Click toggles the [`MenuPopover`]
/// which exposes the Stats / Achievements / Profile / Settings /
/// Leaderboard overlays without needing the S/A/P/O/L hotkeys.
#[derive(Component, Debug)]
pub struct MenuButton;

/// Marker on the dropdown panel that opens below the [`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)]
struct MenuPopoverBackdrop;

/// Fullscreen transparent backdrop spawned behind the [`ModesPopover`].
/// Pressing it (tap anywhere outside the popover) light-dismisses it.
#[derive(Component, Debug)]
struct ModesPopoverBackdrop;

/// One row inside the [`MenuPopover`]. The variant selects which
/// `Toggle*RequestEvent` the click handler fires.
#[derive(Component, Debug, Clone, Copy)]
pub enum MenuOption {
    Help,
    Modes,
    Stats,
    Achievements,
    Profile,
    Settings,
    Leaderboard,
}

/// HUD Z-layer — above cards (which start at z=0) but below overlay screens.
/// Mirrors `ui_theme::Z_HUD` and is duplicated here only so the hud module
/// can use it as a `const` without a non-const expression in `ZIndex(...)`.
const Z_HUD: i32 = crate::ui_theme::Z_HUD;
const Z_HUD_POPOVER_BACKDROP: i32 = crate::ui_theme::Z_HUD_POPOVER_BACKDROP;
const Z_HUD_POPOVER: i32 = crate::ui_theme::Z_HUD_POPOVER;
const Z_HUD_TOP: i32 = crate::ui_theme::Z_HUD_TOP;

/// Idle / hover / pressed colours shared by every action button. Aliased
/// to the theme tokens so the HUD picks up palette changes for free.
const ACTION_BTN_IDLE: Color = BG_ELEVATED;
const ACTION_BTN_HOVER: Color = BG_ELEVATED_HI;
const ACTION_BTN_PRESSED: Color = BG_ELEVATED_PRESSED;

/// Renders the in-game HUD: score counter, move counter, elapsed timer, draw-mode indicator, and the auto-complete badge that lights up when the game is solvable without further input.
pub struct HudPlugin;

impl Plugin for HudPlugin {
    fn build(&self, app: &mut App) {
        // The click handlers write to messages registered elsewhere by their
        // owning plugins (`GamePlugin`, `PausePlugin`, `HelpPlugin`,
        // `challenge_plugin`, `daily_challenge_plugin`, `time_attack_plugin`,
        // `input_plugin`). Re-register defensively so the HUD plugin works in
        // isolation under `MinimalPlugins` (tests). `add_message` is
        // idempotent.
        app.add_message::<NewGameRequestEvent>()
            .add_message::<UndoRequestEvent>()
            .add_message::<PauseRequestEvent>()
            .add_message::<HelpRequestEvent>()
            .add_message::<StartZenRequestEvent>()
            .add_message::<StartChallengeRequestEvent>()
            .add_message::<StartTimeAttackRequestEvent>()
            .add_message::<StartDailyChallengeRequestEvent>()
            .add_message::<ToggleStatsRequestEvent>()
            .add_message::<ToggleAchievementsRequestEvent>()
            .add_message::<ToggleProfileRequestEvent>()
            .add_message::<ToggleSettingsRequestEvent>()
            .add_message::<ToggleLeaderboardRequestEvent>()
            .add_message::<WinStreakMilestoneEvent>()
            .init_resource::<PreviousScore>()
            .init_resource::<HudActionFade>()
            .init_resource::<HudVisibility>()
            // 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>()
            .add_systems(Startup, (spawn_hud_band, spawn_hud, spawn_action_buttons, spawn_hud_avatar))
            // HUD text updaters run as one deterministic chain (they write
            // disjoint Text nodes, but Bevy can't prove it); update_hud also
            // reads AutoCompleteState, so the chain sits after the
            // auto-complete detect/drive chain (#143).
            .add_systems(
                Update,
                (
                    update_hud,
                    update_selection_hud.run_if(
                        resource_exists_and_changed::<SelectionState>
                            .or(resource_exists_and_changed::<GameStateResource>),
                    ),
                    update_won_previously,
                )
                    .chain()
                    .after(GameMutation)
                    .after(crate::auto_complete_plugin::AutoComplete)
                    .in_set(UiTextFx)
                    .ambiguous_with(UiTextFx),
            )
            // HUD chrome visibility: modal-restore writes HudVisibility, the
            // applier consumes it, and the layout recompute reads it — a
            // fixed chain instead of three racing systems (#143).
            .add_systems(
                Update,
                (restore_hud_on_modal, apply_hud_visibility)
                    .chain()
                    .before(LayoutSystem::UpdateOnResize),
            )
            .add_systems(
                Update,
                (
                    update_hud_avatar.after(crate::settings_plugin::SettingsMutation),
                    handle_avatar_button.ambiguous_with(HudButtons),
                ),
            )
            .add_systems(
                Update,
                announce_auto_complete
                    .after(GameMutation)
                    .after(crate::auto_complete_plugin::AutoComplete)
                    .in_set(crate::game_plugin::InfoToastWriters)
                    .ambiguous_with(crate::game_plugin::InfoToastWriters),
            )
            // Typography rescale touches HUD TextFont only, but orders after
            // the board painters that resize card/label text (#143).
            .add_systems(
                Update,
                update_hud_typography.after(crate::card_plugin::BoardVisuals),
            )
            .add_systems(
                Update,
                (
                    detect_score_change,
                    advance_score_pulse,
                    advance_score_floater,
                )
                    .chain()
                    .after(GameMutation)
                    .in_set(UiTextFx)
                    .ambiguous_with(UiTextFx)
                    .ambiguous_with(crate::card_plugin::BoardVisuals),
            )
            .add_systems(
                Update,
                (start_streak_flourish, advance_streak_flourish)
                    .chain()
                    .after(GameMutation)
                    .in_set(UiTextFx)
                    .ambiguous_with(UiTextFx)
                    .ambiguous_with(crate::card_plugin::BoardVisuals),
            )
            .add_systems(
                Update,
                (
                    handle_new_game_button
                        .in_set(NewGameRequestWriters)
                        .ambiguous_with(NewGameRequestWriters),
                    handle_undo_button
                        .in_set(crate::game_plugin::UndoRequestWriters)
                        .ambiguous_with(crate::game_plugin::UndoRequestWriters)
                        .before(GameMutation),
                    handle_pause_button,
                    handle_help_button,
                    handle_hint_button
                        .after(GameMutation)
                        .in_set(crate::game_plugin::InfoToastWriters)
                        .ambiguous_with(crate::game_plugin::InfoToastWriters),
                    handle_modes_button,
                    handle_mode_option_click
                        .in_set(NewGameRequestWriters)
                        .ambiguous_with(NewGameRequestWriters),
                    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,
                )
                    .chain()
                    .in_set(HudButtons)
                    .before(crate::ui_focus::FocusKeys),
            )
            // Fade lives in `Last` so it always overrides whatever the
            // hover/paint pass set on `BackgroundColor` this frame.
            // Otherwise on a hover-state change (`Changed<Interaction>`),
            // `paint_action_buttons` would clobber the alpha back to 1.0
            // mid-fade and produce a visible blip.
            ;
        // Desktop-only: cursor-proximity fade. On Android the bar
        // visibility is toggled explicitly; cursor_position() returning
        // Some(touch_pos) during a tap would otherwise fade the bar out.
        #[cfg(not(target_os = "android"))]
        app.add_systems(Last, (update_action_fade, apply_action_fade).chain());
        #[cfg(target_os = "android")]
        {
            app.init_resource::<HudTapTracker>()
                .add_message::<TouchInput>()
                .add_systems(
                    Update,
                    toggle_hud_on_tap
                        .after(TouchDragSet::AfterStartDrag)
                        .in_set(TouchDragSet::BeforeEndDrag),
                );
            app.add_systems(
                Update,
                resize_action_bar_labels
                    .run_if(resource_exists_and_changed::<crate::layout::LayoutResource>),
            );
        }
    }
}

#[cfg(test)]
mod tests;
