From 9fe650fa2019b5fb745e6db4acf018829b1aebcf Mon Sep 17 00:00:00 2001 From: funman300 Date: Wed, 6 May 2026 16:45:30 +0000 Subject: [PATCH] =?UTF-8?q?feat(engine):=20Home=20picker=20=E2=80=94=202-c?= =?UTF-8?q?olumn=20picture=20tiles=20with=20Unicode=20glyphs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- solitaire_engine/src/home_plugin.rs | 83 ++++++++++++++++++++++++----- 1 file changed, 70 insertions(+), 13 deletions(-) diff --git a/solitaire_engine/src/home_plugin.rs b/solitaire_engine/src/home_plugin.rs index bf67060..b467fde 100644 --- a/solitaire_engine/src/home_plugin.rs +++ b/solitaire_engine/src/home_plugin.rs @@ -39,7 +39,7 @@ use crate::ui_modal::{ use crate::ui_theme::{ 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, - 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, /// shown in a small chip on the card. 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_draw_mode_row(card, &ctx); - for mode in [ - HomeMode::Classic, - HomeMode::Daily, - HomeMode::Zen, - HomeMode::Challenge, - HomeMode::TimeAttack, - ] { - spawn_mode_card(card, mode, &ctx); - } + // Mode tiles in a wrapping 2-column grid. Each tile takes 48% + // of the row so column_gap fits comfortably; the 5 modes wrap + // to a third row of one tile, which we leave left-aligned — + // the asymmetry matches MSSC's "Daily Challenges / Today's + // Event" half-cell on the right of their grid and keeps the + // visual rhythm. + card.spawn(Node { + 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_button( @@ -964,10 +1001,17 @@ fn spawn_mode_card( ..default() }; let font_chip = TextFont { - font: font_handle, + font: font_handle.clone(), font_size: TYPE_CAPTION, ..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 // 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 desc_color = if unlocked { TEXT_SECONDARY } else { TEXT_DISABLED }; let border_color = if unlocked { BORDER_SUBTLE } else { BORDER_STRONG }; + let glyph_color = if unlocked { ACCENT_PRIMARY } else { TEXT_DISABLED }; parent .spawn(( @@ -985,9 +1030,13 @@ fn spawn_mode_card( Button, Node { flex_direction: FlexDirection::Column, - row_gap: VAL_SPACE_1, + row_gap: VAL_SPACE_2, 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_radius: BorderRadius::all(Val::Px(RADIUS_MD)), ..default() @@ -996,12 +1045,20 @@ fn spawn_mode_card( BorderColor::all(border_color), )) .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. c.spawn(Node { flex_direction: FlexDirection::Row, align_items: AlignItems::Center, justify_content: JustifyContent::SpaceBetween, column_gap: VAL_SPACE_3, + width: Val::Percent(100.0), ..default() }) .with_children(|row| {