From d4448bf0cdc1bcda9f047be32118897884091273 Mon Sep 17 00:00:00 2001 From: funman300 Date: Wed, 8 Jul 2026 20:31:44 -0700 Subject: [PATCH] feat(engine): group HUD menu popover into Play/You/Community/System sections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase C of docs/ui-redesign-2026-07.md. The Modes row is gone — Home owns mode selection, so the popover's Play section carries a Home row firing the new ToggleHomeRequestEvent (read by toggle_home_screen alongside the existing M accelerator). Section headers are quiet caption-size labels inside the existing panel widget, not a new widget. The action-bar Modes button and its popover are untouched (their removal is Phase B territory when Home gains hierarchy). - MenuOption: Modes variant replaced by Home; rows grouped Play (Home) · You (Profile, Stats, Achievements) · Community (Leaderboard) · System (Settings, Help) - handle_menu_option_click no longer chains into spawn_modes_popover - Tests: tooltip sweep updated (7 rows still), new toggle_home_event_opens_home_screen covers the popover's open path Co-Authored-By: Claude Fable 5 --- solitaire_engine/src/events.rs | 5 + solitaire_engine/src/home_plugin.rs | 46 ++++- .../src/hud_plugin/interaction.rs | 159 +++++++++++------- solitaire_engine/src/hud_plugin/mod.rs | 24 +-- solitaire_engine/src/hud_plugin/tests.rs | 2 +- 5 files changed, 159 insertions(+), 77 deletions(-) diff --git a/solitaire_engine/src/events.rs b/solitaire_engine/src/events.rs index 79eb0ea..4e3678a 100644 --- a/solitaire_engine/src/events.rs +++ b/solitaire_engine/src/events.rs @@ -237,6 +237,11 @@ pub struct ToggleSettingsRequestEvent; #[derive(Message, Debug, Clone, Copy, Default)] pub struct ToggleLeaderboardRequestEvent; +/// Request to toggle the Home mode launcher. Fired by the HUD +/// Menu-popover "Home" row alongside the existing `M` accelerator. +#[derive(Message, Debug, Clone, Copy, Default)] +pub struct ToggleHomeRequestEvent; + /// Fired by `SyncPlugin` after a pull task resolves and the merged result has /// been persisted to disk. `Ok(SyncResponse)` carries the merged payload plus /// any `ConflictReport`s the merge produced. `Err(String)` carries a diff --git a/solitaire_engine/src/home_plugin.rs b/solitaire_engine/src/home_plugin.rs index 01d2065..5193a13 100644 --- a/solitaire_engine/src/home_plugin.rs +++ b/solitaire_engine/src/home_plugin.rs @@ -24,7 +24,8 @@ use crate::daily_challenge_plugin::DailyChallengeResource; use crate::events::{ InfoToastEvent, NewGameRequestEvent, StartChallengeRequestEvent, StartDailyChallengeRequestEvent, StartDifficultyRequestEvent, StartPlayBySeedRequestEvent, - StartTimeAttackRequestEvent, StartZenRequestEvent, ToggleProfileRequestEvent, + StartTimeAttackRequestEvent, StartZenRequestEvent, ToggleHomeRequestEvent, + ToggleProfileRequestEvent, }; use crate::font_plugin::FontResource; use crate::progress_plugin::ProgressResource; @@ -264,6 +265,7 @@ impl Plugin for HomePlugin { .add_message::() .add_message::() .add_message::() + .add_message::() .add_message::() .add_message::() // Defensively register MouseWheel so `scroll_home_panel` @@ -371,6 +373,7 @@ fn spawn_home_on_launch( fn toggle_home_screen( mut commands: Commands, keys: Res>, + mut requests: MessageReader, progress: Option>, stats: Option>, settings: Option>, @@ -380,7 +383,8 @@ fn toggle_home_screen( other_modal_scrims: Query<(), (With, Without)>, diff_expanded: Res, ) { - if !keys.just_pressed(KeyCode::KeyM) { + let button_clicked = requests.read().count() > 0; + if !keys.just_pressed(KeyCode::KeyM) && !button_clicked { return; } if let Ok(entity) = screens.single() { @@ -1621,6 +1625,44 @@ mod tests { ); } + /// The HUD Menu popover's "Home" row fires + /// `ToggleHomeRequestEvent`; it must open Home exactly like the + /// `M` accelerator (Phase C: the popover's Play section replaces + /// the old Modes row). + #[test] + fn toggle_home_event_opens_home_screen() { + let mut app = headless_app(); + app.world_mut() + .resource_mut::>() + .write(ToggleHomeRequestEvent); + app.update(); + + assert_eq!( + app.world_mut() + .query::<&HomeScreen>() + .iter(app.world()) + .count(), + 1, + "ToggleHomeRequestEvent must open the Home modal" + ); + + // A second request toggles it closed, matching the M key. + app.world_mut() + .resource_mut::>() + .write(ToggleHomeRequestEvent); + app.update(); + app.update(); + + assert_eq!( + app.world_mut() + .query::<&HomeScreen>() + .iter(app.world()) + .count(), + 0, + "second ToggleHomeRequestEvent must close the Home modal" + ); + } + #[test] fn pressing_m_twice_closes_home_screen() { let mut app = headless_app(); diff --git a/solitaire_engine/src/hud_plugin/interaction.rs b/solitaire_engine/src/hud_plugin/interaction.rs index b7e5a19..128200c 100644 --- a/solitaire_engine/src/hud_plugin/interaction.rs +++ b/solitaire_engine/src/hud_plugin/interaction.rs @@ -311,44 +311,65 @@ pub(super) fn spawn_menu_popover(commands: &mut Commands, font_res: Option<&Font ..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] = [ + // One popover row: destination, label, hover tooltip. + type MenuRow = (MenuOption, &'static str, &'static str); + // Destinations grouped into labelled sections (Phase C of the menu + // redesign): Play · You · Community · System. 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. Mode + // selection lives on Home now, so there is no Modes row. + let sections: [(&'static str, &'static [MenuRow]); 4] = [ ( - MenuOption::Help, - "Help", - "Show controls, rules, and keyboard shortcuts.", + "Play", + &[( + MenuOption::Home, + "Home", + "Pick a mode, continue, or start a new game.", + )], ), ( - MenuOption::Modes, - "Game Modes", - "Switch modes: Classic, Daily, Zen, Challenge, Time Attack.", + "You", + &[ + ( + MenuOption::Profile, + "Profile", + "Your level, XP progress, and sync status.", + ), + ( + MenuOption::Stats, + "Stats", + "Lifetime totals: wins, streaks, fastest time, best score.", + ), + ( + MenuOption::Achievements, + "Achievements", + "Browse unlocked achievements and the rewards still ahead.", + ), + ], ), ( - MenuOption::Stats, - "Stats", - "Lifetime totals: wins, streaks, fastest time, best score.", + "Community", + &[( + MenuOption::Leaderboard, + "Leaderboard", + "Top players from your sync server. Opt in from Profile.", + )], ), ( - 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.", + "System", + &[ + ( + MenuOption::Settings, + "Settings", + "Audio, animations, theme, draw mode, and sync.", + ), + ( + MenuOption::Help, + "Help", + "Show controls, rules, and keyboard shortcuts.", + ), + ], ), ]; @@ -373,27 +394,48 @@ pub(super) fn spawn_menu_popover(commands: &mut Commands, font_res: Option<&Font ZIndex(Z_HUD_POPOVER), )) .with_children(|panel| { - for (option, label, tooltip) in rows { + let section_font = TextFont { + font: font_res.map(|f| f.0.clone()).unwrap_or_default(), + font_size: TYPE_CAPTION, + ..default() + }; + for (section, rows) in sections { + // Non-interactive section header — a quiet divider + // inside the existing panel, not a new widget. 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), - )) + .spawn(Node { + padding: UiRect::axes(VAL_SPACE_3, Val::Px(2.0)), + ..default() + }) .with_children(|b| { - b.spawn((Text::new(label), font.clone(), TextColor(TEXT_PRIMARY))); + b.spawn(( + Text::new(section), + section_font.clone(), + TextColor(TEXT_SECONDARY), + )); }); + 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))); + }); + } } }); @@ -422,31 +464,28 @@ pub(super) fn handle_menu_option_click( interaction_query: Query<(&Interaction, &MenuOption), Changed>, popovers: Query>, backdrops: Query>, + mut home: MessageWriter, mut stats: MessageWriter, mut achievements: MessageWriter, mut profile: MessageWriter, mut settings: MessageWriter, mut leaderboard: MessageWriter, mut help: MessageWriter, - progress: Option>, - daily: Option>, - font_res: Option>, 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::Home => { + home.write(ToggleHomeRequestEvent); + } MenuOption::Help => { help.write(HelpRequestEvent); } - MenuOption::Modes => { - open_modes = true; - } MenuOption::Stats => { stats.write(ToggleStatsRequestEvent); } @@ -470,14 +509,6 @@ pub(super) fn handle_menu_option_click( 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 diff --git a/solitaire_engine/src/hud_plugin/mod.rs b/solitaire_engine/src/hud_plugin/mod.rs index 8d5d7cd..62bcc45 100644 --- a/solitaire_engine/src/hud_plugin/mod.rs +++ b/solitaire_engine/src/hud_plugin/mod.rs @@ -30,9 +30,9 @@ use crate::daily_challenge_plugin::DailyChallengeResource; use crate::events::{ HelpRequestEvent, InfoToastEvent, NewGameRequestEvent, PauseRequestEvent, StartChallengeRequestEvent, StartDailyChallengeRequestEvent, StartTimeAttackRequestEvent, - StartZenRequestEvent, ToggleAchievementsRequestEvent, ToggleLeaderboardRequestEvent, - ToggleProfileRequestEvent, ToggleSettingsRequestEvent, ToggleStatsRequestEvent, - UndoRequestEvent, WinStreakMilestoneEvent, + StartZenRequestEvent, ToggleAchievementsRequestEvent, ToggleHomeRequestEvent, + ToggleLeaderboardRequestEvent, ToggleProfileRequestEvent, ToggleSettingsRequestEvent, + ToggleStatsRequestEvent, UndoRequestEvent, WinStreakMilestoneEvent, }; use crate::font_plugin::FontResource; use crate::game_plugin::{GameMutation, NewGameRequestWriters}; @@ -386,8 +386,9 @@ pub enum ModeOption { } /// 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. +/// which exposes the Home / Profile / Stats / Achievements / +/// Leaderboard / Settings / Help overlays without needing the +/// M/P/S/A/L/O/F1 hotkeys. #[derive(Component, Debug)] pub struct MenuButton; @@ -413,16 +414,18 @@ struct MenuPopoverBackdrop; struct ModesPopoverBackdrop; /// One row inside the [`MenuPopover`]. The variant selects which -/// `Toggle*RequestEvent` the click handler fires. +/// `Toggle*RequestEvent` the click handler fires. Rows render grouped +/// under section headers (Play · You · Community · System); mode +/// selection lives on Home, so there is no Modes row here. #[derive(Component, Debug, Clone, Copy)] pub enum MenuOption { - Help, - Modes, + Home, + Profile, Stats, Achievements, - Profile, - Settings, Leaderboard, + Settings, + Help, } /// HUD Z-layer — above cards (which start at z=0) but below overlay screens. @@ -459,6 +462,7 @@ impl Plugin for HudPlugin { .add_message::() .add_message::() .add_message::() + .add_message::() .add_message::() .add_message::() .add_message::() diff --git a/solitaire_engine/src/hud_plugin/tests.rs b/solitaire_engine/src/hud_plugin/tests.rs index 0b2c086..8a8d7db 100644 --- a/solitaire_engine/src/hud_plugin/tests.rs +++ b/solitaire_engine/src/hud_plugin/tests.rs @@ -786,8 +786,8 @@ fn popover_rows_carry_tooltip_strings() { menu_tooltips.len() ); for expected in [ + "Pick a mode, continue, or start a new game.", "Show controls, rules, and keyboard shortcuts.", - "Switch modes: Classic, Daily, Zen, Challenge, Time Attack.", "Lifetime totals: wins, streaks, fastest time, best score.", "Browse unlocked achievements and the rewards still ahead.", "Your level, XP progress, and sync status.",