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>
This commit is contained in:
@@ -36,9 +36,9 @@ use crate::replay_playback::{
|
||||
use solitaire_data::ReplayMove;
|
||||
use crate::ui_modal::{spawn_modal_button, ButtonVariant};
|
||||
use crate::ui_theme::{
|
||||
ACCENT_PRIMARY, BG_ELEVATED_HI, BORDER_SUBTLE, HighContrastBorder, STATE_SUCCESS, TEXT_PRIMARY,
|
||||
TEXT_SECONDARY, TYPE_BODY, TYPE_CAPTION, TYPE_HEADLINE, VAL_SPACE_1, VAL_SPACE_2, VAL_SPACE_4,
|
||||
Z_DROP_OVERLAY,
|
||||
ACCENT_PRIMARY, BG_ELEVATED_HI, BORDER_SUBTLE, HighContrastBackground, HighContrastBorder,
|
||||
STATE_SUCCESS, TEXT_PRIMARY, TEXT_SECONDARY, TYPE_BODY, TYPE_CAPTION, TYPE_HEADLINE,
|
||||
VAL_SPACE_1, VAL_SPACE_2, VAL_SPACE_4, Z_DROP_OVERLAY,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -537,6 +537,14 @@ fn spawn_overlay(
|
||||
..default()
|
||||
},
|
||||
BackgroundColor(BORDER_SUBTLE),
|
||||
// HC marker: bumps the 1 px track from #505050
|
||||
// → #a0a0a0 under high-contrast mode. The track
|
||||
// paints via BackgroundColor (it's a 1 px Node,
|
||||
// not a border on a wider container) so the
|
||||
// BorderColor-targeting HighContrastBorder marker
|
||||
// doesn't apply — HighContrastBackground is the
|
||||
// parallel primitive for this case.
|
||||
HighContrastBackground::with_default(BORDER_SUBTLE),
|
||||
))
|
||||
.with_children(|track| {
|
||||
track.spawn((
|
||||
@@ -592,6 +600,12 @@ fn spawn_overlay(
|
||||
..default()
|
||||
},
|
||||
BackgroundColor(BORDER_SUBTLE),
|
||||
// Same HC-paint reasoning as the track
|
||||
// above: 5 px tall × 1 px wide tick mark
|
||||
// paints via BackgroundColor, so
|
||||
// HighContrastBackground (not -Border) is
|
||||
// the right marker.
|
||||
HighContrastBackground::with_default(BORDER_SUBTLE),
|
||||
));
|
||||
}
|
||||
});
|
||||
@@ -1809,6 +1823,74 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Each spawned notch carries `HighContrastBackground` so the
|
||||
/// existing `update_high_contrast_backgrounds` system bumps
|
||||
/// `BORDER_SUBTLE` → `BORDER_SUBTLE_HC` under HC mode.
|
||||
/// Five-of-five — every notch tagged.
|
||||
#[test]
|
||||
fn scrub_notches_carry_high_contrast_background_marker() {
|
||||
let mut app = headless_app();
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 0,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
);
|
||||
app.update();
|
||||
|
||||
let count = app
|
||||
.world_mut()
|
||||
.query_filtered::<&HighContrastBackground, With<ReplayOverlayScrubNotch>>()
|
||||
.iter(app.world())
|
||||
.count();
|
||||
assert_eq!(
|
||||
count,
|
||||
scrub_notch_positions().len(),
|
||||
"every notch must carry HighContrastBackground for HC repaint coverage",
|
||||
);
|
||||
}
|
||||
|
||||
/// The 1 px scrub track also carries `HighContrastBackground` so
|
||||
/// the unfilled portion bumps under HC. The fill (ACCENT_PRIMARY,
|
||||
/// brick-red) doesn't need a marker — accent colours are
|
||||
/// already saturated and don't need an HC variant.
|
||||
#[test]
|
||||
fn scrub_track_carries_high_contrast_background_marker() {
|
||||
let mut app = headless_app();
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(10),
|
||||
cursor: 0,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
);
|
||||
app.update();
|
||||
|
||||
// Track is the parent Node of the scrub-fill. Find it by
|
||||
// walking up from `ReplayOverlayScrubFill` to its parent.
|
||||
let world = app.world_mut();
|
||||
let mut fill_q = world.query_filtered::<Entity, With<ReplayOverlayScrubFill>>();
|
||||
let fill = fill_q
|
||||
.iter(world)
|
||||
.next()
|
||||
.expect("scrub fill must exist while overlay is spawned");
|
||||
let mut parent_q = world.query::<&ChildOf>();
|
||||
let parent = parent_q
|
||||
.get(world, fill)
|
||||
.map(|p| p.parent())
|
||||
.expect("scrub fill must have a parent (the track)");
|
||||
let mut hc_q = world.query::<&HighContrastBackground>();
|
||||
assert!(
|
||||
hc_q.get(world, parent).is_ok(),
|
||||
"scrub track Node (parent of scrub fill) must carry HighContrastBackground",
|
||||
);
|
||||
}
|
||||
|
||||
/// Notches share the overlay tree's lifecycle — they despawn on
|
||||
/// `Playing → Inactive` along with the banner root.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user