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