Skip to main content

jag_ui/
theme.rs

1//! Theme system providing dark and light color palettes for UI elements.
2//!
3//! Uses [`jag_draw::ColorLinPremul`] for color values, constructed via
4//! `ColorLinPremul::from_srgba_u8`.
5
6use jag_draw::ColorLinPremul;
7
8/// Active color mode.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum ThemeMode {
11    Dark,
12    Light,
13}
14
15/// Semantic colors for common UI elements.
16#[derive(Debug, Clone, Copy)]
17pub struct ElementColors {
18    /// Primary text color.
19    pub text: ColorLinPremul,
20    /// Background color for text inputs and text areas.
21    pub input_bg: ColorLinPremul,
22    /// Border color for inputs.
23    pub input_border: ColorLinPremul,
24    /// Background color for buttons.
25    pub button_bg: ColorLinPremul,
26    /// Foreground (text) color for buttons.
27    pub button_fg: ColorLinPremul,
28    /// Color of the keyboard-focus ring.
29    pub focus_ring: ColorLinPremul,
30    /// Color used for error indicators.
31    pub error: ColorLinPremul,
32}
33
34impl ElementColors {
35    /// Return appropriate colors for the given theme mode.
36    pub fn for_theme(mode: ThemeMode) -> Self {
37        match mode {
38            ThemeMode::Dark => Self {
39                text: ColorLinPremul::from_srgba_u8([255, 255, 255, 255]),
40                input_bg: ColorLinPremul::from_srgba_u8([40, 40, 40, 255]),
41                input_border: ColorLinPremul::from_srgba_u8([80, 80, 80, 255]),
42                button_bg: ColorLinPremul::from_srgba_u8([59, 130, 246, 255]),
43                button_fg: ColorLinPremul::from_srgba_u8([255, 255, 255, 255]),
44                focus_ring: ColorLinPremul::from_srgba_u8([59, 130, 246, 255]),
45                error: ColorLinPremul::from_srgba_u8([239, 68, 68, 255]),
46            },
47            ThemeMode::Light => Self {
48                text: ColorLinPremul::from_srgba_u8([15, 23, 42, 255]),
49                input_bg: ColorLinPremul::from_srgba_u8([248, 250, 252, 255]),
50                input_border: ColorLinPremul::from_srgba_u8([203, 213, 225, 255]),
51                button_bg: ColorLinPremul::from_srgba_u8([37, 99, 235, 255]),
52                button_fg: ColorLinPremul::from_srgba_u8([255, 255, 255, 255]),
53                focus_ring: ColorLinPremul::from_srgba_u8([37, 99, 235, 255]),
54                error: ColorLinPremul::from_srgba_u8([220, 38, 38, 255]),
55            },
56        }
57    }
58}
59
60/// Complete theme applied to UI rendering.
61#[derive(Debug, Clone, Copy)]
62pub struct Theme {
63    /// Active color mode.
64    pub mode: ThemeMode,
65    /// Semantic element colors.
66    pub colors: ElementColors,
67    /// Default font size in logical pixels.
68    pub font_size: f32,
69    /// Default border radius in logical pixels.
70    pub border_radius: f32,
71    /// Default spacing between elements in logical pixels.
72    pub spacing: f32,
73}
74
75impl Theme {
76    /// Create the dark theme.
77    pub fn dark() -> Self {
78        Self {
79            mode: ThemeMode::Dark,
80            colors: ElementColors::for_theme(ThemeMode::Dark),
81            font_size: 14.0,
82            border_radius: 6.0,
83            spacing: 8.0,
84        }
85    }
86
87    /// Create the light theme.
88    pub fn light() -> Self {
89        Self {
90            mode: ThemeMode::Light,
91            colors: ElementColors::for_theme(ThemeMode::Light),
92            font_size: 14.0,
93            border_radius: 6.0,
94            spacing: 8.0,
95        }
96    }
97}
98
99impl Default for Theme {
100    fn default() -> Self {
101        Self::dark()
102    }
103}
104
105// ---------------------------------------------------------------------------
106// Tests
107// ---------------------------------------------------------------------------
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn dark_theme_has_dark_mode() {
115        let t = Theme::dark();
116        assert_eq!(t.mode, ThemeMode::Dark);
117    }
118
119    #[test]
120    fn light_theme_has_light_mode() {
121        let t = Theme::light();
122        assert_eq!(t.mode, ThemeMode::Light);
123    }
124
125    #[test]
126    fn default_is_dark() {
127        let t = Theme::default();
128        assert_eq!(t.mode, ThemeMode::Dark);
129    }
130
131    #[test]
132    fn element_colors_for_dark() {
133        let c = ElementColors::for_theme(ThemeMode::Dark);
134        // White text in dark theme.
135        let white = ColorLinPremul::from_srgba_u8([255, 255, 255, 255]);
136        assert_eq!(c.text, white);
137    }
138
139    #[test]
140    fn element_colors_for_light() {
141        let c = ElementColors::for_theme(ThemeMode::Light);
142        // Dark text in light theme.
143        let dark_text = ColorLinPremul::from_srgba_u8([15, 23, 42, 255]);
144        assert_eq!(c.text, dark_text);
145    }
146
147    #[test]
148    fn theme_defaults_reasonable() {
149        let t = Theme::dark();
150        assert!(t.font_size > 0.0);
151        assert!(t.border_radius >= 0.0);
152        assert!(t.spacing > 0.0);
153    }
154}