Skip to main content

cleansys_gui/
theme.rs

1//! Theme helpers for CleanSys's GUI.
2//!
3//! Colours are derived from the unified `cleansys_core::AppTheme` definitions
4//! so the GUI renders exactly the same 43-theme catalogue as the rest of the
5//! CleanSys ecosystem (and, in spirit, the same catalogue as GitKraft).
6
7use cleansys_core::AppTheme;
8use iced::{Color, Theme};
9
10/// A resolved set of colours derived from the active theme, for the small
11/// number of custom-styled elements in the GUI (badges, cards, status text).
12///
13/// Everything else (buttons, checkboxes, text inputs, pick-lists, scrollbars)
14/// is themed automatically via [`iced_theme_for`], which builds a custom
15/// `iced::Theme::Palette` from the same core theme.
16#[derive(Debug, Clone, Copy)]
17pub struct ThemeColors {
18    pub accent: Color,
19    pub text_primary: Color,
20    pub text_secondary: Color,
21    pub muted: Color,
22    pub bg: Color,
23    pub surface: Color,
24    pub surface_highlight: Color,
25    pub header_bg: Color,
26    pub border: Color,
27    pub selection: Color,
28    pub green: Color,
29    pub red: Color,
30    pub yellow: Color,
31}
32
33fn clamp(v: f32) -> f32 {
34    v.clamp(0.0, 1.0)
35}
36
37/// Shift every RGB channel of `base` by `delta` (positive = lighter, negative = darker).
38fn shift(base: Color, delta: f32) -> Color {
39    Color {
40        r: clamp(base.r + delta),
41        g: clamp(base.g + delta),
42        b: clamp(base.b + delta),
43        a: base.a,
44    }
45}
46
47/// Convert a core [`cleansys_core::Rgb`] to an [`iced::Color`].
48fn rgb_to_iced(rgb: cleansys_core::Rgb) -> Color {
49    Color::from_rgb8(rgb.r, rgb.g, rgb.b)
50}
51
52impl ThemeColors {
53    /// Build a complete GUI colour set from the core's platform-agnostic theme.
54    pub fn from_core(t: &AppTheme) -> Self {
55        let bg = rgb_to_iced(t.background);
56        let surface = rgb_to_iced(t.surface);
57
58        let sign: f32 = if t.is_dark { 1.0 } else { -1.0 };
59        let surface_highlight = shift(surface, sign * 0.04);
60        let header_bg = shift(bg, sign * 0.02);
61
62        Self {
63            accent: rgb_to_iced(t.accent),
64            text_primary: rgb_to_iced(t.text_primary),
65            text_secondary: rgb_to_iced(t.text_secondary),
66            muted: rgb_to_iced(t.text_muted),
67            bg,
68            surface,
69            surface_highlight,
70            header_bg,
71            border: rgb_to_iced(t.border),
72            selection: rgb_to_iced(t.selection),
73            green: rgb_to_iced(t.success),
74            red: rgb_to_iced(t.error),
75            yellow: rgb_to_iced(t.warning),
76        }
77    }
78}
79
80/// Build a custom `iced::Theme` whose palette is derived from the active core
81/// theme, so every built-in Iced widget (checkboxes, buttons, text inputs,
82/// pick-lists, scrollbars) automatically inherits the right colours without
83/// each one needing an explicit `.style()` override.
84pub fn iced_theme_for(index: usize) -> Theme {
85    let core = cleansys_core::theme_by_index(index);
86    let name = cleansys_core::THEME_NAMES
87        .get(index)
88        .copied()
89        .unwrap_or("Default")
90        .to_string();
91
92    let palette = iced::theme::Palette {
93        background: rgb_to_iced(core.background),
94        text: rgb_to_iced(core.text_primary),
95        primary: rgb_to_iced(core.accent),
96        success: rgb_to_iced(core.success),
97        warning: rgb_to_iced(core.warning),
98        danger: rgb_to_iced(core.error),
99    };
100
101    Theme::custom(name, palette)
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn from_core_produces_valid_colors() {
110        for i in 0..cleansys_core::THEME_COUNT {
111            let core = cleansys_core::theme_by_index(i);
112            let colors = ThemeColors::from_core(&core);
113            // Just make sure nothing panics and alpha stays sane.
114            assert!(colors.bg.a > 0.0);
115            assert!(colors.text_primary.a > 0.0);
116        }
117    }
118
119    #[test]
120    fn iced_theme_for_all_indices_does_not_panic() {
121        for i in 0..cleansys_core::THEME_COUNT {
122            let _ = iced_theme_for(i);
123        }
124    }
125
126    #[test]
127    fn iced_theme_for_out_of_range_falls_back_to_default_name() {
128        // theme_by_index falls back to default() for out-of-range, and the
129        // name lookup falls back to "Default" too.
130        let theme = iced_theme_for(9999);
131        assert_eq!(format!("{theme}"), "Default");
132    }
133}