1use std::collections::HashMap;
4
5use super::types::TcssColor;
6
7fn rgb_to_hsl(r: u8, g: u8, b: u8) -> (f64, f64, f64) {
9 let rf = r as f64 / 255.0;
10 let gf = g as f64 / 255.0;
11 let bf = b as f64 / 255.0;
12
13 let max = rf.max(gf).max(bf);
14 let min = rf.min(gf).min(bf);
15 let delta = max - min;
16
17 let l = (max + min) / 2.0;
18
19 if delta < 1e-10 {
20 return (0.0, 0.0, l);
21 }
22
23 let s = if l <= 0.5 {
24 delta / (max + min)
25 } else {
26 delta / (2.0 - max - min)
27 };
28
29 let h = if (max - rf).abs() < 1e-10 {
30 let mut h = (gf - bf) / delta;
31 if h < 0.0 {
32 h += 6.0;
33 }
34 h * 60.0
35 } else if (max - gf).abs() < 1e-10 {
36 ((bf - rf) / delta + 2.0) * 60.0
37 } else {
38 ((rf - gf) / delta + 4.0) * 60.0
39 };
40
41 (h, s, l)
42}
43
44fn hsl_to_rgb(h: f64, s: f64, l: f64) -> (u8, u8, u8) {
46 if s < 1e-10 {
47 let v = (l * 255.0).round() as u8;
48 return (v, v, v);
49 }
50
51 let q = if l < 0.5 {
52 l * (1.0 + s)
53 } else {
54 l + s - l * s
55 };
56 let p = 2.0 * l - q;
57 let h_norm = h / 360.0;
58
59 let hue_to_rgb = |t: f64| -> f64 {
60 let mut t = t;
61 if t < 0.0 {
62 t += 1.0;
63 }
64 if t > 1.0 {
65 t -= 1.0;
66 }
67 if t < 1.0 / 6.0 {
68 p + (q - p) * 6.0 * t
69 } else if t < 0.5 {
70 q
71 } else if t < 2.0 / 3.0 {
72 p + (q - p) * (2.0 / 3.0 - t) * 6.0
73 } else {
74 p
75 }
76 };
77
78 let r = (hue_to_rgb(h_norm + 1.0 / 3.0) * 255.0).round() as u8;
79 let g = (hue_to_rgb(h_norm) * 255.0).round() as u8;
80 let b = (hue_to_rgb(h_norm - 1.0 / 3.0) * 255.0).round() as u8;
81
82 (r, g, b)
83}
84
85pub fn lighten_color(color: TcssColor, delta: f64) -> TcssColor {
88 match color {
89 TcssColor::Rgb(r, g, b) => {
90 let (h, s, l) = rgb_to_hsl(r, g, b);
91 let new_l = (l + delta).clamp(0.0, 1.0);
92 let (nr, ng, nb) = hsl_to_rgb(h, s, new_l);
93 TcssColor::Rgb(nr, ng, nb)
94 }
95 other => other,
96 }
97}
98
99#[derive(Debug, Clone)]
105pub struct Theme {
106 pub name: String,
108 pub primary: (u8, u8, u8),
110 pub secondary: (u8, u8, u8),
112 pub accent: (u8, u8, u8),
114 pub surface: (u8, u8, u8),
116 pub panel: (u8, u8, u8),
118 pub background: (u8, u8, u8),
120 pub foreground: (u8, u8, u8),
122 pub success: (u8, u8, u8),
124 pub warning: (u8, u8, u8),
126 pub error: (u8, u8, u8),
128 pub dark: bool,
130 pub luminosity_spread: f64,
132 pub variables: HashMap<String, TcssColor>,
134}
135
136impl Theme {
137 pub fn resolve(&self, name: &str) -> Option<TcssColor> {
143 if let Some(color) = self.variables.get(name) {
145 return Some(*color);
146 }
147
148 let (base_name, shade_delta) = if let Some(rest) = name.strip_suffix("-lighten-1") {
150 (rest, Some(1))
151 } else if let Some(rest) = name.strip_suffix("-lighten-2") {
152 (rest, Some(2))
153 } else if let Some(rest) = name.strip_suffix("-lighten-3") {
154 (rest, Some(3))
155 } else if let Some(rest) = name.strip_suffix("-darken-1") {
156 (rest, Some(-1))
157 } else if let Some(rest) = name.strip_suffix("-darken-2") {
158 (rest, Some(-2))
159 } else if let Some(rest) = name.strip_suffix("-darken-3") {
160 (rest, Some(-3))
161 } else {
162 (name, None)
163 };
164
165 let base_rgb = match base_name {
167 "primary" => Some(self.primary),
168 "secondary" => Some(self.secondary),
169 "accent" => Some(self.accent),
170 "surface" => Some(self.surface),
171 "panel" => Some(self.panel),
172 "background" => Some(self.background),
173 "foreground" => Some(self.foreground),
174 "success" => Some(self.success),
175 "warning" => Some(self.warning),
176 "error" => Some(self.error),
177 _ => None,
178 }?;
179
180 let base_color = TcssColor::Rgb(base_rgb.0, base_rgb.1, base_rgb.2);
181
182 match shade_delta {
183 None => Some(base_color),
184 Some(n) => {
185 let step = self.luminosity_spread / 2.0;
186 let delta = n as f64 * step;
187 Some(lighten_color(base_color, delta))
188 }
189 }
190 }
191}
192
193fn blend_rgb(a: (u8, u8, u8), b: (u8, u8, u8), factor: f64) -> (u8, u8, u8) {
195 let r = (a.0 as f64 * (1.0 - factor) + b.0 as f64 * factor).round() as u8;
196 let g = (a.1 as f64 * (1.0 - factor) + b.1 as f64 * factor).round() as u8;
197 let b_val = (a.2 as f64 * (1.0 - factor) + b.2 as f64 * factor).round() as u8;
198 (r, g, b_val)
199}
200
201pub fn default_dark_theme() -> Theme {
203 let primary = (1, 120, 212);
204 let surface = (30, 30, 30);
205 let panel = blend_rgb(surface, primary, 0.1);
206
207 Theme {
208 name: "textual-dark".to_string(),
209 primary,
210 secondary: (0, 69, 120),
211 accent: (255, 166, 43),
212 surface,
213 panel,
214 background: (18, 18, 18),
215 foreground: (224, 224, 224),
216 success: (78, 191, 113),
217 warning: (255, 166, 43),
218 error: (186, 60, 91),
219 dark: true,
220 luminosity_spread: 0.15,
221 variables: HashMap::new(),
222 }
223}
224
225pub fn default_light_theme() -> Theme {
227 let primary = (0, 120, 212);
228 let surface = (242, 242, 242);
229 let panel = blend_rgb(surface, primary, 0.1);
230
231 Theme {
232 name: "textual-light".to_string(),
233 primary,
234 secondary: (26, 95, 180),
235 accent: (214, 122, 0),
236 surface,
237 panel,
238 background: (255, 255, 255),
239 foreground: (36, 36, 36),
240 success: (22, 128, 57),
241 warning: (214, 122, 0),
242 error: (196, 43, 28),
243 dark: false,
244 luminosity_spread: 0.15,
245 variables: HashMap::new(),
246 }
247}
248
249pub fn tokyo_night_theme() -> Theme {
251 let bg = (26, 27, 38);
252 let primary = (122, 162, 247);
253 let surface = (36, 40, 59);
254 let panel = blend_rgb(surface, primary, 0.1);
255
256 Theme {
257 name: "tokyo-night".to_string(),
258 primary,
259 secondary: (125, 207, 255),
260 accent: (187, 154, 247),
261 surface,
262 panel,
263 background: bg,
264 foreground: (192, 202, 245),
265 success: (115, 218, 202),
266 warning: (224, 175, 104),
267 error: (247, 118, 142),
268 dark: true,
269 luminosity_spread: 0.15,
270 variables: HashMap::new(),
271 }
272}
273
274pub fn nord_theme() -> Theme {
276 let bg = (46, 52, 64);
277 let primary = (136, 192, 208);
278 let surface = (59, 66, 82);
279 let panel = blend_rgb(surface, primary, 0.1);
280
281 Theme {
282 name: "nord".to_string(),
283 primary,
284 secondary: (129, 161, 193),
285 accent: (235, 203, 139),
286 surface,
287 panel,
288 background: bg,
289 foreground: (236, 239, 244),
290 success: (163, 190, 140),
291 warning: (235, 203, 139),
292 error: (191, 97, 106),
293 dark: true,
294 luminosity_spread: 0.15,
295 variables: HashMap::new(),
296 }
297}
298
299pub fn gruvbox_dark_theme() -> Theme {
301 let bg = (40, 40, 40);
302 let primary = (69, 133, 136);
303 let surface = (50, 48, 47);
304 let panel = blend_rgb(surface, primary, 0.1);
305
306 Theme {
307 name: "gruvbox".to_string(),
308 primary,
309 secondary: (131, 165, 152),
310 accent: (215, 153, 33),
311 surface,
312 panel,
313 background: bg,
314 foreground: (235, 219, 178),
315 success: (152, 151, 26),
316 warning: (215, 153, 33),
317 error: (204, 36, 29),
318 dark: true,
319 luminosity_spread: 0.15,
320 variables: HashMap::new(),
321 }
322}
323
324pub fn dracula_theme() -> Theme {
326 let bg = (40, 42, 54);
327 let primary = (189, 147, 249);
328 let surface = (68, 71, 90);
329 let panel = blend_rgb(surface, primary, 0.1);
330
331 Theme {
332 name: "dracula".to_string(),
333 primary,
334 secondary: (139, 233, 253),
335 accent: (255, 121, 198),
336 surface,
337 panel,
338 background: bg,
339 foreground: (248, 248, 242),
340 success: (80, 250, 123),
341 warning: (241, 250, 140),
342 error: (255, 85, 85),
343 dark: true,
344 luminosity_spread: 0.15,
345 variables: HashMap::new(),
346 }
347}
348
349pub fn catppuccin_mocha_theme() -> Theme {
351 let bg = (30, 30, 46);
352 let primary = (137, 180, 250);
353 let surface = (49, 50, 68);
354 let panel = blend_rgb(surface, primary, 0.1);
355
356 Theme {
357 name: "catppuccin".to_string(),
358 primary,
359 secondary: (116, 199, 236),
360 accent: (245, 194, 231),
361 surface,
362 panel,
363 background: bg,
364 foreground: (205, 214, 244),
365 success: (166, 227, 161),
366 warning: (249, 226, 175),
367 error: (243, 139, 168),
368 dark: true,
369 luminosity_spread: 0.15,
370 variables: HashMap::new(),
371 }
372}
373
374pub fn builtin_themes() -> Vec<Theme> {
376 vec![
377 default_dark_theme(),
378 default_light_theme(),
379 tokyo_night_theme(),
380 nord_theme(),
381 gruvbox_dark_theme(),
382 dracula_theme(),
383 catppuccin_mocha_theme(),
384 ]
385}
386
387pub fn theme_by_name(name: &str) -> Option<Theme> {
389 builtin_themes().into_iter().find(|t| t.name == name)
390}
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395
396 #[test]
399 fn hsl_round_trip_pure_red() {
400 let (h, s, l) = rgb_to_hsl(255, 0, 0);
401 assert!((h - 0.0).abs() < 1.0);
402 assert!((s - 1.0).abs() < 0.01);
403 assert!((l - 0.5).abs() < 0.01);
404 let (r, g, b) = hsl_to_rgb(h, s, l);
405 assert_eq!((r, g, b), (255, 0, 0));
406 }
407
408 #[test]
409 fn hsl_round_trip_white() {
410 let (h, _s, l) = rgb_to_hsl(255, 255, 255);
411 assert!((l - 1.0).abs() < 0.01);
412 let (r, g, b) = hsl_to_rgb(h, 0.0, l);
413 assert_eq!((r, g, b), (255, 255, 255));
414 }
415
416 #[test]
417 fn hsl_round_trip_black() {
418 let (_h, _s, l) = rgb_to_hsl(0, 0, 0);
419 assert!((l - 0.0).abs() < 0.01);
420 let (r, g, b) = hsl_to_rgb(0.0, 0.0, l);
421 assert_eq!((r, g, b), (0, 0, 0));
422 }
423
424 #[test]
425 fn hsl_round_trip_primary_blue() {
426 let (h, s, l) = rgb_to_hsl(1, 120, 212);
428 let (r, g, b) = hsl_to_rgb(h, s, l);
429 assert!((r as i16 - 1).abs() <= 1);
430 assert!((g as i16 - 120).abs() <= 1);
431 assert!((b as i16 - 212).abs() <= 1);
432 }
433
434 #[test]
437 fn default_dark_theme_primary() {
438 let theme = default_dark_theme();
439 assert_eq!(theme.primary, (1, 120, 212));
440 }
441
442 #[test]
443 fn default_dark_theme_all_colors() {
444 let theme = default_dark_theme();
445 assert_eq!(theme.name, "textual-dark");
446 assert_eq!(theme.primary, (1, 120, 212));
447 assert_eq!(theme.secondary, (0, 69, 120));
448 assert_eq!(theme.accent, (255, 166, 43));
449 assert_eq!(theme.warning, (255, 166, 43));
450 assert_eq!(theme.error, (186, 60, 91));
451 assert_eq!(theme.success, (78, 191, 113));
452 assert_eq!(theme.foreground, (224, 224, 224));
453 assert_eq!(theme.background, (18, 18, 18));
454 assert_eq!(theme.surface, (30, 30, 30));
455 assert!(theme.dark);
456 assert!((theme.luminosity_spread - 0.15).abs() < 0.001);
457 }
458
459 #[test]
460 fn default_dark_theme_panel_blend() {
461 let theme = default_dark_theme();
462 assert_eq!(theme.panel, (27, 39, 48));
467 }
468
469 #[test]
472 fn resolve_primary_returns_rgb() {
473 let theme = default_dark_theme();
474 assert_eq!(theme.resolve("primary"), Some(TcssColor::Rgb(1, 120, 212)));
475 }
476
477 #[test]
478 fn resolve_all_base_names() {
479 let theme = default_dark_theme();
480 assert_eq!(theme.resolve("secondary"), Some(TcssColor::Rgb(0, 69, 120)));
481 assert_eq!(theme.resolve("accent"), Some(TcssColor::Rgb(255, 166, 43)));
482 assert_eq!(theme.resolve("surface"), Some(TcssColor::Rgb(30, 30, 30)));
483 assert_eq!(theme.resolve("panel"), Some(TcssColor::Rgb(27, 39, 48)));
484 assert_eq!(
485 theme.resolve("background"),
486 Some(TcssColor::Rgb(18, 18, 18))
487 );
488 assert_eq!(
489 theme.resolve("foreground"),
490 Some(TcssColor::Rgb(224, 224, 224))
491 );
492 assert_eq!(theme.resolve("success"), Some(TcssColor::Rgb(78, 191, 113)));
493 assert_eq!(theme.resolve("warning"), Some(TcssColor::Rgb(255, 166, 43)));
494 assert_eq!(theme.resolve("error"), Some(TcssColor::Rgb(186, 60, 91)));
495 }
496
497 #[test]
498 fn resolve_unknown_returns_none() {
499 let theme = default_dark_theme();
500 assert_eq!(theme.resolve("nonexistent"), None);
501 assert_eq!(theme.resolve(""), None);
502 assert_eq!(theme.resolve("primary-lighten-99"), None);
503 }
504
505 #[test]
508 fn resolve_primary_lighten_1_is_lighter() {
509 let theme = default_dark_theme();
510 let base = theme.resolve("primary").unwrap();
511 let lighter = theme.resolve("primary-lighten-1").unwrap();
512 let base_l = match base {
514 TcssColor::Rgb(r, g, b) => rgb_to_hsl(r, g, b).2,
515 _ => panic!("expected Rgb"),
516 };
517 let lighter_l = match lighter {
518 TcssColor::Rgb(r, g, b) => rgb_to_hsl(r, g, b).2,
519 _ => panic!("expected Rgb"),
520 };
521 assert!(
522 lighter_l > base_l,
523 "lighten-1 should have higher L than base"
524 );
525 }
526
527 #[test]
528 fn resolve_primary_darken_1_is_darker() {
529 let theme = default_dark_theme();
530 let base = theme.resolve("primary").unwrap();
531 let darker = theme.resolve("primary-darken-1").unwrap();
532 let base_l = match base {
533 TcssColor::Rgb(r, g, b) => rgb_to_hsl(r, g, b).2,
534 _ => panic!("expected Rgb"),
535 };
536 let darker_l = match darker {
537 TcssColor::Rgb(r, g, b) => rgb_to_hsl(r, g, b).2,
538 _ => panic!("expected Rgb"),
539 };
540 assert!(darker_l < base_l, "darken-1 should have lower L than base");
541 }
542
543 #[test]
544 fn shades_are_monotonically_ordered() {
545 let theme = default_dark_theme();
546 let names = [
547 "primary-darken-3",
548 "primary-darken-2",
549 "primary-darken-1",
550 "primary",
551 "primary-lighten-1",
552 "primary-lighten-2",
553 "primary-lighten-3",
554 ];
555 let luminosities: Vec<f64> = names
556 .iter()
557 .map(|n| {
558 let color = theme
559 .resolve(n)
560 .unwrap_or_else(|| panic!("failed to resolve {}", n));
561 match color {
562 TcssColor::Rgb(r, g, b) => rgb_to_hsl(r, g, b).2,
563 _ => panic!("expected Rgb"),
564 }
565 })
566 .collect();
567
568 for i in 1..luminosities.len() {
569 assert!(
570 luminosities[i] > luminosities[i - 1],
571 "L[{}] ({}) should be > L[{}] ({}), names: {} > {}",
572 i,
573 luminosities[i],
574 i - 1,
575 luminosities[i - 1],
576 names[i],
577 names[i - 1]
578 );
579 }
580 }
581
582 #[test]
583 fn accent_lighten_2_works() {
584 let theme = default_dark_theme();
585 let result = theme.resolve("accent-lighten-2");
586 assert!(result.is_some(), "accent-lighten-2 should resolve");
587 let base_l = match theme.resolve("accent").unwrap() {
588 TcssColor::Rgb(r, g, b) => rgb_to_hsl(r, g, b).2,
589 _ => panic!("expected Rgb"),
590 };
591 let shade_l = match result.unwrap() {
592 TcssColor::Rgb(r, g, b) => rgb_to_hsl(r, g, b).2,
593 _ => panic!("expected Rgb"),
594 };
595 assert!(shade_l > base_l);
596 }
597
598 #[test]
601 fn variables_override_computed_shades() {
602 let mut theme = default_dark_theme();
603 let override_color = TcssColor::Rgb(99, 99, 99);
604 theme
605 .variables
606 .insert("primary".to_string(), override_color);
607 assert_eq!(theme.resolve("primary"), Some(TcssColor::Rgb(99, 99, 99)));
608 }
609
610 #[test]
611 fn variables_override_shade_variant() {
612 let mut theme = default_dark_theme();
613 let override_color = TcssColor::Rgb(42, 42, 42);
614 theme
615 .variables
616 .insert("primary-lighten-1".to_string(), override_color);
617 assert_eq!(
618 theme.resolve("primary-lighten-1"),
619 Some(TcssColor::Rgb(42, 42, 42))
620 );
621 }
622
623 #[test]
626 fn lighten_color_positive_delta() {
627 let base = TcssColor::Rgb(100, 100, 100);
628 let lighter = lighten_color(base, 0.1);
629 let base_l = rgb_to_hsl(100, 100, 100).2;
630 match lighter {
631 TcssColor::Rgb(r, g, b) => {
632 let new_l = rgb_to_hsl(r, g, b).2;
633 assert!(new_l > base_l);
634 }
635 _ => panic!("expected Rgb"),
636 }
637 }
638
639 #[test]
640 fn lighten_color_negative_delta_darkens() {
641 let base = TcssColor::Rgb(100, 100, 100);
642 let darker = lighten_color(base, -0.1);
643 let base_l = rgb_to_hsl(100, 100, 100).2;
644 match darker {
645 TcssColor::Rgb(r, g, b) => {
646 let new_l = rgb_to_hsl(r, g, b).2;
647 assert!(new_l < base_l);
648 }
649 _ => panic!("expected Rgb"),
650 }
651 }
652
653 #[test]
654 fn lighten_color_clamps_to_max() {
655 let base = TcssColor::Rgb(250, 250, 250);
656 let result = lighten_color(base, 1.0);
657 match result {
658 TcssColor::Rgb(r, g, b) => {
659 let l = rgb_to_hsl(r, g, b).2;
660 assert!(l <= 1.0);
661 }
662 _ => panic!("expected Rgb"),
663 }
664 }
665
666 #[test]
667 fn lighten_color_non_rgb_unchanged() {
668 let reset = TcssColor::Reset;
669 assert_eq!(lighten_color(reset, 0.5), TcssColor::Reset);
670
671 let named = TcssColor::Named("red");
672 assert_eq!(lighten_color(named, 0.5), TcssColor::Named("red"));
673 }
674
675 #[test]
678 fn default_light_theme_colors() {
679 let theme = default_light_theme();
680 assert_eq!(theme.name, "textual-light");
681 assert_eq!(theme.primary, (0, 120, 212));
682 assert_eq!(theme.background, (255, 255, 255));
683 assert_eq!(theme.foreground, (36, 36, 36));
684 assert!(!theme.dark);
685 }
686
687 #[test]
688 fn light_theme_resolves_variables() {
689 let theme = default_light_theme();
690 assert_eq!(theme.resolve("primary"), Some(TcssColor::Rgb(0, 120, 212)));
691 assert_eq!(
692 theme.resolve("background"),
693 Some(TcssColor::Rgb(255, 255, 255))
694 );
695 assert!(theme.resolve("primary-lighten-1").is_some());
696 }
697
698 #[test]
701 fn tokyo_night_theme_colors() {
702 let theme = tokyo_night_theme();
703 assert_eq!(theme.name, "tokyo-night");
704 assert_eq!(theme.background, (26, 27, 38));
705 assert_eq!(theme.primary, (122, 162, 247));
706 assert!(theme.dark);
707 }
708
709 #[test]
710 fn nord_theme_colors() {
711 let theme = nord_theme();
712 assert_eq!(theme.name, "nord");
713 assert_eq!(theme.background, (46, 52, 64));
714 assert_eq!(theme.primary, (136, 192, 208));
715 assert!(theme.dark);
716 }
717
718 #[test]
719 fn gruvbox_dark_theme_colors() {
720 let theme = gruvbox_dark_theme();
721 assert_eq!(theme.name, "gruvbox");
722 assert_eq!(theme.background, (40, 40, 40));
723 assert_eq!(theme.primary, (69, 133, 136));
724 assert!(theme.dark);
725 }
726
727 #[test]
728 fn dracula_theme_colors() {
729 let theme = dracula_theme();
730 assert_eq!(theme.name, "dracula");
731 assert_eq!(theme.background, (40, 42, 54));
732 assert_eq!(theme.primary, (189, 147, 249));
733 assert!(theme.dark);
734 }
735
736 #[test]
737 fn catppuccin_mocha_theme_colors() {
738 let theme = catppuccin_mocha_theme();
739 assert_eq!(theme.name, "catppuccin");
740 assert_eq!(theme.background, (30, 30, 46));
741 assert_eq!(theme.primary, (137, 180, 250));
742 assert!(theme.dark);
743 }
744
745 #[test]
748 fn builtin_themes_count() {
749 let themes = builtin_themes();
750 assert_eq!(themes.len(), 7);
751 }
752
753 #[test]
754 fn builtin_themes_unique_names() {
755 let themes = builtin_themes();
756 let names: Vec<&str> = themes.iter().map(|t| t.name.as_str()).collect();
757 let mut unique = names.clone();
758 unique.sort();
759 unique.dedup();
760 assert_eq!(names.len(), unique.len(), "theme names must be unique");
761 }
762
763 #[test]
764 fn theme_by_name_found() {
765 assert!(theme_by_name("textual-dark").is_some());
766 assert!(theme_by_name("textual-light").is_some());
767 assert!(theme_by_name("tokyo-night").is_some());
768 assert!(theme_by_name("nord").is_some());
769 assert!(theme_by_name("gruvbox").is_some());
770 assert!(theme_by_name("dracula").is_some());
771 assert!(theme_by_name("catppuccin").is_some());
772 }
773
774 #[test]
775 fn theme_by_name_not_found() {
776 assert!(theme_by_name("nonexistent").is_none());
777 }
778
779 #[test]
780 fn all_themes_resolve_all_base_names() {
781 let base_names = [
782 "primary",
783 "secondary",
784 "accent",
785 "surface",
786 "panel",
787 "background",
788 "foreground",
789 "success",
790 "warning",
791 "error",
792 ];
793 for theme in builtin_themes() {
794 for name in &base_names {
795 assert!(
796 theme.resolve(name).is_some(),
797 "theme '{}' failed to resolve '{}'",
798 theme.name,
799 name
800 );
801 }
802 }
803 }
804
805 #[test]
806 fn all_themes_resolve_shades() {
807 for theme in builtin_themes() {
808 let base = theme.resolve("primary").unwrap();
810 let lighter = theme.resolve("primary-lighten-1").unwrap();
811 let darker = theme.resolve("primary-darken-1").unwrap();
812 assert_ne!(base, lighter, "theme '{}' lighten-1 == base", theme.name);
813 assert_ne!(base, darker, "theme '{}' darken-1 == base", theme.name);
814 }
815 }
816}