1use super::*;
2
3#[derive(Debug, Clone, Copy)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10pub struct Theme {
11 pub primary: Color,
13 pub secondary: Color,
15 pub accent: Color,
17 pub text: Color,
19 pub text_dim: Color,
21 pub border: Color,
23 pub bg: Color,
25 pub success: Color,
27 pub warning: Color,
29 pub error: Color,
31 pub selected_bg: Color,
33 pub selected_fg: Color,
35 pub surface: Color,
37 pub surface_hover: Color,
42 pub surface_text: Color,
48 pub is_dark: bool,
50}
51
52impl Theme {
53 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 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 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 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 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 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 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 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
241pub 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 {
264 self.primary = Some(color);
265 self
266 }
267
268 pub fn secondary(mut self, color: Color) -> Self {
270 self.secondary = Some(color);
271 self
272 }
273
274 pub fn accent(mut self, color: Color) -> Self {
276 self.accent = Some(color);
277 self
278 }
279
280 pub fn text(mut self, color: Color) -> Self {
282 self.text = Some(color);
283 self
284 }
285
286 pub fn text_dim(mut self, color: Color) -> Self {
288 self.text_dim = Some(color);
289 self
290 }
291
292 pub fn border(mut self, color: Color) -> Self {
294 self.border = Some(color);
295 self
296 }
297
298 pub fn bg(mut self, color: Color) -> Self {
300 self.bg = Some(color);
301 self
302 }
303
304 pub fn success(mut self, color: Color) -> Self {
306 self.success = Some(color);
307 self
308 }
309
310 pub fn warning(mut self, color: Color) -> Self {
312 self.warning = Some(color);
313 self
314 }
315
316 pub fn error(mut self, color: Color) -> Self {
318 self.error = Some(color);
319 self
320 }
321
322 pub fn selected_bg(mut self, color: Color) -> Self {
324 self.selected_bg = Some(color);
325 self
326 }
327
328 pub fn selected_fg(mut self, color: Color) -> Self {
330 self.selected_fg = Some(color);
331 self
332 }
333
334 pub fn surface(mut self, color: Color) -> Self {
336 self.surface = Some(color);
337 self
338 }
339
340 pub fn surface_hover(mut self, color: Color) -> Self {
342 self.surface_hover = Some(color);
343 self
344 }
345
346 pub fn surface_text(mut self, color: Color) -> Self {
348 self.surface_text = Some(color);
349 self
350 }
351
352 pub fn is_dark(mut self, is_dark: bool) -> Self {
354 self.is_dark = Some(is_dark);
355 self
356 }
357
358 pub fn build(self) -> Theme {
360 let defaults = Theme::dark();
361 Theme {
362 primary: self.primary.unwrap_or(defaults.primary),
363 secondary: self.secondary.unwrap_or(defaults.secondary),
364 accent: self.accent.unwrap_or(defaults.accent),
365 text: self.text.unwrap_or(defaults.text),
366 text_dim: self.text_dim.unwrap_or(defaults.text_dim),
367 border: self.border.unwrap_or(defaults.border),
368 bg: self.bg.unwrap_or(defaults.bg),
369 success: self.success.unwrap_or(defaults.success),
370 warning: self.warning.unwrap_or(defaults.warning),
371 error: self.error.unwrap_or(defaults.error),
372 selected_bg: self.selected_bg.unwrap_or(defaults.selected_bg),
373 selected_fg: self.selected_fg.unwrap_or(defaults.selected_fg),
374 surface: self.surface.unwrap_or(defaults.surface),
375 surface_hover: self.surface_hover.unwrap_or(defaults.surface_hover),
376 surface_text: self.surface_text.unwrap_or(defaults.surface_text),
377 is_dark: self.is_dark.unwrap_or(defaults.is_dark),
378 }
379 }
380}
381
382impl Default for Theme {
383 fn default() -> Self {
384 Self::dark()
385 }
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391
392 #[test]
393 fn theme_dark_preset_builds() {
394 let t = Theme::dark();
395 assert_eq!(t.primary, Color::Cyan);
396 assert!(t.is_dark);
397 }
398
399 #[test]
400 fn theme_light_preset_builds() {
401 let t = Theme::light();
402 assert_eq!(t.selected_fg, Color::White);
403 assert!(!t.is_dark);
404 }
405
406 #[test]
407 fn theme_dracula_preset_builds() {
408 let t = Theme::dracula();
409 assert_eq!(t.bg, Color::Rgb(40, 42, 54));
410 assert!(t.is_dark);
411 }
412
413 #[test]
414 fn theme_catppuccin_preset_builds() {
415 let t = Theme::catppuccin();
416 assert_eq!(t.bg, Color::Rgb(30, 30, 46));
417 assert!(t.is_dark);
418 }
419
420 #[test]
421 fn theme_nord_preset_builds() {
422 let t = Theme::nord();
423 assert_eq!(t.bg, Color::Rgb(46, 52, 64));
424 assert!(t.is_dark);
425 }
426
427 #[test]
428 fn theme_solarized_dark_preset_builds() {
429 let t = Theme::solarized_dark();
430 assert_eq!(t.bg, Color::Rgb(0, 43, 54));
431 assert!(t.is_dark);
432 }
433
434 #[test]
435 fn theme_tokyo_night_preset_builds() {
436 let t = Theme::tokyo_night();
437 assert_eq!(t.bg, Color::Rgb(26, 27, 38));
438 assert!(t.is_dark);
439 }
440
441 #[test]
442 fn theme_builder_sets_primary_and_accent() {
443 let theme = Theme::builder()
444 .primary(Color::Red)
445 .accent(Color::Yellow)
446 .build();
447
448 assert_eq!(theme.primary, Color::Red);
449 assert_eq!(theme.accent, Color::Yellow);
450 }
451
452 #[test]
453 fn theme_builder_defaults_to_dark_for_unset_fields() {
454 let defaults = Theme::dark();
455 let theme = Theme::builder().primary(Color::Green).build();
456
457 assert_eq!(theme.primary, Color::Green);
458 assert_eq!(theme.secondary, defaults.secondary);
459 assert_eq!(theme.text, defaults.text);
460 assert_eq!(theme.text_dim, defaults.text_dim);
461 assert_eq!(theme.border, defaults.border);
462 assert_eq!(theme.surface_hover, defaults.surface_hover);
463 assert_eq!(theme.is_dark, defaults.is_dark);
464 }
465
466 #[test]
467 fn theme_builder_can_override_is_dark() {
468 let theme = Theme::builder().is_dark(false).build();
469 assert!(!theme.is_dark);
470 }
471
472 #[test]
473 fn theme_default_matches_dark() {
474 let default_theme = Theme::default();
475 let dark = Theme::dark();
476 assert_eq!(default_theme.primary, dark.primary);
477 assert_eq!(default_theme.bg, dark.bg);
478 assert_eq!(default_theme.is_dark, dark.is_dark);
479 }
480}