1use crate::error::ThemeResolutionError;
4use crate::model::resolved::{
5 ResolvedIconSizes, ResolvedTextScale, ResolvedTextScaleEntry, ResolvedThemeDefaults,
6 ResolvedThemeSpacing, ResolvedThemeVariant,
7};
8use crate::model::{FontSpec, ResolvedFontSpec, TextScaleEntry, ThemeVariant};
9
10fn resolve_font(widget_font: &mut Option<FontSpec>, defaults_font: &FontSpec) {
14 match widget_font {
15 None => {
16 *widget_font = Some(defaults_font.clone());
17 }
18 Some(font) => {
19 if font.family.is_none() {
20 font.family = defaults_font.family.clone();
21 }
22 if font.size.is_none() {
23 font.size = defaults_font.size;
24 }
25 if font.weight.is_none() {
26 font.weight = defaults_font.weight;
27 }
28 }
29 }
30}
31
32fn resolve_text_scale_entry(
36 entry: &mut Option<TextScaleEntry>,
37 defaults_font: &FontSpec,
38 defaults_line_height: Option<f32>,
39) {
40 let entry = entry.get_or_insert_with(TextScaleEntry::default);
41 if entry.size.is_none() {
42 entry.size = defaults_font.size;
43 }
44 if entry.weight.is_none() {
45 entry.weight = defaults_font.weight;
46 }
47 if entry.line_height.is_none()
48 && let (Some(lh_mult), Some(size)) = (defaults_line_height, entry.size)
49 {
50 entry.line_height = Some(lh_mult * size);
51 }
52}
53
54fn require<T: Clone + Default>(field: &Option<T>, path: &str, missing: &mut Vec<String>) -> T {
62 match field {
63 Some(val) => val.clone(),
64 None => {
65 missing.push(path.to_string());
66 T::default()
67 }
68 }
69}
70
71fn require_font(font: &FontSpec, prefix: &str, missing: &mut Vec<String>) -> ResolvedFontSpec {
74 let family = require(&font.family, &format!("{prefix}.family"), missing);
75 let size = require(&font.size, &format!("{prefix}.size"), missing);
76 let weight = require(&font.weight, &format!("{prefix}.weight"), missing);
77 ResolvedFontSpec {
78 family,
79 size,
80 weight,
81 }
82}
83
84fn require_font_opt(
87 font: &Option<FontSpec>,
88 prefix: &str,
89 missing: &mut Vec<String>,
90) -> ResolvedFontSpec {
91 match font {
92 None => {
93 missing.push(prefix.to_string());
94 ResolvedFontSpec::default()
95 }
96 Some(f) => {
97 let family = require(&f.family, &format!("{prefix}.family"), missing);
98 let size = require(&f.size, &format!("{prefix}.size"), missing);
99 let weight = require(&f.weight, &format!("{prefix}.weight"), missing);
100 ResolvedFontSpec {
101 family,
102 size,
103 weight,
104 }
105 }
106 }
107}
108
109fn require_text_scale_entry(
111 entry: &Option<TextScaleEntry>,
112 prefix: &str,
113 missing: &mut Vec<String>,
114) -> ResolvedTextScaleEntry {
115 match entry {
116 None => {
117 missing.push(prefix.to_string());
118 ResolvedTextScaleEntry::default()
119 }
120 Some(e) => {
121 let size = require(&e.size, &format!("{prefix}.size"), missing);
122 let weight = require(&e.weight, &format!("{prefix}.weight"), missing);
123 let line_height = require(&e.line_height, &format!("{prefix}.line_height"), missing);
124 ResolvedTextScaleEntry {
125 size,
126 weight,
127 line_height,
128 }
129 }
130 }
131}
132
133impl ThemeVariant {
134 pub fn resolve(&mut self) {
149 self.resolve_defaults_internal();
150 self.resolve_safety_nets();
151 self.resolve_widgets_from_defaults();
152 self.resolve_widget_to_widget();
153
154 if self.icon_set.is_none() {
156 self.icon_set = Some(crate::model::icons::system_icon_set());
157 }
158
159 if self.icon_theme.is_none() {
161 self.icon_theme = Some(crate::model::icons::system_icon_theme().to_string());
162 }
163 }
164
165 #[must_use = "this returns the resolved theme; it does not modify self"]
189 pub fn into_resolved(mut self) -> crate::Result<ResolvedThemeVariant> {
190 self.resolve();
191 self.validate()
192 }
193
194 fn resolve_defaults_internal(&mut self) {
197 let d = &mut self.defaults;
198
199 if d.selection.is_none() {
201 d.selection = d.accent;
202 }
203 if d.focus_ring_color.is_none() {
205 d.focus_ring_color = d.accent;
206 }
207 if d.selection_inactive.is_none() {
209 d.selection_inactive = d.selection;
210 }
211 }
212
213 fn resolve_safety_nets(&mut self) {
216 if self.input.caret.is_none() {
218 self.input.caret = self.defaults.foreground;
219 }
220 if self.scrollbar.track.is_none() {
222 self.scrollbar.track = self.defaults.background;
223 }
224 if self.spinner.fill.is_none() {
226 self.spinner.fill = self.defaults.foreground;
227 }
228 if self.popover.background.is_none() {
230 self.popover.background = self.defaults.background;
231 }
232 if self.list.background.is_none() {
234 self.list.background = self.defaults.background;
235 }
236 }
237
238 fn resolve_widgets_from_defaults(&mut self) {
241 self.resolve_color_inheritance();
242 self.resolve_font_inheritance();
243 self.resolve_text_scale();
244 }
245
246 fn resolve_color_inheritance(&mut self) {
247 let d = &self.defaults;
248
249 if self.window.background.is_none() {
251 self.window.background = d.background;
252 }
253 if self.window.foreground.is_none() {
254 self.window.foreground = d.foreground;
255 }
256 if self.window.border.is_none() {
257 self.window.border = d.border;
258 }
259 if self.window.title_bar_background.is_none() {
260 self.window.title_bar_background = d.surface;
261 }
262 if self.window.title_bar_foreground.is_none() {
263 self.window.title_bar_foreground = d.foreground;
264 }
265 if self.window.radius.is_none() {
266 self.window.radius = d.radius_lg;
267 }
268 if self.window.shadow.is_none() {
269 self.window.shadow = d.shadow_enabled;
270 }
271
272 if self.button.background.is_none() {
274 self.button.background = d.background;
275 }
276 if self.button.foreground.is_none() {
277 self.button.foreground = d.foreground;
278 }
279 if self.button.border.is_none() {
280 self.button.border = d.border;
281 }
282 if self.button.primary_bg.is_none() {
283 self.button.primary_bg = d.accent;
284 }
285 if self.button.primary_fg.is_none() {
286 self.button.primary_fg = d.accent_foreground;
287 }
288 if self.button.radius.is_none() {
289 self.button.radius = d.radius;
290 }
291 if self.button.disabled_opacity.is_none() {
292 self.button.disabled_opacity = d.disabled_opacity;
293 }
294 if self.button.shadow.is_none() {
295 self.button.shadow = d.shadow_enabled;
296 }
297
298 if self.input.background.is_none() {
300 self.input.background = d.background;
301 }
302 if self.input.foreground.is_none() {
303 self.input.foreground = d.foreground;
304 }
305 if self.input.border.is_none() {
306 self.input.border = d.border;
307 }
308 if self.input.placeholder.is_none() {
309 self.input.placeholder = d.muted;
310 }
311 if self.input.selection.is_none() {
312 self.input.selection = d.selection;
313 }
314 if self.input.selection_foreground.is_none() {
315 self.input.selection_foreground = d.selection_foreground;
316 }
317 if self.input.radius.is_none() {
318 self.input.radius = d.radius;
319 }
320 if self.input.border_width.is_none() {
321 self.input.border_width = d.frame_width;
322 }
323
324 if self.checkbox.checked_bg.is_none() {
326 self.checkbox.checked_bg = d.accent;
327 }
328 if self.checkbox.radius.is_none() {
329 self.checkbox.radius = d.radius;
330 }
331 if self.checkbox.border_width.is_none() {
332 self.checkbox.border_width = d.frame_width;
333 }
334
335 if self.menu.background.is_none() {
337 self.menu.background = d.background;
338 }
339 if self.menu.foreground.is_none() {
340 self.menu.foreground = d.foreground;
341 }
342 if self.menu.separator.is_none() {
343 self.menu.separator = d.border;
344 }
345
346 if self.tooltip.background.is_none() {
348 self.tooltip.background = d.background;
349 }
350 if self.tooltip.foreground.is_none() {
351 self.tooltip.foreground = d.foreground;
352 }
353 if self.tooltip.radius.is_none() {
354 self.tooltip.radius = d.radius;
355 }
356
357 if self.scrollbar.thumb.is_none() {
359 self.scrollbar.thumb = d.muted;
360 }
361 if self.scrollbar.thumb_hover.is_none() {
362 self.scrollbar.thumb_hover = d.muted;
363 }
364
365 if self.slider.fill.is_none() {
367 self.slider.fill = d.accent;
368 }
369 if self.slider.track.is_none() {
370 self.slider.track = d.muted;
371 }
372 if self.slider.thumb.is_none() {
373 self.slider.thumb = d.surface;
374 }
375
376 if self.progress_bar.fill.is_none() {
378 self.progress_bar.fill = d.accent;
379 }
380 if self.progress_bar.track.is_none() {
381 self.progress_bar.track = d.muted;
382 }
383 if self.progress_bar.radius.is_none() {
384 self.progress_bar.radius = d.radius;
385 }
386
387 if self.tab.background.is_none() {
389 self.tab.background = d.background;
390 }
391 if self.tab.foreground.is_none() {
392 self.tab.foreground = d.foreground;
393 }
394 if self.tab.active_background.is_none() {
395 self.tab.active_background = d.background;
396 }
397 if self.tab.active_foreground.is_none() {
398 self.tab.active_foreground = d.foreground;
399 }
400 if self.tab.bar_background.is_none() {
401 self.tab.bar_background = d.background;
402 }
403
404 if self.sidebar.background.is_none() {
406 self.sidebar.background = d.background;
407 }
408 if self.sidebar.foreground.is_none() {
409 self.sidebar.foreground = d.foreground;
410 }
411
412 if self.list.foreground.is_none() {
414 self.list.foreground = d.foreground;
415 }
416 if self.list.alternate_row.is_none() {
417 self.list.alternate_row = self.list.background;
418 }
419 if self.list.selection.is_none() {
420 self.list.selection = d.selection;
421 }
422 if self.list.selection_foreground.is_none() {
423 self.list.selection_foreground = d.selection_foreground;
424 }
425 if self.list.header_background.is_none() {
426 self.list.header_background = d.surface;
427 }
428 if self.list.header_foreground.is_none() {
429 self.list.header_foreground = d.foreground;
430 }
431 if self.list.grid_color.is_none() {
432 self.list.grid_color = d.border;
433 }
434
435 if self.popover.foreground.is_none() {
437 self.popover.foreground = d.foreground;
438 }
439 if self.popover.border.is_none() {
440 self.popover.border = d.border;
441 }
442 if self.popover.radius.is_none() {
443 self.popover.radius = d.radius_lg;
444 }
445
446 if self.separator.color.is_none() {
448 self.separator.color = d.border;
449 }
450
451 if self.switch.checked_bg.is_none() {
453 self.switch.checked_bg = d.accent;
454 }
455 if self.switch.thumb_bg.is_none() {
456 self.switch.thumb_bg = d.surface;
457 }
458
459 if self.dialog.radius.is_none() {
461 self.dialog.radius = d.radius_lg;
462 }
463
464 if self.combo_box.radius.is_none() {
466 self.combo_box.radius = d.radius;
467 }
468
469 if self.segmented_control.radius.is_none() {
471 self.segmented_control.radius = d.radius;
472 }
473
474 if self.card.background.is_none() {
476 self.card.background = d.surface;
477 }
478 if self.card.border.is_none() {
479 self.card.border = d.border;
480 }
481 if self.card.radius.is_none() {
482 self.card.radius = d.radius_lg;
483 }
484 if self.card.shadow.is_none() {
485 self.card.shadow = d.shadow_enabled;
486 }
487
488 if self.expander.radius.is_none() {
490 self.expander.radius = d.radius;
491 }
492
493 if self.link.color.is_none() {
495 self.link.color = d.link;
496 }
497 if self.link.visited.is_none() {
498 self.link.visited = d.link;
499 }
500 }
501
502 fn resolve_font_inheritance(&mut self) {
503 let defaults_font = &self.defaults.font.clone();
504 resolve_font(&mut self.window.title_bar_font, defaults_font);
505 resolve_font(&mut self.button.font, defaults_font);
506 resolve_font(&mut self.input.font, defaults_font);
507 resolve_font(&mut self.menu.font, defaults_font);
508 resolve_font(&mut self.tooltip.font, defaults_font);
509 resolve_font(&mut self.toolbar.font, defaults_font);
510 resolve_font(&mut self.status_bar.font, defaults_font);
511 resolve_font(&mut self.dialog.title_font, defaults_font);
512 }
513
514 fn resolve_text_scale(&mut self) {
515 let defaults_font = &self.defaults.font.clone();
516 let defaults_lh = self.defaults.line_height;
517 resolve_text_scale_entry(&mut self.text_scale.caption, defaults_font, defaults_lh);
518 resolve_text_scale_entry(
519 &mut self.text_scale.section_heading,
520 defaults_font,
521 defaults_lh,
522 );
523 resolve_text_scale_entry(
524 &mut self.text_scale.dialog_title,
525 defaults_font,
526 defaults_lh,
527 );
528 resolve_text_scale_entry(&mut self.text_scale.display, defaults_font, defaults_lh);
529 }
530
531 fn resolve_widget_to_widget(&mut self) {
534 if self.window.inactive_title_bar_background.is_none() {
536 self.window.inactive_title_bar_background = self.window.title_bar_background;
537 }
538 if self.window.inactive_title_bar_foreground.is_none() {
539 self.window.inactive_title_bar_foreground = self.window.title_bar_foreground;
540 }
541 }
542
543 #[must_use = "this returns the resolved theme; it does not modify self"]
556 pub fn validate(&self) -> crate::Result<ResolvedThemeVariant> {
557 let mut missing = Vec::new();
558
559 let defaults_font = require_font(&self.defaults.font, "defaults.font", &mut missing);
562 let defaults_line_height = require(
563 &self.defaults.line_height,
564 "defaults.line_height",
565 &mut missing,
566 );
567 let defaults_mono_font =
568 require_font(&self.defaults.mono_font, "defaults.mono_font", &mut missing);
569
570 let defaults_background = require(
571 &self.defaults.background,
572 "defaults.background",
573 &mut missing,
574 );
575 let defaults_foreground = require(
576 &self.defaults.foreground,
577 "defaults.foreground",
578 &mut missing,
579 );
580 let defaults_accent = require(&self.defaults.accent, "defaults.accent", &mut missing);
581 let defaults_accent_foreground = require(
582 &self.defaults.accent_foreground,
583 "defaults.accent_foreground",
584 &mut missing,
585 );
586 let defaults_surface = require(&self.defaults.surface, "defaults.surface", &mut missing);
587 let defaults_border = require(&self.defaults.border, "defaults.border", &mut missing);
588 let defaults_muted = require(&self.defaults.muted, "defaults.muted", &mut missing);
589 let defaults_shadow = require(&self.defaults.shadow, "defaults.shadow", &mut missing);
590 let defaults_link = require(&self.defaults.link, "defaults.link", &mut missing);
591 let defaults_selection =
592 require(&self.defaults.selection, "defaults.selection", &mut missing);
593 let defaults_selection_foreground = require(
594 &self.defaults.selection_foreground,
595 "defaults.selection_foreground",
596 &mut missing,
597 );
598 let defaults_selection_inactive = require(
599 &self.defaults.selection_inactive,
600 "defaults.selection_inactive",
601 &mut missing,
602 );
603 let defaults_disabled_foreground = require(
604 &self.defaults.disabled_foreground,
605 "defaults.disabled_foreground",
606 &mut missing,
607 );
608
609 let defaults_danger = require(&self.defaults.danger, "defaults.danger", &mut missing);
610 let defaults_danger_foreground = require(
611 &self.defaults.danger_foreground,
612 "defaults.danger_foreground",
613 &mut missing,
614 );
615 let defaults_warning = require(&self.defaults.warning, "defaults.warning", &mut missing);
616 let defaults_warning_foreground = require(
617 &self.defaults.warning_foreground,
618 "defaults.warning_foreground",
619 &mut missing,
620 );
621 let defaults_success = require(&self.defaults.success, "defaults.success", &mut missing);
622 let defaults_success_foreground = require(
623 &self.defaults.success_foreground,
624 "defaults.success_foreground",
625 &mut missing,
626 );
627 let defaults_info = require(&self.defaults.info, "defaults.info", &mut missing);
628 let defaults_info_foreground = require(
629 &self.defaults.info_foreground,
630 "defaults.info_foreground",
631 &mut missing,
632 );
633
634 let defaults_radius = require(&self.defaults.radius, "defaults.radius", &mut missing);
635 let defaults_radius_lg =
636 require(&self.defaults.radius_lg, "defaults.radius_lg", &mut missing);
637 let defaults_frame_width = require(
638 &self.defaults.frame_width,
639 "defaults.frame_width",
640 &mut missing,
641 );
642 let defaults_disabled_opacity = require(
643 &self.defaults.disabled_opacity,
644 "defaults.disabled_opacity",
645 &mut missing,
646 );
647 let defaults_border_opacity = require(
648 &self.defaults.border_opacity,
649 "defaults.border_opacity",
650 &mut missing,
651 );
652 let defaults_shadow_enabled = require(
653 &self.defaults.shadow_enabled,
654 "defaults.shadow_enabled",
655 &mut missing,
656 );
657
658 let defaults_focus_ring_color = require(
659 &self.defaults.focus_ring_color,
660 "defaults.focus_ring_color",
661 &mut missing,
662 );
663 let defaults_focus_ring_width = require(
664 &self.defaults.focus_ring_width,
665 "defaults.focus_ring_width",
666 &mut missing,
667 );
668 let defaults_focus_ring_offset = require(
669 &self.defaults.focus_ring_offset,
670 "defaults.focus_ring_offset",
671 &mut missing,
672 );
673
674 let defaults_spacing_xxs = require(
675 &self.defaults.spacing.xxs,
676 "defaults.spacing.xxs",
677 &mut missing,
678 );
679 let defaults_spacing_xs = require(
680 &self.defaults.spacing.xs,
681 "defaults.spacing.xs",
682 &mut missing,
683 );
684 let defaults_spacing_s =
685 require(&self.defaults.spacing.s, "defaults.spacing.s", &mut missing);
686 let defaults_spacing_m =
687 require(&self.defaults.spacing.m, "defaults.spacing.m", &mut missing);
688 let defaults_spacing_l =
689 require(&self.defaults.spacing.l, "defaults.spacing.l", &mut missing);
690 let defaults_spacing_xl = require(
691 &self.defaults.spacing.xl,
692 "defaults.spacing.xl",
693 &mut missing,
694 );
695 let defaults_spacing_xxl = require(
696 &self.defaults.spacing.xxl,
697 "defaults.spacing.xxl",
698 &mut missing,
699 );
700
701 let defaults_icon_sizes_toolbar = require(
702 &self.defaults.icon_sizes.toolbar,
703 "defaults.icon_sizes.toolbar",
704 &mut missing,
705 );
706 let defaults_icon_sizes_small = require(
707 &self.defaults.icon_sizes.small,
708 "defaults.icon_sizes.small",
709 &mut missing,
710 );
711 let defaults_icon_sizes_large = require(
712 &self.defaults.icon_sizes.large,
713 "defaults.icon_sizes.large",
714 &mut missing,
715 );
716 let defaults_icon_sizes_dialog = require(
717 &self.defaults.icon_sizes.dialog,
718 "defaults.icon_sizes.dialog",
719 &mut missing,
720 );
721 let defaults_icon_sizes_panel = require(
722 &self.defaults.icon_sizes.panel,
723 "defaults.icon_sizes.panel",
724 &mut missing,
725 );
726
727 let defaults_text_scaling_factor = require(
728 &self.defaults.text_scaling_factor,
729 "defaults.text_scaling_factor",
730 &mut missing,
731 );
732 let defaults_reduce_motion = require(
733 &self.defaults.reduce_motion,
734 "defaults.reduce_motion",
735 &mut missing,
736 );
737 let defaults_high_contrast = require(
738 &self.defaults.high_contrast,
739 "defaults.high_contrast",
740 &mut missing,
741 );
742 let defaults_reduce_transparency = require(
743 &self.defaults.reduce_transparency,
744 "defaults.reduce_transparency",
745 &mut missing,
746 );
747
748 let ts_caption =
751 require_text_scale_entry(&self.text_scale.caption, "text_scale.caption", &mut missing);
752 let ts_section_heading = require_text_scale_entry(
753 &self.text_scale.section_heading,
754 "text_scale.section_heading",
755 &mut missing,
756 );
757 let ts_dialog_title = require_text_scale_entry(
758 &self.text_scale.dialog_title,
759 "text_scale.dialog_title",
760 &mut missing,
761 );
762 let ts_display =
763 require_text_scale_entry(&self.text_scale.display, "text_scale.display", &mut missing);
764
765 let window_background = require(&self.window.background, "window.background", &mut missing);
768 let window_foreground = require(&self.window.foreground, "window.foreground", &mut missing);
769 let window_border = require(&self.window.border, "window.border", &mut missing);
770 let window_title_bar_background = require(
771 &self.window.title_bar_background,
772 "window.title_bar_background",
773 &mut missing,
774 );
775 let window_title_bar_foreground = require(
776 &self.window.title_bar_foreground,
777 "window.title_bar_foreground",
778 &mut missing,
779 );
780 let window_inactive_title_bar_background = require(
781 &self.window.inactive_title_bar_background,
782 "window.inactive_title_bar_background",
783 &mut missing,
784 );
785 let window_inactive_title_bar_foreground = require(
786 &self.window.inactive_title_bar_foreground,
787 "window.inactive_title_bar_foreground",
788 &mut missing,
789 );
790 let window_radius = require(&self.window.radius, "window.radius", &mut missing);
791 let window_shadow = require(&self.window.shadow, "window.shadow", &mut missing);
792 let window_title_bar_font = require_font_opt(
793 &self.window.title_bar_font,
794 "window.title_bar_font",
795 &mut missing,
796 );
797
798 let button_background = require(&self.button.background, "button.background", &mut missing);
801 let button_foreground = require(&self.button.foreground, "button.foreground", &mut missing);
802 let button_border = require(&self.button.border, "button.border", &mut missing);
803 let button_primary_bg = require(&self.button.primary_bg, "button.primary_bg", &mut missing);
804 let button_primary_fg = require(&self.button.primary_fg, "button.primary_fg", &mut missing);
805 let button_min_width = require(&self.button.min_width, "button.min_width", &mut missing);
806 let button_min_height = require(&self.button.min_height, "button.min_height", &mut missing);
807 let button_padding_horizontal = require(
808 &self.button.padding_horizontal,
809 "button.padding_horizontal",
810 &mut missing,
811 );
812 let button_padding_vertical = require(
813 &self.button.padding_vertical,
814 "button.padding_vertical",
815 &mut missing,
816 );
817 let button_radius = require(&self.button.radius, "button.radius", &mut missing);
818 let button_icon_spacing = require(
819 &self.button.icon_spacing,
820 "button.icon_spacing",
821 &mut missing,
822 );
823 let button_disabled_opacity = require(
824 &self.button.disabled_opacity,
825 "button.disabled_opacity",
826 &mut missing,
827 );
828 let button_shadow = require(&self.button.shadow, "button.shadow", &mut missing);
829 let button_font = require_font_opt(&self.button.font, "button.font", &mut missing);
830
831 let input_background = require(&self.input.background, "input.background", &mut missing);
834 let input_foreground = require(&self.input.foreground, "input.foreground", &mut missing);
835 let input_border = require(&self.input.border, "input.border", &mut missing);
836 let input_placeholder = require(&self.input.placeholder, "input.placeholder", &mut missing);
837 let input_caret = require(&self.input.caret, "input.caret", &mut missing);
838 let input_selection = require(&self.input.selection, "input.selection", &mut missing);
839 let input_selection_foreground = require(
840 &self.input.selection_foreground,
841 "input.selection_foreground",
842 &mut missing,
843 );
844 let input_min_height = require(&self.input.min_height, "input.min_height", &mut missing);
845 let input_padding_horizontal = require(
846 &self.input.padding_horizontal,
847 "input.padding_horizontal",
848 &mut missing,
849 );
850 let input_padding_vertical = require(
851 &self.input.padding_vertical,
852 "input.padding_vertical",
853 &mut missing,
854 );
855 let input_radius = require(&self.input.radius, "input.radius", &mut missing);
856 let input_border_width =
857 require(&self.input.border_width, "input.border_width", &mut missing);
858 let input_font = require_font_opt(&self.input.font, "input.font", &mut missing);
859
860 let checkbox_checked_bg = require(
863 &self.checkbox.checked_bg,
864 "checkbox.checked_bg",
865 &mut missing,
866 );
867 let checkbox_indicator_size = require(
868 &self.checkbox.indicator_size,
869 "checkbox.indicator_size",
870 &mut missing,
871 );
872 let checkbox_spacing = require(&self.checkbox.spacing, "checkbox.spacing", &mut missing);
873 let checkbox_radius = require(&self.checkbox.radius, "checkbox.radius", &mut missing);
874 let checkbox_border_width = require(
875 &self.checkbox.border_width,
876 "checkbox.border_width",
877 &mut missing,
878 );
879
880 let menu_background = require(&self.menu.background, "menu.background", &mut missing);
883 let menu_foreground = require(&self.menu.foreground, "menu.foreground", &mut missing);
884 let menu_separator = require(&self.menu.separator, "menu.separator", &mut missing);
885 let menu_item_height = require(&self.menu.item_height, "menu.item_height", &mut missing);
886 let menu_padding_horizontal = require(
887 &self.menu.padding_horizontal,
888 "menu.padding_horizontal",
889 &mut missing,
890 );
891 let menu_padding_vertical = require(
892 &self.menu.padding_vertical,
893 "menu.padding_vertical",
894 &mut missing,
895 );
896 let menu_icon_spacing = require(&self.menu.icon_spacing, "menu.icon_spacing", &mut missing);
897 let menu_font = require_font_opt(&self.menu.font, "menu.font", &mut missing);
898
899 let tooltip_background =
902 require(&self.tooltip.background, "tooltip.background", &mut missing);
903 let tooltip_foreground =
904 require(&self.tooltip.foreground, "tooltip.foreground", &mut missing);
905 let tooltip_padding_horizontal = require(
906 &self.tooltip.padding_horizontal,
907 "tooltip.padding_horizontal",
908 &mut missing,
909 );
910 let tooltip_padding_vertical = require(
911 &self.tooltip.padding_vertical,
912 "tooltip.padding_vertical",
913 &mut missing,
914 );
915 let tooltip_max_width = require(&self.tooltip.max_width, "tooltip.max_width", &mut missing);
916 let tooltip_radius = require(&self.tooltip.radius, "tooltip.radius", &mut missing);
917 let tooltip_font = require_font_opt(&self.tooltip.font, "tooltip.font", &mut missing);
918
919 let scrollbar_track = require(&self.scrollbar.track, "scrollbar.track", &mut missing);
922 let scrollbar_thumb = require(&self.scrollbar.thumb, "scrollbar.thumb", &mut missing);
923 let scrollbar_thumb_hover = require(
924 &self.scrollbar.thumb_hover,
925 "scrollbar.thumb_hover",
926 &mut missing,
927 );
928 let scrollbar_width = require(&self.scrollbar.width, "scrollbar.width", &mut missing);
929 let scrollbar_min_thumb_height = require(
930 &self.scrollbar.min_thumb_height,
931 "scrollbar.min_thumb_height",
932 &mut missing,
933 );
934 let scrollbar_slider_width = require(
935 &self.scrollbar.slider_width,
936 "scrollbar.slider_width",
937 &mut missing,
938 );
939 let scrollbar_overlay_mode = require(
940 &self.scrollbar.overlay_mode,
941 "scrollbar.overlay_mode",
942 &mut missing,
943 );
944
945 let slider_fill = require(&self.slider.fill, "slider.fill", &mut missing);
948 let slider_track = require(&self.slider.track, "slider.track", &mut missing);
949 let slider_thumb = require(&self.slider.thumb, "slider.thumb", &mut missing);
950 let slider_track_height = require(
951 &self.slider.track_height,
952 "slider.track_height",
953 &mut missing,
954 );
955 let slider_thumb_size = require(&self.slider.thumb_size, "slider.thumb_size", &mut missing);
956 let slider_tick_length =
957 require(&self.slider.tick_length, "slider.tick_length", &mut missing);
958
959 let progress_bar_fill = require(&self.progress_bar.fill, "progress_bar.fill", &mut missing);
962 let progress_bar_track =
963 require(&self.progress_bar.track, "progress_bar.track", &mut missing);
964 let progress_bar_height = require(
965 &self.progress_bar.height,
966 "progress_bar.height",
967 &mut missing,
968 );
969 let progress_bar_min_width = require(
970 &self.progress_bar.min_width,
971 "progress_bar.min_width",
972 &mut missing,
973 );
974 let progress_bar_radius = require(
975 &self.progress_bar.radius,
976 "progress_bar.radius",
977 &mut missing,
978 );
979
980 let tab_background = require(&self.tab.background, "tab.background", &mut missing);
983 let tab_foreground = require(&self.tab.foreground, "tab.foreground", &mut missing);
984 let tab_active_background = require(
985 &self.tab.active_background,
986 "tab.active_background",
987 &mut missing,
988 );
989 let tab_active_foreground = require(
990 &self.tab.active_foreground,
991 "tab.active_foreground",
992 &mut missing,
993 );
994 let tab_bar_background =
995 require(&self.tab.bar_background, "tab.bar_background", &mut missing);
996 let tab_min_width = require(&self.tab.min_width, "tab.min_width", &mut missing);
997 let tab_min_height = require(&self.tab.min_height, "tab.min_height", &mut missing);
998 let tab_padding_horizontal = require(
999 &self.tab.padding_horizontal,
1000 "tab.padding_horizontal",
1001 &mut missing,
1002 );
1003 let tab_padding_vertical = require(
1004 &self.tab.padding_vertical,
1005 "tab.padding_vertical",
1006 &mut missing,
1007 );
1008
1009 let sidebar_background =
1012 require(&self.sidebar.background, "sidebar.background", &mut missing);
1013 let sidebar_foreground =
1014 require(&self.sidebar.foreground, "sidebar.foreground", &mut missing);
1015
1016 let toolbar_height = require(&self.toolbar.height, "toolbar.height", &mut missing);
1019 let toolbar_item_spacing = require(
1020 &self.toolbar.item_spacing,
1021 "toolbar.item_spacing",
1022 &mut missing,
1023 );
1024 let toolbar_padding = require(&self.toolbar.padding, "toolbar.padding", &mut missing);
1025 let toolbar_font = require_font_opt(&self.toolbar.font, "toolbar.font", &mut missing);
1026
1027 let status_bar_font =
1030 require_font_opt(&self.status_bar.font, "status_bar.font", &mut missing);
1031
1032 let list_background = require(&self.list.background, "list.background", &mut missing);
1035 let list_foreground = require(&self.list.foreground, "list.foreground", &mut missing);
1036 let list_alternate_row =
1037 require(&self.list.alternate_row, "list.alternate_row", &mut missing);
1038 let list_selection = require(&self.list.selection, "list.selection", &mut missing);
1039 let list_selection_foreground = require(
1040 &self.list.selection_foreground,
1041 "list.selection_foreground",
1042 &mut missing,
1043 );
1044 let list_header_background = require(
1045 &self.list.header_background,
1046 "list.header_background",
1047 &mut missing,
1048 );
1049 let list_header_foreground = require(
1050 &self.list.header_foreground,
1051 "list.header_foreground",
1052 &mut missing,
1053 );
1054 let list_grid_color = require(&self.list.grid_color, "list.grid_color", &mut missing);
1055 let list_item_height = require(&self.list.item_height, "list.item_height", &mut missing);
1056 let list_padding_horizontal = require(
1057 &self.list.padding_horizontal,
1058 "list.padding_horizontal",
1059 &mut missing,
1060 );
1061 let list_padding_vertical = require(
1062 &self.list.padding_vertical,
1063 "list.padding_vertical",
1064 &mut missing,
1065 );
1066
1067 let popover_background =
1070 require(&self.popover.background, "popover.background", &mut missing);
1071 let popover_foreground =
1072 require(&self.popover.foreground, "popover.foreground", &mut missing);
1073 let popover_border = require(&self.popover.border, "popover.border", &mut missing);
1074 let popover_radius = require(&self.popover.radius, "popover.radius", &mut missing);
1075
1076 let splitter_width = require(&self.splitter.width, "splitter.width", &mut missing);
1079
1080 let separator_color = require(&self.separator.color, "separator.color", &mut missing);
1083
1084 let switch_checked_bg = require(&self.switch.checked_bg, "switch.checked_bg", &mut missing);
1087 let switch_unchecked_bg = require(
1088 &self.switch.unchecked_bg,
1089 "switch.unchecked_bg",
1090 &mut missing,
1091 );
1092 let switch_thumb_bg = require(&self.switch.thumb_bg, "switch.thumb_bg", &mut missing);
1093 let switch_track_width =
1094 require(&self.switch.track_width, "switch.track_width", &mut missing);
1095 let switch_track_height = require(
1096 &self.switch.track_height,
1097 "switch.track_height",
1098 &mut missing,
1099 );
1100 let switch_thumb_size = require(&self.switch.thumb_size, "switch.thumb_size", &mut missing);
1101 let switch_track_radius = require(
1102 &self.switch.track_radius,
1103 "switch.track_radius",
1104 &mut missing,
1105 );
1106
1107 let dialog_min_width = require(&self.dialog.min_width, "dialog.min_width", &mut missing);
1110 let dialog_max_width = require(&self.dialog.max_width, "dialog.max_width", &mut missing);
1111 let dialog_min_height = require(&self.dialog.min_height, "dialog.min_height", &mut missing);
1112 let dialog_max_height = require(&self.dialog.max_height, "dialog.max_height", &mut missing);
1113 let dialog_content_padding = require(
1114 &self.dialog.content_padding,
1115 "dialog.content_padding",
1116 &mut missing,
1117 );
1118 let dialog_button_spacing = require(
1119 &self.dialog.button_spacing,
1120 "dialog.button_spacing",
1121 &mut missing,
1122 );
1123 let dialog_radius = require(&self.dialog.radius, "dialog.radius", &mut missing);
1124 let dialog_icon_size = require(&self.dialog.icon_size, "dialog.icon_size", &mut missing);
1125 let dialog_button_order = require(
1126 &self.dialog.button_order,
1127 "dialog.button_order",
1128 &mut missing,
1129 );
1130 let dialog_title_font =
1131 require_font_opt(&self.dialog.title_font, "dialog.title_font", &mut missing);
1132
1133 let spinner_fill = require(&self.spinner.fill, "spinner.fill", &mut missing);
1136 let spinner_diameter = require(&self.spinner.diameter, "spinner.diameter", &mut missing);
1137 let spinner_min_size = require(&self.spinner.min_size, "spinner.min_size", &mut missing);
1138 let spinner_stroke_width = require(
1139 &self.spinner.stroke_width,
1140 "spinner.stroke_width",
1141 &mut missing,
1142 );
1143
1144 let combo_box_min_height = require(
1147 &self.combo_box.min_height,
1148 "combo_box.min_height",
1149 &mut missing,
1150 );
1151 let combo_box_min_width = require(
1152 &self.combo_box.min_width,
1153 "combo_box.min_width",
1154 &mut missing,
1155 );
1156 let combo_box_padding_horizontal = require(
1157 &self.combo_box.padding_horizontal,
1158 "combo_box.padding_horizontal",
1159 &mut missing,
1160 );
1161 let combo_box_arrow_size = require(
1162 &self.combo_box.arrow_size,
1163 "combo_box.arrow_size",
1164 &mut missing,
1165 );
1166 let combo_box_arrow_area_width = require(
1167 &self.combo_box.arrow_area_width,
1168 "combo_box.arrow_area_width",
1169 &mut missing,
1170 );
1171 let combo_box_radius = require(&self.combo_box.radius, "combo_box.radius", &mut missing);
1172
1173 let segmented_control_segment_height = require(
1176 &self.segmented_control.segment_height,
1177 "segmented_control.segment_height",
1178 &mut missing,
1179 );
1180 let segmented_control_separator_width = require(
1181 &self.segmented_control.separator_width,
1182 "segmented_control.separator_width",
1183 &mut missing,
1184 );
1185 let segmented_control_padding_horizontal = require(
1186 &self.segmented_control.padding_horizontal,
1187 "segmented_control.padding_horizontal",
1188 &mut missing,
1189 );
1190 let segmented_control_radius = require(
1191 &self.segmented_control.radius,
1192 "segmented_control.radius",
1193 &mut missing,
1194 );
1195
1196 let card_background = require(&self.card.background, "card.background", &mut missing);
1199 let card_border = require(&self.card.border, "card.border", &mut missing);
1200 let card_radius = require(&self.card.radius, "card.radius", &mut missing);
1201 let card_padding = require(&self.card.padding, "card.padding", &mut missing);
1202 let card_shadow = require(&self.card.shadow, "card.shadow", &mut missing);
1203
1204 let expander_header_height = require(
1207 &self.expander.header_height,
1208 "expander.header_height",
1209 &mut missing,
1210 );
1211 let expander_arrow_size = require(
1212 &self.expander.arrow_size,
1213 "expander.arrow_size",
1214 &mut missing,
1215 );
1216 let expander_content_padding = require(
1217 &self.expander.content_padding,
1218 "expander.content_padding",
1219 &mut missing,
1220 );
1221 let expander_radius = require(&self.expander.radius, "expander.radius", &mut missing);
1222
1223 let link_color = require(&self.link.color, "link.color", &mut missing);
1226 let link_visited = require(&self.link.visited, "link.visited", &mut missing);
1227 let link_background = require(&self.link.background, "link.background", &mut missing);
1228 let link_hover_bg = require(&self.link.hover_bg, "link.hover_bg", &mut missing);
1229 let link_underline = require(&self.link.underline, "link.underline", &mut missing);
1230
1231 let icon_set = require(&self.icon_set, "icon_set", &mut missing);
1234 let icon_theme = require(&self.icon_theme, "icon_theme", &mut missing);
1235
1236 if !missing.is_empty() {
1239 return Err(crate::Error::Resolution(ThemeResolutionError {
1240 missing_fields: missing,
1241 }));
1242 }
1243
1244 Ok(ResolvedThemeVariant {
1248 defaults: ResolvedThemeDefaults {
1249 font: defaults_font,
1250 line_height: defaults_line_height,
1251 mono_font: defaults_mono_font,
1252 background: defaults_background,
1253 foreground: defaults_foreground,
1254 accent: defaults_accent,
1255 accent_foreground: defaults_accent_foreground,
1256 surface: defaults_surface,
1257 border: defaults_border,
1258 muted: defaults_muted,
1259 shadow: defaults_shadow,
1260 link: defaults_link,
1261 selection: defaults_selection,
1262 selection_foreground: defaults_selection_foreground,
1263 selection_inactive: defaults_selection_inactive,
1264 disabled_foreground: defaults_disabled_foreground,
1265 danger: defaults_danger,
1266 danger_foreground: defaults_danger_foreground,
1267 warning: defaults_warning,
1268 warning_foreground: defaults_warning_foreground,
1269 success: defaults_success,
1270 success_foreground: defaults_success_foreground,
1271 info: defaults_info,
1272 info_foreground: defaults_info_foreground,
1273 radius: defaults_radius,
1274 radius_lg: defaults_radius_lg,
1275 frame_width: defaults_frame_width,
1276 disabled_opacity: defaults_disabled_opacity,
1277 border_opacity: defaults_border_opacity,
1278 shadow_enabled: defaults_shadow_enabled,
1279 focus_ring_color: defaults_focus_ring_color,
1280 focus_ring_width: defaults_focus_ring_width,
1281 focus_ring_offset: defaults_focus_ring_offset,
1282 spacing: ResolvedThemeSpacing {
1283 xxs: defaults_spacing_xxs,
1284 xs: defaults_spacing_xs,
1285 s: defaults_spacing_s,
1286 m: defaults_spacing_m,
1287 l: defaults_spacing_l,
1288 xl: defaults_spacing_xl,
1289 xxl: defaults_spacing_xxl,
1290 },
1291 icon_sizes: ResolvedIconSizes {
1292 toolbar: defaults_icon_sizes_toolbar,
1293 small: defaults_icon_sizes_small,
1294 large: defaults_icon_sizes_large,
1295 dialog: defaults_icon_sizes_dialog,
1296 panel: defaults_icon_sizes_panel,
1297 },
1298 text_scaling_factor: defaults_text_scaling_factor,
1299 reduce_motion: defaults_reduce_motion,
1300 high_contrast: defaults_high_contrast,
1301 reduce_transparency: defaults_reduce_transparency,
1302 },
1303 text_scale: ResolvedTextScale {
1304 caption: ts_caption,
1305 section_heading: ts_section_heading,
1306 dialog_title: ts_dialog_title,
1307 display: ts_display,
1308 },
1309 window: crate::model::widgets::ResolvedWindowTheme {
1310 background: window_background,
1311 foreground: window_foreground,
1312 border: window_border,
1313 title_bar_background: window_title_bar_background,
1314 title_bar_foreground: window_title_bar_foreground,
1315 inactive_title_bar_background: window_inactive_title_bar_background,
1316 inactive_title_bar_foreground: window_inactive_title_bar_foreground,
1317 radius: window_radius,
1318 shadow: window_shadow,
1319 title_bar_font: window_title_bar_font,
1320 },
1321 button: crate::model::widgets::ResolvedButtonTheme {
1322 background: button_background,
1323 foreground: button_foreground,
1324 border: button_border,
1325 primary_bg: button_primary_bg,
1326 primary_fg: button_primary_fg,
1327 min_width: button_min_width,
1328 min_height: button_min_height,
1329 padding_horizontal: button_padding_horizontal,
1330 padding_vertical: button_padding_vertical,
1331 radius: button_radius,
1332 icon_spacing: button_icon_spacing,
1333 disabled_opacity: button_disabled_opacity,
1334 shadow: button_shadow,
1335 font: button_font,
1336 },
1337 input: crate::model::widgets::ResolvedInputTheme {
1338 background: input_background,
1339 foreground: input_foreground,
1340 border: input_border,
1341 placeholder: input_placeholder,
1342 caret: input_caret,
1343 selection: input_selection,
1344 selection_foreground: input_selection_foreground,
1345 min_height: input_min_height,
1346 padding_horizontal: input_padding_horizontal,
1347 padding_vertical: input_padding_vertical,
1348 radius: input_radius,
1349 border_width: input_border_width,
1350 font: input_font,
1351 },
1352 checkbox: crate::model::widgets::ResolvedCheckboxTheme {
1353 checked_bg: checkbox_checked_bg,
1354 indicator_size: checkbox_indicator_size,
1355 spacing: checkbox_spacing,
1356 radius: checkbox_radius,
1357 border_width: checkbox_border_width,
1358 },
1359 menu: crate::model::widgets::ResolvedMenuTheme {
1360 background: menu_background,
1361 foreground: menu_foreground,
1362 separator: menu_separator,
1363 item_height: menu_item_height,
1364 padding_horizontal: menu_padding_horizontal,
1365 padding_vertical: menu_padding_vertical,
1366 icon_spacing: menu_icon_spacing,
1367 font: menu_font,
1368 },
1369 tooltip: crate::model::widgets::ResolvedTooltipTheme {
1370 background: tooltip_background,
1371 foreground: tooltip_foreground,
1372 padding_horizontal: tooltip_padding_horizontal,
1373 padding_vertical: tooltip_padding_vertical,
1374 max_width: tooltip_max_width,
1375 radius: tooltip_radius,
1376 font: tooltip_font,
1377 },
1378 scrollbar: crate::model::widgets::ResolvedScrollbarTheme {
1379 track: scrollbar_track,
1380 thumb: scrollbar_thumb,
1381 thumb_hover: scrollbar_thumb_hover,
1382 width: scrollbar_width,
1383 min_thumb_height: scrollbar_min_thumb_height,
1384 slider_width: scrollbar_slider_width,
1385 overlay_mode: scrollbar_overlay_mode,
1386 },
1387 slider: crate::model::widgets::ResolvedSliderTheme {
1388 fill: slider_fill,
1389 track: slider_track,
1390 thumb: slider_thumb,
1391 track_height: slider_track_height,
1392 thumb_size: slider_thumb_size,
1393 tick_length: slider_tick_length,
1394 },
1395 progress_bar: crate::model::widgets::ResolvedProgressBarTheme {
1396 fill: progress_bar_fill,
1397 track: progress_bar_track,
1398 height: progress_bar_height,
1399 min_width: progress_bar_min_width,
1400 radius: progress_bar_radius,
1401 },
1402 tab: crate::model::widgets::ResolvedTabTheme {
1403 background: tab_background,
1404 foreground: tab_foreground,
1405 active_background: tab_active_background,
1406 active_foreground: tab_active_foreground,
1407 bar_background: tab_bar_background,
1408 min_width: tab_min_width,
1409 min_height: tab_min_height,
1410 padding_horizontal: tab_padding_horizontal,
1411 padding_vertical: tab_padding_vertical,
1412 },
1413 sidebar: crate::model::widgets::ResolvedSidebarTheme {
1414 background: sidebar_background,
1415 foreground: sidebar_foreground,
1416 },
1417 toolbar: crate::model::widgets::ResolvedToolbarTheme {
1418 height: toolbar_height,
1419 item_spacing: toolbar_item_spacing,
1420 padding: toolbar_padding,
1421 font: toolbar_font,
1422 },
1423 status_bar: crate::model::widgets::ResolvedStatusBarTheme {
1424 font: status_bar_font,
1425 },
1426 list: crate::model::widgets::ResolvedListTheme {
1427 background: list_background,
1428 foreground: list_foreground,
1429 alternate_row: list_alternate_row,
1430 selection: list_selection,
1431 selection_foreground: list_selection_foreground,
1432 header_background: list_header_background,
1433 header_foreground: list_header_foreground,
1434 grid_color: list_grid_color,
1435 item_height: list_item_height,
1436 padding_horizontal: list_padding_horizontal,
1437 padding_vertical: list_padding_vertical,
1438 },
1439 popover: crate::model::widgets::ResolvedPopoverTheme {
1440 background: popover_background,
1441 foreground: popover_foreground,
1442 border: popover_border,
1443 radius: popover_radius,
1444 },
1445 splitter: crate::model::widgets::ResolvedSplitterTheme {
1446 width: splitter_width,
1447 },
1448 separator: crate::model::widgets::ResolvedSeparatorTheme {
1449 color: separator_color,
1450 },
1451 switch: crate::model::widgets::ResolvedSwitchTheme {
1452 checked_bg: switch_checked_bg,
1453 unchecked_bg: switch_unchecked_bg,
1454 thumb_bg: switch_thumb_bg,
1455 track_width: switch_track_width,
1456 track_height: switch_track_height,
1457 thumb_size: switch_thumb_size,
1458 track_radius: switch_track_radius,
1459 },
1460 dialog: crate::model::widgets::ResolvedDialogTheme {
1461 min_width: dialog_min_width,
1462 max_width: dialog_max_width,
1463 min_height: dialog_min_height,
1464 max_height: dialog_max_height,
1465 content_padding: dialog_content_padding,
1466 button_spacing: dialog_button_spacing,
1467 radius: dialog_radius,
1468 icon_size: dialog_icon_size,
1469 button_order: dialog_button_order,
1470 title_font: dialog_title_font,
1471 },
1472 spinner: crate::model::widgets::ResolvedSpinnerTheme {
1473 fill: spinner_fill,
1474 diameter: spinner_diameter,
1475 min_size: spinner_min_size,
1476 stroke_width: spinner_stroke_width,
1477 },
1478 combo_box: crate::model::widgets::ResolvedComboBoxTheme {
1479 min_height: combo_box_min_height,
1480 min_width: combo_box_min_width,
1481 padding_horizontal: combo_box_padding_horizontal,
1482 arrow_size: combo_box_arrow_size,
1483 arrow_area_width: combo_box_arrow_area_width,
1484 radius: combo_box_radius,
1485 },
1486 segmented_control: crate::model::widgets::ResolvedSegmentedControlTheme {
1487 segment_height: segmented_control_segment_height,
1488 separator_width: segmented_control_separator_width,
1489 padding_horizontal: segmented_control_padding_horizontal,
1490 radius: segmented_control_radius,
1491 },
1492 card: crate::model::widgets::ResolvedCardTheme {
1493 background: card_background,
1494 border: card_border,
1495 radius: card_radius,
1496 padding: card_padding,
1497 shadow: card_shadow,
1498 },
1499 expander: crate::model::widgets::ResolvedExpanderTheme {
1500 header_height: expander_header_height,
1501 arrow_size: expander_arrow_size,
1502 content_padding: expander_content_padding,
1503 radius: expander_radius,
1504 },
1505 link: crate::model::widgets::ResolvedLinkTheme {
1506 color: link_color,
1507 visited: link_visited,
1508 background: link_background,
1509 hover_bg: link_hover_bg,
1510 underline: link_underline,
1511 },
1512 icon_set,
1513 icon_theme,
1514 })
1515 }
1516}
1517
1518#[cfg(test)]
1519#[allow(clippy::unwrap_used, clippy::expect_used)]
1520mod tests {
1521 use super::*;
1522 use crate::Rgba;
1523 use crate::model::{DialogButtonOrder, FontSpec};
1524
1525 fn variant_with_defaults() -> ThemeVariant {
1527 let c1 = Rgba::rgb(0, 120, 215); let c2 = Rgba::rgb(255, 255, 255); let c3 = Rgba::rgb(30, 30, 30); let c4 = Rgba::rgb(240, 240, 240); let c5 = Rgba::rgb(200, 200, 200); let c6 = Rgba::rgb(128, 128, 128); let c7 = Rgba::rgb(0, 0, 0); let c8 = Rgba::rgb(0, 100, 200); let c9 = Rgba::rgb(255, 255, 255); let c10 = Rgba::rgb(220, 53, 69); let c11 = Rgba::rgb(255, 255, 255); let c12 = Rgba::rgb(240, 173, 78); let c13 = Rgba::rgb(30, 30, 30); let c14 = Rgba::rgb(40, 167, 69); let c15 = Rgba::rgb(255, 255, 255); let c16 = Rgba::rgb(0, 120, 215); let c17 = Rgba::rgb(255, 255, 255); let mut v = ThemeVariant::default();
1546 v.defaults.accent = Some(c1);
1547 v.defaults.background = Some(c2);
1548 v.defaults.foreground = Some(c3);
1549 v.defaults.surface = Some(c4);
1550 v.defaults.border = Some(c5);
1551 v.defaults.muted = Some(c6);
1552 v.defaults.shadow = Some(c7);
1553 v.defaults.link = Some(c8);
1554 v.defaults.accent_foreground = Some(c9);
1555 v.defaults.selection_foreground = Some(Rgba::rgb(255, 255, 255));
1556 v.defaults.disabled_foreground = Some(Rgba::rgb(160, 160, 160));
1557 v.defaults.danger = Some(c10);
1558 v.defaults.danger_foreground = Some(c11);
1559 v.defaults.warning = Some(c12);
1560 v.defaults.warning_foreground = Some(c13);
1561 v.defaults.success = Some(c14);
1562 v.defaults.success_foreground = Some(c15);
1563 v.defaults.info = Some(c16);
1564 v.defaults.info_foreground = Some(c17);
1565
1566 v.defaults.radius = Some(4.0);
1567 v.defaults.radius_lg = Some(8.0);
1568 v.defaults.frame_width = Some(1.0);
1569 v.defaults.disabled_opacity = Some(0.5);
1570 v.defaults.border_opacity = Some(0.15);
1571 v.defaults.shadow_enabled = Some(true);
1572
1573 v.defaults.focus_ring_width = Some(2.0);
1574 v.defaults.focus_ring_offset = Some(1.0);
1575
1576 v.defaults.font = FontSpec {
1577 family: Some("Inter".into()),
1578 size: Some(14.0),
1579 weight: Some(400),
1580 };
1581 v.defaults.line_height = Some(1.4);
1582 v.defaults.mono_font = FontSpec {
1583 family: Some("JetBrains Mono".into()),
1584 size: Some(13.0),
1585 weight: Some(400),
1586 };
1587
1588 v.defaults.spacing.xxs = Some(2.0);
1589 v.defaults.spacing.xs = Some(4.0);
1590 v.defaults.spacing.s = Some(6.0);
1591 v.defaults.spacing.m = Some(12.0);
1592 v.defaults.spacing.l = Some(18.0);
1593 v.defaults.spacing.xl = Some(24.0);
1594 v.defaults.spacing.xxl = Some(36.0);
1595
1596 v.defaults.icon_sizes.toolbar = Some(24.0);
1597 v.defaults.icon_sizes.small = Some(16.0);
1598 v.defaults.icon_sizes.large = Some(32.0);
1599 v.defaults.icon_sizes.dialog = Some(22.0);
1600 v.defaults.icon_sizes.panel = Some(20.0);
1601
1602 v.defaults.text_scaling_factor = Some(1.0);
1603 v.defaults.reduce_motion = Some(false);
1604 v.defaults.high_contrast = Some(false);
1605 v.defaults.reduce_transparency = Some(false);
1606
1607 v
1608 }
1609
1610 #[test]
1613 fn resolve_phase1_accent_fills_selection_and_focus_ring() {
1614 let mut v = ThemeVariant::default();
1615 v.defaults.accent = Some(Rgba::rgb(0, 120, 215));
1616 v.resolve();
1617 assert_eq!(v.defaults.selection, Some(Rgba::rgb(0, 120, 215)));
1618 assert_eq!(v.defaults.focus_ring_color, Some(Rgba::rgb(0, 120, 215)));
1619 }
1620
1621 #[test]
1622 fn resolve_phase1_selection_fills_selection_inactive() {
1623 let mut v = ThemeVariant::default();
1624 v.defaults.accent = Some(Rgba::rgb(0, 120, 215));
1625 v.resolve();
1626 assert_eq!(v.defaults.selection_inactive, Some(Rgba::rgb(0, 120, 215)));
1628 }
1629
1630 #[test]
1631 fn resolve_phase1_explicit_selection_preserved() {
1632 let mut v = ThemeVariant::default();
1633 v.defaults.accent = Some(Rgba::rgb(0, 120, 215));
1634 v.defaults.selection = Some(Rgba::rgb(100, 100, 100));
1635 v.resolve();
1636 assert_eq!(v.defaults.selection, Some(Rgba::rgb(100, 100, 100)));
1638 assert_eq!(
1640 v.defaults.selection_inactive,
1641 Some(Rgba::rgb(100, 100, 100))
1642 );
1643 }
1644
1645 #[test]
1648 fn resolve_phase2_safety_nets() {
1649 let mut v = ThemeVariant::default();
1650 v.defaults.foreground = Some(Rgba::rgb(30, 30, 30));
1651 v.defaults.background = Some(Rgba::rgb(255, 255, 255));
1652 v.resolve();
1653
1654 assert_eq!(
1655 v.input.caret,
1656 Some(Rgba::rgb(30, 30, 30)),
1657 "input.caret <- foreground"
1658 );
1659 assert_eq!(
1660 v.scrollbar.track,
1661 Some(Rgba::rgb(255, 255, 255)),
1662 "scrollbar.track <- background"
1663 );
1664 assert_eq!(
1665 v.spinner.fill,
1666 Some(Rgba::rgb(30, 30, 30)),
1667 "spinner.fill <- foreground"
1668 );
1669 assert_eq!(
1670 v.popover.background,
1671 Some(Rgba::rgb(255, 255, 255)),
1672 "popover.background <- background"
1673 );
1674 assert_eq!(
1675 v.list.background,
1676 Some(Rgba::rgb(255, 255, 255)),
1677 "list.background <- background"
1678 );
1679 }
1680
1681 #[test]
1684 fn resolve_phase3_accent_propagation() {
1685 let mut v = ThemeVariant::default();
1686 v.defaults.accent = Some(Rgba::rgb(0, 120, 215));
1687 v.resolve();
1688
1689 assert_eq!(
1690 v.button.primary_bg,
1691 Some(Rgba::rgb(0, 120, 215)),
1692 "button.primary_bg <- accent"
1693 );
1694 assert_eq!(
1695 v.checkbox.checked_bg,
1696 Some(Rgba::rgb(0, 120, 215)),
1697 "checkbox.checked_bg <- accent"
1698 );
1699 assert_eq!(
1700 v.slider.fill,
1701 Some(Rgba::rgb(0, 120, 215)),
1702 "slider.fill <- accent"
1703 );
1704 assert_eq!(
1705 v.progress_bar.fill,
1706 Some(Rgba::rgb(0, 120, 215)),
1707 "progress_bar.fill <- accent"
1708 );
1709 assert_eq!(
1710 v.switch.checked_bg,
1711 Some(Rgba::rgb(0, 120, 215)),
1712 "switch.checked_bg <- accent"
1713 );
1714 }
1715
1716 #[test]
1719 fn resolve_phase3_font_subfield_inheritance() {
1720 let mut v = ThemeVariant::default();
1721 v.defaults.font = FontSpec {
1722 family: Some("Inter".into()),
1723 size: Some(14.0),
1724 weight: Some(400),
1725 };
1726 v.menu.font = Some(FontSpec {
1728 family: None,
1729 size: Some(12.0),
1730 weight: None,
1731 });
1732 v.resolve();
1733
1734 let menu_font = v.menu.font.as_ref().unwrap();
1735 assert_eq!(
1736 menu_font.family.as_deref(),
1737 Some("Inter"),
1738 "family from defaults"
1739 );
1740 assert_eq!(menu_font.size, Some(12.0), "explicit size preserved");
1741 assert_eq!(menu_font.weight, Some(400), "weight from defaults");
1742 }
1743
1744 #[test]
1745 fn resolve_phase3_font_entire_inheritance() {
1746 let mut v = ThemeVariant::default();
1747 v.defaults.font = FontSpec {
1748 family: Some("Inter".into()),
1749 size: Some(14.0),
1750 weight: Some(400),
1751 };
1752 assert!(v.button.font.is_none());
1754 v.resolve();
1755
1756 let button_font = v.button.font.as_ref().unwrap();
1757 assert_eq!(button_font.family.as_deref(), Some("Inter"));
1758 assert_eq!(button_font.size, Some(14.0));
1759 assert_eq!(button_font.weight, Some(400));
1760 }
1761
1762 #[test]
1765 fn resolve_phase3_text_scale_inheritance() {
1766 let mut v = ThemeVariant::default();
1767 v.defaults.font = FontSpec {
1768 family: Some("Inter".into()),
1769 size: Some(14.0),
1770 weight: Some(400),
1771 };
1772 v.defaults.line_height = Some(1.4);
1773 v.resolve();
1775
1776 let caption = v.text_scale.caption.as_ref().unwrap();
1777 assert_eq!(caption.size, Some(14.0), "size from defaults.font.size");
1778 assert_eq!(
1779 caption.weight,
1780 Some(400),
1781 "weight from defaults.font.weight"
1782 );
1783 assert!(
1785 (caption.line_height.unwrap() - 19.6).abs() < 0.001,
1786 "line_height computed"
1787 );
1788 }
1789
1790 #[test]
1793 fn resolve_phase3_color_inheritance() {
1794 let mut v = variant_with_defaults();
1795 v.resolve();
1796
1797 assert_eq!(v.window.background, Some(Rgba::rgb(255, 255, 255)));
1799 assert_eq!(v.window.border, v.defaults.border);
1800 assert_eq!(v.button.border, v.defaults.border);
1802 assert_eq!(v.tooltip.radius, v.defaults.radius);
1804 }
1805
1806 #[test]
1809 fn resolve_phase4_inactive_title_bar_from_active() {
1810 let mut v = ThemeVariant::default();
1811 v.defaults.surface = Some(Rgba::rgb(240, 240, 240));
1812 v.defaults.foreground = Some(Rgba::rgb(30, 30, 30));
1813 v.resolve();
1814
1815 assert_eq!(
1818 v.window.inactive_title_bar_background,
1819 v.window.title_bar_background
1820 );
1821 assert_eq!(
1822 v.window.inactive_title_bar_foreground,
1823 v.window.title_bar_foreground
1824 );
1825 }
1826
1827 #[test]
1830 fn resolve_does_not_overwrite_existing_some_values() {
1831 let mut v = variant_with_defaults();
1832 let explicit = Rgba::rgb(255, 0, 0);
1833 v.window.background = Some(explicit);
1834 v.button.primary_bg = Some(explicit);
1835 v.resolve();
1836
1837 assert_eq!(
1838 v.window.background,
1839 Some(explicit),
1840 "window.background preserved"
1841 );
1842 assert_eq!(
1843 v.button.primary_bg,
1844 Some(explicit),
1845 "button.primary_bg preserved"
1846 );
1847 }
1848
1849 #[test]
1852 fn resolve_is_idempotent() {
1853 let mut v = variant_with_defaults();
1854 v.resolve();
1855 let after_first = v.clone();
1856 v.resolve();
1857 assert_eq!(v, after_first, "second resolve() produces same result");
1858 }
1859
1860 #[test]
1863 fn resolve_all_font_carrying_widgets_get_resolved_fonts() {
1864 let mut v = ThemeVariant::default();
1865 v.defaults.font = FontSpec {
1866 family: Some("Inter".into()),
1867 size: Some(14.0),
1868 weight: Some(400),
1869 };
1870 v.resolve();
1871
1872 assert!(v.window.title_bar_font.is_some(), "window.title_bar_font");
1874 assert!(v.button.font.is_some(), "button.font");
1875 assert!(v.input.font.is_some(), "input.font");
1876 assert!(v.menu.font.is_some(), "menu.font");
1877 assert!(v.tooltip.font.is_some(), "tooltip.font");
1878 assert!(v.toolbar.font.is_some(), "toolbar.font");
1879 assert!(v.status_bar.font.is_some(), "status_bar.font");
1880 assert!(v.dialog.title_font.is_some(), "dialog.title_font");
1881
1882 for (name, font) in [
1884 ("window.title_bar_font", &v.window.title_bar_font),
1885 ("button.font", &v.button.font),
1886 ("input.font", &v.input.font),
1887 ("menu.font", &v.menu.font),
1888 ("tooltip.font", &v.tooltip.font),
1889 ("toolbar.font", &v.toolbar.font),
1890 ("status_bar.font", &v.status_bar.font),
1891 ("dialog.title_font", &v.dialog.title_font),
1892 ] {
1893 let f = font.as_ref().unwrap();
1894 assert_eq!(f.family.as_deref(), Some("Inter"), "{name} family");
1895 assert_eq!(f.size, Some(14.0), "{name} size");
1896 assert_eq!(f.weight, Some(400), "{name} weight");
1897 }
1898 }
1899
1900 fn fully_populated_variant() -> ThemeVariant {
1904 let mut v = variant_with_defaults();
1905 let c = Rgba::rgb(128, 128, 128);
1906
1907 v.defaults.selection = Some(Rgba::rgb(0, 120, 215));
1909 v.defaults.selection_foreground = Some(Rgba::rgb(255, 255, 255));
1910 v.defaults.selection_inactive = Some(Rgba::rgb(0, 120, 215));
1911 v.defaults.focus_ring_color = Some(Rgba::rgb(0, 120, 215));
1912
1913 v.icon_set = Some(crate::IconSet::Freedesktop);
1915 v.icon_theme = Some("breeze".into());
1916
1917 v.window.background = Some(c);
1919 v.window.foreground = Some(c);
1920 v.window.border = Some(c);
1921 v.window.title_bar_background = Some(c);
1922 v.window.title_bar_foreground = Some(c);
1923 v.window.inactive_title_bar_background = Some(c);
1924 v.window.inactive_title_bar_foreground = Some(c);
1925 v.window.radius = Some(8.0);
1926 v.window.shadow = Some(true);
1927 v.window.title_bar_font = Some(FontSpec {
1928 family: Some("Inter".into()),
1929 size: Some(14.0),
1930 weight: Some(400),
1931 });
1932
1933 v.button.background = Some(c);
1935 v.button.foreground = Some(c);
1936 v.button.border = Some(c);
1937 v.button.primary_bg = Some(c);
1938 v.button.primary_fg = Some(c);
1939 v.button.min_width = Some(64.0);
1940 v.button.min_height = Some(28.0);
1941 v.button.padding_horizontal = Some(12.0);
1942 v.button.padding_vertical = Some(6.0);
1943 v.button.radius = Some(4.0);
1944 v.button.icon_spacing = Some(6.0);
1945 v.button.disabled_opacity = Some(0.5);
1946 v.button.shadow = Some(false);
1947 v.button.font = Some(FontSpec {
1948 family: Some("Inter".into()),
1949 size: Some(14.0),
1950 weight: Some(400),
1951 });
1952
1953 v.input.background = Some(c);
1955 v.input.foreground = Some(c);
1956 v.input.border = Some(c);
1957 v.input.placeholder = Some(c);
1958 v.input.caret = Some(c);
1959 v.input.selection = Some(c);
1960 v.input.selection_foreground = Some(c);
1961 v.input.min_height = Some(28.0);
1962 v.input.padding_horizontal = Some(8.0);
1963 v.input.padding_vertical = Some(4.0);
1964 v.input.radius = Some(4.0);
1965 v.input.border_width = Some(1.0);
1966 v.input.font = Some(FontSpec {
1967 family: Some("Inter".into()),
1968 size: Some(14.0),
1969 weight: Some(400),
1970 });
1971
1972 v.checkbox.checked_bg = Some(c);
1974 v.checkbox.indicator_size = Some(18.0);
1975 v.checkbox.spacing = Some(6.0);
1976 v.checkbox.radius = Some(2.0);
1977 v.checkbox.border_width = Some(1.0);
1978
1979 v.menu.background = Some(c);
1981 v.menu.foreground = Some(c);
1982 v.menu.separator = Some(c);
1983 v.menu.item_height = Some(28.0);
1984 v.menu.padding_horizontal = Some(8.0);
1985 v.menu.padding_vertical = Some(4.0);
1986 v.menu.icon_spacing = Some(6.0);
1987 v.menu.font = Some(FontSpec {
1988 family: Some("Inter".into()),
1989 size: Some(14.0),
1990 weight: Some(400),
1991 });
1992
1993 v.tooltip.background = Some(c);
1995 v.tooltip.foreground = Some(c);
1996 v.tooltip.padding_horizontal = Some(6.0);
1997 v.tooltip.padding_vertical = Some(4.0);
1998 v.tooltip.max_width = Some(300.0);
1999 v.tooltip.radius = Some(4.0);
2000 v.tooltip.font = Some(FontSpec {
2001 family: Some("Inter".into()),
2002 size: Some(14.0),
2003 weight: Some(400),
2004 });
2005
2006 v.scrollbar.track = Some(c);
2008 v.scrollbar.thumb = Some(c);
2009 v.scrollbar.thumb_hover = Some(c);
2010 v.scrollbar.width = Some(14.0);
2011 v.scrollbar.min_thumb_height = Some(20.0);
2012 v.scrollbar.slider_width = Some(8.0);
2013 v.scrollbar.overlay_mode = Some(false);
2014
2015 v.slider.fill = Some(c);
2017 v.slider.track = Some(c);
2018 v.slider.thumb = Some(c);
2019 v.slider.track_height = Some(4.0);
2020 v.slider.thumb_size = Some(16.0);
2021 v.slider.tick_length = Some(6.0);
2022
2023 v.progress_bar.fill = Some(c);
2025 v.progress_bar.track = Some(c);
2026 v.progress_bar.height = Some(6.0);
2027 v.progress_bar.min_width = Some(100.0);
2028 v.progress_bar.radius = Some(3.0);
2029
2030 v.tab.background = Some(c);
2032 v.tab.foreground = Some(c);
2033 v.tab.active_background = Some(c);
2034 v.tab.active_foreground = Some(c);
2035 v.tab.bar_background = Some(c);
2036 v.tab.min_width = Some(60.0);
2037 v.tab.min_height = Some(32.0);
2038 v.tab.padding_horizontal = Some(12.0);
2039 v.tab.padding_vertical = Some(6.0);
2040
2041 v.sidebar.background = Some(c);
2043 v.sidebar.foreground = Some(c);
2044
2045 v.toolbar.height = Some(40.0);
2047 v.toolbar.item_spacing = Some(4.0);
2048 v.toolbar.padding = Some(4.0);
2049 v.toolbar.font = Some(FontSpec {
2050 family: Some("Inter".into()),
2051 size: Some(14.0),
2052 weight: Some(400),
2053 });
2054
2055 v.status_bar.font = Some(FontSpec {
2057 family: Some("Inter".into()),
2058 size: Some(14.0),
2059 weight: Some(400),
2060 });
2061
2062 v.list.background = Some(c);
2064 v.list.foreground = Some(c);
2065 v.list.alternate_row = Some(c);
2066 v.list.selection = Some(c);
2067 v.list.selection_foreground = Some(c);
2068 v.list.header_background = Some(c);
2069 v.list.header_foreground = Some(c);
2070 v.list.grid_color = Some(c);
2071 v.list.item_height = Some(28.0);
2072 v.list.padding_horizontal = Some(8.0);
2073 v.list.padding_vertical = Some(4.0);
2074
2075 v.popover.background = Some(c);
2077 v.popover.foreground = Some(c);
2078 v.popover.border = Some(c);
2079 v.popover.radius = Some(6.0);
2080
2081 v.splitter.width = Some(4.0);
2083
2084 v.separator.color = Some(c);
2086
2087 v.switch.checked_bg = Some(c);
2089 v.switch.unchecked_bg = Some(c);
2090 v.switch.thumb_bg = Some(c);
2091 v.switch.track_width = Some(40.0);
2092 v.switch.track_height = Some(20.0);
2093 v.switch.thumb_size = Some(14.0);
2094 v.switch.track_radius = Some(10.0);
2095
2096 v.dialog.min_width = Some(320.0);
2098 v.dialog.max_width = Some(600.0);
2099 v.dialog.min_height = Some(200.0);
2100 v.dialog.max_height = Some(800.0);
2101 v.dialog.content_padding = Some(16.0);
2102 v.dialog.button_spacing = Some(8.0);
2103 v.dialog.radius = Some(8.0);
2104 v.dialog.icon_size = Some(22.0);
2105 v.dialog.button_order = Some(DialogButtonOrder::TrailingAffirmative);
2106 v.dialog.title_font = Some(FontSpec {
2107 family: Some("Inter".into()),
2108 size: Some(16.0),
2109 weight: Some(700),
2110 });
2111
2112 v.spinner.fill = Some(c);
2114 v.spinner.diameter = Some(24.0);
2115 v.spinner.min_size = Some(16.0);
2116 v.spinner.stroke_width = Some(2.0);
2117
2118 v.combo_box.min_height = Some(28.0);
2120 v.combo_box.min_width = Some(80.0);
2121 v.combo_box.padding_horizontal = Some(8.0);
2122 v.combo_box.arrow_size = Some(12.0);
2123 v.combo_box.arrow_area_width = Some(20.0);
2124 v.combo_box.radius = Some(4.0);
2125
2126 v.segmented_control.segment_height = Some(28.0);
2128 v.segmented_control.separator_width = Some(1.0);
2129 v.segmented_control.padding_horizontal = Some(12.0);
2130 v.segmented_control.radius = Some(4.0);
2131
2132 v.card.background = Some(c);
2134 v.card.border = Some(c);
2135 v.card.radius = Some(8.0);
2136 v.card.padding = Some(12.0);
2137 v.card.shadow = Some(true);
2138
2139 v.expander.header_height = Some(32.0);
2141 v.expander.arrow_size = Some(12.0);
2142 v.expander.content_padding = Some(8.0);
2143 v.expander.radius = Some(4.0);
2144
2145 v.link.color = Some(c);
2147 v.link.visited = Some(c);
2148 v.link.background = Some(c);
2149 v.link.hover_bg = Some(c);
2150 v.link.underline = Some(true);
2151
2152 v.text_scale.caption = Some(crate::model::TextScaleEntry {
2154 size: Some(11.0),
2155 weight: Some(400),
2156 line_height: Some(15.4),
2157 });
2158 v.text_scale.section_heading = Some(crate::model::TextScaleEntry {
2159 size: Some(14.0),
2160 weight: Some(600),
2161 line_height: Some(19.6),
2162 });
2163 v.text_scale.dialog_title = Some(crate::model::TextScaleEntry {
2164 size: Some(16.0),
2165 weight: Some(700),
2166 line_height: Some(22.4),
2167 });
2168 v.text_scale.display = Some(crate::model::TextScaleEntry {
2169 size: Some(24.0),
2170 weight: Some(300),
2171 line_height: Some(33.6),
2172 });
2173
2174 v
2175 }
2176
2177 #[test]
2178 fn validate_fully_populated_returns_ok() {
2179 let v = fully_populated_variant();
2180 let result = v.validate();
2181 assert!(
2182 result.is_ok(),
2183 "validate() should succeed on fully populated variant, got: {:?}",
2184 result.err()
2185 );
2186 let resolved = result.unwrap();
2187 assert_eq!(resolved.defaults.font.family, "Inter");
2188 assert_eq!(resolved.icon_set, crate::IconSet::Freedesktop);
2189 }
2190
2191 #[test]
2192 fn validate_missing_3_fields_returns_all_paths() {
2193 let mut v = fully_populated_variant();
2194 v.defaults.muted = None;
2196 v.window.radius = None;
2197 v.icon_set = None;
2198
2199 let result = v.validate();
2200 assert!(result.is_err());
2201 let err = match result.unwrap_err() {
2202 crate::Error::Resolution(e) => e,
2203 other => panic!("expected Resolution error, got: {other:?}"),
2204 };
2205 assert_eq!(
2206 err.missing_fields.len(),
2207 3,
2208 "should report exactly 3 missing fields, got: {:?}",
2209 err.missing_fields
2210 );
2211 assert!(err.missing_fields.contains(&"defaults.muted".to_string()));
2212 assert!(err.missing_fields.contains(&"window.radius".to_string()));
2213 assert!(err.missing_fields.contains(&"icon_set".to_string()));
2214 }
2215
2216 #[test]
2217 fn validate_error_message_includes_count_and_paths() {
2218 let mut v = fully_populated_variant();
2219 v.defaults.muted = None;
2220 v.button.min_height = None;
2221
2222 let result = v.validate();
2223 assert!(result.is_err());
2224 let err = match result.unwrap_err() {
2225 crate::Error::Resolution(e) => e,
2226 other => panic!("expected Resolution error, got: {other:?}"),
2227 };
2228 let msg = err.to_string();
2229 assert!(msg.contains("2 missing field(s)"), "got: {msg}");
2230 assert!(msg.contains("defaults.muted"), "got: {msg}");
2231 assert!(msg.contains("button.min_height"), "got: {msg}");
2232 }
2233
2234 #[test]
2235 fn validate_checks_all_defaults_fields() {
2236 let v = ThemeVariant::default();
2238 let result = v.validate();
2239 assert!(result.is_err());
2240 let err = match result.unwrap_err() {
2241 crate::Error::Resolution(e) => e,
2242 other => panic!("expected Resolution error, got: {other:?}"),
2243 };
2244 assert!(
2246 err.missing_fields
2247 .iter()
2248 .any(|f| f.starts_with("defaults.")),
2249 "should include defaults.* fields in missing"
2250 );
2251 assert!(
2253 err.missing_fields
2254 .contains(&"defaults.font.family".to_string())
2255 );
2256 assert!(
2257 err.missing_fields
2258 .contains(&"defaults.background".to_string())
2259 );
2260 assert!(err.missing_fields.contains(&"defaults.accent".to_string()));
2261 assert!(err.missing_fields.contains(&"defaults.radius".to_string()));
2262 assert!(
2263 err.missing_fields
2264 .contains(&"defaults.spacing.m".to_string())
2265 );
2266 assert!(
2267 err.missing_fields
2268 .contains(&"defaults.icon_sizes.toolbar".to_string())
2269 );
2270 assert!(
2271 err.missing_fields
2272 .contains(&"defaults.text_scaling_factor".to_string())
2273 );
2274 }
2275
2276 #[test]
2277 fn validate_checks_all_widget_structs() {
2278 let v = ThemeVariant::default();
2279 let result = v.validate();
2280 let err = match result.unwrap_err() {
2281 crate::Error::Resolution(e) => e,
2282 other => panic!("expected Resolution error, got: {other:?}"),
2283 };
2284 for prefix in [
2286 "window.",
2287 "button.",
2288 "input.",
2289 "checkbox.",
2290 "menu.",
2291 "tooltip.",
2292 "scrollbar.",
2293 "slider.",
2294 "progress_bar.",
2295 "tab.",
2296 "sidebar.",
2297 "toolbar.",
2298 "status_bar.",
2299 "list.",
2300 "popover.",
2301 "splitter.",
2302 "separator.",
2303 "switch.",
2304 "dialog.",
2305 "spinner.",
2306 "combo_box.",
2307 "segmented_control.",
2308 "card.",
2309 "expander.",
2310 "link.",
2311 ] {
2312 assert!(
2313 err.missing_fields.iter().any(|f| f.starts_with(prefix)),
2314 "missing fields should include {prefix}* but got: {:?}",
2315 err.missing_fields
2316 .iter()
2317 .filter(|f| f.starts_with(prefix))
2318 .collect::<Vec<_>>()
2319 );
2320 }
2321 }
2322
2323 #[test]
2324 fn validate_checks_text_scale_entries() {
2325 let v = ThemeVariant::default();
2326 let result = v.validate();
2327 let err = match result.unwrap_err() {
2328 crate::Error::Resolution(e) => e,
2329 other => panic!("expected Resolution error, got: {other:?}"),
2330 };
2331 assert!(
2332 err.missing_fields
2333 .contains(&"text_scale.caption".to_string())
2334 );
2335 assert!(
2336 err.missing_fields
2337 .contains(&"text_scale.section_heading".to_string())
2338 );
2339 assert!(
2340 err.missing_fields
2341 .contains(&"text_scale.dialog_title".to_string())
2342 );
2343 assert!(
2344 err.missing_fields
2345 .contains(&"text_scale.display".to_string())
2346 );
2347 }
2348
2349 #[test]
2350 fn validate_checks_icon_set() {
2351 let mut v = fully_populated_variant();
2352 v.icon_set = None;
2353
2354 let result = v.validate();
2355 let err = match result.unwrap_err() {
2356 crate::Error::Resolution(e) => e,
2357 other => panic!("expected Resolution error, got: {other:?}"),
2358 };
2359 assert!(err.missing_fields.contains(&"icon_set".to_string()));
2360 }
2361
2362 #[test]
2363 fn validate_after_resolve_succeeds_for_derivable_fields() {
2364 let mut v = variant_with_defaults();
2366 v.icon_set = Some(crate::IconSet::Freedesktop);
2368
2369 v.button.min_width = Some(64.0);
2372 v.button.min_height = Some(28.0);
2373 v.button.padding_horizontal = Some(12.0);
2374 v.button.padding_vertical = Some(6.0);
2375 v.button.icon_spacing = Some(6.0);
2376 v.input.min_height = Some(28.0);
2378 v.input.padding_horizontal = Some(8.0);
2379 v.input.padding_vertical = Some(4.0);
2380 v.checkbox.indicator_size = Some(18.0);
2382 v.checkbox.spacing = Some(6.0);
2383 v.menu.item_height = Some(28.0);
2385 v.menu.padding_horizontal = Some(8.0);
2386 v.menu.padding_vertical = Some(4.0);
2387 v.menu.icon_spacing = Some(6.0);
2388 v.tooltip.padding_horizontal = Some(6.0);
2390 v.tooltip.padding_vertical = Some(4.0);
2391 v.tooltip.max_width = Some(300.0);
2392 v.scrollbar.width = Some(14.0);
2394 v.scrollbar.min_thumb_height = Some(20.0);
2395 v.scrollbar.slider_width = Some(8.0);
2396 v.scrollbar.overlay_mode = Some(false);
2397 v.slider.track_height = Some(4.0);
2399 v.slider.thumb_size = Some(16.0);
2400 v.slider.tick_length = Some(6.0);
2401 v.progress_bar.height = Some(6.0);
2403 v.progress_bar.min_width = Some(100.0);
2404 v.tab.min_width = Some(60.0);
2406 v.tab.min_height = Some(32.0);
2407 v.tab.padding_horizontal = Some(12.0);
2408 v.tab.padding_vertical = Some(6.0);
2409 v.toolbar.height = Some(40.0);
2411 v.toolbar.item_spacing = Some(4.0);
2412 v.toolbar.padding = Some(4.0);
2413 v.list.item_height = Some(28.0);
2415 v.list.padding_horizontal = Some(8.0);
2416 v.list.padding_vertical = Some(4.0);
2417 v.splitter.width = Some(4.0);
2419 v.switch.unchecked_bg = Some(Rgba::rgb(180, 180, 180));
2421 v.switch.track_width = Some(40.0);
2422 v.switch.track_height = Some(20.0);
2423 v.switch.thumb_size = Some(14.0);
2424 v.switch.track_radius = Some(10.0);
2425 v.dialog.min_width = Some(320.0);
2427 v.dialog.max_width = Some(600.0);
2428 v.dialog.min_height = Some(200.0);
2429 v.dialog.max_height = Some(800.0);
2430 v.dialog.content_padding = Some(16.0);
2431 v.dialog.button_spacing = Some(8.0);
2432 v.dialog.icon_size = Some(22.0);
2433 v.dialog.button_order = Some(DialogButtonOrder::TrailingAffirmative);
2434 v.spinner.diameter = Some(24.0);
2436 v.spinner.min_size = Some(16.0);
2437 v.spinner.stroke_width = Some(2.0);
2438 v.combo_box.min_height = Some(28.0);
2440 v.combo_box.min_width = Some(80.0);
2441 v.combo_box.padding_horizontal = Some(8.0);
2442 v.combo_box.arrow_size = Some(12.0);
2443 v.combo_box.arrow_area_width = Some(20.0);
2444 v.segmented_control.segment_height = Some(28.0);
2446 v.segmented_control.separator_width = Some(1.0);
2447 v.segmented_control.padding_horizontal = Some(12.0);
2448 v.card.padding = Some(12.0);
2450 v.expander.header_height = Some(32.0);
2452 v.expander.arrow_size = Some(12.0);
2453 v.expander.content_padding = Some(8.0);
2454 v.link.background = Some(Rgba::rgb(255, 255, 255));
2456 v.link.hover_bg = Some(Rgba::rgb(230, 230, 255));
2457 v.link.underline = Some(true);
2458
2459 v.resolve();
2460 let result = v.validate();
2461 assert!(
2462 result.is_ok(),
2463 "validate() should succeed after resolve() with all non-derivable fields set, got: {:?}",
2464 result.err()
2465 );
2466 }
2467
2468 #[test]
2469 fn test_gnome_resolve_validate() {
2470 let adwaita = crate::ThemeSpec::preset("adwaita").unwrap();
2474
2475 let mut variant = adwaita
2477 .dark
2478 .clone()
2479 .expect("adwaita should have dark variant");
2480
2481 variant.dialog.button_order = Some(DialogButtonOrder::TrailingAffirmative);
2483 variant.icon_set = Some(crate::IconSet::Freedesktop);
2485
2486 variant.defaults.font = FontSpec {
2488 family: Some("Cantarell".to_string()),
2489 size: Some(11.0),
2490 weight: Some(400),
2491 };
2492
2493 variant.resolve();
2494 let resolved = variant.validate().unwrap_or_else(|e| {
2495 panic!("GNOME resolve/validate pipeline failed: {e}");
2496 });
2497
2498 assert_eq!(
2501 resolved.defaults.accent,
2502 Rgba::rgb(53, 132, 228),
2503 "accent should be from adwaita preset"
2504 );
2505 assert_eq!(
2506 resolved.defaults.font.family, "Cantarell",
2507 "font family should be from GNOME reader overlay"
2508 );
2509 assert_eq!(
2510 resolved.dialog.button_order,
2511 DialogButtonOrder::TrailingAffirmative,
2512 "dialog button order should be trailing affirmative for GNOME"
2513 );
2514 assert_eq!(
2515 resolved.icon_set,
2516 crate::IconSet::Freedesktop,
2517 "icon_set should be from GNOME reader"
2518 );
2519 }
2520}