Skip to main content

rio_theme/
contrast.rs

1//! WCAG 2.1 contrast primitives. Cases 1, 4, and 5 all measure
2//! against these โ€” implement once, correctly.
3//!
4//! `relative_luminance` works on linearized sRGB; the conversion
5//! lives in `color::linear_srgb_of` so this file does not duplicate
6//! the gamma curve.
7
8use crate::color::{linear_srgb_of, Color};
9
10/// WCAG 2.1 relative luminance of an sRGB color, range `0.0..=1.0`.
11pub fn relative_luminance(color: &Color) -> f64 {
12    let [r, g, b] = linear_srgb_of(color);
13    0.2126 * r + 0.7152 * g + 0.0722 * b
14}
15
16/// WCAG 2.1 contrast ratio. Range `1.0..=21.0` โ€” identical colors
17/// give 1.0, black-on-white gives 21.0.
18pub fn contrast_ratio(a: &Color, b: &Color) -> f64 {
19    let la = relative_luminance(a);
20    let lb = relative_luminance(b);
21    let (light, dark) = if la >= lb { (la, lb) } else { (lb, la) };
22    (light + 0.05) / (dark + 0.05)
23}
24
25/// AA threshold for normal-sized text.
26pub const AA_TEXT: f64 = 4.5;
27/// AA threshold for large text (>= 18pt regular / 14pt bold).
28pub const AA_LARGE_TEXT: f64 = 3.0;
29/// AA threshold for UI components, icons, borders.
30pub const AA_NON_TEXT: f64 = 3.0;
31
32/// Page background the engine measures the light-mode palette against.
33pub const LIGHT_BG: &str = "#ffffff";
34/// Page background for the future dark mode (ยง8 โ€” emitted today even
35/// though the framework is light-only).
36pub const DARK_BG: &str = "#15161a";
37/// Default near-black text used on light surfaces.
38pub const TEXT_ON_LIGHT: &str = "#1a1a1a";
39/// Default near-white text used on dark surfaces.
40pub const TEXT_ON_DARK: &str = "#f5f5f5";
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45    use crate::color::Color;
46
47    fn c(hex: &str) -> Color {
48        Color::from_hex(hex).unwrap()
49    }
50
51    #[test]
52    fn black_on_white_is_twenty_one() {
53        let r = contrast_ratio(&c("#000000"), &c("#ffffff"));
54        assert!((r - 21.0).abs() < 0.01, "got {r}");
55    }
56
57    #[test]
58    fn identical_colors_yield_one() {
59        let r = contrast_ratio(&c("#3f6089"), &c("#3f6089"));
60        assert!((r - 1.0).abs() < 1e-9);
61    }
62
63    #[test]
64    fn ratio_is_symmetric() {
65        let a = c("#0d9488");
66        let b = c("#ffffff");
67        assert!((contrast_ratio(&a, &b) - contrast_ratio(&b, &a)).abs() < 1e-9);
68    }
69}