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
133fn check_non_negative(value: f32, path: &str, errors: &mut Vec<String>) {
141 if value < 0.0 {
142 errors.push(format!("{path} must be >= 0, got {value}"));
143 }
144}
145
146fn check_positive(value: f32, path: &str, errors: &mut Vec<String>) {
148 if value <= 0.0 {
149 errors.push(format!("{path} must be > 0, got {value}"));
150 }
151}
152
153fn check_range_f32(value: f32, min: f32, max: f32, path: &str, errors: &mut Vec<String>) {
155 if value < min || value > max {
156 errors.push(format!("{path} must be {min}..={max}, got {value}"));
157 }
158}
159
160fn check_range_u16(value: u16, min: u16, max: u16, path: &str, errors: &mut Vec<String>) {
162 if value < min || value > max {
163 errors.push(format!("{path} must be {min}..={max}, got {value}"));
164 }
165}
166
167impl ThemeVariant {
168 pub fn resolve(&mut self) {
188 self.resolve_defaults_internal();
189 self.resolve_safety_nets();
190 self.resolve_widgets_from_defaults();
191 self.resolve_widget_to_widget();
192
193 if self.icon_set.is_none() {
195 self.icon_set = Some(crate::model::icons::system_icon_set());
196 }
197 }
198
199 pub fn resolve_platform_defaults(&mut self) {
206 if self.icon_theme.is_none() {
207 self.icon_theme = Some(crate::model::icons::system_icon_theme().to_string());
208 }
209 }
210
211 pub fn resolve_all(&mut self) {
218 self.resolve();
219 self.resolve_platform_defaults();
220 }
221
222 #[must_use = "this returns the resolved theme; it does not modify self"]
246 pub fn into_resolved(mut self) -> crate::Result<ResolvedThemeVariant> {
247 self.resolve_all();
248 self.validate()
249 }
250
251 fn resolve_defaults_internal(&mut self) {
254 let d = &mut self.defaults;
255
256 if d.selection.is_none() {
258 d.selection = d.accent;
259 }
260 if d.focus_ring_color.is_none() {
262 d.focus_ring_color = d.accent;
263 }
264 if d.selection_inactive.is_none() {
266 d.selection_inactive = d.selection;
267 }
268 }
269
270 fn resolve_safety_nets(&mut self) {
273 if self.input.caret.is_none() {
275 self.input.caret = self.defaults.foreground;
276 }
277 if self.scrollbar.track.is_none() {
279 self.scrollbar.track = self.defaults.background;
280 }
281 if self.spinner.fill.is_none() {
283 self.spinner.fill = self.defaults.foreground;
284 }
285 if self.popover.background.is_none() {
287 self.popover.background = self.defaults.background;
288 }
289 if self.list.background.is_none() {
291 self.list.background = self.defaults.background;
292 }
293 }
294
295 fn resolve_widgets_from_defaults(&mut self) {
298 self.resolve_color_inheritance();
299 self.resolve_font_inheritance();
300 self.resolve_text_scale();
301 }
302
303 fn resolve_color_inheritance(&mut self) {
304 let d = &self.defaults;
305
306 if self.window.background.is_none() {
308 self.window.background = d.background;
309 }
310 if self.window.foreground.is_none() {
311 self.window.foreground = d.foreground;
312 }
313 if self.window.border.is_none() {
314 self.window.border = d.border;
315 }
316 if self.window.title_bar_background.is_none() {
317 self.window.title_bar_background = d.surface;
318 }
319 if self.window.title_bar_foreground.is_none() {
320 self.window.title_bar_foreground = d.foreground;
321 }
322 if self.window.radius.is_none() {
323 self.window.radius = d.radius_lg;
324 }
325 if self.window.shadow.is_none() {
326 self.window.shadow = d.shadow_enabled;
327 }
328
329 if self.button.background.is_none() {
331 self.button.background = d.background;
332 }
333 if self.button.foreground.is_none() {
334 self.button.foreground = d.foreground;
335 }
336 if self.button.border.is_none() {
337 self.button.border = d.border;
338 }
339 if self.button.primary_background.is_none() {
340 self.button.primary_background = d.accent;
341 }
342 if self.button.primary_foreground.is_none() {
343 self.button.primary_foreground = d.accent_foreground;
344 }
345 if self.button.radius.is_none() {
346 self.button.radius = d.radius;
347 }
348 if self.button.disabled_opacity.is_none() {
349 self.button.disabled_opacity = d.disabled_opacity;
350 }
351 if self.button.shadow.is_none() {
352 self.button.shadow = d.shadow_enabled;
353 }
354
355 if self.input.background.is_none() {
357 self.input.background = d.background;
358 }
359 if self.input.foreground.is_none() {
360 self.input.foreground = d.foreground;
361 }
362 if self.input.border.is_none() {
363 self.input.border = d.border;
364 }
365 if self.input.placeholder.is_none() {
366 self.input.placeholder = d.muted;
367 }
368 if self.input.selection.is_none() {
369 self.input.selection = d.selection;
370 }
371 if self.input.selection_foreground.is_none() {
372 self.input.selection_foreground = d.selection_foreground;
373 }
374 if self.input.radius.is_none() {
375 self.input.radius = d.radius;
376 }
377 if self.input.border_width.is_none() {
378 self.input.border_width = d.frame_width;
379 }
380
381 if self.checkbox.checked_background.is_none() {
383 self.checkbox.checked_background = d.accent;
384 }
385 if self.checkbox.radius.is_none() {
386 self.checkbox.radius = d.radius;
387 }
388 if self.checkbox.border_width.is_none() {
389 self.checkbox.border_width = d.frame_width;
390 }
391
392 if self.menu.background.is_none() {
394 self.menu.background = d.background;
395 }
396 if self.menu.foreground.is_none() {
397 self.menu.foreground = d.foreground;
398 }
399 if self.menu.separator.is_none() {
400 self.menu.separator = d.border;
401 }
402
403 if self.tooltip.background.is_none() {
405 self.tooltip.background = d.background;
406 }
407 if self.tooltip.foreground.is_none() {
408 self.tooltip.foreground = d.foreground;
409 }
410 if self.tooltip.radius.is_none() {
411 self.tooltip.radius = d.radius;
412 }
413
414 if self.scrollbar.thumb.is_none() {
416 self.scrollbar.thumb = d.muted;
417 }
418 if self.scrollbar.thumb_hover.is_none() {
419 self.scrollbar.thumb_hover = d.muted;
420 }
421
422 if self.slider.fill.is_none() {
424 self.slider.fill = d.accent;
425 }
426 if self.slider.track.is_none() {
427 self.slider.track = d.muted;
428 }
429 if self.slider.thumb.is_none() {
430 self.slider.thumb = d.surface;
431 }
432
433 if self.progress_bar.fill.is_none() {
435 self.progress_bar.fill = d.accent;
436 }
437 if self.progress_bar.track.is_none() {
438 self.progress_bar.track = d.muted;
439 }
440 if self.progress_bar.radius.is_none() {
441 self.progress_bar.radius = d.radius;
442 }
443
444 if self.tab.background.is_none() {
446 self.tab.background = d.background;
447 }
448 if self.tab.foreground.is_none() {
449 self.tab.foreground = d.foreground;
450 }
451 if self.tab.active_background.is_none() {
452 self.tab.active_background = d.background;
453 }
454 if self.tab.active_foreground.is_none() {
455 self.tab.active_foreground = d.foreground;
456 }
457 if self.tab.bar_background.is_none() {
458 self.tab.bar_background = d.background;
459 }
460
461 if self.sidebar.background.is_none() {
463 self.sidebar.background = d.background;
464 }
465 if self.sidebar.foreground.is_none() {
466 self.sidebar.foreground = d.foreground;
467 }
468
469 if self.list.foreground.is_none() {
471 self.list.foreground = d.foreground;
472 }
473 if self.list.alternate_row.is_none() {
474 self.list.alternate_row = self.list.background;
475 }
476 if self.list.selection.is_none() {
477 self.list.selection = d.selection;
478 }
479 if self.list.selection_foreground.is_none() {
480 self.list.selection_foreground = d.selection_foreground;
481 }
482 if self.list.header_background.is_none() {
483 self.list.header_background = d.surface;
484 }
485 if self.list.header_foreground.is_none() {
486 self.list.header_foreground = d.foreground;
487 }
488 if self.list.grid_color.is_none() {
489 self.list.grid_color = d.border;
490 }
491
492 if self.popover.foreground.is_none() {
494 self.popover.foreground = d.foreground;
495 }
496 if self.popover.border.is_none() {
497 self.popover.border = d.border;
498 }
499 if self.popover.radius.is_none() {
500 self.popover.radius = d.radius_lg;
501 }
502
503 if self.separator.color.is_none() {
505 self.separator.color = d.border;
506 }
507
508 if self.switch.checked_background.is_none() {
510 self.switch.checked_background = d.accent;
511 }
512 if self.switch.unchecked_background.is_none() {
513 self.switch.unchecked_background = d.muted;
514 }
515 if self.switch.thumb_background.is_none() {
516 self.switch.thumb_background = d.surface;
517 }
518
519 if self.dialog.radius.is_none() {
521 self.dialog.radius = d.radius_lg;
522 }
523
524 if self.combo_box.radius.is_none() {
526 self.combo_box.radius = d.radius;
527 }
528
529 if self.segmented_control.radius.is_none() {
531 self.segmented_control.radius = d.radius;
532 }
533
534 if self.card.background.is_none() {
536 self.card.background = d.surface;
537 }
538 if self.card.border.is_none() {
539 self.card.border = d.border;
540 }
541 if self.card.radius.is_none() {
542 self.card.radius = d.radius_lg;
543 }
544 if self.card.shadow.is_none() {
545 self.card.shadow = d.shadow_enabled;
546 }
547
548 if self.expander.radius.is_none() {
550 self.expander.radius = d.radius;
551 }
552
553 if self.link.color.is_none() {
555 self.link.color = d.link;
556 }
557 if self.link.visited.is_none() {
558 self.link.visited = d.link;
559 }
560 }
561
562 fn resolve_font_inheritance(&mut self) {
563 let defaults_font = &self.defaults.font;
564 resolve_font(&mut self.window.title_bar_font, defaults_font);
565 resolve_font(&mut self.button.font, defaults_font);
566 resolve_font(&mut self.input.font, defaults_font);
567 resolve_font(&mut self.menu.font, defaults_font);
568 resolve_font(&mut self.tooltip.font, defaults_font);
569 resolve_font(&mut self.toolbar.font, defaults_font);
570 resolve_font(&mut self.status_bar.font, defaults_font);
571 resolve_font(&mut self.dialog.title_font, defaults_font);
572 }
573
574 fn resolve_text_scale(&mut self) {
575 let defaults_font = &self.defaults.font;
576 let defaults_lh = self.defaults.line_height;
577 resolve_text_scale_entry(&mut self.text_scale.caption, defaults_font, defaults_lh);
578 resolve_text_scale_entry(
579 &mut self.text_scale.section_heading,
580 defaults_font,
581 defaults_lh,
582 );
583 resolve_text_scale_entry(
584 &mut self.text_scale.dialog_title,
585 defaults_font,
586 defaults_lh,
587 );
588 resolve_text_scale_entry(&mut self.text_scale.display, defaults_font, defaults_lh);
589 }
590
591 fn resolve_widget_to_widget(&mut self) {
594 if self.window.inactive_title_bar_background.is_none() {
596 self.window.inactive_title_bar_background = self.window.title_bar_background;
597 }
598 if self.window.inactive_title_bar_foreground.is_none() {
599 self.window.inactive_title_bar_foreground = self.window.title_bar_foreground;
600 }
601 }
602
603 #[must_use = "this returns the resolved theme; it does not modify self"]
618 pub fn validate(&self) -> crate::Result<ResolvedThemeVariant> {
619 let mut missing = Vec::new();
620
621 let defaults_font = require_font(&self.defaults.font, "defaults.font", &mut missing);
624 let defaults_line_height = require(
625 &self.defaults.line_height,
626 "defaults.line_height",
627 &mut missing,
628 );
629 let defaults_mono_font =
630 require_font(&self.defaults.mono_font, "defaults.mono_font", &mut missing);
631
632 let defaults_background = require(
633 &self.defaults.background,
634 "defaults.background",
635 &mut missing,
636 );
637 let defaults_foreground = require(
638 &self.defaults.foreground,
639 "defaults.foreground",
640 &mut missing,
641 );
642 let defaults_accent = require(&self.defaults.accent, "defaults.accent", &mut missing);
643 let defaults_accent_foreground = require(
644 &self.defaults.accent_foreground,
645 "defaults.accent_foreground",
646 &mut missing,
647 );
648 let defaults_surface = require(&self.defaults.surface, "defaults.surface", &mut missing);
649 let defaults_border = require(&self.defaults.border, "defaults.border", &mut missing);
650 let defaults_muted = require(&self.defaults.muted, "defaults.muted", &mut missing);
651 let defaults_shadow = require(&self.defaults.shadow, "defaults.shadow", &mut missing);
652 let defaults_link = require(&self.defaults.link, "defaults.link", &mut missing);
653 let defaults_selection =
654 require(&self.defaults.selection, "defaults.selection", &mut missing);
655 let defaults_selection_foreground = require(
656 &self.defaults.selection_foreground,
657 "defaults.selection_foreground",
658 &mut missing,
659 );
660 let defaults_selection_inactive = require(
661 &self.defaults.selection_inactive,
662 "defaults.selection_inactive",
663 &mut missing,
664 );
665 let defaults_disabled_foreground = require(
666 &self.defaults.disabled_foreground,
667 "defaults.disabled_foreground",
668 &mut missing,
669 );
670
671 let defaults_danger = require(&self.defaults.danger, "defaults.danger", &mut missing);
672 let defaults_danger_foreground = require(
673 &self.defaults.danger_foreground,
674 "defaults.danger_foreground",
675 &mut missing,
676 );
677 let defaults_warning = require(&self.defaults.warning, "defaults.warning", &mut missing);
678 let defaults_warning_foreground = require(
679 &self.defaults.warning_foreground,
680 "defaults.warning_foreground",
681 &mut missing,
682 );
683 let defaults_success = require(&self.defaults.success, "defaults.success", &mut missing);
684 let defaults_success_foreground = require(
685 &self.defaults.success_foreground,
686 "defaults.success_foreground",
687 &mut missing,
688 );
689 let defaults_info = require(&self.defaults.info, "defaults.info", &mut missing);
690 let defaults_info_foreground = require(
691 &self.defaults.info_foreground,
692 "defaults.info_foreground",
693 &mut missing,
694 );
695
696 let defaults_radius = require(&self.defaults.radius, "defaults.radius", &mut missing);
697 let defaults_radius_lg =
698 require(&self.defaults.radius_lg, "defaults.radius_lg", &mut missing);
699 let defaults_frame_width = require(
700 &self.defaults.frame_width,
701 "defaults.frame_width",
702 &mut missing,
703 );
704 let defaults_disabled_opacity = require(
705 &self.defaults.disabled_opacity,
706 "defaults.disabled_opacity",
707 &mut missing,
708 );
709 let defaults_border_opacity = require(
710 &self.defaults.border_opacity,
711 "defaults.border_opacity",
712 &mut missing,
713 );
714 let defaults_shadow_enabled = require(
715 &self.defaults.shadow_enabled,
716 "defaults.shadow_enabled",
717 &mut missing,
718 );
719
720 let defaults_focus_ring_color = require(
721 &self.defaults.focus_ring_color,
722 "defaults.focus_ring_color",
723 &mut missing,
724 );
725 let defaults_focus_ring_width = require(
726 &self.defaults.focus_ring_width,
727 "defaults.focus_ring_width",
728 &mut missing,
729 );
730 let defaults_focus_ring_offset = require(
731 &self.defaults.focus_ring_offset,
732 "defaults.focus_ring_offset",
733 &mut missing,
734 );
735
736 let defaults_spacing_xxs = require(
737 &self.defaults.spacing.xxs,
738 "defaults.spacing.xxs",
739 &mut missing,
740 );
741 let defaults_spacing_xs = require(
742 &self.defaults.spacing.xs,
743 "defaults.spacing.xs",
744 &mut missing,
745 );
746 let defaults_spacing_s =
747 require(&self.defaults.spacing.s, "defaults.spacing.s", &mut missing);
748 let defaults_spacing_m =
749 require(&self.defaults.spacing.m, "defaults.spacing.m", &mut missing);
750 let defaults_spacing_l =
751 require(&self.defaults.spacing.l, "defaults.spacing.l", &mut missing);
752 let defaults_spacing_xl = require(
753 &self.defaults.spacing.xl,
754 "defaults.spacing.xl",
755 &mut missing,
756 );
757 let defaults_spacing_xxl = require(
758 &self.defaults.spacing.xxl,
759 "defaults.spacing.xxl",
760 &mut missing,
761 );
762
763 let defaults_icon_sizes_toolbar = require(
764 &self.defaults.icon_sizes.toolbar,
765 "defaults.icon_sizes.toolbar",
766 &mut missing,
767 );
768 let defaults_icon_sizes_small = require(
769 &self.defaults.icon_sizes.small,
770 "defaults.icon_sizes.small",
771 &mut missing,
772 );
773 let defaults_icon_sizes_large = require(
774 &self.defaults.icon_sizes.large,
775 "defaults.icon_sizes.large",
776 &mut missing,
777 );
778 let defaults_icon_sizes_dialog = require(
779 &self.defaults.icon_sizes.dialog,
780 "defaults.icon_sizes.dialog",
781 &mut missing,
782 );
783 let defaults_icon_sizes_panel = require(
784 &self.defaults.icon_sizes.panel,
785 "defaults.icon_sizes.panel",
786 &mut missing,
787 );
788
789 let defaults_text_scaling_factor = require(
790 &self.defaults.text_scaling_factor,
791 "defaults.text_scaling_factor",
792 &mut missing,
793 );
794 let defaults_reduce_motion = require(
795 &self.defaults.reduce_motion,
796 "defaults.reduce_motion",
797 &mut missing,
798 );
799 let defaults_high_contrast = require(
800 &self.defaults.high_contrast,
801 "defaults.high_contrast",
802 &mut missing,
803 );
804 let defaults_reduce_transparency = require(
805 &self.defaults.reduce_transparency,
806 "defaults.reduce_transparency",
807 &mut missing,
808 );
809
810 let ts_caption =
813 require_text_scale_entry(&self.text_scale.caption, "text_scale.caption", &mut missing);
814 let ts_section_heading = require_text_scale_entry(
815 &self.text_scale.section_heading,
816 "text_scale.section_heading",
817 &mut missing,
818 );
819 let ts_dialog_title = require_text_scale_entry(
820 &self.text_scale.dialog_title,
821 "text_scale.dialog_title",
822 &mut missing,
823 );
824 let ts_display =
825 require_text_scale_entry(&self.text_scale.display, "text_scale.display", &mut missing);
826
827 let window_background = require(&self.window.background, "window.background", &mut missing);
830 let window_foreground = require(&self.window.foreground, "window.foreground", &mut missing);
831 let window_border = require(&self.window.border, "window.border", &mut missing);
832 let window_title_bar_background = require(
833 &self.window.title_bar_background,
834 "window.title_bar_background",
835 &mut missing,
836 );
837 let window_title_bar_foreground = require(
838 &self.window.title_bar_foreground,
839 "window.title_bar_foreground",
840 &mut missing,
841 );
842 let window_inactive_title_bar_background = require(
843 &self.window.inactive_title_bar_background,
844 "window.inactive_title_bar_background",
845 &mut missing,
846 );
847 let window_inactive_title_bar_foreground = require(
848 &self.window.inactive_title_bar_foreground,
849 "window.inactive_title_bar_foreground",
850 &mut missing,
851 );
852 let window_radius = require(&self.window.radius, "window.radius", &mut missing);
853 let window_shadow = require(&self.window.shadow, "window.shadow", &mut missing);
854 let window_title_bar_font = require_font_opt(
855 &self.window.title_bar_font,
856 "window.title_bar_font",
857 &mut missing,
858 );
859
860 let button_background = require(&self.button.background, "button.background", &mut missing);
863 let button_foreground = require(&self.button.foreground, "button.foreground", &mut missing);
864 let button_border = require(&self.button.border, "button.border", &mut missing);
865 let button_primary_background = require(
866 &self.button.primary_background,
867 "button.primary_background",
868 &mut missing,
869 );
870 let button_primary_foreground = require(
871 &self.button.primary_foreground,
872 "button.primary_foreground",
873 &mut missing,
874 );
875 let button_min_width = require(&self.button.min_width, "button.min_width", &mut missing);
876 let button_min_height = require(&self.button.min_height, "button.min_height", &mut missing);
877 let button_padding_horizontal = require(
878 &self.button.padding_horizontal,
879 "button.padding_horizontal",
880 &mut missing,
881 );
882 let button_padding_vertical = require(
883 &self.button.padding_vertical,
884 "button.padding_vertical",
885 &mut missing,
886 );
887 let button_radius = require(&self.button.radius, "button.radius", &mut missing);
888 let button_icon_spacing = require(
889 &self.button.icon_spacing,
890 "button.icon_spacing",
891 &mut missing,
892 );
893 let button_disabled_opacity = require(
894 &self.button.disabled_opacity,
895 "button.disabled_opacity",
896 &mut missing,
897 );
898 let button_shadow = require(&self.button.shadow, "button.shadow", &mut missing);
899 let button_font = require_font_opt(&self.button.font, "button.font", &mut missing);
900
901 let input_background = require(&self.input.background, "input.background", &mut missing);
904 let input_foreground = require(&self.input.foreground, "input.foreground", &mut missing);
905 let input_border = require(&self.input.border, "input.border", &mut missing);
906 let input_placeholder = require(&self.input.placeholder, "input.placeholder", &mut missing);
907 let input_caret = require(&self.input.caret, "input.caret", &mut missing);
908 let input_selection = require(&self.input.selection, "input.selection", &mut missing);
909 let input_selection_foreground = require(
910 &self.input.selection_foreground,
911 "input.selection_foreground",
912 &mut missing,
913 );
914 let input_min_height = require(&self.input.min_height, "input.min_height", &mut missing);
915 let input_padding_horizontal = require(
916 &self.input.padding_horizontal,
917 "input.padding_horizontal",
918 &mut missing,
919 );
920 let input_padding_vertical = require(
921 &self.input.padding_vertical,
922 "input.padding_vertical",
923 &mut missing,
924 );
925 let input_radius = require(&self.input.radius, "input.radius", &mut missing);
926 let input_border_width =
927 require(&self.input.border_width, "input.border_width", &mut missing);
928 let input_font = require_font_opt(&self.input.font, "input.font", &mut missing);
929
930 let checkbox_checked_background = require(
933 &self.checkbox.checked_background,
934 "checkbox.checked_background",
935 &mut missing,
936 );
937 let checkbox_indicator_size = require(
938 &self.checkbox.indicator_size,
939 "checkbox.indicator_size",
940 &mut missing,
941 );
942 let checkbox_spacing = require(&self.checkbox.spacing, "checkbox.spacing", &mut missing);
943 let checkbox_radius = require(&self.checkbox.radius, "checkbox.radius", &mut missing);
944 let checkbox_border_width = require(
945 &self.checkbox.border_width,
946 "checkbox.border_width",
947 &mut missing,
948 );
949
950 let menu_background = require(&self.menu.background, "menu.background", &mut missing);
953 let menu_foreground = require(&self.menu.foreground, "menu.foreground", &mut missing);
954 let menu_separator = require(&self.menu.separator, "menu.separator", &mut missing);
955 let menu_item_height = require(&self.menu.item_height, "menu.item_height", &mut missing);
956 let menu_padding_horizontal = require(
957 &self.menu.padding_horizontal,
958 "menu.padding_horizontal",
959 &mut missing,
960 );
961 let menu_padding_vertical = require(
962 &self.menu.padding_vertical,
963 "menu.padding_vertical",
964 &mut missing,
965 );
966 let menu_icon_spacing = require(&self.menu.icon_spacing, "menu.icon_spacing", &mut missing);
967 let menu_font = require_font_opt(&self.menu.font, "menu.font", &mut missing);
968
969 let tooltip_background =
972 require(&self.tooltip.background, "tooltip.background", &mut missing);
973 let tooltip_foreground =
974 require(&self.tooltip.foreground, "tooltip.foreground", &mut missing);
975 let tooltip_padding_horizontal = require(
976 &self.tooltip.padding_horizontal,
977 "tooltip.padding_horizontal",
978 &mut missing,
979 );
980 let tooltip_padding_vertical = require(
981 &self.tooltip.padding_vertical,
982 "tooltip.padding_vertical",
983 &mut missing,
984 );
985 let tooltip_max_width = require(&self.tooltip.max_width, "tooltip.max_width", &mut missing);
986 let tooltip_radius = require(&self.tooltip.radius, "tooltip.radius", &mut missing);
987 let tooltip_font = require_font_opt(&self.tooltip.font, "tooltip.font", &mut missing);
988
989 let scrollbar_track = require(&self.scrollbar.track, "scrollbar.track", &mut missing);
992 let scrollbar_thumb = require(&self.scrollbar.thumb, "scrollbar.thumb", &mut missing);
993 let scrollbar_thumb_hover = require(
994 &self.scrollbar.thumb_hover,
995 "scrollbar.thumb_hover",
996 &mut missing,
997 );
998 let scrollbar_width = require(&self.scrollbar.width, "scrollbar.width", &mut missing);
999 let scrollbar_min_thumb_height = require(
1000 &self.scrollbar.min_thumb_height,
1001 "scrollbar.min_thumb_height",
1002 &mut missing,
1003 );
1004 let scrollbar_slider_width = require(
1005 &self.scrollbar.slider_width,
1006 "scrollbar.slider_width",
1007 &mut missing,
1008 );
1009 let scrollbar_overlay_mode = require(
1010 &self.scrollbar.overlay_mode,
1011 "scrollbar.overlay_mode",
1012 &mut missing,
1013 );
1014
1015 let slider_fill = require(&self.slider.fill, "slider.fill", &mut missing);
1018 let slider_track = require(&self.slider.track, "slider.track", &mut missing);
1019 let slider_thumb = require(&self.slider.thumb, "slider.thumb", &mut missing);
1020 let slider_track_height = require(
1021 &self.slider.track_height,
1022 "slider.track_height",
1023 &mut missing,
1024 );
1025 let slider_thumb_size = require(&self.slider.thumb_size, "slider.thumb_size", &mut missing);
1026 let slider_tick_length =
1027 require(&self.slider.tick_length, "slider.tick_length", &mut missing);
1028
1029 let progress_bar_fill = require(&self.progress_bar.fill, "progress_bar.fill", &mut missing);
1032 let progress_bar_track =
1033 require(&self.progress_bar.track, "progress_bar.track", &mut missing);
1034 let progress_bar_height = require(
1035 &self.progress_bar.height,
1036 "progress_bar.height",
1037 &mut missing,
1038 );
1039 let progress_bar_min_width = require(
1040 &self.progress_bar.min_width,
1041 "progress_bar.min_width",
1042 &mut missing,
1043 );
1044 let progress_bar_radius = require(
1045 &self.progress_bar.radius,
1046 "progress_bar.radius",
1047 &mut missing,
1048 );
1049
1050 let tab_background = require(&self.tab.background, "tab.background", &mut missing);
1053 let tab_foreground = require(&self.tab.foreground, "tab.foreground", &mut missing);
1054 let tab_active_background = require(
1055 &self.tab.active_background,
1056 "tab.active_background",
1057 &mut missing,
1058 );
1059 let tab_active_foreground = require(
1060 &self.tab.active_foreground,
1061 "tab.active_foreground",
1062 &mut missing,
1063 );
1064 let tab_bar_background =
1065 require(&self.tab.bar_background, "tab.bar_background", &mut missing);
1066 let tab_min_width = require(&self.tab.min_width, "tab.min_width", &mut missing);
1067 let tab_min_height = require(&self.tab.min_height, "tab.min_height", &mut missing);
1068 let tab_padding_horizontal = require(
1069 &self.tab.padding_horizontal,
1070 "tab.padding_horizontal",
1071 &mut missing,
1072 );
1073 let tab_padding_vertical = require(
1074 &self.tab.padding_vertical,
1075 "tab.padding_vertical",
1076 &mut missing,
1077 );
1078
1079 let sidebar_background =
1082 require(&self.sidebar.background, "sidebar.background", &mut missing);
1083 let sidebar_foreground =
1084 require(&self.sidebar.foreground, "sidebar.foreground", &mut missing);
1085
1086 let toolbar_height = require(&self.toolbar.height, "toolbar.height", &mut missing);
1089 let toolbar_item_spacing = require(
1090 &self.toolbar.item_spacing,
1091 "toolbar.item_spacing",
1092 &mut missing,
1093 );
1094 let toolbar_padding = require(&self.toolbar.padding, "toolbar.padding", &mut missing);
1095 let toolbar_font = require_font_opt(&self.toolbar.font, "toolbar.font", &mut missing);
1096
1097 let status_bar_font =
1100 require_font_opt(&self.status_bar.font, "status_bar.font", &mut missing);
1101
1102 let list_background = require(&self.list.background, "list.background", &mut missing);
1105 let list_foreground = require(&self.list.foreground, "list.foreground", &mut missing);
1106 let list_alternate_row =
1107 require(&self.list.alternate_row, "list.alternate_row", &mut missing);
1108 let list_selection = require(&self.list.selection, "list.selection", &mut missing);
1109 let list_selection_foreground = require(
1110 &self.list.selection_foreground,
1111 "list.selection_foreground",
1112 &mut missing,
1113 );
1114 let list_header_background = require(
1115 &self.list.header_background,
1116 "list.header_background",
1117 &mut missing,
1118 );
1119 let list_header_foreground = require(
1120 &self.list.header_foreground,
1121 "list.header_foreground",
1122 &mut missing,
1123 );
1124 let list_grid_color = require(&self.list.grid_color, "list.grid_color", &mut missing);
1125 let list_item_height = require(&self.list.item_height, "list.item_height", &mut missing);
1126 let list_padding_horizontal = require(
1127 &self.list.padding_horizontal,
1128 "list.padding_horizontal",
1129 &mut missing,
1130 );
1131 let list_padding_vertical = require(
1132 &self.list.padding_vertical,
1133 "list.padding_vertical",
1134 &mut missing,
1135 );
1136
1137 let popover_background =
1140 require(&self.popover.background, "popover.background", &mut missing);
1141 let popover_foreground =
1142 require(&self.popover.foreground, "popover.foreground", &mut missing);
1143 let popover_border = require(&self.popover.border, "popover.border", &mut missing);
1144 let popover_radius = require(&self.popover.radius, "popover.radius", &mut missing);
1145
1146 let splitter_width = require(&self.splitter.width, "splitter.width", &mut missing);
1149
1150 let separator_color = require(&self.separator.color, "separator.color", &mut missing);
1153
1154 let switch_checked_background = require(
1157 &self.switch.checked_background,
1158 "switch.checked_background",
1159 &mut missing,
1160 );
1161 let switch_unchecked_background = require(
1162 &self.switch.unchecked_background,
1163 "switch.unchecked_background",
1164 &mut missing,
1165 );
1166 let switch_thumb_background = require(
1167 &self.switch.thumb_background,
1168 "switch.thumb_background",
1169 &mut missing,
1170 );
1171 let switch_track_width =
1172 require(&self.switch.track_width, "switch.track_width", &mut missing);
1173 let switch_track_height = require(
1174 &self.switch.track_height,
1175 "switch.track_height",
1176 &mut missing,
1177 );
1178 let switch_thumb_size = require(&self.switch.thumb_size, "switch.thumb_size", &mut missing);
1179 let switch_track_radius = require(
1180 &self.switch.track_radius,
1181 "switch.track_radius",
1182 &mut missing,
1183 );
1184
1185 let dialog_min_width = require(&self.dialog.min_width, "dialog.min_width", &mut missing);
1188 let dialog_max_width = require(&self.dialog.max_width, "dialog.max_width", &mut missing);
1189 let dialog_min_height = require(&self.dialog.min_height, "dialog.min_height", &mut missing);
1190 let dialog_max_height = require(&self.dialog.max_height, "dialog.max_height", &mut missing);
1191 let dialog_content_padding = require(
1192 &self.dialog.content_padding,
1193 "dialog.content_padding",
1194 &mut missing,
1195 );
1196 let dialog_button_spacing = require(
1197 &self.dialog.button_spacing,
1198 "dialog.button_spacing",
1199 &mut missing,
1200 );
1201 let dialog_radius = require(&self.dialog.radius, "dialog.radius", &mut missing);
1202 let dialog_icon_size = require(&self.dialog.icon_size, "dialog.icon_size", &mut missing);
1203 let dialog_button_order = require(
1204 &self.dialog.button_order,
1205 "dialog.button_order",
1206 &mut missing,
1207 );
1208 let dialog_title_font =
1209 require_font_opt(&self.dialog.title_font, "dialog.title_font", &mut missing);
1210
1211 let spinner_fill = require(&self.spinner.fill, "spinner.fill", &mut missing);
1214 let spinner_diameter = require(&self.spinner.diameter, "spinner.diameter", &mut missing);
1215 let spinner_min_size = require(&self.spinner.min_size, "spinner.min_size", &mut missing);
1216 let spinner_stroke_width = require(
1217 &self.spinner.stroke_width,
1218 "spinner.stroke_width",
1219 &mut missing,
1220 );
1221
1222 let combo_box_min_height = require(
1225 &self.combo_box.min_height,
1226 "combo_box.min_height",
1227 &mut missing,
1228 );
1229 let combo_box_min_width = require(
1230 &self.combo_box.min_width,
1231 "combo_box.min_width",
1232 &mut missing,
1233 );
1234 let combo_box_padding_horizontal = require(
1235 &self.combo_box.padding_horizontal,
1236 "combo_box.padding_horizontal",
1237 &mut missing,
1238 );
1239 let combo_box_arrow_size = require(
1240 &self.combo_box.arrow_size,
1241 "combo_box.arrow_size",
1242 &mut missing,
1243 );
1244 let combo_box_arrow_area_width = require(
1245 &self.combo_box.arrow_area_width,
1246 "combo_box.arrow_area_width",
1247 &mut missing,
1248 );
1249 let combo_box_radius = require(&self.combo_box.radius, "combo_box.radius", &mut missing);
1250
1251 let segmented_control_segment_height = require(
1254 &self.segmented_control.segment_height,
1255 "segmented_control.segment_height",
1256 &mut missing,
1257 );
1258 let segmented_control_separator_width = require(
1259 &self.segmented_control.separator_width,
1260 "segmented_control.separator_width",
1261 &mut missing,
1262 );
1263 let segmented_control_padding_horizontal = require(
1264 &self.segmented_control.padding_horizontal,
1265 "segmented_control.padding_horizontal",
1266 &mut missing,
1267 );
1268 let segmented_control_radius = require(
1269 &self.segmented_control.radius,
1270 "segmented_control.radius",
1271 &mut missing,
1272 );
1273
1274 let card_background = require(&self.card.background, "card.background", &mut missing);
1277 let card_border = require(&self.card.border, "card.border", &mut missing);
1278 let card_radius = require(&self.card.radius, "card.radius", &mut missing);
1279 let card_padding = require(&self.card.padding, "card.padding", &mut missing);
1280 let card_shadow = require(&self.card.shadow, "card.shadow", &mut missing);
1281
1282 let expander_header_height = require(
1285 &self.expander.header_height,
1286 "expander.header_height",
1287 &mut missing,
1288 );
1289 let expander_arrow_size = require(
1290 &self.expander.arrow_size,
1291 "expander.arrow_size",
1292 &mut missing,
1293 );
1294 let expander_content_padding = require(
1295 &self.expander.content_padding,
1296 "expander.content_padding",
1297 &mut missing,
1298 );
1299 let expander_radius = require(&self.expander.radius, "expander.radius", &mut missing);
1300
1301 let link_color = require(&self.link.color, "link.color", &mut missing);
1304 let link_visited = require(&self.link.visited, "link.visited", &mut missing);
1305 let link_background = require(&self.link.background, "link.background", &mut missing);
1306 let link_hover_bg = require(&self.link.hover_bg, "link.hover_bg", &mut missing);
1307 let link_underline = require(&self.link.underline, "link.underline", &mut missing);
1308
1309 let icon_set = require(&self.icon_set, "icon_set", &mut missing);
1312 let icon_theme = require(&self.icon_theme, "icon_theme", &mut missing);
1313
1314 check_positive(defaults_font.size, "defaults.font.size", &mut missing);
1323 check_range_u16(
1324 defaults_font.weight,
1325 100,
1326 900,
1327 "defaults.font.weight",
1328 &mut missing,
1329 );
1330 check_positive(
1331 defaults_mono_font.size,
1332 "defaults.mono_font.size",
1333 &mut missing,
1334 );
1335 check_range_u16(
1336 defaults_mono_font.weight,
1337 100,
1338 900,
1339 "defaults.mono_font.weight",
1340 &mut missing,
1341 );
1342
1343 check_positive(defaults_line_height, "defaults.line_height", &mut missing);
1345 check_positive(
1346 defaults_text_scaling_factor,
1347 "defaults.text_scaling_factor",
1348 &mut missing,
1349 );
1350
1351 check_non_negative(defaults_radius, "defaults.radius", &mut missing);
1353 check_non_negative(defaults_radius_lg, "defaults.radius_lg", &mut missing);
1354 check_non_negative(defaults_frame_width, "defaults.frame_width", &mut missing);
1355 check_non_negative(
1356 defaults_focus_ring_width,
1357 "defaults.focus_ring_width",
1358 &mut missing,
1359 );
1360 check_range_f32(
1365 defaults_disabled_opacity,
1366 0.0,
1367 1.0,
1368 "defaults.disabled_opacity",
1369 &mut missing,
1370 );
1371 check_range_f32(
1372 defaults_border_opacity,
1373 0.0,
1374 1.0,
1375 "defaults.border_opacity",
1376 &mut missing,
1377 );
1378
1379 check_non_negative(defaults_spacing_xxs, "defaults.spacing.xxs", &mut missing);
1381 check_non_negative(defaults_spacing_xs, "defaults.spacing.xs", &mut missing);
1382 check_non_negative(defaults_spacing_s, "defaults.spacing.s", &mut missing);
1383 check_non_negative(defaults_spacing_m, "defaults.spacing.m", &mut missing);
1384 check_non_negative(defaults_spacing_l, "defaults.spacing.l", &mut missing);
1385 check_non_negative(defaults_spacing_xl, "defaults.spacing.xl", &mut missing);
1386 check_non_negative(defaults_spacing_xxl, "defaults.spacing.xxl", &mut missing);
1387
1388 check_non_negative(
1390 defaults_icon_sizes_toolbar,
1391 "defaults.icon_sizes.toolbar",
1392 &mut missing,
1393 );
1394 check_non_negative(
1395 defaults_icon_sizes_small,
1396 "defaults.icon_sizes.small",
1397 &mut missing,
1398 );
1399 check_non_negative(
1400 defaults_icon_sizes_large,
1401 "defaults.icon_sizes.large",
1402 &mut missing,
1403 );
1404 check_non_negative(
1405 defaults_icon_sizes_dialog,
1406 "defaults.icon_sizes.dialog",
1407 &mut missing,
1408 );
1409 check_non_negative(
1410 defaults_icon_sizes_panel,
1411 "defaults.icon_sizes.panel",
1412 &mut missing,
1413 );
1414
1415 check_positive(ts_caption.size, "text_scale.caption.size", &mut missing);
1417 check_positive(
1418 ts_caption.line_height,
1419 "text_scale.caption.line_height",
1420 &mut missing,
1421 );
1422 check_range_u16(
1423 ts_caption.weight,
1424 100,
1425 900,
1426 "text_scale.caption.weight",
1427 &mut missing,
1428 );
1429 check_positive(
1430 ts_section_heading.size,
1431 "text_scale.section_heading.size",
1432 &mut missing,
1433 );
1434 check_positive(
1435 ts_section_heading.line_height,
1436 "text_scale.section_heading.line_height",
1437 &mut missing,
1438 );
1439 check_range_u16(
1440 ts_section_heading.weight,
1441 100,
1442 900,
1443 "text_scale.section_heading.weight",
1444 &mut missing,
1445 );
1446 check_positive(
1447 ts_dialog_title.size,
1448 "text_scale.dialog_title.size",
1449 &mut missing,
1450 );
1451 check_positive(
1452 ts_dialog_title.line_height,
1453 "text_scale.dialog_title.line_height",
1454 &mut missing,
1455 );
1456 check_range_u16(
1457 ts_dialog_title.weight,
1458 100,
1459 900,
1460 "text_scale.dialog_title.weight",
1461 &mut missing,
1462 );
1463 check_positive(ts_display.size, "text_scale.display.size", &mut missing);
1464 check_positive(
1465 ts_display.line_height,
1466 "text_scale.display.line_height",
1467 &mut missing,
1468 );
1469 check_range_u16(
1470 ts_display.weight,
1471 100,
1472 900,
1473 "text_scale.display.weight",
1474 &mut missing,
1475 );
1476
1477 check_non_negative(window_radius, "window.radius", &mut missing);
1479
1480 check_positive(
1482 window_title_bar_font.size,
1483 "window.title_bar_font.size",
1484 &mut missing,
1485 );
1486 check_range_u16(
1487 window_title_bar_font.weight,
1488 100,
1489 900,
1490 "window.title_bar_font.weight",
1491 &mut missing,
1492 );
1493
1494 check_non_negative(button_min_width, "button.min_width", &mut missing);
1496 check_non_negative(button_min_height, "button.min_height", &mut missing);
1497 check_non_negative(
1498 button_padding_horizontal,
1499 "button.padding_horizontal",
1500 &mut missing,
1501 );
1502 check_non_negative(
1503 button_padding_vertical,
1504 "button.padding_vertical",
1505 &mut missing,
1506 );
1507 check_non_negative(button_radius, "button.radius", &mut missing);
1508 check_non_negative(button_icon_spacing, "button.icon_spacing", &mut missing);
1509 check_range_f32(
1510 button_disabled_opacity,
1511 0.0,
1512 1.0,
1513 "button.disabled_opacity",
1514 &mut missing,
1515 );
1516 check_positive(button_font.size, "button.font.size", &mut missing);
1517 check_range_u16(
1518 button_font.weight,
1519 100,
1520 900,
1521 "button.font.weight",
1522 &mut missing,
1523 );
1524
1525 check_non_negative(input_min_height, "input.min_height", &mut missing);
1527 check_non_negative(
1528 input_padding_horizontal,
1529 "input.padding_horizontal",
1530 &mut missing,
1531 );
1532 check_non_negative(
1533 input_padding_vertical,
1534 "input.padding_vertical",
1535 &mut missing,
1536 );
1537 check_non_negative(input_radius, "input.radius", &mut missing);
1538 check_non_negative(input_border_width, "input.border_width", &mut missing);
1539 check_positive(input_font.size, "input.font.size", &mut missing);
1540 check_range_u16(
1541 input_font.weight,
1542 100,
1543 900,
1544 "input.font.weight",
1545 &mut missing,
1546 );
1547
1548 check_non_negative(
1550 checkbox_indicator_size,
1551 "checkbox.indicator_size",
1552 &mut missing,
1553 );
1554 check_non_negative(checkbox_spacing, "checkbox.spacing", &mut missing);
1555 check_non_negative(checkbox_radius, "checkbox.radius", &mut missing);
1556 check_non_negative(checkbox_border_width, "checkbox.border_width", &mut missing);
1557
1558 check_non_negative(menu_item_height, "menu.item_height", &mut missing);
1560 check_non_negative(
1561 menu_padding_horizontal,
1562 "menu.padding_horizontal",
1563 &mut missing,
1564 );
1565 check_non_negative(menu_padding_vertical, "menu.padding_vertical", &mut missing);
1566 check_non_negative(menu_icon_spacing, "menu.icon_spacing", &mut missing);
1567 check_positive(menu_font.size, "menu.font.size", &mut missing);
1568 check_range_u16(menu_font.weight, 100, 900, "menu.font.weight", &mut missing);
1569
1570 check_non_negative(
1572 tooltip_padding_horizontal,
1573 "tooltip.padding_horizontal",
1574 &mut missing,
1575 );
1576 check_non_negative(
1577 tooltip_padding_vertical,
1578 "tooltip.padding_vertical",
1579 &mut missing,
1580 );
1581 check_non_negative(tooltip_max_width, "tooltip.max_width", &mut missing);
1582 check_non_negative(tooltip_radius, "tooltip.radius", &mut missing);
1583 check_positive(tooltip_font.size, "tooltip.font.size", &mut missing);
1584 check_range_u16(
1585 tooltip_font.weight,
1586 100,
1587 900,
1588 "tooltip.font.weight",
1589 &mut missing,
1590 );
1591
1592 check_non_negative(scrollbar_width, "scrollbar.width", &mut missing);
1594 check_non_negative(
1595 scrollbar_min_thumb_height,
1596 "scrollbar.min_thumb_height",
1597 &mut missing,
1598 );
1599 check_non_negative(
1600 scrollbar_slider_width,
1601 "scrollbar.slider_width",
1602 &mut missing,
1603 );
1604
1605 check_non_negative(slider_track_height, "slider.track_height", &mut missing);
1607 check_non_negative(slider_thumb_size, "slider.thumb_size", &mut missing);
1608 check_non_negative(slider_tick_length, "slider.tick_length", &mut missing);
1609
1610 check_non_negative(progress_bar_height, "progress_bar.height", &mut missing);
1612 check_non_negative(
1613 progress_bar_min_width,
1614 "progress_bar.min_width",
1615 &mut missing,
1616 );
1617 check_non_negative(progress_bar_radius, "progress_bar.radius", &mut missing);
1618
1619 check_non_negative(tab_min_width, "tab.min_width", &mut missing);
1621 check_non_negative(tab_min_height, "tab.min_height", &mut missing);
1622 check_non_negative(
1623 tab_padding_horizontal,
1624 "tab.padding_horizontal",
1625 &mut missing,
1626 );
1627 check_non_negative(tab_padding_vertical, "tab.padding_vertical", &mut missing);
1628
1629 check_non_negative(toolbar_height, "toolbar.height", &mut missing);
1631 check_non_negative(toolbar_item_spacing, "toolbar.item_spacing", &mut missing);
1632 check_non_negative(toolbar_padding, "toolbar.padding", &mut missing);
1633 check_positive(toolbar_font.size, "toolbar.font.size", &mut missing);
1634 check_range_u16(
1635 toolbar_font.weight,
1636 100,
1637 900,
1638 "toolbar.font.weight",
1639 &mut missing,
1640 );
1641
1642 check_positive(status_bar_font.size, "status_bar.font.size", &mut missing);
1644 check_range_u16(
1645 status_bar_font.weight,
1646 100,
1647 900,
1648 "status_bar.font.weight",
1649 &mut missing,
1650 );
1651
1652 check_non_negative(list_item_height, "list.item_height", &mut missing);
1654 check_non_negative(
1655 list_padding_horizontal,
1656 "list.padding_horizontal",
1657 &mut missing,
1658 );
1659 check_non_negative(list_padding_vertical, "list.padding_vertical", &mut missing);
1660
1661 check_non_negative(popover_radius, "popover.radius", &mut missing);
1663
1664 check_non_negative(splitter_width, "splitter.width", &mut missing);
1666
1667 check_non_negative(switch_track_width, "switch.track_width", &mut missing);
1669 check_non_negative(switch_track_height, "switch.track_height", &mut missing);
1670 check_non_negative(switch_thumb_size, "switch.thumb_size", &mut missing);
1671 check_non_negative(switch_track_radius, "switch.track_radius", &mut missing);
1672
1673 check_non_negative(dialog_min_width, "dialog.min_width", &mut missing);
1675 check_non_negative(dialog_max_width, "dialog.max_width", &mut missing);
1676 check_non_negative(dialog_min_height, "dialog.min_height", &mut missing);
1677 check_non_negative(dialog_max_height, "dialog.max_height", &mut missing);
1678 check_non_negative(
1679 dialog_content_padding,
1680 "dialog.content_padding",
1681 &mut missing,
1682 );
1683 check_non_negative(dialog_button_spacing, "dialog.button_spacing", &mut missing);
1684 check_non_negative(dialog_radius, "dialog.radius", &mut missing);
1685 check_non_negative(dialog_icon_size, "dialog.icon_size", &mut missing);
1686 check_positive(
1687 dialog_title_font.size,
1688 "dialog.title_font.size",
1689 &mut missing,
1690 );
1691 check_range_u16(
1692 dialog_title_font.weight,
1693 100,
1694 900,
1695 "dialog.title_font.weight",
1696 &mut missing,
1697 );
1698
1699 check_non_negative(spinner_diameter, "spinner.diameter", &mut missing);
1701 check_non_negative(spinner_min_size, "spinner.min_size", &mut missing);
1702 check_non_negative(spinner_stroke_width, "spinner.stroke_width", &mut missing);
1703
1704 check_non_negative(combo_box_min_height, "combo_box.min_height", &mut missing);
1706 check_non_negative(combo_box_min_width, "combo_box.min_width", &mut missing);
1707 check_non_negative(
1708 combo_box_padding_horizontal,
1709 "combo_box.padding_horizontal",
1710 &mut missing,
1711 );
1712 check_non_negative(combo_box_arrow_size, "combo_box.arrow_size", &mut missing);
1713 check_non_negative(
1714 combo_box_arrow_area_width,
1715 "combo_box.arrow_area_width",
1716 &mut missing,
1717 );
1718 check_non_negative(combo_box_radius, "combo_box.radius", &mut missing);
1719
1720 check_non_negative(
1722 segmented_control_segment_height,
1723 "segmented_control.segment_height",
1724 &mut missing,
1725 );
1726 check_non_negative(
1727 segmented_control_separator_width,
1728 "segmented_control.separator_width",
1729 &mut missing,
1730 );
1731 check_non_negative(
1732 segmented_control_padding_horizontal,
1733 "segmented_control.padding_horizontal",
1734 &mut missing,
1735 );
1736 check_non_negative(
1737 segmented_control_radius,
1738 "segmented_control.radius",
1739 &mut missing,
1740 );
1741
1742 check_non_negative(card_radius, "card.radius", &mut missing);
1744 check_non_negative(card_padding, "card.padding", &mut missing);
1745
1746 check_non_negative(
1748 expander_header_height,
1749 "expander.header_height",
1750 &mut missing,
1751 );
1752 check_non_negative(expander_arrow_size, "expander.arrow_size", &mut missing);
1753 check_non_negative(
1754 expander_content_padding,
1755 "expander.content_padding",
1756 &mut missing,
1757 );
1758 check_non_negative(expander_radius, "expander.radius", &mut missing);
1759
1760 if !missing.is_empty() {
1763 return Err(crate::Error::Resolution(ThemeResolutionError {
1764 missing_fields: missing,
1765 }));
1766 }
1767
1768 Ok(ResolvedThemeVariant {
1772 defaults: ResolvedThemeDefaults {
1773 font: defaults_font,
1774 line_height: defaults_line_height,
1775 mono_font: defaults_mono_font,
1776 background: defaults_background,
1777 foreground: defaults_foreground,
1778 accent: defaults_accent,
1779 accent_foreground: defaults_accent_foreground,
1780 surface: defaults_surface,
1781 border: defaults_border,
1782 muted: defaults_muted,
1783 shadow: defaults_shadow,
1784 link: defaults_link,
1785 selection: defaults_selection,
1786 selection_foreground: defaults_selection_foreground,
1787 selection_inactive: defaults_selection_inactive,
1788 disabled_foreground: defaults_disabled_foreground,
1789 danger: defaults_danger,
1790 danger_foreground: defaults_danger_foreground,
1791 warning: defaults_warning,
1792 warning_foreground: defaults_warning_foreground,
1793 success: defaults_success,
1794 success_foreground: defaults_success_foreground,
1795 info: defaults_info,
1796 info_foreground: defaults_info_foreground,
1797 radius: defaults_radius,
1798 radius_lg: defaults_radius_lg,
1799 frame_width: defaults_frame_width,
1800 disabled_opacity: defaults_disabled_opacity,
1801 border_opacity: defaults_border_opacity,
1802 shadow_enabled: defaults_shadow_enabled,
1803 focus_ring_color: defaults_focus_ring_color,
1804 focus_ring_width: defaults_focus_ring_width,
1805 focus_ring_offset: defaults_focus_ring_offset,
1806 spacing: ResolvedThemeSpacing {
1807 xxs: defaults_spacing_xxs,
1808 xs: defaults_spacing_xs,
1809 s: defaults_spacing_s,
1810 m: defaults_spacing_m,
1811 l: defaults_spacing_l,
1812 xl: defaults_spacing_xl,
1813 xxl: defaults_spacing_xxl,
1814 },
1815 icon_sizes: ResolvedIconSizes {
1816 toolbar: defaults_icon_sizes_toolbar,
1817 small: defaults_icon_sizes_small,
1818 large: defaults_icon_sizes_large,
1819 dialog: defaults_icon_sizes_dialog,
1820 panel: defaults_icon_sizes_panel,
1821 },
1822 text_scaling_factor: defaults_text_scaling_factor,
1823 reduce_motion: defaults_reduce_motion,
1824 high_contrast: defaults_high_contrast,
1825 reduce_transparency: defaults_reduce_transparency,
1826 },
1827 text_scale: ResolvedTextScale {
1828 caption: ts_caption,
1829 section_heading: ts_section_heading,
1830 dialog_title: ts_dialog_title,
1831 display: ts_display,
1832 },
1833 window: crate::model::widgets::ResolvedWindowTheme {
1834 background: window_background,
1835 foreground: window_foreground,
1836 border: window_border,
1837 title_bar_background: window_title_bar_background,
1838 title_bar_foreground: window_title_bar_foreground,
1839 inactive_title_bar_background: window_inactive_title_bar_background,
1840 inactive_title_bar_foreground: window_inactive_title_bar_foreground,
1841 radius: window_radius,
1842 shadow: window_shadow,
1843 title_bar_font: window_title_bar_font,
1844 },
1845 button: crate::model::widgets::ResolvedButtonTheme {
1846 background: button_background,
1847 foreground: button_foreground,
1848 border: button_border,
1849 primary_background: button_primary_background,
1850 primary_foreground: button_primary_foreground,
1851 min_width: button_min_width,
1852 min_height: button_min_height,
1853 padding_horizontal: button_padding_horizontal,
1854 padding_vertical: button_padding_vertical,
1855 radius: button_radius,
1856 icon_spacing: button_icon_spacing,
1857 disabled_opacity: button_disabled_opacity,
1858 shadow: button_shadow,
1859 font: button_font,
1860 },
1861 input: crate::model::widgets::ResolvedInputTheme {
1862 background: input_background,
1863 foreground: input_foreground,
1864 border: input_border,
1865 placeholder: input_placeholder,
1866 caret: input_caret,
1867 selection: input_selection,
1868 selection_foreground: input_selection_foreground,
1869 min_height: input_min_height,
1870 padding_horizontal: input_padding_horizontal,
1871 padding_vertical: input_padding_vertical,
1872 radius: input_radius,
1873 border_width: input_border_width,
1874 font: input_font,
1875 },
1876 checkbox: crate::model::widgets::ResolvedCheckboxTheme {
1877 checked_background: checkbox_checked_background,
1878 indicator_size: checkbox_indicator_size,
1879 spacing: checkbox_spacing,
1880 radius: checkbox_radius,
1881 border_width: checkbox_border_width,
1882 },
1883 menu: crate::model::widgets::ResolvedMenuTheme {
1884 background: menu_background,
1885 foreground: menu_foreground,
1886 separator: menu_separator,
1887 item_height: menu_item_height,
1888 padding_horizontal: menu_padding_horizontal,
1889 padding_vertical: menu_padding_vertical,
1890 icon_spacing: menu_icon_spacing,
1891 font: menu_font,
1892 },
1893 tooltip: crate::model::widgets::ResolvedTooltipTheme {
1894 background: tooltip_background,
1895 foreground: tooltip_foreground,
1896 padding_horizontal: tooltip_padding_horizontal,
1897 padding_vertical: tooltip_padding_vertical,
1898 max_width: tooltip_max_width,
1899 radius: tooltip_radius,
1900 font: tooltip_font,
1901 },
1902 scrollbar: crate::model::widgets::ResolvedScrollbarTheme {
1903 track: scrollbar_track,
1904 thumb: scrollbar_thumb,
1905 thumb_hover: scrollbar_thumb_hover,
1906 width: scrollbar_width,
1907 min_thumb_height: scrollbar_min_thumb_height,
1908 slider_width: scrollbar_slider_width,
1909 overlay_mode: scrollbar_overlay_mode,
1910 },
1911 slider: crate::model::widgets::ResolvedSliderTheme {
1912 fill: slider_fill,
1913 track: slider_track,
1914 thumb: slider_thumb,
1915 track_height: slider_track_height,
1916 thumb_size: slider_thumb_size,
1917 tick_length: slider_tick_length,
1918 },
1919 progress_bar: crate::model::widgets::ResolvedProgressBarTheme {
1920 fill: progress_bar_fill,
1921 track: progress_bar_track,
1922 height: progress_bar_height,
1923 min_width: progress_bar_min_width,
1924 radius: progress_bar_radius,
1925 },
1926 tab: crate::model::widgets::ResolvedTabTheme {
1927 background: tab_background,
1928 foreground: tab_foreground,
1929 active_background: tab_active_background,
1930 active_foreground: tab_active_foreground,
1931 bar_background: tab_bar_background,
1932 min_width: tab_min_width,
1933 min_height: tab_min_height,
1934 padding_horizontal: tab_padding_horizontal,
1935 padding_vertical: tab_padding_vertical,
1936 },
1937 sidebar: crate::model::widgets::ResolvedSidebarTheme {
1938 background: sidebar_background,
1939 foreground: sidebar_foreground,
1940 },
1941 toolbar: crate::model::widgets::ResolvedToolbarTheme {
1942 height: toolbar_height,
1943 item_spacing: toolbar_item_spacing,
1944 padding: toolbar_padding,
1945 font: toolbar_font,
1946 },
1947 status_bar: crate::model::widgets::ResolvedStatusBarTheme {
1948 font: status_bar_font,
1949 },
1950 list: crate::model::widgets::ResolvedListTheme {
1951 background: list_background,
1952 foreground: list_foreground,
1953 alternate_row: list_alternate_row,
1954 selection: list_selection,
1955 selection_foreground: list_selection_foreground,
1956 header_background: list_header_background,
1957 header_foreground: list_header_foreground,
1958 grid_color: list_grid_color,
1959 item_height: list_item_height,
1960 padding_horizontal: list_padding_horizontal,
1961 padding_vertical: list_padding_vertical,
1962 },
1963 popover: crate::model::widgets::ResolvedPopoverTheme {
1964 background: popover_background,
1965 foreground: popover_foreground,
1966 border: popover_border,
1967 radius: popover_radius,
1968 },
1969 splitter: crate::model::widgets::ResolvedSplitterTheme {
1970 width: splitter_width,
1971 },
1972 separator: crate::model::widgets::ResolvedSeparatorTheme {
1973 color: separator_color,
1974 },
1975 switch: crate::model::widgets::ResolvedSwitchTheme {
1976 checked_background: switch_checked_background,
1977 unchecked_background: switch_unchecked_background,
1978 thumb_background: switch_thumb_background,
1979 track_width: switch_track_width,
1980 track_height: switch_track_height,
1981 thumb_size: switch_thumb_size,
1982 track_radius: switch_track_radius,
1983 },
1984 dialog: crate::model::widgets::ResolvedDialogTheme {
1985 min_width: dialog_min_width,
1986 max_width: dialog_max_width,
1987 min_height: dialog_min_height,
1988 max_height: dialog_max_height,
1989 content_padding: dialog_content_padding,
1990 button_spacing: dialog_button_spacing,
1991 radius: dialog_radius,
1992 icon_size: dialog_icon_size,
1993 button_order: dialog_button_order,
1994 title_font: dialog_title_font,
1995 },
1996 spinner: crate::model::widgets::ResolvedSpinnerTheme {
1997 fill: spinner_fill,
1998 diameter: spinner_diameter,
1999 min_size: spinner_min_size,
2000 stroke_width: spinner_stroke_width,
2001 },
2002 combo_box: crate::model::widgets::ResolvedComboBoxTheme {
2003 min_height: combo_box_min_height,
2004 min_width: combo_box_min_width,
2005 padding_horizontal: combo_box_padding_horizontal,
2006 arrow_size: combo_box_arrow_size,
2007 arrow_area_width: combo_box_arrow_area_width,
2008 radius: combo_box_radius,
2009 },
2010 segmented_control: crate::model::widgets::ResolvedSegmentedControlTheme {
2011 segment_height: segmented_control_segment_height,
2012 separator_width: segmented_control_separator_width,
2013 padding_horizontal: segmented_control_padding_horizontal,
2014 radius: segmented_control_radius,
2015 },
2016 card: crate::model::widgets::ResolvedCardTheme {
2017 background: card_background,
2018 border: card_border,
2019 radius: card_radius,
2020 padding: card_padding,
2021 shadow: card_shadow,
2022 },
2023 expander: crate::model::widgets::ResolvedExpanderTheme {
2024 header_height: expander_header_height,
2025 arrow_size: expander_arrow_size,
2026 content_padding: expander_content_padding,
2027 radius: expander_radius,
2028 },
2029 link: crate::model::widgets::ResolvedLinkTheme {
2030 color: link_color,
2031 visited: link_visited,
2032 background: link_background,
2033 hover_bg: link_hover_bg,
2034 underline: link_underline,
2035 },
2036 icon_set,
2037 icon_theme,
2038 })
2039 }
2040}
2041
2042#[cfg(test)]
2043#[allow(clippy::unwrap_used, clippy::expect_used)]
2044mod tests {
2045 use super::*;
2046 use crate::Rgba;
2047 use crate::model::{DialogButtonOrder, FontSpec};
2048
2049 fn variant_with_defaults() -> ThemeVariant {
2051 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();
2070 v.defaults.accent = Some(c1);
2071 v.defaults.background = Some(c2);
2072 v.defaults.foreground = Some(c3);
2073 v.defaults.surface = Some(c4);
2074 v.defaults.border = Some(c5);
2075 v.defaults.muted = Some(c6);
2076 v.defaults.shadow = Some(c7);
2077 v.defaults.link = Some(c8);
2078 v.defaults.accent_foreground = Some(c9);
2079 v.defaults.selection_foreground = Some(Rgba::rgb(255, 255, 255));
2080 v.defaults.disabled_foreground = Some(Rgba::rgb(160, 160, 160));
2081 v.defaults.danger = Some(c10);
2082 v.defaults.danger_foreground = Some(c11);
2083 v.defaults.warning = Some(c12);
2084 v.defaults.warning_foreground = Some(c13);
2085 v.defaults.success = Some(c14);
2086 v.defaults.success_foreground = Some(c15);
2087 v.defaults.info = Some(c16);
2088 v.defaults.info_foreground = Some(c17);
2089
2090 v.defaults.radius = Some(4.0);
2091 v.defaults.radius_lg = Some(8.0);
2092 v.defaults.frame_width = Some(1.0);
2093 v.defaults.disabled_opacity = Some(0.5);
2094 v.defaults.border_opacity = Some(0.15);
2095 v.defaults.shadow_enabled = Some(true);
2096
2097 v.defaults.focus_ring_width = Some(2.0);
2098 v.defaults.focus_ring_offset = Some(1.0);
2099
2100 v.defaults.font = FontSpec {
2101 family: Some("Inter".into()),
2102 size: Some(14.0),
2103 weight: Some(400),
2104 };
2105 v.defaults.line_height = Some(1.4);
2106 v.defaults.mono_font = FontSpec {
2107 family: Some("JetBrains Mono".into()),
2108 size: Some(13.0),
2109 weight: Some(400),
2110 };
2111
2112 v.defaults.spacing.xxs = Some(2.0);
2113 v.defaults.spacing.xs = Some(4.0);
2114 v.defaults.spacing.s = Some(6.0);
2115 v.defaults.spacing.m = Some(12.0);
2116 v.defaults.spacing.l = Some(18.0);
2117 v.defaults.spacing.xl = Some(24.0);
2118 v.defaults.spacing.xxl = Some(36.0);
2119
2120 v.defaults.icon_sizes.toolbar = Some(24.0);
2121 v.defaults.icon_sizes.small = Some(16.0);
2122 v.defaults.icon_sizes.large = Some(32.0);
2123 v.defaults.icon_sizes.dialog = Some(22.0);
2124 v.defaults.icon_sizes.panel = Some(20.0);
2125
2126 v.defaults.text_scaling_factor = Some(1.0);
2127 v.defaults.reduce_motion = Some(false);
2128 v.defaults.high_contrast = Some(false);
2129 v.defaults.reduce_transparency = Some(false);
2130
2131 v
2132 }
2133
2134 #[test]
2137 fn resolve_phase1_accent_fills_selection_and_focus_ring() {
2138 let mut v = ThemeVariant::default();
2139 v.defaults.accent = Some(Rgba::rgb(0, 120, 215));
2140 v.resolve();
2141 assert_eq!(v.defaults.selection, Some(Rgba::rgb(0, 120, 215)));
2142 assert_eq!(v.defaults.focus_ring_color, Some(Rgba::rgb(0, 120, 215)));
2143 }
2144
2145 #[test]
2146 fn resolve_phase1_selection_fills_selection_inactive() {
2147 let mut v = ThemeVariant::default();
2148 v.defaults.accent = Some(Rgba::rgb(0, 120, 215));
2149 v.resolve();
2150 assert_eq!(v.defaults.selection_inactive, Some(Rgba::rgb(0, 120, 215)));
2152 }
2153
2154 #[test]
2155 fn resolve_phase1_explicit_selection_preserved() {
2156 let mut v = ThemeVariant::default();
2157 v.defaults.accent = Some(Rgba::rgb(0, 120, 215));
2158 v.defaults.selection = Some(Rgba::rgb(100, 100, 100));
2159 v.resolve();
2160 assert_eq!(v.defaults.selection, Some(Rgba::rgb(100, 100, 100)));
2162 assert_eq!(
2164 v.defaults.selection_inactive,
2165 Some(Rgba::rgb(100, 100, 100))
2166 );
2167 }
2168
2169 #[test]
2172 fn resolve_phase2_safety_nets() {
2173 let mut v = ThemeVariant::default();
2174 v.defaults.foreground = Some(Rgba::rgb(30, 30, 30));
2175 v.defaults.background = Some(Rgba::rgb(255, 255, 255));
2176 v.resolve();
2177
2178 assert_eq!(
2179 v.input.caret,
2180 Some(Rgba::rgb(30, 30, 30)),
2181 "input.caret <- foreground"
2182 );
2183 assert_eq!(
2184 v.scrollbar.track,
2185 Some(Rgba::rgb(255, 255, 255)),
2186 "scrollbar.track <- background"
2187 );
2188 assert_eq!(
2189 v.spinner.fill,
2190 Some(Rgba::rgb(30, 30, 30)),
2191 "spinner.fill <- foreground"
2192 );
2193 assert_eq!(
2194 v.popover.background,
2195 Some(Rgba::rgb(255, 255, 255)),
2196 "popover.background <- background"
2197 );
2198 assert_eq!(
2199 v.list.background,
2200 Some(Rgba::rgb(255, 255, 255)),
2201 "list.background <- background"
2202 );
2203 }
2204
2205 #[test]
2208 fn resolve_phase3_accent_propagation() {
2209 let mut v = ThemeVariant::default();
2210 v.defaults.accent = Some(Rgba::rgb(0, 120, 215));
2211 v.resolve();
2212
2213 assert_eq!(
2214 v.button.primary_background,
2215 Some(Rgba::rgb(0, 120, 215)),
2216 "button.primary_background <- accent"
2217 );
2218 assert_eq!(
2219 v.checkbox.checked_background,
2220 Some(Rgba::rgb(0, 120, 215)),
2221 "checkbox.checked_background <- accent"
2222 );
2223 assert_eq!(
2224 v.slider.fill,
2225 Some(Rgba::rgb(0, 120, 215)),
2226 "slider.fill <- accent"
2227 );
2228 assert_eq!(
2229 v.progress_bar.fill,
2230 Some(Rgba::rgb(0, 120, 215)),
2231 "progress_bar.fill <- accent"
2232 );
2233 assert_eq!(
2234 v.switch.checked_background,
2235 Some(Rgba::rgb(0, 120, 215)),
2236 "switch.checked_background <- accent"
2237 );
2238 }
2239
2240 #[test]
2243 fn resolve_phase3_font_subfield_inheritance() {
2244 let mut v = ThemeVariant::default();
2245 v.defaults.font = FontSpec {
2246 family: Some("Inter".into()),
2247 size: Some(14.0),
2248 weight: Some(400),
2249 };
2250 v.menu.font = Some(FontSpec {
2252 family: None,
2253 size: Some(12.0),
2254 weight: None,
2255 });
2256 v.resolve();
2257
2258 let menu_font = v.menu.font.as_ref().unwrap();
2259 assert_eq!(
2260 menu_font.family.as_deref(),
2261 Some("Inter"),
2262 "family from defaults"
2263 );
2264 assert_eq!(menu_font.size, Some(12.0), "explicit size preserved");
2265 assert_eq!(menu_font.weight, Some(400), "weight from defaults");
2266 }
2267
2268 #[test]
2269 fn resolve_phase3_font_entire_inheritance() {
2270 let mut v = ThemeVariant::default();
2271 v.defaults.font = FontSpec {
2272 family: Some("Inter".into()),
2273 size: Some(14.0),
2274 weight: Some(400),
2275 };
2276 assert!(v.button.font.is_none());
2278 v.resolve();
2279
2280 let button_font = v.button.font.as_ref().unwrap();
2281 assert_eq!(button_font.family.as_deref(), Some("Inter"));
2282 assert_eq!(button_font.size, Some(14.0));
2283 assert_eq!(button_font.weight, Some(400));
2284 }
2285
2286 #[test]
2289 fn resolve_phase3_text_scale_inheritance() {
2290 let mut v = ThemeVariant::default();
2291 v.defaults.font = FontSpec {
2292 family: Some("Inter".into()),
2293 size: Some(14.0),
2294 weight: Some(400),
2295 };
2296 v.defaults.line_height = Some(1.4);
2297 v.resolve();
2299
2300 let caption = v.text_scale.caption.as_ref().unwrap();
2301 assert_eq!(caption.size, Some(14.0), "size from defaults.font.size");
2302 assert_eq!(
2303 caption.weight,
2304 Some(400),
2305 "weight from defaults.font.weight"
2306 );
2307 assert!(
2309 (caption.line_height.unwrap() - 19.6).abs() < 0.001,
2310 "line_height computed"
2311 );
2312 }
2313
2314 #[test]
2317 fn resolve_phase3_color_inheritance() {
2318 let mut v = variant_with_defaults();
2319 v.resolve();
2320
2321 assert_eq!(v.window.background, Some(Rgba::rgb(255, 255, 255)));
2323 assert_eq!(v.window.border, v.defaults.border);
2324 assert_eq!(v.button.border, v.defaults.border);
2326 assert_eq!(v.tooltip.radius, v.defaults.radius);
2328 }
2329
2330 #[test]
2333 fn resolve_phase4_inactive_title_bar_from_active() {
2334 let mut v = ThemeVariant::default();
2335 v.defaults.surface = Some(Rgba::rgb(240, 240, 240));
2336 v.defaults.foreground = Some(Rgba::rgb(30, 30, 30));
2337 v.resolve();
2338
2339 assert_eq!(
2342 v.window.inactive_title_bar_background,
2343 v.window.title_bar_background
2344 );
2345 assert_eq!(
2346 v.window.inactive_title_bar_foreground,
2347 v.window.title_bar_foreground
2348 );
2349 }
2350
2351 #[test]
2354 fn resolve_does_not_overwrite_existing_some_values() {
2355 let mut v = variant_with_defaults();
2356 let explicit = Rgba::rgb(255, 0, 0);
2357 v.window.background = Some(explicit);
2358 v.button.primary_background = Some(explicit);
2359 v.resolve();
2360
2361 assert_eq!(
2362 v.window.background,
2363 Some(explicit),
2364 "window.background preserved"
2365 );
2366 assert_eq!(
2367 v.button.primary_background,
2368 Some(explicit),
2369 "button.primary_background preserved"
2370 );
2371 }
2372
2373 #[test]
2376 fn resolve_is_idempotent() {
2377 let mut v = variant_with_defaults();
2378 v.resolve();
2379 let after_first = v.clone();
2380 v.resolve();
2381 assert_eq!(v, after_first, "second resolve() produces same result");
2382 }
2383
2384 #[test]
2387 fn resolve_all_font_carrying_widgets_get_resolved_fonts() {
2388 let mut v = ThemeVariant::default();
2389 v.defaults.font = FontSpec {
2390 family: Some("Inter".into()),
2391 size: Some(14.0),
2392 weight: Some(400),
2393 };
2394 v.resolve();
2395
2396 assert!(v.window.title_bar_font.is_some(), "window.title_bar_font");
2398 assert!(v.button.font.is_some(), "button.font");
2399 assert!(v.input.font.is_some(), "input.font");
2400 assert!(v.menu.font.is_some(), "menu.font");
2401 assert!(v.tooltip.font.is_some(), "tooltip.font");
2402 assert!(v.toolbar.font.is_some(), "toolbar.font");
2403 assert!(v.status_bar.font.is_some(), "status_bar.font");
2404 assert!(v.dialog.title_font.is_some(), "dialog.title_font");
2405
2406 for (name, font) in [
2408 ("window.title_bar_font", &v.window.title_bar_font),
2409 ("button.font", &v.button.font),
2410 ("input.font", &v.input.font),
2411 ("menu.font", &v.menu.font),
2412 ("tooltip.font", &v.tooltip.font),
2413 ("toolbar.font", &v.toolbar.font),
2414 ("status_bar.font", &v.status_bar.font),
2415 ("dialog.title_font", &v.dialog.title_font),
2416 ] {
2417 let f = font.as_ref().unwrap();
2418 assert_eq!(f.family.as_deref(), Some("Inter"), "{name} family");
2419 assert_eq!(f.size, Some(14.0), "{name} size");
2420 assert_eq!(f.weight, Some(400), "{name} weight");
2421 }
2422 }
2423
2424 fn fully_populated_variant() -> ThemeVariant {
2428 let mut v = variant_with_defaults();
2429 let c = Rgba::rgb(128, 128, 128);
2430
2431 v.defaults.selection = Some(Rgba::rgb(0, 120, 215));
2433 v.defaults.selection_foreground = Some(Rgba::rgb(255, 255, 255));
2434 v.defaults.selection_inactive = Some(Rgba::rgb(0, 120, 215));
2435 v.defaults.focus_ring_color = Some(Rgba::rgb(0, 120, 215));
2436
2437 v.icon_set = Some(crate::IconSet::Freedesktop);
2439 v.icon_theme = Some("breeze".into());
2440
2441 v.window.background = Some(c);
2443 v.window.foreground = Some(c);
2444 v.window.border = Some(c);
2445 v.window.title_bar_background = Some(c);
2446 v.window.title_bar_foreground = Some(c);
2447 v.window.inactive_title_bar_background = Some(c);
2448 v.window.inactive_title_bar_foreground = Some(c);
2449 v.window.radius = Some(8.0);
2450 v.window.shadow = Some(true);
2451 v.window.title_bar_font = Some(FontSpec {
2452 family: Some("Inter".into()),
2453 size: Some(14.0),
2454 weight: Some(400),
2455 });
2456
2457 v.button.background = Some(c);
2459 v.button.foreground = Some(c);
2460 v.button.border = Some(c);
2461 v.button.primary_background = Some(c);
2462 v.button.primary_foreground = Some(c);
2463 v.button.min_width = Some(64.0);
2464 v.button.min_height = Some(28.0);
2465 v.button.padding_horizontal = Some(12.0);
2466 v.button.padding_vertical = Some(6.0);
2467 v.button.radius = Some(4.0);
2468 v.button.icon_spacing = Some(6.0);
2469 v.button.disabled_opacity = Some(0.5);
2470 v.button.shadow = Some(false);
2471 v.button.font = Some(FontSpec {
2472 family: Some("Inter".into()),
2473 size: Some(14.0),
2474 weight: Some(400),
2475 });
2476
2477 v.input.background = Some(c);
2479 v.input.foreground = Some(c);
2480 v.input.border = Some(c);
2481 v.input.placeholder = Some(c);
2482 v.input.caret = Some(c);
2483 v.input.selection = Some(c);
2484 v.input.selection_foreground = Some(c);
2485 v.input.min_height = Some(28.0);
2486 v.input.padding_horizontal = Some(8.0);
2487 v.input.padding_vertical = Some(4.0);
2488 v.input.radius = Some(4.0);
2489 v.input.border_width = Some(1.0);
2490 v.input.font = Some(FontSpec {
2491 family: Some("Inter".into()),
2492 size: Some(14.0),
2493 weight: Some(400),
2494 });
2495
2496 v.checkbox.checked_background = Some(c);
2498 v.checkbox.indicator_size = Some(18.0);
2499 v.checkbox.spacing = Some(6.0);
2500 v.checkbox.radius = Some(2.0);
2501 v.checkbox.border_width = Some(1.0);
2502
2503 v.menu.background = Some(c);
2505 v.menu.foreground = Some(c);
2506 v.menu.separator = Some(c);
2507 v.menu.item_height = Some(28.0);
2508 v.menu.padding_horizontal = Some(8.0);
2509 v.menu.padding_vertical = Some(4.0);
2510 v.menu.icon_spacing = Some(6.0);
2511 v.menu.font = Some(FontSpec {
2512 family: Some("Inter".into()),
2513 size: Some(14.0),
2514 weight: Some(400),
2515 });
2516
2517 v.tooltip.background = Some(c);
2519 v.tooltip.foreground = Some(c);
2520 v.tooltip.padding_horizontal = Some(6.0);
2521 v.tooltip.padding_vertical = Some(4.0);
2522 v.tooltip.max_width = Some(300.0);
2523 v.tooltip.radius = Some(4.0);
2524 v.tooltip.font = Some(FontSpec {
2525 family: Some("Inter".into()),
2526 size: Some(14.0),
2527 weight: Some(400),
2528 });
2529
2530 v.scrollbar.track = Some(c);
2532 v.scrollbar.thumb = Some(c);
2533 v.scrollbar.thumb_hover = Some(c);
2534 v.scrollbar.width = Some(14.0);
2535 v.scrollbar.min_thumb_height = Some(20.0);
2536 v.scrollbar.slider_width = Some(8.0);
2537 v.scrollbar.overlay_mode = Some(false);
2538
2539 v.slider.fill = Some(c);
2541 v.slider.track = Some(c);
2542 v.slider.thumb = Some(c);
2543 v.slider.track_height = Some(4.0);
2544 v.slider.thumb_size = Some(16.0);
2545 v.slider.tick_length = Some(6.0);
2546
2547 v.progress_bar.fill = Some(c);
2549 v.progress_bar.track = Some(c);
2550 v.progress_bar.height = Some(6.0);
2551 v.progress_bar.min_width = Some(100.0);
2552 v.progress_bar.radius = Some(3.0);
2553
2554 v.tab.background = Some(c);
2556 v.tab.foreground = Some(c);
2557 v.tab.active_background = Some(c);
2558 v.tab.active_foreground = Some(c);
2559 v.tab.bar_background = Some(c);
2560 v.tab.min_width = Some(60.0);
2561 v.tab.min_height = Some(32.0);
2562 v.tab.padding_horizontal = Some(12.0);
2563 v.tab.padding_vertical = Some(6.0);
2564
2565 v.sidebar.background = Some(c);
2567 v.sidebar.foreground = Some(c);
2568
2569 v.toolbar.height = Some(40.0);
2571 v.toolbar.item_spacing = Some(4.0);
2572 v.toolbar.padding = Some(4.0);
2573 v.toolbar.font = Some(FontSpec {
2574 family: Some("Inter".into()),
2575 size: Some(14.0),
2576 weight: Some(400),
2577 });
2578
2579 v.status_bar.font = Some(FontSpec {
2581 family: Some("Inter".into()),
2582 size: Some(14.0),
2583 weight: Some(400),
2584 });
2585
2586 v.list.background = Some(c);
2588 v.list.foreground = Some(c);
2589 v.list.alternate_row = Some(c);
2590 v.list.selection = Some(c);
2591 v.list.selection_foreground = Some(c);
2592 v.list.header_background = Some(c);
2593 v.list.header_foreground = Some(c);
2594 v.list.grid_color = Some(c);
2595 v.list.item_height = Some(28.0);
2596 v.list.padding_horizontal = Some(8.0);
2597 v.list.padding_vertical = Some(4.0);
2598
2599 v.popover.background = Some(c);
2601 v.popover.foreground = Some(c);
2602 v.popover.border = Some(c);
2603 v.popover.radius = Some(6.0);
2604
2605 v.splitter.width = Some(4.0);
2607
2608 v.separator.color = Some(c);
2610
2611 v.switch.checked_background = Some(c);
2613 v.switch.unchecked_background = Some(c);
2614 v.switch.thumb_background = Some(c);
2615 v.switch.track_width = Some(40.0);
2616 v.switch.track_height = Some(20.0);
2617 v.switch.thumb_size = Some(14.0);
2618 v.switch.track_radius = Some(10.0);
2619
2620 v.dialog.min_width = Some(320.0);
2622 v.dialog.max_width = Some(600.0);
2623 v.dialog.min_height = Some(200.0);
2624 v.dialog.max_height = Some(800.0);
2625 v.dialog.content_padding = Some(16.0);
2626 v.dialog.button_spacing = Some(8.0);
2627 v.dialog.radius = Some(8.0);
2628 v.dialog.icon_size = Some(22.0);
2629 v.dialog.button_order = Some(DialogButtonOrder::TrailingAffirmative);
2630 v.dialog.title_font = Some(FontSpec {
2631 family: Some("Inter".into()),
2632 size: Some(16.0),
2633 weight: Some(700),
2634 });
2635
2636 v.spinner.fill = Some(c);
2638 v.spinner.diameter = Some(24.0);
2639 v.spinner.min_size = Some(16.0);
2640 v.spinner.stroke_width = Some(2.0);
2641
2642 v.combo_box.min_height = Some(28.0);
2644 v.combo_box.min_width = Some(80.0);
2645 v.combo_box.padding_horizontal = Some(8.0);
2646 v.combo_box.arrow_size = Some(12.0);
2647 v.combo_box.arrow_area_width = Some(20.0);
2648 v.combo_box.radius = Some(4.0);
2649
2650 v.segmented_control.segment_height = Some(28.0);
2652 v.segmented_control.separator_width = Some(1.0);
2653 v.segmented_control.padding_horizontal = Some(12.0);
2654 v.segmented_control.radius = Some(4.0);
2655
2656 v.card.background = Some(c);
2658 v.card.border = Some(c);
2659 v.card.radius = Some(8.0);
2660 v.card.padding = Some(12.0);
2661 v.card.shadow = Some(true);
2662
2663 v.expander.header_height = Some(32.0);
2665 v.expander.arrow_size = Some(12.0);
2666 v.expander.content_padding = Some(8.0);
2667 v.expander.radius = Some(4.0);
2668
2669 v.link.color = Some(c);
2671 v.link.visited = Some(c);
2672 v.link.background = Some(c);
2673 v.link.hover_bg = Some(c);
2674 v.link.underline = Some(true);
2675
2676 v.text_scale.caption = Some(crate::model::TextScaleEntry {
2678 size: Some(11.0),
2679 weight: Some(400),
2680 line_height: Some(15.4),
2681 });
2682 v.text_scale.section_heading = Some(crate::model::TextScaleEntry {
2683 size: Some(14.0),
2684 weight: Some(600),
2685 line_height: Some(19.6),
2686 });
2687 v.text_scale.dialog_title = Some(crate::model::TextScaleEntry {
2688 size: Some(16.0),
2689 weight: Some(700),
2690 line_height: Some(22.4),
2691 });
2692 v.text_scale.display = Some(crate::model::TextScaleEntry {
2693 size: Some(24.0),
2694 weight: Some(300),
2695 line_height: Some(33.6),
2696 });
2697
2698 v
2699 }
2700
2701 #[test]
2702 fn validate_fully_populated_returns_ok() {
2703 let v = fully_populated_variant();
2704 let result = v.validate();
2705 assert!(
2706 result.is_ok(),
2707 "validate() should succeed on fully populated variant, got: {:?}",
2708 result.err()
2709 );
2710 let resolved = result.unwrap();
2711 assert_eq!(resolved.defaults.font.family, "Inter");
2712 assert_eq!(resolved.icon_set, crate::IconSet::Freedesktop);
2713 }
2714
2715 #[test]
2716 fn validate_missing_3_fields_returns_all_paths() {
2717 let mut v = fully_populated_variant();
2718 v.defaults.muted = None;
2720 v.window.radius = None;
2721 v.icon_set = None;
2722
2723 let result = v.validate();
2724 assert!(result.is_err());
2725 let err = match result.unwrap_err() {
2726 crate::Error::Resolution(e) => e,
2727 other => panic!("expected Resolution error, got: {other:?}"),
2728 };
2729 assert_eq!(
2730 err.missing_fields.len(),
2731 3,
2732 "should report exactly 3 missing fields, got: {:?}",
2733 err.missing_fields
2734 );
2735 assert!(err.missing_fields.contains(&"defaults.muted".to_string()));
2736 assert!(err.missing_fields.contains(&"window.radius".to_string()));
2737 assert!(err.missing_fields.contains(&"icon_set".to_string()));
2738 }
2739
2740 #[test]
2741 fn validate_error_message_includes_count_and_paths() {
2742 let mut v = fully_populated_variant();
2743 v.defaults.muted = None;
2744 v.button.min_height = None;
2745
2746 let result = v.validate();
2747 assert!(result.is_err());
2748 let err = match result.unwrap_err() {
2749 crate::Error::Resolution(e) => e,
2750 other => panic!("expected Resolution error, got: {other:?}"),
2751 };
2752 let msg = err.to_string();
2753 assert!(msg.contains("2 missing field(s)"), "got: {msg}");
2754 assert!(msg.contains("defaults.muted"), "got: {msg}");
2755 assert!(msg.contains("button.min_height"), "got: {msg}");
2756 }
2757
2758 #[test]
2759 fn validate_checks_all_defaults_fields() {
2760 let v = ThemeVariant::default();
2762 let result = v.validate();
2763 assert!(result.is_err());
2764 let err = match result.unwrap_err() {
2765 crate::Error::Resolution(e) => e,
2766 other => panic!("expected Resolution error, got: {other:?}"),
2767 };
2768 assert!(
2770 err.missing_fields
2771 .iter()
2772 .any(|f| f.starts_with("defaults.")),
2773 "should include defaults.* fields in missing"
2774 );
2775 assert!(
2777 err.missing_fields
2778 .contains(&"defaults.font.family".to_string())
2779 );
2780 assert!(
2781 err.missing_fields
2782 .contains(&"defaults.background".to_string())
2783 );
2784 assert!(err.missing_fields.contains(&"defaults.accent".to_string()));
2785 assert!(err.missing_fields.contains(&"defaults.radius".to_string()));
2786 assert!(
2787 err.missing_fields
2788 .contains(&"defaults.spacing.m".to_string())
2789 );
2790 assert!(
2791 err.missing_fields
2792 .contains(&"defaults.icon_sizes.toolbar".to_string())
2793 );
2794 assert!(
2795 err.missing_fields
2796 .contains(&"defaults.text_scaling_factor".to_string())
2797 );
2798 }
2799
2800 #[test]
2801 fn validate_checks_all_widget_structs() {
2802 let v = ThemeVariant::default();
2803 let result = v.validate();
2804 let err = match result.unwrap_err() {
2805 crate::Error::Resolution(e) => e,
2806 other => panic!("expected Resolution error, got: {other:?}"),
2807 };
2808 for prefix in [
2810 "window.",
2811 "button.",
2812 "input.",
2813 "checkbox.",
2814 "menu.",
2815 "tooltip.",
2816 "scrollbar.",
2817 "slider.",
2818 "progress_bar.",
2819 "tab.",
2820 "sidebar.",
2821 "toolbar.",
2822 "status_bar.",
2823 "list.",
2824 "popover.",
2825 "splitter.",
2826 "separator.",
2827 "switch.",
2828 "dialog.",
2829 "spinner.",
2830 "combo_box.",
2831 "segmented_control.",
2832 "card.",
2833 "expander.",
2834 "link.",
2835 ] {
2836 assert!(
2837 err.missing_fields.iter().any(|f| f.starts_with(prefix)),
2838 "missing fields should include {prefix}* but got: {:?}",
2839 err.missing_fields
2840 .iter()
2841 .filter(|f| f.starts_with(prefix))
2842 .collect::<Vec<_>>()
2843 );
2844 }
2845 }
2846
2847 #[test]
2848 fn validate_checks_text_scale_entries() {
2849 let v = ThemeVariant::default();
2850 let result = v.validate();
2851 let err = match result.unwrap_err() {
2852 crate::Error::Resolution(e) => e,
2853 other => panic!("expected Resolution error, got: {other:?}"),
2854 };
2855 assert!(
2856 err.missing_fields
2857 .contains(&"text_scale.caption".to_string())
2858 );
2859 assert!(
2860 err.missing_fields
2861 .contains(&"text_scale.section_heading".to_string())
2862 );
2863 assert!(
2864 err.missing_fields
2865 .contains(&"text_scale.dialog_title".to_string())
2866 );
2867 assert!(
2868 err.missing_fields
2869 .contains(&"text_scale.display".to_string())
2870 );
2871 }
2872
2873 #[test]
2874 fn validate_checks_icon_set() {
2875 let mut v = fully_populated_variant();
2876 v.icon_set = None;
2877
2878 let result = v.validate();
2879 let err = match result.unwrap_err() {
2880 crate::Error::Resolution(e) => e,
2881 other => panic!("expected Resolution error, got: {other:?}"),
2882 };
2883 assert!(err.missing_fields.contains(&"icon_set".to_string()));
2884 }
2885
2886 #[test]
2887 fn validate_after_resolve_succeeds_for_derivable_fields() {
2888 let mut v = variant_with_defaults();
2890 v.icon_set = Some(crate::IconSet::Freedesktop);
2892
2893 v.button.min_width = Some(64.0);
2896 v.button.min_height = Some(28.0);
2897 v.button.padding_horizontal = Some(12.0);
2898 v.button.padding_vertical = Some(6.0);
2899 v.button.icon_spacing = Some(6.0);
2900 v.input.min_height = Some(28.0);
2902 v.input.padding_horizontal = Some(8.0);
2903 v.input.padding_vertical = Some(4.0);
2904 v.checkbox.indicator_size = Some(18.0);
2906 v.checkbox.spacing = Some(6.0);
2907 v.menu.item_height = Some(28.0);
2909 v.menu.padding_horizontal = Some(8.0);
2910 v.menu.padding_vertical = Some(4.0);
2911 v.menu.icon_spacing = Some(6.0);
2912 v.tooltip.padding_horizontal = Some(6.0);
2914 v.tooltip.padding_vertical = Some(4.0);
2915 v.tooltip.max_width = Some(300.0);
2916 v.scrollbar.width = Some(14.0);
2918 v.scrollbar.min_thumb_height = Some(20.0);
2919 v.scrollbar.slider_width = Some(8.0);
2920 v.scrollbar.overlay_mode = Some(false);
2921 v.slider.track_height = Some(4.0);
2923 v.slider.thumb_size = Some(16.0);
2924 v.slider.tick_length = Some(6.0);
2925 v.progress_bar.height = Some(6.0);
2927 v.progress_bar.min_width = Some(100.0);
2928 v.tab.min_width = Some(60.0);
2930 v.tab.min_height = Some(32.0);
2931 v.tab.padding_horizontal = Some(12.0);
2932 v.tab.padding_vertical = Some(6.0);
2933 v.toolbar.height = Some(40.0);
2935 v.toolbar.item_spacing = Some(4.0);
2936 v.toolbar.padding = Some(4.0);
2937 v.list.item_height = Some(28.0);
2939 v.list.padding_horizontal = Some(8.0);
2940 v.list.padding_vertical = Some(4.0);
2941 v.splitter.width = Some(4.0);
2943 v.switch.unchecked_background = Some(Rgba::rgb(180, 180, 180));
2945 v.switch.track_width = Some(40.0);
2946 v.switch.track_height = Some(20.0);
2947 v.switch.thumb_size = Some(14.0);
2948 v.switch.track_radius = Some(10.0);
2949 v.dialog.min_width = Some(320.0);
2951 v.dialog.max_width = Some(600.0);
2952 v.dialog.min_height = Some(200.0);
2953 v.dialog.max_height = Some(800.0);
2954 v.dialog.content_padding = Some(16.0);
2955 v.dialog.button_spacing = Some(8.0);
2956 v.dialog.icon_size = Some(22.0);
2957 v.dialog.button_order = Some(DialogButtonOrder::TrailingAffirmative);
2958 v.spinner.diameter = Some(24.0);
2960 v.spinner.min_size = Some(16.0);
2961 v.spinner.stroke_width = Some(2.0);
2962 v.combo_box.min_height = Some(28.0);
2964 v.combo_box.min_width = Some(80.0);
2965 v.combo_box.padding_horizontal = Some(8.0);
2966 v.combo_box.arrow_size = Some(12.0);
2967 v.combo_box.arrow_area_width = Some(20.0);
2968 v.segmented_control.segment_height = Some(28.0);
2970 v.segmented_control.separator_width = Some(1.0);
2971 v.segmented_control.padding_horizontal = Some(12.0);
2972 v.card.padding = Some(12.0);
2974 v.expander.header_height = Some(32.0);
2976 v.expander.arrow_size = Some(12.0);
2977 v.expander.content_padding = Some(8.0);
2978 v.link.background = Some(Rgba::rgb(255, 255, 255));
2980 v.link.hover_bg = Some(Rgba::rgb(230, 230, 255));
2981 v.link.underline = Some(true);
2982
2983 v.resolve_all();
2984 let result = v.validate();
2985 assert!(
2986 result.is_ok(),
2987 "validate() should succeed after resolve_all() with all non-derivable fields set, got: {:?}",
2988 result.err()
2989 );
2990 }
2991
2992 #[test]
2993 fn test_gnome_resolve_validate() {
2994 let adwaita = crate::ThemeSpec::preset("adwaita").unwrap();
2998
2999 let mut variant = adwaita
3001 .dark
3002 .clone()
3003 .expect("adwaita should have dark variant");
3004
3005 variant.dialog.button_order = Some(DialogButtonOrder::TrailingAffirmative);
3007 variant.icon_set = Some(crate::IconSet::Freedesktop);
3009
3010 variant.defaults.font = FontSpec {
3012 family: Some("Cantarell".to_string()),
3013 size: Some(11.0),
3014 weight: Some(400),
3015 };
3016
3017 variant.resolve_all();
3018 let resolved = variant.validate().unwrap_or_else(|e| {
3019 panic!("GNOME resolve/validate pipeline failed: {e}");
3020 });
3021
3022 assert_eq!(
3025 resolved.defaults.accent,
3026 Rgba::rgb(53, 132, 228),
3027 "accent should be from adwaita preset"
3028 );
3029 assert_eq!(
3030 resolved.defaults.font.family, "Cantarell",
3031 "font family should be from GNOME reader overlay"
3032 );
3033 assert_eq!(
3034 resolved.dialog.button_order,
3035 DialogButtonOrder::TrailingAffirmative,
3036 "dialog button order should be trailing affirmative for GNOME"
3037 );
3038 assert_eq!(
3039 resolved.icon_set,
3040 crate::IconSet::Freedesktop,
3041 "icon_set should be from GNOME reader"
3042 );
3043 }
3044
3045 #[test]
3048 fn validate_catches_negative_radius() {
3049 let mut v = fully_populated_variant();
3050 v.defaults.radius = Some(-5.0);
3051 v.button.radius = Some(-1.0);
3052 v.window.radius = Some(-3.0);
3053
3054 let result = v.validate();
3055 assert!(result.is_err());
3056 let err = match result.unwrap_err() {
3057 crate::Error::Resolution(e) => e,
3058 other => panic!("expected Resolution error, got: {other:?}"),
3059 };
3060 assert!(
3061 err.missing_fields
3062 .iter()
3063 .any(|f| f.contains("defaults.radius") && f.contains("-5")),
3064 "should report negative defaults.radius, got: {:?}",
3065 err.missing_fields
3066 );
3067 assert!(
3068 err.missing_fields
3069 .iter()
3070 .any(|f| f.contains("button.radius") && f.contains("-1")),
3071 "should report negative button.radius, got: {:?}",
3072 err.missing_fields
3073 );
3074 assert!(
3075 err.missing_fields
3076 .iter()
3077 .any(|f| f.contains("window.radius") && f.contains("-3")),
3078 "should report negative window.radius, got: {:?}",
3079 err.missing_fields
3080 );
3081 }
3082
3083 #[test]
3084 fn validate_catches_zero_font_size() {
3085 let mut v = fully_populated_variant();
3086 v.defaults.font.size = Some(0.0);
3087
3088 let result = v.validate();
3089 assert!(result.is_err());
3090 let err = match result.unwrap_err() {
3091 crate::Error::Resolution(e) => e,
3092 other => panic!("expected Resolution error, got: {other:?}"),
3093 };
3094 assert!(
3095 err.missing_fields
3096 .iter()
3097 .any(|f| f.contains("defaults.font.size") && f.contains("> 0")),
3098 "should report zero defaults.font.size, got: {:?}",
3099 err.missing_fields
3100 );
3101 }
3102
3103 #[test]
3104 fn validate_catches_opacity_out_of_range() {
3105 let mut v = fully_populated_variant();
3106 v.defaults.disabled_opacity = Some(1.5);
3107 v.defaults.border_opacity = Some(-0.1);
3108 v.button.disabled_opacity = Some(3.0);
3109
3110 let result = v.validate();
3111 assert!(result.is_err());
3112 let err = match result.unwrap_err() {
3113 crate::Error::Resolution(e) => e,
3114 other => panic!("expected Resolution error, got: {other:?}"),
3115 };
3116 assert!(
3117 err.missing_fields
3118 .iter()
3119 .any(|f| f.contains("defaults.disabled_opacity")),
3120 "should report out-of-range disabled_opacity, got: {:?}",
3121 err.missing_fields
3122 );
3123 assert!(
3124 err.missing_fields
3125 .iter()
3126 .any(|f| f.contains("defaults.border_opacity")),
3127 "should report out-of-range border_opacity, got: {:?}",
3128 err.missing_fields
3129 );
3130 assert!(
3131 err.missing_fields
3132 .iter()
3133 .any(|f| f.contains("button.disabled_opacity")),
3134 "should report out-of-range button.disabled_opacity, got: {:?}",
3135 err.missing_fields
3136 );
3137 }
3138
3139 #[test]
3140 fn validate_catches_invalid_font_weight() {
3141 let mut v = fully_populated_variant();
3142 v.defaults.font.weight = Some(50); v.defaults.mono_font.weight = Some(1000); let result = v.validate();
3146 assert!(result.is_err());
3147 let err = match result.unwrap_err() {
3148 crate::Error::Resolution(e) => e,
3149 other => panic!("expected Resolution error, got: {other:?}"),
3150 };
3151 assert!(
3152 err.missing_fields
3153 .iter()
3154 .any(|f| f.contains("defaults.font.weight") && f.contains("50")),
3155 "should report out-of-range font weight 50, got: {:?}",
3156 err.missing_fields
3157 );
3158 assert!(
3159 err.missing_fields
3160 .iter()
3161 .any(|f| f.contains("defaults.mono_font.weight") && f.contains("1000")),
3162 "should report out-of-range mono_font weight 1000, got: {:?}",
3163 err.missing_fields
3164 );
3165 }
3166
3167 #[test]
3168 fn validate_reports_multiple_range_errors_together() {
3169 let mut v = fully_populated_variant();
3170 v.defaults.radius = Some(-1.0);
3171 v.defaults.disabled_opacity = Some(2.0);
3172 v.defaults.font.size = Some(0.0);
3173 v.defaults.font.weight = Some(50);
3174
3175 let result = v.validate();
3176 assert!(result.is_err());
3177 let err = match result.unwrap_err() {
3178 crate::Error::Resolution(e) => e,
3179 other => panic!("expected Resolution error, got: {other:?}"),
3180 };
3181 assert!(
3183 err.missing_fields.len() >= 4,
3184 "should report at least 4 range errors, got {}: {:?}",
3185 err.missing_fields.len(),
3186 err.missing_fields
3187 );
3188 }
3189
3190 #[test]
3191 fn validate_allows_zero_radius_and_frame_width() {
3192 let mut v = fully_populated_variant();
3194 v.defaults.radius = Some(0.0);
3195 v.defaults.radius_lg = Some(0.0);
3196 v.defaults.frame_width = Some(0.0);
3197 v.button.radius = Some(0.0);
3198 v.defaults.disabled_opacity = Some(0.0);
3199 v.defaults.border_opacity = Some(0.0);
3200
3201 let result = v.validate();
3202 assert!(
3203 result.is_ok(),
3204 "zero values should be valid for radius/frame_width/opacity, got: {:?}",
3205 result.err()
3206 );
3207 }
3208
3209 #[test]
3221 fn resolve_completeness_minimal_variant() {
3222 let mut v = variant_with_defaults();
3224
3225 v.icon_set = Some(crate::IconSet::Freedesktop);
3227
3228 v.button.min_width = Some(64.0);
3233 v.button.min_height = Some(28.0);
3234 v.button.padding_horizontal = Some(12.0);
3235 v.button.padding_vertical = Some(6.0);
3236 v.button.icon_spacing = Some(6.0);
3237
3238 v.input.min_height = Some(28.0);
3240 v.input.padding_horizontal = Some(8.0);
3241 v.input.padding_vertical = Some(4.0);
3242
3243 v.checkbox.indicator_size = Some(18.0);
3245 v.checkbox.spacing = Some(6.0);
3246
3247 v.menu.item_height = Some(28.0);
3249 v.menu.padding_horizontal = Some(8.0);
3250 v.menu.padding_vertical = Some(4.0);
3251 v.menu.icon_spacing = Some(6.0);
3252
3253 v.tooltip.padding_horizontal = Some(6.0);
3255 v.tooltip.padding_vertical = Some(4.0);
3256 v.tooltip.max_width = Some(300.0);
3257
3258 v.scrollbar.width = Some(14.0);
3260 v.scrollbar.min_thumb_height = Some(20.0);
3261 v.scrollbar.slider_width = Some(8.0);
3262 v.scrollbar.overlay_mode = Some(false);
3263
3264 v.slider.track_height = Some(4.0);
3266 v.slider.thumb_size = Some(16.0);
3267 v.slider.tick_length = Some(6.0);
3268
3269 v.progress_bar.height = Some(6.0);
3271 v.progress_bar.min_width = Some(100.0);
3272
3273 v.tab.min_width = Some(60.0);
3275 v.tab.min_height = Some(32.0);
3276 v.tab.padding_horizontal = Some(12.0);
3277 v.tab.padding_vertical = Some(6.0);
3278
3279 v.toolbar.height = Some(40.0);
3281 v.toolbar.item_spacing = Some(4.0);
3282 v.toolbar.padding = Some(4.0);
3283
3284 v.list.item_height = Some(28.0);
3286 v.list.padding_horizontal = Some(8.0);
3287 v.list.padding_vertical = Some(4.0);
3288
3289 v.splitter.width = Some(4.0);
3291
3292 v.switch.track_width = Some(40.0);
3294 v.switch.track_height = Some(20.0);
3295 v.switch.thumb_size = Some(14.0);
3296 v.switch.track_radius = Some(10.0);
3297
3298 v.dialog.min_width = Some(320.0);
3300 v.dialog.max_width = Some(600.0);
3301 v.dialog.min_height = Some(200.0);
3302 v.dialog.max_height = Some(800.0);
3303 v.dialog.content_padding = Some(16.0);
3304 v.dialog.button_spacing = Some(8.0);
3305 v.dialog.icon_size = Some(22.0);
3306 v.dialog.button_order = Some(DialogButtonOrder::TrailingAffirmative);
3307
3308 v.spinner.diameter = Some(24.0);
3310 v.spinner.min_size = Some(16.0);
3311 v.spinner.stroke_width = Some(2.0);
3312
3313 v.combo_box.min_height = Some(28.0);
3315 v.combo_box.min_width = Some(80.0);
3316 v.combo_box.padding_horizontal = Some(8.0);
3317 v.combo_box.arrow_size = Some(12.0);
3318 v.combo_box.arrow_area_width = Some(20.0);
3319
3320 v.segmented_control.segment_height = Some(28.0);
3322 v.segmented_control.separator_width = Some(1.0);
3323 v.segmented_control.padding_horizontal = Some(12.0);
3324
3325 v.card.padding = Some(12.0);
3327
3328 v.expander.header_height = Some(32.0);
3330 v.expander.arrow_size = Some(12.0);
3331 v.expander.content_padding = Some(8.0);
3332
3333 v.link.background = Some(Rgba::rgb(255, 255, 255));
3335 v.link.hover_bg = Some(Rgba::rgb(230, 230, 255));
3336 v.link.underline = Some(true);
3337
3338 assert!(
3341 v.window.background.is_none(),
3342 "window.background should be None before resolve"
3343 );
3344 assert!(
3345 v.button.background.is_none(),
3346 "button.background should be None before resolve"
3347 );
3348 assert!(
3349 v.button.font.is_none(),
3350 "button.font should be None before resolve"
3351 );
3352 assert!(
3353 v.text_scale.caption.is_none(),
3354 "text_scale.caption should be None before resolve"
3355 );
3356
3357 v.resolve_all();
3359 let result = v.validate();
3360 assert!(
3361 result.is_ok(),
3362 "Resolve completeness failed -- some derived fields lack resolve rules: {:?}",
3363 result.err()
3364 );
3365 }
3366
3367 #[test]
3374 fn resolve_completeness_from_preset() {
3375 let spec = crate::ThemeSpec::preset("material").unwrap();
3376 let mut v = spec.dark.expect("material should have dark variant");
3377
3378 v.window.background = None;
3381 v.window.foreground = None;
3382 v.window.border = None;
3383 v.window.title_bar_background = None;
3384 v.window.title_bar_foreground = None;
3385 v.window.inactive_title_bar_background = None;
3386 v.window.inactive_title_bar_foreground = None;
3387 v.window.radius = None;
3388 v.window.shadow = None;
3389
3390 v.button.background = None;
3392 v.button.foreground = None;
3393 v.button.border = None;
3394 v.button.primary_background = None;
3395 v.button.primary_foreground = None;
3396 v.button.radius = None;
3397 v.button.disabled_opacity = None;
3398 v.button.shadow = None;
3399
3400 v.input.background = None;
3402 v.input.foreground = None;
3403 v.input.border = None;
3404 v.input.placeholder = None;
3405 v.input.caret = None;
3406 v.input.selection = None;
3407 v.input.selection_foreground = None;
3408 v.input.radius = None;
3409 v.input.border_width = None;
3410
3411 v.checkbox.checked_background = None;
3413 v.checkbox.radius = None;
3414 v.checkbox.border_width = None;
3415
3416 v.menu.background = None;
3418 v.menu.foreground = None;
3419 v.menu.separator = None;
3420
3421 v.tooltip.background = None;
3423 v.tooltip.foreground = None;
3424 v.tooltip.radius = None;
3425
3426 v.scrollbar.track = None;
3428 v.scrollbar.thumb = None;
3429 v.scrollbar.thumb_hover = None;
3430
3431 v.slider.fill = None;
3433 v.slider.track = None;
3434 v.slider.thumb = None;
3435
3436 v.progress_bar.fill = None;
3438 v.progress_bar.track = None;
3439 v.progress_bar.radius = None;
3440
3441 v.tab.background = None;
3443 v.tab.foreground = None;
3444 v.tab.active_background = None;
3445 v.tab.active_foreground = None;
3446 v.tab.bar_background = None;
3447
3448 v.sidebar.background = None;
3450 v.sidebar.foreground = None;
3451
3452 v.list.background = None;
3454 v.list.foreground = None;
3455 v.list.alternate_row = None;
3456 v.list.selection = None;
3457 v.list.selection_foreground = None;
3458 v.list.header_background = None;
3459 v.list.header_foreground = None;
3460 v.list.grid_color = None;
3461
3462 v.popover.background = None;
3464 v.popover.foreground = None;
3465 v.popover.border = None;
3466 v.popover.radius = None;
3467
3468 v.separator.color = None;
3470
3471 v.switch.checked_background = None;
3473 v.switch.unchecked_background = None;
3474 v.switch.thumb_background = None;
3475
3476 v.dialog.radius = None;
3478
3479 v.combo_box.radius = None;
3481
3482 v.segmented_control.radius = None;
3484
3485 v.card.background = None;
3487 v.card.border = None;
3488 v.card.radius = None;
3489 v.card.shadow = None;
3490
3491 v.expander.radius = None;
3493
3494 v.link.color = None;
3496 v.link.visited = None;
3497
3498 v.spinner.fill = None;
3500
3501 v.window.title_bar_font = None;
3503 v.button.font = None;
3504 v.input.font = None;
3505 v.menu.font = None;
3506 v.tooltip.font = None;
3507 v.toolbar.font = None;
3508 v.status_bar.font = None;
3509 v.dialog.title_font = None;
3510
3511 v.text_scale.caption = None;
3513 v.text_scale.section_heading = None;
3514 v.text_scale.dialog_title = None;
3515 v.text_scale.display = None;
3516
3517 v.defaults.selection = None;
3519 v.defaults.focus_ring_color = None;
3520 v.defaults.selection_inactive = None;
3521
3522 v.resolve_all();
3524 let result = v.validate();
3525 assert!(
3526 result.is_ok(),
3527 "Resolve completeness from preset failed -- some derived fields lack resolve rules: {:?}",
3528 result.err()
3529 );
3530 }
3531
3532 #[test]
3533 fn validate_all_presets_pass_range_checks() {
3534 let names = crate::ThemeSpec::list_presets();
3536 assert!(names.len() >= 16, "expected at least 16 presets");
3537
3538 for name in names {
3539 let spec = crate::ThemeSpec::preset(name).unwrap();
3540 if let Some(light) = spec.light {
3541 let resolved = light.into_resolved();
3542 assert!(
3543 resolved.is_ok(),
3544 "preset '{name}' light variant failed: {:?}",
3545 resolved.err()
3546 );
3547 }
3548 if let Some(dark) = spec.dark {
3549 let resolved = dark.into_resolved();
3550 assert!(
3551 resolved.is_ok(),
3552 "preset '{name}' dark variant failed: {:?}",
3553 resolved.err()
3554 );
3555 }
3556 }
3557 }
3558}