1use super::*;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21pub struct Spacing {
22 pub base: u32,
24}
25
26impl Spacing {
27 pub const fn new(base: u32) -> Self {
29 Self { base }
30 }
31
32 pub const fn none(&self) -> u32 {
34 0
35 }
36
37 pub const fn xs(&self) -> u32 {
39 self.base
40 }
41
42 pub const fn sm(&self) -> u32 {
44 self.base * 2
45 }
46
47 pub const fn md(&self) -> u32 {
49 self.base * 3
50 }
51
52 pub const fn lg(&self) -> u32 {
54 self.base * 4
55 }
56
57 pub const fn xl(&self) -> u32 {
59 self.base * 6
60 }
61
62 pub const fn xxl(&self) -> u32 {
64 self.base * 8
65 }
66}
67
68impl Default for Spacing {
69 fn default() -> Self {
70 Self { base: 1 }
71 }
72}
73
74#[non_exhaustive]
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
91#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
92pub enum ThemeColor {
93 Primary,
95 Secondary,
97 Accent,
99 Text,
101 TextDim,
103 Border,
105 Bg,
107 Success,
109 Warning,
111 Error,
113 SelectedBg,
115 SelectedFg,
117 Surface,
119 SurfaceHover,
121 SurfaceText,
123 Info,
125 Link,
127 FocusRing,
129 Custom(Color),
131}
132
133#[non_exhaustive]
149#[derive(Debug, Clone, Copy)]
150#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
151pub struct Theme {
152 pub primary: Color,
154 pub secondary: Color,
156 pub accent: Color,
158 pub text: Color,
160 pub text_dim: Color,
162 pub border: Color,
164 pub bg: Color,
166 pub success: Color,
168 pub warning: Color,
170 pub error: Color,
172 pub selected_bg: Color,
174 pub selected_fg: Color,
176 pub surface: Color,
178 pub surface_hover: Color,
183 pub surface_text: Color,
189 pub is_dark: bool,
191 pub spacing: Spacing,
193}
194
195impl Theme {
196 pub fn resolve(&self, token: ThemeColor) -> Color {
198 match token {
199 ThemeColor::Primary => self.primary,
200 ThemeColor::Secondary => self.secondary,
201 ThemeColor::Accent => self.accent,
202 ThemeColor::Text => self.text,
203 ThemeColor::TextDim => self.text_dim,
204 ThemeColor::Border => self.border,
205 ThemeColor::Bg => self.bg,
206 ThemeColor::Success => self.success,
207 ThemeColor::Warning => self.warning,
208 ThemeColor::Error => self.error,
209 ThemeColor::SelectedBg => self.selected_bg,
210 ThemeColor::SelectedFg => self.selected_fg,
211 ThemeColor::Surface => self.surface,
212 ThemeColor::SurfaceHover => self.surface_hover,
213 ThemeColor::SurfaceText => self.surface_text,
214 ThemeColor::Info | ThemeColor::Link | ThemeColor::FocusRing => self.primary,
215 ThemeColor::Custom(c) => c,
216 }
217 }
218
219 pub fn contrast_text_on(&self, bg: Color) -> Color {
223 Color::contrast_fg(bg)
224 }
225
226 pub fn overlay(&self, color: Color, alpha: f32) -> Color {
230 color.blend(self.bg, alpha)
231 }
232
233 pub fn dark() -> Self {
235 Self {
236 primary: Color::Cyan,
237 secondary: Color::Blue,
238 accent: Color::Magenta,
239 text: Color::White,
240 text_dim: Color::Indexed(245),
241 border: Color::Indexed(240),
242 bg: Color::Reset,
243 success: Color::Green,
244 warning: Color::Yellow,
245 error: Color::Red,
246 selected_bg: Color::Cyan,
247 selected_fg: Color::Black,
248 surface: Color::Indexed(236),
249 surface_hover: Color::Indexed(238),
250 surface_text: Color::Indexed(250),
251 is_dark: true,
252 spacing: Spacing::new(1),
253 }
254 }
255
256 pub fn light() -> Self {
258 Self {
259 primary: Color::Rgb(37, 99, 235),
260 secondary: Color::Rgb(14, 116, 144),
261 accent: Color::Rgb(147, 51, 234),
262 text: Color::Rgb(15, 23, 42),
263 text_dim: Color::Rgb(100, 116, 139),
264 border: Color::Rgb(203, 213, 225),
265 bg: Color::Rgb(248, 250, 252),
266 success: Color::Rgb(22, 163, 74),
267 warning: Color::Rgb(202, 138, 4),
268 error: Color::Rgb(220, 38, 38),
269 selected_bg: Color::Rgb(37, 99, 235),
270 selected_fg: Color::White,
271 surface: Color::Rgb(241, 245, 249),
272 surface_hover: Color::Rgb(226, 232, 240),
273 surface_text: Color::Rgb(51, 65, 85),
274 is_dark: false,
275 spacing: Spacing::new(1),
276 }
277 }
278
279 pub fn builder() -> ThemeBuilder {
292 ThemeBuilder {
293 primary: None,
294 secondary: None,
295 accent: None,
296 text: None,
297 text_dim: None,
298 border: None,
299 bg: None,
300 success: None,
301 warning: None,
302 error: None,
303 selected_bg: None,
304 selected_fg: None,
305 surface: None,
306 surface_hover: None,
307 surface_text: None,
308 is_dark: None,
309 spacing: None,
310 }
311 }
312
313 pub fn dracula() -> Self {
315 Self {
316 primary: Color::Rgb(189, 147, 249),
317 secondary: Color::Rgb(139, 233, 253),
318 accent: Color::Rgb(255, 121, 198),
319 text: Color::Rgb(248, 248, 242),
320 text_dim: Color::Rgb(98, 114, 164),
321 border: Color::Rgb(68, 71, 90),
322 bg: Color::Rgb(40, 42, 54),
323 success: Color::Rgb(80, 250, 123),
324 warning: Color::Rgb(241, 250, 140),
325 error: Color::Rgb(255, 85, 85),
326 selected_bg: Color::Rgb(189, 147, 249),
327 selected_fg: Color::Rgb(40, 42, 54),
328 surface: Color::Rgb(68, 71, 90),
329 surface_hover: Color::Rgb(98, 100, 120),
330 surface_text: Color::Rgb(191, 194, 210),
331 is_dark: true,
332 spacing: Spacing::new(1),
333 }
334 }
335
336 pub fn catppuccin() -> Self {
338 Self {
339 primary: Color::Rgb(180, 190, 254),
340 secondary: Color::Rgb(137, 180, 250),
341 accent: Color::Rgb(245, 194, 231),
342 text: Color::Rgb(205, 214, 244),
343 text_dim: Color::Rgb(127, 132, 156),
344 border: Color::Rgb(88, 91, 112),
345 bg: Color::Rgb(30, 30, 46),
346 success: Color::Rgb(166, 227, 161),
347 warning: Color::Rgb(249, 226, 175),
348 error: Color::Rgb(243, 139, 168),
349 selected_bg: Color::Rgb(180, 190, 254),
350 selected_fg: Color::Rgb(30, 30, 46),
351 surface: Color::Rgb(49, 50, 68),
352 surface_hover: Color::Rgb(69, 71, 90),
353 surface_text: Color::Rgb(166, 173, 200),
354 is_dark: true,
355 spacing: Spacing::new(1),
356 }
357 }
358
359 pub fn nord() -> Self {
361 Self {
362 primary: Color::Rgb(136, 192, 208),
363 secondary: Color::Rgb(129, 161, 193),
364 accent: Color::Rgb(180, 142, 173),
365 text: Color::Rgb(236, 239, 244),
366 text_dim: Color::Rgb(76, 86, 106),
367 border: Color::Rgb(76, 86, 106),
368 bg: Color::Rgb(46, 52, 64),
369 success: Color::Rgb(163, 190, 140),
370 warning: Color::Rgb(235, 203, 139),
371 error: Color::Rgb(191, 97, 106),
372 selected_bg: Color::Rgb(136, 192, 208),
373 selected_fg: Color::Rgb(46, 52, 64),
374 surface: Color::Rgb(59, 66, 82),
375 surface_hover: Color::Rgb(67, 76, 94),
376 surface_text: Color::Rgb(216, 222, 233),
377 is_dark: true,
378 spacing: Spacing::new(1),
379 }
380 }
381
382 pub fn solarized_dark() -> Self {
384 Self {
385 primary: Color::Rgb(38, 139, 210),
386 secondary: Color::Rgb(42, 161, 152),
387 accent: Color::Rgb(211, 54, 130),
388 text: Color::Rgb(131, 148, 150),
389 text_dim: Color::Rgb(88, 110, 117),
390 border: Color::Rgb(88, 110, 117),
391 bg: Color::Rgb(0, 43, 54),
392 success: Color::Rgb(133, 153, 0),
393 warning: Color::Rgb(181, 137, 0),
394 error: Color::Rgb(220, 50, 47),
395 selected_bg: Color::Rgb(38, 139, 210),
396 selected_fg: Color::Rgb(253, 246, 227),
397 surface: Color::Rgb(7, 54, 66),
398 surface_hover: Color::Rgb(23, 72, 85),
399 surface_text: Color::Rgb(147, 161, 161),
400 is_dark: true,
401 spacing: Spacing::new(1),
402 }
403 }
404
405 pub fn solarized_light() -> Self {
407 Self {
408 primary: Color::Rgb(38, 139, 210),
409 secondary: Color::Rgb(42, 161, 152),
410 accent: Color::Rgb(211, 54, 130),
411 text: Color::Rgb(101, 123, 131),
412 text_dim: Color::Rgb(147, 161, 161),
413 border: Color::Rgb(147, 161, 161),
414 bg: Color::Rgb(253, 246, 227),
415 success: Color::Rgb(133, 153, 0),
416 warning: Color::Rgb(181, 137, 0),
417 error: Color::Rgb(220, 50, 47),
418 selected_bg: Color::Rgb(38, 139, 210),
419 selected_fg: Color::Rgb(253, 246, 227),
420 surface: Color::Rgb(238, 232, 213),
421 surface_hover: Color::Rgb(227, 221, 201),
422 surface_text: Color::Rgb(88, 110, 117),
423 is_dark: false,
424 spacing: Spacing::new(1),
425 }
426 }
427
428 pub fn tokyo_night() -> Self {
430 Self {
431 primary: Color::Rgb(122, 162, 247),
432 secondary: Color::Rgb(125, 207, 255),
433 accent: Color::Rgb(187, 154, 247),
434 text: Color::Rgb(169, 177, 214),
435 text_dim: Color::Rgb(86, 95, 137),
436 border: Color::Rgb(54, 58, 79),
437 bg: Color::Rgb(26, 27, 38),
438 success: Color::Rgb(158, 206, 106),
439 warning: Color::Rgb(224, 175, 104),
440 error: Color::Rgb(247, 118, 142),
441 selected_bg: Color::Rgb(122, 162, 247),
442 selected_fg: Color::Rgb(26, 27, 38),
443 surface: Color::Rgb(36, 40, 59),
444 surface_hover: Color::Rgb(41, 46, 66),
445 surface_text: Color::Rgb(192, 202, 245),
446 is_dark: true,
447 spacing: Spacing::new(1),
448 }
449 }
450
451 pub fn gruvbox_dark() -> Self {
453 Self {
454 primary: Color::Rgb(215, 153, 33),
455 secondary: Color::Rgb(69, 133, 136),
456 accent: Color::Rgb(177, 98, 134),
457 text: Color::Rgb(235, 219, 178),
458 text_dim: Color::Rgb(146, 131, 116),
459 border: Color::Rgb(80, 73, 69),
460 bg: Color::Rgb(40, 40, 40),
461 success: Color::Rgb(152, 151, 26),
462 warning: Color::Rgb(250, 189, 47),
463 error: Color::Rgb(204, 36, 29),
464 selected_bg: Color::Rgb(215, 153, 33),
465 selected_fg: Color::Rgb(40, 40, 40),
466 surface: Color::Rgb(60, 56, 54),
467 surface_hover: Color::Rgb(80, 73, 69),
468 surface_text: Color::Rgb(189, 174, 147),
469 is_dark: true,
470 spacing: Spacing::new(1),
471 }
472 }
473
474 pub fn one_dark() -> Self {
476 Self {
477 primary: Color::Rgb(97, 175, 239),
478 secondary: Color::Rgb(86, 182, 194),
479 accent: Color::Rgb(198, 120, 221),
480 text: Color::Rgb(171, 178, 191),
481 text_dim: Color::Rgb(92, 99, 112),
482 border: Color::Rgb(62, 68, 81),
483 bg: Color::Rgb(40, 44, 52),
484 success: Color::Rgb(152, 195, 121),
485 warning: Color::Rgb(229, 192, 123),
486 error: Color::Rgb(224, 108, 117),
487 selected_bg: Color::Rgb(97, 175, 239),
488 selected_fg: Color::Rgb(40, 44, 52),
489 surface: Color::Rgb(50, 55, 65),
490 surface_hover: Color::Rgb(62, 68, 81),
491 surface_text: Color::Rgb(152, 159, 172),
492 is_dark: true,
493 spacing: Spacing::new(1),
494 }
495 }
496}
497
498pub struct ThemeBuilder {
500 primary: Option<Color>,
501 secondary: Option<Color>,
502 accent: Option<Color>,
503 text: Option<Color>,
504 text_dim: Option<Color>,
505 border: Option<Color>,
506 bg: Option<Color>,
507 success: Option<Color>,
508 warning: Option<Color>,
509 error: Option<Color>,
510 selected_bg: Option<Color>,
511 selected_fg: Option<Color>,
512 surface: Option<Color>,
513 surface_hover: Option<Color>,
514 surface_text: Option<Color>,
515 is_dark: Option<bool>,
516 spacing: Option<Spacing>,
517}
518
519impl ThemeBuilder {
520 pub fn primary(mut self, color: Color) -> Self {
522 self.primary = Some(color);
523 self
524 }
525
526 pub fn secondary(mut self, color: Color) -> Self {
528 self.secondary = Some(color);
529 self
530 }
531
532 pub fn accent(mut self, color: Color) -> Self {
534 self.accent = Some(color);
535 self
536 }
537
538 pub fn text(mut self, color: Color) -> Self {
540 self.text = Some(color);
541 self
542 }
543
544 pub fn text_dim(mut self, color: Color) -> Self {
546 self.text_dim = Some(color);
547 self
548 }
549
550 pub fn border(mut self, color: Color) -> Self {
552 self.border = Some(color);
553 self
554 }
555
556 pub fn bg(mut self, color: Color) -> Self {
558 self.bg = Some(color);
559 self
560 }
561
562 pub fn success(mut self, color: Color) -> Self {
564 self.success = Some(color);
565 self
566 }
567
568 pub fn warning(mut self, color: Color) -> Self {
570 self.warning = Some(color);
571 self
572 }
573
574 pub fn error(mut self, color: Color) -> Self {
576 self.error = Some(color);
577 self
578 }
579
580 pub fn selected_bg(mut self, color: Color) -> Self {
582 self.selected_bg = Some(color);
583 self
584 }
585
586 pub fn selected_fg(mut self, color: Color) -> Self {
588 self.selected_fg = Some(color);
589 self
590 }
591
592 pub fn surface(mut self, color: Color) -> Self {
594 self.surface = Some(color);
595 self
596 }
597
598 pub fn surface_hover(mut self, color: Color) -> Self {
600 self.surface_hover = Some(color);
601 self
602 }
603
604 pub fn surface_text(mut self, color: Color) -> Self {
606 self.surface_text = Some(color);
607 self
608 }
609
610 pub fn is_dark(mut self, is_dark: bool) -> Self {
612 self.is_dark = Some(is_dark);
613 self
614 }
615
616 pub fn spacing(mut self, spacing: Spacing) -> Self {
618 self.spacing = Some(spacing);
619 self
620 }
621
622 pub fn build(self) -> Theme {
624 let defaults = Theme::dark();
625 Theme {
626 primary: self.primary.unwrap_or(defaults.primary),
627 secondary: self.secondary.unwrap_or(defaults.secondary),
628 accent: self.accent.unwrap_or(defaults.accent),
629 text: self.text.unwrap_or(defaults.text),
630 text_dim: self.text_dim.unwrap_or(defaults.text_dim),
631 border: self.border.unwrap_or(defaults.border),
632 bg: self.bg.unwrap_or(defaults.bg),
633 success: self.success.unwrap_or(defaults.success),
634 warning: self.warning.unwrap_or(defaults.warning),
635 error: self.error.unwrap_or(defaults.error),
636 selected_bg: self.selected_bg.unwrap_or(defaults.selected_bg),
637 selected_fg: self.selected_fg.unwrap_or(defaults.selected_fg),
638 surface: self.surface.unwrap_or(defaults.surface),
639 surface_hover: self.surface_hover.unwrap_or(defaults.surface_hover),
640 surface_text: self.surface_text.unwrap_or(defaults.surface_text),
641 is_dark: self.is_dark.unwrap_or(defaults.is_dark),
642 spacing: self.spacing.unwrap_or(defaults.spacing),
643 }
644 }
645}
646
647impl Default for Theme {
648 fn default() -> Self {
649 Self::dark()
650 }
651}
652
653#[cfg(test)]
654mod tests {
655 use super::*;
656
657 #[test]
658 fn theme_dark_preset_builds() {
659 let t = Theme::dark();
660 assert_eq!(t.primary, Color::Cyan);
661 assert!(t.is_dark);
662 }
663
664 #[test]
665 fn theme_light_preset_builds() {
666 let t = Theme::light();
667 assert_eq!(t.selected_fg, Color::White);
668 assert!(!t.is_dark);
669 }
670
671 #[test]
672 fn theme_dracula_preset_builds() {
673 let t = Theme::dracula();
674 assert_eq!(t.bg, Color::Rgb(40, 42, 54));
675 assert!(t.is_dark);
676 }
677
678 #[test]
679 fn theme_catppuccin_preset_builds() {
680 let t = Theme::catppuccin();
681 assert_eq!(t.bg, Color::Rgb(30, 30, 46));
682 assert!(t.is_dark);
683 }
684
685 #[test]
686 fn theme_nord_preset_builds() {
687 let t = Theme::nord();
688 assert_eq!(t.bg, Color::Rgb(46, 52, 64));
689 assert!(t.is_dark);
690 }
691
692 #[test]
693 fn theme_solarized_dark_preset_builds() {
694 let t = Theme::solarized_dark();
695 assert_eq!(t.bg, Color::Rgb(0, 43, 54));
696 assert!(t.is_dark);
697 }
698
699 #[test]
700 fn theme_tokyo_night_preset_builds() {
701 let t = Theme::tokyo_night();
702 assert_eq!(t.bg, Color::Rgb(26, 27, 38));
703 assert!(t.is_dark);
704 }
705
706 #[test]
707 fn theme_builder_sets_primary_and_accent() {
708 let theme = Theme::builder()
709 .primary(Color::Red)
710 .accent(Color::Yellow)
711 .build();
712
713 assert_eq!(theme.primary, Color::Red);
714 assert_eq!(theme.accent, Color::Yellow);
715 }
716
717 #[test]
718 fn theme_builder_defaults_to_dark_for_unset_fields() {
719 let defaults = Theme::dark();
720 let theme = Theme::builder().primary(Color::Green).build();
721
722 assert_eq!(theme.primary, Color::Green);
723 assert_eq!(theme.secondary, defaults.secondary);
724 assert_eq!(theme.text, defaults.text);
725 assert_eq!(theme.text_dim, defaults.text_dim);
726 assert_eq!(theme.border, defaults.border);
727 assert_eq!(theme.surface_hover, defaults.surface_hover);
728 assert_eq!(theme.is_dark, defaults.is_dark);
729 }
730
731 #[test]
732 fn theme_builder_can_override_is_dark() {
733 let theme = Theme::builder().is_dark(false).build();
734 assert!(!theme.is_dark);
735 }
736
737 #[test]
738 fn theme_default_matches_dark() {
739 let default_theme = Theme::default();
740 let dark = Theme::dark();
741 assert_eq!(default_theme.primary, dark.primary);
742 assert_eq!(default_theme.bg, dark.bg);
743 assert_eq!(default_theme.is_dark, dark.is_dark);
744 }
745
746 #[test]
747 fn theme_solarized_light_preset_builds() {
748 let t = Theme::solarized_light();
749 assert_eq!(t.bg, Color::Rgb(253, 246, 227));
750 assert!(!t.is_dark);
751 }
752
753 #[test]
754 fn theme_gruvbox_dark_preset_builds() {
755 let t = Theme::gruvbox_dark();
756 assert_eq!(t.bg, Color::Rgb(40, 40, 40));
757 assert!(t.is_dark);
758 }
759
760 #[test]
761 fn theme_one_dark_preset_builds() {
762 let t = Theme::one_dark();
763 assert_eq!(t.bg, Color::Rgb(40, 44, 52));
764 assert!(t.is_dark);
765 }
766
767 #[test]
768 fn spacing_scale_values() {
769 let sp = Spacing::new(1);
770 assert_eq!(sp.none(), 0);
771 assert_eq!(sp.xs(), 1);
772 assert_eq!(sp.sm(), 2);
773 assert_eq!(sp.md(), 3);
774 assert_eq!(sp.lg(), 4);
775 assert_eq!(sp.xl(), 6);
776 assert_eq!(sp.xxl(), 8);
777 }
778
779 #[test]
780 fn spacing_custom_base() {
781 let sp = Spacing::new(2);
782 assert_eq!(sp.xs(), 2);
783 assert_eq!(sp.sm(), 4);
784 assert_eq!(sp.md(), 6);
785 }
786
787 #[test]
788 fn theme_color_resolve_maps_correctly() {
789 let t = Theme::dark();
790 assert_eq!(t.resolve(ThemeColor::Primary), t.primary);
791 assert_eq!(t.resolve(ThemeColor::Secondary), t.secondary);
792 assert_eq!(t.resolve(ThemeColor::Accent), t.accent);
793 assert_eq!(t.resolve(ThemeColor::Text), t.text);
794 assert_eq!(t.resolve(ThemeColor::TextDim), t.text_dim);
795 assert_eq!(t.resolve(ThemeColor::Border), t.border);
796 assert_eq!(t.resolve(ThemeColor::Bg), t.bg);
797 assert_eq!(t.resolve(ThemeColor::Success), t.success);
798 assert_eq!(t.resolve(ThemeColor::Warning), t.warning);
799 assert_eq!(t.resolve(ThemeColor::Error), t.error);
800 assert_eq!(t.resolve(ThemeColor::SelectedBg), t.selected_bg);
801 assert_eq!(t.resolve(ThemeColor::SelectedFg), t.selected_fg);
802 assert_eq!(t.resolve(ThemeColor::Surface), t.surface);
803 assert_eq!(t.resolve(ThemeColor::SurfaceHover), t.surface_hover);
804 assert_eq!(t.resolve(ThemeColor::SurfaceText), t.surface_text);
805 }
806
807 #[test]
808 fn theme_color_aliases_resolve_to_primary() {
809 let t = Theme::dark();
810 assert_eq!(t.resolve(ThemeColor::Info), t.primary);
811 assert_eq!(t.resolve(ThemeColor::Link), t.primary);
812 assert_eq!(t.resolve(ThemeColor::FocusRing), t.primary);
813 }
814
815 #[test]
816 fn theme_color_custom_passes_through() {
817 let t = Theme::dark();
818 let custom = Color::Rgb(42, 42, 42);
819 assert_eq!(t.resolve(ThemeColor::Custom(custom)), custom);
820 }
821
822 #[test]
823 fn theme_builder_spacing() {
824 let sp = Spacing::new(3);
825 let theme = Theme::builder().spacing(sp).build();
826 assert_eq!(theme.spacing, sp);
827 }
828
829 #[test]
830 fn theme_contrast_text_on_dark_bg() {
831 let t = Theme::dark();
832 let fg = t.contrast_text_on(Color::Rgb(0, 0, 0));
833 assert_eq!(fg, Color::Rgb(255, 255, 255));
834 }
835
836 #[test]
837 fn theme_contrast_text_on_light_bg() {
838 let t = Theme::dark();
839 let fg = t.contrast_text_on(Color::Rgb(255, 255, 255));
840 assert_eq!(fg, Color::Rgb(0, 0, 0));
841 }
842}