Skip to main content

native_theme/resolve/
validate.rs

1// Theme validation: orchestrate defaults extraction, per-widget dispatch, range checks,
2// and ResolvedThemeVariant construction.
3// Helper functions, range-check utilities, and ValidateNested trait live in validate_helpers.rs.
4
5use super::validate_helpers::{
6    self, DEFAULT_FONT_DPI, require, require_font, require_text_scale_entry,
7};
8use crate::error::ThemeResolutionError;
9use crate::model::ThemeVariant;
10use crate::model::resolved::{
11    ResolvedIconSizes, ResolvedTextScale, ResolvedThemeDefaults, ResolvedThemeVariant,
12};
13
14impl ThemeVariant {
15    /// Convert this ThemeVariant into a [`ResolvedThemeVariant`] with all fields guaranteed.
16    ///
17    /// Should be called after [`resolve()`](ThemeVariant::resolve). Walks every field
18    /// and collects missing (None) field paths, then validates that numeric values
19    /// are within legal ranges (e.g., spacing >= 0, opacity 0..=1, font weight
20    /// 100..=900). Returns `Ok(ResolvedThemeVariant)` if all fields are populated
21    /// and in range.
22    ///
23    /// # Errors
24    ///
25    /// Returns [`crate::Error::Resolution`] containing a [`ThemeResolutionError`]
26    /// with all missing field paths and out-of-range diagnostics.
27    #[must_use = "this returns the validation result; handle the Result or propagate with ?"]
28    pub fn validate(&self) -> crate::Result<ResolvedThemeVariant> {
29        let mut missing = Vec::new();
30        let dpi = self.defaults.font_dpi.unwrap_or(DEFAULT_FONT_DPI);
31
32        // --- defaults extraction ---
33        let defaults_font = require_font(&self.defaults.font, "defaults.font", dpi, &mut missing);
34        let defaults_line_height = require(
35            &self.defaults.line_height,
36            "defaults.line_height",
37            &mut missing,
38        );
39        let defaults_mono_font = require_font(
40            &self.defaults.mono_font,
41            "defaults.mono_font",
42            dpi,
43            &mut missing,
44        );
45
46        let defaults_background = require(
47            &self.defaults.background_color,
48            "defaults.background_color",
49            &mut missing,
50        );
51        let defaults_foreground = require(
52            &self.defaults.text_color,
53            "defaults.text_color",
54            &mut missing,
55        );
56        let defaults_accent = require(
57            &self.defaults.accent_color,
58            "defaults.accent_color",
59            &mut missing,
60        );
61        let defaults_accent_foreground = require(
62            &self.defaults.accent_text_color,
63            "defaults.accent_text_color",
64            &mut missing,
65        );
66        let defaults_surface = require(
67            &self.defaults.surface_color,
68            "defaults.surface_color",
69            &mut missing,
70        );
71        let defaults_border = require(
72            &self.defaults.border.color,
73            "defaults.border.color",
74            &mut missing,
75        );
76        let defaults_muted = require(
77            &self.defaults.muted_color,
78            "defaults.muted_color",
79            &mut missing,
80        );
81        let defaults_shadow = require(
82            &self.defaults.shadow_color,
83            "defaults.shadow_color",
84            &mut missing,
85        );
86        let defaults_link = require(
87            &self.defaults.link_color,
88            "defaults.link_color",
89            &mut missing,
90        );
91        let defaults_selection = require(
92            &self.defaults.selection_background,
93            "defaults.selection_background",
94            &mut missing,
95        );
96        let defaults_selection_foreground = require(
97            &self.defaults.selection_text_color,
98            "defaults.selection_text_color",
99            &mut missing,
100        );
101        let defaults_selection_inactive = require(
102            &self.defaults.selection_inactive_background,
103            "defaults.selection_inactive_background",
104            &mut missing,
105        );
106        let defaults_disabled_foreground = require(
107            &self.defaults.disabled_text_color,
108            "defaults.disabled_text_color",
109            &mut missing,
110        );
111
112        let defaults_danger = require(
113            &self.defaults.danger_color,
114            "defaults.danger_color",
115            &mut missing,
116        );
117        let defaults_danger_foreground = require(
118            &self.defaults.danger_text_color,
119            "defaults.danger_text_color",
120            &mut missing,
121        );
122        let defaults_warning = require(
123            &self.defaults.warning_color,
124            "defaults.warning_color",
125            &mut missing,
126        );
127        let defaults_warning_foreground = require(
128            &self.defaults.warning_text_color,
129            "defaults.warning_text_color",
130            &mut missing,
131        );
132        let defaults_success = require(
133            &self.defaults.success_color,
134            "defaults.success_color",
135            &mut missing,
136        );
137        let defaults_success_foreground = require(
138            &self.defaults.success_text_color,
139            "defaults.success_text_color",
140            &mut missing,
141        );
142        let defaults_info = require(
143            &self.defaults.info_color,
144            "defaults.info_color",
145            &mut missing,
146        );
147        let defaults_info_foreground = require(
148            &self.defaults.info_text_color,
149            "defaults.info_text_color",
150            &mut missing,
151        );
152
153        let defaults_radius = require(
154            &self.defaults.border.corner_radius,
155            "defaults.border.corner_radius",
156            &mut missing,
157        );
158        let defaults_radius_lg = require(
159            &self.defaults.border.corner_radius_lg,
160            "defaults.border.corner_radius_lg",
161            &mut missing,
162        );
163        let defaults_frame_width = require(
164            &self.defaults.border.line_width,
165            "defaults.border.line_width",
166            &mut missing,
167        );
168        let defaults_disabled_opacity = require(
169            &self.defaults.disabled_opacity,
170            "defaults.disabled_opacity",
171            &mut missing,
172        );
173        let defaults_border_opacity = require(
174            &self.defaults.border.opacity,
175            "defaults.border.opacity",
176            &mut missing,
177        );
178        let defaults_shadow_enabled = require(
179            &self.defaults.border.shadow_enabled,
180            "defaults.border.shadow_enabled",
181            &mut missing,
182        );
183
184        let defaults_focus_ring_color = require(
185            &self.defaults.focus_ring_color,
186            "defaults.focus_ring_color",
187            &mut missing,
188        );
189        let defaults_focus_ring_width = require(
190            &self.defaults.focus_ring_width,
191            "defaults.focus_ring_width",
192            &mut missing,
193        );
194        let defaults_focus_ring_offset = require(
195            &self.defaults.focus_ring_offset,
196            "defaults.focus_ring_offset",
197            &mut missing,
198        );
199
200        let defaults_border_padding_h = require(
201            &self.defaults.border.padding_horizontal,
202            "defaults.border.padding_horizontal",
203            &mut missing,
204        );
205        let defaults_border_padding_v = require(
206            &self.defaults.border.padding_vertical,
207            "defaults.border.padding_vertical",
208            &mut missing,
209        );
210        let defaults_text_selection_background = require(
211            &self.defaults.text_selection_background,
212            "defaults.text_selection_background",
213            &mut missing,
214        );
215        let defaults_text_selection_color = require(
216            &self.defaults.text_selection_color,
217            "defaults.text_selection_color",
218            &mut missing,
219        );
220
221        let defaults_icon_sizes_toolbar = require(
222            &self.defaults.icon_sizes.toolbar,
223            "defaults.icon_sizes.toolbar",
224            &mut missing,
225        );
226        let defaults_icon_sizes_small = require(
227            &self.defaults.icon_sizes.small,
228            "defaults.icon_sizes.small",
229            &mut missing,
230        );
231        let defaults_icon_sizes_large = require(
232            &self.defaults.icon_sizes.large,
233            "defaults.icon_sizes.large",
234            &mut missing,
235        );
236        let defaults_icon_sizes_dialog = require(
237            &self.defaults.icon_sizes.dialog,
238            "defaults.icon_sizes.dialog",
239            &mut missing,
240        );
241        let defaults_icon_sizes_panel = require(
242            &self.defaults.icon_sizes.panel,
243            "defaults.icon_sizes.panel",
244            &mut missing,
245        );
246
247        let defaults_font_dpi = dpi;
248        let defaults_text_scaling_factor = require(
249            &self.defaults.text_scaling_factor,
250            "defaults.text_scaling_factor",
251            &mut missing,
252        );
253        let defaults_reduce_motion = require(
254            &self.defaults.reduce_motion,
255            "defaults.reduce_motion",
256            &mut missing,
257        );
258        let defaults_high_contrast = require(
259            &self.defaults.high_contrast,
260            "defaults.high_contrast",
261            &mut missing,
262        );
263        let defaults_reduce_transparency = require(
264            &self.defaults.reduce_transparency,
265            "defaults.reduce_transparency",
266            &mut missing,
267        );
268
269        let ts_caption = require_text_scale_entry(
270            &self.text_scale.caption,
271            "text_scale.caption",
272            dpi,
273            &mut missing,
274        );
275        let ts_section_heading = require_text_scale_entry(
276            &self.text_scale.section_heading,
277            "text_scale.section_heading",
278            dpi,
279            &mut missing,
280        );
281        let ts_dialog_title = require_text_scale_entry(
282            &self.text_scale.dialog_title,
283            "text_scale.dialog_title",
284            dpi,
285            &mut missing,
286        );
287        let ts_display = require_text_scale_entry(
288            &self.text_scale.display,
289            "text_scale.display",
290            dpi,
291            &mut missing,
292        );
293
294        // --- construct defaults and text_scale structs (before range checks) ---
295        use crate::model::border::ResolvedBorderSpec;
296        let defaults = ResolvedThemeDefaults {
297            font: defaults_font,
298            line_height: defaults_line_height,
299            mono_font: defaults_mono_font,
300            background_color: defaults_background,
301            text_color: defaults_foreground,
302            accent_color: defaults_accent,
303            accent_text_color: defaults_accent_foreground,
304            surface_color: defaults_surface,
305            border: ResolvedBorderSpec {
306                color: defaults_border,
307                corner_radius: defaults_radius,
308                corner_radius_lg: defaults_radius_lg,
309                line_width: defaults_frame_width,
310                opacity: defaults_border_opacity,
311                shadow_enabled: defaults_shadow_enabled,
312                padding_horizontal: defaults_border_padding_h,
313                padding_vertical: defaults_border_padding_v,
314            },
315            muted_color: defaults_muted,
316            shadow_color: defaults_shadow,
317            link_color: defaults_link,
318            selection_background: defaults_selection,
319            selection_text_color: defaults_selection_foreground,
320            selection_inactive_background: defaults_selection_inactive,
321            text_selection_background: defaults_text_selection_background,
322            text_selection_color: defaults_text_selection_color,
323            disabled_text_color: defaults_disabled_foreground,
324            danger_color: defaults_danger,
325            danger_text_color: defaults_danger_foreground,
326            warning_color: defaults_warning,
327            warning_text_color: defaults_warning_foreground,
328            success_color: defaults_success,
329            success_text_color: defaults_success_foreground,
330            info_color: defaults_info,
331            info_text_color: defaults_info_foreground,
332            disabled_opacity: defaults_disabled_opacity,
333            focus_ring_color: defaults_focus_ring_color,
334            focus_ring_width: defaults_focus_ring_width,
335            focus_ring_offset: defaults_focus_ring_offset,
336            icon_sizes: ResolvedIconSizes {
337                toolbar: defaults_icon_sizes_toolbar,
338                small: defaults_icon_sizes_small,
339                large: defaults_icon_sizes_large,
340                dialog: defaults_icon_sizes_dialog,
341                panel: defaults_icon_sizes_panel,
342            },
343            font_dpi: defaults_font_dpi,
344            text_scaling_factor: defaults_text_scaling_factor,
345            reduce_motion: defaults_reduce_motion,
346            high_contrast: defaults_high_contrast,
347            reduce_transparency: defaults_reduce_transparency,
348        };
349        let text_scale = ResolvedTextScale {
350            caption: ts_caption,
351            section_heading: ts_section_heading,
352            dialog_title: ts_dialog_title,
353            display: ts_display,
354        };
355
356        validate_helpers::check_defaults_ranges(&defaults, &text_scale, &mut missing);
357
358        // --- per-widget extraction (generated by define_widget_pair!) ---
359        use crate::model::widgets::*;
360        let window =
361            ResolvedWindowTheme::validate_widget(&self.window, "window", dpi, &mut missing);
362        let button =
363            ResolvedButtonTheme::validate_widget(&self.button, "button", dpi, &mut missing);
364        let input = ResolvedInputTheme::validate_widget(&self.input, "input", dpi, &mut missing);
365        let checkbox =
366            ResolvedCheckboxTheme::validate_widget(&self.checkbox, "checkbox", dpi, &mut missing);
367        let menu = ResolvedMenuTheme::validate_widget(&self.menu, "menu", dpi, &mut missing);
368        let tooltip =
369            ResolvedTooltipTheme::validate_widget(&self.tooltip, "tooltip", dpi, &mut missing);
370        let scrollbar = ResolvedScrollbarTheme::validate_widget(
371            &self.scrollbar,
372            "scrollbar",
373            dpi,
374            &mut missing,
375        );
376        let slider =
377            ResolvedSliderTheme::validate_widget(&self.slider, "slider", dpi, &mut missing);
378        let progress_bar = ResolvedProgressBarTheme::validate_widget(
379            &self.progress_bar,
380            "progress_bar",
381            dpi,
382            &mut missing,
383        );
384        let tab = ResolvedTabTheme::validate_widget(&self.tab, "tab", dpi, &mut missing);
385        let sidebar =
386            ResolvedSidebarTheme::validate_widget(&self.sidebar, "sidebar", dpi, &mut missing);
387        let toolbar =
388            ResolvedToolbarTheme::validate_widget(&self.toolbar, "toolbar", dpi, &mut missing);
389        let status_bar = ResolvedStatusBarTheme::validate_widget(
390            &self.status_bar,
391            "status_bar",
392            dpi,
393            &mut missing,
394        );
395        let list = ResolvedListTheme::validate_widget(&self.list, "list", dpi, &mut missing);
396        let popover =
397            ResolvedPopoverTheme::validate_widget(&self.popover, "popover", dpi, &mut missing);
398        let splitter =
399            ResolvedSplitterTheme::validate_widget(&self.splitter, "splitter", dpi, &mut missing);
400        let separator = ResolvedSeparatorTheme::validate_widget(
401            &self.separator,
402            "separator",
403            dpi,
404            &mut missing,
405        );
406        let switch =
407            ResolvedSwitchTheme::validate_widget(&self.switch, "switch", dpi, &mut missing);
408        let dialog =
409            ResolvedDialogTheme::validate_widget(&self.dialog, "dialog", dpi, &mut missing);
410        let spinner =
411            ResolvedSpinnerTheme::validate_widget(&self.spinner, "spinner", dpi, &mut missing);
412        let combo_box =
413            ResolvedComboBoxTheme::validate_widget(&self.combo_box, "combo_box", dpi, &mut missing);
414        let segmented_control = ResolvedSegmentedControlTheme::validate_widget(
415            &self.segmented_control,
416            "segmented_control",
417            dpi,
418            &mut missing,
419        );
420        let card = ResolvedCardTheme::validate_widget(&self.card, "card", dpi, &mut missing);
421        let expander =
422            ResolvedExpanderTheme::validate_widget(&self.expander, "expander", dpi, &mut missing);
423        let link = ResolvedLinkTheme::validate_widget(&self.link, "link", dpi, &mut missing);
424
425        let icon_set = require(&self.icon_set, "icon_set", &mut missing);
426        let icon_theme = require(&self.icon_theme, "icon_theme", &mut missing);
427
428        // --- per-widget range checks ---
429        window.check_ranges("window", &mut missing);
430        button.check_ranges("button", &mut missing);
431        input.check_ranges("input", &mut missing);
432        checkbox.check_ranges("checkbox", &mut missing);
433        menu.check_ranges("menu", &mut missing);
434        tooltip.check_ranges("tooltip", &mut missing);
435        scrollbar.check_ranges("scrollbar", &mut missing);
436        slider.check_ranges("slider", &mut missing);
437        progress_bar.check_ranges("progress_bar", &mut missing);
438        tab.check_ranges("tab", &mut missing);
439        sidebar.check_ranges("sidebar", &mut missing);
440        toolbar.check_ranges("toolbar", &mut missing);
441        status_bar.check_ranges("status_bar", &mut missing);
442        list.check_ranges("list", &mut missing);
443        popover.check_ranges("popover", &mut missing);
444        splitter.check_ranges("splitter", &mut missing);
445        separator.check_ranges("separator", &mut missing);
446        switch.check_ranges("switch", &mut missing);
447        dialog.check_ranges("dialog", &mut missing);
448        spinner.check_ranges("spinner", &mut missing);
449        combo_box.check_ranges("combo_box", &mut missing);
450        segmented_control.check_ranges("segmented_control", &mut missing);
451        expander.check_ranges("expander", &mut missing);
452        link.check_ranges("link", &mut missing);
453
454        if !missing.is_empty() {
455            return Err(crate::Error::Resolution(ThemeResolutionError {
456                missing_fields: missing,
457            }));
458        }
459
460        Ok(ResolvedThemeVariant {
461            defaults,
462            text_scale,
463            window,
464            button,
465            input,
466            checkbox,
467            menu,
468            tooltip,
469            scrollbar,
470            slider,
471            progress_bar,
472            tab,
473            sidebar,
474            toolbar,
475            status_bar,
476            list,
477            popover,
478            splitter,
479            separator,
480            switch,
481            dialog,
482            spinner,
483            combo_box,
484            segmented_control,
485            card,
486            expander,
487            link,
488            icon_set,
489            icon_theme,
490        })
491    }
492}