Skip to main content

native_theme/model/
resolved.rs

1// Resolved (non-optional) theme types produced after theme resolution.
2//
3// These types mirror their Option-based counterparts in defaults.rs, font.rs,
4// spacing.rs, icon_sizes.rs, and mod.rs (ThemeVariant), but with all fields
5// guaranteed populated. Produced by validate() after resolve().
6
7use super::widgets::ResolvedFontSpec;
8use crate::Rgba;
9
10// --- ResolvedSpacing ---
11
12/// A fully resolved spacing scale where every tier is guaranteed populated.
13#[derive(Clone, Debug, PartialEq)]
14pub struct ResolvedSpacing {
15    /// Extra-extra-small spacing in logical pixels.
16    pub xxs: f32,
17    /// Extra-small spacing in logical pixels.
18    pub xs: f32,
19    /// Small spacing in logical pixels.
20    pub s: f32,
21    /// Medium spacing in logical pixels.
22    pub m: f32,
23    /// Large spacing in logical pixels.
24    pub l: f32,
25    /// Extra-large spacing in logical pixels.
26    pub xl: f32,
27    /// Extra-extra-large spacing in logical pixels.
28    pub xxl: f32,
29}
30
31// --- ResolvedIconSizes ---
32
33/// Fully resolved per-context icon sizes where every context is guaranteed populated.
34#[derive(Clone, Debug, PartialEq)]
35pub struct ResolvedIconSizes {
36    /// Icon size for toolbar buttons.
37    pub toolbar: f32,
38    /// Small icon size for inline use.
39    pub small: f32,
40    /// Large icon size for menus/lists.
41    pub large: f32,
42    /// Icon size for dialog buttons.
43    pub dialog: f32,
44    /// Icon size for panel headers.
45    pub panel: f32,
46}
47
48// --- ResolvedTextScaleEntry ---
49
50/// A single resolved text scale entry with guaranteed size, weight, and line height.
51#[derive(Clone, Debug, Default, PartialEq)]
52pub struct ResolvedTextScaleEntry {
53    /// Font size in logical pixels.
54    pub size: f32,
55    /// CSS font weight (100-900).
56    pub weight: u16,
57    /// Line height in logical pixels (computed as `defaults.line_height × size`).
58    pub line_height: f32,
59}
60
61// --- ResolvedTextScale ---
62
63/// A fully resolved text scale with all four typographic roles populated.
64#[derive(Clone, Debug, PartialEq)]
65pub struct ResolvedTextScale {
66    /// Caption / small label text.
67    pub caption: ResolvedTextScaleEntry,
68    /// Section heading text.
69    pub section_heading: ResolvedTextScaleEntry,
70    /// Dialog title text.
71    pub dialog_title: ResolvedTextScaleEntry,
72    /// Large display / hero text.
73    pub display: ResolvedTextScaleEntry,
74}
75
76// --- ResolvedDefaults ---
77
78/// Fully resolved global theme defaults where every field is guaranteed populated.
79///
80/// Mirrors [`crate::model::ThemeDefaults`] but with concrete (non-Option) types.
81/// Produced by the resolution/validation pipeline.
82#[derive(Clone, Debug, PartialEq)]
83pub struct ResolvedDefaults {
84    // ---- Base font ----
85    /// Primary UI font.
86    pub font: ResolvedFontSpec,
87    /// Line height multiplier.
88    pub line_height: f32,
89    /// Monospace font for code/terminal content.
90    pub mono_font: ResolvedFontSpec,
91
92    // ---- Base colors ----
93    /// Main window/surface background color.
94    pub background: Rgba,
95    /// Default text color.
96    pub foreground: Rgba,
97    /// Accent/brand color for interactive elements.
98    pub accent: Rgba,
99    /// Text color used on accent-colored backgrounds.
100    pub accent_foreground: Rgba,
101    /// Elevated surface color.
102    pub surface: Rgba,
103    /// Border/divider color.
104    pub border: Rgba,
105    /// Secondary/subdued text color.
106    pub muted: Rgba,
107    /// Drop shadow color.
108    pub shadow: Rgba,
109    /// Hyperlink text color.
110    pub link: Rgba,
111    /// Selection highlight background.
112    pub selection: Rgba,
113    /// Text color over selection highlight.
114    pub selection_foreground: Rgba,
115    /// Selection background when window is unfocused.
116    pub selection_inactive: Rgba,
117    /// Text color for disabled controls.
118    pub disabled_foreground: Rgba,
119
120    // ---- Status colors ----
121    /// Danger/error color.
122    pub danger: Rgba,
123    /// Text color on danger-colored backgrounds.
124    pub danger_foreground: Rgba,
125    /// Warning color.
126    pub warning: Rgba,
127    /// Text color on warning-colored backgrounds.
128    pub warning_foreground: Rgba,
129    /// Success/confirmation color.
130    pub success: Rgba,
131    /// Text color on success-colored backgrounds.
132    pub success_foreground: Rgba,
133    /// Informational color.
134    pub info: Rgba,
135    /// Text color on info-colored backgrounds.
136    pub info_foreground: Rgba,
137
138    // ---- Global geometry ----
139    /// Default corner radius in logical pixels.
140    pub radius: f32,
141    /// Large corner radius.
142    pub radius_lg: f32,
143    /// Border/frame width in logical pixels.
144    pub frame_width: f32,
145    /// Opacity for disabled controls.
146    pub disabled_opacity: f32,
147    /// Border alpha multiplier.
148    pub border_opacity: f32,
149    /// Whether drop shadows are enabled.
150    pub shadow_enabled: bool,
151
152    // ---- Focus ring ----
153    /// Focus indicator outline color.
154    pub focus_ring_color: Rgba,
155    /// Focus indicator outline width.
156    pub focus_ring_width: f32,
157    /// Gap between element edge and focus indicator.
158    pub focus_ring_offset: f32,
159
160    // ---- Spacing scale ----
161    /// Logical spacing scale.
162    pub spacing: ResolvedSpacing,
163
164    // ---- Icon sizes ----
165    /// Per-context icon sizes.
166    pub icon_sizes: ResolvedIconSizes,
167
168    // ---- Accessibility ----
169    /// Text scaling factor (1.0 = no scaling).
170    pub text_scaling_factor: f32,
171    /// Whether the user has requested reduced motion.
172    pub reduce_motion: bool,
173    /// Whether a high-contrast mode is active.
174    pub high_contrast: bool,
175    /// Whether the user has requested reduced transparency.
176    pub reduce_transparency: bool,
177}
178
179// --- ResolvedTheme ---
180
181/// A fully resolved theme where every field is guaranteed populated.
182///
183/// Produced by `validate()` after `resolve()`. Consumed by toolkit connectors.
184/// Mirrors [`crate::model::ThemeVariant`] but with concrete (non-Option) types
185/// for all 25 per-widget structs plus defaults and text scale.
186#[derive(Clone, Debug, PartialEq)]
187pub struct ResolvedTheme {
188    /// Global defaults.
189    pub defaults: ResolvedDefaults,
190    /// Per-role text scale.
191    pub text_scale: ResolvedTextScale,
192
193    // ---- Per-widget resolved structs ----
194    /// Window chrome.
195    pub window: super::widgets::ResolvedWindow,
196    /// Push button.
197    pub button: super::widgets::ResolvedButton,
198    /// Text input.
199    pub input: super::widgets::ResolvedInput,
200    /// Checkbox / radio button.
201    pub checkbox: super::widgets::ResolvedCheckbox,
202    /// Popup / context menu.
203    pub menu: super::widgets::ResolvedMenu,
204    /// Tooltip.
205    pub tooltip: super::widgets::ResolvedTooltip,
206    /// Scrollbar.
207    pub scrollbar: super::widgets::ResolvedScrollbar,
208    /// Slider.
209    pub slider: super::widgets::ResolvedSlider,
210    /// Progress bar.
211    pub progress_bar: super::widgets::ResolvedProgressBar,
212    /// Tab bar.
213    pub tab: super::widgets::ResolvedTab,
214    /// Sidebar panel.
215    pub sidebar: super::widgets::ResolvedSidebar,
216    /// Toolbar.
217    pub toolbar: super::widgets::ResolvedToolbar,
218    /// Status bar.
219    pub status_bar: super::widgets::ResolvedStatusBar,
220    /// List / table.
221    pub list: super::widgets::ResolvedList,
222    /// Popover / dropdown.
223    pub popover: super::widgets::ResolvedPopover,
224    /// Splitter handle.
225    pub splitter: super::widgets::ResolvedSplitter,
226    /// Separator line.
227    pub separator: super::widgets::ResolvedSeparator,
228    /// Toggle switch.
229    pub switch: super::widgets::ResolvedSwitch,
230    /// Dialog.
231    pub dialog: super::widgets::ResolvedDialog,
232    /// Spinner / progress ring.
233    pub spinner: super::widgets::ResolvedSpinner,
234    /// ComboBox / dropdown trigger.
235    pub combo_box: super::widgets::ResolvedComboBox,
236    /// Segmented control.
237    pub segmented_control: super::widgets::ResolvedSegmentedControl,
238    /// Card / container.
239    pub card: super::widgets::ResolvedCard,
240    /// Expander / disclosure.
241    pub expander: super::widgets::ResolvedExpander,
242    /// Hyperlink.
243    pub link: super::widgets::ResolvedLink,
244
245    /// Icon set / naming convention (e.g., "sf-symbols", "freedesktop").
246    pub icon_set: String,
247}
248
249#[cfg(test)]
250#[allow(
251    clippy::unwrap_used,
252    clippy::expect_used,
253    clippy::bool_assert_comparison
254)]
255mod tests {
256    use super::*;
257    use crate::Rgba;
258    use crate::model::DialogButtonOrder;
259    use crate::model::widgets::{
260        ResolvedButton, ResolvedCard, ResolvedCheckbox, ResolvedComboBox, ResolvedDialog,
261        ResolvedExpander, ResolvedFontSpec, ResolvedInput, ResolvedLink, ResolvedList,
262        ResolvedMenu, ResolvedPopover, ResolvedProgressBar, ResolvedScrollbar,
263        ResolvedSegmentedControl, ResolvedSeparator, ResolvedSidebar, ResolvedSlider,
264        ResolvedSpinner, ResolvedSplitter, ResolvedStatusBar, ResolvedSwitch, ResolvedTab,
265        ResolvedToolbar, ResolvedTooltip, ResolvedWindow,
266    };
267
268    fn sample_font() -> ResolvedFontSpec {
269        ResolvedFontSpec {
270            family: "Inter".into(),
271            size: 14.0,
272            weight: 400,
273        }
274    }
275
276    fn sample_spacing() -> ResolvedSpacing {
277        ResolvedSpacing {
278            xxs: 2.0,
279            xs: 4.0,
280            s: 6.0,
281            m: 12.0,
282            l: 18.0,
283            xl: 24.0,
284            xxl: 36.0,
285        }
286    }
287
288    fn sample_icon_sizes() -> ResolvedIconSizes {
289        ResolvedIconSizes {
290            toolbar: 24.0,
291            small: 16.0,
292            large: 32.0,
293            dialog: 22.0,
294            panel: 20.0,
295        }
296    }
297
298    fn sample_text_scale_entry() -> ResolvedTextScaleEntry {
299        ResolvedTextScaleEntry {
300            size: 12.0,
301            weight: 400,
302            line_height: 1.4,
303        }
304    }
305
306    fn sample_defaults() -> ResolvedDefaults {
307        let c = Rgba::rgb(128, 128, 128);
308        ResolvedDefaults {
309            font: sample_font(),
310            line_height: 1.4,
311            mono_font: ResolvedFontSpec {
312                family: "JetBrains Mono".into(),
313                size: 12.0,
314                weight: 400,
315            },
316            background: c,
317            foreground: c,
318            accent: c,
319            accent_foreground: c,
320            surface: c,
321            border: c,
322            muted: c,
323            shadow: c,
324            link: c,
325            selection: c,
326            selection_foreground: c,
327            selection_inactive: c,
328            disabled_foreground: c,
329            danger: c,
330            danger_foreground: c,
331            warning: c,
332            warning_foreground: c,
333            success: c,
334            success_foreground: c,
335            info: c,
336            info_foreground: c,
337            radius: 4.0,
338            radius_lg: 8.0,
339            frame_width: 1.0,
340            disabled_opacity: 0.5,
341            border_opacity: 0.15,
342            shadow_enabled: true,
343            focus_ring_color: c,
344            focus_ring_width: 2.0,
345            focus_ring_offset: 1.0,
346            spacing: sample_spacing(),
347            icon_sizes: sample_icon_sizes(),
348            text_scaling_factor: 1.0,
349            reduce_motion: false,
350            high_contrast: false,
351            reduce_transparency: false,
352        }
353    }
354
355    // --- ResolvedSpacing tests ---
356
357    #[test]
358    fn resolved_spacing_has_7_concrete_fields() {
359        let s = sample_spacing();
360        assert_eq!(s.xxs, 2.0);
361        assert_eq!(s.xs, 4.0);
362        assert_eq!(s.s, 6.0);
363        assert_eq!(s.m, 12.0);
364        assert_eq!(s.l, 18.0);
365        assert_eq!(s.xl, 24.0);
366        assert_eq!(s.xxl, 36.0);
367    }
368
369    #[test]
370    fn resolved_spacing_derives_clone_debug_partialeq() {
371        let s = sample_spacing();
372        let s2 = s.clone();
373        assert_eq!(s, s2);
374        let dbg = format!("{s:?}");
375        assert!(dbg.contains("ResolvedSpacing"));
376    }
377
378    // --- ResolvedIconSizes tests ---
379
380    #[test]
381    fn resolved_icon_sizes_has_5_concrete_fields() {
382        let i = sample_icon_sizes();
383        assert_eq!(i.toolbar, 24.0);
384        assert_eq!(i.small, 16.0);
385        assert_eq!(i.large, 32.0);
386        assert_eq!(i.dialog, 22.0);
387        assert_eq!(i.panel, 20.0);
388    }
389
390    #[test]
391    fn resolved_icon_sizes_derives_clone_debug_partialeq() {
392        let i = sample_icon_sizes();
393        let i2 = i.clone();
394        assert_eq!(i, i2);
395        let dbg = format!("{i:?}");
396        assert!(dbg.contains("ResolvedIconSizes"));
397    }
398
399    // --- ResolvedTextScaleEntry tests ---
400
401    #[test]
402    fn resolved_text_scale_entry_has_3_concrete_fields() {
403        let e = sample_text_scale_entry();
404        assert_eq!(e.size, 12.0);
405        assert_eq!(e.weight, 400);
406        assert_eq!(e.line_height, 1.4);
407    }
408
409    #[test]
410    fn resolved_text_scale_entry_derives_clone_debug_partialeq() {
411        let e = sample_text_scale_entry();
412        let e2 = e.clone();
413        assert_eq!(e, e2);
414        let dbg = format!("{e:?}");
415        assert!(dbg.contains("ResolvedTextScaleEntry"));
416    }
417
418    // --- ResolvedTextScale tests ---
419
420    #[test]
421    fn resolved_text_scale_has_4_entries() {
422        let ts = ResolvedTextScale {
423            caption: ResolvedTextScaleEntry {
424                size: 11.0,
425                weight: 400,
426                line_height: 1.3,
427            },
428            section_heading: ResolvedTextScaleEntry {
429                size: 14.0,
430                weight: 600,
431                line_height: 1.4,
432            },
433            dialog_title: ResolvedTextScaleEntry {
434                size: 16.0,
435                weight: 700,
436                line_height: 1.2,
437            },
438            display: ResolvedTextScaleEntry {
439                size: 24.0,
440                weight: 300,
441                line_height: 1.1,
442            },
443        };
444        assert_eq!(ts.caption.size, 11.0);
445        assert_eq!(ts.section_heading.weight, 600);
446        assert_eq!(ts.dialog_title.size, 16.0);
447        assert_eq!(ts.display.weight, 300);
448    }
449
450    #[test]
451    fn resolved_text_scale_derives_clone_debug_partialeq() {
452        let e = sample_text_scale_entry();
453        let ts = ResolvedTextScale {
454            caption: e.clone(),
455            section_heading: e.clone(),
456            dialog_title: e.clone(),
457            display: e,
458        };
459        let ts2 = ts.clone();
460        assert_eq!(ts, ts2);
461        let dbg = format!("{ts:?}");
462        assert!(dbg.contains("ResolvedTextScale"));
463    }
464
465    // --- ResolvedDefaults tests ---
466
467    #[test]
468    fn resolved_defaults_all_fields_concrete() {
469        let d = sample_defaults();
470        // Fonts
471        assert_eq!(d.font.family, "Inter");
472        assert_eq!(d.mono_font.family, "JetBrains Mono");
473        assert_eq!(d.line_height, 1.4);
474        // Some colors
475        assert_eq!(d.background, Rgba::rgb(128, 128, 128));
476        assert_eq!(d.accent, Rgba::rgb(128, 128, 128));
477        // Geometry
478        assert_eq!(d.radius, 4.0);
479        assert_eq!(d.shadow_enabled, true);
480        // Focus ring
481        assert_eq!(d.focus_ring_width, 2.0);
482        // Spacing and icon sizes
483        assert_eq!(d.spacing.m, 12.0);
484        assert_eq!(d.icon_sizes.toolbar, 24.0);
485        // Accessibility
486        assert_eq!(d.text_scaling_factor, 1.0);
487        assert_eq!(d.reduce_motion, false);
488    }
489
490    #[test]
491    fn resolved_defaults_derives_clone_debug_partialeq() {
492        let d = sample_defaults();
493        let d2 = d.clone();
494        assert_eq!(d, d2);
495        let dbg = format!("{d:?}");
496        assert!(dbg.contains("ResolvedDefaults"));
497    }
498
499    // --- ResolvedTheme tests ---
500
501    #[test]
502    fn resolved_theme_construction_with_all_widgets() {
503        let c = Rgba::rgb(100, 100, 100);
504        let f = sample_font();
505        let e = sample_text_scale_entry();
506
507        let theme = ResolvedTheme {
508            defaults: sample_defaults(),
509            text_scale: ResolvedTextScale {
510                caption: e.clone(),
511                section_heading: e.clone(),
512                dialog_title: e.clone(),
513                display: e,
514            },
515            window: ResolvedWindow {
516                background: c,
517                foreground: c,
518                border: c,
519                title_bar_background: c,
520                title_bar_foreground: c,
521                inactive_title_bar_background: c,
522                inactive_title_bar_foreground: c,
523                radius: 4.0,
524                shadow: true,
525                title_bar_font: f.clone(),
526            },
527            button: ResolvedButton {
528                background: c,
529                foreground: c,
530                border: c,
531                primary_bg: c,
532                primary_fg: c,
533                min_width: 64.0,
534                min_height: 28.0,
535                padding_horizontal: 12.0,
536                padding_vertical: 6.0,
537                radius: 4.0,
538                icon_spacing: 6.0,
539                disabled_opacity: 0.5,
540                shadow: false,
541                font: f.clone(),
542            },
543            input: ResolvedInput {
544                background: c,
545                foreground: c,
546                border: c,
547                placeholder: c,
548                caret: c,
549                selection: c,
550                selection_foreground: c,
551                min_height: 28.0,
552                padding_horizontal: 8.0,
553                padding_vertical: 4.0,
554                radius: 4.0,
555                border_width: 1.0,
556                font: f.clone(),
557            },
558            checkbox: ResolvedCheckbox {
559                checked_bg: c,
560                indicator_size: 18.0,
561                spacing: 6.0,
562                radius: 2.0,
563                border_width: 1.0,
564            },
565            menu: ResolvedMenu {
566                background: c,
567                foreground: c,
568                separator: c,
569                item_height: 28.0,
570                padding_horizontal: 8.0,
571                padding_vertical: 4.0,
572                icon_spacing: 6.0,
573                font: f.clone(),
574            },
575            tooltip: ResolvedTooltip {
576                background: c,
577                foreground: c,
578                padding_horizontal: 6.0,
579                padding_vertical: 4.0,
580                max_width: 300.0,
581                radius: 4.0,
582                font: f.clone(),
583            },
584            scrollbar: ResolvedScrollbar {
585                track: c,
586                thumb: c,
587                thumb_hover: c,
588                width: 14.0,
589                min_thumb_height: 20.0,
590                slider_width: 8.0,
591                overlay_mode: false,
592            },
593            slider: ResolvedSlider {
594                fill: c,
595                track: c,
596                thumb: c,
597                track_height: 4.0,
598                thumb_size: 16.0,
599                tick_length: 6.0,
600            },
601            progress_bar: ResolvedProgressBar {
602                fill: c,
603                track: c,
604                height: 6.0,
605                min_width: 100.0,
606                radius: 3.0,
607            },
608            tab: ResolvedTab {
609                background: c,
610                foreground: c,
611                active_background: c,
612                active_foreground: c,
613                bar_background: c,
614                min_width: 60.0,
615                min_height: 32.0,
616                padding_horizontal: 12.0,
617                padding_vertical: 6.0,
618            },
619            sidebar: ResolvedSidebar {
620                background: c,
621                foreground: c,
622            },
623            toolbar: ResolvedToolbar {
624                height: 40.0,
625                item_spacing: 4.0,
626                padding: 4.0,
627                font: f.clone(),
628            },
629            status_bar: ResolvedStatusBar { font: f.clone() },
630            list: ResolvedList {
631                background: c,
632                foreground: c,
633                alternate_row: c,
634                selection: c,
635                selection_foreground: c,
636                header_background: c,
637                header_foreground: c,
638                grid_color: c,
639                item_height: 28.0,
640                padding_horizontal: 8.0,
641                padding_vertical: 4.0,
642            },
643            popover: ResolvedPopover {
644                background: c,
645                foreground: c,
646                border: c,
647                radius: 6.0,
648            },
649            splitter: ResolvedSplitter { width: 4.0 },
650            separator: ResolvedSeparator { color: c },
651            switch: ResolvedSwitch {
652                checked_bg: c,
653                unchecked_bg: c,
654                thumb_bg: c,
655                track_width: 40.0,
656                track_height: 20.0,
657                thumb_size: 14.0,
658                track_radius: 10.0,
659            },
660            dialog: ResolvedDialog {
661                min_width: 320.0,
662                max_width: 600.0,
663                min_height: 200.0,
664                max_height: 800.0,
665                content_padding: 16.0,
666                button_spacing: 8.0,
667                radius: 8.0,
668                icon_size: 22.0,
669                button_order: DialogButtonOrder::TrailingAffirmative,
670                title_font: f.clone(),
671            },
672            spinner: ResolvedSpinner {
673                fill: c,
674                diameter: 24.0,
675                min_size: 16.0,
676                stroke_width: 2.0,
677            },
678            combo_box: ResolvedComboBox {
679                min_height: 28.0,
680                min_width: 80.0,
681                padding_horizontal: 8.0,
682                arrow_size: 12.0,
683                arrow_area_width: 20.0,
684                radius: 4.0,
685            },
686            segmented_control: ResolvedSegmentedControl {
687                segment_height: 28.0,
688                separator_width: 1.0,
689                padding_horizontal: 12.0,
690                radius: 4.0,
691            },
692            card: ResolvedCard {
693                background: c,
694                border: c,
695                radius: 8.0,
696                padding: 12.0,
697                shadow: true,
698            },
699            expander: ResolvedExpander {
700                header_height: 32.0,
701                arrow_size: 12.0,
702                content_padding: 8.0,
703                radius: 4.0,
704            },
705            link: ResolvedLink {
706                color: c,
707                visited: c,
708                background: c,
709                hover_bg: c,
710                underline: true,
711            },
712            icon_set: "freedesktop".into(),
713        };
714
715        // Verify key fields
716        assert_eq!(theme.defaults.font.family, "Inter");
717        assert_eq!(theme.window.radius, 4.0);
718        assert_eq!(theme.button.min_height, 28.0);
719        assert_eq!(theme.icon_set, "freedesktop");
720        assert_eq!(theme.text_scale.caption.size, 12.0);
721    }
722
723    #[test]
724    fn resolved_theme_derives_clone_debug_partialeq() {
725        let c = Rgba::rgb(100, 100, 100);
726        let f = sample_font();
727        let e = sample_text_scale_entry();
728
729        let theme = ResolvedTheme {
730            defaults: sample_defaults(),
731            text_scale: ResolvedTextScale {
732                caption: e.clone(),
733                section_heading: e.clone(),
734                dialog_title: e.clone(),
735                display: e,
736            },
737            window: ResolvedWindow {
738                background: c,
739                foreground: c,
740                border: c,
741                title_bar_background: c,
742                title_bar_foreground: c,
743                inactive_title_bar_background: c,
744                inactive_title_bar_foreground: c,
745                radius: 4.0,
746                shadow: true,
747                title_bar_font: f.clone(),
748            },
749            button: ResolvedButton {
750                background: c,
751                foreground: c,
752                border: c,
753                primary_bg: c,
754                primary_fg: c,
755                min_width: 64.0,
756                min_height: 28.0,
757                padding_horizontal: 12.0,
758                padding_vertical: 6.0,
759                radius: 4.0,
760                icon_spacing: 6.0,
761                disabled_opacity: 0.5,
762                shadow: false,
763                font: f.clone(),
764            },
765            input: ResolvedInput {
766                background: c,
767                foreground: c,
768                border: c,
769                placeholder: c,
770                caret: c,
771                selection: c,
772                selection_foreground: c,
773                min_height: 28.0,
774                padding_horizontal: 8.0,
775                padding_vertical: 4.0,
776                radius: 4.0,
777                border_width: 1.0,
778                font: f.clone(),
779            },
780            checkbox: ResolvedCheckbox {
781                checked_bg: c,
782                indicator_size: 18.0,
783                spacing: 6.0,
784                radius: 2.0,
785                border_width: 1.0,
786            },
787            menu: ResolvedMenu {
788                background: c,
789                foreground: c,
790                separator: c,
791                item_height: 28.0,
792                padding_horizontal: 8.0,
793                padding_vertical: 4.0,
794                icon_spacing: 6.0,
795                font: f.clone(),
796            },
797            tooltip: ResolvedTooltip {
798                background: c,
799                foreground: c,
800                padding_horizontal: 6.0,
801                padding_vertical: 4.0,
802                max_width: 300.0,
803                radius: 4.0,
804                font: f.clone(),
805            },
806            scrollbar: ResolvedScrollbar {
807                track: c,
808                thumb: c,
809                thumb_hover: c,
810                width: 14.0,
811                min_thumb_height: 20.0,
812                slider_width: 8.0,
813                overlay_mode: false,
814            },
815            slider: ResolvedSlider {
816                fill: c,
817                track: c,
818                thumb: c,
819                track_height: 4.0,
820                thumb_size: 16.0,
821                tick_length: 6.0,
822            },
823            progress_bar: ResolvedProgressBar {
824                fill: c,
825                track: c,
826                height: 6.0,
827                min_width: 100.0,
828                radius: 3.0,
829            },
830            tab: ResolvedTab {
831                background: c,
832                foreground: c,
833                active_background: c,
834                active_foreground: c,
835                bar_background: c,
836                min_width: 60.0,
837                min_height: 32.0,
838                padding_horizontal: 12.0,
839                padding_vertical: 6.0,
840            },
841            sidebar: ResolvedSidebar {
842                background: c,
843                foreground: c,
844            },
845            toolbar: ResolvedToolbar {
846                height: 40.0,
847                item_spacing: 4.0,
848                padding: 4.0,
849                font: f.clone(),
850            },
851            status_bar: ResolvedStatusBar { font: f.clone() },
852            list: ResolvedList {
853                background: c,
854                foreground: c,
855                alternate_row: c,
856                selection: c,
857                selection_foreground: c,
858                header_background: c,
859                header_foreground: c,
860                grid_color: c,
861                item_height: 28.0,
862                padding_horizontal: 8.0,
863                padding_vertical: 4.0,
864            },
865            popover: ResolvedPopover {
866                background: c,
867                foreground: c,
868                border: c,
869                radius: 6.0,
870            },
871            splitter: ResolvedSplitter { width: 4.0 },
872            separator: ResolvedSeparator { color: c },
873            switch: ResolvedSwitch {
874                checked_bg: c,
875                unchecked_bg: c,
876                thumb_bg: c,
877                track_width: 40.0,
878                track_height: 20.0,
879                thumb_size: 14.0,
880                track_radius: 10.0,
881            },
882            dialog: ResolvedDialog {
883                min_width: 320.0,
884                max_width: 600.0,
885                min_height: 200.0,
886                max_height: 800.0,
887                content_padding: 16.0,
888                button_spacing: 8.0,
889                radius: 8.0,
890                icon_size: 22.0,
891                button_order: DialogButtonOrder::TrailingAffirmative,
892                title_font: f.clone(),
893            },
894            spinner: ResolvedSpinner {
895                fill: c,
896                diameter: 24.0,
897                min_size: 16.0,
898                stroke_width: 2.0,
899            },
900            combo_box: ResolvedComboBox {
901                min_height: 28.0,
902                min_width: 80.0,
903                padding_horizontal: 8.0,
904                arrow_size: 12.0,
905                arrow_area_width: 20.0,
906                radius: 4.0,
907            },
908            segmented_control: ResolvedSegmentedControl {
909                segment_height: 28.0,
910                separator_width: 1.0,
911                padding_horizontal: 12.0,
912                radius: 4.0,
913            },
914            card: ResolvedCard {
915                background: c,
916                border: c,
917                radius: 8.0,
918                padding: 12.0,
919                shadow: true,
920            },
921            expander: ResolvedExpander {
922                header_height: 32.0,
923                arrow_size: 12.0,
924                content_padding: 8.0,
925                radius: 4.0,
926            },
927            link: ResolvedLink {
928                color: c,
929                visited: c,
930                background: c,
931                hover_bg: c,
932                underline: true,
933            },
934            icon_set: "freedesktop".into(),
935        };
936
937        let theme2 = theme.clone();
938        assert_eq!(theme, theme2);
939        let dbg = format!("{theme:?}");
940        assert!(dbg.contains("ResolvedTheme"));
941    }
942}