feat(engine): convert HomeScreen to modal scaffold + Done button
Phase 3 step 5f of the UX overhaul. Closes the per-overlay conversion phase: every read-only overlay (Help, Stats, Achievements, Profile, Leaderboard, and now Home) sits inside the same ui_modal scaffold, picks colours from ui_theme, and dismisses via a real "Done" primary button alongside its keyboard accelerator. Home modal: - Header: "Solitaire Quest" - Mode badge: "Current mode: <mode>" in ACCENT_PRIMARY (yellow) - Two sections (Game Controls / Screens), each rendering keyboard shortcuts as kbd-chip rows — the same pattern Help uses, so the two reference screens read consistently. Section titles use STATE_INFO. - "L" leaderboard row added so the screens list is now complete. - Actions: primary Done button with the M hotkey chip. - handle_home_close_button is the click counterpart to M. Home overlap with Help is intentional during the overhaul — both exist as hotkey references for now. A future commit can repurpose Home as a true mode launcher (the proposal called for this) or remove it entirely if Help is sufficient. Either path is easier with both screens already in the consistent shape. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,18 +7,30 @@ use bevy::input::ButtonInput;
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use solitaire_core::game_state::GameMode;
|
use solitaire_core::game_state::GameMode;
|
||||||
|
|
||||||
|
use crate::font_plugin::FontResource;
|
||||||
use crate::resources::GameStateResource;
|
use crate::resources::GameStateResource;
|
||||||
|
use crate::ui_modal::{
|
||||||
|
spawn_modal, spawn_modal_actions, spawn_modal_button, spawn_modal_header, ButtonVariant,
|
||||||
|
};
|
||||||
|
use crate::ui_theme::{
|
||||||
|
ACCENT_PRIMARY, BORDER_SUBTLE, RADIUS_SM, STATE_INFO, TEXT_PRIMARY, TEXT_SECONDARY, TYPE_BODY,
|
||||||
|
TYPE_BODY_LG, TYPE_CAPTION, VAL_SPACE_1, VAL_SPACE_2, VAL_SPACE_3, Z_MODAL_PANEL,
|
||||||
|
};
|
||||||
|
|
||||||
/// Marker component on the home-menu overlay root node.
|
/// Marker component on the home-menu overlay root node.
|
||||||
#[derive(Component, Debug)]
|
#[derive(Component, Debug)]
|
||||||
pub struct HomeScreen;
|
pub struct HomeScreen;
|
||||||
|
|
||||||
|
/// Marker on the "Done" button inside the Home modal.
|
||||||
|
#[derive(Component, Debug)]
|
||||||
|
pub struct HomeCloseButton;
|
||||||
|
|
||||||
/// Registers the M-key toggle and the overlay spawn/despawn logic.
|
/// Registers the M-key toggle and the overlay spawn/despawn logic.
|
||||||
pub struct HomePlugin;
|
pub struct HomePlugin;
|
||||||
|
|
||||||
impl Plugin for HomePlugin {
|
impl Plugin for HomePlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.add_systems(Update, toggle_home_screen);
|
app.add_systems(Update, (toggle_home_screen, handle_home_close_button));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,6 +38,7 @@ fn toggle_home_screen(
|
|||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
keys: Res<ButtonInput<KeyCode>>,
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
game: Res<GameStateResource>,
|
game: Res<GameStateResource>,
|
||||||
|
font_res: Option<Res<FontResource>>,
|
||||||
screens: Query<Entity, With<HomeScreen>>,
|
screens: Query<Entity, With<HomeScreen>>,
|
||||||
) {
|
) {
|
||||||
if !keys.just_pressed(KeyCode::KeyM) {
|
if !keys.just_pressed(KeyCode::KeyM) {
|
||||||
@@ -34,12 +47,32 @@ fn toggle_home_screen(
|
|||||||
if let Ok(entity) = screens.single() {
|
if let Ok(entity) = screens.single() {
|
||||||
commands.entity(entity).despawn();
|
commands.entity(entity).despawn();
|
||||||
} else {
|
} else {
|
||||||
spawn_home_screen(&mut commands, &game);
|
spawn_home_screen(&mut commands, &game, font_res.as_deref());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawns the full-window home-menu overlay derived from the current `game` state.
|
fn handle_home_close_button(
|
||||||
fn spawn_home_screen(commands: &mut Commands, game: &GameStateResource) {
|
mut commands: Commands,
|
||||||
|
close_buttons: Query<&Interaction, (With<HomeCloseButton>, Changed<Interaction>)>,
|
||||||
|
screens: Query<Entity, With<HomeScreen>>,
|
||||||
|
) {
|
||||||
|
if !close_buttons.iter().any(|i| *i == Interaction::Pressed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for entity in &screens {
|
||||||
|
commands.entity(entity).despawn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawns the home-menu modal — a hotkey reference grouped into "Game
|
||||||
|
/// Controls" and "Screens" sections plus the current game mode badge.
|
||||||
|
/// A future pass can pivot Home into a true mode launcher (the
|
||||||
|
/// Modes-popover already covers that path from the action bar).
|
||||||
|
fn spawn_home_screen(
|
||||||
|
commands: &mut Commands,
|
||||||
|
game: &GameStateResource,
|
||||||
|
font_res: Option<&FontResource>,
|
||||||
|
) {
|
||||||
let mode_label = match game.0.mode {
|
let mode_label = match game.0.mode {
|
||||||
GameMode::Classic => "Classic",
|
GameMode::Classic => "Classic",
|
||||||
GameMode::Zen => "Zen",
|
GameMode::Zen => "Zen",
|
||||||
@@ -47,121 +80,122 @@ fn spawn_home_screen(commands: &mut Commands, game: &GameStateResource) {
|
|||||||
GameMode::TimeAttack => "Time Attack",
|
GameMode::TimeAttack => "Time Attack",
|
||||||
};
|
};
|
||||||
|
|
||||||
commands
|
let font_handle = font_res.map(|f| f.0.clone()).unwrap_or_default();
|
||||||
.spawn((
|
let font_section = TextFont {
|
||||||
HomeScreen,
|
font: font_handle.clone(),
|
||||||
Node {
|
font_size: TYPE_BODY_LG,
|
||||||
position_type: PositionType::Absolute,
|
|
||||||
left: Val::Percent(0.0),
|
|
||||||
top: Val::Percent(0.0),
|
|
||||||
width: Val::Percent(100.0),
|
|
||||||
height: Val::Percent(100.0),
|
|
||||||
flex_direction: FlexDirection::Column,
|
|
||||||
justify_content: JustifyContent::FlexStart,
|
|
||||||
align_items: AlignItems::Center,
|
|
||||||
row_gap: Val::Px(6.0),
|
|
||||||
padding: UiRect::all(Val::Px(24.0)),
|
|
||||||
overflow: Overflow::clip(),
|
|
||||||
..default()
|
..default()
|
||||||
},
|
};
|
||||||
BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.88)),
|
let font_row = TextFont {
|
||||||
ZIndex(200),
|
font: font_handle.clone(),
|
||||||
))
|
font_size: TYPE_BODY,
|
||||||
.with_children(|root| {
|
..default()
|
||||||
// Title
|
};
|
||||||
root.spawn((
|
let font_kbd = TextFont {
|
||||||
Text::new("Solitaire Quest"),
|
font: font_handle,
|
||||||
TextFont { font_size: 48.0, ..default() },
|
font_size: TYPE_CAPTION,
|
||||||
TextColor(Color::srgb(1.0, 0.85, 0.3)),
|
..default()
|
||||||
));
|
};
|
||||||
|
|
||||||
// Mode subtitle
|
spawn_modal(commands, HomeScreen, Z_MODAL_PANEL, |card| {
|
||||||
root.spawn((
|
spawn_modal_header(card, "Solitaire Quest", font_res);
|
||||||
|
|
||||||
|
// Mode badge — current game's mode, ACCENT_PRIMARY so it pops.
|
||||||
|
card.spawn((
|
||||||
Text::new(format!("Current mode: {mode_label}")),
|
Text::new(format!("Current mode: {mode_label}")),
|
||||||
TextFont { font_size: 28.0, ..default() },
|
font_section.clone(),
|
||||||
TextColor(Color::srgb(0.8, 0.8, 0.8)),
|
TextColor(ACCENT_PRIMARY),
|
||||||
));
|
));
|
||||||
|
|
||||||
// Spacer
|
// Game controls section.
|
||||||
root.spawn(Node {
|
card.spawn((
|
||||||
height: Val::Px(8.0),
|
|
||||||
..default()
|
|
||||||
});
|
|
||||||
|
|
||||||
// "Game Controls" section header
|
|
||||||
root.spawn((
|
|
||||||
Text::new("Game Controls"),
|
Text::new("Game Controls"),
|
||||||
TextFont { font_size: 22.0, ..default() },
|
font_section.clone(),
|
||||||
TextColor(Color::srgb(0.9, 0.9, 0.9)),
|
TextColor(STATE_INFO),
|
||||||
));
|
));
|
||||||
|
for (key, action) in [
|
||||||
|
("N", "New game (N again confirms)"),
|
||||||
|
("U", "Undo last move"),
|
||||||
|
("Space / D", "Draw from stock"),
|
||||||
|
("G", "Forfeit current game"),
|
||||||
|
("Tab", "Cycle hint highlight"),
|
||||||
|
("Enter", "Auto-complete if available"),
|
||||||
|
] {
|
||||||
|
spawn_shortcut_row(card, key, action, &font_row, &font_kbd);
|
||||||
|
}
|
||||||
|
|
||||||
spawn_shortcut_row(root, "N", "New game (N again confirms)");
|
// Screens section.
|
||||||
spawn_shortcut_row(root, "U", "Undo last move");
|
card.spawn((
|
||||||
spawn_shortcut_row(root, "Space / D", "Draw from stock");
|
|
||||||
spawn_shortcut_row(root, "G", "Forfeit current game");
|
|
||||||
spawn_shortcut_row(root, "Tab", "Cycle hint highlight");
|
|
||||||
spawn_shortcut_row(root, "Enter", "Auto-complete if available");
|
|
||||||
|
|
||||||
// Spacer
|
|
||||||
root.spawn(Node {
|
|
||||||
height: Val::Px(8.0),
|
|
||||||
..default()
|
|
||||||
});
|
|
||||||
|
|
||||||
// "Screens" section header
|
|
||||||
root.spawn((
|
|
||||||
Text::new("Screens"),
|
Text::new("Screens"),
|
||||||
TextFont { font_size: 22.0, ..default() },
|
font_section.clone(),
|
||||||
TextColor(Color::srgb(0.9, 0.9, 0.9)),
|
TextColor(STATE_INFO),
|
||||||
));
|
));
|
||||||
|
for (key, action) in [
|
||||||
|
("M", "Main menu (this screen)"),
|
||||||
|
("S", "Statistics"),
|
||||||
|
("A", "Achievements"),
|
||||||
|
("O", "Settings"),
|
||||||
|
("P", "Profile"),
|
||||||
|
("L", "Leaderboard"),
|
||||||
|
("F1", "Help"),
|
||||||
|
("F11", "Toggle fullscreen"),
|
||||||
|
("Esc", "Pause / Resume"),
|
||||||
|
] {
|
||||||
|
spawn_shortcut_row(card, key, action, &font_row, &font_kbd);
|
||||||
|
}
|
||||||
|
|
||||||
spawn_shortcut_row(root, "M", "Main menu (this screen)");
|
spawn_modal_actions(card, |actions| {
|
||||||
spawn_shortcut_row(root, "S", "Statistics");
|
spawn_modal_button(
|
||||||
spawn_shortcut_row(root, "A", "Achievements");
|
actions,
|
||||||
spawn_shortcut_row(root, "O", "Settings");
|
HomeCloseButton,
|
||||||
spawn_shortcut_row(root, "P", "Profile");
|
"Done",
|
||||||
spawn_shortcut_row(root, "F1", "Help");
|
Some("M"),
|
||||||
spawn_shortcut_row(root, "F11", "Toggle fullscreen");
|
ButtonVariant::Primary,
|
||||||
spawn_shortcut_row(root, "Esc", "Pause / Resume");
|
font_res,
|
||||||
|
);
|
||||||
// Spacer
|
|
||||||
root.spawn(Node {
|
|
||||||
height: Val::Px(16.0),
|
|
||||||
..default()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Dismiss hint
|
|
||||||
root.spawn((
|
|
||||||
Text::new("Press M to close"),
|
|
||||||
TextFont { font_size: 16.0, ..default() },
|
|
||||||
TextColor(Color::srgb(0.55, 0.55, 0.55)),
|
|
||||||
));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_shortcut_row(parent: &mut ChildSpawnerCommands, key: &str, action: &str) {
|
/// One row inside Home's controls reference: a kbd-chip + description.
|
||||||
|
/// Same look as Help's rows so the two screens read consistently.
|
||||||
|
fn spawn_shortcut_row(
|
||||||
|
parent: &mut ChildSpawnerCommands,
|
||||||
|
key: &str,
|
||||||
|
action: &str,
|
||||||
|
font_row: &TextFont,
|
||||||
|
font_kbd: &TextFont,
|
||||||
|
) {
|
||||||
parent
|
parent
|
||||||
.spawn(Node {
|
.spawn(Node {
|
||||||
flex_direction: FlexDirection::Row,
|
flex_direction: FlexDirection::Row,
|
||||||
align_items: AlignItems::Center,
|
align_items: AlignItems::Center,
|
||||||
min_width: Val::Px(380.0),
|
column_gap: VAL_SPACE_3,
|
||||||
column_gap: Val::Px(16.0),
|
|
||||||
..default()
|
..default()
|
||||||
})
|
})
|
||||||
.with_children(|row| {
|
.with_children(|row| {
|
||||||
row.spawn((
|
row.spawn((
|
||||||
Text::new(key.to_string()),
|
|
||||||
TextFont { font_size: 16.0, ..default() },
|
|
||||||
TextColor(Color::srgb(1.0, 0.85, 0.4)),
|
|
||||||
Node {
|
Node {
|
||||||
min_width: Val::Px(120.0),
|
padding: UiRect::axes(VAL_SPACE_2, VAL_SPACE_1),
|
||||||
|
min_width: Val::Px(80.0),
|
||||||
|
justify_content: JustifyContent::Center,
|
||||||
|
border: UiRect::all(Val::Px(1.0)),
|
||||||
|
border_radius: BorderRadius::all(Val::Px(RADIUS_SM)),
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
|
BorderColor::all(BORDER_SUBTLE),
|
||||||
|
))
|
||||||
|
.with_children(|chip| {
|
||||||
|
chip.spawn((
|
||||||
|
Text::new(key.to_string()),
|
||||||
|
font_kbd.clone(),
|
||||||
|
TextColor(TEXT_PRIMARY),
|
||||||
));
|
));
|
||||||
|
});
|
||||||
row.spawn((
|
row.spawn((
|
||||||
Text::new(action.to_string()),
|
Text::new(action.to_string()),
|
||||||
TextFont { font_size: 16.0, ..default() },
|
font_row.clone(),
|
||||||
TextColor(Color::srgb(0.85, 0.85, 0.85)),
|
TextColor(TEXT_SECONDARY),
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user