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