Skip to main content

native_theme/model/
spacing.rs

1// Theme spacing scale
2
3use serde::{Deserialize, Serialize};
4
5/// Named spacing scale from extra-extra-small to extra-extra-large.
6///
7/// All values are in logical pixels. The scale provides a consistent
8/// spacing vocabulary across platforms.
9#[serde_with::skip_serializing_none]
10#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
11#[serde(default)]
12#[non_exhaustive]
13pub struct ThemeSpacing {
14    /// Extra-extra-small spacing (e.g., 2px).
15    pub xxs: Option<f32>,
16
17    /// Extra-small spacing (e.g., 4px).
18    pub xs: Option<f32>,
19
20    /// Small spacing (e.g., 8px).
21    pub s: Option<f32>,
22
23    /// Medium spacing (e.g., 12px).
24    pub m: Option<f32>,
25
26    /// Large spacing (e.g., 16px).
27    pub l: Option<f32>,
28
29    /// Extra-large spacing (e.g., 24px).
30    pub xl: Option<f32>,
31
32    /// Extra-extra-large spacing (e.g., 32px).
33    pub xxl: Option<f32>,
34}
35
36impl_merge!(ThemeSpacing {
37    option { xxs, xs, s, m, l, xl, xxl }
38});
39
40#[cfg(test)]
41#[allow(clippy::unwrap_used, clippy::expect_used)]
42mod tests {
43    use super::*;
44
45    #[test]
46    fn default_is_empty() {
47        assert!(ThemeSpacing::default().is_empty());
48    }
49
50    #[test]
51    fn not_empty_when_field_set() {
52        let s = ThemeSpacing {
53            m: Some(12.0),
54            ..Default::default()
55        };
56        assert!(!s.is_empty());
57    }
58
59    #[test]
60    fn merge_some_replaces_none() {
61        let mut base = ThemeSpacing::default();
62        let overlay = ThemeSpacing {
63            s: Some(8.0),
64            m: Some(12.0),
65            l: Some(16.0),
66            ..Default::default()
67        };
68        base.merge(&overlay);
69        assert_eq!(base.s, Some(8.0));
70        assert_eq!(base.m, Some(12.0));
71        assert_eq!(base.l, Some(16.0));
72    }
73
74    #[test]
75    fn merge_none_preserves_base() {
76        let mut base = ThemeSpacing {
77            xxs: Some(2.0),
78            xs: Some(4.0),
79            ..Default::default()
80        };
81        let overlay = ThemeSpacing::default();
82        base.merge(&overlay);
83        assert_eq!(base.xxs, Some(2.0));
84        assert_eq!(base.xs, Some(4.0));
85    }
86
87    #[test]
88    fn serde_toml_round_trip() {
89        let spacing = ThemeSpacing {
90            xxs: Some(2.0),
91            xs: Some(4.0),
92            s: Some(8.0),
93            m: Some(12.0),
94            l: Some(16.0),
95            xl: Some(24.0),
96            xxl: Some(32.0),
97        };
98        let toml_str = toml::to_string(&spacing).unwrap();
99        let deserialized: ThemeSpacing = toml::from_str(&toml_str).unwrap();
100        assert_eq!(deserialized, spacing);
101    }
102}