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_f64(&self, color: Color, alpha: f64) -> Color {
419 color.blend_f64(self.bg, alpha)
420 }
421
422 #[deprecated(
424 since = "0.22.2",
425 note = "use Theme::overlay_f64() to keep public float APIs on f64"
426 )]
427 pub fn overlay(&self, color: Color, alpha: f32) -> Color {
428 self.overlay_f64(color, f64::from(alpha))
429 }
430
431 pub const fn dark() -> Self {
433 Self {
434 primary: Color::Cyan,
435 secondary: Color::Blue,
436 accent: Color::Magenta,
437 text: Color::White,
438 text_dim: Color::Indexed(245),
439 border: Color::Indexed(240),
440 bg: Color::Reset,
441 success: Color::Green,
442 warning: Color::Yellow,
443 error: Color::Red,
444 selected_bg: Color::Cyan,
445 selected_fg: Color::Black,
446 surface: Color::Indexed(236),
447 surface_hover: Color::Indexed(238),
448 surface_text: Color::Indexed(250),
449 is_dark: true,
450 spacing: Spacing::new(1),
451 syntax: SyntaxPalette::one_dark(),
452 }
453 }
454
455 pub const fn light() -> Self {
457 Self {
458 primary: Color::Rgb(37, 99, 235),
459 secondary: Color::Rgb(14, 116, 144),
460 accent: Color::Rgb(147, 51, 234),
461 text: Color::Rgb(15, 23, 42),
462 text_dim: Color::Rgb(100, 116, 139),
463 border: Color::Rgb(203, 213, 225),
464 bg: Color::Rgb(248, 250, 252),
465 success: Color::Rgb(22, 163, 74),
466 warning: Color::Rgb(202, 138, 4),
467 error: Color::Rgb(220, 38, 38),
468 selected_bg: Color::Rgb(37, 99, 235),
469 selected_fg: Color::White,
470 surface: Color::Rgb(241, 245, 249),
471 surface_hover: Color::Rgb(226, 232, 240),
472 surface_text: Color::Rgb(51, 65, 85),
473 is_dark: false,
474 spacing: Spacing::new(1),
475 syntax: SyntaxPalette::one_light(),
476 }
477 }
478
479 pub const fn builder() -> ThemeBuilder {
496 ThemeBuilder {
497 primary: None,
498 secondary: None,
499 accent: None,
500 text: None,
501 text_dim: None,
502 border: None,
503 bg: None,
504 success: None,
505 warning: None,
506 error: None,
507 selected_bg: None,
508 selected_fg: None,
509 surface: None,
510 surface_hover: None,
511 surface_text: None,
512 is_dark: None,
513 spacing: None,
514 syntax: None,
515 }
516 }
517
518 pub const fn builder_from(base: Theme) -> ThemeBuilder {
538 ThemeBuilder {
539 primary: Some(base.primary),
540 secondary: Some(base.secondary),
541 accent: Some(base.accent),
542 text: Some(base.text),
543 text_dim: Some(base.text_dim),
544 border: Some(base.border),
545 bg: Some(base.bg),
546 success: Some(base.success),
547 warning: Some(base.warning),
548 error: Some(base.error),
549 selected_bg: Some(base.selected_bg),
550 selected_fg: Some(base.selected_fg),
551 surface: Some(base.surface),
552 surface_hover: Some(base.surface_hover),
553 surface_text: Some(base.surface_text),
554 is_dark: Some(base.is_dark),
555 spacing: Some(base.spacing),
556 syntax: Some(base.syntax),
557 }
558 }
559
560 pub const fn light_builder() -> ThemeBuilder {
578 Self::builder_from(Self::light())
579 }
580
581 pub fn dracula() -> Self {
583 Self {
584 primary: Color::Rgb(189, 147, 249),
585 secondary: Color::Rgb(139, 233, 253),
586 accent: Color::Rgb(255, 121, 198),
587 text: Color::Rgb(248, 248, 242),
588 text_dim: Color::Rgb(98, 114, 164),
589 border: Color::Rgb(68, 71, 90),
590 bg: Color::Rgb(40, 42, 54),
591 success: Color::Rgb(80, 250, 123),
592 warning: Color::Rgb(241, 250, 140),
593 error: Color::Rgb(255, 85, 85),
594 selected_bg: Color::Rgb(189, 147, 249),
595 selected_fg: Color::Rgb(40, 42, 54),
596 surface: Color::Rgb(68, 71, 90),
597 surface_hover: Color::Rgb(98, 100, 120),
598 surface_text: Color::Rgb(191, 194, 210),
599 is_dark: true,
600 spacing: Spacing::new(1),
601 syntax: SyntaxPalette::dracula(),
602 }
603 }
604
605 pub fn catppuccin() -> Self {
607 Self {
608 primary: Color::Rgb(180, 190, 254),
609 secondary: Color::Rgb(137, 180, 250),
610 accent: Color::Rgb(245, 194, 231),
611 text: Color::Rgb(205, 214, 244),
612 text_dim: Color::Rgb(127, 132, 156),
613 border: Color::Rgb(88, 91, 112),
614 bg: Color::Rgb(30, 30, 46),
615 success: Color::Rgb(166, 227, 161),
616 warning: Color::Rgb(249, 226, 175),
617 error: Color::Rgb(243, 139, 168),
618 selected_bg: Color::Rgb(180, 190, 254),
619 selected_fg: Color::Rgb(30, 30, 46),
620 surface: Color::Rgb(49, 50, 68),
621 surface_hover: Color::Rgb(69, 71, 90),
622 surface_text: Color::Rgb(166, 173, 200),
623 is_dark: true,
624 spacing: Spacing::new(1),
625 syntax: SyntaxPalette::catppuccin(),
626 }
627 }
628
629 pub fn nord() -> Self {
631 Self {
632 primary: Color::Rgb(136, 192, 208),
633 secondary: Color::Rgb(129, 161, 193),
634 accent: Color::Rgb(180, 142, 173),
635 text: Color::Rgb(236, 239, 244),
636 text_dim: Color::Rgb(216, 222, 233),
637 border: Color::Rgb(59, 66, 82),
638 bg: Color::Rgb(46, 52, 64),
639 success: Color::Rgb(163, 190, 140),
640 warning: Color::Rgb(235, 203, 139),
641 error: Color::Rgb(191, 97, 106),
642 selected_bg: Color::Rgb(136, 192, 208),
643 selected_fg: Color::Rgb(46, 52, 64),
644 surface: Color::Rgb(59, 66, 82),
645 surface_hover: Color::Rgb(67, 76, 94),
646 surface_text: Color::Rgb(216, 222, 233),
647 is_dark: true,
648 spacing: Spacing::new(1),
649 syntax: SyntaxPalette::nord(),
650 }
651 }
652
653 pub fn solarized_dark() -> Self {
655 Self {
656 primary: Color::Rgb(38, 139, 210),
657 secondary: Color::Rgb(42, 161, 152),
658 accent: Color::Rgb(211, 54, 130),
659 text: Color::Rgb(131, 148, 150),
660 text_dim: Color::Rgb(101, 123, 131),
661 border: Color::Rgb(7, 54, 66),
662 bg: Color::Rgb(0, 43, 54),
663 success: Color::Rgb(133, 153, 0),
664 warning: Color::Rgb(181, 137, 0),
665 error: Color::Rgb(220, 50, 47),
666 selected_bg: Color::Rgb(38, 139, 210),
667 selected_fg: Color::Rgb(253, 246, 227),
668 surface: Color::Rgb(7, 54, 66),
669 surface_hover: Color::Rgb(23, 72, 85),
670 surface_text: Color::Rgb(147, 161, 161),
671 is_dark: true,
672 spacing: Spacing::new(1),
673 syntax: SyntaxPalette::solarized_dark(),
674 }
675 }
676
677 pub fn solarized_light() -> Self {
679 Self {
680 primary: Color::Rgb(38, 139, 210),
681 secondary: Color::Rgb(42, 161, 152),
682 accent: Color::Rgb(211, 54, 130),
683 text: Color::Rgb(101, 123, 131),
684 text_dim: Color::Rgb(88, 110, 117),
685 border: Color::Rgb(238, 232, 213),
686 bg: Color::Rgb(253, 246, 227),
687 success: Color::Rgb(133, 153, 0),
688 warning: Color::Rgb(181, 137, 0),
689 error: Color::Rgb(220, 50, 47),
690 selected_bg: Color::Rgb(38, 139, 210),
691 selected_fg: Color::Rgb(253, 246, 227),
692 surface: Color::Rgb(238, 232, 213),
693 surface_hover: Color::Rgb(227, 221, 201),
694 surface_text: Color::Rgb(88, 110, 117),
695 is_dark: false,
696 spacing: Spacing::new(1),
697 syntax: SyntaxPalette::solarized_light(),
698 }
699 }
700
701 pub fn tokyo_night() -> Self {
703 Self {
704 primary: Color::Rgb(122, 162, 247),
705 secondary: Color::Rgb(125, 207, 255),
706 accent: Color::Rgb(187, 154, 247),
707 text: Color::Rgb(169, 177, 214),
708 text_dim: Color::Rgb(86, 95, 137),
709 border: Color::Rgb(54, 58, 79),
710 bg: Color::Rgb(26, 27, 38),
711 success: Color::Rgb(158, 206, 106),
712 warning: Color::Rgb(224, 175, 104),
713 error: Color::Rgb(247, 118, 142),
714 selected_bg: Color::Rgb(122, 162, 247),
715 selected_fg: Color::Rgb(26, 27, 38),
716 surface: Color::Rgb(36, 40, 59),
717 surface_hover: Color::Rgb(41, 46, 66),
718 surface_text: Color::Rgb(192, 202, 245),
719 is_dark: true,
720 spacing: Spacing::new(1),
721 syntax: SyntaxPalette::tokyo_night(),
722 }
723 }
724
725 pub fn gruvbox_dark() -> Self {
727 Self {
728 primary: Color::Rgb(215, 153, 33),
729 secondary: Color::Rgb(69, 133, 136),
730 accent: Color::Rgb(177, 98, 134),
731 text: Color::Rgb(235, 219, 178),
732 text_dim: Color::Rgb(146, 131, 116),
733 border: Color::Rgb(80, 73, 69),
734 bg: Color::Rgb(40, 40, 40),
735 success: Color::Rgb(152, 151, 26),
736 warning: Color::Rgb(250, 189, 47),
737 error: Color::Rgb(204, 36, 29),
738 selected_bg: Color::Rgb(215, 153, 33),
739 selected_fg: Color::Rgb(40, 40, 40),
740 surface: Color::Rgb(60, 56, 54),
741 surface_hover: Color::Rgb(80, 73, 69),
742 surface_text: Color::Rgb(189, 174, 147),
743 is_dark: true,
744 spacing: Spacing::new(1),
745 syntax: SyntaxPalette::gruvbox_dark(),
746 }
747 }
748
749 pub const fn compact() -> Self {
763 let base = Self::dark();
764 Self {
765 spacing: Spacing::new(1),
766 ..base
767 }
768 }
769
770 pub const fn comfortable() -> Self {
786 let base = Self::dark();
787 Self {
788 spacing: Spacing::new(2),
789 ..base
790 }
791 }
792
793 pub const fn spacious() -> Self {
809 let base = Self::dark();
810 Self {
811 spacing: Spacing::new(3),
812 ..base
813 }
814 }
815
816 pub const fn with_spacing(mut self, spacing: Spacing) -> Self {
832 self.spacing = spacing;
833 self
834 }
835
836 pub fn one_dark() -> Self {
838 Self {
839 primary: Color::Rgb(97, 175, 239),
840 secondary: Color::Rgb(86, 182, 194),
841 accent: Color::Rgb(198, 120, 221),
842 text: Color::Rgb(171, 178, 191),
843 text_dim: Color::Rgb(92, 99, 112),
844 border: Color::Rgb(62, 68, 81),
845 bg: Color::Rgb(40, 44, 52),
846 success: Color::Rgb(152, 195, 121),
847 warning: Color::Rgb(229, 192, 123),
848 error: Color::Rgb(224, 108, 117),
849 selected_bg: Color::Rgb(97, 175, 239),
850 selected_fg: Color::Rgb(40, 44, 52),
851 surface: Color::Rgb(50, 55, 65),
852 surface_hover: Color::Rgb(62, 68, 81),
853 surface_text: Color::Rgb(152, 159, 172),
854 is_dark: true,
855 spacing: Spacing::new(1),
856 syntax: SyntaxPalette::one_dark(),
857 }
858 }
859
860 #[cfg(feature = "serde")]
887 #[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
888 pub fn from_toml_str(src: &str) -> Result<Theme, ThemeLoadError> {
889 ThemeFile::from_toml_str(src).map(|tf| tf.theme)
890 }
891
892 #[cfg(feature = "serde")]
910 #[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
911 pub fn load(path: impl AsRef<std::path::Path>) -> Result<Theme, ThemeLoadError> {
912 ThemeFile::load(path).map(|tf| tf.theme)
913 }
914}
915
916#[must_use = "ThemeBuilder does nothing until .build() is called"]
918pub struct ThemeBuilder {
919 primary: Option<Color>,
920 secondary: Option<Color>,
921 accent: Option<Color>,
922 text: Option<Color>,
923 text_dim: Option<Color>,
924 border: Option<Color>,
925 bg: Option<Color>,
926 success: Option<Color>,
927 warning: Option<Color>,
928 error: Option<Color>,
929 selected_bg: Option<Color>,
930 selected_fg: Option<Color>,
931 surface: Option<Color>,
932 surface_hover: Option<Color>,
933 surface_text: Option<Color>,
934 is_dark: Option<bool>,
935 spacing: Option<Spacing>,
936 syntax: Option<SyntaxPalette>,
937}
938
939impl ThemeBuilder {
940 pub const fn primary(mut self, color: Color) -> Self {
942 self.primary = Some(color);
943 self
944 }
945
946 pub const fn secondary(mut self, color: Color) -> Self {
948 self.secondary = Some(color);
949 self
950 }
951
952 pub const fn accent(mut self, color: Color) -> Self {
954 self.accent = Some(color);
955 self
956 }
957
958 pub const fn text(mut self, color: Color) -> Self {
960 self.text = Some(color);
961 self
962 }
963
964 pub const fn text_dim(mut self, color: Color) -> Self {
966 self.text_dim = Some(color);
967 self
968 }
969
970 pub const fn border(mut self, color: Color) -> Self {
972 self.border = Some(color);
973 self
974 }
975
976 pub const fn bg(mut self, color: Color) -> Self {
978 self.bg = Some(color);
979 self
980 }
981
982 pub const fn success(mut self, color: Color) -> Self {
984 self.success = Some(color);
985 self
986 }
987
988 pub const fn warning(mut self, color: Color) -> Self {
990 self.warning = Some(color);
991 self
992 }
993
994 pub const fn error(mut self, color: Color) -> Self {
996 self.error = Some(color);
997 self
998 }
999
1000 pub const fn selected_bg(mut self, color: Color) -> Self {
1002 self.selected_bg = Some(color);
1003 self
1004 }
1005
1006 pub const fn selected_fg(mut self, color: Color) -> Self {
1008 self.selected_fg = Some(color);
1009 self
1010 }
1011
1012 pub const fn surface(mut self, color: Color) -> Self {
1014 self.surface = Some(color);
1015 self
1016 }
1017
1018 pub const fn surface_hover(mut self, color: Color) -> Self {
1020 self.surface_hover = Some(color);
1021 self
1022 }
1023
1024 pub const fn surface_text(mut self, color: Color) -> Self {
1026 self.surface_text = Some(color);
1027 self
1028 }
1029
1030 pub const fn is_dark(mut self, is_dark: bool) -> Self {
1032 self.is_dark = Some(is_dark);
1033 self
1034 }
1035
1036 pub const fn spacing(mut self, spacing: Spacing) -> Self {
1038 self.spacing = Some(spacing);
1039 self
1040 }
1041
1042 pub const fn syntax(mut self, syntax: SyntaxPalette) -> Self {
1044 self.syntax = Some(syntax);
1045 self
1046 }
1047
1048 pub const fn build(self) -> Theme {
1054 let d = Theme::dark();
1055 Theme {
1056 primary: match self.primary {
1057 Some(c) => c,
1058 None => d.primary,
1059 },
1060 secondary: match self.secondary {
1061 Some(c) => c,
1062 None => d.secondary,
1063 },
1064 accent: match self.accent {
1065 Some(c) => c,
1066 None => d.accent,
1067 },
1068 text: match self.text {
1069 Some(c) => c,
1070 None => d.text,
1071 },
1072 text_dim: match self.text_dim {
1073 Some(c) => c,
1074 None => d.text_dim,
1075 },
1076 border: match self.border {
1077 Some(c) => c,
1078 None => d.border,
1079 },
1080 bg: match self.bg {
1081 Some(c) => c,
1082 None => d.bg,
1083 },
1084 success: match self.success {
1085 Some(c) => c,
1086 None => d.success,
1087 },
1088 warning: match self.warning {
1089 Some(c) => c,
1090 None => d.warning,
1091 },
1092 error: match self.error {
1093 Some(c) => c,
1094 None => d.error,
1095 },
1096 selected_bg: match self.selected_bg {
1097 Some(c) => c,
1098 None => d.selected_bg,
1099 },
1100 selected_fg: match self.selected_fg {
1101 Some(c) => c,
1102 None => d.selected_fg,
1103 },
1104 surface: match self.surface {
1105 Some(c) => c,
1106 None => d.surface,
1107 },
1108 surface_hover: match self.surface_hover {
1109 Some(c) => c,
1110 None => d.surface_hover,
1111 },
1112 surface_text: match self.surface_text {
1113 Some(c) => c,
1114 None => d.surface_text,
1115 },
1116 is_dark: match self.is_dark {
1117 Some(b) => b,
1118 None => d.is_dark,
1119 },
1120 spacing: match self.spacing {
1121 Some(s) => s,
1122 None => d.spacing,
1123 },
1124 syntax: match self.syntax {
1125 Some(s) => s,
1126 None => d.syntax,
1127 },
1128 }
1129 }
1130}
1131
1132impl Default for Theme {
1133 fn default() -> Self {
1134 Self::dark()
1135 }
1136}
1137
1138#[cfg(test)]
1139mod tests {
1140 use super::*;
1141
1142 #[test]
1143 fn theme_dark_preset_builds() {
1144 let t = Theme::dark();
1145 assert_eq!(t.primary, Color::Cyan);
1146 assert!(t.is_dark);
1147 }
1148
1149 #[test]
1150 fn dark_preset_uses_one_dark_syntax_palette() {
1151 assert_eq!(Theme::dark().syntax, SyntaxPalette::one_dark());
1152 assert_eq!(Theme::one_dark().syntax, SyntaxPalette::one_dark());
1153 }
1154
1155 #[test]
1156 fn presets_carry_distinct_syntax_palettes() {
1157 assert_ne!(
1159 Theme::nord().syntax.keyword,
1160 SyntaxPalette::one_dark().keyword
1161 );
1162 assert_ne!(
1163 Theme::nord().syntax.keyword,
1164 Theme::catppuccin().syntax.keyword
1165 );
1166 }
1167
1168 #[test]
1169 fn builder_syntax_setter_overrides_palette() {
1170 let custom = SyntaxPalette {
1171 keyword: Color::Rgb(1, 2, 3),
1172 ..SyntaxPalette::one_dark()
1173 };
1174 let theme = Theme::builder().syntax(custom).build();
1175 assert_eq!(theme.syntax.keyword, Color::Rgb(1, 2, 3));
1176 }
1177
1178 #[test]
1179 fn builder_from_threads_syntax_palette() {
1180 let theme = Theme::builder_from(Theme::nord())
1181 .primary(Color::Rgb(255, 0, 0))
1182 .build();
1183 assert_eq!(theme.syntax, Theme::nord().syntax);
1185 }
1186
1187 #[test]
1188 fn syntax_palette_default_is_one_dark() {
1189 assert_eq!(SyntaxPalette::default(), SyntaxPalette::one_dark());
1190 }
1191
1192 #[test]
1193 fn theme_light_preset_builds() {
1194 let t = Theme::light();
1195 assert_eq!(t.selected_fg, Color::White);
1196 assert!(!t.is_dark);
1197 }
1198
1199 #[test]
1200 fn theme_dracula_preset_builds() {
1201 let t = Theme::dracula();
1202 assert_eq!(t.bg, Color::Rgb(40, 42, 54));
1203 assert!(t.is_dark);
1204 }
1205
1206 #[test]
1207 fn theme_catppuccin_preset_builds() {
1208 let t = Theme::catppuccin();
1209 assert_eq!(t.bg, Color::Rgb(30, 30, 46));
1210 assert!(t.is_dark);
1211 }
1212
1213 #[test]
1214 fn theme_nord_preset_builds() {
1215 let t = Theme::nord();
1216 assert_eq!(t.bg, Color::Rgb(46, 52, 64));
1217 assert!(t.is_dark);
1218 }
1219
1220 #[test]
1221 fn theme_solarized_dark_preset_builds() {
1222 let t = Theme::solarized_dark();
1223 assert_eq!(t.bg, Color::Rgb(0, 43, 54));
1224 assert!(t.is_dark);
1225 }
1226
1227 #[test]
1228 fn theme_tokyo_night_preset_builds() {
1229 let t = Theme::tokyo_night();
1230 assert_eq!(t.bg, Color::Rgb(26, 27, 38));
1231 assert!(t.is_dark);
1232 }
1233
1234 #[test]
1235 fn theme_builder_sets_primary_and_accent() {
1236 let theme = Theme::builder()
1237 .primary(Color::Red)
1238 .accent(Color::Yellow)
1239 .build();
1240
1241 assert_eq!(theme.primary, Color::Red);
1242 assert_eq!(theme.accent, Color::Yellow);
1243 }
1244
1245 #[test]
1246 fn theme_builder_defaults_to_dark_for_unset_fields() {
1247 let defaults = Theme::dark();
1248 let theme = Theme::builder().primary(Color::Green).build();
1249
1250 assert_eq!(theme.primary, Color::Green);
1251 assert_eq!(theme.secondary, defaults.secondary);
1252 assert_eq!(theme.text, defaults.text);
1253 assert_eq!(theme.text_dim, defaults.text_dim);
1254 assert_eq!(theme.border, defaults.border);
1255 assert_eq!(theme.surface_hover, defaults.surface_hover);
1256 assert_eq!(theme.is_dark, defaults.is_dark);
1257 }
1258
1259 #[test]
1260 fn theme_builder_can_override_is_dark() {
1261 let theme = Theme::builder().is_dark(false).build();
1262 assert!(!theme.is_dark);
1263 }
1264
1265 #[test]
1266 fn theme_default_matches_dark() {
1267 let default_theme = Theme::default();
1268 let dark = Theme::dark();
1269 assert_eq!(default_theme.primary, dark.primary);
1270 assert_eq!(default_theme.bg, dark.bg);
1271 assert_eq!(default_theme.is_dark, dark.is_dark);
1272 }
1273
1274 #[test]
1275 fn theme_solarized_light_preset_builds() {
1276 let t = Theme::solarized_light();
1277 assert_eq!(t.bg, Color::Rgb(253, 246, 227));
1278 assert!(!t.is_dark);
1279 }
1280
1281 #[test]
1282 fn theme_gruvbox_dark_preset_builds() {
1283 let t = Theme::gruvbox_dark();
1284 assert_eq!(t.bg, Color::Rgb(40, 40, 40));
1285 assert!(t.is_dark);
1286 }
1287
1288 #[test]
1289 fn theme_one_dark_preset_builds() {
1290 let t = Theme::one_dark();
1291 assert_eq!(t.bg, Color::Rgb(40, 44, 52));
1292 assert!(t.is_dark);
1293 }
1294
1295 #[test]
1298 fn theme_text_dim_ne_border() {
1299 for theme in [
1300 Theme::nord(),
1301 Theme::solarized_dark(),
1302 Theme::solarized_light(),
1303 ] {
1304 assert_ne!(theme.text_dim, theme.border, "text_dim == border in theme");
1305 }
1306 }
1307
1308 #[test]
1309 fn spacing_scale_values() {
1310 let sp = Spacing::new(1);
1311 assert_eq!(sp.none(), 0);
1312 assert_eq!(sp.xs(), 1);
1313 assert_eq!(sp.sm(), 2);
1314 assert_eq!(sp.md(), 3);
1315 assert_eq!(sp.lg(), 4);
1316 assert_eq!(sp.xl(), 6);
1317 assert_eq!(sp.xxl(), 8);
1318 }
1319
1320 #[test]
1321 fn spacing_custom_base() {
1322 let sp = Spacing::new(2);
1323 assert_eq!(sp.xs(), 2);
1324 assert_eq!(sp.sm(), 4);
1325 assert_eq!(sp.md(), 6);
1326 }
1327
1328 #[test]
1329 fn theme_color_resolve_maps_correctly() {
1330 let t = Theme::dark();
1331 assert_eq!(t.resolve(ThemeColor::Primary), t.primary);
1332 assert_eq!(t.resolve(ThemeColor::Secondary), t.secondary);
1333 assert_eq!(t.resolve(ThemeColor::Accent), t.accent);
1334 assert_eq!(t.resolve(ThemeColor::Text), t.text);
1335 assert_eq!(t.resolve(ThemeColor::TextDim), t.text_dim);
1336 assert_eq!(t.resolve(ThemeColor::Border), t.border);
1337 assert_eq!(t.resolve(ThemeColor::Bg), t.bg);
1338 assert_eq!(t.resolve(ThemeColor::Success), t.success);
1339 assert_eq!(t.resolve(ThemeColor::Warning), t.warning);
1340 assert_eq!(t.resolve(ThemeColor::Error), t.error);
1341 assert_eq!(t.resolve(ThemeColor::SelectedBg), t.selected_bg);
1342 assert_eq!(t.resolve(ThemeColor::SelectedFg), t.selected_fg);
1343 assert_eq!(t.resolve(ThemeColor::Surface), t.surface);
1344 assert_eq!(t.resolve(ThemeColor::SurfaceHover), t.surface_hover);
1345 assert_eq!(t.resolve(ThemeColor::SurfaceText), t.surface_text);
1346 }
1347
1348 #[test]
1349 fn theme_color_aliases_resolve_to_primary() {
1350 let t = Theme::dark();
1351 assert_eq!(t.resolve(ThemeColor::Info), t.primary);
1352 assert_eq!(t.resolve(ThemeColor::Link), t.primary);
1353 assert_eq!(t.resolve(ThemeColor::FocusRing), t.primary);
1354 }
1355
1356 #[test]
1357 fn theme_color_custom_passes_through() {
1358 let t = Theme::dark();
1359 let custom = Color::Rgb(42, 42, 42);
1360 assert_eq!(t.resolve(ThemeColor::Custom(custom)), custom);
1361 }
1362
1363 #[test]
1364 fn theme_builder_spacing() {
1365 let sp = Spacing::new(3);
1366 let theme = Theme::builder().spacing(sp).build();
1367 assert_eq!(theme.spacing, sp);
1368 }
1369
1370 #[test]
1371 fn theme_contrast_text_on_dark_bg() {
1372 let t = Theme::dark();
1373 let fg = t.contrast_text_on(Color::Rgb(0, 0, 0));
1374 assert_eq!(fg, Color::Rgb(255, 255, 255));
1375 }
1376
1377 #[test]
1378 fn theme_contrast_text_on_light_bg() {
1379 let t = Theme::dark();
1380 let fg = t.contrast_text_on(Color::Rgb(255, 255, 255));
1381 assert_eq!(fg, Color::Rgb(0, 0, 0));
1382 }
1383
1384 const _CONST_THEME: Theme = Theme::builder()
1389 .primary(Color::Rgb(255, 100, 100))
1390 .bg(Color::Rgb(20, 20, 20))
1391 .is_dark(true)
1392 .spacing(Spacing::new(2))
1393 .build();
1394
1395 #[test]
1396 fn theme_builder_const_eval() {
1397 assert_eq!(_CONST_THEME.primary, Color::Rgb(255, 100, 100));
1399 assert_eq!(_CONST_THEME.bg, Color::Rgb(20, 20, 20));
1400 assert_eq!(_CONST_THEME.spacing, Spacing::new(2));
1401 let dark = Theme::dark();
1404 assert_eq!(_CONST_THEME.text, dark.text);
1405 assert_eq!(_CONST_THEME.border, dark.border);
1406 assert_eq!(_CONST_THEME.surface, dark.surface);
1407 }
1408
1409 #[test]
1412 fn builder_from_preserves_base_fields() {
1413 let nord = Theme::nord();
1416 let t = Theme::builder_from(nord).build();
1417 assert_eq!(t.primary, nord.primary);
1418 assert_eq!(t.secondary, nord.secondary);
1419 assert_eq!(t.accent, nord.accent);
1420 assert_eq!(t.text, nord.text);
1421 assert_eq!(t.text_dim, nord.text_dim);
1422 assert_eq!(t.border, nord.border);
1423 assert_eq!(t.bg, nord.bg);
1424 assert_eq!(t.success, nord.success);
1425 assert_eq!(t.warning, nord.warning);
1426 assert_eq!(t.error, nord.error);
1427 assert_eq!(t.selected_bg, nord.selected_bg);
1428 assert_eq!(t.selected_fg, nord.selected_fg);
1429 assert_eq!(t.surface, nord.surface);
1430 assert_eq!(t.surface_hover, nord.surface_hover);
1431 assert_eq!(t.surface_text, nord.surface_text);
1432 assert_eq!(t.is_dark, nord.is_dark);
1433 assert_eq!(t.spacing, nord.spacing);
1434 }
1435
1436 #[test]
1437 fn builder_from_overrides_only_specified_fields() {
1438 let t = Theme::builder_from(Theme::nord())
1439 .primary(Color::Rgb(255, 0, 0))
1440 .build();
1441 assert_eq!(t.primary, Color::Rgb(255, 0, 0));
1443 assert_eq!(t.bg, Theme::nord().bg);
1444 assert_eq!(t.text, Theme::nord().text);
1445 assert_ne!(t.primary, Theme::nord().primary);
1446 }
1447
1448 #[test]
1449 fn light_builder_starts_from_light_preset() {
1450 let t = Theme::light_builder()
1454 .primary(Color::Rgb(0, 100, 200))
1455 .build();
1456 let light = Theme::light();
1457 assert_eq!(t.primary, Color::Rgb(0, 100, 200));
1458 assert_eq!(t.bg, light.bg);
1459 assert_eq!(t.text, light.text);
1460 assert_eq!(t.surface, light.surface);
1461 assert!(!t.is_dark);
1462 }
1463
1464 const _CONST_LIGHT: Theme = Theme::light_builder().primary(Color::Rgb(1, 2, 3)).build();
1466
1467 #[test]
1468 fn light_builder_is_const_evaluable() {
1469 assert_eq!(_CONST_LIGHT.primary, Color::Rgb(1, 2, 3));
1470 assert_eq!(_CONST_LIGHT.bg, Theme::light().bg);
1471 const { assert!(!_CONST_LIGHT.is_dark) };
1472 }
1473}