1use crate::Rgba;
4use crate::model::{DialogButtonOrder, FontSpec};
5
6#[derive(Clone, Debug, Default, PartialEq)]
11pub struct ResolvedFontSpec {
12 pub family: String,
14 pub size: f32,
16 pub weight: u16,
18}
19
20macro_rules! define_widget_pair {
47 (
48 $(#[$attr:meta])*
49 $opt_name:ident / $resolved_name:ident {
50 $(option {
51 $($opt_field:ident : $opt_type:ty),* $(,)?
52 })?
53 $(optional_nested {
54 $($on_field:ident : [$on_opt_type:ty, $on_res_type:ty]),* $(,)?
55 })?
56 }
57 ) => {
58 $(#[$attr])*
59 #[serde_with::skip_serializing_none]
60 #[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
61 #[serde(default)]
62 #[allow(missing_docs)]
63 pub struct $opt_name {
64 $($(pub $opt_field: Option<$opt_type>,)*)?
65 $($(pub $on_field: Option<$on_opt_type>,)*)?
66 }
67
68 $(#[$attr])*
69 #[derive(Clone, Debug, PartialEq)]
70 #[allow(missing_docs)]
71 pub struct $resolved_name {
72 $($(pub $opt_field: $opt_type,)*)?
73 $($(pub $on_field: $on_res_type,)*)?
74 }
75
76 $crate::impl_merge!($opt_name {
77 $(option { $($opt_field),* })?
78 $(optional_nested { $($on_field),* })?
79 });
80 };
81}
82
83define_widget_pair! {
86 WindowTheme / ResolvedWindow {
88 option {
89 background: Rgba,
90 foreground: Rgba,
91 border: Rgba,
92 title_bar_background: Rgba,
93 title_bar_foreground: Rgba,
94 inactive_title_bar_background: Rgba,
95 inactive_title_bar_foreground: Rgba,
96 radius: f32,
97 shadow: bool,
98 }
99 optional_nested {
100 title_bar_font: [FontSpec, ResolvedFontSpec],
101 }
102 }
103}
104
105define_widget_pair! {
108 ButtonTheme / ResolvedButton {
110 option {
111 background: Rgba,
112 foreground: Rgba,
113 border: Rgba,
114 primary_bg: Rgba,
115 primary_fg: Rgba,
116 min_width: f32,
117 min_height: f32,
118 padding_horizontal: f32,
119 padding_vertical: f32,
120 radius: f32,
121 icon_spacing: f32,
122 disabled_opacity: f32,
123 shadow: bool,
124 }
125 optional_nested {
126 font: [FontSpec, ResolvedFontSpec],
127 }
128 }
129}
130
131define_widget_pair! {
134 InputTheme / ResolvedInput {
136 option {
137 background: Rgba,
138 foreground: Rgba,
139 border: Rgba,
140 placeholder: Rgba,
141 caret: Rgba,
142 selection: Rgba,
143 selection_foreground: Rgba,
144 min_height: f32,
145 padding_horizontal: f32,
146 padding_vertical: f32,
147 radius: f32,
148 border_width: f32,
149 }
150 optional_nested {
151 font: [FontSpec, ResolvedFontSpec],
152 }
153 }
154}
155
156define_widget_pair! {
159 CheckboxTheme / ResolvedCheckbox {
161 option {
162 checked_bg: Rgba,
163 indicator_size: f32,
164 spacing: f32,
165 radius: f32,
166 border_width: f32,
167 }
168 }
169}
170
171define_widget_pair! {
174 MenuTheme / ResolvedMenu {
176 option {
177 background: Rgba,
178 foreground: Rgba,
179 separator: Rgba,
180 item_height: f32,
181 padding_horizontal: f32,
182 padding_vertical: f32,
183 icon_spacing: f32,
184 }
185 optional_nested {
186 font: [FontSpec, ResolvedFontSpec],
187 }
188 }
189}
190
191define_widget_pair! {
194 TooltipTheme / ResolvedTooltip {
196 option {
197 background: Rgba,
198 foreground: Rgba,
199 padding_horizontal: f32,
200 padding_vertical: f32,
201 max_width: f32,
202 radius: f32,
203 }
204 optional_nested {
205 font: [FontSpec, ResolvedFontSpec],
206 }
207 }
208}
209
210define_widget_pair! {
213 ScrollbarTheme / ResolvedScrollbar {
215 option {
216 track: Rgba,
217 thumb: Rgba,
218 thumb_hover: Rgba,
219 width: f32,
220 min_thumb_height: f32,
221 slider_width: f32,
222 overlay_mode: bool,
223 }
224 }
225}
226
227define_widget_pair! {
230 SliderTheme / ResolvedSlider {
232 option {
233 fill: Rgba,
234 track: Rgba,
235 thumb: Rgba,
236 track_height: f32,
237 thumb_size: f32,
238 tick_length: f32,
239 }
240 }
241}
242
243define_widget_pair! {
246 ProgressBarTheme / ResolvedProgressBar {
248 option {
249 fill: Rgba,
250 track: Rgba,
251 height: f32,
252 min_width: f32,
253 radius: f32,
254 }
255 }
256}
257
258define_widget_pair! {
261 TabTheme / ResolvedTab {
263 option {
264 background: Rgba,
265 foreground: Rgba,
266 active_background: Rgba,
267 active_foreground: Rgba,
268 bar_background: Rgba,
269 min_width: f32,
270 min_height: f32,
271 padding_horizontal: f32,
272 padding_vertical: f32,
273 }
274 }
275}
276
277define_widget_pair! {
280 SidebarTheme / ResolvedSidebar {
282 option {
283 background: Rgba,
284 foreground: Rgba,
285 }
286 }
287}
288
289define_widget_pair! {
292 ToolbarTheme / ResolvedToolbar {
294 option {
295 height: f32,
296 item_spacing: f32,
297 padding: f32,
298 }
299 optional_nested {
300 font: [FontSpec, ResolvedFontSpec],
301 }
302 }
303}
304
305define_widget_pair! {
308 StatusBarTheme / ResolvedStatusBar {
310 optional_nested {
311 font: [FontSpec, ResolvedFontSpec],
312 }
313 }
314}
315
316define_widget_pair! {
319 ListTheme / ResolvedList {
321 option {
322 background: Rgba,
323 foreground: Rgba,
324 alternate_row: Rgba,
325 selection: Rgba,
326 selection_foreground: Rgba,
327 header_background: Rgba,
328 header_foreground: Rgba,
329 grid_color: Rgba,
330 item_height: f32,
331 padding_horizontal: f32,
332 padding_vertical: f32,
333 }
334 }
335}
336
337define_widget_pair! {
340 PopoverTheme / ResolvedPopover {
342 option {
343 background: Rgba,
344 foreground: Rgba,
345 border: Rgba,
346 radius: f32,
347 }
348 }
349}
350
351define_widget_pair! {
354 SplitterTheme / ResolvedSplitter {
356 option {
357 width: f32,
358 }
359 }
360}
361
362define_widget_pair! {
365 SeparatorTheme / ResolvedSeparator {
367 option {
368 color: Rgba,
369 }
370 }
371}
372
373define_widget_pair! {
376 SwitchTheme / ResolvedSwitch {
378 option {
379 checked_bg: Rgba,
380 unchecked_bg: Rgba,
381 thumb_bg: Rgba,
382 track_width: f32,
383 track_height: f32,
384 thumb_size: f32,
385 track_radius: f32,
386 }
387 }
388}
389
390define_widget_pair! {
393 DialogTheme / ResolvedDialog {
395 option {
396 min_width: f32,
397 max_width: f32,
398 min_height: f32,
399 max_height: f32,
400 content_padding: f32,
401 button_spacing: f32,
402 radius: f32,
403 icon_size: f32,
404 button_order: DialogButtonOrder,
405 }
406 optional_nested {
407 title_font: [FontSpec, ResolvedFontSpec],
408 }
409 }
410}
411
412define_widget_pair! {
415 SpinnerTheme / ResolvedSpinner {
417 option {
418 fill: Rgba,
419 diameter: f32,
420 min_size: f32,
421 stroke_width: f32,
422 }
423 }
424}
425
426define_widget_pair! {
429 ComboBoxTheme / ResolvedComboBox {
431 option {
432 min_height: f32,
433 min_width: f32,
434 padding_horizontal: f32,
435 arrow_size: f32,
436 arrow_area_width: f32,
437 radius: f32,
438 }
439 }
440}
441
442define_widget_pair! {
445 SegmentedControlTheme / ResolvedSegmentedControl {
447 option {
448 segment_height: f32,
449 separator_width: f32,
450 padding_horizontal: f32,
451 radius: f32,
452 }
453 }
454}
455
456define_widget_pair! {
459 CardTheme / ResolvedCard {
461 option {
462 background: Rgba,
463 border: Rgba,
464 radius: f32,
465 padding: f32,
466 shadow: bool,
467 }
468 }
469}
470
471define_widget_pair! {
474 ExpanderTheme / ResolvedExpander {
476 option {
477 header_height: f32,
478 arrow_size: f32,
479 content_padding: f32,
480 radius: f32,
481 }
482 }
483}
484
485define_widget_pair! {
488 LinkTheme / ResolvedLink {
490 option {
491 color: Rgba,
492 visited: Rgba,
493 background: Rgba,
494 hover_bg: Rgba,
495 underline: bool,
496 }
497 }
498}
499
500#[cfg(test)]
501#[allow(clippy::unwrap_used, clippy::expect_used)]
502mod tests {
503 use super::*;
504 use crate::Rgba;
505 use crate::model::{DialogButtonOrder, FontSpec};
506
507 define_widget_pair! {
509 TestWidget / ResolvedTestWidget {
511 option {
512 size: f32,
513 label: String,
514 }
515 optional_nested {
516 font: [FontSpec, ResolvedFontSpec],
517 }
518 }
519 }
520
521 #[test]
524 fn resolved_font_spec_fields_are_concrete() {
525 let rfs = ResolvedFontSpec {
526 family: "Inter".into(),
527 size: 14.0,
528 weight: 400,
529 };
530 assert_eq!(rfs.family, "Inter");
531 assert_eq!(rfs.size, 14.0);
532 assert_eq!(rfs.weight, 400);
533 }
534
535 #[test]
538 fn generated_option_struct_has_option_fields() {
539 let w = TestWidget::default();
540 assert!(w.size.is_none());
541 assert!(w.label.is_none());
542 assert!(w.font.is_none());
543 }
544
545 #[test]
546 fn generated_option_struct_is_empty_by_default() {
547 assert!(TestWidget::default().is_empty());
548 }
549
550 #[test]
551 fn generated_option_struct_not_empty_when_size_set() {
552 let w = TestWidget {
553 size: Some(24.0),
554 ..Default::default()
555 };
556 assert!(!w.is_empty());
557 }
558
559 #[test]
560 fn generated_option_struct_not_empty_when_font_set() {
561 let w = TestWidget {
562 font: Some(FontSpec {
563 size: Some(14.0),
564 ..Default::default()
565 }),
566 ..Default::default()
567 };
568 assert!(!w.is_empty());
569 }
570
571 #[test]
572 fn generated_resolved_struct_has_concrete_fields() {
573 let resolved = ResolvedTestWidget {
574 size: 24.0,
575 label: "Click me".into(),
576 font: ResolvedFontSpec {
577 family: "Inter".into(),
578 size: 14.0,
579 weight: 400,
580 },
581 };
582 assert_eq!(resolved.size, 24.0);
583 assert_eq!(resolved.label, "Click me");
584 assert_eq!(resolved.font.family, "Inter");
585 }
586
587 #[test]
590 fn generated_merge_option_field_overlay_wins() {
591 let mut base = TestWidget {
592 size: Some(20.0),
593 ..Default::default()
594 };
595 let overlay = TestWidget {
596 size: Some(24.0),
597 ..Default::default()
598 };
599 base.merge(&overlay);
600 assert_eq!(base.size, Some(24.0));
601 }
602
603 #[test]
604 fn generated_merge_option_field_none_preserves_base() {
605 let mut base = TestWidget {
606 size: Some(20.0),
607 ..Default::default()
608 };
609 let overlay = TestWidget::default();
610 base.merge(&overlay);
611 assert_eq!(base.size, Some(20.0));
612 }
613
614 #[test]
615 fn generated_merge_optional_nested_both_some_merges_inner() {
616 let mut base = TestWidget {
617 font: Some(FontSpec {
618 family: Some("Noto Sans".into()),
619 size: Some(12.0),
620 weight: None,
621 }),
622 ..Default::default()
623 };
624 let overlay = TestWidget {
625 font: Some(FontSpec {
626 family: None,
627 size: None,
628 weight: Some(700),
629 }),
630 ..Default::default()
631 };
632 base.merge(&overlay);
633 let font = base.font.as_ref().unwrap();
634 assert_eq!(font.family.as_deref(), Some("Noto Sans")); assert_eq!(font.size, Some(12.0)); assert_eq!(font.weight, Some(700)); }
638
639 #[test]
640 fn generated_merge_optional_nested_none_plus_some_clones() {
641 let mut base = TestWidget::default();
642 let overlay = TestWidget {
643 font: Some(FontSpec {
644 family: Some("Inter".into()),
645 size: Some(14.0),
646 weight: Some(400),
647 }),
648 ..Default::default()
649 };
650 base.merge(&overlay);
651 let font = base.font.as_ref().unwrap();
652 assert_eq!(font.family.as_deref(), Some("Inter"));
653 assert_eq!(font.size, Some(14.0));
654 assert_eq!(font.weight, Some(400));
655 }
656
657 #[test]
658 fn generated_merge_optional_nested_some_plus_none_preserves_base() {
659 let mut base = TestWidget {
660 font: Some(FontSpec {
661 family: Some("Inter".into()),
662 size: Some(14.0),
663 weight: Some(400),
664 }),
665 ..Default::default()
666 };
667 let overlay = TestWidget::default();
668 base.merge(&overlay);
669 let font = base.font.as_ref().unwrap();
670 assert_eq!(font.family.as_deref(), Some("Inter"));
671 }
672
673 #[test]
674 fn generated_merge_optional_nested_none_plus_none_stays_none() {
675 let mut base = TestWidget::default();
676 let overlay = TestWidget::default();
677 base.merge(&overlay);
678 assert!(base.font.is_none());
679 }
680
681 #[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
685 struct WithFont {
686 name: Option<String>,
687 font: Option<FontSpec>,
688 }
689
690 impl_merge!(WithFont {
691 option { name }
692 optional_nested { font }
693 });
694
695 #[test]
696 fn impl_merge_optional_nested_none_none_stays_none() {
697 let mut base = WithFont::default();
698 let overlay = WithFont::default();
699 base.merge(&overlay);
700 assert!(base.font.is_none());
701 }
702
703 #[test]
704 fn impl_merge_optional_nested_some_none_preserves_base() {
705 let mut base = WithFont {
706 font: Some(FontSpec {
707 size: Some(12.0),
708 ..Default::default()
709 }),
710 ..Default::default()
711 };
712 let overlay = WithFont::default();
713 base.merge(&overlay);
714 assert_eq!(base.font.as_ref().unwrap().size, Some(12.0));
715 }
716
717 #[test]
718 fn impl_merge_optional_nested_none_some_clones_overlay() {
719 let mut base = WithFont::default();
720 let overlay = WithFont {
721 font: Some(FontSpec {
722 family: Some("Inter".into()),
723 ..Default::default()
724 }),
725 ..Default::default()
726 };
727 base.merge(&overlay);
728 assert_eq!(base.font.as_ref().unwrap().family.as_deref(), Some("Inter"));
729 }
730
731 #[test]
732 fn impl_merge_optional_nested_some_some_merges_inner() {
733 let mut base = WithFont {
734 font: Some(FontSpec {
735 family: Some("Noto".into()),
736 size: Some(11.0),
737 weight: None,
738 }),
739 ..Default::default()
740 };
741 let overlay = WithFont {
742 font: Some(FontSpec {
743 family: None,
744 size: Some(14.0),
745 weight: Some(400),
746 }),
747 ..Default::default()
748 };
749 base.merge(&overlay);
750 let f = base.font.as_ref().unwrap();
751 assert_eq!(f.family.as_deref(), Some("Noto")); assert_eq!(f.size, Some(14.0)); assert_eq!(f.weight, Some(400)); }
755
756 #[test]
757 fn impl_merge_optional_nested_is_empty_none() {
758 let w = WithFont::default();
759 assert!(w.is_empty());
760 }
761
762 #[test]
763 fn impl_merge_optional_nested_is_empty_some() {
764 let w = WithFont {
765 font: Some(FontSpec::default()),
766 ..Default::default()
767 };
768 assert!(!w.is_empty());
769 }
770
771 #[test]
774 fn button_theme_has_all_fields_and_not_empty_when_set() {
775 let b = ButtonTheme {
776 background: Some(Rgba::rgb(200, 200, 200)),
777 foreground: Some(Rgba::rgb(30, 30, 30)),
778 border: Some(Rgba::rgb(150, 150, 150)),
779 primary_bg: Some(Rgba::rgb(0, 120, 215)),
780 primary_fg: Some(Rgba::rgb(255, 255, 255)),
781 min_width: Some(64.0),
782 min_height: Some(28.0),
783 padding_horizontal: Some(12.0),
784 padding_vertical: Some(6.0),
785 radius: Some(4.0),
786 icon_spacing: Some(6.0),
787 disabled_opacity: Some(0.5),
788 shadow: Some(false),
789 font: Some(FontSpec {
790 family: Some("Inter".into()),
791 size: Some(14.0),
792 weight: Some(400),
793 }),
794 };
795 assert!(!b.is_empty());
796 assert_eq!(b.min_width, Some(64.0));
797 assert_eq!(b.primary_bg, Some(Rgba::rgb(0, 120, 215)));
798 }
799
800 #[test]
801 fn button_theme_default_is_empty() {
802 assert!(ButtonTheme::default().is_empty());
803 }
804
805 #[test]
806 fn button_theme_merge_font_optional_nested() {
807 let mut base = ButtonTheme {
808 font: Some(FontSpec {
809 family: Some("Noto Sans".into()),
810 size: Some(11.0),
811 weight: None,
812 }),
813 ..Default::default()
814 };
815 let overlay = ButtonTheme {
816 font: Some(FontSpec {
817 family: None,
818 weight: Some(700),
819 ..Default::default()
820 }),
821 ..Default::default()
822 };
823 base.merge(&overlay);
824 let f = base.font.as_ref().unwrap();
825 assert_eq!(f.family.as_deref(), Some("Noto Sans")); assert_eq!(f.weight, Some(700)); }
828
829 #[test]
830 fn button_theme_toml_round_trip_with_font() {
831 let b = ButtonTheme {
832 background: Some(Rgba::rgb(200, 200, 200)),
833 radius: Some(4.0),
834 font: Some(FontSpec {
835 family: Some("Inter".into()),
836 size: Some(14.0),
837 weight: Some(400),
838 }),
839 ..Default::default()
840 };
841 let toml_str = toml::to_string(&b).unwrap();
842 let b2: ButtonTheme = toml::from_str(&toml_str).unwrap();
843 assert_eq!(b, b2);
844 }
845
846 #[test]
849 fn window_theme_has_inactive_title_bar_fields() {
850 let w = WindowTheme {
851 inactive_title_bar_background: Some(Rgba::rgb(180, 180, 180)),
852 inactive_title_bar_foreground: Some(Rgba::rgb(120, 120, 120)),
853 title_bar_font: Some(FontSpec {
854 weight: Some(700),
855 ..Default::default()
856 }),
857 ..Default::default()
858 };
859 assert!(!w.is_empty());
860 assert!(w.inactive_title_bar_background.is_some());
861 assert!(w.inactive_title_bar_foreground.is_some());
862 assert!(w.title_bar_font.is_some());
863 }
864
865 #[test]
866 fn window_theme_default_is_empty() {
867 assert!(WindowTheme::default().is_empty());
868 }
869
870 #[test]
873 fn dialog_theme_button_order_works() {
874 let d = DialogTheme {
875 button_order: Some(DialogButtonOrder::TrailingAffirmative),
876 min_width: Some(300.0),
877 ..Default::default()
878 };
879 assert_eq!(d.button_order, Some(DialogButtonOrder::TrailingAffirmative));
880 assert_eq!(d.min_width, Some(300.0));
881 assert!(!d.is_empty());
882 }
883
884 #[test]
885 fn dialog_theme_button_order_toml_round_trip() {
886 let d = DialogTheme {
887 button_order: Some(DialogButtonOrder::LeadingAffirmative),
888 radius: Some(8.0),
889 ..Default::default()
890 };
891 let toml_str = toml::to_string(&d).unwrap();
892 let d2: DialogTheme = toml::from_str(&toml_str).unwrap();
893 assert_eq!(d, d2);
894 }
895
896 #[test]
897 fn dialog_theme_default_is_empty() {
898 assert!(DialogTheme::default().is_empty());
899 }
900
901 #[test]
904 fn splitter_theme_single_field_merge() {
905 let mut base = SplitterTheme { width: Some(4.0) };
906 let overlay = SplitterTheme { width: Some(6.0) };
907 base.merge(&overlay);
908 assert_eq!(base.width, Some(6.0));
909 }
910
911 #[test]
912 fn splitter_theme_merge_none_preserves_base() {
913 let mut base = SplitterTheme { width: Some(4.0) };
914 let overlay = SplitterTheme::default();
915 base.merge(&overlay);
916 assert_eq!(base.width, Some(4.0));
917 }
918
919 #[test]
920 fn splitter_theme_default_is_empty() {
921 assert!(SplitterTheme::default().is_empty());
922 }
923
924 #[test]
925 fn splitter_theme_not_empty_when_set() {
926 assert!(!SplitterTheme { width: Some(4.0) }.is_empty());
927 }
928
929 #[test]
932 fn separator_theme_single_field() {
933 let s = SeparatorTheme {
934 color: Some(Rgba::rgb(200, 200, 200)),
935 };
936 assert!(!s.is_empty());
937 }
938
939 #[test]
942 fn all_widget_theme_defaults_are_empty() {
943 assert!(WindowTheme::default().is_empty());
944 assert!(ButtonTheme::default().is_empty());
945 assert!(InputTheme::default().is_empty());
946 assert!(CheckboxTheme::default().is_empty());
947 assert!(MenuTheme::default().is_empty());
948 assert!(TooltipTheme::default().is_empty());
949 assert!(ScrollbarTheme::default().is_empty());
950 assert!(SliderTheme::default().is_empty());
951 assert!(ProgressBarTheme::default().is_empty());
952 assert!(TabTheme::default().is_empty());
953 assert!(SidebarTheme::default().is_empty());
954 assert!(ToolbarTheme::default().is_empty());
955 assert!(StatusBarTheme::default().is_empty());
956 assert!(ListTheme::default().is_empty());
957 assert!(PopoverTheme::default().is_empty());
958 assert!(SplitterTheme::default().is_empty());
959 assert!(SeparatorTheme::default().is_empty());
960 assert!(SwitchTheme::default().is_empty());
961 assert!(DialogTheme::default().is_empty());
962 assert!(SpinnerTheme::default().is_empty());
963 assert!(ComboBoxTheme::default().is_empty());
964 assert!(SegmentedControlTheme::default().is_empty());
965 assert!(CardTheme::default().is_empty());
966 assert!(ExpanderTheme::default().is_empty());
967 assert!(LinkTheme::default().is_empty());
968 }
969
970 #[test]
973 fn input_theme_toml_round_trip() {
974 let t = InputTheme {
975 background: Some(Rgba::rgb(255, 255, 255)),
976 border: Some(Rgba::rgb(180, 180, 180)),
977 radius: Some(4.0),
978 font: Some(FontSpec {
979 family: Some("Noto Sans".into()),
980 ..Default::default()
981 }),
982 ..Default::default()
983 };
984 let toml_str = toml::to_string(&t).unwrap();
985 let t2: InputTheme = toml::from_str(&toml_str).unwrap();
986 assert_eq!(t, t2);
987 }
988
989 #[test]
990 fn switch_theme_toml_round_trip() {
991 let s = SwitchTheme {
992 checked_bg: Some(Rgba::rgb(0, 120, 215)),
993 track_width: Some(40.0),
994 track_height: Some(20.0),
995 thumb_size: Some(14.0),
996 track_radius: Some(10.0),
997 ..Default::default()
998 };
999 let toml_str = toml::to_string(&s).unwrap();
1000 let s2: SwitchTheme = toml::from_str(&toml_str).unwrap();
1001 assert_eq!(s, s2);
1002 }
1003
1004 #[test]
1005 fn card_theme_has_shadow_bool_field() {
1006 let c = CardTheme {
1007 shadow: Some(true),
1008 radius: Some(8.0),
1009 ..Default::default()
1010 };
1011 assert!(!c.is_empty());
1012 assert_eq!(c.shadow, Some(true));
1013 }
1014
1015 #[test]
1016 fn link_theme_has_underline_bool_field() {
1017 let l = LinkTheme {
1018 color: Some(Rgba::rgb(0, 100, 200)),
1019 underline: Some(true),
1020 ..Default::default()
1021 };
1022 assert!(!l.is_empty());
1023 assert_eq!(l.underline, Some(true));
1024 }
1025
1026 #[test]
1027 fn status_bar_theme_has_only_font_field() {
1028 let s = StatusBarTheme {
1030 font: Some(FontSpec {
1031 size: Some(11.0),
1032 ..Default::default()
1033 }),
1034 };
1035 assert!(!s.is_empty());
1036 }
1037}