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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
152#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
153pub struct SyntaxPalette {
154 pub keyword: Color,
156 pub string: Color,
158 pub number: Color,
160 pub function: Color,
162 pub macro_: Color,
164 pub type_: Color,
166 pub constant: Color,
168 pub property: Color,
170 pub tag: Color,
172}
173
174impl SyntaxPalette {
175 pub const fn one_dark() -> Self {
178 Self {
179 keyword: Color::Rgb(198, 120, 221),
180 string: Color::Rgb(152, 195, 121),
181 number: Color::Rgb(209, 154, 102),
182 function: Color::Rgb(97, 175, 239),
183 macro_: Color::Rgb(86, 182, 194),
184 type_: Color::Rgb(229, 192, 123),
185 constant: Color::Rgb(209, 154, 102),
186 property: Color::Rgb(97, 175, 239),
187 tag: Color::Rgb(224, 108, 117),
188 }
189 }
190
191 pub const fn one_light() -> Self {
194 Self {
195 keyword: Color::Rgb(166, 38, 164),
196 string: Color::Rgb(80, 161, 79),
197 number: Color::Rgb(152, 104, 1),
198 function: Color::Rgb(64, 120, 242),
199 macro_: Color::Rgb(1, 132, 188),
200 type_: Color::Rgb(152, 104, 1),
201 constant: Color::Rgb(152, 104, 1),
202 property: Color::Rgb(64, 120, 242),
203 tag: Color::Rgb(166, 38, 164),
204 }
205 }
206
207 pub const fn dracula() -> Self {
209 Self {
210 keyword: Color::Rgb(255, 121, 198),
211 string: Color::Rgb(241, 250, 140),
212 number: Color::Rgb(189, 147, 249),
213 function: Color::Rgb(80, 250, 123),
214 macro_: Color::Rgb(139, 233, 253),
215 type_: Color::Rgb(139, 233, 253),
216 constant: Color::Rgb(189, 147, 249),
217 property: Color::Rgb(102, 217, 239),
218 tag: Color::Rgb(255, 121, 198),
219 }
220 }
221
222 pub const fn catppuccin() -> Self {
224 Self {
225 keyword: Color::Rgb(203, 166, 247),
226 string: Color::Rgb(166, 227, 161),
227 number: Color::Rgb(250, 179, 135),
228 function: Color::Rgb(137, 180, 250),
229 macro_: Color::Rgb(245, 194, 231),
230 type_: Color::Rgb(249, 226, 175),
231 constant: Color::Rgb(250, 179, 135),
232 property: Color::Rgb(137, 220, 235),
233 tag: Color::Rgb(243, 139, 168),
234 }
235 }
236
237 pub const fn nord() -> Self {
239 Self {
240 keyword: Color::Rgb(180, 142, 173),
241 string: Color::Rgb(163, 190, 140),
242 number: Color::Rgb(180, 142, 173),
243 function: Color::Rgb(136, 192, 208),
244 macro_: Color::Rgb(143, 188, 187),
245 type_: Color::Rgb(143, 188, 187),
246 constant: Color::Rgb(208, 135, 112),
247 property: Color::Rgb(129, 161, 193),
248 tag: Color::Rgb(191, 97, 106),
249 }
250 }
251
252 pub const fn solarized_dark() -> Self {
254 Self {
255 keyword: Color::Rgb(133, 153, 0),
256 string: Color::Rgb(42, 161, 152),
257 number: Color::Rgb(211, 54, 130),
258 function: Color::Rgb(38, 139, 210),
259 macro_: Color::Rgb(203, 75, 22),
260 type_: Color::Rgb(181, 137, 0),
261 constant: Color::Rgb(211, 54, 130),
262 property: Color::Rgb(38, 139, 210),
263 tag: Color::Rgb(220, 50, 47),
264 }
265 }
266
267 pub const fn solarized_light() -> Self {
269 Self {
270 keyword: Color::Rgb(133, 153, 0),
271 string: Color::Rgb(42, 161, 152),
272 number: Color::Rgb(211, 54, 130),
273 function: Color::Rgb(38, 139, 210),
274 macro_: Color::Rgb(203, 75, 22),
275 type_: Color::Rgb(181, 137, 0),
276 constant: Color::Rgb(211, 54, 130),
277 property: Color::Rgb(38, 139, 210),
278 tag: Color::Rgb(220, 50, 47),
279 }
280 }
281
282 pub const fn tokyo_night() -> Self {
284 Self {
285 keyword: Color::Rgb(187, 154, 247),
286 string: Color::Rgb(158, 206, 106),
287 number: Color::Rgb(255, 158, 100),
288 function: Color::Rgb(122, 162, 247),
289 macro_: Color::Rgb(125, 207, 255),
290 type_: Color::Rgb(43, 178, 187),
291 constant: Color::Rgb(255, 158, 100),
292 property: Color::Rgb(115, 218, 202),
293 tag: Color::Rgb(247, 118, 142),
294 }
295 }
296
297 pub const fn gruvbox_dark() -> Self {
299 Self {
300 keyword: Color::Rgb(251, 73, 52),
301 string: Color::Rgb(184, 187, 38),
302 number: Color::Rgb(211, 134, 155),
303 function: Color::Rgb(184, 187, 38),
304 macro_: Color::Rgb(142, 192, 124),
305 type_: Color::Rgb(250, 189, 47),
306 constant: Color::Rgb(211, 134, 155),
307 property: Color::Rgb(131, 165, 152),
308 tag: Color::Rgb(251, 73, 52),
309 }
310 }
311}
312
313impl Default for SyntaxPalette {
314 fn default() -> Self {
315 Self::one_dark()
316 }
317}
318
319#[non_exhaustive]
335#[derive(Debug, Clone, Copy)]
336#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
337#[cfg_attr(feature = "serde", serde(default))]
338pub struct Theme {
339 pub primary: Color,
341 pub secondary: Color,
343 pub accent: Color,
345 pub text: Color,
347 pub text_dim: Color,
349 pub border: Color,
351 pub bg: Color,
353 pub success: Color,
355 pub warning: Color,
357 pub error: Color,
359 pub selected_bg: Color,
361 pub selected_fg: Color,
363 pub surface: Color,
365 pub surface_hover: Color,
370 pub surface_text: Color,
376 pub is_dark: bool,
378 pub spacing: Spacing,
380 pub syntax: SyntaxPalette,
382}
383
384impl Theme {
385 pub fn resolve(&self, token: ThemeColor) -> Color {
387 match token {
388 ThemeColor::Primary => self.primary,
389 ThemeColor::Secondary => self.secondary,
390 ThemeColor::Accent => self.accent,
391 ThemeColor::Text => self.text,
392 ThemeColor::TextDim => self.text_dim,
393 ThemeColor::Border => self.border,
394 ThemeColor::Bg => self.bg,
395 ThemeColor::Success => self.success,
396 ThemeColor::Warning => self.warning,
397 ThemeColor::Error => self.error,
398 ThemeColor::SelectedBg => self.selected_bg,
399 ThemeColor::SelectedFg => self.selected_fg,
400 ThemeColor::Surface => self.surface,
401 ThemeColor::SurfaceHover => self.surface_hover,
402 ThemeColor::SurfaceText => self.surface_text,
403 ThemeColor::Info | ThemeColor::Link | ThemeColor::FocusRing => self.primary,
404 ThemeColor::Custom(c) => c,
405 }
406 }
407
408 pub fn contrast_text_on(&self, bg: Color) -> Color {
412 Color::contrast_fg(bg)
413 }
414
415 pub fn overlay(&self, color: Color, alpha: f32) -> Color {
419 color.blend(self.bg, alpha)
420 }
421
422 pub const fn dark() -> Self {
424 Self {
425 primary: Color::Cyan,
426 secondary: Color::Blue,
427 accent: Color::Magenta,
428 text: Color::White,
429 text_dim: Color::Indexed(245),
430 border: Color::Indexed(240),
431 bg: Color::Reset,
432 success: Color::Green,
433 warning: Color::Yellow,
434 error: Color::Red,
435 selected_bg: Color::Cyan,
436 selected_fg: Color::Black,
437 surface: Color::Indexed(236),
438 surface_hover: Color::Indexed(238),
439 surface_text: Color::Indexed(250),
440 is_dark: true,
441 spacing: Spacing::new(1),
442 syntax: SyntaxPalette::one_dark(),
443 }
444 }
445
446 pub const fn light() -> Self {
448 Self {
449 primary: Color::Rgb(37, 99, 235),
450 secondary: Color::Rgb(14, 116, 144),
451 accent: Color::Rgb(147, 51, 234),
452 text: Color::Rgb(15, 23, 42),
453 text_dim: Color::Rgb(100, 116, 139),
454 border: Color::Rgb(203, 213, 225),
455 bg: Color::Rgb(248, 250, 252),
456 success: Color::Rgb(22, 163, 74),
457 warning: Color::Rgb(202, 138, 4),
458 error: Color::Rgb(220, 38, 38),
459 selected_bg: Color::Rgb(37, 99, 235),
460 selected_fg: Color::White,
461 surface: Color::Rgb(241, 245, 249),
462 surface_hover: Color::Rgb(226, 232, 240),
463 surface_text: Color::Rgb(51, 65, 85),
464 is_dark: false,
465 spacing: Spacing::new(1),
466 syntax: SyntaxPalette::one_light(),
467 }
468 }
469
470 pub const fn builder() -> ThemeBuilder {
487 ThemeBuilder {
488 primary: None,
489 secondary: None,
490 accent: None,
491 text: None,
492 text_dim: None,
493 border: None,
494 bg: None,
495 success: None,
496 warning: None,
497 error: None,
498 selected_bg: None,
499 selected_fg: None,
500 surface: None,
501 surface_hover: None,
502 surface_text: None,
503 is_dark: None,
504 spacing: None,
505 syntax: None,
506 }
507 }
508
509 pub const fn builder_from(base: Theme) -> ThemeBuilder {
529 ThemeBuilder {
530 primary: Some(base.primary),
531 secondary: Some(base.secondary),
532 accent: Some(base.accent),
533 text: Some(base.text),
534 text_dim: Some(base.text_dim),
535 border: Some(base.border),
536 bg: Some(base.bg),
537 success: Some(base.success),
538 warning: Some(base.warning),
539 error: Some(base.error),
540 selected_bg: Some(base.selected_bg),
541 selected_fg: Some(base.selected_fg),
542 surface: Some(base.surface),
543 surface_hover: Some(base.surface_hover),
544 surface_text: Some(base.surface_text),
545 is_dark: Some(base.is_dark),
546 spacing: Some(base.spacing),
547 syntax: Some(base.syntax),
548 }
549 }
550
551 pub const fn light_builder() -> ThemeBuilder {
569 Self::builder_from(Self::light())
570 }
571
572 pub fn dracula() -> Self {
574 Self {
575 primary: Color::Rgb(189, 147, 249),
576 secondary: Color::Rgb(139, 233, 253),
577 accent: Color::Rgb(255, 121, 198),
578 text: Color::Rgb(248, 248, 242),
579 text_dim: Color::Rgb(98, 114, 164),
580 border: Color::Rgb(68, 71, 90),
581 bg: Color::Rgb(40, 42, 54),
582 success: Color::Rgb(80, 250, 123),
583 warning: Color::Rgb(241, 250, 140),
584 error: Color::Rgb(255, 85, 85),
585 selected_bg: Color::Rgb(189, 147, 249),
586 selected_fg: Color::Rgb(40, 42, 54),
587 surface: Color::Rgb(68, 71, 90),
588 surface_hover: Color::Rgb(98, 100, 120),
589 surface_text: Color::Rgb(191, 194, 210),
590 is_dark: true,
591 spacing: Spacing::new(1),
592 syntax: SyntaxPalette::dracula(),
593 }
594 }
595
596 pub fn catppuccin() -> Self {
598 Self {
599 primary: Color::Rgb(180, 190, 254),
600 secondary: Color::Rgb(137, 180, 250),
601 accent: Color::Rgb(245, 194, 231),
602 text: Color::Rgb(205, 214, 244),
603 text_dim: Color::Rgb(127, 132, 156),
604 border: Color::Rgb(88, 91, 112),
605 bg: Color::Rgb(30, 30, 46),
606 success: Color::Rgb(166, 227, 161),
607 warning: Color::Rgb(249, 226, 175),
608 error: Color::Rgb(243, 139, 168),
609 selected_bg: Color::Rgb(180, 190, 254),
610 selected_fg: Color::Rgb(30, 30, 46),
611 surface: Color::Rgb(49, 50, 68),
612 surface_hover: Color::Rgb(69, 71, 90),
613 surface_text: Color::Rgb(166, 173, 200),
614 is_dark: true,
615 spacing: Spacing::new(1),
616 syntax: SyntaxPalette::catppuccin(),
617 }
618 }
619
620 pub fn nord() -> Self {
622 Self {
623 primary: Color::Rgb(136, 192, 208),
624 secondary: Color::Rgb(129, 161, 193),
625 accent: Color::Rgb(180, 142, 173),
626 text: Color::Rgb(236, 239, 244),
627 text_dim: Color::Rgb(216, 222, 233),
628 border: Color::Rgb(59, 66, 82),
629 bg: Color::Rgb(46, 52, 64),
630 success: Color::Rgb(163, 190, 140),
631 warning: Color::Rgb(235, 203, 139),
632 error: Color::Rgb(191, 97, 106),
633 selected_bg: Color::Rgb(136, 192, 208),
634 selected_fg: Color::Rgb(46, 52, 64),
635 surface: Color::Rgb(59, 66, 82),
636 surface_hover: Color::Rgb(67, 76, 94),
637 surface_text: Color::Rgb(216, 222, 233),
638 is_dark: true,
639 spacing: Spacing::new(1),
640 syntax: SyntaxPalette::nord(),
641 }
642 }
643
644 pub fn solarized_dark() -> Self {
646 Self {
647 primary: Color::Rgb(38, 139, 210),
648 secondary: Color::Rgb(42, 161, 152),
649 accent: Color::Rgb(211, 54, 130),
650 text: Color::Rgb(131, 148, 150),
651 text_dim: Color::Rgb(101, 123, 131),
652 border: Color::Rgb(7, 54, 66),
653 bg: Color::Rgb(0, 43, 54),
654 success: Color::Rgb(133, 153, 0),
655 warning: Color::Rgb(181, 137, 0),
656 error: Color::Rgb(220, 50, 47),
657 selected_bg: Color::Rgb(38, 139, 210),
658 selected_fg: Color::Rgb(253, 246, 227),
659 surface: Color::Rgb(7, 54, 66),
660 surface_hover: Color::Rgb(23, 72, 85),
661 surface_text: Color::Rgb(147, 161, 161),
662 is_dark: true,
663 spacing: Spacing::new(1),
664 syntax: SyntaxPalette::solarized_dark(),
665 }
666 }
667
668 pub fn solarized_light() -> Self {
670 Self {
671 primary: Color::Rgb(38, 139, 210),
672 secondary: Color::Rgb(42, 161, 152),
673 accent: Color::Rgb(211, 54, 130),
674 text: Color::Rgb(101, 123, 131),
675 text_dim: Color::Rgb(88, 110, 117),
676 border: Color::Rgb(238, 232, 213),
677 bg: Color::Rgb(253, 246, 227),
678 success: Color::Rgb(133, 153, 0),
679 warning: Color::Rgb(181, 137, 0),
680 error: Color::Rgb(220, 50, 47),
681 selected_bg: Color::Rgb(38, 139, 210),
682 selected_fg: Color::Rgb(253, 246, 227),
683 surface: Color::Rgb(238, 232, 213),
684 surface_hover: Color::Rgb(227, 221, 201),
685 surface_text: Color::Rgb(88, 110, 117),
686 is_dark: false,
687 spacing: Spacing::new(1),
688 syntax: SyntaxPalette::solarized_light(),
689 }
690 }
691
692 pub fn tokyo_night() -> Self {
694 Self {
695 primary: Color::Rgb(122, 162, 247),
696 secondary: Color::Rgb(125, 207, 255),
697 accent: Color::Rgb(187, 154, 247),
698 text: Color::Rgb(169, 177, 214),
699 text_dim: Color::Rgb(86, 95, 137),
700 border: Color::Rgb(54, 58, 79),
701 bg: Color::Rgb(26, 27, 38),
702 success: Color::Rgb(158, 206, 106),
703 warning: Color::Rgb(224, 175, 104),
704 error: Color::Rgb(247, 118, 142),
705 selected_bg: Color::Rgb(122, 162, 247),
706 selected_fg: Color::Rgb(26, 27, 38),
707 surface: Color::Rgb(36, 40, 59),
708 surface_hover: Color::Rgb(41, 46, 66),
709 surface_text: Color::Rgb(192, 202, 245),
710 is_dark: true,
711 spacing: Spacing::new(1),
712 syntax: SyntaxPalette::tokyo_night(),
713 }
714 }
715
716 pub fn gruvbox_dark() -> Self {
718 Self {
719 primary: Color::Rgb(215, 153, 33),
720 secondary: Color::Rgb(69, 133, 136),
721 accent: Color::Rgb(177, 98, 134),
722 text: Color::Rgb(235, 219, 178),
723 text_dim: Color::Rgb(146, 131, 116),
724 border: Color::Rgb(80, 73, 69),
725 bg: Color::Rgb(40, 40, 40),
726 success: Color::Rgb(152, 151, 26),
727 warning: Color::Rgb(250, 189, 47),
728 error: Color::Rgb(204, 36, 29),
729 selected_bg: Color::Rgb(215, 153, 33),
730 selected_fg: Color::Rgb(40, 40, 40),
731 surface: Color::Rgb(60, 56, 54),
732 surface_hover: Color::Rgb(80, 73, 69),
733 surface_text: Color::Rgb(189, 174, 147),
734 is_dark: true,
735 spacing: Spacing::new(1),
736 syntax: SyntaxPalette::gruvbox_dark(),
737 }
738 }
739
740 pub const fn compact() -> Self {
754 let base = Self::dark();
755 Self {
756 spacing: Spacing::new(1),
757 ..base
758 }
759 }
760
761 pub const fn comfortable() -> Self {
777 let base = Self::dark();
778 Self {
779 spacing: Spacing::new(2),
780 ..base
781 }
782 }
783
784 pub const fn spacious() -> Self {
800 let base = Self::dark();
801 Self {
802 spacing: Spacing::new(3),
803 ..base
804 }
805 }
806
807 pub const fn with_spacing(mut self, spacing: Spacing) -> Self {
823 self.spacing = spacing;
824 self
825 }
826
827 pub fn one_dark() -> Self {
829 Self {
830 primary: Color::Rgb(97, 175, 239),
831 secondary: Color::Rgb(86, 182, 194),
832 accent: Color::Rgb(198, 120, 221),
833 text: Color::Rgb(171, 178, 191),
834 text_dim: Color::Rgb(92, 99, 112),
835 border: Color::Rgb(62, 68, 81),
836 bg: Color::Rgb(40, 44, 52),
837 success: Color::Rgb(152, 195, 121),
838 warning: Color::Rgb(229, 192, 123),
839 error: Color::Rgb(224, 108, 117),
840 selected_bg: Color::Rgb(97, 175, 239),
841 selected_fg: Color::Rgb(40, 44, 52),
842 surface: Color::Rgb(50, 55, 65),
843 surface_hover: Color::Rgb(62, 68, 81),
844 surface_text: Color::Rgb(152, 159, 172),
845 is_dark: true,
846 spacing: Spacing::new(1),
847 syntax: SyntaxPalette::one_dark(),
848 }
849 }
850
851 #[cfg(feature = "serde")]
878 pub fn from_toml_str(src: &str) -> Result<Theme, ThemeLoadError> {
879 ThemeFile::from_toml_str(src).map(|tf| tf.theme)
880 }
881
882 #[cfg(feature = "serde")]
900 pub fn load(path: impl AsRef<std::path::Path>) -> Result<Theme, ThemeLoadError> {
901 ThemeFile::load(path).map(|tf| tf.theme)
902 }
903}
904
905pub struct ThemeBuilder {
907 primary: Option<Color>,
908 secondary: Option<Color>,
909 accent: Option<Color>,
910 text: Option<Color>,
911 text_dim: Option<Color>,
912 border: Option<Color>,
913 bg: Option<Color>,
914 success: Option<Color>,
915 warning: Option<Color>,
916 error: Option<Color>,
917 selected_bg: Option<Color>,
918 selected_fg: Option<Color>,
919 surface: Option<Color>,
920 surface_hover: Option<Color>,
921 surface_text: Option<Color>,
922 is_dark: Option<bool>,
923 spacing: Option<Spacing>,
924 syntax: Option<SyntaxPalette>,
925}
926
927impl ThemeBuilder {
928 pub const fn primary(mut self, color: Color) -> Self {
930 self.primary = Some(color);
931 self
932 }
933
934 pub const fn secondary(mut self, color: Color) -> Self {
936 self.secondary = Some(color);
937 self
938 }
939
940 pub const fn accent(mut self, color: Color) -> Self {
942 self.accent = Some(color);
943 self
944 }
945
946 pub const fn text(mut self, color: Color) -> Self {
948 self.text = Some(color);
949 self
950 }
951
952 pub const fn text_dim(mut self, color: Color) -> Self {
954 self.text_dim = Some(color);
955 self
956 }
957
958 pub const fn border(mut self, color: Color) -> Self {
960 self.border = Some(color);
961 self
962 }
963
964 pub const fn bg(mut self, color: Color) -> Self {
966 self.bg = Some(color);
967 self
968 }
969
970 pub const fn success(mut self, color: Color) -> Self {
972 self.success = Some(color);
973 self
974 }
975
976 pub const fn warning(mut self, color: Color) -> Self {
978 self.warning = Some(color);
979 self
980 }
981
982 pub const fn error(mut self, color: Color) -> Self {
984 self.error = Some(color);
985 self
986 }
987
988 pub const fn selected_bg(mut self, color: Color) -> Self {
990 self.selected_bg = Some(color);
991 self
992 }
993
994 pub const fn selected_fg(mut self, color: Color) -> Self {
996 self.selected_fg = Some(color);
997 self
998 }
999
1000 pub const fn surface(mut self, color: Color) -> Self {
1002 self.surface = Some(color);
1003 self
1004 }
1005
1006 pub const fn surface_hover(mut self, color: Color) -> Self {
1008 self.surface_hover = Some(color);
1009 self
1010 }
1011
1012 pub const fn surface_text(mut self, color: Color) -> Self {
1014 self.surface_text = Some(color);
1015 self
1016 }
1017
1018 pub const fn is_dark(mut self, is_dark: bool) -> Self {
1020 self.is_dark = Some(is_dark);
1021 self
1022 }
1023
1024 pub const fn spacing(mut self, spacing: Spacing) -> Self {
1026 self.spacing = Some(spacing);
1027 self
1028 }
1029
1030 pub const fn syntax(mut self, syntax: SyntaxPalette) -> Self {
1032 self.syntax = Some(syntax);
1033 self
1034 }
1035
1036 pub const fn build(self) -> Theme {
1042 let d = Theme::dark();
1043 Theme {
1044 primary: match self.primary {
1045 Some(c) => c,
1046 None => d.primary,
1047 },
1048 secondary: match self.secondary {
1049 Some(c) => c,
1050 None => d.secondary,
1051 },
1052 accent: match self.accent {
1053 Some(c) => c,
1054 None => d.accent,
1055 },
1056 text: match self.text {
1057 Some(c) => c,
1058 None => d.text,
1059 },
1060 text_dim: match self.text_dim {
1061 Some(c) => c,
1062 None => d.text_dim,
1063 },
1064 border: match self.border {
1065 Some(c) => c,
1066 None => d.border,
1067 },
1068 bg: match self.bg {
1069 Some(c) => c,
1070 None => d.bg,
1071 },
1072 success: match self.success {
1073 Some(c) => c,
1074 None => d.success,
1075 },
1076 warning: match self.warning {
1077 Some(c) => c,
1078 None => d.warning,
1079 },
1080 error: match self.error {
1081 Some(c) => c,
1082 None => d.error,
1083 },
1084 selected_bg: match self.selected_bg {
1085 Some(c) => c,
1086 None => d.selected_bg,
1087 },
1088 selected_fg: match self.selected_fg {
1089 Some(c) => c,
1090 None => d.selected_fg,
1091 },
1092 surface: match self.surface {
1093 Some(c) => c,
1094 None => d.surface,
1095 },
1096 surface_hover: match self.surface_hover {
1097 Some(c) => c,
1098 None => d.surface_hover,
1099 },
1100 surface_text: match self.surface_text {
1101 Some(c) => c,
1102 None => d.surface_text,
1103 },
1104 is_dark: match self.is_dark {
1105 Some(b) => b,
1106 None => d.is_dark,
1107 },
1108 spacing: match self.spacing {
1109 Some(s) => s,
1110 None => d.spacing,
1111 },
1112 syntax: match self.syntax {
1113 Some(s) => s,
1114 None => d.syntax,
1115 },
1116 }
1117 }
1118}
1119
1120impl Default for Theme {
1121 fn default() -> Self {
1122 Self::dark()
1123 }
1124}
1125
1126#[cfg(test)]
1127mod tests {
1128 use super::*;
1129
1130 #[test]
1131 fn theme_dark_preset_builds() {
1132 let t = Theme::dark();
1133 assert_eq!(t.primary, Color::Cyan);
1134 assert!(t.is_dark);
1135 }
1136
1137 #[test]
1138 fn dark_preset_uses_one_dark_syntax_palette() {
1139 assert_eq!(Theme::dark().syntax, SyntaxPalette::one_dark());
1140 assert_eq!(Theme::one_dark().syntax, SyntaxPalette::one_dark());
1141 }
1142
1143 #[test]
1144 fn presets_carry_distinct_syntax_palettes() {
1145 assert_ne!(
1147 Theme::nord().syntax.keyword,
1148 SyntaxPalette::one_dark().keyword
1149 );
1150 assert_ne!(
1151 Theme::nord().syntax.keyword,
1152 Theme::catppuccin().syntax.keyword
1153 );
1154 }
1155
1156 #[test]
1157 fn builder_syntax_setter_overrides_palette() {
1158 let custom = SyntaxPalette {
1159 keyword: Color::Rgb(1, 2, 3),
1160 ..SyntaxPalette::one_dark()
1161 };
1162 let theme = Theme::builder().syntax(custom).build();
1163 assert_eq!(theme.syntax.keyword, Color::Rgb(1, 2, 3));
1164 }
1165
1166 #[test]
1167 fn builder_from_threads_syntax_palette() {
1168 let theme = Theme::builder_from(Theme::nord())
1169 .primary(Color::Rgb(255, 0, 0))
1170 .build();
1171 assert_eq!(theme.syntax, Theme::nord().syntax);
1173 }
1174
1175 #[test]
1176 fn syntax_palette_default_is_one_dark() {
1177 assert_eq!(SyntaxPalette::default(), SyntaxPalette::one_dark());
1178 }
1179
1180 #[test]
1181 fn theme_light_preset_builds() {
1182 let t = Theme::light();
1183 assert_eq!(t.selected_fg, Color::White);
1184 assert!(!t.is_dark);
1185 }
1186
1187 #[test]
1188 fn theme_dracula_preset_builds() {
1189 let t = Theme::dracula();
1190 assert_eq!(t.bg, Color::Rgb(40, 42, 54));
1191 assert!(t.is_dark);
1192 }
1193
1194 #[test]
1195 fn theme_catppuccin_preset_builds() {
1196 let t = Theme::catppuccin();
1197 assert_eq!(t.bg, Color::Rgb(30, 30, 46));
1198 assert!(t.is_dark);
1199 }
1200
1201 #[test]
1202 fn theme_nord_preset_builds() {
1203 let t = Theme::nord();
1204 assert_eq!(t.bg, Color::Rgb(46, 52, 64));
1205 assert!(t.is_dark);
1206 }
1207
1208 #[test]
1209 fn theme_solarized_dark_preset_builds() {
1210 let t = Theme::solarized_dark();
1211 assert_eq!(t.bg, Color::Rgb(0, 43, 54));
1212 assert!(t.is_dark);
1213 }
1214
1215 #[test]
1216 fn theme_tokyo_night_preset_builds() {
1217 let t = Theme::tokyo_night();
1218 assert_eq!(t.bg, Color::Rgb(26, 27, 38));
1219 assert!(t.is_dark);
1220 }
1221
1222 #[test]
1223 fn theme_builder_sets_primary_and_accent() {
1224 let theme = Theme::builder()
1225 .primary(Color::Red)
1226 .accent(Color::Yellow)
1227 .build();
1228
1229 assert_eq!(theme.primary, Color::Red);
1230 assert_eq!(theme.accent, Color::Yellow);
1231 }
1232
1233 #[test]
1234 fn theme_builder_defaults_to_dark_for_unset_fields() {
1235 let defaults = Theme::dark();
1236 let theme = Theme::builder().primary(Color::Green).build();
1237
1238 assert_eq!(theme.primary, Color::Green);
1239 assert_eq!(theme.secondary, defaults.secondary);
1240 assert_eq!(theme.text, defaults.text);
1241 assert_eq!(theme.text_dim, defaults.text_dim);
1242 assert_eq!(theme.border, defaults.border);
1243 assert_eq!(theme.surface_hover, defaults.surface_hover);
1244 assert_eq!(theme.is_dark, defaults.is_dark);
1245 }
1246
1247 #[test]
1248 fn theme_builder_can_override_is_dark() {
1249 let theme = Theme::builder().is_dark(false).build();
1250 assert!(!theme.is_dark);
1251 }
1252
1253 #[test]
1254 fn theme_default_matches_dark() {
1255 let default_theme = Theme::default();
1256 let dark = Theme::dark();
1257 assert_eq!(default_theme.primary, dark.primary);
1258 assert_eq!(default_theme.bg, dark.bg);
1259 assert_eq!(default_theme.is_dark, dark.is_dark);
1260 }
1261
1262 #[test]
1263 fn theme_solarized_light_preset_builds() {
1264 let t = Theme::solarized_light();
1265 assert_eq!(t.bg, Color::Rgb(253, 246, 227));
1266 assert!(!t.is_dark);
1267 }
1268
1269 #[test]
1270 fn theme_gruvbox_dark_preset_builds() {
1271 let t = Theme::gruvbox_dark();
1272 assert_eq!(t.bg, Color::Rgb(40, 40, 40));
1273 assert!(t.is_dark);
1274 }
1275
1276 #[test]
1277 fn theme_one_dark_preset_builds() {
1278 let t = Theme::one_dark();
1279 assert_eq!(t.bg, Color::Rgb(40, 44, 52));
1280 assert!(t.is_dark);
1281 }
1282
1283 #[test]
1286 fn theme_text_dim_ne_border() {
1287 for theme in [
1288 Theme::nord(),
1289 Theme::solarized_dark(),
1290 Theme::solarized_light(),
1291 ] {
1292 assert_ne!(theme.text_dim, theme.border, "text_dim == border in theme");
1293 }
1294 }
1295
1296 #[test]
1297 fn spacing_scale_values() {
1298 let sp = Spacing::new(1);
1299 assert_eq!(sp.none(), 0);
1300 assert_eq!(sp.xs(), 1);
1301 assert_eq!(sp.sm(), 2);
1302 assert_eq!(sp.md(), 3);
1303 assert_eq!(sp.lg(), 4);
1304 assert_eq!(sp.xl(), 6);
1305 assert_eq!(sp.xxl(), 8);
1306 }
1307
1308 #[test]
1309 fn spacing_custom_base() {
1310 let sp = Spacing::new(2);
1311 assert_eq!(sp.xs(), 2);
1312 assert_eq!(sp.sm(), 4);
1313 assert_eq!(sp.md(), 6);
1314 }
1315
1316 #[test]
1317 fn theme_color_resolve_maps_correctly() {
1318 let t = Theme::dark();
1319 assert_eq!(t.resolve(ThemeColor::Primary), t.primary);
1320 assert_eq!(t.resolve(ThemeColor::Secondary), t.secondary);
1321 assert_eq!(t.resolve(ThemeColor::Accent), t.accent);
1322 assert_eq!(t.resolve(ThemeColor::Text), t.text);
1323 assert_eq!(t.resolve(ThemeColor::TextDim), t.text_dim);
1324 assert_eq!(t.resolve(ThemeColor::Border), t.border);
1325 assert_eq!(t.resolve(ThemeColor::Bg), t.bg);
1326 assert_eq!(t.resolve(ThemeColor::Success), t.success);
1327 assert_eq!(t.resolve(ThemeColor::Warning), t.warning);
1328 assert_eq!(t.resolve(ThemeColor::Error), t.error);
1329 assert_eq!(t.resolve(ThemeColor::SelectedBg), t.selected_bg);
1330 assert_eq!(t.resolve(ThemeColor::SelectedFg), t.selected_fg);
1331 assert_eq!(t.resolve(ThemeColor::Surface), t.surface);
1332 assert_eq!(t.resolve(ThemeColor::SurfaceHover), t.surface_hover);
1333 assert_eq!(t.resolve(ThemeColor::SurfaceText), t.surface_text);
1334 }
1335
1336 #[test]
1337 fn theme_color_aliases_resolve_to_primary() {
1338 let t = Theme::dark();
1339 assert_eq!(t.resolve(ThemeColor::Info), t.primary);
1340 assert_eq!(t.resolve(ThemeColor::Link), t.primary);
1341 assert_eq!(t.resolve(ThemeColor::FocusRing), t.primary);
1342 }
1343
1344 #[test]
1345 fn theme_color_custom_passes_through() {
1346 let t = Theme::dark();
1347 let custom = Color::Rgb(42, 42, 42);
1348 assert_eq!(t.resolve(ThemeColor::Custom(custom)), custom);
1349 }
1350
1351 #[test]
1352 fn theme_builder_spacing() {
1353 let sp = Spacing::new(3);
1354 let theme = Theme::builder().spacing(sp).build();
1355 assert_eq!(theme.spacing, sp);
1356 }
1357
1358 #[test]
1359 fn theme_contrast_text_on_dark_bg() {
1360 let t = Theme::dark();
1361 let fg = t.contrast_text_on(Color::Rgb(0, 0, 0));
1362 assert_eq!(fg, Color::Rgb(255, 255, 255));
1363 }
1364
1365 #[test]
1366 fn theme_contrast_text_on_light_bg() {
1367 let t = Theme::dark();
1368 let fg = t.contrast_text_on(Color::Rgb(255, 255, 255));
1369 assert_eq!(fg, Color::Rgb(0, 0, 0));
1370 }
1371
1372 const _CONST_THEME: Theme = Theme::builder()
1377 .primary(Color::Rgb(255, 100, 100))
1378 .bg(Color::Rgb(20, 20, 20))
1379 .is_dark(true)
1380 .spacing(Spacing::new(2))
1381 .build();
1382
1383 #[test]
1384 fn theme_builder_const_eval() {
1385 assert_eq!(_CONST_THEME.primary, Color::Rgb(255, 100, 100));
1387 assert_eq!(_CONST_THEME.bg, Color::Rgb(20, 20, 20));
1388 assert_eq!(_CONST_THEME.spacing, Spacing::new(2));
1389 let dark = Theme::dark();
1392 assert_eq!(_CONST_THEME.text, dark.text);
1393 assert_eq!(_CONST_THEME.border, dark.border);
1394 assert_eq!(_CONST_THEME.surface, dark.surface);
1395 }
1396
1397 #[test]
1400 fn builder_from_preserves_base_fields() {
1401 let nord = Theme::nord();
1404 let t = Theme::builder_from(nord).build();
1405 assert_eq!(t.primary, nord.primary);
1406 assert_eq!(t.secondary, nord.secondary);
1407 assert_eq!(t.accent, nord.accent);
1408 assert_eq!(t.text, nord.text);
1409 assert_eq!(t.text_dim, nord.text_dim);
1410 assert_eq!(t.border, nord.border);
1411 assert_eq!(t.bg, nord.bg);
1412 assert_eq!(t.success, nord.success);
1413 assert_eq!(t.warning, nord.warning);
1414 assert_eq!(t.error, nord.error);
1415 assert_eq!(t.selected_bg, nord.selected_bg);
1416 assert_eq!(t.selected_fg, nord.selected_fg);
1417 assert_eq!(t.surface, nord.surface);
1418 assert_eq!(t.surface_hover, nord.surface_hover);
1419 assert_eq!(t.surface_text, nord.surface_text);
1420 assert_eq!(t.is_dark, nord.is_dark);
1421 assert_eq!(t.spacing, nord.spacing);
1422 }
1423
1424 #[test]
1425 fn builder_from_overrides_only_specified_fields() {
1426 let t = Theme::builder_from(Theme::nord())
1427 .primary(Color::Rgb(255, 0, 0))
1428 .build();
1429 assert_eq!(t.primary, Color::Rgb(255, 0, 0));
1431 assert_eq!(t.bg, Theme::nord().bg);
1432 assert_eq!(t.text, Theme::nord().text);
1433 assert_ne!(t.primary, Theme::nord().primary);
1434 }
1435
1436 #[test]
1437 fn light_builder_starts_from_light_preset() {
1438 let t = Theme::light_builder()
1442 .primary(Color::Rgb(0, 100, 200))
1443 .build();
1444 let light = Theme::light();
1445 assert_eq!(t.primary, Color::Rgb(0, 100, 200));
1446 assert_eq!(t.bg, light.bg);
1447 assert_eq!(t.text, light.text);
1448 assert_eq!(t.surface, light.surface);
1449 assert!(!t.is_dark);
1450 }
1451
1452 const _CONST_LIGHT: Theme = Theme::light_builder().primary(Color::Rgb(1, 2, 3)).build();
1454
1455 #[test]
1456 fn light_builder_is_const_evaluable() {
1457 assert_eq!(_CONST_LIGHT.primary, Color::Rgb(1, 2, 3));
1458 assert_eq!(_CONST_LIGHT.bg, Theme::light().bg);
1459 const { assert!(!_CONST_LIGHT.is_dark) };
1460 }
1461}