Skip to main content

native_theme/model/
icon_sizes.rs

1// Icon size configuration
2
3use serde::{Deserialize, Serialize};
4
5/// Per-context icon sizes in logical pixels.
6///
7/// Defines the expected icon size for each visual context. All fields are
8/// optional to support partial overlays.
9#[serde_with::skip_serializing_none]
10#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
11#[serde(default)]
12#[non_exhaustive]
13pub struct IconSizes {
14    /// Icon size for toolbar buttons (e.g., 24px).
15    pub toolbar: Option<f32>,
16    /// Small icon size for inline use (e.g., 16px).
17    pub small: Option<f32>,
18    /// Large icon size for menus/lists (e.g., 32px).
19    pub large: Option<f32>,
20    /// Icon size for dialog buttons (e.g., 22px).
21    pub dialog: Option<f32>,
22    /// Icon size for panel headers (e.g., 20px).
23    pub panel: Option<f32>,
24}
25
26impl_merge!(IconSizes {
27    option { toolbar, small, large, dialog, panel }
28});
29
30#[cfg(test)]
31#[allow(clippy::unwrap_used, clippy::expect_used)]
32mod tests {
33    use super::*;
34
35    #[test]
36    fn icon_sizes_default_is_empty() {
37        assert!(IconSizes::default().is_empty());
38    }
39
40    #[test]
41    fn icon_sizes_not_empty_when_toolbar_set() {
42        let s = IconSizes {
43            toolbar: Some(24.0),
44            ..Default::default()
45        };
46        assert!(!s.is_empty());
47    }
48
49    #[test]
50    fn icon_sizes_not_empty_when_any_field_set() {
51        for sizes in [
52            IconSizes {
53                toolbar: Some(24.0),
54                ..Default::default()
55            },
56            IconSizes {
57                small: Some(16.0),
58                ..Default::default()
59            },
60            IconSizes {
61                large: Some(32.0),
62                ..Default::default()
63            },
64            IconSizes {
65                dialog: Some(22.0),
66                ..Default::default()
67            },
68            IconSizes {
69                panel: Some(20.0),
70                ..Default::default()
71            },
72        ] {
73            assert!(!sizes.is_empty());
74        }
75    }
76
77    #[test]
78    fn icon_sizes_merge_overlay_wins() {
79        let mut base = IconSizes {
80            toolbar: Some(24.0),
81            small: Some(16.0),
82            large: None,
83            dialog: None,
84            panel: None,
85        };
86        let overlay = IconSizes {
87            toolbar: None,
88            small: Some(18.0),
89            large: Some(32.0),
90            dialog: None,
91            panel: None,
92        };
93        base.merge(&overlay);
94        assert_eq!(base.toolbar, Some(24.0)); // preserved
95        assert_eq!(base.small, Some(18.0)); // overlay wins
96        assert_eq!(base.large, Some(32.0)); // overlay sets
97        assert_eq!(base.dialog, None);
98        assert_eq!(base.panel, None);
99    }
100
101    #[test]
102    fn icon_sizes_merge_none_preserves_base() {
103        let mut base = IconSizes {
104            toolbar: Some(24.0),
105            small: Some(16.0),
106            large: Some(32.0),
107            dialog: Some(22.0),
108            panel: Some(20.0),
109        };
110        let overlay = IconSizes::default();
111        base.merge(&overlay);
112        assert_eq!(base.toolbar, Some(24.0));
113        assert_eq!(base.small, Some(16.0));
114        assert_eq!(base.large, Some(32.0));
115        assert_eq!(base.dialog, Some(22.0));
116        assert_eq!(base.panel, Some(20.0));
117    }
118
119    #[test]
120    fn icon_sizes_toml_round_trip() {
121        let sizes = IconSizes {
122            toolbar: Some(24.0),
123            small: Some(16.0),
124            large: Some(32.0),
125            dialog: Some(22.0),
126            panel: Some(20.0),
127        };
128        let toml_str = toml::to_string(&sizes).unwrap();
129        let deserialized: IconSizes = toml::from_str(&toml_str).unwrap();
130        assert_eq!(deserialized, sizes);
131    }
132
133    #[test]
134    fn icon_sizes_toml_partial_round_trip() {
135        let sizes = IconSizes {
136            toolbar: Some(24.0),
137            ..Default::default()
138        };
139        let toml_str = toml::to_string(&sizes).unwrap();
140        let deserialized: IconSizes = toml::from_str(&toml_str).unwrap();
141        assert_eq!(deserialized, sizes);
142        assert!(deserialized.small.is_none());
143    }
144}