fae5933d293dff3f585f6462200dcd54b336f66a
21 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4aafc0a53d |
refactor(engine): name HUD popover Z-layers; replace raw arithmetic (M-24)
ZIndex(Z_HUD + 4) and ZIndex(Z_HUD + 5) across four sites in hud_plugin.rs were magic-number expressions. Define named constants in ui_theme: Z_HUD_POPOVER_BACKDROP = Z_HUD + 4 (fullscreen dismiss backdrop) Z_HUD_POPOVER = Z_HUD + 5 (popover panel) The score-delta floater (Z_HUD + 10) now uses the existing Z_HUD_TOP constant, whose doc is updated to mention transient annotations. Both new constants are added to the monotonic z-hierarchy test. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
c8878d6e8b |
docs(engine): fix stale FOCUS_RING colour comment from Cyan to brick-red (M-23)
The FOCUS_RING constant was updated to match ACCENT_PRIMARY (brick-red, srgb 0.647/0.259/0.259) during the Terminal palette swap but the doc comment still described the old cyan value (rgba 111/194/239). Update the colour name and rgba sample to match the actual constant. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
c50eaf81f7 |
feat(replay): add HC bump for WIN MOVE scrub-bar marker; extend HighContrastBackground
HighContrastBackground gains an optional hc_color field so sites can specify a domain-specific HC variant rather than always bumping to BORDER_SUBTLE_HC (gray). with_default() fills hc_color = BORDER_SUBTLE_HC preserving all existing behaviour; new with_hc(default, hc) lets callers specify both ends. update_high_contrast_backgrounds reads marker.hc_color instead of the hardcoded constant. STATE_SUCCESS_HC (#c8e862, L≈0.73) added to ui_theme — a brighter lime that maintains the success hue while standing out from bumped notch ticks (BORDER_SUBTLE_HC gray, L≈0.60) under HC mode. WIN MOVE marker now carries HighContrastBackground::with_hc(STATE_SUCCESS, STATE_SUCCESS_HC): lime stays lime under HC instead of turning gray. Unit test pins both the default and hc color fields on the spawned marker. 1276 tests pass / 0 failing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
d3cb1a51d4 |
feat(replay): HC-mode coverage for scrub track + notches
The 1 px scrub track and 5 quarter-mark notch ticks paint their shape via BackgroundColor (not BorderColor — they're tiny full-bleed Nodes, not borders on wider containers), so the existing HighContrastBorder marker doesn't apply to them. Add a parallel primitive in ui_theme: HighContrastBackground marker carrying default_color, mirroring HighContrastBorder's shape exactly. Add update_high_contrast_backgrounds system in settings_plugin alongside update_high_contrast_borders — same on/off rule (off → marker.default_color, on → BORDER_SUBTLE_HC), same change-suppression idiom (only mutate when different so Bevy's change-detection doesn't trigger per-frame repaints). Tag the scrub track Node and all five notch Nodes with HighContrastBackground::with_default(BORDER_SUBTLE) so the existing settings repaint cycle picks them up under HC mode. The scrub fill (ACCENT_PRIMARY brick-red) and WIN MOVE marker (STATE_SUCCESS lime-green) don't get the marker — accent and state colours are already saturated and don't need an HC luminance variant. 2 new tests: spawn-time marker presence on the track and cardinality-matches-notch-count on the ticks. Tests: 1250 → 1252. Clippy clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
c9af1ead22 |
feat(accessibility): wire BORDER_SUBTLE_HC into the modal scaffold
Resume-prompt Option E, part 2 of 2 — HC chrome borders. Pairs with the reduce-motion gating in `ed152e2`. v0.21.1 introduced `BORDER_SUBTLE_HC` (#a0a0a0) but never wired it: the constant existed, no consumer used it. Spec at `design-system.md` §Accessibility (#2) mandates outline boost from `#505050` (BORDER_STRONG) to `#a0a0a0` under high-contrast mode so panels and popovers stay legible on low-quality displays. ### Architecture - New `HighContrastBorder` component in `ui_theme` carrying a `default_color: Color` field that records the off-state colour the entity was spawned with. Tag any UI node where border legibility is accessibility-critical. - New `update_high_contrast_borders` system in `settings_plugin` walks all tagged entities each Update tick, sets `BorderColor` to `BORDER_SUBTLE_HC` when `Settings::high_contrast_mode` is on, otherwise to `marker.default_color`. Compares against current `BorderColor` and only mutates when different so Bevy's change-detection doesn't trigger repaints every frame. ### Tagged in this commit - The modal scaffold's card border (`ui_modal::spawn_modal`). This is the primary accessibility target — modals demand attention and a low-vision player needs to perceive the panel boundary. Default colour: `BORDER_STRONG` (#505050); HC variant: `BORDER_SUBTLE_HC` (#a0a0a0). ### Future scope Other `BORDER_SUBTLE` / `BORDER_STRONG` consumer sites (help panel, stats panel, tooltip, action buttons, settings rows, etc.) can be tagged in follow-ups by adding `HighContrastBorder::with_default(...)` to their spawn tuple. The system handles any entity carrying the marker — no further changes needed once a site is tagged. Started small here to keep the commit reviewable and prove the architecture before rolling out broadly. Workspace clippy + cargo test --workspace clean. 1193 passing (unchanged from prior — no new tests added; the system is small enough that the running-game verification is the meaningful check). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
c5787c6953 |
feat(accessibility): wire high-contrast + reduce-motion modes through engine
Resume-prompt Option F, part 1 of 2. Adds two accessibility flags to Settings and threads each through the engine surfaces that react to them. Settings UI toggle rows follow in a separate commit; players who want to test today can edit `settings.json` manually. Spec at `docs/ui-mockups/design-system.md` §Accessibility (#2 and #3). ### High-contrast mode `Settings::high_contrast_mode: bool` (defaults to false; serde- default for back-compat). When on: - Red-suit text colour boosts from `RED_SUIT_COLOUR` (`#fb9fb1`) to a new `RED_SUIT_COLOUR_HC` (`#ff8aa0`). - Black-suit text colour boosts from `BLACK_SUIT_COLOUR` (`#d0d0d0`) to a new `TEXT_PRIMARY_HC` (`#f5f5f5`). - New `BORDER_SUBTLE_HC` (`#a0a0a0`) constant available for future chrome-side wiring (this commit only routes HC through card text rendering — chrome border boost is a separable follow-up). The HC and CBM flags compose. CBM red→lime wins over HC on red suits when both are on (lime is itself a high-luminance accent, so the HC boost has nothing further to do). HC still applies to black suits when both flags are on (CBM doesn't touch black). Four new `text_colour` tests pin the truth table. ### Reduce-motion mode `Settings::reduce_motion_mode: bool` (defaults to false; serde- default for back-compat). When on: - Card-slide animation duration is forced to `0.0` regardless of the player's `AnimSpeed` selection — cards snap instantly to their target position. Implemented by extracting a new `effective_slide_secs(&Settings)` helper that wraps `anim_speed_to_secs` with the reduce-motion gate. - Future scaffolding hooks (splash scanline, warning-chip pulse, card-lift z-bump animation) follow the same `if settings.reduce_motion_mode { skip }` pattern when wired — stays out of scope for this commit since each motion path needs its own per-system gate. Two new tests cover the gate behaviour and the fall-through-to- AnimSpeed pass-through path. ### Threading `text_colour` signature extended with a `high_contrast: bool` parameter; `sync_cards` / `sync_cards_startup` / `sync_cards_on_change` / `sync_cards` core / `spawn_card_entity` / `update_card_entity` all gain a parallel parameter mirroring the existing `color_blind: bool` plumbing. Verbose but matches the established pattern; a future refactor could pack both into an `AccessibilityView` struct, but bigger blast radius. ### Stats 1191 passing / 0 failing across the workspace (net +6 from v0.21.0's 1185 baseline once the icon-pin test landed): - 4 new `text_colour` HC tests in `card_plugin` (red-suit boost, black-suit boost, CBM-wins-on-red, black-suits-with-CBM+HC-still-boost). - 2 new `effective_slide_secs` tests in `animation_plugin` (zero-out under reduce-motion, fall-through to AnimSpeed when off). `cargo clippy --workspace --all-targets -- -D warnings` clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
a292a7ead0 |
feat(engine): swap ACCENT_PRIMARY from cyan #6fc2ef to brick red #a54242
Project-wide palette shift at user request. Replaces the cyan primary accent everywhere it surfaces — splash boot screen, home menu glyphs, action chevrons, replay overlay banner + scrub fill + chip border, achievement checkmarks, leaderboard #1 indicator, radial menu fill, focus ring, card-back canonical badge, etc. — with `#a54242` from the same base16-eighties family as the existing pink suit colour. Knock-on changes that all land in this commit per the lockstep rule: - ui_theme.rs: ACCENT_PRIMARY (#a54242), ACCENT_PRIMARY_HOVER (#c25e5e brightened companion), FOCUS_RING (same hue, 0.85 alpha). Module-level palette comment + STOCK_BADGE_FG + CARD_SHADOW_ALPHA_DRAG doc strings updated to match. - card_plugin.rs: card_back_colour(0) now returns the brick-red ACCENT_PRIMARY (was cyan). RED_SUIT_COLOUR_CBM swapped from cyan to lime #acc267 — the CBM alternative needs to stay hue-distinct from the new red-family primary, lime is the next-best non-red base16-eighties accent. text_colour doc + CBM tests renamed cyan→lime in lockstep (text_colour_color_blind_mode_swaps_red_suits_to_lime). - card_face_svg.rs: BACK_ACCENTS[0] now "#a54242" (canonical Terminal back). - splash_plugin.rs / ui_modal.rs / replay_overlay.rs / selection_plugin.rs: descriptive "cyan" comments swapped to "accent" / "primary-accent" wording so the doc strings stay decoupled from any specific hue. Future palette tweaks won't require comment churn. - design-system.md: YAML token frontmatter updated (primary, surface-tint, suit-red-cb, primary-container, on-primary-container, inverse-primary). Palette table gains a project-specific `base08` slot for the new red. CTA / Selection / Card-back badge / Primary button / Bottom-bar active-icon / glow / CBM swap text all retuned. Historical references preserved (e.g. "Was cyan #6fc2ef before the 2026-05-08 swap") so the audit trail stays in the spec. - card_face_svg_pin.rs: rebaselined. Exactly one hash drift (back_0 — the canonical Terminal back's badge changed colour). Other 56 hashes identical (face SVGs don't reference the accent; back_1..4 use unchanged accents). The one-hash-drift signal confirms the change scope was surgical. Workspace clippy + cargo test --workspace clean, 1184 passing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
0d477ac9fd |
feat(engine): Terminal design-token system in ui_theme
Replaces the prior Premium-Solitaire palette and ad-hoc constants with the full Terminal (base16-eighties) token set: near-black surface ramp, cyan primary CTA, lime/lavender/gold/teal/pink semantic accents, 5-rung type scale, 7-rung 4-multiple spacing scale, 3-step radius, 14-rung z-index hierarchy, and a complete motion budget. Card drop-shadow alphas pinned to 0 — Terminal depth is 1px borders + tonal layering, not box-shadow. Tokens stay as `pub const` so static contexts (default Sprite colours etc.) keep compiling; a future UiTheme resource can layer runtime switching on top without breaking the constant API. Four unit tests pin the spacing/type/z-index invariants so a careless edit can't silently break the scale. Plugin-by-plugin migration to consume these tokens follows in subsequent commits. Spec: docs/ui-mockups/design-system.md Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
ddc8f27c82 |
feat(engine): UX iteration round — tooltip slider, streak fire, score breakdown
Three small UX improvements bundled because they share ui_theme token
edits.
Tooltip-delay slider in Settings → Gameplay
- Settings.tooltip_delay_secs (f32, #[serde(default)] = 0.5) tunable
via "−" / "+" icon buttons next to a value readout. Range
[TOOLTIP_DELAY_MIN_SECS, TOOLTIP_DELAY_MAX_SECS] = [0.0, 1.5] in
TOOLTIP_DELAY_STEP_SECS (0.1) increments. "Instant" label when
value is 0; "{n:.1} s" otherwise.
- ui_tooltip's hover-delay comparison reads from SettingsResource
with MOTION_TOOLTIP_DELAY_SECS as the fallback when the resource
is absent (test path). New tooltip_should_show(elapsed, delay)
pure helper covers the boundary cases.
- adjust_tooltip_delay clamps; sanitized() carries the clamp through
load. Five round-trip / default / legacy-deserialise tests.
Win-streak milestone fire animation
- New WinStreakMilestoneEvent { streak: u32 } fired from stats_plugin
when win_streak_current crosses any of [3, 5, 10] (only the
threshold crossing — not every subsequent win). HUD streak readout
scale-pulses 1.0 → 1.20 → 1.0 over MOTION_STREAK_FLOURISH_SECS
(0.6 s) on receipt; mirrors the foundation-flourish curve shape.
- Three threshold-crossing tests pin the firing contract.
Score-breakdown reveal on the win modal
- Win modal body replaces the single "Score: N" line with a
per-component reveal: Base score, Time bonus (m:ss), No-undo
bonus, Mode multiplier, separator, Total. Rows fade in over
MOTION_SCORE_BREAKDOWN_FADE_SECS (0.12 s) staggered by
MOTION_SCORE_BREAKDOWN_STAGGER_SECS (0.15 s) so the math reads as
it animates. Skipped rows: zero time bonus, undo-tainted no-undo
bonus, multiplier == 1.0.
- Honours AnimSpeed::Instant: rows spawn fully visible, no stagger.
- New ScoreBreakdown::compute helper sources base from
GameWonEvent.score, time bonus from
solitaire_core::scoring::compute_time_bonus, no-undo from a +25
constant when undo_count == 0, mode multiplier from GameMode (Zen
zeros the total). 9 new tests cover the math and the reveal
cadence.
Test count net: +25 across the workspace (1007 → 1031).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
9887343d8b |
feat(engine): focus ring breathes at 1.4 s — gentle pulse instead of flat
The keyboard focus ring rendered as a static yellow outline. A new pulse_focus_overlay system modulates the overlay's BorderColor alpha with a sin curve over MOTION_FOCUS_PULSE_SECS (1.4 s), breathing the visible alpha between 0.65× and 1.0× of FOCUS_RING's native value. The motion is slow enough to read as a calm heartbeat in peripheral vision rather than a competing animation, and a focus change still draws the eye because the ring re-attaches at full brightness on the next pulse cycle. The pulse honours AnimSpeed::Instant by reading SettingsResource and skipping the modulation entirely (static FOCUS_RING colour) for reduced-motion users — matches the convention used elsewhere for animation gating. A pure focus_ring_pulse_factor(elapsed_secs) helper is unit-tested for the curve shape: 0.825 at t=0 (mid-point), 1.0 at the quarter-period peak, 0.65 at the three-quarter-period trough, and a sweep across two full periods stays within the [0.65, 1.0] range. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
525fe0fe76 |
feat(engine): drag-cancel return tween — smooth ease-out instead of shake
Illegal drops previously snapped each dragged card to its origin slot and ran a horizontal ShakeAnim wiggle for negative feedback — which read as punitive on every misclick. The rejection now plays a 150 ms quintic ease-out glide from the drop location back to the resting slot. The audio cue (card_invalid.wav) still fires so the player gets clear "no" feedback; the visual is just gentler. Both rejection paths in input_plugin (mouse end_drag and touch end_drag) construct a CardAnimation::slide(drag_pos → target_pos) with MotionCurve::Responsive — the curve module's own docs recommend Responsive specifically for invalid snap-back because its zero overshoot reads forgiving rather than jittery. card_plugin's update_card_entity gates its snap path on CardAnimation absence so the StateChangedEvent that follows a rejection no longer fights the in-flight tween. Mirrors how resize_cards_in_place already drops in-flight tweens during a window resize. ShakeAnim itself stays in feedback_anim_plugin — the right-click invalid-target and double-click in-place rejection paths still use it because there's no movement to interpolate, just a "no" wiggle. Only the drag-rejection path swaps to the smooth tween. Six new rejection-tween tests pin the contract: CardAnimation is inserted on every dragged card, start/end positions and z values match the drag-to-resting transition, duration matches the new MOTION_DRAG_REJECT_SECS token, and the curve is Responsive. The two legacy ShakeAnim drag-rejection tests are removed since their contract is intentionally inverted by this commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
69ce9afab9 |
feat(engine): foundation completion flourish — King-on-foundation celebration
Now that foundations are unlocked and "completing" one is a real moment (rather than a foregone conclusion based on suit assignment), each Ace-through-King run gets its own small celebration when the King lands. Three layers fire on a single FoundationCompletedEvent emitted by game_plugin's handle_move when a successful move leaves a PileType::Foundation pile holding 13 cards: 1. King card scale-pulse via a new FoundationFlourish component. Triangular curve 1.0 → 1.15 → 1.0 over MOTION_FOUNDATION_FLOURISH _SECS (0.4s) — same shape as the existing ScorePulse so the feel matches. 2. Pile-marker tint flourish via FoundationMarkerFlourish — the foundation marker's sprite colour lerps to STATE_SUCCESS for the first half of the duration then fades back. Reuses the existing success-signal palette; no new colour token. 3. Audio cue: foundation_complete.wav, a synthesised C6→E6→G6 triad with 2nd-harmonic warmth and AR decay (~240 ms). Sits an octave above win_fanfare's root so the layered fourth-completion + win cascade reads cleanly. Generated via solitaire_assetgen's foundation_complete() function and embedded via include_bytes!(). The visual systems run .after(GameMutation) so the post-move pile state is visible when the King is identified. Both flourish components remove themselves once elapsed time exceeds duration — no animation queue or scheduler integration needed. Pure foundation_flourish_scale(elapsed, duration) helper is unit-tested for the curve, edge clamps, and zero-duration safety. Three integration tests on the firing logic verify the event fires exactly once when a King completes a foundation, doesn't fire for non-foundation moves, and doesn't fire when the foundation is at 12 cards. The fourth completion still co-occurs with the win cascade — the two layer cleanly because the flourish's scale is on the King card sprite while the cascade is a screen-shake + per-card rotation, and the foundation_complete ping is a higher octave than the win fanfare's root. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f712b89fe4 |
feat(engine): drop shadows on cards with lifted state during drag
Cards previously read as flat stickers on the felt — no separation cue, no sense the play surface had any depth. Each CardEntity now spawns a CardShadow child sprite: neutral black at 25 % alpha, sized to card_size + 4 px halo, offset (2, -3) and rendered at local z -0.05 so it sits behind its card. Cards in the active drag set switch to a lifted shadow: alpha 40 %, offset (4, -6), padding (8, 8). update_card_shadows_on_drag runs every Update and snaps each shadow to the right state based on DragState membership — no lerp, no animation cost. The pure card_shadow_params(is_dragged) helper is unit-tested for the four parameter values. resize_cards_in_place gains a third query for shadows so the in-place resize keeps shadows cheap (no Sprite regeneration); the shadow's current alpha is read to preserve idle vs lifted padding across a resize. update_card_entity's despawn_related call is followed by a fresh add_card_shadow_child so the shadow re-attaches when the card is repainted (face flip, settings change, theme swap). The pre-existing bulk drag-shadow under the whole lifted stack is untouched — per-card shadows complement it. All shadow values flow through eight new ui_theme tokens (CARD_SHADOW_COLOR, alphas, offsets, paddings, local z) so the visual is tunable in one place. Color is neutral black so the shadows don't conflict with color-blind mode's red/blue suit tints. Four new tests pin the contract: shadow params for idle and drag states, every CardEntity spawns with exactly one CardShadow child, and dragging shifts only the dragged shadow's offset while leaving unrelated shadows on the idle offset. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f6c916641a |
feat(engine): visible drop-target overlay during drag
The existing update_drop_highlights system tinted PileMarker sprites green for valid drops, but the marker is a card-sized rectangle that sits behind the stack. Once a tableau column had any cards on it the marker was occluded and the highlight effectively invisible — the handoff's "drops feel guess-y because there's no preview" point. A new update_drop_target_overlays system spawns an overlay above every legal target during drag: a soft DROP_TARGET_FILL rectangle sized to the pile's actual visible footprint (full fanned column for tableaux, card-sized for foundations and empty tableaux) plus four thin DROP_TARGET_OUTLINE edges forming a 3 px border. Z_DROP_OVERLAY = 50 sits above static cards (z ~1) but below the dragged stack (DRAG_Z = 500), so the overlay never occludes the card the player is holding. The valid-target enumeration mirrors update_drop_highlights exactly so the rules can't drift, and pile geometry mirrors input_plugin's pile_drop_rect. The original marker-tint system is untouched; it still does its job for empty-pile placeholders. The overlay layer is purely additive — running alongside, not replacing. Token values reuse the existing STATE_SUCCESS hue (#4ADE80) at 10% fill / 75% outline so the overlay green matches the rest of the success-signal palette (foundation completion, sync OK, etc.). Three headless tests pin the contract: overlay spawns for valid tableau drops, doesn't spawn for invalid destinations, and despawns the moment the drag ends. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
2c72e1fc87 |
feat(engine): reserve top band for HUD so it stops crowding the cards
Player report: the action button bar (Menu / Undo / Pause / Help / Modes / New Game) and Score / Moves / Timer text were sharing the same vertical band as the stock + foundation row, with no visual separation. The HUD read as part of the play surface. Two-part fix: 1. layout.rs reserves HUD_BAND_HEIGHT (64 px) at the top of the window. Card-grid math takes that off the available vertical budget so cards still fit; top_y shifts down by the same amount. New layout test pins the reservation. Existing worst_case_tableau_fits_vertically tests verify the height-budget arithmetic still holds. 2. hud_plugin.rs spawns a translucent purple band (BG_HUD_BAND, new token in ui_theme.rs at the BG_BASE hue with 0.70 alpha) filling that reserved zone. Z-index sits one rung below Z_HUD so action buttons paint on top while the band reads as their container. The band's bottom edge lines up with the top edge of the highest playable card, so the buttons feel anchored to a "tools strip" rather than floating in the play area. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
5d57b67934 |
feat(engine): branded splash screen on launch
The window previously snapped straight to a card deal, which read more like a prototype than a finished game. SplashPlugin lays a fullscreen overlay (BG_BASE backdrop, ACCENT_PRIMARY title, version subtitle) on top of the gameplay layer for MOTION_SPLASH_TOTAL_SECS — the board deals behind it so the splash dissolve hands off naturally to the deal animation. Visibility curves through fade-in (300ms), hold (~1s), fade-out (300ms) using a pure splash_alpha helper that gets pinned by a unit test rather than wired to the Bevy clock — Time<Virtual>'s 250ms per-tick clamp makes float-tight alpha assertions around the fade boundary brittle. Any keystroke or mouse-button press jumps the age forward to the fade-out window so the splash dissolves immediately. The dismiss handler is read-only on ButtonInput / Touches, so the same press is still visible to gameplay handlers downstream — pressing Space on the splash both dismisses it and triggers the next-tick stock draw, as verified by dismissal_keypress_is_visible_to_other_systems. Z_SPLASH sits above every other UI rung (Z_TOAST + 100) so the splash owns the viewport for its brief lifetime. The hierarchy test was extended to enforce the new rung's monotonic position. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
54d34972d4 |
feat(engine): tooltip infrastructure with hover delay (foundation only)
A new ui_tooltip module owns a Tooltip(Cow<'static, str>) component that turns any UI node into a hover-revealing help target. Bevy 0.18's required-components attribute auto-inserts an Interaction so callers just attach Tooltip and the rest is wired. A single overlay entity is reparented above the focus ring (new Z_TOOLTIP token = Z_FOCUS_RING + 10) and tracked from the hovered target's GlobalTransform + ComputedNode. The chained Update systems start a hover timer on Interaction::Hovered, show the overlay once MOTION_TOOLTIP_DELAY_SECS (0.5s) has elapsed, hide it the moment hover ends, and refresh the text when the hover target switches without an intervening unhover. Tested headless under MinimalPlugins with a 200ms ManualDuration ticker — Bevy clamps Time<Virtual>'s max_delta to 250ms by default, so a one-shot 1s step doesn't actually advance the clock past the threshold; the tests step five times to exercise both pre- and post-delay invariants. This commit ships the infrastructure only — no entity in the engine has Tooltip attached yet. A follow-up applies tooltips to the HUD readouts and action bar. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
12789529a1 |
feat(engine): keyboard focus rings on modal buttons (Phase 1)
Every button spawned via spawn_modal_button is now keyboard-navigable. Tab/Shift-Tab cycles focus within the active modal, Enter activates the focused button via the same Interaction::Pressed signal mouse clicks use, and the primary action auto-focuses on modal open. Mouse clicks transfer focus so the two input modes stay in sync. The visual indicator is a single overlay entity that's reparented above the topmost modal scrim and tracks the focused button's GlobalTransform + ComputedNode each frame. Sitting outside the modal-card subtree means the ring isn't affected by the open animation's 0.96→1.0 scale, and sitting outside any scroll container means it can't be clipped by Settings' Overflow::scroll_y. Z-order sits one rung above Z_MODAL_TOP via the new Z_FOCUS_RING token. Existing 11 modals (Help, Stats, Achievements, Settings, Profile, Leaderboard, Pause, Forfeit confirm, GameOver, Confirm new game, Onboarding, Home) get focus support without any call-site changes — attach_focusable_to_modal_buttons walks the ancestry of any ModalButton lacking Focusable to find its scrim and tags it automatically. selection_plugin's Tab handler keeps working when no modal is open; when one is, focus consumes Tab/Enter before the selection system sees them. Phase 1 scope only — HUD action bar, Home mode cards, and Settings bespoke buttons (icon, swatch, toggle) come in Phase 2/3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
6723416a55 |
feat(engine): convert PauseScreen to modal + add ForfeitConfirmScreen
Pause overlay drops its bespoke full-screen layout and is rebuilt on the standard `ui_modal` scaffold: uniform scrim, centred card, real Resume (Primary, Esc) and Forfeit (Tertiary, G) action buttons. The Draw Mode row stays inline in the body so the existing toggle still fires `SettingsChangedEvent`. The G-key double-press toast countdown is replaced with a real modal: `G` (or clicking Forfeit on Pause) fires the new `ForfeitRequestEvent`, which `PausePlugin` answers by spawning `ForfeitConfirmScreen` at `Z_PAUSE_DIALOG` (above pause). The modal exposes Cancel + "Yes, forfeit" buttons plus Y/Enter/N/Esc accelerators; confirmation despawns both modals, clears `PausedResource`, and fires `ForfeitEvent` for `StatsPlugin`. `toggle_pause` now early-returns when a forfeit modal is visible (and runs `.before(handle_forfeit_keyboard)`) so an Esc that closes the forfeit modal doesn't also re-open pause in the same frame. The legacy `forfeit_countdown` field, `FORFEIT_CONFIRM_WINDOW` constant, and the six pure-function countdown tests are removed; new tests cover the modal-spawn / confirm / cancel paths and the active- game predicate that still gates the G hotkey. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8da62bd05f |
feat(engine): add ui_modal primitive (scaffold + button variants)
Phase 3 step 3 of the UX overhaul. Adds a reusable modal helper that
the next 6 commits use to convert each overlay screen. The audit found
11 overlays using 3 different visual styles with scrim alpha drift
between 0.60 and 0.92; this primitive collapses all of that into one
consistent shape.
API surface:
- spawn_modal(commands, plugin_marker, z, build_card) — full-screen
scrim (uniform SCRIM token) + centred card (BG_ELEVATED, RADIUS_LG,
BORDER_STRONG outline, max-width 720, min-width 360, padding
SPACE_5). Returns the scrim entity for one-call despawn.
- spawn_modal_header(parent, title, font_res) — TYPE_HEADLINE
+ TEXT_PRIMARY, the canonical overlay heading.
- spawn_modal_body_text(parent, text, color, font_res) — TYPE_BODY_LG
paragraph; pass TEXT_PRIMARY or TEXT_SECONDARY.
- spawn_modal_actions(parent, build_buttons) — flex-row
justify-end with margin-top.
- spawn_modal_button(parent, marker, label, hotkey,
variant, font_res) — real Button
entity with optional TYPE_CAPTION hotkey-hint chip.
ButtonVariant enum drives colour:
Primary idle ACCENT_PRIMARY hover ACCENT_PRIMARY_HOVER
pressed ACCENT_SECONDARY (yellow → pink press flash)
Secondary idle BG_ELEVATED_HI hover BG_ELEVATED_TOP
pressed BG_ELEVATED
Tertiary idle BG_ELEVATED hover BG_ELEVATED_HI
pressed BG_ELEVATED_PRESSED
A new BG_ELEVATED_TOP token plus ACCENT_PRIMARY_HOVER cover the new
hover/press combinations cleanly.
UiModalPlugin registers paint_modal_buttons so every ModalButton gets
hover and press feedback automatically — overlay plugins don't add
their own paint systems. Plugin registered in solitaire_app.
A self-test asserts each variant's idle / hover / pressed colours are
all distinct; another verifies the plugin builds under MinimalPlugins.
This commit is purely additive — no overlay calls the new helpers
yet. The next commits convert each overlay to use them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
e14852c093 |
feat(engine): add ui_theme.rs design-token module
Phase 3 step 1 of the UX overhaul. Centralises every UI design token — colours, typography, spacing, border-radius, z-index, and motion durations — so subsequent overhaul commits read from one source of truth instead of scattering hex codes and magic numbers across plugin files. The audit (2026-04-30) found: - 40+ hardcoded Color::srgb literals across UI surfaces. - 12 distinct font sizes (14/15/16/17/18/22/26/28/30/32/40/48 px) with no scale. - 8+ z-index magic numbers across overlay plugins (200, 210, 220, 230, 250, 300, 400) with no documented hierarchy. - Motion durations only partially honouring AnimSpeed — slide and cascade did, but toast / shake / settle / deal were hardcoded. ui_theme.rs collapses these into: - Midnight Purple base (BG_BASE / BG_ELEVATED / BG_ELEVATED_HI / BG_ELEVATED_PRESSED) + Balatro-yellow ACCENT_PRIMARY + warm magenta ACCENT_SECONDARY + state colours (success/warning/danger/ info) + text tiers (primary/secondary/disabled) + a uniform SCRIM. - 5-rung typography scale (display 40 / headline 26 / body-lg 18 / body 14 / caption 11). - 4-multiple spacing scale (4/8/12/16/24/32/48), with VAL_SPACE_* Val::Px convenience constants. - 3 border-radius rungs (sm 4 / md 8 / lg 16). - Documented monotonically-increasing z-index hierarchy enforced by a unit test. - All MOTION_* duration constants funnelled through scaled_duration() so AnimSpeed (Normal/Fast/Instant) applies to every animation, not just slide and cascade. This commit is purely additive — no call sites change yet. Subsequent commits in the overhaul migrate plugins to the tokens one region at a time (HUD restructure, modal primitive, then per- overlay conversions). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |