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::font::ResolvedFontSpec;
8use crate::Rgba;
9
10// --- ResolvedThemeSpacing ---
11
12/// A fully resolved spacing scale where every tier is guaranteed populated.
13#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
14pub struct ResolvedThemeSpacing {
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, serde::Serialize, serde::Deserialize)]
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, serde::Serialize, serde::Deserialize)]
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, serde::Serialize, serde::Deserialize)]
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// --- ResolvedThemeDefaults ---
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, serde::Serialize, serde::Deserialize)]
83pub struct ResolvedThemeDefaults {
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: ResolvedThemeSpacing,
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// --- ResolvedThemeVariant ---
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, serde::Serialize, serde::Deserialize)]
187pub struct ResolvedThemeVariant {
188    /// Global defaults.
189    pub defaults: ResolvedThemeDefaults,
190    /// Per-role text scale.
191    pub text_scale: ResolvedTextScale,
192
193    // ---- Per-widget resolved structs ----
194    /// Window chrome.
195    pub window: super::widgets::ResolvedWindowTheme,
196    /// Push button.
197    pub button: super::widgets::ResolvedButtonTheme,
198    /// Text input.
199    pub input: super::widgets::ResolvedInputTheme,
200    /// Checkbox / radio button.
201    pub checkbox: super::widgets::ResolvedCheckboxTheme,
202    /// Popup / context menu.
203    pub menu: super::widgets::ResolvedMenuTheme,
204    /// Tooltip.
205    pub tooltip: super::widgets::ResolvedTooltipTheme,
206    /// Scrollbar.
207    pub scrollbar: super::widgets::ResolvedScrollbarTheme,
208    /// Slider.
209    pub slider: super::widgets::ResolvedSliderTheme,
210    /// Progress bar.
211    pub progress_bar: super::widgets::ResolvedProgressBarTheme,
212    /// Tab bar.
213    pub tab: super::widgets::ResolvedTabTheme,
214    /// Sidebar panel.
215    pub sidebar: super::widgets::ResolvedSidebarTheme,
216    /// Toolbar.
217    pub toolbar: super::widgets::ResolvedToolbarTheme,
218    /// Status bar.
219    pub status_bar: super::widgets::ResolvedStatusBarTheme,
220    /// List / table.
221    pub list: super::widgets::ResolvedListTheme,
222    /// Popover / dropdown.
223    pub popover: super::widgets::ResolvedPopoverTheme,
224    /// Splitter handle.
225    pub splitter: super::widgets::ResolvedSplitterTheme,
226    /// Separator line.
227    pub separator: super::widgets::ResolvedSeparatorTheme,
228    /// Toggle switch.
229    pub switch: super::widgets::ResolvedSwitchTheme,
230    /// Dialog.
231    pub dialog: super::widgets::ResolvedDialogTheme,
232    /// Spinner / progress ring.
233    pub spinner: super::widgets::ResolvedSpinnerTheme,
234    /// ComboBox / dropdown trigger.
235    pub combo_box: super::widgets::ResolvedComboBoxTheme,
236    /// Segmented control.
237    pub segmented_control: super::widgets::ResolvedSegmentedControlTheme,
238    /// Card / container.
239    pub card: super::widgets::ResolvedCardTheme,
240    /// Expander / disclosure.
241    pub expander: super::widgets::ResolvedExpanderTheme,
242    /// Hyperlink.
243    pub link: super::widgets::ResolvedLinkTheme,
244
245    /// Which icon loading mechanism to use — determines *how* icons are looked
246    /// up (freedesktop theme directories, bundled SVG tables, SF Symbols, etc.).
247    pub icon_set: crate::IconSet,
248
249    /// The name of the visual icon theme that provides the actual icon files
250    /// (e.g. `"breeze"`, `"Adwaita"`, `"Lucide"`).
251    pub icon_theme: String,
252}
253
254#[cfg(test)]
255#[allow(
256    clippy::unwrap_used,
257    clippy::expect_used,
258    clippy::bool_assert_comparison
259)]
260mod tests {
261    use super::*;
262    use crate::Rgba;
263    use crate::model::DialogButtonOrder;
264    use crate::model::ResolvedFontSpec;
265    use crate::model::widgets::{
266        ResolvedButtonTheme, ResolvedCardTheme, ResolvedCheckboxTheme, ResolvedComboBoxTheme,
267        ResolvedDialogTheme, ResolvedExpanderTheme, ResolvedInputTheme, ResolvedLinkTheme,
268        ResolvedListTheme, ResolvedMenuTheme, ResolvedPopoverTheme, ResolvedProgressBarTheme,
269        ResolvedScrollbarTheme, ResolvedSegmentedControlTheme, ResolvedSeparatorTheme,
270        ResolvedSidebarTheme, ResolvedSliderTheme, ResolvedSpinnerTheme, ResolvedSplitterTheme,
271        ResolvedStatusBarTheme, ResolvedSwitchTheme, ResolvedTabTheme, ResolvedToolbarTheme,
272        ResolvedTooltipTheme, ResolvedWindowTheme,
273    };
274
275    fn sample_font() -> ResolvedFontSpec {
276        ResolvedFontSpec {
277            family: "Inter".into(),
278            size: 14.0,
279            weight: 400,
280        }
281    }
282
283    fn sample_spacing() -> ResolvedThemeSpacing {
284        ResolvedThemeSpacing {
285            xxs: 2.0,
286            xs: 4.0,
287            s: 6.0,
288            m: 12.0,
289            l: 18.0,
290            xl: 24.0,
291            xxl: 36.0,
292        }
293    }
294
295    fn sample_icon_sizes() -> ResolvedIconSizes {
296        ResolvedIconSizes {
297            toolbar: 24.0,
298            small: 16.0,
299            large: 32.0,
300            dialog: 22.0,
301            panel: 20.0,
302        }
303    }
304
305    fn sample_text_scale_entry() -> ResolvedTextScaleEntry {
306        ResolvedTextScaleEntry {
307            size: 12.0,
308            weight: 400,
309            line_height: 1.4,
310        }
311    }
312
313    fn sample_defaults() -> ResolvedThemeDefaults {
314        let c = Rgba::rgb(128, 128, 128);
315        ResolvedThemeDefaults {
316            font: sample_font(),
317            line_height: 1.4,
318            mono_font: ResolvedFontSpec {
319                family: "JetBrains Mono".into(),
320                size: 12.0,
321                weight: 400,
322            },
323            background: c,
324            foreground: c,
325            accent: c,
326            accent_foreground: c,
327            surface: c,
328            border: c,
329            muted: c,
330            shadow: c,
331            link: c,
332            selection: c,
333            selection_foreground: c,
334            selection_inactive: c,
335            disabled_foreground: c,
336            danger: c,
337            danger_foreground: c,
338            warning: c,
339            warning_foreground: c,
340            success: c,
341            success_foreground: c,
342            info: c,
343            info_foreground: c,
344            radius: 4.0,
345            radius_lg: 8.0,
346            frame_width: 1.0,
347            disabled_opacity: 0.5,
348            border_opacity: 0.15,
349            shadow_enabled: true,
350            focus_ring_color: c,
351            focus_ring_width: 2.0,
352            focus_ring_offset: 1.0,
353            spacing: sample_spacing(),
354            icon_sizes: sample_icon_sizes(),
355            text_scaling_factor: 1.0,
356            reduce_motion: false,
357            high_contrast: false,
358            reduce_transparency: false,
359        }
360    }
361
362    // --- ResolvedThemeSpacing tests ---
363
364    #[test]
365    fn resolved_spacing_has_7_concrete_fields() {
366        let s = sample_spacing();
367        assert_eq!(s.xxs, 2.0);
368        assert_eq!(s.xs, 4.0);
369        assert_eq!(s.s, 6.0);
370        assert_eq!(s.m, 12.0);
371        assert_eq!(s.l, 18.0);
372        assert_eq!(s.xl, 24.0);
373        assert_eq!(s.xxl, 36.0);
374    }
375
376    #[test]
377    fn resolved_spacing_derives_clone_debug_partialeq() {
378        let s = sample_spacing();
379        let s2 = s.clone();
380        assert_eq!(s, s2);
381        let dbg = format!("{s:?}");
382        assert!(dbg.contains("ResolvedThemeSpacing"));
383    }
384
385    // --- ResolvedIconSizes tests ---
386
387    #[test]
388    fn resolved_icon_sizes_has_5_concrete_fields() {
389        let i = sample_icon_sizes();
390        assert_eq!(i.toolbar, 24.0);
391        assert_eq!(i.small, 16.0);
392        assert_eq!(i.large, 32.0);
393        assert_eq!(i.dialog, 22.0);
394        assert_eq!(i.panel, 20.0);
395    }
396
397    #[test]
398    fn resolved_icon_sizes_derives_clone_debug_partialeq() {
399        let i = sample_icon_sizes();
400        let i2 = i.clone();
401        assert_eq!(i, i2);
402        let dbg = format!("{i:?}");
403        assert!(dbg.contains("ResolvedIconSizes"));
404    }
405
406    // --- ResolvedTextScaleEntry tests ---
407
408    #[test]
409    fn resolved_text_scale_entry_has_3_concrete_fields() {
410        let e = sample_text_scale_entry();
411        assert_eq!(e.size, 12.0);
412        assert_eq!(e.weight, 400);
413        assert_eq!(e.line_height, 1.4);
414    }
415
416    #[test]
417    fn resolved_text_scale_entry_derives_clone_debug_partialeq() {
418        let e = sample_text_scale_entry();
419        let e2 = e.clone();
420        assert_eq!(e, e2);
421        let dbg = format!("{e:?}");
422        assert!(dbg.contains("ResolvedTextScaleEntry"));
423    }
424
425    // --- ResolvedTextScale tests ---
426
427    #[test]
428    fn resolved_text_scale_has_4_entries() {
429        let ts = ResolvedTextScale {
430            caption: ResolvedTextScaleEntry {
431                size: 11.0,
432                weight: 400,
433                line_height: 1.3,
434            },
435            section_heading: ResolvedTextScaleEntry {
436                size: 14.0,
437                weight: 600,
438                line_height: 1.4,
439            },
440            dialog_title: ResolvedTextScaleEntry {
441                size: 16.0,
442                weight: 700,
443                line_height: 1.2,
444            },
445            display: ResolvedTextScaleEntry {
446                size: 24.0,
447                weight: 300,
448                line_height: 1.1,
449            },
450        };
451        assert_eq!(ts.caption.size, 11.0);
452        assert_eq!(ts.section_heading.weight, 600);
453        assert_eq!(ts.dialog_title.size, 16.0);
454        assert_eq!(ts.display.weight, 300);
455    }
456
457    #[test]
458    fn resolved_text_scale_derives_clone_debug_partialeq() {
459        let e = sample_text_scale_entry();
460        let ts = ResolvedTextScale {
461            caption: e.clone(),
462            section_heading: e.clone(),
463            dialog_title: e.clone(),
464            display: e,
465        };
466        let ts2 = ts.clone();
467        assert_eq!(ts, ts2);
468        let dbg = format!("{ts:?}");
469        assert!(dbg.contains("ResolvedTextScale"));
470    }
471
472    // --- ResolvedThemeDefaults tests ---
473
474    #[test]
475    fn resolved_defaults_all_fields_concrete() {
476        let d = sample_defaults();
477        // Fonts
478        assert_eq!(d.font.family, "Inter");
479        assert_eq!(d.mono_font.family, "JetBrains Mono");
480        assert_eq!(d.line_height, 1.4);
481        // Some colors
482        assert_eq!(d.background, Rgba::rgb(128, 128, 128));
483        assert_eq!(d.accent, Rgba::rgb(128, 128, 128));
484        // Geometry
485        assert_eq!(d.radius, 4.0);
486        assert_eq!(d.shadow_enabled, true);
487        // Focus ring
488        assert_eq!(d.focus_ring_width, 2.0);
489        // Spacing and icon sizes
490        assert_eq!(d.spacing.m, 12.0);
491        assert_eq!(d.icon_sizes.toolbar, 24.0);
492        // Accessibility
493        assert_eq!(d.text_scaling_factor, 1.0);
494        assert_eq!(d.reduce_motion, false);
495    }
496
497    #[test]
498    fn resolved_defaults_derives_clone_debug_partialeq() {
499        let d = sample_defaults();
500        let d2 = d.clone();
501        assert_eq!(d, d2);
502        let dbg = format!("{d:?}");
503        assert!(dbg.contains("ResolvedThemeDefaults"));
504    }
505
506    // --- ResolvedThemeVariant tests ---
507
508    #[test]
509    fn resolved_theme_construction_with_all_widgets() {
510        let c = Rgba::rgb(100, 100, 100);
511        let f = sample_font();
512        let e = sample_text_scale_entry();
513
514        let theme = ResolvedThemeVariant {
515            defaults: sample_defaults(),
516            text_scale: ResolvedTextScale {
517                caption: e.clone(),
518                section_heading: e.clone(),
519                dialog_title: e.clone(),
520                display: e,
521            },
522            window: ResolvedWindowTheme {
523                background: c,
524                foreground: c,
525                border: c,
526                title_bar_background: c,
527                title_bar_foreground: c,
528                inactive_title_bar_background: c,
529                inactive_title_bar_foreground: c,
530                radius: 4.0,
531                shadow: true,
532                title_bar_font: f.clone(),
533            },
534            button: ResolvedButtonTheme {
535                background: c,
536                foreground: c,
537                border: c,
538                primary_background: c,
539                primary_foreground: c,
540                min_width: 64.0,
541                min_height: 28.0,
542                padding_horizontal: 12.0,
543                padding_vertical: 6.0,
544                radius: 4.0,
545                icon_spacing: 6.0,
546                disabled_opacity: 0.5,
547                shadow: false,
548                font: f.clone(),
549            },
550            input: ResolvedInputTheme {
551                background: c,
552                foreground: c,
553                border: c,
554                placeholder: c,
555                caret: c,
556                selection: c,
557                selection_foreground: c,
558                min_height: 28.0,
559                padding_horizontal: 8.0,
560                padding_vertical: 4.0,
561                radius: 4.0,
562                border_width: 1.0,
563                font: f.clone(),
564            },
565            checkbox: ResolvedCheckboxTheme {
566                checked_background: c,
567                indicator_size: 18.0,
568                spacing: 6.0,
569                radius: 2.0,
570                border_width: 1.0,
571            },
572            menu: ResolvedMenuTheme {
573                background: c,
574                foreground: c,
575                separator: c,
576                item_height: 28.0,
577                padding_horizontal: 8.0,
578                padding_vertical: 4.0,
579                icon_spacing: 6.0,
580                font: f.clone(),
581            },
582            tooltip: ResolvedTooltipTheme {
583                background: c,
584                foreground: c,
585                padding_horizontal: 6.0,
586                padding_vertical: 4.0,
587                max_width: 300.0,
588                radius: 4.0,
589                font: f.clone(),
590            },
591            scrollbar: ResolvedScrollbarTheme {
592                track: c,
593                thumb: c,
594                thumb_hover: c,
595                width: 14.0,
596                min_thumb_height: 20.0,
597                slider_width: 8.0,
598                overlay_mode: false,
599            },
600            slider: ResolvedSliderTheme {
601                fill: c,
602                track: c,
603                thumb: c,
604                track_height: 4.0,
605                thumb_size: 16.0,
606                tick_length: 6.0,
607            },
608            progress_bar: ResolvedProgressBarTheme {
609                fill: c,
610                track: c,
611                height: 6.0,
612                min_width: 100.0,
613                radius: 3.0,
614            },
615            tab: ResolvedTabTheme {
616                background: c,
617                foreground: c,
618                active_background: c,
619                active_foreground: c,
620                bar_background: c,
621                min_width: 60.0,
622                min_height: 32.0,
623                padding_horizontal: 12.0,
624                padding_vertical: 6.0,
625            },
626            sidebar: ResolvedSidebarTheme {
627                background: c,
628                foreground: c,
629            },
630            toolbar: ResolvedToolbarTheme {
631                height: 40.0,
632                item_spacing: 4.0,
633                padding: 4.0,
634                font: f.clone(),
635            },
636            status_bar: ResolvedStatusBarTheme { font: f.clone() },
637            list: ResolvedListTheme {
638                background: c,
639                foreground: c,
640                alternate_row: c,
641                selection: c,
642                selection_foreground: c,
643                header_background: c,
644                header_foreground: c,
645                grid_color: c,
646                item_height: 28.0,
647                padding_horizontal: 8.0,
648                padding_vertical: 4.0,
649            },
650            popover: ResolvedPopoverTheme {
651                background: c,
652                foreground: c,
653                border: c,
654                radius: 6.0,
655            },
656            splitter: ResolvedSplitterTheme { width: 4.0 },
657            separator: ResolvedSeparatorTheme { color: c },
658            switch: ResolvedSwitchTheme {
659                checked_background: c,
660                unchecked_background: c,
661                thumb_background: c,
662                track_width: 40.0,
663                track_height: 20.0,
664                thumb_size: 14.0,
665                track_radius: 10.0,
666            },
667            dialog: ResolvedDialogTheme {
668                min_width: 320.0,
669                max_width: 600.0,
670                min_height: 200.0,
671                max_height: 800.0,
672                content_padding: 16.0,
673                button_spacing: 8.0,
674                radius: 8.0,
675                icon_size: 22.0,
676                button_order: DialogButtonOrder::TrailingAffirmative,
677                title_font: f.clone(),
678            },
679            spinner: ResolvedSpinnerTheme {
680                fill: c,
681                diameter: 24.0,
682                min_size: 16.0,
683                stroke_width: 2.0,
684            },
685            combo_box: ResolvedComboBoxTheme {
686                min_height: 28.0,
687                min_width: 80.0,
688                padding_horizontal: 8.0,
689                arrow_size: 12.0,
690                arrow_area_width: 20.0,
691                radius: 4.0,
692            },
693            segmented_control: ResolvedSegmentedControlTheme {
694                segment_height: 28.0,
695                separator_width: 1.0,
696                padding_horizontal: 12.0,
697                radius: 4.0,
698            },
699            card: ResolvedCardTheme {
700                background: c,
701                border: c,
702                radius: 8.0,
703                padding: 12.0,
704                shadow: true,
705            },
706            expander: ResolvedExpanderTheme {
707                header_height: 32.0,
708                arrow_size: 12.0,
709                content_padding: 8.0,
710                radius: 4.0,
711            },
712            link: ResolvedLinkTheme {
713                color: c,
714                visited: c,
715                background: c,
716                hover_bg: c,
717                underline: true,
718            },
719            icon_set: crate::IconSet::Freedesktop,
720            icon_theme: "breeze".into(),
721        };
722
723        // Verify key fields
724        assert_eq!(theme.defaults.font.family, "Inter");
725        assert_eq!(theme.window.radius, 4.0);
726        assert_eq!(theme.button.min_height, 28.0);
727        assert_eq!(theme.icon_set, crate::IconSet::Freedesktop);
728        assert_eq!(theme.icon_theme, "breeze");
729        assert_eq!(theme.text_scale.caption.size, 12.0);
730    }
731
732    #[test]
733    fn resolved_theme_derives_clone_debug_partialeq() {
734        let c = Rgba::rgb(100, 100, 100);
735        let f = sample_font();
736        let e = sample_text_scale_entry();
737
738        let theme = ResolvedThemeVariant {
739            defaults: sample_defaults(),
740            text_scale: ResolvedTextScale {
741                caption: e.clone(),
742                section_heading: e.clone(),
743                dialog_title: e.clone(),
744                display: e,
745            },
746            window: ResolvedWindowTheme {
747                background: c,
748                foreground: c,
749                border: c,
750                title_bar_background: c,
751                title_bar_foreground: c,
752                inactive_title_bar_background: c,
753                inactive_title_bar_foreground: c,
754                radius: 4.0,
755                shadow: true,
756                title_bar_font: f.clone(),
757            },
758            button: ResolvedButtonTheme {
759                background: c,
760                foreground: c,
761                border: c,
762                primary_background: c,
763                primary_foreground: c,
764                min_width: 64.0,
765                min_height: 28.0,
766                padding_horizontal: 12.0,
767                padding_vertical: 6.0,
768                radius: 4.0,
769                icon_spacing: 6.0,
770                disabled_opacity: 0.5,
771                shadow: false,
772                font: f.clone(),
773            },
774            input: ResolvedInputTheme {
775                background: c,
776                foreground: c,
777                border: c,
778                placeholder: c,
779                caret: c,
780                selection: c,
781                selection_foreground: c,
782                min_height: 28.0,
783                padding_horizontal: 8.0,
784                padding_vertical: 4.0,
785                radius: 4.0,
786                border_width: 1.0,
787                font: f.clone(),
788            },
789            checkbox: ResolvedCheckboxTheme {
790                checked_background: c,
791                indicator_size: 18.0,
792                spacing: 6.0,
793                radius: 2.0,
794                border_width: 1.0,
795            },
796            menu: ResolvedMenuTheme {
797                background: c,
798                foreground: c,
799                separator: c,
800                item_height: 28.0,
801                padding_horizontal: 8.0,
802                padding_vertical: 4.0,
803                icon_spacing: 6.0,
804                font: f.clone(),
805            },
806            tooltip: ResolvedTooltipTheme {
807                background: c,
808                foreground: c,
809                padding_horizontal: 6.0,
810                padding_vertical: 4.0,
811                max_width: 300.0,
812                radius: 4.0,
813                font: f.clone(),
814            },
815            scrollbar: ResolvedScrollbarTheme {
816                track: c,
817                thumb: c,
818                thumb_hover: c,
819                width: 14.0,
820                min_thumb_height: 20.0,
821                slider_width: 8.0,
822                overlay_mode: false,
823            },
824            slider: ResolvedSliderTheme {
825                fill: c,
826                track: c,
827                thumb: c,
828                track_height: 4.0,
829                thumb_size: 16.0,
830                tick_length: 6.0,
831            },
832            progress_bar: ResolvedProgressBarTheme {
833                fill: c,
834                track: c,
835                height: 6.0,
836                min_width: 100.0,
837                radius: 3.0,
838            },
839            tab: ResolvedTabTheme {
840                background: c,
841                foreground: c,
842                active_background: c,
843                active_foreground: c,
844                bar_background: c,
845                min_width: 60.0,
846                min_height: 32.0,
847                padding_horizontal: 12.0,
848                padding_vertical: 6.0,
849            },
850            sidebar: ResolvedSidebarTheme {
851                background: c,
852                foreground: c,
853            },
854            toolbar: ResolvedToolbarTheme {
855                height: 40.0,
856                item_spacing: 4.0,
857                padding: 4.0,
858                font: f.clone(),
859            },
860            status_bar: ResolvedStatusBarTheme { font: f.clone() },
861            list: ResolvedListTheme {
862                background: c,
863                foreground: c,
864                alternate_row: c,
865                selection: c,
866                selection_foreground: c,
867                header_background: c,
868                header_foreground: c,
869                grid_color: c,
870                item_height: 28.0,
871                padding_horizontal: 8.0,
872                padding_vertical: 4.0,
873            },
874            popover: ResolvedPopoverTheme {
875                background: c,
876                foreground: c,
877                border: c,
878                radius: 6.0,
879            },
880            splitter: ResolvedSplitterTheme { width: 4.0 },
881            separator: ResolvedSeparatorTheme { color: c },
882            switch: ResolvedSwitchTheme {
883                checked_background: c,
884                unchecked_background: c,
885                thumb_background: c,
886                track_width: 40.0,
887                track_height: 20.0,
888                thumb_size: 14.0,
889                track_radius: 10.0,
890            },
891            dialog: ResolvedDialogTheme {
892                min_width: 320.0,
893                max_width: 600.0,
894                min_height: 200.0,
895                max_height: 800.0,
896                content_padding: 16.0,
897                button_spacing: 8.0,
898                radius: 8.0,
899                icon_size: 22.0,
900                button_order: DialogButtonOrder::TrailingAffirmative,
901                title_font: f.clone(),
902            },
903            spinner: ResolvedSpinnerTheme {
904                fill: c,
905                diameter: 24.0,
906                min_size: 16.0,
907                stroke_width: 2.0,
908            },
909            combo_box: ResolvedComboBoxTheme {
910                min_height: 28.0,
911                min_width: 80.0,
912                padding_horizontal: 8.0,
913                arrow_size: 12.0,
914                arrow_area_width: 20.0,
915                radius: 4.0,
916            },
917            segmented_control: ResolvedSegmentedControlTheme {
918                segment_height: 28.0,
919                separator_width: 1.0,
920                padding_horizontal: 12.0,
921                radius: 4.0,
922            },
923            card: ResolvedCardTheme {
924                background: c,
925                border: c,
926                radius: 8.0,
927                padding: 12.0,
928                shadow: true,
929            },
930            expander: ResolvedExpanderTheme {
931                header_height: 32.0,
932                arrow_size: 12.0,
933                content_padding: 8.0,
934                radius: 4.0,
935            },
936            link: ResolvedLinkTheme {
937                color: c,
938                visited: c,
939                background: c,
940                hover_bg: c,
941                underline: true,
942            },
943            icon_set: crate::IconSet::Freedesktop,
944            icon_theme: "breeze".into(),
945        };
946
947        let theme2 = theme.clone();
948        assert_eq!(theme, theme2);
949        let dbg = format!("{theme:?}");
950        assert!(dbg.contains("ResolvedThemeVariant"));
951    }
952}