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)]
12pub struct IconSizes {
13    /// Icon size for toolbar buttons (e.g., 24px).
14    #[serde(rename = "toolbar_px")]
15    pub toolbar: Option<f32>,
16    /// Small icon size for inline use (e.g., 16px).
17    #[serde(rename = "small_px")]
18    pub small: Option<f32>,
19    /// Large icon size for menus/lists (e.g., 32px).
20    #[serde(rename = "large_px")]
21    pub large: Option<f32>,
22    /// Icon size for dialog buttons (e.g., 22px).
23    #[serde(rename = "dialog_px")]
24    pub dialog: Option<f32>,
25    /// Icon size for panel headers (e.g., 20px).
26    #[serde(rename = "panel_px")]
27    pub panel: Option<f32>,
28}
29
30impl IconSizes {
31    /// All serialized field names for TOML linting (issue 3b).
32    pub const FIELD_NAMES: &[&str] = &[
33        "toolbar_px",
34        "small_px",
35        "large_px",
36        "dialog_px",
37        "panel_px",
38    ];
39}
40
41impl_merge!(IconSizes {
42    option { toolbar, small, large, dialog, panel }
43});
44
45#[cfg(test)]
46#[allow(clippy::unwrap_used, clippy::expect_used)]
47mod tests {
48    use super::*;
49
50    #[test]
51    fn icon_sizes_default_is_empty() {
52        assert!(IconSizes::default().is_empty());
53    }
54
55    #[test]
56    fn icon_sizes_not_empty_when_toolbar_set() {
57        let s = IconSizes {
58            toolbar: Some(24.0),
59            ..Default::default()
60        };
61        assert!(!s.is_empty());
62    }
63
64    #[test]
65    fn icon_sizes_not_empty_when_any_field_set() {
66        for sizes in [
67            IconSizes {
68                toolbar: Some(24.0),
69                ..Default::default()
70            },
71            IconSizes {
72                small: Some(16.0),
73                ..Default::default()
74            },
75            IconSizes {
76                large: Some(32.0),
77                ..Default::default()
78            },
79            IconSizes {
80                dialog: Some(22.0),
81                ..Default::default()
82            },
83            IconSizes {
84                panel: Some(20.0),
85                ..Default::default()
86            },
87        ] {
88            assert!(!sizes.is_empty());
89        }
90    }
91
92    #[test]
93    fn icon_sizes_merge_overlay_wins() {
94        let mut base = IconSizes {
95            toolbar: Some(24.0),
96            small: Some(16.0),
97            large: None,
98            dialog: None,
99            panel: None,
100        };
101        let overlay = IconSizes {
102            toolbar: None,
103            small: Some(18.0),
104            large: Some(32.0),
105            dialog: None,
106            panel: None,
107        };
108        base.merge(&overlay);
109        assert_eq!(base.toolbar, Some(24.0)); // preserved
110        assert_eq!(base.small, Some(18.0)); // overlay wins
111        assert_eq!(base.large, Some(32.0)); // overlay sets
112        assert_eq!(base.dialog, None);
113        assert_eq!(base.panel, None);
114    }
115
116    #[test]
117    fn icon_sizes_merge_none_preserves_base() {
118        let mut base = IconSizes {
119            toolbar: Some(24.0),
120            small: Some(16.0),
121            large: Some(32.0),
122            dialog: Some(22.0),
123            panel: Some(20.0),
124        };
125        let overlay = IconSizes::default();
126        base.merge(&overlay);
127        assert_eq!(base.toolbar, Some(24.0));
128        assert_eq!(base.small, Some(16.0));
129        assert_eq!(base.large, Some(32.0));
130        assert_eq!(base.dialog, Some(22.0));
131        assert_eq!(base.panel, Some(20.0));
132    }
133
134    #[test]
135    fn icon_sizes_toml_round_trip() {
136        let sizes = IconSizes {
137            toolbar: Some(24.0),
138            small: Some(16.0),
139            large: Some(32.0),
140            dialog: Some(22.0),
141            panel: Some(20.0),
142        };
143        let toml_str = toml::to_string(&sizes).unwrap();
144        let deserialized: IconSizes = toml::from_str(&toml_str).unwrap();
145        assert_eq!(deserialized, sizes);
146    }
147
148    #[test]
149    fn icon_sizes_toml_partial_round_trip() {
150        let sizes = IconSizes {
151            toolbar: Some(24.0),
152            ..Default::default()
153        };
154        let toml_str = toml::to_string(&sizes).unwrap();
155        let deserialized: IconSizes = toml::from_str(&toml_str).unwrap();
156        assert_eq!(deserialized, sizes);
157        assert!(deserialized.small.is_none());
158    }
159}