Skip to main content

tokenburn_core/features/theme/
mod.rs

1//! Colour themes shared by every front-end.
2
3pub mod presets;
4pub mod types;
5
6pub use presets::*;
7pub use types::*;
8
9#[cfg(test)]
10mod tests {
11    use super::*;
12
13    #[test]
14    fn every_name_has_a_theme_and_the_counts_agree() {
15        assert_eq!(THEME_NAMES.len(), THEME_COUNT);
16        for i in 0..THEME_COUNT {
17            let t = theme_by_index(i);
18            assert_ne!(t.background, t.text_primary, "theme {i} is unreadable");
19            assert_ne!(
20                t.border, t.accent,
21                "theme {i}: border and accent are identical"
22            );
23        }
24    }
25
26    #[test]
27    fn names_are_unique_and_found_case_insensitively() {
28        for (i, n) in THEME_NAMES.iter().enumerate() {
29            assert_eq!(
30                THEME_NAMES.iter().filter(|m| m == &n).count(),
31                1,
32                "duplicate {n}"
33            );
34            assert_eq!(find_theme(&n.to_ascii_uppercase()), Some(i));
35        }
36        assert_eq!(find_theme("  nord  "), find_theme("Nord"));
37        assert_eq!(find_theme("no such theme"), None);
38        assert_eq!(theme_index_by_name("no such theme"), 0);
39    }
40
41    #[test]
42    fn dark_and_light_themes_have_matching_luminance() {
43        // `is_dark` must agree with the actual background so front-ends can rely on it
44        // (CSS `color-scheme`, iced palette).
45        let lum = |c: Rgb| 0.299 * f32::from(c.r) + 0.587 * f32::from(c.g) + 0.114 * f32::from(c.b);
46        for (i, name) in THEME_NAMES.iter().enumerate() {
47            let t = theme_by_index(i);
48            assert_eq!(
49                t.is_dark,
50                lum(t.background) < 128.0,
51                "{name} says is_dark={}",
52                t.is_dark
53            );
54        }
55    }
56
57    #[test]
58    fn there_are_light_and_dark_themes() {
59        let dark = (0..THEME_COUNT)
60            .filter(|i| theme_by_index(*i).is_dark)
61            .count();
62        assert!(dark > 10 && dark < THEME_COUNT - 5);
63    }
64
65    #[test]
66    fn text_contrasts_with_the_background_in_every_theme() {
67        // WCAG-ish: primary text must be clearly distinguishable from the background.
68        let lum = |c: Rgb| 0.299 * f32::from(c.r) + 0.587 * f32::from(c.g) + 0.114 * f32::from(c.b);
69        for (i, name) in THEME_NAMES.iter().enumerate() {
70            let t = theme_by_index(i);
71            let d = (lum(t.text_primary) - lum(t.background)).abs();
72            assert!(
73                d > 90.0,
74                "{name}: text/background luminance gap only {d:.0}"
75            );
76        }
77    }
78
79    #[test]
80    fn hex_and_unit_conversions() {
81        assert_eq!(Rgb::new(255, 0, 16).hex(), "#ff0010");
82        assert_eq!(Rgb::new(255, 0, 0).unit(), (1.0, 0.0, 0.0));
83    }
84
85    #[test]
86    fn every_tool_gets_a_series_colour() {
87        let t = theme_by_index(0);
88        for tool in crate::Tool::ALL {
89            let _ = t.tool_color(tool);
90        }
91        // The eight tools use eight distinct series slots.
92        let idx: std::collections::HashSet<_> = crate::Tool::ALL
93            .iter()
94            .map(|t| t.series_index() % 8)
95            .collect();
96        assert_eq!(idx.len(), crate::Tool::ALL.len());
97    }
98}