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