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>
This commit is contained in:
funman300
2026-05-08 18:19:00 -07:00
parent b44d2777ec
commit c50eaf81f7
3 changed files with 81 additions and 10 deletions
+45 -2
View File
@@ -38,8 +38,8 @@ use solitaire_data::ReplayMove;
use crate::ui_modal::{spawn_modal_button, ButtonVariant}; use crate::ui_modal::{spawn_modal_button, ButtonVariant};
use crate::ui_theme::{ use crate::ui_theme::{
ACCENT_PRIMARY, BG_ELEVATED_HI, BORDER_SUBTLE, HighContrastBackground, HighContrastBorder, ACCENT_PRIMARY, BG_ELEVATED_HI, BORDER_SUBTLE, HighContrastBackground, HighContrastBorder,
STATE_SUCCESS, TEXT_PRIMARY, TEXT_PRIMARY_HC, TEXT_SECONDARY, TYPE_BODY, TYPE_CAPTION, STATE_SUCCESS, STATE_SUCCESS_HC, TEXT_PRIMARY, TEXT_PRIMARY_HC, TEXT_SECONDARY, TYPE_BODY,
TYPE_HEADLINE, VAL_SPACE_1, 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,
}; };
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -779,6 +779,11 @@ fn spawn_overlay(
..default() ..default()
}, },
BackgroundColor(STATE_SUCCESS), BackgroundColor(STATE_SUCCESS),
// HC bump: lime → brighter lime so the win
// marker reads clearly above the bumped
// notch ticks (BORDER_SUBTLE_HC gray) under
// high-contrast mode.
HighContrastBackground::with_hc(STATE_SUCCESS, STATE_SUCCESS_HC),
)); ));
} }
// Fixed quarter-mark notches: five 1px vertical // Fixed quarter-mark notches: five 1px vertical
@@ -2349,6 +2354,44 @@ mod tests {
); );
} }
/// The WIN MOVE marker carries `HighContrastBackground::with_hc(
/// STATE_SUCCESS, STATE_SUCCESS_HC)` so the lime bumps to brighter
/// lime under HC mode rather than to a neutral gray. Pin the
/// presence of the marker so a future refactor can't accidentally
/// drop it and silently regress HC legibility.
#[test]
fn win_move_marker_carries_hc_background_marker() {
let mut app = headless_app();
set_state(
&mut app,
ReplayPlaybackState::Playing {
replay: synthetic_replay(8).with_win_move_index(Some(7)),
cursor: 0,
secs_to_next: 0.5,
paused: false,
},
);
app.update();
let mut q = app
.world_mut()
.query_filtered::<&HighContrastBackground, With<ReplayOverlayWinMoveMarker>>();
let marker = q
.iter(app.world())
.next()
.expect("WIN MOVE marker must carry HighContrastBackground");
assert_eq!(
marker.default_color,
STATE_SUCCESS,
"default colour must be STATE_SUCCESS"
);
assert_eq!(
marker.hc_color,
STATE_SUCCESS_HC,
"HC colour must be STATE_SUCCESS_HC (brighter lime, not gray)"
);
}
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// scrub_notch_positions + ReplayOverlayScrubNotch spawn behaviour // scrub_notch_positions + ReplayOverlayScrubNotch spawn behaviour
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
+1 -1
View File
@@ -701,7 +701,7 @@ pub(crate) fn update_high_contrast_backgrounds(
let high_contrast = settings.0.high_contrast_mode; let high_contrast = settings.0.high_contrast_mode;
for (marker, mut bg) in backgrounds.iter_mut() { for (marker, mut bg) in backgrounds.iter_mut() {
let target = if high_contrast { let target = if high_contrast {
BORDER_SUBTLE_HC marker.hc_color
} else { } else {
marker.default_color marker.default_color
}; };
+35 -7
View File
@@ -93,6 +93,13 @@ pub const ACCENT_SECONDARY: Color = Color::srgb(0.882, 0.639, 0.933);
/// from base16-eighties. `#acc267`. /// from base16-eighties. `#acc267`.
pub const STATE_SUCCESS: Color = Color::srgb(0.675, 0.761, 0.404); pub const STATE_SUCCESS: Color = Color::srgb(0.675, 0.761, 0.404);
/// High-contrast variant of [`STATE_SUCCESS`] — `#c8e862`. Brighter
/// lime that maintains the success hue while lifting luminance from
/// ~0.51 → ~0.73 so the WIN MOVE scrub-bar marker stands out from
/// the bumped notch ticks (`BORDER_SUBTLE_HC` `#a0a0a0`, L≈0.60) in
/// high-contrast mode.
pub const STATE_SUCCESS_HC: Color = Color::srgb(0.784, 0.910, 0.384);
/// Warning — penalty signal, daily-seed expiry countdown, sync-pending /// Warning — penalty signal, daily-seed expiry countdown, sync-pending
/// status. Gold from base16-eighties. **Both** Undo and Recycle /// status. Gold from base16-eighties. **Both** Undo and Recycle
/// counters use this when non-zero. `#ddb26f`. /// counters use this when non-zero. `#ddb26f`.
@@ -260,24 +267,45 @@ impl HighContrastBorder {
/// often render as tiny full-bleed `Node`s, not as borders, so the /// often render as tiny full-bleed `Node`s, not as borders, so the
/// border-marker pattern doesn't apply. /// border-marker pattern doesn't apply.
/// ///
/// `default_color` records the off-state colour the entity was /// `default_color` records the off-state colour; `hc_color` the on-
/// spawned with so the system can revert when HC is toggled back /// state colour. [`with_default`] fills `hc_color` with
/// off. The accompanying paint system is /// [`BORDER_SUBTLE_HC`] so the 90 % of sites that just need the
/// [`update_high_contrast_backgrounds`](crate::settings_plugin::update_high_contrast_backgrounds). /// standard subtle-border bump can continue using a one-argument
/// constructor. [`with_hc`] overrides the HC colour for the rare
/// site (currently only the WIN MOVE scrub-bar marker) that needs a
/// domain-specific HC variant (`STATE_SUCCESS_HC` instead of a gray).
/// ///
/// [`with_default`]: HighContrastBackground::with_default
/// [`with_hc`]: HighContrastBackground::with_hc
/// [`BackgroundColor`]: bevy::prelude::BackgroundColor /// [`BackgroundColor`]: bevy::prelude::BackgroundColor
#[derive(bevy::prelude::Component, Debug, Clone, Copy)] #[derive(bevy::prelude::Component, Debug, Clone, Copy)]
pub struct HighContrastBackground { pub struct HighContrastBackground {
/// Background colour to use when high-contrast mode is *off* — /// Background colour to use when high-contrast mode is *off* —
/// the site's normal idle / active-state colour. /// the site's normal idle / active-state colour.
pub default_color: bevy::prelude::Color, pub default_color: bevy::prelude::Color,
/// Background colour to use when high-contrast mode is *on*.
/// Defaults to [`BORDER_SUBTLE_HC`] via [`with_default`].
///
/// [`with_default`]: HighContrastBackground::with_default
pub hc_color: bevy::prelude::Color,
} }
impl HighContrastBackground { impl HighContrastBackground {
/// Convenience constructor — /// Convenience constructor — HC colour defaults to
/// `HighContrastBackground::with_default(BORDER_SUBTLE)`. /// [`BORDER_SUBTLE_HC`].
pub const fn with_default(default_color: bevy::prelude::Color) -> Self { pub const fn with_default(default_color: bevy::prelude::Color) -> Self {
Self { default_color } Self { default_color, hc_color: BORDER_SUBTLE_HC }
}
/// Constructor for sites whose HC colour differs from the standard
/// [`BORDER_SUBTLE_HC`]. Currently used by the WIN MOVE scrub-bar
/// marker which bumps `STATE_SUCCESS` → `STATE_SUCCESS_HC` rather
/// than to a neutral gray.
pub const fn with_hc(
default_color: bevy::prelude::Color,
hc_color: bevy::prelude::Color,
) -> Self {
Self { default_color, hc_color }
} }
} }