From e080b49914f9212f05e1ed3b7fb5fa4a0655c433 Mon Sep 17 00:00:00 2001 From: funman300 Date: Thu, 7 May 2026 22:22:36 -0700 Subject: [PATCH] feat(engine): restyle replay progress text as Terminal MOVE chip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the centre-text half of the replay-overlay enrichments arc. The plain "Move N of M" text becomes a 1px ACCENT_PRIMARY-bordered chip containing "MOVE N/M" — uppercase + slash separator reads as a Terminal output line and matches the floating-chip motif in docs/ui-mockups/replay-overlay-mobile.html. The chip lives in-banner rather than floating above the focused card; the screen-takeover treatment that requires plumbing cursor → card identity remains deferred per SESSION_HANDOFF. Implementation: the centre Text spawn is now wrapped in a Node with 1px border + axes(VAL_SPACE_2, VAL_SPACE_1) padding and no background fill (Terminal aesthetic gets depth from borders + tonal layering, not shadows). The ReplayOverlayProgressText marker stays on the inner Text so update_progress_text continues to repaint contents unchanged. format_progress now returns "MOVE N/M" for Playing and "REPLAY COMPLETE" for Completed (uppercase to match the chip's typographic treatment); Inactive still returns "" since the overlay shouldn't be spawned in that state. Used BorderColor::all(ACCENT_PRIMARY) — Bevy's BorderColor is per-side in 0.18, no longer the tuple struct it was earlier. Module-level docstring + ReplayOverlayScrubFill doc comment both updated to quote the new "MOVE N/M" string. Test overlay_progress_text_reflects_cursor swapped its assertion to match. 1182 tests still pass; clippy clean. This closes Option C from the SESSION_HANDOFF Resume prompt's banner- local enrichments. The full screen-takeover redesign (mini-tableau, playback controls, move-log scroll, WIN MOVE marker requiring a win_move_index field on Replay) remains the multi-session item. --- solitaire_engine/src/replay_overlay.rs | 52 +++++++++++++++++--------- 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/solitaire_engine/src/replay_overlay.rs b/solitaire_engine/src/replay_overlay.rs index 0b66e3f..a444af5 100644 --- a/solitaire_engine/src/replay_overlay.rs +++ b/solitaire_engine/src/replay_overlay.rs @@ -4,8 +4,9 @@ //! //! - A "▌ replay" label on the left so the player knows the surface is //! under playback control rather than live input. -//! - A "Move N of M" progress indicator in the centre, recomputed every -//! frame the cursor advances. +//! - A "MOVE N/M" progress chip in the centre, recomputed every frame +//! the cursor advances and bordered in cyan ACCENT_PRIMARY so it +//! reads as a discrete callout. //! - A "Stop" button on the right that aborts playback and returns //! control to the player. //! @@ -30,7 +31,7 @@ use crate::replay_playback::{stop_replay_playback, ReplayPlaybackState}; use crate::ui_modal::{spawn_modal_button, ButtonVariant}; use crate::ui_theme::{ ACCENT_PRIMARY, BG_ELEVATED_HI, BORDER_SUBTLE, TEXT_PRIMARY, TEXT_SECONDARY, TYPE_BODY, - TYPE_CAPTION, TYPE_HEADLINE, VAL_SPACE_2, VAL_SPACE_4, Z_DROP_OVERLAY, + TYPE_CAPTION, TYPE_HEADLINE, VAL_SPACE_1, VAL_SPACE_2, VAL_SPACE_4, Z_DROP_OVERLAY, }; // --------------------------------------------------------------------------- @@ -110,7 +111,7 @@ pub struct ReplayOverlayGameCaption; /// continuous visual cue of how far through the replay they are. /// /// Distinct from the simpler text-based `ReplayOverlayProgressText` -/// (which spells out "Move N of M"): the scrub fill gives immediate +/// (which spells out "MOVE N/M" in a chip): the scrub fill gives immediate /// at-a-glance positioning; the text gives the exact numbers. Both /// surfaces stay together because they answer the same question for /// players with different scanning preferences. @@ -284,19 +285,33 @@ fn spawn_overlay( )); }); - // Centre: progress readout — neutral primary text - // colour so the eye treats it as data, not a - // callout. + // Centre: progress readout, wrapped in a 1 px + // ACCENT_PRIMARY-bordered chip so it reads as a + // discrete callout rather than free-floating + // text. No fill — the Terminal aesthetic gets + // depth from borders + tonal layering, not + // shadows. The marker stays on the inner Text so + // `update_progress_text` keeps working unchanged. row.spawn(( - ReplayOverlayProgressText, - Text::new(progress_label), - TextFont { - font: font_handle, - font_size: TYPE_BODY, + Node { + border: UiRect::all(Val::Px(1.0)), + padding: UiRect::axes(VAL_SPACE_2, VAL_SPACE_1), ..default() }, - TextColor(TEXT_PRIMARY), - )); + BorderColor::all(ACCENT_PRIMARY), + )) + .with_children(|chip| { + chip.spawn(( + ReplayOverlayProgressText, + Text::new(progress_label), + TextFont { + font: font_handle, + font_size: TYPE_BODY, + ..default() + }, + TextColor(TEXT_PRIMARY), + )); + }); // Right: Stop button. Tertiary variant — the // action is available but not the loudest element @@ -452,8 +467,11 @@ fn format_game_caption(state: &ReplayPlaybackState) -> Option { /// path produce the exact same string. fn format_progress(state: &ReplayPlaybackState) -> String { match state.progress() { - Some((cursor, total)) => format!("Move {cursor} of {total}"), - None if state.is_completed() => "Replay complete".to_string(), + // `MOVE N/M` (uppercase + slash) reads as a Terminal output + // line and matches the floating-chip motif in the mockup at + // `docs/ui-mockups/replay-overlay-mobile.html`. + Some((cursor, total)) => format!("MOVE {cursor}/{total}"), + None if state.is_completed() => "REPLAY COMPLETE".to_string(), None => String::new(), } } @@ -604,7 +622,7 @@ mod tests { ); app.update(); - assert_eq!(progress_text(&mut app), "Move 5 of 10"); + assert_eq!(progress_text(&mut app), "MOVE 5/10"); } /// Pressing the Stop button resets the state back to `Inactive` and