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 const 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 const 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 const fn builder() -> ThemeBuilder {
296 ThemeBuilder {
297 primary: None,
298 secondary: None,
299 accent: None,
300 text: None,
301 text_dim: None,
302 border: None,
303 bg: None,
304 success: None,
305 warning: None,
306 error: None,
307 selected_bg: None,
308 selected_fg: None,
309 surface: None,
310 surface_hover: None,
311 surface_text: None,
312 is_dark: None,
313 spacing: None,
314 }
315 }
316
317 pub const fn builder_from(base: Theme) -> ThemeBuilder {
337 ThemeBuilder {
338 primary: Some(base.primary),
339 secondary: Some(base.secondary),
340 accent: Some(base.accent),
341 text: Some(base.text),
342 text_dim: Some(base.text_dim),
343 border: Some(base.border),
344 bg: Some(base.bg),
345 success: Some(base.success),
346 warning: Some(base.warning),
347 error: Some(base.error),
348 selected_bg: Some(base.selected_bg),
349 selected_fg: Some(base.selected_fg),
350 surface: Some(base.surface),
351 surface_hover: Some(base.surface_hover),
352 surface_text: Some(base.surface_text),
353 is_dark: Some(base.is_dark),
354 spacing: Some(base.spacing),
355 }
356 }
357
358 pub const fn light_builder() -> ThemeBuilder {
376 Self::builder_from(Self::light())
377 }
378
379 pub fn dracula() -> Self {
381 Self {
382 primary: Color::Rgb(189, 147, 249),
383 secondary: Color::Rgb(139, 233, 253),
384 accent: Color::Rgb(255, 121, 198),
385 text: Color::Rgb(248, 248, 242),
386 text_dim: Color::Rgb(98, 114, 164),
387 border: Color::Rgb(68, 71, 90),
388 bg: Color::Rgb(40, 42, 54),
389 success: Color::Rgb(80, 250, 123),
390 warning: Color::Rgb(241, 250, 140),
391 error: Color::Rgb(255, 85, 85),
392 selected_bg: Color::Rgb(189, 147, 249),
393 selected_fg: Color::Rgb(40, 42, 54),
394 surface: Color::Rgb(68, 71, 90),
395 surface_hover: Color::Rgb(98, 100, 120),
396 surface_text: Color::Rgb(191, 194, 210),
397 is_dark: true,
398 spacing: Spacing::new(1),
399 }
400 }
401
402 pub fn catppuccin() -> Self {
404 Self {
405 primary: Color::Rgb(180, 190, 254),
406 secondary: Color::Rgb(137, 180, 250),
407 accent: Color::Rgb(245, 194, 231),
408 text: Color::Rgb(205, 214, 244),
409 text_dim: Color::Rgb(127, 132, 156),
410 border: Color::Rgb(88, 91, 112),
411 bg: Color::Rgb(30, 30, 46),
412 success: Color::Rgb(166, 227, 161),
413 warning: Color::Rgb(249, 226, 175),
414 error: Color::Rgb(243, 139, 168),
415 selected_bg: Color::Rgb(180, 190, 254),
416 selected_fg: Color::Rgb(30, 30, 46),
417 surface: Color::Rgb(49, 50, 68),
418 surface_hover: Color::Rgb(69, 71, 90),
419 surface_text: Color::Rgb(166, 173, 200),
420 is_dark: true,
421 spacing: Spacing::new(1),
422 }
423 }
424
425 pub fn nord() -> Self {
427 Self {
428 primary: Color::Rgb(136, 192, 208),
429 secondary: Color::Rgb(129, 161, 193),
430 accent: Color::Rgb(180, 142, 173),
431 text: Color::Rgb(236, 239, 244),
432 text_dim: Color::Rgb(216, 222, 233),
433 border: Color::Rgb(59, 66, 82),
434 bg: Color::Rgb(46, 52, 64),
435 success: Color::Rgb(163, 190, 140),
436 warning: Color::Rgb(235, 203, 139),
437 error: Color::Rgb(191, 97, 106),
438 selected_bg: Color::Rgb(136, 192, 208),
439 selected_fg: Color::Rgb(46, 52, 64),
440 surface: Color::Rgb(59, 66, 82),
441 surface_hover: Color::Rgb(67, 76, 94),
442 surface_text: Color::Rgb(216, 222, 233),
443 is_dark: true,
444 spacing: Spacing::new(1),
445 }
446 }
447
448 pub fn solarized_dark() -> Self {
450 Self {
451 primary: Color::Rgb(38, 139, 210),
452 secondary: Color::Rgb(42, 161, 152),
453 accent: Color::Rgb(211, 54, 130),
454 text: Color::Rgb(131, 148, 150),
455 text_dim: Color::Rgb(101, 123, 131),
456 border: Color::Rgb(7, 54, 66),
457 bg: Color::Rgb(0, 43, 54),
458 success: Color::Rgb(133, 153, 0),
459 warning: Color::Rgb(181, 137, 0),
460 error: Color::Rgb(220, 50, 47),
461 selected_bg: Color::Rgb(38, 139, 210),
462 selected_fg: Color::Rgb(253, 246, 227),
463 surface: Color::Rgb(7, 54, 66),
464 surface_hover: Color::Rgb(23, 72, 85),
465 surface_text: Color::Rgb(147, 161, 161),
466 is_dark: true,
467 spacing: Spacing::new(1),
468 }
469 }
470
471 pub fn solarized_light() -> Self {
473 Self {
474 primary: Color::Rgb(38, 139, 210),
475 secondary: Color::Rgb(42, 161, 152),
476 accent: Color::Rgb(211, 54, 130),
477 text: Color::Rgb(101, 123, 131),
478 text_dim: Color::Rgb(88, 110, 117),
479 border: Color::Rgb(238, 232, 213),
480 bg: Color::Rgb(253, 246, 227),
481 success: Color::Rgb(133, 153, 0),
482 warning: Color::Rgb(181, 137, 0),
483 error: Color::Rgb(220, 50, 47),
484 selected_bg: Color::Rgb(38, 139, 210),
485 selected_fg: Color::Rgb(253, 246, 227),
486 surface: Color::Rgb(238, 232, 213),
487 surface_hover: Color::Rgb(227, 221, 201),
488 surface_text: Color::Rgb(88, 110, 117),
489 is_dark: false,
490 spacing: Spacing::new(1),
491 }
492 }
493
494 pub fn tokyo_night() -> Self {
496 Self {
497 primary: Color::Rgb(122, 162, 247),
498 secondary: Color::Rgb(125, 207, 255),
499 accent: Color::Rgb(187, 154, 247),
500 text: Color::Rgb(169, 177, 214),
501 text_dim: Color::Rgb(86, 95, 137),
502 border: Color::Rgb(54, 58, 79),
503 bg: Color::Rgb(26, 27, 38),
504 success: Color::Rgb(158, 206, 106),
505 warning: Color::Rgb(224, 175, 104),
506 error: Color::Rgb(247, 118, 142),
507 selected_bg: Color::Rgb(122, 162, 247),
508 selected_fg: Color::Rgb(26, 27, 38),
509 surface: Color::Rgb(36, 40, 59),
510 surface_hover: Color::Rgb(41, 46, 66),
511 surface_text: Color::Rgb(192, 202, 245),
512 is_dark: true,
513 spacing: Spacing::new(1),
514 }
515 }
516
517 pub fn gruvbox_dark() -> Self {
519 Self {
520 primary: Color::Rgb(215, 153, 33),
521 secondary: Color::Rgb(69, 133, 136),
522 accent: Color::Rgb(177, 98, 134),
523 text: Color::Rgb(235, 219, 178),
524 text_dim: Color::Rgb(146, 131, 116),
525 border: Color::Rgb(80, 73, 69),
526 bg: Color::Rgb(40, 40, 40),
527 success: Color::Rgb(152, 151, 26),
528 warning: Color::Rgb(250, 189, 47),
529 error: Color::Rgb(204, 36, 29),
530 selected_bg: Color::Rgb(215, 153, 33),
531 selected_fg: Color::Rgb(40, 40, 40),
532 surface: Color::Rgb(60, 56, 54),
533 surface_hover: Color::Rgb(80, 73, 69),
534 surface_text: Color::Rgb(189, 174, 147),
535 is_dark: true,
536 spacing: Spacing::new(1),
537 }
538 }
539
540 pub const fn compact() -> Self {
554 let base = Self::dark();
555 Self {
556 spacing: Spacing::new(1),
557 ..base
558 }
559 }
560
561 pub const fn comfortable() -> Self {
577 let base = Self::dark();
578 Self {
579 spacing: Spacing::new(2),
580 ..base
581 }
582 }
583
584 pub const fn spacious() -> Self {
600 let base = Self::dark();
601 Self {
602 spacing: Spacing::new(3),
603 ..base
604 }
605 }
606
607 pub const fn with_spacing(mut self, spacing: Spacing) -> Self {
623 self.spacing = spacing;
624 self
625 }
626
627 pub fn one_dark() -> Self {
629 Self {
630 primary: Color::Rgb(97, 175, 239),
631 secondary: Color::Rgb(86, 182, 194),
632 accent: Color::Rgb(198, 120, 221),
633 text: Color::Rgb(171, 178, 191),
634 text_dim: Color::Rgb(92, 99, 112),
635 border: Color::Rgb(62, 68, 81),
636 bg: Color::Rgb(40, 44, 52),
637 success: Color::Rgb(152, 195, 121),
638 warning: Color::Rgb(229, 192, 123),
639 error: Color::Rgb(224, 108, 117),
640 selected_bg: Color::Rgb(97, 175, 239),
641 selected_fg: Color::Rgb(40, 44, 52),
642 surface: Color::Rgb(50, 55, 65),
643 surface_hover: Color::Rgb(62, 68, 81),
644 surface_text: Color::Rgb(152, 159, 172),
645 is_dark: true,
646 spacing: Spacing::new(1),
647 }
648 }
649}
650
651pub struct ThemeBuilder {
653 primary: Option<Color>,
654 secondary: Option<Color>,
655 accent: Option<Color>,
656 text: Option<Color>,
657 text_dim: Option<Color>,
658 border: Option<Color>,
659 bg: Option<Color>,
660 success: Option<Color>,
661 warning: Option<Color>,
662 error: Option<Color>,
663 selected_bg: Option<Color>,
664 selected_fg: Option<Color>,
665 surface: Option<Color>,
666 surface_hover: Option<Color>,
667 surface_text: Option<Color>,
668 is_dark: Option<bool>,
669 spacing: Option<Spacing>,
670}
671
672impl ThemeBuilder {
673 pub const fn primary(mut self, color: Color) -> Self {
675 self.primary = Some(color);
676 self
677 }
678
679 pub const fn secondary(mut self, color: Color) -> Self {
681 self.secondary = Some(color);
682 self
683 }
684
685 pub const fn accent(mut self, color: Color) -> Self {
687 self.accent = Some(color);
688 self
689 }
690
691 pub const fn text(mut self, color: Color) -> Self {
693 self.text = Some(color);
694 self
695 }
696
697 pub const fn text_dim(mut self, color: Color) -> Self {
699 self.text_dim = Some(color);
700 self
701 }
702
703 pub const fn border(mut self, color: Color) -> Self {
705 self.border = Some(color);
706 self
707 }
708
709 pub const fn bg(mut self, color: Color) -> Self {
711 self.bg = Some(color);
712 self
713 }
714
715 pub const fn success(mut self, color: Color) -> Self {
717 self.success = Some(color);
718 self
719 }
720
721 pub const fn warning(mut self, color: Color) -> Self {
723 self.warning = Some(color);
724 self
725 }
726
727 pub const fn error(mut self, color: Color) -> Self {
729 self.error = Some(color);
730 self
731 }
732
733 pub const fn selected_bg(mut self, color: Color) -> Self {
735 self.selected_bg = Some(color);
736 self
737 }
738
739 pub const fn selected_fg(mut self, color: Color) -> Self {
741 self.selected_fg = Some(color);
742 self
743 }
744
745 pub const fn surface(mut self, color: Color) -> Self {
747 self.surface = Some(color);
748 self
749 }
750
751 pub const fn surface_hover(mut self, color: Color) -> Self {
753 self.surface_hover = Some(color);
754 self
755 }
756
757 pub const fn surface_text(mut self, color: Color) -> Self {
759 self.surface_text = Some(color);
760 self
761 }
762
763 pub const fn is_dark(mut self, is_dark: bool) -> Self {
765 self.is_dark = Some(is_dark);
766 self
767 }
768
769 pub const fn spacing(mut self, spacing: Spacing) -> Self {
771 self.spacing = Some(spacing);
772 self
773 }
774
775 pub const fn build(self) -> Theme {
781 let d = Theme::dark();
782 Theme {
783 primary: match self.primary {
784 Some(c) => c,
785 None => d.primary,
786 },
787 secondary: match self.secondary {
788 Some(c) => c,
789 None => d.secondary,
790 },
791 accent: match self.accent {
792 Some(c) => c,
793 None => d.accent,
794 },
795 text: match self.text {
796 Some(c) => c,
797 None => d.text,
798 },
799 text_dim: match self.text_dim {
800 Some(c) => c,
801 None => d.text_dim,
802 },
803 border: match self.border {
804 Some(c) => c,
805 None => d.border,
806 },
807 bg: match self.bg {
808 Some(c) => c,
809 None => d.bg,
810 },
811 success: match self.success {
812 Some(c) => c,
813 None => d.success,
814 },
815 warning: match self.warning {
816 Some(c) => c,
817 None => d.warning,
818 },
819 error: match self.error {
820 Some(c) => c,
821 None => d.error,
822 },
823 selected_bg: match self.selected_bg {
824 Some(c) => c,
825 None => d.selected_bg,
826 },
827 selected_fg: match self.selected_fg {
828 Some(c) => c,
829 None => d.selected_fg,
830 },
831 surface: match self.surface {
832 Some(c) => c,
833 None => d.surface,
834 },
835 surface_hover: match self.surface_hover {
836 Some(c) => c,
837 None => d.surface_hover,
838 },
839 surface_text: match self.surface_text {
840 Some(c) => c,
841 None => d.surface_text,
842 },
843 is_dark: match self.is_dark {
844 Some(b) => b,
845 None => d.is_dark,
846 },
847 spacing: match self.spacing {
848 Some(s) => s,
849 None => d.spacing,
850 },
851 }
852 }
853}
854
855impl Default for Theme {
856 fn default() -> Self {
857 Self::dark()
858 }
859}
860
861#[cfg(test)]
862mod tests {
863 use super::*;
864
865 #[test]
866 fn theme_dark_preset_builds() {
867 let t = Theme::dark();
868 assert_eq!(t.primary, Color::Cyan);
869 assert!(t.is_dark);
870 }
871
872 #[test]
873 fn theme_light_preset_builds() {
874 let t = Theme::light();
875 assert_eq!(t.selected_fg, Color::White);
876 assert!(!t.is_dark);
877 }
878
879 #[test]
880 fn theme_dracula_preset_builds() {
881 let t = Theme::dracula();
882 assert_eq!(t.bg, Color::Rgb(40, 42, 54));
883 assert!(t.is_dark);
884 }
885
886 #[test]
887 fn theme_catppuccin_preset_builds() {
888 let t = Theme::catppuccin();
889 assert_eq!(t.bg, Color::Rgb(30, 30, 46));
890 assert!(t.is_dark);
891 }
892
893 #[test]
894 fn theme_nord_preset_builds() {
895 let t = Theme::nord();
896 assert_eq!(t.bg, Color::Rgb(46, 52, 64));
897 assert!(t.is_dark);
898 }
899
900 #[test]
901 fn theme_solarized_dark_preset_builds() {
902 let t = Theme::solarized_dark();
903 assert_eq!(t.bg, Color::Rgb(0, 43, 54));
904 assert!(t.is_dark);
905 }
906
907 #[test]
908 fn theme_tokyo_night_preset_builds() {
909 let t = Theme::tokyo_night();
910 assert_eq!(t.bg, Color::Rgb(26, 27, 38));
911 assert!(t.is_dark);
912 }
913
914 #[test]
915 fn theme_builder_sets_primary_and_accent() {
916 let theme = Theme::builder()
917 .primary(Color::Red)
918 .accent(Color::Yellow)
919 .build();
920
921 assert_eq!(theme.primary, Color::Red);
922 assert_eq!(theme.accent, Color::Yellow);
923 }
924
925 #[test]
926 fn theme_builder_defaults_to_dark_for_unset_fields() {
927 let defaults = Theme::dark();
928 let theme = Theme::builder().primary(Color::Green).build();
929
930 assert_eq!(theme.primary, Color::Green);
931 assert_eq!(theme.secondary, defaults.secondary);
932 assert_eq!(theme.text, defaults.text);
933 assert_eq!(theme.text_dim, defaults.text_dim);
934 assert_eq!(theme.border, defaults.border);
935 assert_eq!(theme.surface_hover, defaults.surface_hover);
936 assert_eq!(theme.is_dark, defaults.is_dark);
937 }
938
939 #[test]
940 fn theme_builder_can_override_is_dark() {
941 let theme = Theme::builder().is_dark(false).build();
942 assert!(!theme.is_dark);
943 }
944
945 #[test]
946 fn theme_default_matches_dark() {
947 let default_theme = Theme::default();
948 let dark = Theme::dark();
949 assert_eq!(default_theme.primary, dark.primary);
950 assert_eq!(default_theme.bg, dark.bg);
951 assert_eq!(default_theme.is_dark, dark.is_dark);
952 }
953
954 #[test]
955 fn theme_solarized_light_preset_builds() {
956 let t = Theme::solarized_light();
957 assert_eq!(t.bg, Color::Rgb(253, 246, 227));
958 assert!(!t.is_dark);
959 }
960
961 #[test]
962 fn theme_gruvbox_dark_preset_builds() {
963 let t = Theme::gruvbox_dark();
964 assert_eq!(t.bg, Color::Rgb(40, 40, 40));
965 assert!(t.is_dark);
966 }
967
968 #[test]
969 fn theme_one_dark_preset_builds() {
970 let t = Theme::one_dark();
971 assert_eq!(t.bg, Color::Rgb(40, 44, 52));
972 assert!(t.is_dark);
973 }
974
975 #[test]
978 fn theme_text_dim_ne_border() {
979 for theme in [
980 Theme::nord(),
981 Theme::solarized_dark(),
982 Theme::solarized_light(),
983 ] {
984 assert_ne!(theme.text_dim, theme.border, "text_dim == border in theme");
985 }
986 }
987
988 #[test]
989 fn spacing_scale_values() {
990 let sp = Spacing::new(1);
991 assert_eq!(sp.none(), 0);
992 assert_eq!(sp.xs(), 1);
993 assert_eq!(sp.sm(), 2);
994 assert_eq!(sp.md(), 3);
995 assert_eq!(sp.lg(), 4);
996 assert_eq!(sp.xl(), 6);
997 assert_eq!(sp.xxl(), 8);
998 }
999
1000 #[test]
1001 fn spacing_custom_base() {
1002 let sp = Spacing::new(2);
1003 assert_eq!(sp.xs(), 2);
1004 assert_eq!(sp.sm(), 4);
1005 assert_eq!(sp.md(), 6);
1006 }
1007
1008 #[test]
1009 fn theme_color_resolve_maps_correctly() {
1010 let t = Theme::dark();
1011 assert_eq!(t.resolve(ThemeColor::Primary), t.primary);
1012 assert_eq!(t.resolve(ThemeColor::Secondary), t.secondary);
1013 assert_eq!(t.resolve(ThemeColor::Accent), t.accent);
1014 assert_eq!(t.resolve(ThemeColor::Text), t.text);
1015 assert_eq!(t.resolve(ThemeColor::TextDim), t.text_dim);
1016 assert_eq!(t.resolve(ThemeColor::Border), t.border);
1017 assert_eq!(t.resolve(ThemeColor::Bg), t.bg);
1018 assert_eq!(t.resolve(ThemeColor::Success), t.success);
1019 assert_eq!(t.resolve(ThemeColor::Warning), t.warning);
1020 assert_eq!(t.resolve(ThemeColor::Error), t.error);
1021 assert_eq!(t.resolve(ThemeColor::SelectedBg), t.selected_bg);
1022 assert_eq!(t.resolve(ThemeColor::SelectedFg), t.selected_fg);
1023 assert_eq!(t.resolve(ThemeColor::Surface), t.surface);
1024 assert_eq!(t.resolve(ThemeColor::SurfaceHover), t.surface_hover);
1025 assert_eq!(t.resolve(ThemeColor::SurfaceText), t.surface_text);
1026 }
1027
1028 #[test]
1029 fn theme_color_aliases_resolve_to_primary() {
1030 let t = Theme::dark();
1031 assert_eq!(t.resolve(ThemeColor::Info), t.primary);
1032 assert_eq!(t.resolve(ThemeColor::Link), t.primary);
1033 assert_eq!(t.resolve(ThemeColor::FocusRing), t.primary);
1034 }
1035
1036 #[test]
1037 fn theme_color_custom_passes_through() {
1038 let t = Theme::dark();
1039 let custom = Color::Rgb(42, 42, 42);
1040 assert_eq!(t.resolve(ThemeColor::Custom(custom)), custom);
1041 }
1042
1043 #[test]
1044 fn theme_builder_spacing() {
1045 let sp = Spacing::new(3);
1046 let theme = Theme::builder().spacing(sp).build();
1047 assert_eq!(theme.spacing, sp);
1048 }
1049
1050 #[test]
1051 fn theme_contrast_text_on_dark_bg() {
1052 let t = Theme::dark();
1053 let fg = t.contrast_text_on(Color::Rgb(0, 0, 0));
1054 assert_eq!(fg, Color::Rgb(255, 255, 255));
1055 }
1056
1057 #[test]
1058 fn theme_contrast_text_on_light_bg() {
1059 let t = Theme::dark();
1060 let fg = t.contrast_text_on(Color::Rgb(255, 255, 255));
1061 assert_eq!(fg, Color::Rgb(0, 0, 0));
1062 }
1063
1064 const _CONST_THEME: Theme = Theme::builder()
1069 .primary(Color::Rgb(255, 100, 100))
1070 .bg(Color::Rgb(20, 20, 20))
1071 .is_dark(true)
1072 .spacing(Spacing::new(2))
1073 .build();
1074
1075 #[test]
1076 fn theme_builder_const_eval() {
1077 assert_eq!(_CONST_THEME.primary, Color::Rgb(255, 100, 100));
1079 assert_eq!(_CONST_THEME.bg, Color::Rgb(20, 20, 20));
1080 assert_eq!(_CONST_THEME.spacing, Spacing::new(2));
1081 let dark = Theme::dark();
1084 assert_eq!(_CONST_THEME.text, dark.text);
1085 assert_eq!(_CONST_THEME.border, dark.border);
1086 assert_eq!(_CONST_THEME.surface, dark.surface);
1087 }
1088
1089 #[test]
1092 fn builder_from_preserves_base_fields() {
1093 let nord = Theme::nord();
1096 let t = Theme::builder_from(nord).build();
1097 assert_eq!(t.primary, nord.primary);
1098 assert_eq!(t.secondary, nord.secondary);
1099 assert_eq!(t.accent, nord.accent);
1100 assert_eq!(t.text, nord.text);
1101 assert_eq!(t.text_dim, nord.text_dim);
1102 assert_eq!(t.border, nord.border);
1103 assert_eq!(t.bg, nord.bg);
1104 assert_eq!(t.success, nord.success);
1105 assert_eq!(t.warning, nord.warning);
1106 assert_eq!(t.error, nord.error);
1107 assert_eq!(t.selected_bg, nord.selected_bg);
1108 assert_eq!(t.selected_fg, nord.selected_fg);
1109 assert_eq!(t.surface, nord.surface);
1110 assert_eq!(t.surface_hover, nord.surface_hover);
1111 assert_eq!(t.surface_text, nord.surface_text);
1112 assert_eq!(t.is_dark, nord.is_dark);
1113 assert_eq!(t.spacing, nord.spacing);
1114 }
1115
1116 #[test]
1117 fn builder_from_overrides_only_specified_fields() {
1118 let t = Theme::builder_from(Theme::nord())
1119 .primary(Color::Rgb(255, 0, 0))
1120 .build();
1121 assert_eq!(t.primary, Color::Rgb(255, 0, 0));
1123 assert_eq!(t.bg, Theme::nord().bg);
1124 assert_eq!(t.text, Theme::nord().text);
1125 assert_ne!(t.primary, Theme::nord().primary);
1126 }
1127
1128 #[test]
1129 fn light_builder_starts_from_light_preset() {
1130 let t = Theme::light_builder()
1134 .primary(Color::Rgb(0, 100, 200))
1135 .build();
1136 let light = Theme::light();
1137 assert_eq!(t.primary, Color::Rgb(0, 100, 200));
1138 assert_eq!(t.bg, light.bg);
1139 assert_eq!(t.text, light.text);
1140 assert_eq!(t.surface, light.surface);
1141 assert!(!t.is_dark);
1142 }
1143
1144 const _CONST_LIGHT: Theme = Theme::light_builder().primary(Color::Rgb(1, 2, 3)).build();
1146
1147 #[test]
1148 fn light_builder_is_const_evaluable() {
1149 assert_eq!(_CONST_LIGHT.primary, Color::Rgb(1, 2, 3));
1150 assert_eq!(_CONST_LIGHT.bg, Theme::light().bg);
1151 const { assert!(!_CONST_LIGHT.is_dark) };
1152 }
1153}