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