1use crate::Rgba;
4use crate::model::border::{ResolvedWidgetBorder, WidgetBorderSpec};
5use crate::model::{ArrowSide, DialogButtonOrder, FontSpec, ResolvedFontSpec, TabIndicatorSide};
6use native_theme_derive::{ThemeFields, ThemeWidget};
7
8#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize, ThemeWidget)]
12#[serde_with::skip_serializing_none]
13#[serde(default)]
14#[theme_inherit(border_kind = "full_lg", font = "title_bar_font")]
15pub struct WindowTheme {
16 #[theme(inherit_from = "defaults.background_color")]
18 pub background_color: Option<Rgba>,
19 #[theme(inherit_from = "defaults.surface_color")]
21 pub title_bar_background: Option<Rgba>,
22 pub inactive_title_bar_background: Option<Rgba>,
24 pub inactive_title_bar_text_color: Option<Rgba>,
26 #[theme(nested, resolved_type = "ResolvedFontSpec")]
28 pub title_bar_font: Option<FontSpec>,
29 #[theme(nested, resolved_type = "ResolvedWidgetBorder")]
31 pub border: Option<WidgetBorderSpec>,
32}
33
34#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize, ThemeWidget)]
38#[serde_with::skip_serializing_none]
39#[serde(default)]
40#[theme_inherit(border_kind = "full", font = "font")]
41pub struct ButtonTheme {
42 #[theme(inherit_from = "defaults.background_color")]
44 pub background_color: Option<Rgba>,
45 #[theme(inherit_from = "defaults.accent_color")]
47 pub primary_background: Option<Rgba>,
48 #[theme(inherit_from = "defaults.accent_text_color")]
50 pub primary_text_color: Option<Rgba>,
51 #[serde(rename = "min_width_px")]
53 #[theme(check = "non_negative")]
54 pub min_width: Option<f32>,
55 #[serde(rename = "min_height_px")]
57 #[theme(check = "non_negative")]
58 pub min_height: Option<f32>,
59 #[serde(rename = "icon_text_gap_px")]
61 #[theme(check = "non_negative")]
62 pub icon_text_gap: Option<f32>,
63 #[theme(range = "0.0..=1.0", inherit_from = "defaults.disabled_opacity")]
67 pub disabled_opacity: Option<f32>,
68 #[theme(inherit_from = "defaults.background_color")]
70 pub hover_background: Option<Rgba>,
71 pub hover_text_color: Option<Rgba>,
73 pub active_text_color: Option<Rgba>,
75 #[theme(inherit_from = "defaults.disabled_text_color")]
77 pub disabled_text_color: Option<Rgba>,
78 #[theme(category = "soft_option")]
80 pub active_background: Option<Rgba>,
81 #[theme(category = "soft_option")]
83 pub disabled_background: Option<Rgba>,
84 #[theme(category = "soft_option")]
91 pub checked_background: Option<Rgba>,
92 #[theme(category = "soft_option")]
95 pub checked_text_color: Option<Rgba>,
96 #[theme(nested, resolved_type = "ResolvedFontSpec")]
98 pub font: Option<FontSpec>,
99 #[theme(nested, resolved_type = "ResolvedWidgetBorder")]
101 pub border: Option<WidgetBorderSpec>,
102}
103
104#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize, ThemeWidget)]
109#[serde_with::skip_serializing_none]
110#[serde(default)]
111#[theme_inherit(border_kind = "full", font = "font")]
112pub struct InputTheme {
113 #[theme(inherit_from = "defaults.background_color")]
115 pub background_color: Option<Rgba>,
116 #[theme(inherit_from = "defaults.muted_color")]
118 pub placeholder_color: Option<Rgba>,
119 pub caret_color: Option<Rgba>,
121 #[theme(inherit_from = "defaults.text_selection_background")]
123 pub selection_background: Option<Rgba>,
124 #[theme(inherit_from = "defaults.text_selection_color")]
126 pub selection_text_color: Option<Rgba>,
127 #[serde(rename = "min_height_px")]
129 #[theme(check = "non_negative")]
130 pub min_height: Option<f32>,
131 #[theme(range = "0.0..=1.0", inherit_from = "defaults.disabled_opacity")]
135 pub disabled_opacity: Option<f32>,
136 #[theme(inherit_from = "defaults.disabled_text_color")]
138 pub disabled_text_color: Option<Rgba>,
139 #[theme(category = "soft_option")]
141 pub hover_border_color: Option<Rgba>,
142 #[theme(category = "soft_option")]
144 pub focus_border_color: Option<Rgba>,
145 #[theme(category = "soft_option")]
147 pub disabled_background: Option<Rgba>,
148 #[theme(nested, resolved_type = "ResolvedFontSpec")]
150 pub font: Option<FontSpec>,
151 #[theme(nested, resolved_type = "ResolvedWidgetBorder")]
153 pub border: Option<WidgetBorderSpec>,
154}
155
156#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize, ThemeWidget)]
162#[serde_with::skip_serializing_none]
163#[serde(default)]
164pub struct TextAreaTheme {
165 #[theme(nested, resolved_type = "ResolvedWidgetBorder")]
171 pub border: Option<WidgetBorderSpec>,
172}
173
174#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize, ThemeWidget)]
178#[serde_with::skip_serializing_none]
179#[serde(default)]
180#[theme_inherit(border_kind = "full", font = "font")]
181pub struct CheckboxTheme {
182 #[theme(inherit_from = "defaults.background_color")]
184 pub background_color: Option<Rgba>,
185 #[theme(inherit_from = "defaults.accent_color")]
187 pub checked_background: Option<Rgba>,
188 #[theme(inherit_from = "defaults.accent_text_color")]
190 pub indicator_color: Option<Rgba>,
191 #[serde(rename = "indicator_width_px")]
195 #[theme(check = "non_negative")]
196 pub indicator_width: Option<f32>,
197 #[serde(rename = "radio_indicator_width_px")]
202 #[theme(category = "soft_option", check = "non_negative")]
203 pub radio_indicator_width: Option<f32>,
204 #[serde(rename = "radio_dot_diameter_px")]
208 #[theme(category = "soft_option", check = "non_negative")]
209 pub radio_dot_diameter: Option<f32>,
210 #[serde(rename = "check_mark_stroke_width_px")]
214 #[theme(category = "soft_option", check = "non_negative")]
215 pub check_mark_stroke_width: Option<f32>,
216 #[serde(rename = "label_gap_px")]
218 #[theme(check = "non_negative")]
219 pub label_gap: Option<f32>,
220 #[theme(range = "0.0..=1.0", inherit_from = "defaults.disabled_opacity")]
224 pub disabled_opacity: Option<f32>,
225 #[theme(inherit_from = "defaults.disabled_text_color")]
227 pub disabled_text_color: Option<Rgba>,
228 #[theme(category = "soft_option")]
230 pub hover_background: Option<Rgba>,
231 #[theme(category = "soft_option")]
234 pub disabled_background: Option<Rgba>,
235 #[theme(category = "soft_option")]
237 pub unchecked_background: Option<Rgba>,
238 #[theme(category = "soft_option")]
240 pub unchecked_border_color: Option<Rgba>,
241 #[theme(nested, resolved_type = "ResolvedFontSpec")]
243 pub font: Option<FontSpec>,
244 #[theme(nested, resolved_type = "ResolvedWidgetBorder")]
246 pub border: Option<WidgetBorderSpec>,
247}
248
249#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize, ThemeWidget)]
253#[serde_with::skip_serializing_none]
254#[serde(default)]
255#[theme_layer(border_kind = "none")]
256#[theme_inherit(font = "font")]
257pub struct MenuTheme {
258 #[theme(inherit_from = "defaults.background_color")]
260 pub background_color: Option<Rgba>,
261 #[theme(inherit_from = "defaults.border.color")]
263 pub separator_color: Option<Rgba>,
264 #[serde(rename = "row_height_px")]
268 #[theme(category = "soft_option", check = "non_negative")]
269 pub row_height: Option<f32>,
270 #[serde(rename = "icon_text_gap_px")]
272 #[theme(check = "non_negative")]
273 pub icon_text_gap: Option<f32>,
274 #[serde(rename = "icon_size_px")]
276 #[theme(check = "non_negative", inherit_from = "defaults.icon_sizes.toolbar")]
277 pub icon_size: Option<f32>,
278 #[theme(inherit_from = "defaults.selection_background")]
280 pub hover_background: Option<Rgba>,
281 #[theme(inherit_from = "defaults.selection_text_color")]
283 pub hover_text_color: Option<Rgba>,
284 #[theme(inherit_from = "defaults.disabled_text_color")]
286 pub disabled_text_color: Option<Rgba>,
287 #[theme(nested, resolved_type = "ResolvedFontSpec")]
289 pub font: Option<FontSpec>,
290 #[theme(nested, resolved_type = "ResolvedWidgetBorder")]
292 pub border: Option<WidgetBorderSpec>,
293}
294
295#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize, ThemeWidget)]
299#[serde_with::skip_serializing_none]
300#[serde(default)]
301#[theme_inherit(border_kind = "full", font = "font")]
302pub struct TooltipTheme {
303 #[theme(inherit_from = "defaults.background_color")]
305 pub background_color: Option<Rgba>,
306 #[serde(rename = "max_width_px")]
308 #[theme(check = "non_negative")]
309 pub max_width: Option<f32>,
310 #[theme(nested, resolved_type = "ResolvedFontSpec")]
312 pub font: Option<FontSpec>,
313 #[theme(nested, resolved_type = "ResolvedWidgetBorder")]
315 pub border: Option<WidgetBorderSpec>,
316}
317
318#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize, ThemeWidget)]
322#[serde_with::skip_serializing_none]
323#[serde(default)]
324pub struct ScrollbarTheme {
325 pub track_color: Option<Rgba>,
327 #[theme(inherit_from = "defaults.muted_color")]
329 pub thumb_color: Option<Rgba>,
330 #[theme(inherit_from = "defaults.muted_color")]
332 pub thumb_hover_color: Option<Rgba>,
333 #[serde(rename = "groove_width_px")]
335 #[theme(check = "non_negative")]
336 pub groove_width: Option<f32>,
337 #[serde(rename = "min_thumb_length_px")]
339 #[theme(check = "non_negative")]
340 pub min_thumb_length: Option<f32>,
341 #[serde(rename = "thumb_width_px")]
343 #[theme(check = "non_negative")]
344 pub thumb_width: Option<f32>,
345 pub overlay_mode: Option<bool>,
347 #[theme(category = "soft_option")]
349 pub thumb_active_color: Option<Rgba>,
350}
351
352#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize, ThemeWidget)]
356#[serde_with::skip_serializing_none]
357#[serde(default)]
358pub struct SliderTheme {
359 #[theme(inherit_from = "defaults.accent_color")]
361 pub fill_color: Option<Rgba>,
362 #[theme(inherit_from = "defaults.muted_color")]
364 pub track_color: Option<Rgba>,
365 #[theme(inherit_from = "defaults.surface_color")]
367 pub thumb_color: Option<Rgba>,
368 #[serde(rename = "track_height_px")]
370 #[theme(check = "non_negative")]
371 pub track_height: Option<f32>,
372 #[serde(rename = "thumb_diameter_px")]
374 #[theme(check = "non_negative")]
375 pub thumb_diameter: Option<f32>,
376 #[serde(rename = "tick_mark_length_px")]
378 #[theme(check = "non_negative")]
379 pub tick_mark_length: Option<f32>,
380 #[theme(range = "0.0..=1.0", inherit_from = "defaults.disabled_opacity")]
384 pub disabled_opacity: Option<f32>,
385 #[theme(category = "soft_option")]
387 pub thumb_hover_color: Option<Rgba>,
388 #[theme(category = "soft_option")]
391 pub disabled_fill_color: Option<Rgba>,
392 #[theme(category = "soft_option")]
394 pub disabled_track_color: Option<Rgba>,
395 #[theme(category = "soft_option")]
397 pub disabled_thumb_color: Option<Rgba>,
398}
399
400#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize, ThemeWidget)]
404#[serde_with::skip_serializing_none]
405#[serde(default)]
406#[theme_inherit(border_kind = "full")]
407pub struct ProgressBarTheme {
408 #[theme(inherit_from = "defaults.accent_color")]
410 pub fill_color: Option<Rgba>,
411 #[theme(inherit_from = "defaults.muted_color")]
413 pub track_color: Option<Rgba>,
414 #[serde(rename = "track_height_px")]
416 #[theme(check = "non_negative")]
417 pub track_height: Option<f32>,
418 #[serde(rename = "min_width_px")]
420 #[theme(check = "non_negative")]
421 pub min_width: Option<f32>,
422 #[theme(nested, resolved_type = "ResolvedWidgetBorder")]
424 pub border: Option<WidgetBorderSpec>,
425}
426
427#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize, ThemeWidget)]
431#[serde_with::skip_serializing_none]
432#[serde(default)]
433#[theme_layer(border_kind = "none")]
434#[theme_inherit(font = "font")]
435pub struct TabTheme {
436 #[theme(inherit_from = "defaults.background_color")]
438 pub background_color: Option<Rgba>,
439 #[theme(inherit_from = "defaults.background_color")]
441 pub active_background: Option<Rgba>,
442 #[theme(inherit_from = "defaults.text_color")]
444 pub active_text_color: Option<Rgba>,
445 #[theme(inherit_from = "defaults.background_color")]
447 pub bar_background: Option<Rgba>,
448 #[serde(rename = "min_width_px")]
450 #[theme(check = "non_negative")]
451 pub min_width: Option<f32>,
452 #[serde(rename = "min_height_px")]
454 #[theme(check = "non_negative")]
455 pub min_height: Option<f32>,
456 #[serde(rename = "item_gap_px")]
460 #[theme(category = "soft_option", check = "non_negative")]
461 pub item_gap: Option<f32>,
462 #[theme(category = "soft_option")]
468 pub active_indicator_color: Option<Rgba>,
469 #[serde(rename = "active_indicator_width_px")]
472 #[theme(category = "soft_option", check = "non_negative")]
473 pub active_indicator_width: Option<f32>,
474 #[theme(category = "soft_option")]
478 pub active_indicator_side: Option<TabIndicatorSide>,
479 pub hover_text_color: Option<Rgba>,
481 #[theme(category = "soft_option")]
483 pub hover_background: Option<Rgba>,
484 #[theme(nested, resolved_type = "ResolvedFontSpec")]
486 pub font: Option<FontSpec>,
487 #[theme(nested, resolved_type = "ResolvedWidgetBorder")]
489 pub border: Option<WidgetBorderSpec>,
490}
491
492#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize, ThemeWidget)]
496#[serde_with::skip_serializing_none]
497#[serde(default)]
498#[theme_layer(border_kind = "partial")]
499#[theme_inherit(border_kind = "partial", font = "font")]
500pub struct SidebarTheme {
501 #[theme(inherit_from = "defaults.background_color")]
503 pub background_color: Option<Rgba>,
504 #[theme(inherit_from = "defaults.selection_background")]
506 pub selection_background: Option<Rgba>,
507 #[theme(inherit_from = "defaults.selection_text_color")]
509 pub selection_text_color: Option<Rgba>,
510 #[theme(inherit_from = "defaults.background_color")]
512 pub hover_background: Option<Rgba>,
513 #[theme(nested, resolved_type = "ResolvedFontSpec")]
515 pub font: Option<FontSpec>,
516 #[theme(nested, resolved_type = "ResolvedWidgetBorder")]
518 pub border: Option<WidgetBorderSpec>,
519}
520
521#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize, ThemeWidget)]
525#[serde_with::skip_serializing_none]
526#[serde(default)]
527#[theme_inherit(border_kind = "full", font = "font")]
528pub struct ToolbarTheme {
529 #[theme(inherit_from = "defaults.background_color")]
531 pub background_color: Option<Rgba>,
532 #[serde(rename = "bar_height_px")]
536 #[theme(category = "soft_option", check = "non_negative")]
537 pub bar_height: Option<f32>,
538 #[serde(rename = "item_gap_px")]
542 #[theme(category = "soft_option", check = "non_negative")]
543 pub item_gap: Option<f32>,
544 #[serde(rename = "icon_size_px")]
546 #[theme(check = "non_negative", inherit_from = "defaults.icon_sizes.toolbar")]
547 pub icon_size: Option<f32>,
548 #[theme(nested, resolved_type = "ResolvedFontSpec")]
550 pub font: Option<FontSpec>,
551 #[theme(nested, resolved_type = "ResolvedWidgetBorder")]
553 pub border: Option<WidgetBorderSpec>,
554}
555
556#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize, ThemeWidget)]
560#[serde_with::skip_serializing_none]
561#[serde(default)]
562#[theme_layer(border_kind = "partial")]
563#[theme_inherit(border_kind = "partial", font = "font")]
564pub struct StatusBarTheme {
565 #[theme(inherit_from = "defaults.background_color")]
567 pub background_color: Option<Rgba>,
568 #[theme(nested, resolved_type = "ResolvedFontSpec")]
570 pub font: Option<FontSpec>,
571 #[theme(nested, resolved_type = "ResolvedWidgetBorder")]
573 pub border: Option<WidgetBorderSpec>,
574}
575
576#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize, ThemeWidget)]
580#[serde_with::skip_serializing_none]
581#[serde(default)]
582#[theme_inherit(border_kind = "full", font = "item_font")]
583#[theme_inherit(font = "header_font")]
584pub struct ListTheme {
585 pub background_color: Option<Rgba>,
587 pub alternate_row_background: Option<Rgba>,
589 #[theme(inherit_from = "defaults.selection_background")]
591 pub selection_background: Option<Rgba>,
592 #[theme(inherit_from = "defaults.selection_text_color")]
594 pub selection_text_color: Option<Rgba>,
595 #[theme(inherit_from = "defaults.surface_color")]
597 pub header_background: Option<Rgba>,
598 #[theme(inherit_from = "defaults.border.color")]
600 pub grid_color: Option<Rgba>,
601 #[serde(rename = "row_height_px")]
605 #[theme(category = "soft_option", check = "non_negative")]
606 pub row_height: Option<f32>,
607 #[theme(inherit_from = "defaults.background_color")]
609 pub hover_background: Option<Rgba>,
610 pub hover_text_color: Option<Rgba>,
612 #[theme(inherit_from = "defaults.disabled_text_color")]
614 pub disabled_text_color: Option<Rgba>,
615 #[theme(nested, resolved_type = "ResolvedFontSpec")]
617 pub item_font: Option<FontSpec>,
618 #[theme(nested, resolved_type = "ResolvedFontSpec")]
620 pub header_font: Option<FontSpec>,
621 #[theme(nested, resolved_type = "ResolvedWidgetBorder")]
623 pub border: Option<WidgetBorderSpec>,
624}
625
626#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize, ThemeWidget)]
630#[serde_with::skip_serializing_none]
631#[serde(default)]
632#[theme_inherit(border_kind = "full_lg", font = "font")]
633pub struct PopoverTheme {
634 pub background_color: Option<Rgba>,
636 #[theme(nested, resolved_type = "ResolvedFontSpec")]
638 pub font: Option<FontSpec>,
639 #[theme(nested, resolved_type = "ResolvedWidgetBorder")]
641 pub border: Option<WidgetBorderSpec>,
642}
643
644#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize, ThemeWidget)]
648#[serde_with::skip_serializing_none]
649#[serde(default)]
650pub struct SplitterTheme {
651 #[serde(rename = "divider_width_px")]
653 #[theme(check = "non_negative")]
654 pub divider_width: Option<f32>,
655 #[theme(inherit_from = "defaults.border.color")]
657 pub divider_color: Option<Rgba>,
658 pub hover_color: Option<Rgba>,
660}
661
662#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize, ThemeWidget)]
666#[serde_with::skip_serializing_none]
667#[serde(default)]
668pub struct SeparatorTheme {
669 #[theme(inherit_from = "defaults.border.color")]
671 pub line_color: Option<Rgba>,
672 #[serde(rename = "line_width_px")]
674 #[theme(check = "non_negative", inherit_from = "defaults.border.line_width")]
675 pub line_width: Option<f32>,
676}
677
678#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize, ThemeWidget)]
682#[serde_with::skip_serializing_none]
683#[serde(default)]
684pub struct SwitchTheme {
685 #[theme(inherit_from = "defaults.accent_color")]
687 pub checked_background: Option<Rgba>,
688 pub unchecked_background: Option<Rgba>,
690 #[theme(inherit_from = "defaults.surface_color")]
693 pub thumb_background: Option<Rgba>,
694 #[theme(category = "soft_option")]
699 pub unchecked_thumb_background: Option<Rgba>,
700 #[serde(rename = "track_width_px")]
702 #[theme(check = "non_negative")]
703 pub track_width: Option<f32>,
704 #[serde(rename = "track_height_px")]
706 #[theme(check = "non_negative")]
707 pub track_height: Option<f32>,
708 #[serde(rename = "thumb_diameter_px")]
711 #[theme(check = "non_negative")]
712 pub thumb_diameter: Option<f32>,
713 #[serde(rename = "unchecked_thumb_diameter_px")]
718 #[theme(category = "soft_option", check = "non_negative")]
719 pub unchecked_thumb_diameter: Option<f32>,
720 #[serde(rename = "track_radius_px")]
722 #[theme(check = "non_negative")]
723 pub track_radius: Option<f32>,
724 #[theme(range = "0.0..=1.0", inherit_from = "defaults.disabled_opacity")]
728 pub disabled_opacity: Option<f32>,
729 #[theme(category = "soft_option")]
731 pub hover_checked_background: Option<Rgba>,
732 #[theme(category = "soft_option")]
734 pub hover_unchecked_background: Option<Rgba>,
735 #[theme(category = "soft_option")]
738 pub disabled_checked_background: Option<Rgba>,
739 #[theme(category = "soft_option")]
741 pub disabled_unchecked_background: Option<Rgba>,
742 #[theme(category = "soft_option")]
744 pub disabled_thumb_color: Option<Rgba>,
745}
746
747#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize, ThemeWidget)]
751#[serde_with::skip_serializing_none]
752#[serde(default)]
753#[theme_inherit(border_kind = "full_lg", font = "title_font")]
754#[theme_inherit(font = "body_font")]
755pub struct DialogTheme {
756 pub background_color: Option<Rgba>,
758 #[serde(rename = "min_width_px")]
760 #[theme(check = "non_negative", min_max_pair = "max_width")]
761 pub min_width: Option<f32>,
762 #[serde(rename = "max_width_px")]
764 #[theme(check = "non_negative")]
765 pub max_width: Option<f32>,
766 #[serde(rename = "min_height_px")]
768 #[theme(check = "non_negative", min_max_pair = "max_height")]
769 pub min_height: Option<f32>,
770 #[serde(rename = "max_height_px")]
772 #[theme(check = "non_negative")]
773 pub max_height: Option<f32>,
774 #[serde(rename = "button_gap_px")]
776 #[theme(check = "non_negative")]
777 pub button_gap: Option<f32>,
778 #[serde(rename = "icon_size_px")]
780 #[theme(check = "non_negative")]
781 pub icon_size: Option<f32>,
782 pub button_order: Option<DialogButtonOrder>,
784 #[theme(nested, resolved_type = "ResolvedFontSpec")]
786 pub title_font: Option<FontSpec>,
787 #[theme(nested, resolved_type = "ResolvedFontSpec")]
789 pub body_font: Option<FontSpec>,
790 #[theme(nested, resolved_type = "ResolvedWidgetBorder")]
792 pub border: Option<WidgetBorderSpec>,
793}
794
795#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize, ThemeWidget)]
799#[serde_with::skip_serializing_none]
800#[serde(default)]
801pub struct SpinnerTheme {
802 pub fill_color: Option<Rgba>,
804 #[serde(rename = "diameter_px")]
806 #[theme(check = "non_negative")]
807 pub diameter: Option<f32>,
808 #[serde(rename = "min_diameter_px")]
810 #[theme(check = "non_negative")]
811 pub min_diameter: Option<f32>,
812 #[serde(rename = "stroke_width_px")]
814 #[theme(check = "non_negative")]
815 pub stroke_width: Option<f32>,
816}
817
818#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize, ThemeWidget)]
822#[serde_with::skip_serializing_none]
823#[serde(default)]
824#[theme_inherit(border_kind = "full", font = "font")]
825pub struct ComboBoxTheme {
826 #[theme(inherit_from = "defaults.background_color")]
828 pub background_color: Option<Rgba>,
829 #[serde(rename = "min_height_px")]
831 #[theme(check = "non_negative")]
832 pub min_height: Option<f32>,
833 #[serde(rename = "min_width_px")]
835 #[theme(check = "non_negative")]
836 pub min_width: Option<f32>,
837 #[serde(rename = "arrow_icon_size_px")]
839 #[theme(check = "non_negative")]
840 pub arrow_icon_size: Option<f32>,
841 #[serde(rename = "arrow_area_width_px")]
847 #[theme(category = "soft_option", check = "non_negative")]
848 pub arrow_area_width: Option<f32>,
849 #[theme(range = "0.0..=1.0", inherit_from = "defaults.disabled_opacity")]
853 pub disabled_opacity: Option<f32>,
854 #[theme(inherit_from = "defaults.disabled_text_color")]
856 pub disabled_text_color: Option<Rgba>,
857 #[theme(category = "soft_option")]
859 pub hover_background: Option<Rgba>,
860 #[theme(category = "soft_option")]
862 pub disabled_background: Option<Rgba>,
863 #[theme(nested, resolved_type = "ResolvedFontSpec")]
865 pub font: Option<FontSpec>,
866 #[theme(nested, resolved_type = "ResolvedWidgetBorder")]
868 pub border: Option<WidgetBorderSpec>,
869}
870
871#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize, ThemeWidget)]
875#[serde_with::skip_serializing_none]
876#[serde(default)]
877#[theme_inherit(border_kind = "full", font = "font")]
878pub struct SegmentedControlTheme {
879 #[theme(inherit_from = "defaults.background_color")]
881 pub background_color: Option<Rgba>,
882 #[theme(inherit_from = "defaults.accent_color")]
884 pub active_background: Option<Rgba>,
885 #[theme(inherit_from = "defaults.accent_text_color")]
887 pub active_text_color: Option<Rgba>,
888 #[serde(rename = "segment_height_px")]
890 #[theme(check = "non_negative")]
891 pub segment_height: Option<f32>,
892 #[serde(rename = "separator_width_px")]
894 #[theme(check = "non_negative")]
895 pub separator_width: Option<f32>,
896 #[theme(range = "0.0..=1.0", inherit_from = "defaults.disabled_opacity")]
900 pub disabled_opacity: Option<f32>,
901 #[theme(category = "soft_option")]
903 pub hover_background: Option<Rgba>,
904 #[theme(nested, resolved_type = "ResolvedFontSpec")]
906 pub font: Option<FontSpec>,
907 #[theme(nested, resolved_type = "ResolvedWidgetBorder")]
909 pub border: Option<WidgetBorderSpec>,
910}
911
912#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize, ThemeWidget)]
916#[serde_with::skip_serializing_none]
917#[serde(default)]
918#[theme_layer(border_kind = "none")]
919pub struct CardTheme {
920 #[theme(inherit_from = "defaults.surface_color")]
922 pub background_color: Option<Rgba>,
923 #[theme(nested, resolved_type = "ResolvedWidgetBorder")]
925 pub border: Option<WidgetBorderSpec>,
926}
927
928#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize, ThemeWidget)]
932#[serde_with::skip_serializing_none]
933#[serde(default)]
934#[theme_inherit(border_kind = "full", font = "font")]
935pub struct ExpanderTheme {
936 #[serde(rename = "header_height_px")]
938 #[theme(check = "non_negative")]
939 pub header_height: Option<f32>,
940 #[serde(rename = "arrow_icon_size_px")]
942 #[theme(check = "non_negative")]
943 pub arrow_icon_size: Option<f32>,
944 #[theme(category = "soft_option")]
946 pub hover_background: Option<Rgba>,
947 #[theme(category = "soft_option")]
949 pub arrow_color: Option<Rgba>,
950 #[theme(category = "soft_option")]
954 pub arrow_side: Option<ArrowSide>,
955 #[serde(rename = "arrow_gap_px")]
960 #[theme(category = "soft_option", check = "non_negative")]
961 pub arrow_gap: Option<f32>,
962 #[serde(rename = "content_indent_px")]
967 #[theme(category = "soft_option", check = "non_negative")]
968 pub content_indent: Option<f32>,
969 #[theme(category = "soft_option")]
974 pub frame_enabled: Option<bool>,
975 #[theme(nested, resolved_type = "ResolvedFontSpec")]
977 pub font: Option<FontSpec>,
978 #[theme(nested, resolved_type = "ResolvedWidgetBorder")]
980 pub border: Option<WidgetBorderSpec>,
981}
982
983#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize, ThemeWidget)]
987#[serde_with::skip_serializing_none]
988#[serde(default)]
989#[theme_inherit(font = "font")]
990pub struct LinkTheme {
991 #[theme(inherit_from = "defaults.link_color")]
993 pub visited_text_color: Option<Rgba>,
994 pub underline_enabled: Option<bool>,
996 pub background_color: Option<Rgba>,
998 pub hover_background: Option<Rgba>,
1000 pub hover_text_color: Option<Rgba>,
1002 pub active_text_color: Option<Rgba>,
1004 #[theme(inherit_from = "defaults.disabled_text_color")]
1006 pub disabled_text_color: Option<Rgba>,
1007 #[theme(nested, resolved_type = "ResolvedFontSpec")]
1009 pub font: Option<FontSpec>,
1010}
1011
1012#[derive(
1023 Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize, ThemeWidget, ThemeFields,
1024)]
1025#[serde_with::skip_serializing_none]
1026#[serde(default)]
1027#[theme_layer(skip_inventory)]
1028pub struct LayoutTheme {
1029 #[serde(rename = "widget_gap_px")]
1031 #[theme(check = "non_negative")]
1032 pub widget_gap: Option<f32>,
1033 #[serde(rename = "container_margin_px")]
1035 #[theme(check = "non_negative")]
1036 pub container_margin: Option<f32>,
1037 #[serde(rename = "window_margin_px")]
1039 #[theme(check = "non_negative")]
1040 pub window_margin: Option<f32>,
1041 #[serde(rename = "section_gap_px")]
1043 #[theme(check = "non_negative")]
1044 pub section_gap: Option<f32>,
1045}
1046
1047#[cfg(test)]
1048#[allow(clippy::unwrap_used, clippy::expect_used, dead_code)]
1049mod tests {
1050 use super::*;
1051 use crate::Rgba;
1052 use crate::model::border::{ResolvedWidgetBorder, WidgetBorderSpec};
1053 use crate::model::font::FontSize;
1054 use crate::model::{DialogButtonOrder, FontSpec};
1055
1056 #[derive(
1059 Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize, ThemeWidget,
1060 )]
1061 #[serde_with::skip_serializing_none]
1062 #[serde(default)]
1063 #[theme_layer(skip_inventory)]
1064 pub struct TestWidget {
1065 pub size: Option<f32>,
1066 pub label: Option<String>,
1067 #[theme(nested, resolved_type = "ResolvedFontSpec")]
1068 pub font: Option<FontSpec>,
1069 }
1070
1071 #[test]
1074 fn resolved_font_spec_fields_are_concrete() {
1075 let rfs = ResolvedFontSpec {
1076 family: "Inter".into(),
1077 size: 14.0,
1078 defined_size: Some(crate::model::font::FontSize::Px(14.0)),
1079 weight: 400,
1080 style: crate::model::font::FontStyle::Normal,
1081 color: crate::Rgba::rgb(0, 0, 0),
1082 };
1083 assert_eq!(rfs.family.as_ref(), "Inter");
1084 assert_eq!(rfs.size, 14.0);
1085 assert_eq!(rfs.weight, 400);
1086 }
1087
1088 #[test]
1091 fn generated_option_struct_has_option_fields() {
1092 let w = TestWidget::default();
1093 assert!(w.size.is_none());
1094 assert!(w.label.is_none());
1095 assert!(w.font.is_none());
1096 }
1097
1098 #[test]
1099 fn generated_option_struct_is_empty_by_default() {
1100 assert!(TestWidget::default().is_empty());
1101 }
1102
1103 #[test]
1104 fn generated_option_struct_not_empty_when_size_set() {
1105 let w = TestWidget {
1106 size: Some(24.0),
1107 ..Default::default()
1108 };
1109 assert!(!w.is_empty());
1110 }
1111
1112 #[test]
1113 fn generated_option_struct_not_empty_when_font_set() {
1114 let w = TestWidget {
1115 font: Some(FontSpec {
1116 size: Some(FontSize::Px(14.0)),
1117 ..Default::default()
1118 }),
1119 ..Default::default()
1120 };
1121 assert!(!w.is_empty());
1122 }
1123
1124 #[test]
1125 fn generated_resolved_struct_has_concrete_fields() {
1126 let resolved = ResolvedTestWidget {
1127 size: 24.0,
1128 label: "Click me".into(),
1129 font: ResolvedFontSpec {
1130 family: "Inter".into(),
1131 size: 14.0,
1132 defined_size: Some(crate::model::font::FontSize::Px(14.0)),
1133 weight: 400,
1134 style: crate::model::font::FontStyle::Normal,
1135 color: crate::Rgba::rgb(0, 0, 0),
1136 },
1137 };
1138 assert_eq!(resolved.size, 24.0);
1139 assert_eq!(resolved.label, "Click me");
1140 assert_eq!(resolved.font.family.as_ref(), "Inter");
1141 }
1142
1143 #[test]
1146 fn generated_merge_option_field_overlay_wins() {
1147 let mut base = TestWidget {
1148 size: Some(20.0),
1149 ..Default::default()
1150 };
1151 let overlay = TestWidget {
1152 size: Some(24.0),
1153 ..Default::default()
1154 };
1155 base.merge(&overlay);
1156 assert_eq!(base.size, Some(24.0));
1157 }
1158
1159 #[test]
1160 fn generated_merge_option_field_none_preserves_base() {
1161 let mut base = TestWidget {
1162 size: Some(20.0),
1163 ..Default::default()
1164 };
1165 let overlay = TestWidget::default();
1166 base.merge(&overlay);
1167 assert_eq!(base.size, Some(20.0));
1168 }
1169
1170 #[test]
1171 fn generated_merge_optional_nested_both_some_merges_inner() {
1172 let mut base = TestWidget {
1173 font: Some(FontSpec {
1174 family: Some("Noto Sans".into()),
1175 size: Some(FontSize::Px(12.0)),
1176 weight: None,
1177 ..Default::default()
1178 }),
1179 ..Default::default()
1180 };
1181 let overlay = TestWidget {
1182 font: Some(FontSpec {
1183 family: None,
1184 size: None,
1185 weight: Some(700),
1186 ..Default::default()
1187 }),
1188 ..Default::default()
1189 };
1190 base.merge(&overlay);
1191 let font = base.font.as_ref().unwrap();
1192 assert_eq!(font.family.as_deref(), Some("Noto Sans")); assert_eq!(font.size, Some(FontSize::Px(12.0))); assert_eq!(font.weight, Some(700)); }
1196
1197 #[test]
1198 fn generated_merge_optional_nested_none_plus_some_clones() {
1199 let mut base = TestWidget::default();
1200 let overlay = TestWidget {
1201 font: Some(FontSpec {
1202 family: Some("Inter".into()),
1203 size: Some(FontSize::Px(14.0)),
1204 weight: Some(400),
1205 ..Default::default()
1206 }),
1207 ..Default::default()
1208 };
1209 base.merge(&overlay);
1210 let font = base.font.as_ref().unwrap();
1211 assert_eq!(font.family.as_deref(), Some("Inter"));
1212 assert_eq!(font.size, Some(FontSize::Px(14.0)));
1213 assert_eq!(font.weight, Some(400));
1214 }
1215
1216 #[test]
1217 fn generated_merge_optional_nested_some_plus_none_preserves_base() {
1218 let mut base = TestWidget {
1219 font: Some(FontSpec {
1220 family: Some("Inter".into()),
1221 size: Some(FontSize::Px(14.0)),
1222 weight: Some(400),
1223 ..Default::default()
1224 }),
1225 ..Default::default()
1226 };
1227 let overlay = TestWidget::default();
1228 base.merge(&overlay);
1229 let font = base.font.as_ref().unwrap();
1230 assert_eq!(font.family.as_deref(), Some("Inter"));
1231 }
1232
1233 #[test]
1234 fn generated_merge_optional_nested_none_plus_none_stays_none() {
1235 let mut base = TestWidget::default();
1236 let overlay = TestWidget::default();
1237 base.merge(&overlay);
1238 assert!(base.font.is_none());
1239 }
1240
1241 #[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
1245 struct WithFont {
1246 name: Option<String>,
1247 font: Option<FontSpec>,
1248 }
1249
1250 impl_merge!(WithFont {
1251 option { name }
1252 optional_nested { font }
1253 });
1254
1255 #[test]
1256 fn impl_merge_optional_nested_none_none_stays_none() {
1257 let mut base = WithFont::default();
1258 let overlay = WithFont::default();
1259 base.merge(&overlay);
1260 assert!(base.font.is_none());
1261 }
1262
1263 #[test]
1264 fn impl_merge_optional_nested_some_none_preserves_base() {
1265 let mut base = WithFont {
1266 font: Some(FontSpec {
1267 size: Some(FontSize::Px(12.0)),
1268 ..Default::default()
1269 }),
1270 ..Default::default()
1271 };
1272 let overlay = WithFont::default();
1273 base.merge(&overlay);
1274 assert_eq!(base.font.as_ref().unwrap().size, Some(FontSize::Px(12.0)));
1275 }
1276
1277 #[test]
1278 fn impl_merge_optional_nested_none_some_clones_overlay() {
1279 let mut base = WithFont::default();
1280 let overlay = WithFont {
1281 font: Some(FontSpec {
1282 family: Some("Inter".into()),
1283 ..Default::default()
1284 }),
1285 ..Default::default()
1286 };
1287 base.merge(&overlay);
1288 assert_eq!(base.font.as_ref().unwrap().family.as_deref(), Some("Inter"));
1289 }
1290
1291 #[test]
1292 fn impl_merge_optional_nested_some_some_merges_inner() {
1293 let mut base = WithFont {
1294 font: Some(FontSpec {
1295 family: Some("Noto".into()),
1296 size: Some(FontSize::Px(11.0)),
1297 weight: None,
1298 ..Default::default()
1299 }),
1300 ..Default::default()
1301 };
1302 let overlay = WithFont {
1303 font: Some(FontSpec {
1304 family: None,
1305 size: Some(FontSize::Px(14.0)),
1306 weight: Some(400),
1307 ..Default::default()
1308 }),
1309 ..Default::default()
1310 };
1311 base.merge(&overlay);
1312 let f = base.font.as_ref().unwrap();
1313 assert_eq!(f.family.as_deref(), Some("Noto")); assert_eq!(f.size, Some(FontSize::Px(14.0))); assert_eq!(f.weight, Some(400)); }
1317
1318 #[test]
1319 fn impl_merge_optional_nested_is_empty_none() {
1320 let w = WithFont::default();
1321 assert!(w.is_empty());
1322 }
1323
1324 #[test]
1325 fn impl_merge_optional_nested_is_empty_some_default() {
1326 let w = WithFont {
1328 font: Some(FontSpec::default()),
1329 ..Default::default()
1330 };
1331 assert!(w.is_empty());
1332 }
1333
1334 #[test]
1335 fn impl_merge_optional_nested_is_not_empty_when_populated() {
1336 let w = WithFont {
1337 font: Some(FontSpec {
1338 size: Some(FontSize::Px(14.0)),
1339 ..Default::default()
1340 }),
1341 ..Default::default()
1342 };
1343 assert!(!w.is_empty());
1344 }
1345
1346 #[test]
1349 fn button_theme_default_is_empty() {
1350 assert!(ButtonTheme::default().is_empty());
1351 }
1352
1353 #[test]
1354 fn button_theme_not_empty_when_set() {
1355 let b = ButtonTheme {
1356 background_color: Some(Rgba::rgb(200, 200, 200)),
1357 min_width: Some(64.0),
1358 ..Default::default()
1359 };
1360 assert!(!b.is_empty());
1361 }
1362
1363 #[test]
1364 fn button_theme_merge_font_optional_nested() {
1365 let mut base = ButtonTheme {
1366 font: Some(FontSpec {
1367 family: Some("Noto Sans".into()),
1368 size: Some(FontSize::Px(11.0)),
1369 weight: None,
1370 ..Default::default()
1371 }),
1372 ..Default::default()
1373 };
1374 let overlay = ButtonTheme {
1375 font: Some(FontSpec {
1376 family: None,
1377 weight: Some(700),
1378 ..Default::default()
1379 }),
1380 ..Default::default()
1381 };
1382 base.merge(&overlay);
1383 let f = base.font.as_ref().unwrap();
1384 assert_eq!(f.family.as_deref(), Some("Noto Sans")); assert_eq!(f.weight, Some(700)); }
1387
1388 #[test]
1389 fn button_theme_toml_round_trip_with_font_and_border() {
1390 let b = ButtonTheme {
1391 background_color: Some(Rgba::rgb(200, 200, 200)),
1392 font: Some(FontSpec {
1393 family: Some("Inter".into()),
1394 size: Some(FontSize::Px(14.0)),
1395 weight: Some(400),
1396 ..Default::default()
1397 }),
1398 border: Some(WidgetBorderSpec {
1399 corner_radius: Some(4.0),
1400 ..Default::default()
1401 }),
1402 ..Default::default()
1403 };
1404 let toml_str = toml::to_string(&b).unwrap();
1405 let b2: ButtonTheme = toml::from_str(&toml_str).unwrap();
1406 assert_eq!(b, b2);
1407 }
1408
1409 #[test]
1412 fn window_theme_has_new_fields() {
1413 let w = WindowTheme {
1414 inactive_title_bar_background: Some(Rgba::rgb(180, 180, 180)),
1415 inactive_title_bar_text_color: Some(Rgba::rgb(120, 120, 120)),
1416 title_bar_font: Some(FontSpec {
1417 weight: Some(700),
1418 ..Default::default()
1419 }),
1420 border: Some(WidgetBorderSpec {
1421 corner_radius: Some(4.0),
1422 shadow_enabled: Some(true),
1423 ..Default::default()
1424 }),
1425 ..Default::default()
1426 };
1427 assert!(!w.is_empty());
1428 assert!(w.inactive_title_bar_background.is_some());
1429 assert!(w.inactive_title_bar_text_color.is_some());
1430 assert!(w.title_bar_font.is_some());
1431 assert!(w.border.is_some());
1432 }
1433
1434 #[test]
1435 fn window_theme_default_is_empty() {
1436 assert!(WindowTheme::default().is_empty());
1437 }
1438
1439 #[test]
1442 fn dialog_theme_button_order_works() {
1443 let d = DialogTheme {
1444 button_order: Some(DialogButtonOrder::PrimaryRight),
1445 min_width: Some(300.0),
1446 ..Default::default()
1447 };
1448 assert_eq!(d.button_order, Some(DialogButtonOrder::PrimaryRight));
1449 assert_eq!(d.min_width, Some(300.0));
1450 assert!(!d.is_empty());
1451 }
1452
1453 #[test]
1454 fn dialog_theme_button_order_toml_round_trip() {
1455 let d = DialogTheme {
1456 button_order: Some(DialogButtonOrder::PrimaryLeft),
1457 ..Default::default()
1458 };
1459 let toml_str = toml::to_string(&d).unwrap();
1460 let d2: DialogTheme = toml::from_str(&toml_str).unwrap();
1461 assert_eq!(d, d2);
1462 }
1463
1464 #[test]
1465 fn dialog_theme_default_is_empty() {
1466 assert!(DialogTheme::default().is_empty());
1467 }
1468
1469 #[test]
1472 fn splitter_theme_single_field_merge() {
1473 let mut base = SplitterTheme {
1474 divider_width: Some(4.0),
1475 ..Default::default()
1476 };
1477 let overlay = SplitterTheme {
1478 divider_width: Some(6.0),
1479 ..Default::default()
1480 };
1481 base.merge(&overlay);
1482 assert_eq!(base.divider_width, Some(6.0));
1483 }
1484
1485 #[test]
1486 fn splitter_theme_merge_none_preserves_base() {
1487 let mut base = SplitterTheme {
1488 divider_width: Some(4.0),
1489 ..Default::default()
1490 };
1491 let overlay = SplitterTheme::default();
1492 base.merge(&overlay);
1493 assert_eq!(base.divider_width, Some(4.0));
1494 }
1495
1496 #[test]
1497 fn splitter_theme_default_is_empty() {
1498 assert!(SplitterTheme::default().is_empty());
1499 }
1500
1501 #[test]
1502 fn splitter_theme_not_empty_when_set() {
1503 assert!(
1504 !SplitterTheme {
1505 divider_width: Some(4.0),
1506 ..Default::default()
1507 }
1508 .is_empty()
1509 );
1510 }
1511
1512 #[test]
1515 fn separator_theme_single_field() {
1516 let s = SeparatorTheme {
1517 line_color: Some(Rgba::rgb(200, 200, 200)),
1518 ..Default::default()
1519 };
1520 assert!(!s.is_empty());
1521 }
1522
1523 #[test]
1526 fn all_widget_theme_defaults_are_empty() {
1527 assert!(WindowTheme::default().is_empty());
1528 assert!(ButtonTheme::default().is_empty());
1529 assert!(InputTheme::default().is_empty());
1530 assert!(CheckboxTheme::default().is_empty());
1531 assert!(MenuTheme::default().is_empty());
1532 assert!(TooltipTheme::default().is_empty());
1533 assert!(ScrollbarTheme::default().is_empty());
1534 assert!(SliderTheme::default().is_empty());
1535 assert!(ProgressBarTheme::default().is_empty());
1536 assert!(TabTheme::default().is_empty());
1537 assert!(SidebarTheme::default().is_empty());
1538 assert!(ToolbarTheme::default().is_empty());
1539 assert!(StatusBarTheme::default().is_empty());
1540 assert!(ListTheme::default().is_empty());
1541 assert!(PopoverTheme::default().is_empty());
1542 assert!(SplitterTheme::default().is_empty());
1543 assert!(SeparatorTheme::default().is_empty());
1544 assert!(SwitchTheme::default().is_empty());
1545 assert!(DialogTheme::default().is_empty());
1546 assert!(SpinnerTheme::default().is_empty());
1547 assert!(ComboBoxTheme::default().is_empty());
1548 assert!(SegmentedControlTheme::default().is_empty());
1549 assert!(CardTheme::default().is_empty());
1550 assert!(ExpanderTheme::default().is_empty());
1551 assert!(LinkTheme::default().is_empty());
1552 }
1553
1554 #[test]
1557 fn input_theme_toml_round_trip() {
1558 let t = InputTheme {
1559 background_color: Some(Rgba::rgb(255, 255, 255)),
1560 font: Some(FontSpec {
1561 family: Some("Noto Sans".into()),
1562 ..Default::default()
1563 }),
1564 border: Some(WidgetBorderSpec {
1565 color: Some(Rgba::rgb(180, 180, 180)),
1566 corner_radius: Some(4.0),
1567 ..Default::default()
1568 }),
1569 ..Default::default()
1570 };
1571 let toml_str = toml::to_string(&t).unwrap();
1572 let t2: InputTheme = toml::from_str(&toml_str).unwrap();
1573 assert_eq!(t, t2);
1574 }
1575
1576 #[test]
1577 fn switch_theme_toml_round_trip() {
1578 let s = SwitchTheme {
1579 checked_background: Some(Rgba::rgb(0, 120, 215)),
1580 track_width: Some(40.0),
1581 track_height: Some(20.0),
1582 thumb_diameter: Some(14.0),
1583 track_radius: Some(10.0),
1584 ..Default::default()
1585 };
1586 let toml_str = toml::to_string(&s).unwrap();
1587 let s2: SwitchTheme = toml::from_str(&toml_str).unwrap();
1588 assert_eq!(s, s2);
1589 }
1590
1591 #[test]
1592 fn card_theme_with_border() {
1593 let c = CardTheme {
1594 background_color: Some(Rgba::rgb(255, 255, 255)),
1595 border: Some(WidgetBorderSpec {
1596 corner_radius: Some(8.0),
1597 shadow_enabled: Some(true),
1598 ..Default::default()
1599 }),
1600 };
1601 assert!(!c.is_empty());
1602 }
1603
1604 #[test]
1605 fn link_theme_has_underline_enabled_bool_field() {
1606 let l = LinkTheme {
1607 visited_text_color: Some(Rgba::rgb(100, 0, 200)),
1608 underline_enabled: Some(true),
1609 ..Default::default()
1610 };
1611 assert!(!l.is_empty());
1612 assert_eq!(l.underline_enabled, Some(true));
1613 }
1614
1615 #[test]
1616 fn status_bar_theme_has_font_and_background() {
1617 let s = StatusBarTheme {
1618 background_color: Some(Rgba::rgb(240, 240, 240)),
1619 font: Some(FontSpec {
1620 size: Some(FontSize::Px(11.0)),
1621 ..Default::default()
1622 }),
1623 ..Default::default()
1624 };
1625 assert!(!s.is_empty());
1626 }
1627
1628 #[derive(
1633 Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize, ThemeWidget,
1634 )]
1635 #[serde_with::skip_serializing_none]
1636 #[serde(default)]
1637 #[theme_layer(skip_inventory)]
1638 pub struct DualNestedTestWidget {
1639 pub background: Option<Rgba>,
1640 pub min_height: Option<f32>,
1641 #[theme(nested, resolved_type = "ResolvedFontSpec")]
1642 pub font: Option<FontSpec>,
1643 #[theme(nested, resolved_type = "ResolvedWidgetBorder")]
1644 pub border: Option<WidgetBorderSpec>,
1645 }
1646
1647 #[test]
1648 fn dual_nested_default_is_empty() {
1649 assert!(DualNestedTestWidget::default().is_empty());
1650 }
1651
1652 #[test]
1653 fn dual_nested_field_names() {
1654 assert_eq!(DualNestedTestWidget::FIELD_NAMES.len(), 4);
1655 assert!(DualNestedTestWidget::FIELD_NAMES.contains(&"background"));
1656 assert!(DualNestedTestWidget::FIELD_NAMES.contains(&"min_height"));
1657 assert!(DualNestedTestWidget::FIELD_NAMES.contains(&"font"));
1658 assert!(DualNestedTestWidget::FIELD_NAMES.contains(&"border"));
1659 }
1660
1661 #[test]
1662 fn dual_nested_not_empty_when_font_set() {
1663 let w = DualNestedTestWidget {
1664 font: Some(FontSpec {
1665 family: Some("Inter".into()),
1666 ..Default::default()
1667 }),
1668 ..Default::default()
1669 };
1670 assert!(!w.is_empty());
1671 }
1672
1673 #[test]
1674 fn dual_nested_not_empty_when_border_set() {
1675 let w = DualNestedTestWidget {
1676 border: Some(WidgetBorderSpec {
1677 color: Some(Rgba::rgb(100, 100, 100)),
1678 ..Default::default()
1679 }),
1680 ..Default::default()
1681 };
1682 assert!(!w.is_empty());
1683 }
1684
1685 #[test]
1686 fn dual_nested_merge_both_nested() {
1687 let mut base = DualNestedTestWidget {
1688 font: Some(FontSpec {
1689 family: Some("Noto Sans".into()),
1690 ..Default::default()
1691 }),
1692 ..Default::default()
1693 };
1694 let overlay = DualNestedTestWidget {
1695 border: Some(WidgetBorderSpec {
1696 corner_radius: Some(4.0),
1697 ..Default::default()
1698 }),
1699 ..Default::default()
1700 };
1701 base.merge(&overlay);
1702 assert!(base.font.is_some());
1703 assert!(base.border.is_some());
1704 assert_eq!(
1705 base.font.as_ref().and_then(|f| f.family.as_deref()),
1706 Some("Noto Sans")
1707 );
1708 assert_eq!(
1709 base.border.as_ref().and_then(|b| b.corner_radius),
1710 Some(4.0)
1711 );
1712 }
1713
1714 #[test]
1715 fn dual_nested_merge_inner_font_fields() {
1716 let mut base = DualNestedTestWidget {
1717 font: Some(FontSpec {
1718 family: Some("Noto Sans".into()),
1719 ..Default::default()
1720 }),
1721 ..Default::default()
1722 };
1723 let overlay = DualNestedTestWidget {
1724 font: Some(FontSpec {
1725 size: Some(FontSize::Px(14.0)),
1726 ..Default::default()
1727 }),
1728 ..Default::default()
1729 };
1730 base.merge(&overlay);
1731 let font = base.font.as_ref().unwrap();
1732 assert_eq!(font.family.as_deref(), Some("Noto Sans")); assert_eq!(font.size, Some(FontSize::Px(14.0))); }
1735
1736 #[test]
1737 fn dual_nested_toml_round_trip() {
1738 let w = DualNestedTestWidget {
1739 background: Some(Rgba::rgb(240, 240, 240)),
1740 min_height: Some(32.0),
1741 font: Some(FontSpec {
1742 family: Some("Inter".into()),
1743 size: Some(FontSize::Px(14.0)),
1744 weight: Some(400),
1745 ..Default::default()
1746 }),
1747 border: Some(WidgetBorderSpec {
1748 color: Some(Rgba::rgb(180, 180, 180)),
1749 corner_radius: Some(4.0),
1750 line_width: Some(1.0),
1751 ..Default::default()
1752 }),
1753 };
1754 let toml_str = toml::to_string(&w).unwrap();
1755 let w2: DualNestedTestWidget = toml::from_str(&toml_str).unwrap();
1756 assert_eq!(w, w2);
1757 }
1758
1759 #[test]
1764 fn button_validate_widget_extracts_all_fields() {
1765 let button = ButtonTheme {
1766 background_color: Some(Rgba::rgb(200, 200, 200)),
1767 primary_background: Some(Rgba::rgb(0, 120, 215)),
1768 primary_text_color: Some(Rgba::rgb(255, 255, 255)),
1769 min_width: Some(80.0),
1770 min_height: Some(32.0),
1771 icon_text_gap: Some(8.0),
1772 disabled_opacity: Some(0.4),
1773 hover_background: Some(Rgba::rgb(210, 210, 210)),
1774 hover_text_color: Some(Rgba::rgb(0, 0, 0)),
1775 active_text_color: Some(Rgba::rgb(0, 0, 0)),
1776 disabled_text_color: Some(Rgba::rgb(128, 128, 128)),
1777 active_background: Some(Rgba::rgb(180, 180, 180)),
1778 disabled_background: Some(Rgba::rgb(220, 220, 220)),
1779 checked_background: Some(Rgba::rgb(190, 190, 190)),
1780 checked_text_color: None,
1781 font: Some(FontSpec {
1782 family: Some("Inter".into()),
1783 size: Some(FontSize::Px(14.0)),
1784 weight: Some(400),
1785 style: Some(crate::model::font::FontStyle::Normal),
1786 color: Some(Rgba::rgb(0, 0, 0)),
1787 }),
1788 border: Some(WidgetBorderSpec {
1789 color: Some(Rgba::rgb(100, 100, 100)),
1790 corner_radius: Some(4.0),
1791 line_width: Some(1.0),
1792 shadow_enabled: Some(false),
1793 padding_top: Some(6.0),
1794 padding_right: Some(12.0),
1795 padding_bottom: Some(6.0),
1796 padding_left: Some(12.0),
1797 }),
1798 };
1799 let mut missing = Vec::new();
1800 let resolved = ResolvedButtonTheme::validate_widget(&button, "button", 96.0, &mut missing);
1801 assert!(missing.is_empty(), "unexpected missing: {missing:?}");
1802 assert_eq!(resolved.background_color, Rgba::rgb(200, 200, 200));
1803 assert_eq!(resolved.min_width, 80.0);
1804 assert_eq!(resolved.font.family.as_ref(), "Inter");
1805 assert_eq!(resolved.font.size, 14.0);
1806 assert_eq!(resolved.border.corner_radius, 4.0);
1807 assert_eq!(resolved.active_background, Some(Rgba::rgb(180, 180, 180)));
1809 assert_eq!(resolved.disabled_background, Some(Rgba::rgb(220, 220, 220)));
1810 assert_eq!(resolved.checked_background, Some(Rgba::rgb(190, 190, 190)));
1811 assert_eq!(resolved.checked_text_color, None);
1812 }
1813
1814 #[test]
1815 fn button_validate_widget_records_missing_fields() {
1816 let button = ButtonTheme::default(); let mut missing = Vec::new();
1818 let _ = ResolvedButtonTheme::validate_widget(&button, "button", 96.0, &mut missing);
1819 assert!(missing.contains(&"button.background_color".to_string()));
1821 assert!(missing.contains(&"button.min_width".to_string()));
1822 assert!(missing.contains(&"button.font".to_string()));
1824 assert!(missing.contains(&"button.border".to_string()));
1826 assert!(!missing.iter().any(|m| m.contains("active_background")));
1828 assert!(!missing.iter().any(|m| m.contains("disabled_background")));
1829 }
1830
1831 #[test]
1834 fn layout_theme_default_is_empty() {
1835 assert!(LayoutTheme::default().is_empty());
1836 }
1837
1838 #[test]
1839 fn layout_theme_not_empty_when_widget_gap_set() {
1840 let l = LayoutTheme {
1841 widget_gap: Some(8.0),
1842 ..Default::default()
1843 };
1844 assert!(!l.is_empty());
1845 }
1846
1847 #[test]
1848 fn layout_theme_field_names() {
1849 assert_eq!(LayoutTheme::FIELD_NAMES.len(), 4);
1850 assert!(LayoutTheme::FIELD_NAMES.contains(&"widget_gap_px"));
1851 assert!(LayoutTheme::FIELD_NAMES.contains(&"container_margin_px"));
1852 assert!(LayoutTheme::FIELD_NAMES.contains(&"window_margin_px"));
1853 assert!(LayoutTheme::FIELD_NAMES.contains(&"section_gap_px"));
1854 }
1855
1856 #[test]
1857 fn layout_theme_toml_round_trip() {
1858 let l = LayoutTheme {
1859 widget_gap: Some(8.0),
1860 container_margin: Some(12.0),
1861 window_margin: Some(16.0),
1862 section_gap: Some(24.0),
1863 };
1864 let toml_str = toml::to_string(&l).unwrap();
1865 let l2: LayoutTheme = toml::from_str(&toml_str).unwrap();
1866 assert_eq!(l, l2);
1867 }
1868
1869 #[test]
1870 fn layout_theme_merge() {
1871 let mut base = LayoutTheme {
1872 widget_gap: Some(6.0),
1873 container_margin: Some(10.0),
1874 ..Default::default()
1875 };
1876 let overlay = LayoutTheme {
1877 widget_gap: Some(8.0),
1878 section_gap: Some(24.0),
1879 ..Default::default()
1880 };
1881 base.merge(&overlay);
1882 assert_eq!(base.widget_gap, Some(8.0));
1884 assert_eq!(base.container_margin, Some(10.0));
1886 assert_eq!(base.section_gap, Some(24.0));
1888 assert!(base.window_margin.is_none());
1890 }
1891}