Skip to main content

slt/style/
theme.rs

1use super::*;
2
3/// A color theme that flows through all widgets automatically.
4///
5/// Construct with [`Theme::dark()`] or [`Theme::light()`], or build a custom
6/// theme by filling in the fields directly. Pass the theme via [`crate::RunConfig`]
7/// and every widget will pick up the colors without any extra wiring.
8#[derive(Debug, Clone, Copy)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10pub struct Theme {
11    /// Primary accent color, used for focused borders and highlights.
12    pub primary: Color,
13    /// Secondary accent color, used for less prominent highlights.
14    pub secondary: Color,
15    /// Accent color for decorative elements.
16    pub accent: Color,
17    /// Default foreground text color.
18    pub text: Color,
19    /// Dimmed text color for secondary labels and hints.
20    pub text_dim: Color,
21    /// Border color for unfocused containers.
22    pub border: Color,
23    /// Background color. Typically [`Color::Reset`] to inherit the terminal background.
24    pub bg: Color,
25    /// Color for success states (e.g., toast notifications).
26    pub success: Color,
27    /// Color for warning states.
28    pub warning: Color,
29    /// Color for error states.
30    pub error: Color,
31    /// Background color for selected list/table rows.
32    pub selected_bg: Color,
33    /// Foreground color for selected list/table rows.
34    pub selected_fg: Color,
35    /// Subtle surface color for card backgrounds and elevated containers.
36    pub surface: Color,
37    /// Hover/active surface color, one step brighter than `surface`.
38    ///
39    /// Used for interactive element hover states. Should be visually
40    /// distinguishable from both `surface` and `border`.
41    pub surface_hover: Color,
42    /// Secondary text color guaranteed readable on `surface` backgrounds.
43    ///
44    /// Use this instead of `text_dim` when rendering on `surface`-colored
45    /// containers. `text_dim` is tuned for the main `bg`; on `surface` it
46    /// may lack contrast.
47    pub surface_text: Color,
48    /// Whether this theme is a dark theme. Used to initialize dark mode in Context.
49    pub is_dark: bool,
50}
51
52impl Theme {
53    /// Create a dark theme with cyan primary and white text.
54    pub fn dark() -> Self {
55        Self {
56            primary: Color::Cyan,
57            secondary: Color::Blue,
58            accent: Color::Magenta,
59            text: Color::White,
60            text_dim: Color::Indexed(245),
61            border: Color::Indexed(240),
62            bg: Color::Reset,
63            success: Color::Green,
64            warning: Color::Yellow,
65            error: Color::Red,
66            selected_bg: Color::Cyan,
67            selected_fg: Color::Black,
68            surface: Color::Indexed(236),
69            surface_hover: Color::Indexed(238),
70            surface_text: Color::Indexed(250),
71            is_dark: true,
72        }
73    }
74
75    /// Create a light theme with high-contrast dark text on light backgrounds.
76    pub fn light() -> Self {
77        Self {
78            primary: Color::Rgb(37, 99, 235),
79            secondary: Color::Rgb(14, 116, 144),
80            accent: Color::Rgb(147, 51, 234),
81            text: Color::Rgb(15, 23, 42),
82            text_dim: Color::Rgb(100, 116, 139),
83            border: Color::Rgb(203, 213, 225),
84            bg: Color::Rgb(248, 250, 252),
85            success: Color::Rgb(22, 163, 74),
86            warning: Color::Rgb(202, 138, 4),
87            error: Color::Rgb(220, 38, 38),
88            selected_bg: Color::Rgb(37, 99, 235),
89            selected_fg: Color::White,
90            surface: Color::Rgb(241, 245, 249),
91            surface_hover: Color::Rgb(226, 232, 240),
92            surface_text: Color::Rgb(51, 65, 85),
93            is_dark: false,
94        }
95    }
96
97    /// Create a [`ThemeBuilder`] for configuring a custom theme.
98    ///
99    /// # Example
100    ///
101    /// ```
102    /// use slt::{Color, Theme};
103    ///
104    /// let theme = Theme::builder()
105    ///     .primary(Color::Rgb(255, 107, 107))
106    ///     .accent(Color::Cyan)
107    ///     .build();
108    /// ```
109    pub fn builder() -> ThemeBuilder {
110        ThemeBuilder {
111            primary: None,
112            secondary: None,
113            accent: None,
114            text: None,
115            text_dim: None,
116            border: None,
117            bg: None,
118            success: None,
119            warning: None,
120            error: None,
121            selected_bg: None,
122            selected_fg: None,
123            surface: None,
124            surface_hover: None,
125            surface_text: None,
126            is_dark: None,
127        }
128    }
129
130    /// Dracula theme — purple primary on dark gray.
131    pub fn dracula() -> Self {
132        Self {
133            primary: Color::Rgb(189, 147, 249),
134            secondary: Color::Rgb(139, 233, 253),
135            accent: Color::Rgb(255, 121, 198),
136            text: Color::Rgb(248, 248, 242),
137            text_dim: Color::Rgb(98, 114, 164),
138            border: Color::Rgb(68, 71, 90),
139            bg: Color::Rgb(40, 42, 54),
140            success: Color::Rgb(80, 250, 123),
141            warning: Color::Rgb(241, 250, 140),
142            error: Color::Rgb(255, 85, 85),
143            selected_bg: Color::Rgb(189, 147, 249),
144            selected_fg: Color::Rgb(40, 42, 54),
145            surface: Color::Rgb(68, 71, 90),
146            surface_hover: Color::Rgb(98, 100, 120),
147            surface_text: Color::Rgb(191, 194, 210),
148            is_dark: true,
149        }
150    }
151
152    /// Catppuccin Mocha theme — lavender primary on dark base.
153    pub fn catppuccin() -> Self {
154        Self {
155            primary: Color::Rgb(180, 190, 254),
156            secondary: Color::Rgb(137, 180, 250),
157            accent: Color::Rgb(245, 194, 231),
158            text: Color::Rgb(205, 214, 244),
159            text_dim: Color::Rgb(127, 132, 156),
160            border: Color::Rgb(88, 91, 112),
161            bg: Color::Rgb(30, 30, 46),
162            success: Color::Rgb(166, 227, 161),
163            warning: Color::Rgb(249, 226, 175),
164            error: Color::Rgb(243, 139, 168),
165            selected_bg: Color::Rgb(180, 190, 254),
166            selected_fg: Color::Rgb(30, 30, 46),
167            surface: Color::Rgb(49, 50, 68),
168            surface_hover: Color::Rgb(69, 71, 90),
169            surface_text: Color::Rgb(166, 173, 200),
170            is_dark: true,
171        }
172    }
173
174    /// Nord theme — frost blue primary on polar night.
175    pub fn nord() -> Self {
176        Self {
177            primary: Color::Rgb(136, 192, 208),
178            secondary: Color::Rgb(129, 161, 193),
179            accent: Color::Rgb(180, 142, 173),
180            text: Color::Rgb(236, 239, 244),
181            text_dim: Color::Rgb(76, 86, 106),
182            border: Color::Rgb(76, 86, 106),
183            bg: Color::Rgb(46, 52, 64),
184            success: Color::Rgb(163, 190, 140),
185            warning: Color::Rgb(235, 203, 139),
186            error: Color::Rgb(191, 97, 106),
187            selected_bg: Color::Rgb(136, 192, 208),
188            selected_fg: Color::Rgb(46, 52, 64),
189            surface: Color::Rgb(59, 66, 82),
190            surface_hover: Color::Rgb(67, 76, 94),
191            surface_text: Color::Rgb(216, 222, 233),
192            is_dark: true,
193        }
194    }
195
196    /// Solarized Dark theme — blue primary on dark base.
197    pub fn solarized_dark() -> Self {
198        Self {
199            primary: Color::Rgb(38, 139, 210),
200            secondary: Color::Rgb(42, 161, 152),
201            accent: Color::Rgb(211, 54, 130),
202            text: Color::Rgb(131, 148, 150),
203            text_dim: Color::Rgb(88, 110, 117),
204            border: Color::Rgb(88, 110, 117),
205            bg: Color::Rgb(0, 43, 54),
206            success: Color::Rgb(133, 153, 0),
207            warning: Color::Rgb(181, 137, 0),
208            error: Color::Rgb(220, 50, 47),
209            selected_bg: Color::Rgb(38, 139, 210),
210            selected_fg: Color::Rgb(253, 246, 227),
211            surface: Color::Rgb(7, 54, 66),
212            surface_hover: Color::Rgb(23, 72, 85),
213            surface_text: Color::Rgb(147, 161, 161),
214            is_dark: true,
215        }
216    }
217
218    /// Tokyo Night theme — blue primary on dark storm base.
219    pub fn tokyo_night() -> Self {
220        Self {
221            primary: Color::Rgb(122, 162, 247),
222            secondary: Color::Rgb(125, 207, 255),
223            accent: Color::Rgb(187, 154, 247),
224            text: Color::Rgb(169, 177, 214),
225            text_dim: Color::Rgb(86, 95, 137),
226            border: Color::Rgb(54, 58, 79),
227            bg: Color::Rgb(26, 27, 38),
228            success: Color::Rgb(158, 206, 106),
229            warning: Color::Rgb(224, 175, 104),
230            error: Color::Rgb(247, 118, 142),
231            selected_bg: Color::Rgb(122, 162, 247),
232            selected_fg: Color::Rgb(26, 27, 38),
233            surface: Color::Rgb(36, 40, 59),
234            surface_hover: Color::Rgb(41, 46, 66),
235            surface_text: Color::Rgb(192, 202, 245),
236            is_dark: true,
237        }
238    }
239}
240
241/// Builder for creating custom themes with defaults from `Theme::dark()`.
242pub struct ThemeBuilder {
243    primary: Option<Color>,
244    secondary: Option<Color>,
245    accent: Option<Color>,
246    text: Option<Color>,
247    text_dim: Option<Color>,
248    border: Option<Color>,
249    bg: Option<Color>,
250    success: Option<Color>,
251    warning: Option<Color>,
252    error: Option<Color>,
253    selected_bg: Option<Color>,
254    selected_fg: Option<Color>,
255    surface: Option<Color>,
256    surface_hover: Option<Color>,
257    surface_text: Option<Color>,
258    is_dark: Option<bool>,
259}
260
261impl ThemeBuilder {
262    pub fn primary(mut self, color: Color) -> Self {
263        self.primary = Some(color);
264        self
265    }
266
267    pub fn secondary(mut self, color: Color) -> Self {
268        self.secondary = Some(color);
269        self
270    }
271
272    pub fn accent(mut self, color: Color) -> Self {
273        self.accent = Some(color);
274        self
275    }
276
277    pub fn text(mut self, color: Color) -> Self {
278        self.text = Some(color);
279        self
280    }
281
282    pub fn text_dim(mut self, color: Color) -> Self {
283        self.text_dim = Some(color);
284        self
285    }
286
287    pub fn border(mut self, color: Color) -> Self {
288        self.border = Some(color);
289        self
290    }
291
292    pub fn bg(mut self, color: Color) -> Self {
293        self.bg = Some(color);
294        self
295    }
296
297    pub fn success(mut self, color: Color) -> Self {
298        self.success = Some(color);
299        self
300    }
301
302    pub fn warning(mut self, color: Color) -> Self {
303        self.warning = Some(color);
304        self
305    }
306
307    pub fn error(mut self, color: Color) -> Self {
308        self.error = Some(color);
309        self
310    }
311
312    pub fn selected_bg(mut self, color: Color) -> Self {
313        self.selected_bg = Some(color);
314        self
315    }
316
317    pub fn selected_fg(mut self, color: Color) -> Self {
318        self.selected_fg = Some(color);
319        self
320    }
321
322    pub fn surface(mut self, color: Color) -> Self {
323        self.surface = Some(color);
324        self
325    }
326
327    pub fn surface_hover(mut self, color: Color) -> Self {
328        self.surface_hover = Some(color);
329        self
330    }
331
332    pub fn surface_text(mut self, color: Color) -> Self {
333        self.surface_text = Some(color);
334        self
335    }
336
337    pub fn is_dark(mut self, is_dark: bool) -> Self {
338        self.is_dark = Some(is_dark);
339        self
340    }
341
342    pub fn build(self) -> Theme {
343        let defaults = Theme::dark();
344        Theme {
345            primary: self.primary.unwrap_or(defaults.primary),
346            secondary: self.secondary.unwrap_or(defaults.secondary),
347            accent: self.accent.unwrap_or(defaults.accent),
348            text: self.text.unwrap_or(defaults.text),
349            text_dim: self.text_dim.unwrap_or(defaults.text_dim),
350            border: self.border.unwrap_or(defaults.border),
351            bg: self.bg.unwrap_or(defaults.bg),
352            success: self.success.unwrap_or(defaults.success),
353            warning: self.warning.unwrap_or(defaults.warning),
354            error: self.error.unwrap_or(defaults.error),
355            selected_bg: self.selected_bg.unwrap_or(defaults.selected_bg),
356            selected_fg: self.selected_fg.unwrap_or(defaults.selected_fg),
357            surface: self.surface.unwrap_or(defaults.surface),
358            surface_hover: self.surface_hover.unwrap_or(defaults.surface_hover),
359            surface_text: self.surface_text.unwrap_or(defaults.surface_text),
360            is_dark: self.is_dark.unwrap_or(defaults.is_dark),
361        }
362    }
363}
364
365impl Default for Theme {
366    fn default() -> Self {
367        Self::dark()
368    }
369}