diff --git a/solitaire_engine/src/settings_plugin.rs b/solitaire_engine/src/settings_plugin.rs index 0e2de05..abe044c 100644 --- a/solitaire_engine/src/settings_plugin.rs +++ b/solitaire_engine/src/settings_plugin.rs @@ -34,9 +34,9 @@ use crate::ui_modal::{ }; use crate::ui_tooltip::Tooltip; use crate::ui_theme::{ - BG_BASE, BG_ELEVATED, BG_ELEVATED_HI, BORDER_SUBTLE, RADIUS_SM, SPACE_2, STATE_SUCCESS, - TEXT_PRIMARY, TEXT_SECONDARY, TYPE_BODY, TYPE_BODY_LG, TYPE_CAPTION, VAL_SPACE_2, VAL_SPACE_3, - Z_MODAL_PANEL, + BG_BASE, BG_ELEVATED, BG_ELEVATED_HI, BORDER_SUBTLE, BORDER_SUBTLE_HC, HighContrastBorder, + RADIUS_SM, SPACE_2, STATE_SUCCESS, TEXT_PRIMARY, TEXT_SECONDARY, TYPE_BODY, TYPE_BODY_LG, + TYPE_CAPTION, VAL_SPACE_2, VAL_SPACE_3, Z_MODAL_PANEL, }; /// Side length of a swatch button in the card-back / background pickers. @@ -364,6 +364,7 @@ impl Plugin for SettingsPlugin { update_anim_speed_text, update_color_blind_text, update_high_contrast_text, + update_high_contrast_borders, update_reduce_motion_text, update_tooltip_delay_text, update_time_bonus_multiplier_text, @@ -637,6 +638,42 @@ fn update_high_contrast_text( } } +/// Repaints `BorderColor` on every entity tagged with +/// [`HighContrastBorder`] based on `Settings::high_contrast_mode`. +/// Off → the marker's `default_color`; on → `BORDER_SUBTLE_HC` +/// (`#a0a0a0`). Compares against the current border colour and +/// only mutates when different so Bevy's change-detection +/// doesn't trigger repaints every frame. +/// +/// Spec at `design-system.md` §Accessibility (#2): under HC, +/// outlines boost from `#505050` (BORDER_STRONG) to `#a0a0a0` so +/// modal panels, popover edges, and focus-ring carriers stay +/// legible on low-quality displays / for low-vision users. +/// +/// Tagged sites in v0.21.x: the modal scaffold's card border +/// (`ui_modal::spawn_modal`). More sites can be tagged in +/// follow-ups by adding `HighContrastBorder::with_default(...)` +/// to their spawn tuple. +fn update_high_contrast_borders( + settings: Res, + mut borders: Query<(&HighContrastBorder, &mut BorderColor)>, +) { + let high_contrast = settings.0.high_contrast_mode; + for (marker, mut border) in borders.iter_mut() { + let target = if high_contrast { + BORDER_SUBTLE_HC + } else { + marker.default_color + }; + // Only mutate when actually different — avoids per-frame + // change-detection churn. `border.left` is representative + // because every tagged site uses `BorderColor::all(...)`. + if border.left != target { + *border = BorderColor::all(target); + } + } +} + fn update_reduce_motion_text( settings: Res, mut text_nodes: Query<&mut Text, With>, diff --git a/solitaire_engine/src/ui_modal.rs b/solitaire_engine/src/ui_modal.rs index ee1177d..215fdf9 100644 --- a/solitaire_engine/src/ui_modal.rs +++ b/solitaire_engine/src/ui_modal.rs @@ -58,8 +58,9 @@ use crate::settings_plugin::SettingsResource; use crate::ui_theme::{ scaled_duration, ACCENT_PRIMARY, ACCENT_PRIMARY_HOVER, ACCENT_SECONDARY, BG_BASE, BG_ELEVATED, BG_ELEVATED_HI, BG_ELEVATED_PRESSED, BG_ELEVATED_TOP, BORDER_STRONG, BORDER_SUBTLE, - MOTION_MODAL_SECS, RADIUS_LG, RADIUS_MD, SCRIM, TEXT_PRIMARY, TEXT_SECONDARY, TYPE_BODY_LG, - TYPE_CAPTION, TYPE_HEADLINE, VAL_SPACE_2, VAL_SPACE_3, VAL_SPACE_4, VAL_SPACE_5, + HighContrastBorder, MOTION_MODAL_SECS, RADIUS_LG, RADIUS_MD, SCRIM, TEXT_PRIMARY, + TEXT_SECONDARY, TYPE_BODY_LG, TYPE_CAPTION, TYPE_HEADLINE, VAL_SPACE_2, VAL_SPACE_3, + VAL_SPACE_4, VAL_SPACE_5, }; // --------------------------------------------------------------------------- @@ -230,6 +231,13 @@ where Transform::from_scale(Vec3::splat(MODAL_ENTER_START_SCALE)), BackgroundColor(BG_ELEVATED), BorderColor::all(BORDER_STRONG), + // Honour `Settings::high_contrast_mode`: under HC the + // border boosts from `BORDER_STRONG` (#505050) to + // `BORDER_SUBTLE_HC` (#a0a0a0) so the modal panel + // edge stays clearly visible against the scrim and + // surface beneath. `update_high_contrast_borders` in + // `settings_plugin` does the per-frame swap. + HighContrastBorder::with_default(BORDER_STRONG), )) .with_children(build_card); }) diff --git a/solitaire_engine/src/ui_theme.rs b/solitaire_engine/src/ui_theme.rs index cea6a41..032c62a 100644 --- a/solitaire_engine/src/ui_theme.rs +++ b/solitaire_engine/src/ui_theme.rs @@ -226,6 +226,32 @@ pub const BORDER_SUBTLE: Color = Color::srgba(0.208, 0.208, 0.208, 1.0); /// vision users. pub const BORDER_SUBTLE_HC: Color = Color::srgba(0.627, 0.627, 0.627, 1.0); +/// Marker for entities whose [`BorderColor`] should swap to +/// [`BORDER_SUBTLE_HC`] when `Settings::high_contrast_mode` is on. +/// Tag any UI node where border legibility is accessibility-critical +/// — modal panels, popovers, settings rows, focus-ring carriers — +/// then add the `apply_high_contrast_borders` system to react to +/// settings changes. +/// +/// `default_color` records the off-state colour the entity was +/// spawned with so the system can revert when HC is toggled back +/// off. Different sites use different defaults (`BORDER_SUBTLE` for +/// idle popover edges, `BORDER_STRONG` for active modal cards) — the +/// marker captures whichever one applies at this entity. +#[derive(bevy::prelude::Component, Debug, Clone, Copy)] +pub struct HighContrastBorder { + /// Border colour to use when high-contrast mode is *off* — the + /// site's normal idle / active-state colour. + pub default_color: bevy::prelude::Color, +} + +impl HighContrastBorder { + /// Convenience constructor — `HighContrastBorder::with_default(BORDER_SUBTLE)`. + pub const fn with_default(default_color: bevy::prelude::Color) -> Self { + Self { default_color } + } +} + /// Strong border — hover outline, focused button, active popover. /// `outline` from the design system. `#505050`. pub const BORDER_STRONG: Color = Color::srgba(0.314, 0.314, 0.314, 1.0);