Skip to main content

native_theme/
resolve.rs

1// Resolution engine: resolve() fills inheritance rules, validate() produces ResolvedTheme.
2
3use 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
11/// Resolve a per-widget font from defaults.
12/// If the widget font is None, clone defaults entirely.
13/// If the widget font is Some but has None sub-fields, fill from defaults.
14fn 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
33/// Resolve a text scale entry from defaults.
34/// Creates the entry if None, fills sub-fields from defaults.font,
35/// computes line_height from defaults.line_height * resolved_size.
36fn 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
55// --- validate() helpers ---
56
57/// Extract a required field, recording the path if missing.
58///
59/// Returns the value if present, or `T::default()` as a placeholder if missing.
60/// The placeholder is never used: `validate()` returns `Err` before constructing
61/// `ResolvedTheme` when any field was recorded as missing.
62fn 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
72/// Validate a FontSpec that is stored directly (not wrapped in Option).
73/// Checks each sub-field individually.
74fn 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
85/// Validate an `Option<FontSpec>` (widget font fields).
86/// If None, records the path as missing.
87fn 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
110/// Validate an `Option<TextScaleEntry>`.
111fn 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    /// Apply all ~90 inheritance rules in 4-phase order.
136    ///
137    /// After calling resolve(), most Option fields that were None will be filled
138    /// from defaults or related widget fields. Calling resolve() twice produces
139    /// the same result (idempotent).
140    ///
141    /// # Phases
142    ///
143    /// 1. **Defaults internal chains** -- accent derives selection, focus_ring_color;
144    ///    selection derives selection_inactive.
145    /// 2. **Safety nets** -- platform-divergent fields get a reasonable fallback.
146    /// 3. **Widget-from-defaults** -- colors, geometry, fonts, text scale entries
147    ///    all inherit from defaults.
148    /// 4. **Widget-to-widget** -- inactive title bar fields fall back to active.
149    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    // --- Phase 1: Defaults internal chains ---
157
158    fn resolve_defaults_internal(&mut self) {
159        let d = &mut self.defaults;
160
161        // selection <- accent
162        if d.selection.is_none() {
163            d.selection = d.accent;
164        }
165        // focus_ring_color <- accent
166        if d.focus_ring_color.is_none() {
167            d.focus_ring_color = d.accent;
168        }
169        // selection_inactive <- selection (MUST run after selection is set)
170        if d.selection_inactive.is_none() {
171            d.selection_inactive = d.selection;
172        }
173    }
174
175    // --- Phase 2: Safety nets ---
176
177    fn resolve_safety_nets(&mut self) {
178        // input.caret <- defaults.foreground
179        if self.input.caret.is_none() {
180            self.input.caret = self.defaults.foreground;
181        }
182        // scrollbar.track <- defaults.background
183        if self.scrollbar.track.is_none() {
184            self.scrollbar.track = self.defaults.background;
185        }
186        // spinner.fill <- defaults.foreground
187        if self.spinner.fill.is_none() {
188            self.spinner.fill = self.defaults.foreground;
189        }
190        // popover.background <- defaults.background
191        if self.popover.background.is_none() {
192            self.popover.background = self.defaults.background;
193        }
194        // list.background <- defaults.background
195        if self.list.background.is_none() {
196            self.list.background = self.defaults.background;
197        }
198    }
199
200    // --- Phase 3: Widget-from-defaults ---
201
202    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        // --- window ---
212        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        // --- button ---
235        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        // --- input ---
261        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        // --- checkbox ---
287        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        // --- menu ---
298        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        // --- tooltip ---
309        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        // --- scrollbar ---
320        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        // --- slider ---
328        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        // --- progress_bar ---
339        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        // --- tab ---
350        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        // --- sidebar ---
367        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        // --- list ---
375        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        // --- popover ---
398        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        // --- separator ---
409        if self.separator.color.is_none() {
410            self.separator.color = d.border;
411        }
412
413        // --- switch ---
414        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        // --- dialog ---
422        if self.dialog.radius.is_none() {
423            self.dialog.radius = d.radius_lg;
424        }
425
426        // --- combo_box ---
427        if self.combo_box.radius.is_none() {
428            self.combo_box.radius = d.radius;
429        }
430
431        // --- segmented_control ---
432        if self.segmented_control.radius.is_none() {
433            self.segmented_control.radius = d.radius;
434        }
435
436        // --- card ---
437        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        // --- expander ---
451        if self.expander.radius.is_none() {
452            self.expander.radius = d.radius;
453        }
454
455        // --- link ---
456        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    // --- Phase 4: Widget-to-widget chains ---
494
495    fn resolve_widget_to_widget(&mut self) {
496        // inactive title bar <- active title bar
497        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    // --- validate() ---
506
507    /// Convert this ThemeVariant into a [`ResolvedTheme`] with all fields guaranteed.
508    ///
509    /// Should be called after [`resolve()`](ThemeVariant::resolve). Walks every field
510    /// and collects missing (None) field paths. Returns `Ok(ResolvedTheme)` if all fields
511    /// are populated, or `Err(Error::Resolution(...))` listing every missing field.
512    ///
513    /// # Errors
514    ///
515    /// Returns [`crate::Error::Resolution`] containing a [`ThemeResolutionError`]
516    /// with all missing field paths if any fields remain None.
517    pub fn validate(&self) -> crate::Result<ResolvedTheme> {
518        let mut missing = Vec::new();
519
520        // --- defaults ---
521
522        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        // --- text_scale ---
710
711        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        // --- window ---
727
728        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        // --- button ---
760
761        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        // --- input ---
793
794        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        // --- checkbox ---
822
823        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        // --- menu ---
842
843        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        // --- tooltip ---
861
862        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        // --- scrollbar ---
881
882        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        // --- slider ---
907
908        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        // --- progress_bar ---
921
922        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        // --- tab ---
942
943        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        // --- sidebar ---
971
972        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        // --- toolbar ---
978
979        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        // --- status_bar ---
989
990        let status_bar_font =
991            require_font_opt(&self.status_bar.font, "status_bar.font", &mut missing);
992
993        // --- list ---
994
995        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        // --- popover ---
1029
1030        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        // --- splitter ---
1038
1039        let splitter_width = require(&self.splitter.width, "splitter.width", &mut missing);
1040
1041        // --- separator ---
1042
1043        let separator_color = require(&self.separator.color, "separator.color", &mut missing);
1044
1045        // --- switch ---
1046
1047        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        // --- dialog ---
1069
1070        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        // --- spinner ---
1095
1096        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        // --- combo_box ---
1106
1107        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        // --- segmented_control ---
1135
1136        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        // --- card ---
1158
1159        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        // --- expander ---
1166
1167        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        // --- link ---
1185
1186        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        // --- icon_set ---
1193
1194        let icon_set = require(&self.icon_set, "icon_set", &mut missing);
1195
1196        // --- check for missing fields ---
1197
1198        if !missing.is_empty() {
1199            return Err(crate::Error::Resolution(ThemeResolutionError {
1200                missing_fields: missing,
1201            }));
1202        }
1203
1204        // All fields present -- construct ResolvedTheme.
1205        // require() returns T directly (using T::default() as placeholder for missing),
1206        // so no unwrap() is needed. The defaults are never used: we returned Err above.
1207        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    /// Helper: build a ThemeVariant with all defaults.* fields populated.
1485    fn variant_with_defaults() -> ThemeVariant {
1486        let c1 = Rgba::rgb(0, 120, 215); // accent
1487        let c2 = Rgba::rgb(255, 255, 255); // background
1488        let c3 = Rgba::rgb(30, 30, 30); // foreground
1489        let c4 = Rgba::rgb(240, 240, 240); // surface
1490        let c5 = Rgba::rgb(200, 200, 200); // border
1491        let c6 = Rgba::rgb(128, 128, 128); // muted
1492        let c7 = Rgba::rgb(0, 0, 0); // shadow
1493        let c8 = Rgba::rgb(0, 100, 200); // link
1494        let c9 = Rgba::rgb(255, 255, 255); // accent_foreground
1495        let c10 = Rgba::rgb(220, 53, 69); // danger
1496        let c11 = Rgba::rgb(255, 255, 255); // danger_foreground
1497        let c12 = Rgba::rgb(240, 173, 78); // warning
1498        let c13 = Rgba::rgb(30, 30, 30); // warning_foreground
1499        let c14 = Rgba::rgb(40, 167, 69); // success
1500        let c15 = Rgba::rgb(255, 255, 255); // success_foreground
1501        let c16 = Rgba::rgb(0, 120, 215); // info
1502        let c17 = Rgba::rgb(255, 255, 255); // info_foreground
1503
1504        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    // ===== Phase 1: Defaults internal chains =====
1570
1571    #[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        // selection_inactive should be set from selection (which was set from accent)
1586        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        // Explicit selection preserved
1596        assert_eq!(v.defaults.selection, Some(Rgba::rgb(100, 100, 100)));
1597        // selection_inactive inherits from the explicit selection
1598        assert_eq!(
1599            v.defaults.selection_inactive,
1600            Some(Rgba::rgb(100, 100, 100))
1601        );
1602    }
1603
1604    // ===== Phase 2: Safety nets =====
1605
1606    #[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    // ===== Phase 3: Accent propagation (RESOLVE-06) =====
1641
1642    #[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    // ===== Phase 3: Font sub-field inheritance (RESOLVE-04) =====
1676
1677    #[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        // Menu has a font with only size set
1686        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        // button.font is None, should inherit entire defaults.font
1712        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    // ===== Phase 3: Text scale inheritance (RESOLVE-05) =====
1722
1723    #[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        // Leave text_scale.caption as None
1733        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        // line_height = defaults.line_height * resolved_size = 1.4 * 14.0 = 19.6
1743        assert!(
1744            (caption.line_height.unwrap() - 19.6).abs() < 0.001,
1745            "line_height computed"
1746        );
1747    }
1748
1749    // ===== Phase 3: Color inheritance =====
1750
1751    #[test]
1752    fn resolve_phase3_color_inheritance() {
1753        let mut v = variant_with_defaults();
1754        v.resolve();
1755
1756        // window
1757        assert_eq!(v.window.background, Some(Rgba::rgb(255, 255, 255)));
1758        assert_eq!(v.window.border, v.defaults.border);
1759        // button
1760        assert_eq!(v.button.border, v.defaults.border);
1761        // tooltip
1762        assert_eq!(v.tooltip.radius, v.defaults.radius);
1763    }
1764
1765    // ===== Phase 4: Widget-to-widget =====
1766
1767    #[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        // title_bar_background was set to surface in Phase 3
1775        // inactive should inherit from active
1776        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    // ===== Preserve explicit values =====
1787
1788    #[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    // ===== Idempotent =====
1809
1810    #[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    // ===== All 8 font-carrying widgets get resolved fonts =====
1820
1821    #[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        // All 8 should now have Some(FontSpec)
1832        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        // Each should have the defaults values
1842        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    // ===== validate() tests =====
1860
1861    /// Build a fully-populated ThemeVariant (all fields Some) for validate() testing.
1862    fn fully_populated_variant() -> ThemeVariant {
1863        let mut v = variant_with_defaults();
1864        let c = Rgba::rgb(128, 128, 128);
1865
1866        // Ensure derived defaults are set (variant_with_defaults doesn't set these)
1867        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        // icon_set
1873        v.icon_set = Some("freedesktop".into());
1874
1875        // window
1876        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        // button
1892        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        // input
1912        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        // checkbox
1931        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        // menu
1938        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        // tooltip
1952        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        // scrollbar
1965        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        // slider
1974        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        // progress_bar
1982        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        // tab
1989        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        // sidebar
2000        v.sidebar.background = Some(c);
2001        v.sidebar.foreground = Some(c);
2002
2003        // toolbar
2004        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        // status_bar
2014        v.status_bar.font = Some(FontSpec {
2015            family: Some("Inter".into()),
2016            size: Some(14.0),
2017            weight: Some(400),
2018        });
2019
2020        // list
2021        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        // popover
2034        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        // splitter
2040        v.splitter.width = Some(4.0);
2041
2042        // separator
2043        v.separator.color = Some(c);
2044
2045        // switch
2046        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        // dialog
2055        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        // spinner
2071        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        // combo_box
2077        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        // segmented_control
2085        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        // card
2091        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        // expander
2098        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        // link
2104        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        // text_scale (all 4 entries fully populated)
2111        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        // Remove 3 specific fields (non-cascading)
2153        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        // Default variant has ALL fields None, so validate should report many missing
2195        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        // Should include defaults fields
2203        assert!(
2204            err.missing_fields
2205                .iter()
2206                .any(|f| f.starts_with("defaults.")),
2207            "should include defaults.* fields in missing"
2208        );
2209        // Check a representative set of defaults fields
2210        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        // Every widget should have at least one field reported
2243        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        // Start with defaults populated but widgets empty
2323        let mut v = variant_with_defaults();
2324        // Add non-derivable widget sizing fields
2325        v.icon_set = Some("freedesktop".into());
2326
2327        // Non-derivable fields that resolve() cannot fill:
2328        // button sizing
2329        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        // input sizing
2335        v.input.min_height = Some(28.0);
2336        v.input.padding_horizontal = Some(8.0);
2337        v.input.padding_vertical = Some(4.0);
2338        // checkbox sizing
2339        v.checkbox.indicator_size = Some(18.0);
2340        v.checkbox.spacing = Some(6.0);
2341        // menu sizing
2342        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        // tooltip sizing
2347        v.tooltip.padding_horizontal = Some(6.0);
2348        v.tooltip.padding_vertical = Some(4.0);
2349        v.tooltip.max_width = Some(300.0);
2350        // scrollbar sizing
2351        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        // slider sizing
2356        v.slider.track_height = Some(4.0);
2357        v.slider.thumb_size = Some(16.0);
2358        v.slider.tick_length = Some(6.0);
2359        // progress_bar sizing
2360        v.progress_bar.height = Some(6.0);
2361        v.progress_bar.min_width = Some(100.0);
2362        // tab sizing
2363        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        // toolbar sizing
2368        v.toolbar.height = Some(40.0);
2369        v.toolbar.item_spacing = Some(4.0);
2370        v.toolbar.padding = Some(4.0);
2371        // list sizing
2372        v.list.item_height = Some(28.0);
2373        v.list.padding_horizontal = Some(8.0);
2374        v.list.padding_vertical = Some(4.0);
2375        // splitter
2376        v.splitter.width = Some(4.0);
2377        // switch sizing
2378        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        // dialog sizing
2384        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        // spinner sizing
2393        v.spinner.diameter = Some(24.0);
2394        v.spinner.min_size = Some(16.0);
2395        v.spinner.stroke_width = Some(2.0);
2396        // combo_box sizing
2397        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        // segmented_control sizing
2403        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        // card
2407        v.card.padding = Some(12.0);
2408        // expander
2409        v.expander.header_height = Some(32.0);
2410        v.expander.arrow_size = Some(12.0);
2411        v.expander.content_padding = Some(8.0);
2412        // link
2413        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        // Simulate GNOME reader pipeline: adwaita base + GNOME reader overlay.
2429        // On a non-GNOME system, build_gnome_variant() only sets dialog.button_order
2430        // and icon_set (gsettings calls return None). We simulate the full merge.
2431        let adwaita = crate::NativeTheme::preset("adwaita").unwrap();
2432
2433        // Pick dark variant from adwaita (matches GNOME PreferDark path).
2434        let mut variant = adwaita
2435            .dark
2436            .clone()
2437            .expect("adwaita should have dark variant");
2438
2439        // Apply what build_gnome_variant() would set.
2440        variant.dialog.button_order = Some(DialogButtonOrder::TrailingAffirmative);
2441        // icon_set comes from gsettings icon-theme; simulate typical GNOME value.
2442        variant.icon_set = Some("Adwaita".to_string());
2443
2444        // Simulate GNOME reader font output (gsettings font-name on a GNOME system).
2445        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        // Spot-check: adwaita-base fields present.
2457        // Adwaita dark accent is #3584e4 = rgb(53, 132, 228)
2458        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}