1use crate::{Error, Result, ThemeSpec};
11use std::collections::HashMap;
12use std::path::Path;
13use std::sync::LazyLock;
14
15const PRESET_ENTRIES: &[(&str, &str)] = &[
19 ("kde-breeze", include_str!("presets/kde-breeze.toml")),
21 ("adwaita", include_str!("presets/adwaita.toml")),
22 ("windows-11", include_str!("presets/windows-11.toml")),
23 ("macos-sonoma", include_str!("presets/macos-sonoma.toml")),
24 ("material", include_str!("presets/material.toml")),
25 ("ios", include_str!("presets/ios.toml")),
26 (
28 "catppuccin-latte",
29 include_str!("presets/catppuccin-latte.toml"),
30 ),
31 (
32 "catppuccin-frappe",
33 include_str!("presets/catppuccin-frappe.toml"),
34 ),
35 (
36 "catppuccin-macchiato",
37 include_str!("presets/catppuccin-macchiato.toml"),
38 ),
39 (
40 "catppuccin-mocha",
41 include_str!("presets/catppuccin-mocha.toml"),
42 ),
43 ("nord", include_str!("presets/nord.toml")),
44 ("dracula", include_str!("presets/dracula.toml")),
45 ("gruvbox", include_str!("presets/gruvbox.toml")),
46 ("solarized", include_str!("presets/solarized.toml")),
47 ("tokyo-night", include_str!("presets/tokyo-night.toml")),
48 ("one-dark", include_str!("presets/one-dark.toml")),
49 (
51 "kde-breeze-live",
52 include_str!("presets/kde-breeze-live.toml"),
53 ),
54 ("adwaita-live", include_str!("presets/adwaita-live.toml")),
55 (
56 "macos-sonoma-live",
57 include_str!("presets/macos-sonoma-live.toml"),
58 ),
59 (
60 "windows-11-live",
61 include_str!("presets/windows-11-live.toml"),
62 ),
63];
64
65const PRESET_NAMES: &[&str] = &[
67 "kde-breeze",
68 "adwaita",
69 "windows-11",
70 "macos-sonoma",
71 "material",
72 "ios",
73 "catppuccin-latte",
74 "catppuccin-frappe",
75 "catppuccin-macchiato",
76 "catppuccin-mocha",
77 "nord",
78 "dracula",
79 "gruvbox",
80 "solarized",
81 "tokyo-night",
82 "one-dark",
83];
84
85type Parsed = std::result::Result<ThemeSpec, String>;
88
89fn parse(toml_str: &str) -> Parsed {
90 from_toml(toml_str).map_err(|e| e.to_string())
91}
92
93static CACHE: LazyLock<HashMap<&str, Parsed>> = LazyLock::new(|| {
94 PRESET_ENTRIES
95 .iter()
96 .map(|(name, toml_str)| (*name, parse(toml_str)))
97 .collect()
98});
99
100pub(crate) fn preset(name: &str) -> Result<ThemeSpec> {
101 match CACHE.get(name) {
102 None => Err(Error::Unavailable(format!("unknown preset: {name}"))),
103 Some(Ok(theme)) => Ok(theme.clone()),
104 Some(Err(msg)) => Err(Error::Format(format!("bundled preset '{name}': {msg}"))),
105 }
106}
107
108pub(crate) fn list_presets() -> &'static [&'static str] {
109 PRESET_NAMES
110}
111
112const PLATFORM_SPECIFIC: &[(&str, &[&str])] = &[
114 ("kde-breeze", &["linux-kde"]),
115 ("adwaita", &["linux"]),
116 ("windows-11", &["windows"]),
117 ("macos-sonoma", &["macos"]),
118 ("ios", &["macos", "ios"]),
119];
120
121#[allow(unreachable_code)]
125fn detect_platform() -> &'static str {
126 #[cfg(target_os = "macos")]
127 {
128 return "macos";
129 }
130 #[cfg(target_os = "windows")]
131 {
132 return "windows";
133 }
134 #[cfg(target_os = "linux")]
135 {
136 let desktop = std::env::var("XDG_CURRENT_DESKTOP").unwrap_or_default();
137 for component in desktop.split(':') {
138 if component == "KDE" {
139 return "linux-kde";
140 }
141 }
142 "linux"
143 }
144 #[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
145 {
146 "linux"
147 }
148}
149
150pub(crate) fn list_presets_for_platform() -> Vec<&'static str> {
155 let platform = detect_platform();
156
157 PRESET_NAMES
158 .iter()
159 .filter(|name| {
160 if let Some((_, platforms)) = PLATFORM_SPECIFIC.iter().find(|(n, _)| n == *name) {
161 platforms.iter().any(|p| platform.starts_with(p))
162 } else {
163 true }
165 })
166 .copied()
167 .collect()
168}
169
170pub(crate) fn from_toml(toml_str: &str) -> Result<ThemeSpec> {
171 let theme: ThemeSpec = toml::from_str(toml_str)?;
172 Ok(theme)
173}
174
175pub(crate) fn from_file(path: impl AsRef<Path>) -> Result<ThemeSpec> {
176 let contents = std::fs::read_to_string(path)?;
177 from_toml(&contents)
178}
179
180pub(crate) fn to_toml(theme: &ThemeSpec) -> Result<String> {
181 let s = toml::to_string_pretty(theme)?;
182 Ok(s)
183}
184
185#[cfg(test)]
186#[allow(clippy::unwrap_used, clippy::expect_used)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn all_presets_loadable_via_preset_fn() {
192 for name in list_presets() {
193 let theme =
194 preset(name).unwrap_or_else(|e| panic!("preset '{name}' failed to parse: {e}"));
195 assert!(
196 theme.light.is_some(),
197 "preset '{name}' missing light variant"
198 );
199 assert!(theme.dark.is_some(), "preset '{name}' missing dark variant");
200 }
201 }
202
203 #[test]
204 fn all_presets_have_nonempty_core_colors() {
205 for name in list_presets() {
206 let theme = preset(name).unwrap();
207 let light = theme.light.as_ref().unwrap();
208 let dark = theme.dark.as_ref().unwrap();
209
210 assert!(
211 light.defaults.accent.is_some(),
212 "preset '{name}' light missing accent"
213 );
214 assert!(
215 light.defaults.background.is_some(),
216 "preset '{name}' light missing background"
217 );
218 assert!(
219 light.defaults.foreground.is_some(),
220 "preset '{name}' light missing foreground"
221 );
222 assert!(
223 dark.defaults.accent.is_some(),
224 "preset '{name}' dark missing accent"
225 );
226 assert!(
227 dark.defaults.background.is_some(),
228 "preset '{name}' dark missing background"
229 );
230 assert!(
231 dark.defaults.foreground.is_some(),
232 "preset '{name}' dark missing foreground"
233 );
234 }
235 }
236
237 #[test]
238 fn preset_unknown_name_returns_unavailable() {
239 let err = preset("nonexistent").unwrap_err();
240 match err {
241 Error::Unavailable(msg) => assert!(msg.contains("nonexistent")),
242 other => panic!("expected Unavailable, got: {other:?}"),
243 }
244 }
245
246 #[test]
247 fn list_presets_returns_all_sixteen() {
248 let names = list_presets();
249 assert_eq!(names.len(), 16);
250 assert!(names.contains(&"kde-breeze"));
251 assert!(names.contains(&"adwaita"));
252 assert!(names.contains(&"windows-11"));
253 assert!(names.contains(&"macos-sonoma"));
254 assert!(names.contains(&"material"));
255 assert!(names.contains(&"ios"));
256 assert!(names.contains(&"catppuccin-latte"));
257 assert!(names.contains(&"catppuccin-frappe"));
258 assert!(names.contains(&"catppuccin-macchiato"));
259 assert!(names.contains(&"catppuccin-mocha"));
260 assert!(names.contains(&"nord"));
261 assert!(names.contains(&"dracula"));
262 assert!(names.contains(&"gruvbox"));
263 assert!(names.contains(&"solarized"));
264 assert!(names.contains(&"tokyo-night"));
265 assert!(names.contains(&"one-dark"));
266 }
267
268 #[test]
269 fn from_toml_minimal_valid() {
270 let toml_str = r##"
271name = "Minimal"
272
273[light.defaults]
274accent = "#ff0000"
275"##;
276 let theme = from_toml(toml_str).unwrap();
277 assert_eq!(theme.name, "Minimal");
278 assert!(theme.light.is_some());
279 let light = theme.light.unwrap();
280 assert_eq!(light.defaults.accent, Some(crate::Rgba::rgb(255, 0, 0)));
281 }
282
283 #[test]
284 fn from_toml_invalid_returns_format_error() {
285 let err = from_toml("{{{{invalid toml").unwrap_err();
286 match err {
287 Error::Format(_) => {}
288 other => panic!("expected Format, got: {other:?}"),
289 }
290 }
291
292 #[test]
293 fn to_toml_produces_valid_round_trip() {
294 let theme = preset("catppuccin-mocha").unwrap();
295 let toml_str = to_toml(&theme).unwrap();
296
297 let reparsed = from_toml(&toml_str).unwrap();
299 assert_eq!(reparsed.name, theme.name);
300 assert!(reparsed.light.is_some());
301 assert!(reparsed.dark.is_some());
302
303 let orig_light = theme.light.as_ref().unwrap();
305 let new_light = reparsed.light.as_ref().unwrap();
306 assert_eq!(orig_light.defaults.accent, new_light.defaults.accent);
307 }
308
309 #[test]
310 fn from_file_with_tempfile() {
311 let dir = std::env::temp_dir();
312 let path = dir.join("native_theme_test_preset.toml");
313 let toml_str = r##"
314name = "File Test"
315
316[light.defaults]
317accent = "#00ff00"
318"##;
319 std::fs::write(&path, toml_str).unwrap();
320
321 let theme = from_file(&path).unwrap();
322 assert_eq!(theme.name, "File Test");
323 assert!(theme.light.is_some());
324
325 let _ = std::fs::remove_file(&path);
327 }
328
329 #[test]
332 fn icon_set_native_presets_have_correct_values() {
333 use crate::IconSet;
334 let cases: &[(&str, IconSet)] = &[
335 ("windows-11", IconSet::SegoeIcons),
336 ("macos-sonoma", IconSet::SfSymbols),
337 ("ios", IconSet::SfSymbols),
338 ("adwaita", IconSet::Freedesktop),
339 ("kde-breeze", IconSet::Freedesktop),
340 ("material", IconSet::Material),
341 ];
342 for (name, expected) in cases {
343 let theme = preset(name).unwrap();
344 let light = theme.light.as_ref().unwrap();
345 assert_eq!(
346 light.icon_set,
347 Some(*expected),
348 "preset '{name}' light.icon_set should be Some({expected:?})"
349 );
350 let dark = theme.dark.as_ref().unwrap();
351 assert_eq!(
352 dark.icon_set,
353 Some(*expected),
354 "preset '{name}' dark.icon_set should be Some({expected:?})"
355 );
356 }
357 }
358
359 #[test]
360 fn icon_set_community_presets_have_lucide() {
361 let community = &[
362 "catppuccin-latte",
363 "catppuccin-frappe",
364 "catppuccin-macchiato",
365 "catppuccin-mocha",
366 "nord",
367 "dracula",
368 "gruvbox",
369 "solarized",
370 "tokyo-night",
371 "one-dark",
372 ];
373 for name in community {
374 let theme = preset(name).unwrap();
375 let light = theme.light.as_ref().unwrap();
376 assert_eq!(
377 light.icon_set,
378 Some(crate::IconSet::Lucide),
379 "preset '{name}' light.icon_set should be Lucide"
380 );
381 let dark = theme.dark.as_ref().unwrap();
382 assert_eq!(
383 dark.icon_set,
384 Some(crate::IconSet::Lucide),
385 "preset '{name}' dark.icon_set should be Lucide"
386 );
387 }
388 }
389
390 #[test]
391 fn icon_set_community_presets_resolve_to_platform_value() {
392 let community = &[
393 "catppuccin-latte",
394 "catppuccin-frappe",
395 "catppuccin-macchiato",
396 "catppuccin-mocha",
397 "nord",
398 "dracula",
399 "gruvbox",
400 "solarized",
401 "tokyo-night",
402 "one-dark",
403 ];
404 for name in community {
405 let theme = preset(name).unwrap();
406 let mut light = theme.light.clone().unwrap();
407 light.resolve_all();
408 assert!(
409 light.icon_set.is_some(),
410 "preset '{name}' light.icon_set should be Some after resolve_all()"
411 );
412 let mut dark = theme.dark.clone().unwrap();
413 dark.resolve_all();
414 assert!(
415 dark.icon_set.is_some(),
416 "preset '{name}' dark.icon_set should be Some after resolve_all()"
417 );
418 }
419 }
420
421 #[test]
422 fn from_file_nonexistent_returns_error() {
423 let err = from_file("/tmp/nonexistent_theme_file_12345.toml").unwrap_err();
424 match err {
425 Error::Io(e) => assert_eq!(e.kind(), std::io::ErrorKind::NotFound),
426 other => panic!("expected Io, got: {other:?}"),
427 }
428 }
429
430 #[test]
431 fn preset_names_match_list() {
432 for name in list_presets() {
434 assert!(preset(name).is_ok(), "preset '{name}' not loadable");
435 }
436 }
437
438 #[test]
439 fn presets_have_correct_names() {
440 assert_eq!(preset("kde-breeze").unwrap().name, "KDE Breeze");
441 assert_eq!(preset("adwaita").unwrap().name, "Adwaita");
442 assert_eq!(preset("windows-11").unwrap().name, "Windows 11");
443 assert_eq!(preset("macos-sonoma").unwrap().name, "macOS Sonoma");
444 assert_eq!(preset("material").unwrap().name, "Material");
445 assert_eq!(preset("ios").unwrap().name, "iOS");
446 assert_eq!(preset("catppuccin-latte").unwrap().name, "Catppuccin Latte");
447 assert_eq!(
448 preset("catppuccin-frappe").unwrap().name,
449 "Catppuccin Frappe"
450 );
451 assert_eq!(
452 preset("catppuccin-macchiato").unwrap().name,
453 "Catppuccin Macchiato"
454 );
455 assert_eq!(preset("catppuccin-mocha").unwrap().name, "Catppuccin Mocha");
456 assert_eq!(preset("nord").unwrap().name, "Nord");
457 assert_eq!(preset("dracula").unwrap().name, "Dracula");
458 assert_eq!(preset("gruvbox").unwrap().name, "Gruvbox");
459 assert_eq!(preset("solarized").unwrap().name, "Solarized");
460 assert_eq!(preset("tokyo-night").unwrap().name, "Tokyo Night");
461 assert_eq!(preset("one-dark").unwrap().name, "One Dark");
462 }
463
464 #[test]
465 fn all_presets_with_fonts_have_valid_sizes() {
466 for name in list_presets() {
467 let theme = preset(name).unwrap();
468 for (label, variant) in [
469 ("light", theme.light.as_ref()),
470 ("dark", theme.dark.as_ref()),
471 ] {
472 let variant = variant.unwrap();
473 if let Some(size) = variant.defaults.font.size {
475 assert!(
476 size > 0.0,
477 "preset '{name}' {label} font size must be positive, got {size}"
478 );
479 }
480 if let Some(mono_size) = variant.defaults.mono_font.size {
481 assert!(
482 mono_size > 0.0,
483 "preset '{name}' {label} mono font size must be positive, got {mono_size}"
484 );
485 }
486 }
487 }
488 }
489
490 #[test]
491 fn platform_presets_no_derived_fields() {
492 let platform_presets = &["kde-breeze", "adwaita", "windows-11", "macos-sonoma"];
494 for name in platform_presets {
495 let theme = preset(name).unwrap();
496 for (label, variant_opt) in [
497 ("light", theme.light.as_ref()),
498 ("dark", theme.dark.as_ref()),
499 ] {
500 let variant = variant_opt.unwrap();
501 assert!(
503 variant.button.primary_background.is_none(),
504 "preset '{name}' {label}.button.primary_background should be None (derived)"
505 );
506 assert!(
508 variant.checkbox.checked_background.is_none(),
509 "preset '{name}' {label}.checkbox.checked_background should be None (derived)"
510 );
511 assert!(
513 variant.slider.fill.is_none(),
514 "preset '{name}' {label}.slider.fill should be None (derived)"
515 );
516 assert!(
518 variant.progress_bar.fill.is_none(),
519 "preset '{name}' {label}.progress_bar.fill should be None (derived)"
520 );
521 assert!(
523 variant.switch.checked_background.is_none(),
524 "preset '{name}' {label}.switch.checked_background should be None (derived)"
525 );
526 }
527 }
528 }
529
530 #[test]
533 fn all_presets_resolve_validate() {
534 for name in list_presets() {
535 let theme = preset(name).unwrap();
536 if let Some(mut light) = theme.light.clone() {
537 light.resolve_all();
538 light.validate().unwrap_or_else(|e| {
539 panic!("preset {name} light variant failed validation: {e}");
540 });
541 }
542 if let Some(mut dark) = theme.dark.clone() {
543 dark.resolve_all();
544 dark.validate().unwrap_or_else(|e| {
545 panic!("preset {name} dark variant failed validation: {e}");
546 });
547 }
548 }
549 }
550
551 #[test]
552 fn resolve_fills_accent_derived_fields() {
553 let theme = preset("catppuccin-mocha").unwrap();
556 let mut light = theme.light.clone().unwrap();
557
558 assert!(
560 light.button.primary_background.is_none(),
561 "primary_background should be None pre-resolve"
562 );
563 assert!(
564 light.checkbox.checked_background.is_none(),
565 "checkbox.checked_background should be None pre-resolve"
566 );
567 assert!(
568 light.slider.fill.is_none(),
569 "slider.fill should be None pre-resolve"
570 );
571 assert!(
572 light.progress_bar.fill.is_none(),
573 "progress_bar.fill should be None pre-resolve"
574 );
575 assert!(
576 light.switch.checked_background.is_none(),
577 "switch.checked_background should be None pre-resolve"
578 );
579
580 light.resolve();
581
582 let accent = light.defaults.accent.unwrap();
584 assert_eq!(
585 light.button.primary_background,
586 Some(accent),
587 "button.primary_background should match accent"
588 );
589 assert_eq!(
590 light.checkbox.checked_background,
591 Some(accent),
592 "checkbox.checked_background should match accent"
593 );
594 assert_eq!(
595 light.slider.fill,
596 Some(accent),
597 "slider.fill should match accent"
598 );
599 assert_eq!(
600 light.progress_bar.fill,
601 Some(accent),
602 "progress_bar.fill should match accent"
603 );
604 assert_eq!(
605 light.switch.checked_background,
606 Some(accent),
607 "switch.checked_background should match accent"
608 );
609 }
610
611 #[test]
612 fn resolve_then_validate_produces_complete_theme() {
613 let theme = preset("catppuccin-mocha").unwrap();
614 let mut light = theme.light.clone().unwrap();
615 light.resolve_all();
616 let resolved = light.validate().unwrap();
617
618 assert_eq!(resolved.defaults.font.family, "Inter");
619 assert_eq!(resolved.defaults.font.size, 14.0);
620 assert_eq!(resolved.defaults.font.weight, 400);
621 assert_eq!(resolved.defaults.line_height, 1.2);
622 assert_eq!(resolved.defaults.radius, 8.0);
623 assert_eq!(resolved.defaults.focus_ring_width, 2.0);
624 assert_eq!(resolved.defaults.icon_sizes.toolbar, 24.0);
625 assert_eq!(resolved.defaults.text_scaling_factor, 1.0);
626 assert!(!resolved.defaults.reduce_motion);
627 assert_eq!(resolved.window.background, resolved.defaults.background);
629 assert_eq!(resolved.icon_set, crate::IconSet::Lucide);
631 }
632
633 #[test]
634 fn font_subfield_inheritance_integration() {
635 let theme = preset("catppuccin-mocha").unwrap();
638 let mut light = theme.light.clone().unwrap();
639
640 use crate::model::FontSpec;
642 light.menu.font = Some(FontSpec {
643 family: None,
644 size: Some(12.0),
645 weight: None,
646 });
647
648 light.resolve_all();
649 let resolved = light.validate().unwrap();
650
651 assert_eq!(
653 resolved.menu.font.family, "Inter",
654 "menu font family should inherit from defaults"
655 );
656 assert_eq!(
657 resolved.menu.font.size, 12.0,
658 "menu font size should be the explicit value"
659 );
660 assert_eq!(
661 resolved.menu.font.weight, 400,
662 "menu font weight should inherit from defaults"
663 );
664 }
665
666 #[test]
667 fn text_scale_inheritance_integration() {
668 let theme = preset("catppuccin-mocha").unwrap();
670 let mut light = theme.light.clone().unwrap();
671
672 light.text_scale.caption = None;
674
675 light.resolve_all();
676 let resolved = light.validate().unwrap();
677
678 assert_eq!(
680 resolved.text_scale.caption.size, 14.0,
681 "caption size from defaults.font.size"
682 );
683 assert_eq!(
684 resolved.text_scale.caption.weight, 400,
685 "caption weight from defaults.font.weight"
686 );
687 assert!(
689 (resolved.text_scale.caption.line_height - 16.8).abs() < 0.01,
690 "caption line_height should be line_height_multiplier * size = 16.8, got {}",
691 resolved.text_scale.caption.line_height
692 );
693 }
694
695 #[test]
696 fn all_presets_round_trip_exact() {
697 for name in list_presets() {
699 let theme1 =
700 preset(name).unwrap_or_else(|e| panic!("preset '{name}' failed to parse: {e}"));
701 let toml_str = to_toml(&theme1)
702 .unwrap_or_else(|e| panic!("preset '{name}' failed to serialize: {e}"));
703 let theme2 = from_toml(&toml_str)
704 .unwrap_or_else(|e| panic!("preset '{name}' failed to re-parse: {e}"));
705 assert_eq!(
706 theme1, theme2,
707 "preset '{name}' round-trip produced different value"
708 );
709 }
710 }
711
712 #[test]
715 fn live_presets_loadable() {
716 let live_names = &[
717 "kde-breeze-live",
718 "adwaita-live",
719 "macos-sonoma-live",
720 "windows-11-live",
721 ];
722 for name in live_names {
723 let theme = preset(name)
724 .unwrap_or_else(|e| panic!("live preset '{name}' failed to parse: {e}"));
725
726 assert!(
728 theme.light.is_some(),
729 "live preset '{name}' missing light variant"
730 );
731 assert!(
732 theme.dark.is_some(),
733 "live preset '{name}' missing dark variant"
734 );
735
736 let light = theme.light.as_ref().unwrap();
737 let dark = theme.dark.as_ref().unwrap();
738
739 assert!(
741 light.defaults.accent.is_none(),
742 "live preset '{name}' light should have no accent"
743 );
744 assert!(
745 light.defaults.background.is_none(),
746 "live preset '{name}' light should have no background"
747 );
748 assert!(
749 light.defaults.foreground.is_none(),
750 "live preset '{name}' light should have no foreground"
751 );
752 assert!(
753 dark.defaults.accent.is_none(),
754 "live preset '{name}' dark should have no accent"
755 );
756 assert!(
757 dark.defaults.background.is_none(),
758 "live preset '{name}' dark should have no background"
759 );
760 assert!(
761 dark.defaults.foreground.is_none(),
762 "live preset '{name}' dark should have no foreground"
763 );
764
765 assert!(
767 light.defaults.font.family.is_none(),
768 "live preset '{name}' light should have no font family"
769 );
770 assert!(
771 light.defaults.font.size.is_none(),
772 "live preset '{name}' light should have no font size"
773 );
774 assert!(
775 light.defaults.font.weight.is_none(),
776 "live preset '{name}' light should have no font weight"
777 );
778 assert!(
779 dark.defaults.font.family.is_none(),
780 "live preset '{name}' dark should have no font family"
781 );
782 assert!(
783 dark.defaults.font.size.is_none(),
784 "live preset '{name}' dark should have no font size"
785 );
786 assert!(
787 dark.defaults.font.weight.is_none(),
788 "live preset '{name}' dark should have no font weight"
789 );
790 }
791 }
792
793 #[test]
794 fn list_presets_for_platform_returns_subset() {
795 let all = list_presets();
796 let filtered = list_presets_for_platform();
797 for name in &filtered {
799 assert!(
800 all.contains(name),
801 "filtered preset '{name}' not in full list"
802 );
803 }
804 let community = &[
806 "catppuccin-latte",
807 "catppuccin-frappe",
808 "catppuccin-macchiato",
809 "catppuccin-mocha",
810 "nord",
811 "dracula",
812 "gruvbox",
813 "solarized",
814 "tokyo-night",
815 "one-dark",
816 ];
817 for name in community {
818 assert!(
819 filtered.contains(name),
820 "community preset '{name}' should always be in filtered list"
821 );
822 }
823 assert!(
825 filtered.contains(&"material"),
826 "material should always be in filtered list"
827 );
828 }
829
830 #[test]
831 fn live_presets_fail_validate_standalone() {
832 let live_names = &[
833 "kde-breeze-live",
834 "adwaita-live",
835 "macos-sonoma-live",
836 "windows-11-live",
837 ];
838 for name in live_names {
839 let theme = preset(name).unwrap();
840 let mut light = theme.light.clone().unwrap();
841 light.resolve();
842 let result = light.validate();
843 assert!(
844 result.is_err(),
845 "live preset '{name}' light should fail validation standalone (missing colors/fonts)"
846 );
847
848 let mut dark = theme.dark.clone().unwrap();
849 dark.resolve();
850 let result = dark.validate();
851 assert!(
852 result.is_err(),
853 "live preset '{name}' dark should fail validation standalone (missing colors/fonts)"
854 );
855 }
856 }
857}