native_theme/model/
border.rs1use crate::Rgba;
4use serde::{Deserialize, Serialize};
5
6#[serde_with::skip_serializing_none]
11#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
12#[serde(default)]
13pub struct BorderSpec {
14 pub color: Option<Rgba>,
16 #[serde(rename = "corner_radius_px")]
18 pub corner_radius: Option<f32>,
19 #[serde(rename = "corner_radius_lg_px")]
21 pub corner_radius_lg: Option<f32>,
22 #[serde(rename = "line_width_px")]
24 pub line_width: Option<f32>,
25 pub opacity: Option<f32>,
27 pub shadow_enabled: Option<bool>,
29 #[serde(rename = "padding_horizontal_px")]
31 pub padding_horizontal: Option<f32>,
32 #[serde(rename = "padding_vertical_px")]
34 pub padding_vertical: Option<f32>,
35}
36
37impl BorderSpec {
38 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#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
60pub struct ResolvedBorderSpec {
61 pub color: Rgba,
63 pub corner_radius: f32,
65 pub corner_radius_lg: f32,
67 pub line_width: f32,
69 pub opacity: f32,
71 pub shadow_enabled: bool,
73 pub padding_horizontal: f32,
75 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 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}