Skip to main content

native_theme/model/
widget_metrics.rs

1// Per-widget sizing and spacing metrics
2
3use serde::{Deserialize, Serialize};
4
5/// Button sizing and spacing metrics.
6///
7/// Defines minimum dimensions, padding, and icon spacing for push buttons.
8/// All values are in logical pixels.
9#[serde_with::skip_serializing_none]
10#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
11#[serde(default)]
12#[non_exhaustive]
13pub struct ButtonMetrics {
14    /// Minimum button width in logical pixels.
15    pub min_width: Option<f32>,
16    /// Minimum button height in logical pixels.
17    pub min_height: Option<f32>,
18    /// Horizontal padding inside the button.
19    pub padding_horizontal: Option<f32>,
20    /// Vertical padding inside the button.
21    pub padding_vertical: Option<f32>,
22    /// Spacing between icon and label within the button.
23    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/// Checkbox and radio button metrics.
31///
32/// Defines the indicator (check mark area) size and spacing to its label.
33/// All values are in logical pixels.
34#[serde_with::skip_serializing_none]
35#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
36#[serde(default)]
37#[non_exhaustive]
38pub struct CheckboxMetrics {
39    /// Size of the checkbox/radio indicator in logical pixels.
40    pub indicator_size: Option<f32>,
41    /// Gap between indicator and label in logical pixels.
42    pub spacing: Option<f32>,
43}
44
45impl_merge!(CheckboxMetrics {
46    option { indicator_size, spacing }
47});
48
49/// Text input field metrics.
50///
51/// Defines minimum height and padding for single-line text inputs.
52/// All values are in logical pixels.
53#[serde_with::skip_serializing_none]
54#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
55#[serde(default)]
56#[non_exhaustive]
57pub struct InputMetrics {
58    /// Minimum input field height in logical pixels.
59    pub min_height: Option<f32>,
60    /// Horizontal padding inside the input field.
61    pub padding_horizontal: Option<f32>,
62    /// Vertical padding inside the input field.
63    pub padding_vertical: Option<f32>,
64}
65
66impl_merge!(InputMetrics {
67    option { min_height, padding_horizontal, padding_vertical }
68});
69
70/// Scrollbar metrics.
71///
72/// Defines track width, thumb dimensions, and slider width for scrollbars.
73/// All values are in logical pixels.
74#[serde_with::skip_serializing_none]
75#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
76#[serde(default)]
77#[non_exhaustive]
78pub struct ScrollbarMetrics {
79    /// Scrollbar track width in logical pixels.
80    pub width: Option<f32>,
81    /// Minimum thumb/slider height in logical pixels.
82    pub min_thumb_height: Option<f32>,
83    /// Thumb/slider width (may differ from track width) in logical pixels.
84    pub slider_width: Option<f32>,
85}
86
87impl_merge!(ScrollbarMetrics {
88    option { width, min_thumb_height, slider_width }
89});
90
91/// Slider/range control metrics.
92///
93/// Defines track groove thickness, thumb size, and tick mark length.
94/// All values are in logical pixels.
95#[serde_with::skip_serializing_none]
96#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
97#[serde(default)]
98#[non_exhaustive]
99pub struct SliderMetrics {
100    /// Track groove thickness in logical pixels.
101    pub track_height: Option<f32>,
102    /// Control thumb diameter/width in logical pixels.
103    pub thumb_size: Option<f32>,
104    /// Tick mark length in logical pixels.
105    pub tick_length: Option<f32>,
106}
107
108impl_merge!(SliderMetrics {
109    option { track_height, thumb_size, tick_length }
110});
111
112/// Progress bar metrics.
113///
114/// Defines bar height and minimum width for determinate/indeterminate bars.
115/// All values are in logical pixels.
116#[serde_with::skip_serializing_none]
117#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
118#[serde(default)]
119#[non_exhaustive]
120pub struct ProgressBarMetrics {
121    /// Bar thickness in logical pixels.
122    pub height: Option<f32>,
123    /// Minimum bar width in logical pixels.
124    pub min_width: Option<f32>,
125}
126
127impl_merge!(ProgressBarMetrics {
128    option { height, min_width }
129});
130
131/// Tab bar metrics.
132///
133/// Defines minimum tab dimensions and padding for tabbed interfaces.
134/// All values are in logical pixels.
135#[serde_with::skip_serializing_none]
136#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
137#[serde(default)]
138#[non_exhaustive]
139pub struct TabMetrics {
140    /// Minimum tab width in logical pixels.
141    pub min_width: Option<f32>,
142    /// Minimum tab height in logical pixels.
143    pub min_height: Option<f32>,
144    /// Horizontal padding inside the tab.
145    pub padding_horizontal: Option<f32>,
146    /// Vertical padding inside the tab.
147    pub padding_vertical: Option<f32>,
148}
149
150impl_merge!(TabMetrics {
151    option { min_width, min_height, padding_horizontal, padding_vertical }
152});
153
154/// Menu item metrics.
155///
156/// Defines height, padding, and icon spacing for menu items.
157/// All values are in logical pixels.
158#[serde_with::skip_serializing_none]
159#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
160#[serde(default)]
161#[non_exhaustive]
162pub struct MenuItemMetrics {
163    /// Single menu item height in logical pixels.
164    pub height: Option<f32>,
165    /// Horizontal padding inside the menu item.
166    pub padding_horizontal: Option<f32>,
167    /// Vertical padding inside the menu item.
168    pub padding_vertical: Option<f32>,
169    /// Gap between icon and label in logical pixels.
170    pub icon_spacing: Option<f32>,
171}
172
173impl_merge!(MenuItemMetrics {
174    option { height, padding_horizontal, padding_vertical, icon_spacing }
175});
176
177/// Tooltip metrics.
178///
179/// Defines inner padding and maximum width for tooltips.
180/// All values are in logical pixels.
181#[serde_with::skip_serializing_none]
182#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
183#[serde(default)]
184#[non_exhaustive]
185pub struct TooltipMetrics {
186    /// Inner padding in logical pixels.
187    pub padding: Option<f32>,
188    /// Maximum tooltip width in logical pixels.
189    pub max_width: Option<f32>,
190}
191
192impl_merge!(TooltipMetrics {
193    option { padding, max_width }
194});
195
196/// List item / row metrics.
197///
198/// Defines row height and padding for list views and tables.
199/// All values are in logical pixels.
200#[serde_with::skip_serializing_none]
201#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
202#[serde(default)]
203#[non_exhaustive]
204pub struct ListItemMetrics {
205    /// Row height in logical pixels.
206    pub height: Option<f32>,
207    /// Horizontal padding inside the list item.
208    pub padding_horizontal: Option<f32>,
209    /// Vertical padding inside the list item.
210    pub padding_vertical: Option<f32>,
211}
212
213impl_merge!(ListItemMetrics {
214    option { height, padding_horizontal, padding_vertical }
215});
216
217/// Toolbar metrics.
218///
219/// Defines toolbar height, item spacing, and inner padding.
220/// All values are in logical pixels.
221#[serde_with::skip_serializing_none]
222#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
223#[serde(default)]
224#[non_exhaustive]
225pub struct ToolbarMetrics {
226    /// Toolbar height in logical pixels.
227    pub height: Option<f32>,
228    /// Gap between toolbar items in logical pixels.
229    pub item_spacing: Option<f32>,
230    /// Inner padding in logical pixels.
231    pub padding: Option<f32>,
232}
233
234impl_merge!(ToolbarMetrics {
235    option { height, item_spacing, padding }
236});
237
238/// Splitter/divider metrics.
239///
240/// Defines the handle width/thickness for split pane dividers.
241/// All values are in logical pixels.
242#[serde_with::skip_serializing_none]
243#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
244#[serde(default)]
245#[non_exhaustive]
246pub struct SplitterMetrics {
247    /// Splitter handle width/thickness in logical pixels.
248    pub width: Option<f32>,
249}
250
251impl_merge!(SplitterMetrics {
252    option { width }
253});
254
255/// Per-widget sizing and spacing metrics.
256///
257/// Contains sub-structs for each widget type with platform-specific
258/// dimensions. All sub-structs are nested (not Option) because they
259/// default to empty (all fields None). Empty sub-structs are omitted
260/// from serialized output.
261#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
262#[serde(default)]
263#[non_exhaustive]
264pub struct WidgetMetrics {
265    /// Button sizing and spacing.
266    #[serde(default, skip_serializing_if = "ButtonMetrics::is_empty")]
267    pub button: ButtonMetrics,
268
269    /// Checkbox and radio button sizing.
270    #[serde(default, skip_serializing_if = "CheckboxMetrics::is_empty")]
271    pub checkbox: CheckboxMetrics,
272
273    /// Text input field sizing.
274    #[serde(default, skip_serializing_if = "InputMetrics::is_empty")]
275    pub input: InputMetrics,
276
277    /// Scrollbar sizing.
278    #[serde(default, skip_serializing_if = "ScrollbarMetrics::is_empty")]
279    pub scrollbar: ScrollbarMetrics,
280
281    /// Slider/range control sizing.
282    #[serde(default, skip_serializing_if = "SliderMetrics::is_empty")]
283    pub slider: SliderMetrics,
284
285    /// Progress bar sizing.
286    #[serde(default, skip_serializing_if = "ProgressBarMetrics::is_empty")]
287    pub progress_bar: ProgressBarMetrics,
288
289    /// Tab bar sizing.
290    #[serde(default, skip_serializing_if = "TabMetrics::is_empty")]
291    pub tab: TabMetrics,
292
293    /// Menu item sizing.
294    #[serde(default, skip_serializing_if = "MenuItemMetrics::is_empty")]
295    pub menu_item: MenuItemMetrics,
296
297    /// Tooltip sizing.
298    #[serde(default, skip_serializing_if = "TooltipMetrics::is_empty")]
299    pub tooltip: TooltipMetrics,
300
301    /// List item / row sizing.
302    #[serde(default, skip_serializing_if = "ListItemMetrics::is_empty")]
303    pub list_item: ListItemMetrics,
304
305    /// Toolbar sizing.
306    #[serde(default, skip_serializing_if = "ToolbarMetrics::is_empty")]
307    pub toolbar: ToolbarMetrics,
308
309    /// Splitter/divider sizing.
310    #[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); // override
342        overlay.button.padding_horizontal = Some(12.0); // new field
343
344        base.merge(&overlay);
345
346        // overlay value replaces base
347        assert_eq!(base.button.min_width, Some(100.0));
348        // base value preserved when overlay is None
349        assert_eq!(base.button.min_height, Some(30.0));
350        // new overlay field applied
351        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        // button: base preserved, overlay applied
367        assert_eq!(base.button.min_width, Some(80.0));
368        assert_eq!(base.button.padding_horizontal, Some(6.0));
369        // scrollbar: base preserved, overlay applied
370        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        // Other sub-structs remain empty
452        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        // Only button should appear in output
464        assert!(toml_str.contains("button"));
465        // Empty sub-structs should not appear
466        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        // Simulate a TOML string that has no widget_metrics at all
481        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    // === Individual sub-struct tests ===
488
489    #[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}