Skip to main content

native_theme/
resolve.rs

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