Skip to main content

native_theme/model/
border.rs

1// Border specification sub-struct for widget border properties
2
3use crate::Rgba;
4use serde::{Deserialize, Serialize};
5
6/// Border specification: color, geometry, and padding.
7///
8/// All fields are optional to support partial overlays -- a BorderSpec with
9/// only `color` set will only override the color when merged.
10#[serde_with::skip_serializing_none]
11#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
12#[serde(default)]
13pub struct BorderSpec {
14    /// Border color.
15    pub color: Option<Rgba>,
16    /// Corner radius in logical pixels.
17    #[serde(rename = "corner_radius_px")]
18    pub corner_radius: Option<f32>,
19    /// Large corner radius in logical pixels (defaults only).
20    #[serde(rename = "corner_radius_lg_px")]
21    pub corner_radius_lg: Option<f32>,
22    /// Border stroke width in logical pixels.
23    #[serde(rename = "line_width_px")]
24    pub line_width: Option<f32>,
25    /// Border alpha multiplier 0.0–1.0 (defaults only).
26    pub opacity: Option<f32>,
27    /// Whether the bordered element has a drop shadow.
28    pub shadow_enabled: Option<bool>,
29    /// Horizontal padding inside the border in logical pixels.
30    #[serde(rename = "padding_horizontal_px")]
31    pub padding_horizontal: Option<f32>,
32    /// Vertical padding inside the border in logical pixels.
33    #[serde(rename = "padding_vertical_px")]
34    pub padding_vertical: Option<f32>,
35}
36
37impl BorderSpec {
38    /// All serialized field names for BorderSpec, for TOML linting.
39    pub const FIELD_NAMES: &[&str] = &[
40        "color",
41        "corner_radius_px",
42        "corner_radius_lg_px",
43        "line_width_px",
44        "opacity",
45        "shadow_enabled",
46        "padding_horizontal_px",
47        "padding_vertical_px",
48    ];
49}
50
51impl_merge!(BorderSpec {
52    option { color, corner_radius, corner_radius_lg, line_width, opacity, shadow_enabled, padding_horizontal, padding_vertical }
53});
54
55/// A resolved (non-optional) border specification produced after theme resolution.
56///
57/// Unlike [`BorderSpec`], all fields are required (non-optional)
58/// because resolution has already filled in all defaults.
59#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
60pub struct ResolvedBorderSpec {
61    /// Border color.
62    pub color: Rgba,
63    /// Corner radius in logical pixels.
64    pub corner_radius: f32,
65    /// Large corner radius in logical pixels (defaults only).
66    pub corner_radius_lg: f32,
67    /// Border stroke width in logical pixels.
68    pub line_width: f32,
69    /// Border alpha multiplier 0.0–1.0 (defaults only).
70    pub opacity: f32,
71    /// Whether the bordered element has a drop shadow.
72    pub shadow_enabled: bool,
73    /// Horizontal padding inside the border in logical pixels.
74    pub padding_horizontal: f32,
75    /// Vertical padding inside the border in logical pixels.
76    pub padding_vertical: f32,
77}
78
79#[cfg(test)]
80#[allow(clippy::unwrap_used, clippy::expect_used)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn border_spec_default_is_empty() {
86        assert!(BorderSpec::default().is_empty());
87    }
88
89    #[test]
90    fn border_spec_not_empty_when_color_set() {
91        let bs = BorderSpec {
92            color: Some(Rgba::rgb(100, 100, 100)),
93            ..Default::default()
94        };
95        assert!(!bs.is_empty());
96    }
97
98    #[test]
99    fn border_spec_toml_round_trip_full() {
100        let bs = BorderSpec {
101            color: Some(Rgba::rgb(200, 200, 200)),
102            corner_radius: Some(4.0),
103            corner_radius_lg: Some(8.0),
104            line_width: Some(1.0),
105            opacity: Some(0.15),
106            shadow_enabled: Some(true),
107            padding_horizontal: Some(8.0),
108            padding_vertical: Some(6.0),
109        };
110        let toml_str = toml::to_string(&bs).unwrap();
111        let deserialized: BorderSpec = toml::from_str(&toml_str).unwrap();
112        assert_eq!(deserialized, bs);
113    }
114
115    #[test]
116    fn border_spec_toml_round_trip_partial() {
117        let bs = BorderSpec {
118            color: Some(Rgba::rgb(100, 100, 100)),
119            corner_radius: Some(8.0),
120            corner_radius_lg: None,
121            line_width: None,
122            opacity: None,
123            shadow_enabled: None,
124            padding_horizontal: None,
125            padding_vertical: None,
126        };
127        let toml_str = toml::to_string(&bs).unwrap();
128        let deserialized: BorderSpec = toml::from_str(&toml_str).unwrap();
129        assert_eq!(deserialized, bs);
130        assert!(deserialized.corner_radius_lg.is_none());
131        assert!(deserialized.line_width.is_none());
132        assert!(deserialized.opacity.is_none());
133        assert!(deserialized.shadow_enabled.is_none());
134        assert!(deserialized.padding_horizontal.is_none());
135        assert!(deserialized.padding_vertical.is_none());
136    }
137
138    #[test]
139    fn border_spec_merge_overlay_wins() {
140        let mut base = BorderSpec {
141            color: Some(Rgba::rgb(100, 100, 100)),
142            corner_radius: Some(4.0),
143            ..Default::default()
144        };
145        let overlay = BorderSpec {
146            color: Some(Rgba::rgb(200, 200, 200)),
147            ..Default::default()
148        };
149        base.merge(&overlay);
150        assert_eq!(base.color, Some(Rgba::rgb(200, 200, 200)));
151        // base corner_radius preserved since overlay corner_radius is None
152        assert_eq!(base.corner_radius, Some(4.0));
153    }
154
155    #[test]
156    fn resolved_border_spec_default() {
157        let rbs = ResolvedBorderSpec::default();
158        assert_eq!(rbs.padding_horizontal, 0.0);
159        assert_eq!(rbs.padding_vertical, 0.0);
160        assert_eq!(rbs.corner_radius, 0.0);
161        assert_eq!(rbs.corner_radius_lg, 0.0);
162        assert_eq!(rbs.line_width, 0.0);
163        assert_eq!(rbs.opacity, 0.0);
164        assert!(!rbs.shadow_enabled);
165    }
166}