Skip to main content

rio_theme/
guard.rs

1//! Case 1 — contrast guard for text-on-surface pairings.
2//!
3//! The client's requested text color is treated as a *suggestion*.
4//! When it fails AA against the surface, the engine substitutes a
5//! guaranteed-readable text color rather than emit unreadable HTML.
6//! The rejected color is never destroyed — callers may still place it
7//! on a border or divider.
8
9use crate::color::Color;
10use crate::contrast::{contrast_ratio, AA_TEXT, TEXT_ON_DARK, TEXT_ON_LIGHT};
11
12/// Pick whichever of the two default text colors reads best on
13/// `surface`. Used both as the fallback inside [`resolve_text_token`]
14/// and as a direct utility for "I just need text that works".
15pub fn readable_text(surface: &Color) -> Color {
16    let light = Color::from_hex(TEXT_ON_LIGHT).expect("constant");
17    let dark = Color::from_hex(TEXT_ON_DARK).expect("constant");
18    if contrast_ratio(surface, &light) >= contrast_ratio(surface, &dark) {
19        light
20    } else {
21        dark
22    }
23}
24
25/// If `requested` clears AA on `surface`, return it. Otherwise log a
26/// warning and substitute [`readable_text`].
27pub fn resolve_text_token(surface: &Color, requested: &Color) -> Color {
28    let ratio = contrast_ratio(surface, requested);
29    if ratio >= AA_TEXT {
30        return *requested;
31    }
32    log::warn!(
33        "rio-theme: text {} fails AA on surface {} (ratio {:.2} < {:.1}); substituting readable fallback",
34        requested.to_hex(),
35        surface.to_hex(),
36        ratio,
37        AA_TEXT,
38    );
39    readable_text(surface)
40}
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45
46    fn c(hex: &str) -> Color {
47        Color::from_hex(hex).unwrap()
48    }
49
50    #[test]
51    fn near_black_surface_with_dark_gray_text_falls_back_to_light() {
52        let surface = c("#0a0a0a");
53        let bad_text = c("#222222");
54        let resolved = resolve_text_token(&surface, &bad_text);
55        // Fallback must NOT be the same dark gray.
56        assert_ne!(resolved.to_hex(), bad_text.to_hex());
57        // Must clear AA after substitution.
58        assert!(contrast_ratio(&surface, &resolved) >= AA_TEXT);
59    }
60
61    #[test]
62    fn white_surface_with_dark_text_passes_through() {
63        let surface = c("#ffffff");
64        let text = c("#1a1a1a");
65        assert_eq!(resolve_text_token(&surface, &text).to_hex(), text.to_hex());
66    }
67
68    #[test]
69    fn readable_text_picks_higher_ratio() {
70        let surface = c("#ffffff");
71        let t = readable_text(&surface);
72        // On white, near-black wins.
73        assert_eq!(t.to_hex(), "#1a1a1a");
74    }
75}