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, serde::Deserialize)]
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 $opt_name {
61 pub const FIELD_NAMES: &[&str] = &[
63 $($(stringify!($opt_field),)*)?
64 $($(stringify!($on_field),)*)?
65 ];
66 }
67
68 impl_merge!($opt_name {
69 $(option { $($opt_field),* })?
70 $(optional_nested { $($on_field),* })?
71 });
72 };
73}
74
75define_widget_pair! {
78 WindowTheme / ResolvedWindowTheme {
80 option {
81 background: Rgba,
83 foreground: Rgba,
85 border: Rgba,
87 title_bar_background: Rgba,
89 title_bar_foreground: Rgba,
91 inactive_title_bar_background: Rgba,
93 inactive_title_bar_foreground: Rgba,
95 radius: f32,
97 shadow: bool,
99 }
100 optional_nested {
101 title_bar_font: [FontSpec, ResolvedFontSpec],
103 }
104 }
105}
106
107define_widget_pair! {
110 ButtonTheme / ResolvedButtonTheme {
112 option {
113 background: Rgba,
115 foreground: Rgba,
117 border: Rgba,
119 primary_background: Rgba,
121 primary_foreground: Rgba,
123 min_width: f32,
125 min_height: f32,
127 padding_horizontal: f32,
129 padding_vertical: f32,
131 radius: f32,
133 icon_spacing: f32,
135 disabled_opacity: f32,
137 shadow: bool,
139 }
140 optional_nested {
141 font: [FontSpec, ResolvedFontSpec],
143 }
144 }
145}
146
147define_widget_pair! {
150 InputTheme / ResolvedInputTheme {
152 option {
153 background: Rgba,
155 foreground: Rgba,
157 border: Rgba,
159 placeholder: Rgba,
161 caret: Rgba,
163 selection: Rgba,
165 selection_foreground: Rgba,
167 min_height: f32,
169 padding_horizontal: f32,
171 padding_vertical: f32,
173 radius: f32,
175 border_width: f32,
177 }
178 optional_nested {
179 font: [FontSpec, ResolvedFontSpec],
181 }
182 }
183}
184
185define_widget_pair! {
188 CheckboxTheme / ResolvedCheckboxTheme {
190 option {
191 checked_background: Rgba,
193 indicator_size: f32,
195 spacing: f32,
197 radius: f32,
199 border_width: f32,
201 }
202 }
203}
204
205define_widget_pair! {
208 MenuTheme / ResolvedMenuTheme {
210 option {
211 background: Rgba,
213 foreground: Rgba,
215 separator: Rgba,
217 item_height: f32,
219 padding_horizontal: f32,
221 padding_vertical: f32,
223 icon_spacing: f32,
225 }
226 optional_nested {
227 font: [FontSpec, ResolvedFontSpec],
229 }
230 }
231}
232
233define_widget_pair! {
236 TooltipTheme / ResolvedTooltipTheme {
238 option {
239 background: Rgba,
241 foreground: Rgba,
243 padding_horizontal: f32,
245 padding_vertical: f32,
247 max_width: f32,
249 radius: f32,
251 }
252 optional_nested {
253 font: [FontSpec, ResolvedFontSpec],
255 }
256 }
257}
258
259define_widget_pair! {
262 ScrollbarTheme / ResolvedScrollbarTheme {
264 option {
265 track: Rgba,
267 thumb: Rgba,
269 thumb_hover: Rgba,
271 width: f32,
273 min_thumb_height: f32,
275 slider_width: f32,
277 overlay_mode: bool,
279 }
280 }
281}
282
283define_widget_pair! {
286 SliderTheme / ResolvedSliderTheme {
288 option {
289 fill: Rgba,
291 track: Rgba,
293 thumb: Rgba,
295 track_height: f32,
297 thumb_size: f32,
299 tick_length: f32,
301 }
302 }
303}
304
305define_widget_pair! {
308 ProgressBarTheme / ResolvedProgressBarTheme {
310 option {
311 fill: Rgba,
313 track: Rgba,
315 height: f32,
317 min_width: f32,
319 radius: f32,
321 }
322 }
323}
324
325define_widget_pair! {
328 TabTheme / ResolvedTabTheme {
330 option {
331 background: Rgba,
333 foreground: Rgba,
335 active_background: Rgba,
337 active_foreground: Rgba,
339 bar_background: Rgba,
341 min_width: f32,
343 min_height: f32,
345 padding_horizontal: f32,
347 padding_vertical: f32,
349 }
350 }
351}
352
353define_widget_pair! {
356 SidebarTheme / ResolvedSidebarTheme {
358 option {
359 background: Rgba,
361 foreground: Rgba,
363 }
364 }
365}
366
367define_widget_pair! {
370 ToolbarTheme / ResolvedToolbarTheme {
372 option {
373 height: f32,
375 item_spacing: f32,
377 padding: f32,
379 }
380 optional_nested {
381 font: [FontSpec, ResolvedFontSpec],
383 }
384 }
385}
386
387define_widget_pair! {
390 StatusBarTheme / ResolvedStatusBarTheme {
392 optional_nested {
393 font: [FontSpec, ResolvedFontSpec],
395 }
396 }
397}
398
399define_widget_pair! {
402 ListTheme / ResolvedListTheme {
404 option {
405 background: Rgba,
407 foreground: Rgba,
409 alternate_row: Rgba,
411 selection: Rgba,
413 selection_foreground: Rgba,
415 header_background: Rgba,
417 header_foreground: Rgba,
419 grid_color: Rgba,
421 item_height: f32,
423 padding_horizontal: f32,
425 padding_vertical: f32,
427 }
428 }
429}
430
431define_widget_pair! {
434 PopoverTheme / ResolvedPopoverTheme {
436 option {
437 background: Rgba,
439 foreground: Rgba,
441 border: Rgba,
443 radius: f32,
445 }
446 }
447}
448
449define_widget_pair! {
452 SplitterTheme / ResolvedSplitterTheme {
454 option {
455 width: f32,
457 }
458 }
459}
460
461define_widget_pair! {
464 SeparatorTheme / ResolvedSeparatorTheme {
466 option {
467 color: Rgba,
469 }
470 }
471}
472
473define_widget_pair! {
476 SwitchTheme / ResolvedSwitchTheme {
478 option {
479 checked_background: Rgba,
481 unchecked_background: Rgba,
483 thumb_background: Rgba,
485 track_width: f32,
487 track_height: f32,
489 thumb_size: f32,
491 track_radius: f32,
493 }
494 }
495}
496
497define_widget_pair! {
500 DialogTheme / ResolvedDialogTheme {
502 option {
503 min_width: f32,
505 max_width: f32,
507 min_height: f32,
509 max_height: f32,
511 content_padding: f32,
513 button_spacing: f32,
515 radius: f32,
517 icon_size: f32,
519 button_order: DialogButtonOrder,
521 }
522 optional_nested {
523 title_font: [FontSpec, ResolvedFontSpec],
525 }
526 }
527}
528
529define_widget_pair! {
532 SpinnerTheme / ResolvedSpinnerTheme {
534 option {
535 fill: Rgba,
537 diameter: f32,
539 min_size: f32,
541 stroke_width: f32,
543 }
544 }
545}
546
547define_widget_pair! {
550 ComboBoxTheme / ResolvedComboBoxTheme {
552 option {
553 min_height: f32,
555 min_width: f32,
557 padding_horizontal: f32,
559 arrow_size: f32,
561 arrow_area_width: f32,
563 radius: f32,
565 }
566 }
567}
568
569define_widget_pair! {
572 SegmentedControlTheme / ResolvedSegmentedControlTheme {
574 option {
575 segment_height: f32,
577 separator_width: f32,
579 padding_horizontal: f32,
581 radius: f32,
583 }
584 }
585}
586
587define_widget_pair! {
590 CardTheme / ResolvedCardTheme {
592 option {
593 background: Rgba,
595 border: Rgba,
597 radius: f32,
599 padding: f32,
601 shadow: bool,
603 }
604 }
605}
606
607define_widget_pair! {
610 ExpanderTheme / ResolvedExpanderTheme {
612 option {
613 header_height: f32,
615 arrow_size: f32,
617 content_padding: f32,
619 radius: f32,
621 }
622 }
623}
624
625define_widget_pair! {
628 LinkTheme / ResolvedLinkTheme {
630 option {
631 color: Rgba,
633 visited: Rgba,
635 background: Rgba,
637 hover_bg: Rgba,
639 underline: bool,
641 }
642 }
643}
644
645#[cfg(test)]
646#[allow(clippy::unwrap_used, clippy::expect_used, dead_code)]
647mod tests {
648 use super::*;
649 use crate::Rgba;
650 use crate::model::{DialogButtonOrder, FontSpec};
651
652 define_widget_pair! {
654 TestWidget / ResolvedTestWidget {
656 option {
657 size: f32,
658 label: String,
659 }
660 optional_nested {
661 font: [FontSpec, ResolvedFontSpec],
662 }
663 }
664 }
665
666 #[test]
669 fn resolved_font_spec_fields_are_concrete() {
670 let rfs = ResolvedFontSpec {
671 family: "Inter".into(),
672 size: 14.0,
673 weight: 400,
674 };
675 assert_eq!(rfs.family, "Inter");
676 assert_eq!(rfs.size, 14.0);
677 assert_eq!(rfs.weight, 400);
678 }
679
680 #[test]
683 fn generated_option_struct_has_option_fields() {
684 let w = TestWidget::default();
685 assert!(w.size.is_none());
686 assert!(w.label.is_none());
687 assert!(w.font.is_none());
688 }
689
690 #[test]
691 fn generated_option_struct_is_empty_by_default() {
692 assert!(TestWidget::default().is_empty());
693 }
694
695 #[test]
696 fn generated_option_struct_not_empty_when_size_set() {
697 let w = TestWidget {
698 size: Some(24.0),
699 ..Default::default()
700 };
701 assert!(!w.is_empty());
702 }
703
704 #[test]
705 fn generated_option_struct_not_empty_when_font_set() {
706 let w = TestWidget {
707 font: Some(FontSpec {
708 size: Some(14.0),
709 ..Default::default()
710 }),
711 ..Default::default()
712 };
713 assert!(!w.is_empty());
714 }
715
716 #[test]
717 fn generated_resolved_struct_has_concrete_fields() {
718 let resolved = ResolvedTestWidget {
719 size: 24.0,
720 label: "Click me".into(),
721 font: ResolvedFontSpec {
722 family: "Inter".into(),
723 size: 14.0,
724 weight: 400,
725 },
726 };
727 assert_eq!(resolved.size, 24.0);
728 assert_eq!(resolved.label, "Click me");
729 assert_eq!(resolved.font.family, "Inter");
730 }
731
732 #[test]
735 fn generated_merge_option_field_overlay_wins() {
736 let mut base = TestWidget {
737 size: Some(20.0),
738 ..Default::default()
739 };
740 let overlay = TestWidget {
741 size: Some(24.0),
742 ..Default::default()
743 };
744 base.merge(&overlay);
745 assert_eq!(base.size, Some(24.0));
746 }
747
748 #[test]
749 fn generated_merge_option_field_none_preserves_base() {
750 let mut base = TestWidget {
751 size: Some(20.0),
752 ..Default::default()
753 };
754 let overlay = TestWidget::default();
755 base.merge(&overlay);
756 assert_eq!(base.size, Some(20.0));
757 }
758
759 #[test]
760 fn generated_merge_optional_nested_both_some_merges_inner() {
761 let mut base = TestWidget {
762 font: Some(FontSpec {
763 family: Some("Noto Sans".into()),
764 size: Some(12.0),
765 weight: None,
766 }),
767 ..Default::default()
768 };
769 let overlay = TestWidget {
770 font: Some(FontSpec {
771 family: None,
772 size: None,
773 weight: Some(700),
774 }),
775 ..Default::default()
776 };
777 base.merge(&overlay);
778 let font = base.font.as_ref().unwrap();
779 assert_eq!(font.family.as_deref(), Some("Noto Sans")); assert_eq!(font.size, Some(12.0)); assert_eq!(font.weight, Some(700)); }
783
784 #[test]
785 fn generated_merge_optional_nested_none_plus_some_clones() {
786 let mut base = TestWidget::default();
787 let overlay = TestWidget {
788 font: Some(FontSpec {
789 family: Some("Inter".into()),
790 size: Some(14.0),
791 weight: Some(400),
792 }),
793 ..Default::default()
794 };
795 base.merge(&overlay);
796 let font = base.font.as_ref().unwrap();
797 assert_eq!(font.family.as_deref(), Some("Inter"));
798 assert_eq!(font.size, Some(14.0));
799 assert_eq!(font.weight, Some(400));
800 }
801
802 #[test]
803 fn generated_merge_optional_nested_some_plus_none_preserves_base() {
804 let mut base = TestWidget {
805 font: Some(FontSpec {
806 family: Some("Inter".into()),
807 size: Some(14.0),
808 weight: Some(400),
809 }),
810 ..Default::default()
811 };
812 let overlay = TestWidget::default();
813 base.merge(&overlay);
814 let font = base.font.as_ref().unwrap();
815 assert_eq!(font.family.as_deref(), Some("Inter"));
816 }
817
818 #[test]
819 fn generated_merge_optional_nested_none_plus_none_stays_none() {
820 let mut base = TestWidget::default();
821 let overlay = TestWidget::default();
822 base.merge(&overlay);
823 assert!(base.font.is_none());
824 }
825
826 #[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
830 struct WithFont {
831 name: Option<String>,
832 font: Option<FontSpec>,
833 }
834
835 impl_merge!(WithFont {
836 option { name }
837 optional_nested { font }
838 });
839
840 #[test]
841 fn impl_merge_optional_nested_none_none_stays_none() {
842 let mut base = WithFont::default();
843 let overlay = WithFont::default();
844 base.merge(&overlay);
845 assert!(base.font.is_none());
846 }
847
848 #[test]
849 fn impl_merge_optional_nested_some_none_preserves_base() {
850 let mut base = WithFont {
851 font: Some(FontSpec {
852 size: Some(12.0),
853 ..Default::default()
854 }),
855 ..Default::default()
856 };
857 let overlay = WithFont::default();
858 base.merge(&overlay);
859 assert_eq!(base.font.as_ref().unwrap().size, Some(12.0));
860 }
861
862 #[test]
863 fn impl_merge_optional_nested_none_some_clones_overlay() {
864 let mut base = WithFont::default();
865 let overlay = WithFont {
866 font: Some(FontSpec {
867 family: Some("Inter".into()),
868 ..Default::default()
869 }),
870 ..Default::default()
871 };
872 base.merge(&overlay);
873 assert_eq!(base.font.as_ref().unwrap().family.as_deref(), Some("Inter"));
874 }
875
876 #[test]
877 fn impl_merge_optional_nested_some_some_merges_inner() {
878 let mut base = WithFont {
879 font: Some(FontSpec {
880 family: Some("Noto".into()),
881 size: Some(11.0),
882 weight: None,
883 }),
884 ..Default::default()
885 };
886 let overlay = WithFont {
887 font: Some(FontSpec {
888 family: None,
889 size: Some(14.0),
890 weight: Some(400),
891 }),
892 ..Default::default()
893 };
894 base.merge(&overlay);
895 let f = base.font.as_ref().unwrap();
896 assert_eq!(f.family.as_deref(), Some("Noto")); assert_eq!(f.size, Some(14.0)); assert_eq!(f.weight, Some(400)); }
900
901 #[test]
902 fn impl_merge_optional_nested_is_empty_none() {
903 let w = WithFont::default();
904 assert!(w.is_empty());
905 }
906
907 #[test]
908 fn impl_merge_optional_nested_is_empty_some() {
909 let w = WithFont {
910 font: Some(FontSpec::default()),
911 ..Default::default()
912 };
913 assert!(!w.is_empty());
914 }
915
916 #[test]
919 fn button_theme_has_all_fields_and_not_empty_when_set() {
920 let b = ButtonTheme {
921 background: Some(Rgba::rgb(200, 200, 200)),
922 foreground: Some(Rgba::rgb(30, 30, 30)),
923 border: Some(Rgba::rgb(150, 150, 150)),
924 primary_background: Some(Rgba::rgb(0, 120, 215)),
925 primary_foreground: Some(Rgba::rgb(255, 255, 255)),
926 min_width: Some(64.0),
927 min_height: Some(28.0),
928 padding_horizontal: Some(12.0),
929 padding_vertical: Some(6.0),
930 radius: Some(4.0),
931 icon_spacing: Some(6.0),
932 disabled_opacity: Some(0.5),
933 shadow: Some(false),
934 font: Some(FontSpec {
935 family: Some("Inter".into()),
936 size: Some(14.0),
937 weight: Some(400),
938 }),
939 };
940 assert!(!b.is_empty());
941 assert_eq!(b.min_width, Some(64.0));
942 assert_eq!(b.primary_background, Some(Rgba::rgb(0, 120, 215)));
943 }
944
945 #[test]
946 fn button_theme_default_is_empty() {
947 assert!(ButtonTheme::default().is_empty());
948 }
949
950 #[test]
951 fn button_theme_merge_font_optional_nested() {
952 let mut base = ButtonTheme {
953 font: Some(FontSpec {
954 family: Some("Noto Sans".into()),
955 size: Some(11.0),
956 weight: None,
957 }),
958 ..Default::default()
959 };
960 let overlay = ButtonTheme {
961 font: Some(FontSpec {
962 family: None,
963 weight: Some(700),
964 ..Default::default()
965 }),
966 ..Default::default()
967 };
968 base.merge(&overlay);
969 let f = base.font.as_ref().unwrap();
970 assert_eq!(f.family.as_deref(), Some("Noto Sans")); assert_eq!(f.weight, Some(700)); }
973
974 #[test]
975 fn button_theme_toml_round_trip_with_font() {
976 let b = ButtonTheme {
977 background: Some(Rgba::rgb(200, 200, 200)),
978 radius: Some(4.0),
979 font: Some(FontSpec {
980 family: Some("Inter".into()),
981 size: Some(14.0),
982 weight: Some(400),
983 }),
984 ..Default::default()
985 };
986 let toml_str = toml::to_string(&b).unwrap();
987 let b2: ButtonTheme = toml::from_str(&toml_str).unwrap();
988 assert_eq!(b, b2);
989 }
990
991 #[test]
994 fn window_theme_has_inactive_title_bar_fields() {
995 let w = WindowTheme {
996 inactive_title_bar_background: Some(Rgba::rgb(180, 180, 180)),
997 inactive_title_bar_foreground: Some(Rgba::rgb(120, 120, 120)),
998 title_bar_font: Some(FontSpec {
999 weight: Some(700),
1000 ..Default::default()
1001 }),
1002 ..Default::default()
1003 };
1004 assert!(!w.is_empty());
1005 assert!(w.inactive_title_bar_background.is_some());
1006 assert!(w.inactive_title_bar_foreground.is_some());
1007 assert!(w.title_bar_font.is_some());
1008 }
1009
1010 #[test]
1011 fn window_theme_default_is_empty() {
1012 assert!(WindowTheme::default().is_empty());
1013 }
1014
1015 #[test]
1018 fn dialog_theme_button_order_works() {
1019 let d = DialogTheme {
1020 button_order: Some(DialogButtonOrder::TrailingAffirmative),
1021 min_width: Some(300.0),
1022 ..Default::default()
1023 };
1024 assert_eq!(d.button_order, Some(DialogButtonOrder::TrailingAffirmative));
1025 assert_eq!(d.min_width, Some(300.0));
1026 assert!(!d.is_empty());
1027 }
1028
1029 #[test]
1030 fn dialog_theme_button_order_toml_round_trip() {
1031 let d = DialogTheme {
1032 button_order: Some(DialogButtonOrder::LeadingAffirmative),
1033 radius: Some(8.0),
1034 ..Default::default()
1035 };
1036 let toml_str = toml::to_string(&d).unwrap();
1037 let d2: DialogTheme = toml::from_str(&toml_str).unwrap();
1038 assert_eq!(d, d2);
1039 }
1040
1041 #[test]
1042 fn dialog_theme_default_is_empty() {
1043 assert!(DialogTheme::default().is_empty());
1044 }
1045
1046 #[test]
1049 fn splitter_theme_single_field_merge() {
1050 let mut base = SplitterTheme { width: Some(4.0) };
1051 let overlay = SplitterTheme { width: Some(6.0) };
1052 base.merge(&overlay);
1053 assert_eq!(base.width, Some(6.0));
1054 }
1055
1056 #[test]
1057 fn splitter_theme_merge_none_preserves_base() {
1058 let mut base = SplitterTheme { width: Some(4.0) };
1059 let overlay = SplitterTheme::default();
1060 base.merge(&overlay);
1061 assert_eq!(base.width, Some(4.0));
1062 }
1063
1064 #[test]
1065 fn splitter_theme_default_is_empty() {
1066 assert!(SplitterTheme::default().is_empty());
1067 }
1068
1069 #[test]
1070 fn splitter_theme_not_empty_when_set() {
1071 assert!(!SplitterTheme { width: Some(4.0) }.is_empty());
1072 }
1073
1074 #[test]
1077 fn separator_theme_single_field() {
1078 let s = SeparatorTheme {
1079 color: Some(Rgba::rgb(200, 200, 200)),
1080 };
1081 assert!(!s.is_empty());
1082 }
1083
1084 #[test]
1087 fn all_widget_theme_defaults_are_empty() {
1088 assert!(WindowTheme::default().is_empty());
1089 assert!(ButtonTheme::default().is_empty());
1090 assert!(InputTheme::default().is_empty());
1091 assert!(CheckboxTheme::default().is_empty());
1092 assert!(MenuTheme::default().is_empty());
1093 assert!(TooltipTheme::default().is_empty());
1094 assert!(ScrollbarTheme::default().is_empty());
1095 assert!(SliderTheme::default().is_empty());
1096 assert!(ProgressBarTheme::default().is_empty());
1097 assert!(TabTheme::default().is_empty());
1098 assert!(SidebarTheme::default().is_empty());
1099 assert!(ToolbarTheme::default().is_empty());
1100 assert!(StatusBarTheme::default().is_empty());
1101 assert!(ListTheme::default().is_empty());
1102 assert!(PopoverTheme::default().is_empty());
1103 assert!(SplitterTheme::default().is_empty());
1104 assert!(SeparatorTheme::default().is_empty());
1105 assert!(SwitchTheme::default().is_empty());
1106 assert!(DialogTheme::default().is_empty());
1107 assert!(SpinnerTheme::default().is_empty());
1108 assert!(ComboBoxTheme::default().is_empty());
1109 assert!(SegmentedControlTheme::default().is_empty());
1110 assert!(CardTheme::default().is_empty());
1111 assert!(ExpanderTheme::default().is_empty());
1112 assert!(LinkTheme::default().is_empty());
1113 }
1114
1115 #[test]
1118 fn input_theme_toml_round_trip() {
1119 let t = InputTheme {
1120 background: Some(Rgba::rgb(255, 255, 255)),
1121 border: Some(Rgba::rgb(180, 180, 180)),
1122 radius: Some(4.0),
1123 font: Some(FontSpec {
1124 family: Some("Noto Sans".into()),
1125 ..Default::default()
1126 }),
1127 ..Default::default()
1128 };
1129 let toml_str = toml::to_string(&t).unwrap();
1130 let t2: InputTheme = toml::from_str(&toml_str).unwrap();
1131 assert_eq!(t, t2);
1132 }
1133
1134 #[test]
1135 fn switch_theme_toml_round_trip() {
1136 let s = SwitchTheme {
1137 checked_background: Some(Rgba::rgb(0, 120, 215)),
1138 track_width: Some(40.0),
1139 track_height: Some(20.0),
1140 thumb_size: Some(14.0),
1141 track_radius: Some(10.0),
1142 ..Default::default()
1143 };
1144 let toml_str = toml::to_string(&s).unwrap();
1145 let s2: SwitchTheme = toml::from_str(&toml_str).unwrap();
1146 assert_eq!(s, s2);
1147 }
1148
1149 #[test]
1150 fn card_theme_has_shadow_bool_field() {
1151 let c = CardTheme {
1152 shadow: Some(true),
1153 radius: Some(8.0),
1154 ..Default::default()
1155 };
1156 assert!(!c.is_empty());
1157 assert_eq!(c.shadow, Some(true));
1158 }
1159
1160 #[test]
1161 fn link_theme_has_underline_bool_field() {
1162 let l = LinkTheme {
1163 color: Some(Rgba::rgb(0, 100, 200)),
1164 underline: Some(true),
1165 ..Default::default()
1166 };
1167 assert!(!l.is_empty());
1168 assert_eq!(l.underline, Some(true));
1169 }
1170
1171 #[test]
1172 fn status_bar_theme_has_only_font_field() {
1173 let s = StatusBarTheme {
1175 font: Some(FontSpec {
1176 size: Some(11.0),
1177 ..Default::default()
1178 }),
1179 };
1180 assert!(!s.is_empty());
1181 }
1182}