Skip to main content

native_theme/model/
geometry.rs

1// Theme geometry configuration (border radius, widths, opacities)
2
3use serde::{Deserialize, Serialize};
4
5/// Geometric properties for UI elements.
6///
7/// Controls border radius, frame widths, and opacity values
8/// that vary across platform themes.
9#[serde_with::skip_serializing_none]
10#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
11#[serde(default)]
12#[non_exhaustive]
13pub struct ThemeGeometry {
14    /// Corner radius for rounded elements (in logical pixels).
15    pub radius: Option<f32>,
16
17    /// Larger corner radius for dialogs, cards, and panels (in logical pixels).
18    pub radius_lg: Option<f32>,
19
20    /// Window/widget frame border width (in logical pixels).
21    pub frame_width: Option<f32>,
22
23    /// Opacity multiplier for disabled elements (0.0 = invisible, 1.0 = fully opaque).
24    pub disabled_opacity: Option<f32>,
25
26    /// Opacity multiplier for borders (0.0 = invisible, 1.0 = fully opaque).
27    pub border_opacity: Option<f32>,
28
29    /// Scrollbar track width (in logical pixels).
30    pub scroll_width: Option<f32>,
31
32    /// Whether the platform uses drop shadows on windows and popups.
33    pub shadow: Option<bool>,
34}
35
36impl_merge!(ThemeGeometry {
37    option { radius, radius_lg, frame_width, disabled_opacity, border_opacity, scroll_width, shadow }
38});
39
40#[cfg(test)]
41mod tests {
42    use super::*;
43
44    #[test]
45    fn default_is_empty() {
46        assert!(ThemeGeometry::default().is_empty());
47    }
48
49    #[test]
50    fn not_empty_when_field_set() {
51        let g = ThemeGeometry {
52            radius: Some(4.0),
53            ..Default::default()
54        };
55        assert!(!g.is_empty());
56    }
57
58    #[test]
59    fn merge_some_replaces_none() {
60        let mut base = ThemeGeometry::default();
61        let overlay = ThemeGeometry {
62            radius: Some(8.0),
63            frame_width: Some(1.0),
64            ..Default::default()
65        };
66        base.merge(&overlay);
67        assert_eq!(base.radius, Some(8.0));
68        assert_eq!(base.frame_width, Some(1.0));
69    }
70
71    #[test]
72    fn merge_none_preserves_base() {
73        let mut base = ThemeGeometry {
74            radius: Some(4.0),
75            disabled_opacity: Some(0.5),
76            ..Default::default()
77        };
78        let overlay = ThemeGeometry::default();
79        base.merge(&overlay);
80        assert_eq!(base.radius, Some(4.0));
81        assert_eq!(base.disabled_opacity, Some(0.5));
82    }
83
84    #[test]
85    fn serde_toml_round_trip() {
86        let geom = ThemeGeometry {
87            radius: Some(8.0),
88            radius_lg: Some(12.0),
89            frame_width: Some(1.0),
90            disabled_opacity: Some(0.4),
91            border_opacity: Some(0.6),
92            scroll_width: Some(12.0),
93            shadow: Some(true),
94        };
95        let toml_str = toml::to_string(&geom).unwrap();
96        let deserialized: ThemeGeometry = toml::from_str(&toml_str).unwrap();
97        assert_eq!(deserialized, geom);
98    }
99
100    #[test]
101    fn new_fields_default_to_none() {
102        let g = ThemeGeometry::default();
103        assert!(g.radius_lg.is_none());
104        assert!(g.shadow.is_none());
105    }
106
107    #[test]
108    fn merge_new_fields() {
109        let mut base = ThemeGeometry::default();
110        let overlay = ThemeGeometry {
111            radius_lg: Some(16.0),
112            shadow: Some(true),
113            ..Default::default()
114        };
115        base.merge(&overlay);
116        assert_eq!(base.radius_lg, Some(16.0));
117        assert_eq!(base.shadow, Some(true));
118
119        // Verify overlay None doesn't overwrite existing
120        let mut base2 = ThemeGeometry {
121            radius_lg: Some(12.0),
122            shadow: Some(false),
123            ..Default::default()
124        };
125        let overlay2 = ThemeGeometry::default();
126        base2.merge(&overlay2);
127        assert_eq!(base2.radius_lg, Some(12.0));
128        assert_eq!(base2.shadow, Some(false));
129    }
130}