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}