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}