diff --git a/src/ui/presets.rs b/src/ui/presets.rs index 35843cc2..3f951cf2 100644 --- a/src/ui/presets.rs +++ b/src/ui/presets.rs @@ -591,7 +591,19 @@ fn legible_ink(bg: u32, seed: u32, floor: f32) -> u32 { if contrast(seed, bg) >= floor { return seed; } - let away = if is_dark(bg) { 0xffffff } else { 0x000000 }; + // Whichever extreme the background is *further* from, exactly as + // [`legible_foreground`] picks it — not `is_dark`, whose 0.5 luminance + // threshold is the wrong question here. The two answers only diverge on a + // midtone background (luminance 0.18…0.5), where `is_dark` still says "dark" + // but black outreaches white: an imported scheme on a mid-grey ground would + // have been driven to pure white and clamped there *below* the floor, losing + // the hue and failing the job in one go. Every built-in is far enough from + // the midpoint that this picks what `is_dark` did. + let away = if contrast(0xffffff, bg) >= contrast(0x000000, bg) { + 0xffffff + } else { + 0x000000 + }; bisect_contrast(seed, away, bg, floor) } @@ -1666,6 +1678,27 @@ mod tests { assert_eq!(raise(0x000000, 0x808080, 21.0), 0x808080); } + /// Conditioning has to take the extreme it can actually *reach*, which on a + /// midtone ground is not the one `is_dark`'s 0.5 luminance threshold names. + /// A mid-grey background is "dark" by that test, yet white tops out at + /// 3.95:1 on it while black manages 5.32:1 — so driving toward white would + /// clamp at pure white, below the floor and with the hue thrown away, in the + /// one case where a status colour most needs both. Reachable only for an + /// imported scheme; every built-in sits far enough from the midpoint that + /// this picks the same extreme `is_dark` did. + #[test] + fn semantic_conditioning_survives_a_midtone_background() { + let bg = 0x808080; + for seed in [0xff5555u32, 0x50fa7b, 0xf1fa8c, 0x8be9fd] { + let ink = legible_ink(bg, seed, TEXT_FLOOR); + assert!( + contrast(ink, bg) >= TEXT_FLOOR - 0.01, + "{seed:#08x} conditioned to {ink:#08x}, only {:.2}:1 on a midtone ground", + contrast(ink, bg) + ); + } + } + /// A bad foreground is swapped for a legible black/white; a good one is kept. #[test] fn legible_foreground_rescues_unreadable_text() { diff --git a/src/ui/theme.rs b/src/ui/theme.rs index 4f0b85ea..0669d2df 100644 --- a/src/ui/theme.rs +++ b/src/ui/theme.rs @@ -709,6 +709,28 @@ pub(crate) fn apply_theme(mut window: Option<&mut Window>, cx: &mut App) { t.input = rgb(surfaces.window.selected).into(); t.tokens.input = Hsla::from(rgb(surfaces.window.selected)).into(); + // …but `input` only reaches the *outline* path, which reads the field live. + // The plain (non-outline) button family is derived from `input` **once**, + // inside the `apply_config` that `Theme::change` ran above — i.e. from the + // stock `#2f2f2f`, before any of this function's overrides exist — and a + // snapshot never sees the fix. So a plain `Button` still hovered and pressed + // in that grey, and `Button::selected` (the terminal search bar's `Aa` / `.*` + // toggles, the last two in the app) filled from `tokens.secondary_active` the + // same way: on Dracula, ~1.03:1 against the surface behind it. That is issue + // #197 again, one snapshot removed from the field that fixed it. + // + // Only the *state* rungs move — `tokens.button` (the resting fill) is left + // alone, so a plain button keeps the flat look it has today and only its + // hover/pressed/selected join the ladder. + let button_hover: Hsla = rgb(surfaces.window.hover).into(); + let button_active: Hsla = rgb(surfaces.window.selected).into(); + t.tokens.button_hover = button_hover.into(); + t.tokens.button_active = button_active.into(); + t.tokens.secondary_hover = button_hover.into(); + t.tokens.secondary_active = button_active.into(); + t.tokens.button_secondary_hover = button_hover.into(); + t.tokens.button_secondary_active = button_active.into(); + // Focus rings: the one place the theme's *real* accent belongs. A ring is ink // on the background at 1–2px, which is exactly the job `legible_accent` // conditions the seed for; the stock `neutral-300` was both off-theme and