feat(engine): Home picker — 2-column picture tiles with Unicode glyphs
Phase B step 2 of the MSSC-inspired Home rework. Mode cards become a wrapping 2-up grid with a centred Unicode-glyph centrepiece per tile, standing in for real per-mode artwork until that lands. - HomeMode::glyph() returns the placeholder codepoint for each mode: ♣ Classic, calendar Daily, cherry-blossom Zen, lightning Challenge, stopwatch TimeAttack. Cherry-blossom is used over lotus-position because the latter renders inconsistently across desktop fonts. - The mode-card loop is wrapped in a FlexWrap::Wrap row container. Tiles set `width: 48%` + `min_height: 180px`; the 5-mode grid wraps to a third row of one tile, mirroring the half-cell asymmetry in MSSC's screenshot. - The glyph paints in ACCENT_PRIMARY when the mode is unlocked and TEXT_DISABLED when locked, so the gate reads at a glance. - When real art lands, swap the Text node for an Image node — the rest of the tile layout, focus order, click handling, and chip rendering are unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,7 +39,7 @@ use crate::ui_modal::{
|
|||||||
use crate::ui_theme::{
|
use crate::ui_theme::{
|
||||||
ACCENT_PRIMARY, BG_ELEVATED, BG_ELEVATED_HI, BORDER_STRONG, BORDER_SUBTLE, RADIUS_MD,
|
ACCENT_PRIMARY, BG_ELEVATED, BG_ELEVATED_HI, BORDER_STRONG, BORDER_SUBTLE, RADIUS_MD,
|
||||||
STATE_INFO, TEXT_DISABLED, TEXT_PRIMARY, TEXT_SECONDARY, TYPE_BODY, TYPE_BODY_LG,
|
STATE_INFO, TEXT_DISABLED, TEXT_PRIMARY, TEXT_SECONDARY, TYPE_BODY, TYPE_BODY_LG,
|
||||||
TYPE_CAPTION, VAL_SPACE_1, VAL_SPACE_2, VAL_SPACE_3, Z_MODAL_PANEL,
|
TYPE_CAPTION, TYPE_DISPLAY, VAL_SPACE_1, VAL_SPACE_2, VAL_SPACE_3, Z_MODAL_PANEL,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -112,6 +112,27 @@ impl HomeMode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Unicode glyph rendered as the picture-tile centrepiece. Stand-in
|
||||||
|
/// for real per-mode artwork — chosen for one-glyph-tells-the-mode
|
||||||
|
/// readability rather than visual fidelity. Swap to `Image` nodes
|
||||||
|
/// when art lands; the rest of the tile layout doesn't change.
|
||||||
|
fn glyph(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
// Black club is the densest card-suit glyph at small sizes.
|
||||||
|
HomeMode::Classic => "\u{2663}",
|
||||||
|
// Calendar emoji — matches the date callout below.
|
||||||
|
HomeMode::Daily => "\u{1F4C5}",
|
||||||
|
// Lotus flower stands in for the lotus-position emoji
|
||||||
|
// because the latter renders inconsistently across
|
||||||
|
// platforms; the flower is a single codepoint.
|
||||||
|
HomeMode::Zen => "\u{1F338}",
|
||||||
|
// High-voltage / lightning bolt for the hardest mode.
|
||||||
|
HomeMode::Challenge => "\u{26A1}",
|
||||||
|
// Stopwatch matches the timer concept of Time Attack.
|
||||||
|
HomeMode::TimeAttack => "\u{23F1}",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The keyboard accelerator that dispatches the same launch event,
|
/// The keyboard accelerator that dispatches the same launch event,
|
||||||
/// shown in a small chip on the card.
|
/// shown in a small chip on the card.
|
||||||
fn hotkey(self) -> &'static str {
|
fn hotkey(self) -> &'static str {
|
||||||
@@ -668,15 +689,31 @@ fn spawn_home_screen(commands: &mut Commands, ctx: HomeContext<'_>) {
|
|||||||
spawn_home_header_chips(card, &ctx);
|
spawn_home_header_chips(card, &ctx);
|
||||||
spawn_draw_mode_row(card, &ctx);
|
spawn_draw_mode_row(card, &ctx);
|
||||||
|
|
||||||
for mode in [
|
// Mode tiles in a wrapping 2-column grid. Each tile takes 48%
|
||||||
HomeMode::Classic,
|
// of the row so column_gap fits comfortably; the 5 modes wrap
|
||||||
HomeMode::Daily,
|
// to a third row of one tile, which we leave left-aligned —
|
||||||
HomeMode::Zen,
|
// the asymmetry matches MSSC's "Daily Challenges / Today's
|
||||||
HomeMode::Challenge,
|
// Event" half-cell on the right of their grid and keeps the
|
||||||
HomeMode::TimeAttack,
|
// visual rhythm.
|
||||||
] {
|
card.spawn(Node {
|
||||||
spawn_mode_card(card, mode, &ctx);
|
flex_direction: FlexDirection::Row,
|
||||||
}
|
flex_wrap: FlexWrap::Wrap,
|
||||||
|
row_gap: VAL_SPACE_3,
|
||||||
|
column_gap: VAL_SPACE_3,
|
||||||
|
width: Val::Percent(100.0),
|
||||||
|
..default()
|
||||||
|
})
|
||||||
|
.with_children(|grid| {
|
||||||
|
for mode in [
|
||||||
|
HomeMode::Classic,
|
||||||
|
HomeMode::Daily,
|
||||||
|
HomeMode::Zen,
|
||||||
|
HomeMode::Challenge,
|
||||||
|
HomeMode::TimeAttack,
|
||||||
|
] {
|
||||||
|
spawn_mode_card(grid, mode, &ctx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
spawn_modal_actions(card, |actions| {
|
spawn_modal_actions(card, |actions| {
|
||||||
spawn_modal_button(
|
spawn_modal_button(
|
||||||
@@ -964,10 +1001,17 @@ fn spawn_mode_card(
|
|||||||
..default()
|
..default()
|
||||||
};
|
};
|
||||||
let font_chip = TextFont {
|
let font_chip = TextFont {
|
||||||
font: font_handle,
|
font: font_handle.clone(),
|
||||||
font_size: TYPE_CAPTION,
|
font_size: TYPE_CAPTION,
|
||||||
..default()
|
..default()
|
||||||
};
|
};
|
||||||
|
// Glyph rendered at display size — Unicode emoji standing in for
|
||||||
|
// the per-mode artwork. Centred at the top of the tile.
|
||||||
|
let font_glyph = TextFont {
|
||||||
|
font: font_handle,
|
||||||
|
font_size: TYPE_DISPLAY,
|
||||||
|
..default()
|
||||||
|
};
|
||||||
|
|
||||||
// Locked cards mute their text to communicate the disabled state at
|
// Locked cards mute their text to communicate the disabled state at
|
||||||
// a glance; the explicit "Unlocks at level N" caption underneath
|
// a glance; the explicit "Unlocks at level N" caption underneath
|
||||||
@@ -975,6 +1019,7 @@ fn spawn_mode_card(
|
|||||||
let title_color = if unlocked { TEXT_PRIMARY } else { TEXT_DISABLED };
|
let title_color = if unlocked { TEXT_PRIMARY } else { TEXT_DISABLED };
|
||||||
let desc_color = if unlocked { TEXT_SECONDARY } else { TEXT_DISABLED };
|
let desc_color = if unlocked { TEXT_SECONDARY } else { TEXT_DISABLED };
|
||||||
let border_color = if unlocked { BORDER_SUBTLE } else { BORDER_STRONG };
|
let border_color = if unlocked { BORDER_SUBTLE } else { BORDER_STRONG };
|
||||||
|
let glyph_color = if unlocked { ACCENT_PRIMARY } else { TEXT_DISABLED };
|
||||||
|
|
||||||
parent
|
parent
|
||||||
.spawn((
|
.spawn((
|
||||||
@@ -985,9 +1030,13 @@ fn spawn_mode_card(
|
|||||||
Button,
|
Button,
|
||||||
Node {
|
Node {
|
||||||
flex_direction: FlexDirection::Column,
|
flex_direction: FlexDirection::Column,
|
||||||
row_gap: VAL_SPACE_1,
|
row_gap: VAL_SPACE_2,
|
||||||
padding: UiRect::all(VAL_SPACE_3),
|
padding: UiRect::all(VAL_SPACE_3),
|
||||||
width: Val::Percent(100.0),
|
// 48% per tile + the row's column_gap = a clean 2-up
|
||||||
|
// grid that wraps to a single tile on the third row.
|
||||||
|
width: Val::Percent(48.0),
|
||||||
|
min_height: Val::Px(180.0),
|
||||||
|
align_items: AlignItems::Center,
|
||||||
border: UiRect::all(Val::Px(1.0)),
|
border: UiRect::all(Val::Px(1.0)),
|
||||||
border_radius: BorderRadius::all(Val::Px(RADIUS_MD)),
|
border_radius: BorderRadius::all(Val::Px(RADIUS_MD)),
|
||||||
..default()
|
..default()
|
||||||
@@ -996,12 +1045,20 @@ fn spawn_mode_card(
|
|||||||
BorderColor::all(border_color),
|
BorderColor::all(border_color),
|
||||||
))
|
))
|
||||||
.with_children(|c| {
|
.with_children(|c| {
|
||||||
|
// Centerpiece glyph — placeholder for real per-mode art.
|
||||||
|
c.spawn((
|
||||||
|
Text::new(mode.glyph().to_string()),
|
||||||
|
font_glyph.clone(),
|
||||||
|
TextColor(glyph_color),
|
||||||
|
));
|
||||||
|
|
||||||
// Title row — title text on the left, hotkey chip on the right.
|
// Title row — title text on the left, hotkey chip on the right.
|
||||||
c.spawn(Node {
|
c.spawn(Node {
|
||||||
flex_direction: FlexDirection::Row,
|
flex_direction: FlexDirection::Row,
|
||||||
align_items: AlignItems::Center,
|
align_items: AlignItems::Center,
|
||||||
justify_content: JustifyContent::SpaceBetween,
|
justify_content: JustifyContent::SpaceBetween,
|
||||||
column_gap: VAL_SPACE_3,
|
column_gap: VAL_SPACE_3,
|
||||||
|
width: Val::Percent(100.0),
|
||||||
..default()
|
..default()
|
||||||
})
|
})
|
||||||
.with_children(|row| {
|
.with_children(|row| {
|
||||||
|
|||||||
Reference in New Issue
Block a user