1use serde::{Deserialize, Serialize};
4
5#[serde_with::skip_serializing_none]
10#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
11#[serde(default)]
12#[non_exhaustive]
13pub struct ButtonMetrics {
14 pub min_width: Option<f32>,
16 pub min_height: Option<f32>,
18 pub padding_horizontal: Option<f32>,
20 pub padding_vertical: Option<f32>,
22 pub icon_spacing: Option<f32>,
24}
25
26impl_merge!(ButtonMetrics {
27 option { min_width, min_height, padding_horizontal, padding_vertical, icon_spacing }
28});
29
30#[serde_with::skip_serializing_none]
35#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
36#[serde(default)]
37#[non_exhaustive]
38pub struct CheckboxMetrics {
39 pub indicator_size: Option<f32>,
41 pub spacing: Option<f32>,
43}
44
45impl_merge!(CheckboxMetrics {
46 option { indicator_size, spacing }
47});
48
49#[serde_with::skip_serializing_none]
54#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
55#[serde(default)]
56#[non_exhaustive]
57pub struct InputMetrics {
58 pub min_height: Option<f32>,
60 pub padding_horizontal: Option<f32>,
62 pub padding_vertical: Option<f32>,
64}
65
66impl_merge!(InputMetrics {
67 option { min_height, padding_horizontal, padding_vertical }
68});
69
70#[serde_with::skip_serializing_none]
75#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
76#[serde(default)]
77#[non_exhaustive]
78pub struct ScrollbarMetrics {
79 pub width: Option<f32>,
81 pub min_thumb_height: Option<f32>,
83 pub slider_width: Option<f32>,
85}
86
87impl_merge!(ScrollbarMetrics {
88 option { width, min_thumb_height, slider_width }
89});
90
91#[serde_with::skip_serializing_none]
96#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
97#[serde(default)]
98#[non_exhaustive]
99pub struct SliderMetrics {
100 pub track_height: Option<f32>,
102 pub thumb_size: Option<f32>,
104 pub tick_length: Option<f32>,
106}
107
108impl_merge!(SliderMetrics {
109 option { track_height, thumb_size, tick_length }
110});
111
112#[serde_with::skip_serializing_none]
117#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
118#[serde(default)]
119#[non_exhaustive]
120pub struct ProgressBarMetrics {
121 pub height: Option<f32>,
123 pub min_width: Option<f32>,
125}
126
127impl_merge!(ProgressBarMetrics {
128 option { height, min_width }
129});
130
131#[serde_with::skip_serializing_none]
136#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
137#[serde(default)]
138#[non_exhaustive]
139pub struct TabMetrics {
140 pub min_width: Option<f32>,
142 pub min_height: Option<f32>,
144 pub padding_horizontal: Option<f32>,
146 pub padding_vertical: Option<f32>,
148}
149
150impl_merge!(TabMetrics {
151 option { min_width, min_height, padding_horizontal, padding_vertical }
152});
153
154#[serde_with::skip_serializing_none]
159#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
160#[serde(default)]
161#[non_exhaustive]
162pub struct MenuItemMetrics {
163 pub height: Option<f32>,
165 pub padding_horizontal: Option<f32>,
167 pub padding_vertical: Option<f32>,
169 pub icon_spacing: Option<f32>,
171}
172
173impl_merge!(MenuItemMetrics {
174 option { height, padding_horizontal, padding_vertical, icon_spacing }
175});
176
177#[serde_with::skip_serializing_none]
182#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
183#[serde(default)]
184#[non_exhaustive]
185pub struct TooltipMetrics {
186 pub padding: Option<f32>,
188 pub max_width: Option<f32>,
190}
191
192impl_merge!(TooltipMetrics {
193 option { padding, max_width }
194});
195
196#[serde_with::skip_serializing_none]
201#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
202#[serde(default)]
203#[non_exhaustive]
204pub struct ListItemMetrics {
205 pub height: Option<f32>,
207 pub padding_horizontal: Option<f32>,
209 pub padding_vertical: Option<f32>,
211}
212
213impl_merge!(ListItemMetrics {
214 option { height, padding_horizontal, padding_vertical }
215});
216
217#[serde_with::skip_serializing_none]
222#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
223#[serde(default)]
224#[non_exhaustive]
225pub struct ToolbarMetrics {
226 pub height: Option<f32>,
228 pub item_spacing: Option<f32>,
230 pub padding: Option<f32>,
232}
233
234impl_merge!(ToolbarMetrics {
235 option { height, item_spacing, padding }
236});
237
238#[serde_with::skip_serializing_none]
243#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
244#[serde(default)]
245#[non_exhaustive]
246pub struct SplitterMetrics {
247 pub width: Option<f32>,
249}
250
251impl_merge!(SplitterMetrics {
252 option { width }
253});
254
255#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
262#[serde(default)]
263#[non_exhaustive]
264pub struct WidgetMetrics {
265 #[serde(default, skip_serializing_if = "ButtonMetrics::is_empty")]
267 pub button: ButtonMetrics,
268
269 #[serde(default, skip_serializing_if = "CheckboxMetrics::is_empty")]
271 pub checkbox: CheckboxMetrics,
272
273 #[serde(default, skip_serializing_if = "InputMetrics::is_empty")]
275 pub input: InputMetrics,
276
277 #[serde(default, skip_serializing_if = "ScrollbarMetrics::is_empty")]
279 pub scrollbar: ScrollbarMetrics,
280
281 #[serde(default, skip_serializing_if = "SliderMetrics::is_empty")]
283 pub slider: SliderMetrics,
284
285 #[serde(default, skip_serializing_if = "ProgressBarMetrics::is_empty")]
287 pub progress_bar: ProgressBarMetrics,
288
289 #[serde(default, skip_serializing_if = "TabMetrics::is_empty")]
291 pub tab: TabMetrics,
292
293 #[serde(default, skip_serializing_if = "MenuItemMetrics::is_empty")]
295 pub menu_item: MenuItemMetrics,
296
297 #[serde(default, skip_serializing_if = "TooltipMetrics::is_empty")]
299 pub tooltip: TooltipMetrics,
300
301 #[serde(default, skip_serializing_if = "ListItemMetrics::is_empty")]
303 pub list_item: ListItemMetrics,
304
305 #[serde(default, skip_serializing_if = "ToolbarMetrics::is_empty")]
307 pub toolbar: ToolbarMetrics,
308
309 #[serde(default, skip_serializing_if = "SplitterMetrics::is_empty")]
311 pub splitter: SplitterMetrics,
312}
313
314impl_merge!(WidgetMetrics {
315 nested { button, checkbox, input, scrollbar, slider, progress_bar, tab, menu_item, tooltip, list_item, toolbar, splitter }
316});
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 #[test]
323 fn default_is_empty() {
324 assert!(WidgetMetrics::default().is_empty());
325 }
326
327 #[test]
328 fn not_empty_when_button_field_set() {
329 let mut wm = WidgetMetrics::default();
330 wm.button.min_width = Some(80.0);
331 assert!(!wm.is_empty());
332 }
333
334 #[test]
335 fn merge_overlays_some_fields_preserves_none() {
336 let mut base = WidgetMetrics::default();
337 base.button.min_width = Some(80.0);
338 base.button.min_height = Some(30.0);
339
340 let mut overlay = WidgetMetrics::default();
341 overlay.button.min_width = Some(100.0); overlay.button.padding_horizontal = Some(12.0); base.merge(&overlay);
345
346 assert_eq!(base.button.min_width, Some(100.0));
348 assert_eq!(base.button.min_height, Some(30.0));
350 assert_eq!(base.button.padding_horizontal, Some(12.0));
352 }
353
354 #[test]
355 fn merge_nested_sub_struct_recursion() {
356 let mut base = WidgetMetrics::default();
357 base.button.min_width = Some(80.0);
358 base.scrollbar.width = Some(21.0);
359
360 let mut overlay = WidgetMetrics::default();
361 overlay.button.padding_horizontal = Some(6.0);
362 overlay.scrollbar.slider_width = Some(8.0);
363
364 base.merge(&overlay);
365
366 assert_eq!(base.button.min_width, Some(80.0));
368 assert_eq!(base.button.padding_horizontal, Some(6.0));
369 assert_eq!(base.scrollbar.width, Some(21.0));
371 assert_eq!(base.scrollbar.slider_width, Some(8.0));
372 }
373
374 #[test]
375 fn serde_toml_round_trip_all_populated() {
376 let wm = WidgetMetrics {
377 button: ButtonMetrics {
378 min_width: Some(80.0),
379 min_height: Some(30.0),
380 padding_horizontal: Some(6.0),
381 padding_vertical: Some(4.0),
382 icon_spacing: Some(4.0),
383 },
384 checkbox: CheckboxMetrics {
385 indicator_size: Some(20.0),
386 spacing: Some(4.0),
387 },
388 input: InputMetrics {
389 min_height: Some(30.0),
390 padding_horizontal: Some(6.0),
391 padding_vertical: Some(4.0),
392 },
393 scrollbar: ScrollbarMetrics {
394 width: Some(21.0),
395 min_thumb_height: Some(20.0),
396 slider_width: Some(8.0),
397 },
398 slider: SliderMetrics {
399 track_height: Some(6.0),
400 thumb_size: Some(20.0),
401 tick_length: Some(8.0),
402 },
403 progress_bar: ProgressBarMetrics {
404 height: Some(6.0),
405 min_width: Some(14.0),
406 },
407 tab: TabMetrics {
408 min_width: Some(80.0),
409 min_height: Some(30.0),
410 padding_horizontal: Some(8.0),
411 padding_vertical: Some(4.0),
412 },
413 menu_item: MenuItemMetrics {
414 height: Some(22.0),
415 padding_horizontal: Some(4.0),
416 padding_vertical: Some(4.0),
417 icon_spacing: Some(8.0),
418 },
419 tooltip: TooltipMetrics {
420 padding: Some(3.0),
421 max_width: Some(300.0),
422 },
423 list_item: ListItemMetrics {
424 height: Some(24.0),
425 padding_horizontal: Some(2.0),
426 padding_vertical: Some(1.0),
427 },
428 toolbar: ToolbarMetrics {
429 height: Some(38.0),
430 item_spacing: Some(0.0),
431 padding: Some(6.0),
432 },
433 splitter: SplitterMetrics { width: Some(1.0) },
434 };
435
436 let toml_str = toml::to_string(&wm).unwrap();
437 let deserialized: WidgetMetrics = toml::from_str(&toml_str).unwrap();
438 assert_eq!(deserialized, wm);
439 }
440
441 #[test]
442 fn serde_toml_round_trip_sparse() {
443 let mut wm = WidgetMetrics::default();
444 wm.button.min_width = Some(80.0);
445 wm.button.min_height = Some(30.0);
446
447 let toml_str = toml::to_string(&wm).unwrap();
448 let deserialized: WidgetMetrics = toml::from_str(&toml_str).unwrap();
449 assert_eq!(deserialized, wm);
450 assert_eq!(deserialized.button.min_width, Some(80.0));
451 assert!(deserialized.checkbox.is_empty());
453 assert!(deserialized.scrollbar.is_empty());
454 }
455
456 #[test]
457 fn empty_sub_structs_omitted_from_serialized_output() {
458 let mut wm = WidgetMetrics::default();
459 wm.button.min_width = Some(80.0);
460
461 let toml_str = toml::to_string(&wm).unwrap();
462
463 assert!(toml_str.contains("button"));
465 assert!(!toml_str.contains("checkbox"));
467 assert!(!toml_str.contains("scrollbar"));
468 assert!(!toml_str.contains("slider"));
469 assert!(!toml_str.contains("progress_bar"));
470 assert!(!toml_str.contains("tab"));
471 assert!(!toml_str.contains("menu_item"));
472 assert!(!toml_str.contains("tooltip"));
473 assert!(!toml_str.contains("list_item"));
474 assert!(!toml_str.contains("toolbar"));
475 assert!(!toml_str.contains("splitter"));
476 }
477
478 #[test]
479 fn deserialize_missing_widget_metrics_produces_default() {
480 let toml_str = "";
482 let deserialized: WidgetMetrics = toml::from_str(toml_str).unwrap();
483 assert!(deserialized.is_empty());
484 assert_eq!(deserialized, WidgetMetrics::default());
485 }
486
487 #[test]
490 fn button_metrics_is_empty_and_merge() {
491 assert!(ButtonMetrics::default().is_empty());
492
493 let mut base = ButtonMetrics {
494 min_width: Some(80.0),
495 ..Default::default()
496 };
497 let overlay = ButtonMetrics {
498 min_height: Some(30.0),
499 ..Default::default()
500 };
501 base.merge(&overlay);
502 assert_eq!(base.min_width, Some(80.0));
503 assert_eq!(base.min_height, Some(30.0));
504 assert!(!base.is_empty());
505 }
506
507 #[test]
508 fn checkbox_metrics_is_empty_and_merge() {
509 assert!(CheckboxMetrics::default().is_empty());
510
511 let mut base = CheckboxMetrics::default();
512 let overlay = CheckboxMetrics {
513 indicator_size: Some(20.0),
514 spacing: Some(4.0),
515 };
516 base.merge(&overlay);
517 assert_eq!(base.indicator_size, Some(20.0));
518 assert_eq!(base.spacing, Some(4.0));
519 }
520
521 #[test]
522 fn scrollbar_metrics_is_empty_and_merge() {
523 assert!(ScrollbarMetrics::default().is_empty());
524
525 let mut base = ScrollbarMetrics {
526 width: Some(21.0),
527 ..Default::default()
528 };
529 let overlay = ScrollbarMetrics {
530 slider_width: Some(8.0),
531 ..Default::default()
532 };
533 base.merge(&overlay);
534 assert_eq!(base.width, Some(21.0));
535 assert_eq!(base.slider_width, Some(8.0));
536 }
537
538 #[test]
539 fn splitter_metrics_is_empty_and_merge() {
540 assert!(SplitterMetrics::default().is_empty());
541
542 let mut base = SplitterMetrics::default();
543 let overlay = SplitterMetrics { width: Some(1.0) };
544 base.merge(&overlay);
545 assert_eq!(base.width, Some(1.0));
546 }
547}