1use crate::Rgba;
4use crate::model::{DialogButtonOrder, FontSpec, ResolvedFontSpec};
5
6macro_rules! define_widget_pair {
33 (
34 $(#[$attr:meta])*
35 $opt_name:ident / $resolved_name:ident {
36 $(option {
37 $($(#[doc = $opt_doc:expr])* $opt_field:ident : $opt_type:ty),* $(,)?
38 })?
39 $(optional_nested {
40 $($(#[doc = $on_doc:expr])* $on_field:ident : [$on_opt_type:ty, $on_res_type:ty]),* $(,)?
41 })?
42 }
43 ) => {
44 $(#[$attr])*
45 #[serde_with::skip_serializing_none]
46 #[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
47 #[serde(default)]
48 pub struct $opt_name {
49 $($($(#[doc = $opt_doc])* pub $opt_field: Option<$opt_type>,)*)?
50 $($($(#[doc = $on_doc])* pub $on_field: Option<$on_opt_type>,)*)?
51 }
52
53 $(#[$attr])*
54 #[derive(Clone, Debug, PartialEq, serde::Serialize)]
55 pub struct $resolved_name {
56 $($($(#[doc = $opt_doc])* pub $opt_field: $opt_type,)*)?
57 $($($(#[doc = $on_doc])* pub $on_field: $on_res_type,)*)?
58 }
59
60 impl_merge!($opt_name {
61 $(option { $($opt_field),* })?
62 $(optional_nested { $($on_field),* })?
63 });
64 };
65}
66
67define_widget_pair! {
70 WindowTheme / ResolvedWindowTheme {
72 option {
73 background: Rgba,
75 foreground: Rgba,
77 border: Rgba,
79 title_bar_background: Rgba,
81 title_bar_foreground: Rgba,
83 inactive_title_bar_background: Rgba,
85 inactive_title_bar_foreground: Rgba,
87 radius: f32,
89 shadow: bool,
91 }
92 optional_nested {
93 title_bar_font: [FontSpec, ResolvedFontSpec],
95 }
96 }
97}
98
99define_widget_pair! {
102 ButtonTheme / ResolvedButtonTheme {
104 option {
105 background: Rgba,
107 foreground: Rgba,
109 border: Rgba,
111 primary_bg: Rgba,
113 primary_fg: Rgba,
115 min_width: f32,
117 min_height: f32,
119 padding_horizontal: f32,
121 padding_vertical: f32,
123 radius: f32,
125 icon_spacing: f32,
127 disabled_opacity: f32,
129 shadow: bool,
131 }
132 optional_nested {
133 font: [FontSpec, ResolvedFontSpec],
135 }
136 }
137}
138
139define_widget_pair! {
142 InputTheme / ResolvedInputTheme {
144 option {
145 background: Rgba,
147 foreground: Rgba,
149 border: Rgba,
151 placeholder: Rgba,
153 caret: Rgba,
155 selection: Rgba,
157 selection_foreground: Rgba,
159 min_height: f32,
161 padding_horizontal: f32,
163 padding_vertical: f32,
165 radius: f32,
167 border_width: f32,
169 }
170 optional_nested {
171 font: [FontSpec, ResolvedFontSpec],
173 }
174 }
175}
176
177define_widget_pair! {
180 CheckboxTheme / ResolvedCheckboxTheme {
182 option {
183 checked_bg: Rgba,
185 indicator_size: f32,
187 spacing: f32,
189 radius: f32,
191 border_width: f32,
193 }
194 }
195}
196
197define_widget_pair! {
200 MenuTheme / ResolvedMenuTheme {
202 option {
203 background: Rgba,
205 foreground: Rgba,
207 separator: Rgba,
209 item_height: f32,
211 padding_horizontal: f32,
213 padding_vertical: f32,
215 icon_spacing: f32,
217 }
218 optional_nested {
219 font: [FontSpec, ResolvedFontSpec],
221 }
222 }
223}
224
225define_widget_pair! {
228 TooltipTheme / ResolvedTooltipTheme {
230 option {
231 background: Rgba,
233 foreground: Rgba,
235 padding_horizontal: f32,
237 padding_vertical: f32,
239 max_width: f32,
241 radius: f32,
243 }
244 optional_nested {
245 font: [FontSpec, ResolvedFontSpec],
247 }
248 }
249}
250
251define_widget_pair! {
254 ScrollbarTheme / ResolvedScrollbarTheme {
256 option {
257 track: Rgba,
259 thumb: Rgba,
261 thumb_hover: Rgba,
263 width: f32,
265 min_thumb_height: f32,
267 slider_width: f32,
269 overlay_mode: bool,
271 }
272 }
273}
274
275define_widget_pair! {
278 SliderTheme / ResolvedSliderTheme {
280 option {
281 fill: Rgba,
283 track: Rgba,
285 thumb: Rgba,
287 track_height: f32,
289 thumb_size: f32,
291 tick_length: f32,
293 }
294 }
295}
296
297define_widget_pair! {
300 ProgressBarTheme / ResolvedProgressBarTheme {
302 option {
303 fill: Rgba,
305 track: Rgba,
307 height: f32,
309 min_width: f32,
311 radius: f32,
313 }
314 }
315}
316
317define_widget_pair! {
320 TabTheme / ResolvedTabTheme {
322 option {
323 background: Rgba,
325 foreground: Rgba,
327 active_background: Rgba,
329 active_foreground: Rgba,
331 bar_background: Rgba,
333 min_width: f32,
335 min_height: f32,
337 padding_horizontal: f32,
339 padding_vertical: f32,
341 }
342 }
343}
344
345define_widget_pair! {
348 SidebarTheme / ResolvedSidebarTheme {
350 option {
351 background: Rgba,
353 foreground: Rgba,
355 }
356 }
357}
358
359define_widget_pair! {
362 ToolbarTheme / ResolvedToolbarTheme {
364 option {
365 height: f32,
367 item_spacing: f32,
369 padding: f32,
371 }
372 optional_nested {
373 font: [FontSpec, ResolvedFontSpec],
375 }
376 }
377}
378
379define_widget_pair! {
382 StatusBarTheme / ResolvedStatusBarTheme {
384 optional_nested {
385 font: [FontSpec, ResolvedFontSpec],
387 }
388 }
389}
390
391define_widget_pair! {
394 ListTheme / ResolvedListTheme {
396 option {
397 background: Rgba,
399 foreground: Rgba,
401 alternate_row: Rgba,
403 selection: Rgba,
405 selection_foreground: Rgba,
407 header_background: Rgba,
409 header_foreground: Rgba,
411 grid_color: Rgba,
413 item_height: f32,
415 padding_horizontal: f32,
417 padding_vertical: f32,
419 }
420 }
421}
422
423define_widget_pair! {
426 PopoverTheme / ResolvedPopoverTheme {
428 option {
429 background: Rgba,
431 foreground: Rgba,
433 border: Rgba,
435 radius: f32,
437 }
438 }
439}
440
441define_widget_pair! {
444 SplitterTheme / ResolvedSplitterTheme {
446 option {
447 width: f32,
449 }
450 }
451}
452
453define_widget_pair! {
456 SeparatorTheme / ResolvedSeparatorTheme {
458 option {
459 color: Rgba,
461 }
462 }
463}
464
465define_widget_pair! {
468 SwitchTheme / ResolvedSwitchTheme {
470 option {
471 checked_bg: Rgba,
473 unchecked_bg: Rgba,
475 thumb_bg: Rgba,
477 track_width: f32,
479 track_height: f32,
481 thumb_size: f32,
483 track_radius: f32,
485 }
486 }
487}
488
489define_widget_pair! {
492 DialogTheme / ResolvedDialogTheme {
494 option {
495 min_width: f32,
497 max_width: f32,
499 min_height: f32,
501 max_height: f32,
503 content_padding: f32,
505 button_spacing: f32,
507 radius: f32,
509 icon_size: f32,
511 button_order: DialogButtonOrder,
513 }
514 optional_nested {
515 title_font: [FontSpec, ResolvedFontSpec],
517 }
518 }
519}
520
521define_widget_pair! {
524 SpinnerTheme / ResolvedSpinnerTheme {
526 option {
527 fill: Rgba,
529 diameter: f32,
531 min_size: f32,
533 stroke_width: f32,
535 }
536 }
537}
538
539define_widget_pair! {
542 ComboBoxTheme / ResolvedComboBoxTheme {
544 option {
545 min_height: f32,
547 min_width: f32,
549 padding_horizontal: f32,
551 arrow_size: f32,
553 arrow_area_width: f32,
555 radius: f32,
557 }
558 }
559}
560
561define_widget_pair! {
564 SegmentedControlTheme / ResolvedSegmentedControlTheme {
566 option {
567 segment_height: f32,
569 separator_width: f32,
571 padding_horizontal: f32,
573 radius: f32,
575 }
576 }
577}
578
579define_widget_pair! {
582 CardTheme / ResolvedCardTheme {
584 option {
585 background: Rgba,
587 border: Rgba,
589 radius: f32,
591 padding: f32,
593 shadow: bool,
595 }
596 }
597}
598
599define_widget_pair! {
602 ExpanderTheme / ResolvedExpanderTheme {
604 option {
605 header_height: f32,
607 arrow_size: f32,
609 content_padding: f32,
611 radius: f32,
613 }
614 }
615}
616
617define_widget_pair! {
620 LinkTheme / ResolvedLinkTheme {
622 option {
623 color: Rgba,
625 visited: Rgba,
627 background: Rgba,
629 hover_bg: Rgba,
631 underline: bool,
633 }
634 }
635}
636
637#[cfg(test)]
638#[allow(clippy::unwrap_used, clippy::expect_used)]
639mod tests {
640 use super::*;
641 use crate::Rgba;
642 use crate::model::{DialogButtonOrder, FontSpec};
643
644 define_widget_pair! {
646 TestWidget / ResolvedTestWidget {
648 option {
649 size: f32,
650 label: String,
651 }
652 optional_nested {
653 font: [FontSpec, ResolvedFontSpec],
654 }
655 }
656 }
657
658 #[test]
661 fn resolved_font_spec_fields_are_concrete() {
662 let rfs = ResolvedFontSpec {
663 family: "Inter".into(),
664 size: 14.0,
665 weight: 400,
666 };
667 assert_eq!(rfs.family, "Inter");
668 assert_eq!(rfs.size, 14.0);
669 assert_eq!(rfs.weight, 400);
670 }
671
672 #[test]
675 fn generated_option_struct_has_option_fields() {
676 let w = TestWidget::default();
677 assert!(w.size.is_none());
678 assert!(w.label.is_none());
679 assert!(w.font.is_none());
680 }
681
682 #[test]
683 fn generated_option_struct_is_empty_by_default() {
684 assert!(TestWidget::default().is_empty());
685 }
686
687 #[test]
688 fn generated_option_struct_not_empty_when_size_set() {
689 let w = TestWidget {
690 size: Some(24.0),
691 ..Default::default()
692 };
693 assert!(!w.is_empty());
694 }
695
696 #[test]
697 fn generated_option_struct_not_empty_when_font_set() {
698 let w = TestWidget {
699 font: Some(FontSpec {
700 size: Some(14.0),
701 ..Default::default()
702 }),
703 ..Default::default()
704 };
705 assert!(!w.is_empty());
706 }
707
708 #[test]
709 fn generated_resolved_struct_has_concrete_fields() {
710 let resolved = ResolvedTestWidget {
711 size: 24.0,
712 label: "Click me".into(),
713 font: ResolvedFontSpec {
714 family: "Inter".into(),
715 size: 14.0,
716 weight: 400,
717 },
718 };
719 assert_eq!(resolved.size, 24.0);
720 assert_eq!(resolved.label, "Click me");
721 assert_eq!(resolved.font.family, "Inter");
722 }
723
724 #[test]
727 fn generated_merge_option_field_overlay_wins() {
728 let mut base = TestWidget {
729 size: Some(20.0),
730 ..Default::default()
731 };
732 let overlay = TestWidget {
733 size: Some(24.0),
734 ..Default::default()
735 };
736 base.merge(&overlay);
737 assert_eq!(base.size, Some(24.0));
738 }
739
740 #[test]
741 fn generated_merge_option_field_none_preserves_base() {
742 let mut base = TestWidget {
743 size: Some(20.0),
744 ..Default::default()
745 };
746 let overlay = TestWidget::default();
747 base.merge(&overlay);
748 assert_eq!(base.size, Some(20.0));
749 }
750
751 #[test]
752 fn generated_merge_optional_nested_both_some_merges_inner() {
753 let mut base = TestWidget {
754 font: Some(FontSpec {
755 family: Some("Noto Sans".into()),
756 size: Some(12.0),
757 weight: None,
758 }),
759 ..Default::default()
760 };
761 let overlay = TestWidget {
762 font: Some(FontSpec {
763 family: None,
764 size: None,
765 weight: Some(700),
766 }),
767 ..Default::default()
768 };
769 base.merge(&overlay);
770 let font = base.font.as_ref().unwrap();
771 assert_eq!(font.family.as_deref(), Some("Noto Sans")); assert_eq!(font.size, Some(12.0)); assert_eq!(font.weight, Some(700)); }
775
776 #[test]
777 fn generated_merge_optional_nested_none_plus_some_clones() {
778 let mut base = TestWidget::default();
779 let overlay = TestWidget {
780 font: Some(FontSpec {
781 family: Some("Inter".into()),
782 size: Some(14.0),
783 weight: Some(400),
784 }),
785 ..Default::default()
786 };
787 base.merge(&overlay);
788 let font = base.font.as_ref().unwrap();
789 assert_eq!(font.family.as_deref(), Some("Inter"));
790 assert_eq!(font.size, Some(14.0));
791 assert_eq!(font.weight, Some(400));
792 }
793
794 #[test]
795 fn generated_merge_optional_nested_some_plus_none_preserves_base() {
796 let mut base = TestWidget {
797 font: Some(FontSpec {
798 family: Some("Inter".into()),
799 size: Some(14.0),
800 weight: Some(400),
801 }),
802 ..Default::default()
803 };
804 let overlay = TestWidget::default();
805 base.merge(&overlay);
806 let font = base.font.as_ref().unwrap();
807 assert_eq!(font.family.as_deref(), Some("Inter"));
808 }
809
810 #[test]
811 fn generated_merge_optional_nested_none_plus_none_stays_none() {
812 let mut base = TestWidget::default();
813 let overlay = TestWidget::default();
814 base.merge(&overlay);
815 assert!(base.font.is_none());
816 }
817
818 #[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
822 struct WithFont {
823 name: Option<String>,
824 font: Option<FontSpec>,
825 }
826
827 impl_merge!(WithFont {
828 option { name }
829 optional_nested { font }
830 });
831
832 #[test]
833 fn impl_merge_optional_nested_none_none_stays_none() {
834 let mut base = WithFont::default();
835 let overlay = WithFont::default();
836 base.merge(&overlay);
837 assert!(base.font.is_none());
838 }
839
840 #[test]
841 fn impl_merge_optional_nested_some_none_preserves_base() {
842 let mut base = WithFont {
843 font: Some(FontSpec {
844 size: Some(12.0),
845 ..Default::default()
846 }),
847 ..Default::default()
848 };
849 let overlay = WithFont::default();
850 base.merge(&overlay);
851 assert_eq!(base.font.as_ref().unwrap().size, Some(12.0));
852 }
853
854 #[test]
855 fn impl_merge_optional_nested_none_some_clones_overlay() {
856 let mut base = WithFont::default();
857 let overlay = WithFont {
858 font: Some(FontSpec {
859 family: Some("Inter".into()),
860 ..Default::default()
861 }),
862 ..Default::default()
863 };
864 base.merge(&overlay);
865 assert_eq!(base.font.as_ref().unwrap().family.as_deref(), Some("Inter"));
866 }
867
868 #[test]
869 fn impl_merge_optional_nested_some_some_merges_inner() {
870 let mut base = WithFont {
871 font: Some(FontSpec {
872 family: Some("Noto".into()),
873 size: Some(11.0),
874 weight: None,
875 }),
876 ..Default::default()
877 };
878 let overlay = WithFont {
879 font: Some(FontSpec {
880 family: None,
881 size: Some(14.0),
882 weight: Some(400),
883 }),
884 ..Default::default()
885 };
886 base.merge(&overlay);
887 let f = base.font.as_ref().unwrap();
888 assert_eq!(f.family.as_deref(), Some("Noto")); assert_eq!(f.size, Some(14.0)); assert_eq!(f.weight, Some(400)); }
892
893 #[test]
894 fn impl_merge_optional_nested_is_empty_none() {
895 let w = WithFont::default();
896 assert!(w.is_empty());
897 }
898
899 #[test]
900 fn impl_merge_optional_nested_is_empty_some() {
901 let w = WithFont {
902 font: Some(FontSpec::default()),
903 ..Default::default()
904 };
905 assert!(!w.is_empty());
906 }
907
908 #[test]
911 fn button_theme_has_all_fields_and_not_empty_when_set() {
912 let b = ButtonTheme {
913 background: Some(Rgba::rgb(200, 200, 200)),
914 foreground: Some(Rgba::rgb(30, 30, 30)),
915 border: Some(Rgba::rgb(150, 150, 150)),
916 primary_bg: Some(Rgba::rgb(0, 120, 215)),
917 primary_fg: Some(Rgba::rgb(255, 255, 255)),
918 min_width: Some(64.0),
919 min_height: Some(28.0),
920 padding_horizontal: Some(12.0),
921 padding_vertical: Some(6.0),
922 radius: Some(4.0),
923 icon_spacing: Some(6.0),
924 disabled_opacity: Some(0.5),
925 shadow: Some(false),
926 font: Some(FontSpec {
927 family: Some("Inter".into()),
928 size: Some(14.0),
929 weight: Some(400),
930 }),
931 };
932 assert!(!b.is_empty());
933 assert_eq!(b.min_width, Some(64.0));
934 assert_eq!(b.primary_bg, Some(Rgba::rgb(0, 120, 215)));
935 }
936
937 #[test]
938 fn button_theme_default_is_empty() {
939 assert!(ButtonTheme::default().is_empty());
940 }
941
942 #[test]
943 fn button_theme_merge_font_optional_nested() {
944 let mut base = ButtonTheme {
945 font: Some(FontSpec {
946 family: Some("Noto Sans".into()),
947 size: Some(11.0),
948 weight: None,
949 }),
950 ..Default::default()
951 };
952 let overlay = ButtonTheme {
953 font: Some(FontSpec {
954 family: None,
955 weight: Some(700),
956 ..Default::default()
957 }),
958 ..Default::default()
959 };
960 base.merge(&overlay);
961 let f = base.font.as_ref().unwrap();
962 assert_eq!(f.family.as_deref(), Some("Noto Sans")); assert_eq!(f.weight, Some(700)); }
965
966 #[test]
967 fn button_theme_toml_round_trip_with_font() {
968 let b = ButtonTheme {
969 background: Some(Rgba::rgb(200, 200, 200)),
970 radius: Some(4.0),
971 font: Some(FontSpec {
972 family: Some("Inter".into()),
973 size: Some(14.0),
974 weight: Some(400),
975 }),
976 ..Default::default()
977 };
978 let toml_str = toml::to_string(&b).unwrap();
979 let b2: ButtonTheme = toml::from_str(&toml_str).unwrap();
980 assert_eq!(b, b2);
981 }
982
983 #[test]
986 fn window_theme_has_inactive_title_bar_fields() {
987 let w = WindowTheme {
988 inactive_title_bar_background: Some(Rgba::rgb(180, 180, 180)),
989 inactive_title_bar_foreground: Some(Rgba::rgb(120, 120, 120)),
990 title_bar_font: Some(FontSpec {
991 weight: Some(700),
992 ..Default::default()
993 }),
994 ..Default::default()
995 };
996 assert!(!w.is_empty());
997 assert!(w.inactive_title_bar_background.is_some());
998 assert!(w.inactive_title_bar_foreground.is_some());
999 assert!(w.title_bar_font.is_some());
1000 }
1001
1002 #[test]
1003 fn window_theme_default_is_empty() {
1004 assert!(WindowTheme::default().is_empty());
1005 }
1006
1007 #[test]
1010 fn dialog_theme_button_order_works() {
1011 let d = DialogTheme {
1012 button_order: Some(DialogButtonOrder::TrailingAffirmative),
1013 min_width: Some(300.0),
1014 ..Default::default()
1015 };
1016 assert_eq!(d.button_order, Some(DialogButtonOrder::TrailingAffirmative));
1017 assert_eq!(d.min_width, Some(300.0));
1018 assert!(!d.is_empty());
1019 }
1020
1021 #[test]
1022 fn dialog_theme_button_order_toml_round_trip() {
1023 let d = DialogTheme {
1024 button_order: Some(DialogButtonOrder::LeadingAffirmative),
1025 radius: Some(8.0),
1026 ..Default::default()
1027 };
1028 let toml_str = toml::to_string(&d).unwrap();
1029 let d2: DialogTheme = toml::from_str(&toml_str).unwrap();
1030 assert_eq!(d, d2);
1031 }
1032
1033 #[test]
1034 fn dialog_theme_default_is_empty() {
1035 assert!(DialogTheme::default().is_empty());
1036 }
1037
1038 #[test]
1041 fn splitter_theme_single_field_merge() {
1042 let mut base = SplitterTheme { width: Some(4.0) };
1043 let overlay = SplitterTheme { width: Some(6.0) };
1044 base.merge(&overlay);
1045 assert_eq!(base.width, Some(6.0));
1046 }
1047
1048 #[test]
1049 fn splitter_theme_merge_none_preserves_base() {
1050 let mut base = SplitterTheme { width: Some(4.0) };
1051 let overlay = SplitterTheme::default();
1052 base.merge(&overlay);
1053 assert_eq!(base.width, Some(4.0));
1054 }
1055
1056 #[test]
1057 fn splitter_theme_default_is_empty() {
1058 assert!(SplitterTheme::default().is_empty());
1059 }
1060
1061 #[test]
1062 fn splitter_theme_not_empty_when_set() {
1063 assert!(!SplitterTheme { width: Some(4.0) }.is_empty());
1064 }
1065
1066 #[test]
1069 fn separator_theme_single_field() {
1070 let s = SeparatorTheme {
1071 color: Some(Rgba::rgb(200, 200, 200)),
1072 };
1073 assert!(!s.is_empty());
1074 }
1075
1076 #[test]
1079 fn all_widget_theme_defaults_are_empty() {
1080 assert!(WindowTheme::default().is_empty());
1081 assert!(ButtonTheme::default().is_empty());
1082 assert!(InputTheme::default().is_empty());
1083 assert!(CheckboxTheme::default().is_empty());
1084 assert!(MenuTheme::default().is_empty());
1085 assert!(TooltipTheme::default().is_empty());
1086 assert!(ScrollbarTheme::default().is_empty());
1087 assert!(SliderTheme::default().is_empty());
1088 assert!(ProgressBarTheme::default().is_empty());
1089 assert!(TabTheme::default().is_empty());
1090 assert!(SidebarTheme::default().is_empty());
1091 assert!(ToolbarTheme::default().is_empty());
1092 assert!(StatusBarTheme::default().is_empty());
1093 assert!(ListTheme::default().is_empty());
1094 assert!(PopoverTheme::default().is_empty());
1095 assert!(SplitterTheme::default().is_empty());
1096 assert!(SeparatorTheme::default().is_empty());
1097 assert!(SwitchTheme::default().is_empty());
1098 assert!(DialogTheme::default().is_empty());
1099 assert!(SpinnerTheme::default().is_empty());
1100 assert!(ComboBoxTheme::default().is_empty());
1101 assert!(SegmentedControlTheme::default().is_empty());
1102 assert!(CardTheme::default().is_empty());
1103 assert!(ExpanderTheme::default().is_empty());
1104 assert!(LinkTheme::default().is_empty());
1105 }
1106
1107 #[test]
1110 fn input_theme_toml_round_trip() {
1111 let t = InputTheme {
1112 background: Some(Rgba::rgb(255, 255, 255)),
1113 border: Some(Rgba::rgb(180, 180, 180)),
1114 radius: Some(4.0),
1115 font: Some(FontSpec {
1116 family: Some("Noto Sans".into()),
1117 ..Default::default()
1118 }),
1119 ..Default::default()
1120 };
1121 let toml_str = toml::to_string(&t).unwrap();
1122 let t2: InputTheme = toml::from_str(&toml_str).unwrap();
1123 assert_eq!(t, t2);
1124 }
1125
1126 #[test]
1127 fn switch_theme_toml_round_trip() {
1128 let s = SwitchTheme {
1129 checked_bg: Some(Rgba::rgb(0, 120, 215)),
1130 track_width: Some(40.0),
1131 track_height: Some(20.0),
1132 thumb_size: Some(14.0),
1133 track_radius: Some(10.0),
1134 ..Default::default()
1135 };
1136 let toml_str = toml::to_string(&s).unwrap();
1137 let s2: SwitchTheme = toml::from_str(&toml_str).unwrap();
1138 assert_eq!(s, s2);
1139 }
1140
1141 #[test]
1142 fn card_theme_has_shadow_bool_field() {
1143 let c = CardTheme {
1144 shadow: Some(true),
1145 radius: Some(8.0),
1146 ..Default::default()
1147 };
1148 assert!(!c.is_empty());
1149 assert_eq!(c.shadow, Some(true));
1150 }
1151
1152 #[test]
1153 fn link_theme_has_underline_bool_field() {
1154 let l = LinkTheme {
1155 color: Some(Rgba::rgb(0, 100, 200)),
1156 underline: Some(true),
1157 ..Default::default()
1158 };
1159 assert!(!l.is_empty());
1160 assert_eq!(l.underline, Some(true));
1161 }
1162
1163 #[test]
1164 fn status_bar_theme_has_only_font_field() {
1165 let s = StatusBarTheme {
1167 font: Some(FontSpec {
1168 size: Some(11.0),
1169 ..Default::default()
1170 }),
1171 };
1172 assert!(!s.is_empty());
1173 }
1174}