Skip to main content

retroglyph_widgets/
theme.rs

1//! [`Theme`]: named color roles for a light/dark-aware app.
2
3use retroglyph_core::Color;
4
5/// A palette of named color roles, rather than a CSS-style cascade: draw
6/// code picks the role it means (`theme.accent`, `theme.border`) and the
7/// active [`Theme`] decides what color that resolves to.
8///
9/// This crate has no opinion on *how* an app picks between
10/// [`DARK`](Self::DARK) and [`LIGHT`](Self::LIGHT) (a manual toggle key, a
11/// [`SystemTheme`](retroglyph_core::SystemTheme) from
12/// [`Event::ThemeChanged`](retroglyph_core::Event::ThemeChanged), or just
13/// always the same one): it only owns the two palettes themselves, so an
14/// app doesn't have to invent one from scratch.
15///
16/// # Examples
17///
18/// ```
19/// use retroglyph_widgets::Theme;
20///
21/// let theme = Theme::DARK;
22/// assert_eq!(theme.fg, Theme::DARK.fg);
23/// assert_ne!(theme.bg, Theme::LIGHT.bg);
24/// ```
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct Theme {
27    /// The window/screen background, behind every panel.
28    pub bg: Color,
29    /// A panel's own background, layered over `bg`.
30    pub panel_bg: Color,
31    /// Panel borders and dividers.
32    pub border: Color,
33    /// A panel title bar's background.
34    pub title_bg: Color,
35    /// Default (non-emphasized) text.
36    pub fg: Color,
37    /// Emphasis: selection, focus rings, primary actions.
38    pub accent: Color,
39    /// An interactive widget's background while hovered.
40    pub hover_bg: Color,
41    /// An interactive widget's background while pressed.
42    pub press_bg: Color,
43    /// De-emphasized text (hints, secondary labels, disabled-looking text).
44    pub dim: Color,
45}
46
47impl Theme {
48    /// A dark palette: light text on a near-black background.
49    pub const DARK: Self = Self {
50        bg: Color::Rgb {
51            r: 16,
52            g: 16,
53            b: 24,
54        },
55        panel_bg: Color::Rgb {
56            r: 22,
57            g: 22,
58            b: 32,
59        },
60        border: Color::Rgb {
61            r: 70,
62            g: 74,
63            b: 96,
64        },
65        title_bg: Color::Rgb {
66            r: 30,
67            g: 32,
68            b: 48,
69        },
70        fg: Color::Rgb {
71            r: 190,
72            g: 192,
73            b: 208,
74        },
75        accent: Color::Rgb {
76            r: 90,
77            g: 170,
78            b: 250,
79        },
80        hover_bg: Color::Rgb {
81            r: 40,
82            g: 44,
83            b: 64,
84        },
85        press_bg: Color::Rgb {
86            r: 60,
87            g: 110,
88            b: 170,
89        },
90        dim: Color::Rgb {
91            r: 110,
92            g: 112,
93            b: 130,
94        },
95    };
96
97    /// A light palette: dark text on a near-white background. Same role
98    /// relationships as [`DARK`](Self::DARK) (accent stays a legible blue,
99    /// `hover_bg`/`press_bg` stay a step apart from `panel_bg`), inverted
100    /// for contrast against a light background rather than just flipping
101    /// each channel.
102    ///
103    /// Contrast is higher than a typical OS light theme:
104    /// retroglyph's pseudo-graphics (gauges, progress bars, log lines)
105    /// draw with a 2-color paletted look where every panel-bg/border/text
106    /// pair has to be distinct at a glance with no sub-pixel anti-aliasing
107    /// to soften the edges.
108    pub const LIGHT: Self = Self {
109        bg: Color::Rgb {
110            r: 240,
111            g: 240,
112            b: 246,
113        },
114        panel_bg: Color::Rgb {
115            r: 255,
116            g: 255,
117            b: 255,
118        },
119        border: Color::Rgb {
120            r: 160,
121            g: 164,
122            b: 180,
123        },
124        title_bg: Color::Rgb {
125            r: 224,
126            g: 226,
127            b: 240,
128        },
129        fg: Color::Rgb {
130            r: 20,
131            g: 22,
132            b: 32,
133        },
134        accent: Color::Rgb {
135            r: 20,
136            g: 100,
137            b: 210,
138        },
139        hover_bg: Color::Rgb {
140            r: 230,
141            g: 236,
142            b: 248,
143        },
144        press_bg: Color::Rgb {
145            r: 160,
146            g: 194,
147            b: 240,
148        },
149        dim: Color::Rgb {
150            r: 130,
151            g: 132,
152            b: 150,
153        },
154    };
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn dark_and_light_are_distinct() {
163        assert_ne!(Theme::DARK, Theme::LIGHT);
164    }
165
166    #[test]
167    fn dark_background_is_darker_than_light_background() {
168        let Color::Rgb {
169            r: dr,
170            g: dg,
171            b: db,
172        } = Theme::DARK.bg
173        else {
174            unreachable!()
175        };
176        let Color::Rgb {
177            r: lr,
178            g: lg,
179            b: lb,
180        } = Theme::LIGHT.bg
181        else {
182            unreachable!()
183        };
184        let dark_luma = u32::from(dr) + u32::from(dg) + u32::from(db);
185        let light_luma = u32::from(lr) + u32::from(lg) + u32::from(lb);
186        assert!(dark_luma < light_luma);
187    }
188}