Skip to main content

native_theme/model/
icon_sizes.rs

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