1use serde::{Deserialize, Serialize};
4
5#[serde_with::skip_serializing_none]
10#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
11#[serde(default)]
12pub struct FontSpec {
13 pub family: Option<String>,
15 pub size: Option<f32>,
17 pub weight: Option<u16>,
19}
20
21impl_merge!(FontSpec {
22 option { family, size, weight }
23});
24
25#[serde_with::skip_serializing_none]
30#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
31#[serde(default)]
32pub struct TextScaleEntry {
33 pub size: Option<f32>,
35 pub weight: Option<u16>,
37 pub line_height: Option<f32>,
40}
41
42impl_merge!(TextScaleEntry {
43 option { size, weight, line_height }
44});
45
46#[serde_with::skip_serializing_none]
51#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
52#[serde(default)]
53pub struct TextScale {
54 pub caption: Option<TextScaleEntry>,
56 pub section_heading: Option<TextScaleEntry>,
58 pub dialog_title: Option<TextScaleEntry>,
60 pub display: Option<TextScaleEntry>,
62}
63
64impl_merge!(TextScale {
65 optional_nested { caption, section_heading, dialog_title, display }
66});
67
68#[cfg(test)]
69#[allow(clippy::unwrap_used, clippy::expect_used)]
70mod tests {
71 use super::*;
72
73 #[test]
76 fn font_spec_default_is_empty() {
77 assert!(FontSpec::default().is_empty());
78 }
79
80 #[test]
81 fn font_spec_not_empty_when_family_set() {
82 let fs = FontSpec {
83 family: Some("Inter".into()),
84 ..Default::default()
85 };
86 assert!(!fs.is_empty());
87 }
88
89 #[test]
90 fn font_spec_not_empty_when_size_set() {
91 let fs = FontSpec {
92 size: Some(14.0),
93 ..Default::default()
94 };
95 assert!(!fs.is_empty());
96 }
97
98 #[test]
99 fn font_spec_not_empty_when_weight_set() {
100 let fs = FontSpec {
101 weight: Some(700),
102 ..Default::default()
103 };
104 assert!(!fs.is_empty());
105 }
106
107 #[test]
108 fn font_spec_toml_round_trip() {
109 let fs = FontSpec {
110 family: Some("Inter".into()),
111 size: Some(14.0),
112 weight: Some(400),
113 };
114 let toml_str = toml::to_string(&fs).unwrap();
115 let deserialized: FontSpec = toml::from_str(&toml_str).unwrap();
116 assert_eq!(deserialized, fs);
117 }
118
119 #[test]
120 fn font_spec_toml_round_trip_partial() {
121 let fs = FontSpec {
122 family: Some("Inter".into()),
123 size: None,
124 weight: None,
125 };
126 let toml_str = toml::to_string(&fs).unwrap();
127 let deserialized: FontSpec = toml::from_str(&toml_str).unwrap();
128 assert_eq!(deserialized, fs);
129 assert!(deserialized.size.is_none());
130 assert!(deserialized.weight.is_none());
131 }
132
133 #[test]
134 fn font_spec_merge_overlay_family_replaces_base() {
135 let mut base = FontSpec {
136 family: Some("Noto Sans".into()),
137 size: Some(12.0),
138 weight: None,
139 };
140 let overlay = FontSpec {
141 family: Some("Inter".into()),
142 size: None,
143 weight: None,
144 };
145 base.merge(&overlay);
146 assert_eq!(base.family.as_deref(), Some("Inter"));
147 assert_eq!(base.size, Some(12.0));
149 }
150
151 #[test]
152 fn font_spec_merge_none_preserves_base() {
153 let mut base = FontSpec {
154 family: Some("Noto Sans".into()),
155 size: Some(12.0),
156 weight: Some(400),
157 };
158 let overlay = FontSpec::default();
159 base.merge(&overlay);
160 assert_eq!(base.family.as_deref(), Some("Noto Sans"));
161 assert_eq!(base.size, Some(12.0));
162 assert_eq!(base.weight, Some(400));
163 }
164
165 #[test]
168 fn text_scale_entry_default_is_empty() {
169 assert!(TextScaleEntry::default().is_empty());
170 }
171
172 #[test]
173 fn text_scale_entry_toml_round_trip() {
174 let entry = TextScaleEntry {
175 size: Some(12.0),
176 weight: Some(400),
177 line_height: Some(1.4),
178 };
179 let toml_str = toml::to_string(&entry).unwrap();
180 let deserialized: TextScaleEntry = toml::from_str(&toml_str).unwrap();
181 assert_eq!(deserialized, entry);
182 }
183
184 #[test]
185 fn text_scale_entry_merge_overlay_wins() {
186 let mut base = TextScaleEntry {
187 size: Some(12.0),
188 weight: Some(400),
189 line_height: None,
190 };
191 let overlay = TextScaleEntry {
192 size: None,
193 weight: Some(700),
194 line_height: Some(1.5),
195 };
196 base.merge(&overlay);
197 assert_eq!(base.size, Some(12.0)); assert_eq!(base.weight, Some(700)); assert_eq!(base.line_height, Some(1.5)); }
201
202 #[test]
205 fn text_scale_default_is_empty() {
206 assert!(TextScale::default().is_empty());
207 }
208
209 #[test]
210 fn text_scale_not_empty_when_entry_set() {
211 let ts = TextScale {
212 caption: Some(TextScaleEntry {
213 size: Some(11.0),
214 ..Default::default()
215 }),
216 ..Default::default()
217 };
218 assert!(!ts.is_empty());
219 }
220
221 #[test]
222 fn text_scale_toml_round_trip() {
223 let ts = TextScale {
224 caption: Some(TextScaleEntry {
225 size: Some(11.0),
226 weight: Some(400),
227 line_height: Some(1.3),
228 }),
229 section_heading: Some(TextScaleEntry {
230 size: Some(14.0),
231 weight: Some(600),
232 line_height: Some(1.4),
233 }),
234 dialog_title: Some(TextScaleEntry {
235 size: Some(16.0),
236 weight: Some(700),
237 line_height: Some(1.2),
238 }),
239 display: Some(TextScaleEntry {
240 size: Some(24.0),
241 weight: Some(300),
242 line_height: Some(1.1),
243 }),
244 };
245 let toml_str = toml::to_string(&ts).unwrap();
246 let deserialized: TextScale = toml::from_str(&toml_str).unwrap();
247 assert_eq!(deserialized, ts);
248 }
249
250 #[test]
251 fn text_scale_merge_some_plus_some_merges_inner() {
252 let mut base = TextScale {
253 caption: Some(TextScaleEntry {
254 size: Some(11.0),
255 weight: Some(400),
256 line_height: None,
257 }),
258 ..Default::default()
259 };
260 let overlay = TextScale {
261 caption: Some(TextScaleEntry {
262 size: None,
263 weight: Some(600),
264 line_height: Some(1.3),
265 }),
266 ..Default::default()
267 };
268 base.merge(&overlay);
269 let cap = base.caption.as_ref().unwrap();
270 assert_eq!(cap.size, Some(11.0)); assert_eq!(cap.weight, Some(600)); assert_eq!(cap.line_height, Some(1.3)); }
274
275 #[test]
276 fn text_scale_merge_none_plus_some_clones_overlay() {
277 let mut base = TextScale::default();
278 let overlay = TextScale {
279 section_heading: Some(TextScaleEntry {
280 size: Some(14.0),
281 ..Default::default()
282 }),
283 ..Default::default()
284 };
285 base.merge(&overlay);
286 assert!(base.section_heading.is_some());
287 assert_eq!(base.section_heading.unwrap().size, Some(14.0));
288 }
289
290 #[test]
291 fn text_scale_merge_none_preserves_base_entry() {
292 let mut base = TextScale {
293 display: Some(TextScaleEntry {
294 size: Some(24.0),
295 ..Default::default()
296 }),
297 ..Default::default()
298 };
299 let overlay = TextScale::default();
300 base.merge(&overlay);
301 assert!(base.display.is_some());
302 assert_eq!(base.display.unwrap().size, Some(24.0));
303 }
304}