1use crate::Rgba;
4use crate::model::border::{BorderSpec, ResolvedBorderSpec};
5use crate::model::{DialogButtonOrder, FontSpec, ResolvedFontSpec};
6
7#[doc(hidden)]
10macro_rules! __field_name {
11 ($field:ident) => {
12 stringify!($field)
13 };
14 ($field:ident, $rename:literal) => {
15 $rename
16 };
17}
18pub(crate) use __field_name;
19
20macro_rules! define_widget_pair {
49 (
50 $(#[$attr:meta])*
51 $opt_name:ident / $resolved_name:ident {
52 $(option {
53 $($(#[doc = $opt_doc:expr])* $opt_field:ident $(as $opt_rename:literal)? : $opt_type:ty),* $(,)?
54 })?
55 $(soft_option {
56 $($(#[doc = $so_doc:expr])* $so_field:ident : $so_type:ty),* $(,)?
57 })?
58 $(optional_nested {
59 $($(#[doc = $on_doc:expr])* $on_field:ident : [$on_opt_type:ty, $on_res_type:ty]),* $(,)?
60 })?
61 $(border_partial {
62 $($(#[doc = $bp_doc:expr])* $bp_field:ident : [$bp_opt_type:ty, $bp_res_type:ty]),* $(,)?
63 })?
64 $(border_optional {
65 $($(#[doc = $bo_doc:expr])* $bo_field:ident : [$bo_opt_type:ty, $bo_res_type:ty]),* $(,)?
66 })?
67 }
68 ) => {
69 $(#[$attr])*
70 #[serde_with::skip_serializing_none]
71 #[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
72 #[serde(default)]
73 pub struct $opt_name {
74 $($($(#[doc = $opt_doc])* $(#[serde(rename = $opt_rename)])? pub $opt_field: Option<$opt_type>,)*)?
75 $($($(#[doc = $so_doc])* pub $so_field: Option<$so_type>,)*)?
76 $($($(#[doc = $on_doc])* pub $on_field: Option<$on_opt_type>,)*)?
77 $($($(#[doc = $bp_doc])* pub $bp_field: Option<$bp_opt_type>,)*)?
78 $($($(#[doc = $bo_doc])* pub $bo_field: Option<$bo_opt_type>,)*)?
79 }
80
81 $(#[$attr])*
82 #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
83 #[non_exhaustive]
84 pub struct $resolved_name {
85 $($($(#[doc = $opt_doc])* pub $opt_field: $opt_type,)*)?
86 $($($(#[doc = $so_doc])* pub $so_field: Option<$so_type>,)*)?
87 $($($(#[doc = $on_doc])* pub $on_field: $on_res_type,)*)?
88 $($($(#[doc = $bp_doc])* pub $bp_field: $bp_res_type,)*)?
89 $($($(#[doc = $bo_doc])* pub $bo_field: $bo_res_type,)*)?
90 }
91
92 impl $opt_name {
93 pub const FIELD_NAMES: &[&str] = &[
95 $($($crate::model::widgets::__field_name!($opt_field $(, $opt_rename)?),)*)?
96 $($(stringify!($so_field),)*)?
97 $($(stringify!($on_field),)*)?
98 $($(stringify!($bp_field),)*)?
99 $($(stringify!($bo_field),)*)?
100 ];
101 }
102
103 impl_merge!($opt_name {
104 $(option { $($opt_field),* })?
105 $(soft_option { $($so_field),* })?
106 $(optional_nested { $($on_field),* })?
107 $(optional_nested { $($bp_field),* })?
108 $(optional_nested { $($bo_field),* })?
109 });
110
111 #[allow(dead_code)] impl $resolved_name {
113 pub(crate) fn validate_widget(
116 source: &$opt_name,
117 prefix: &str,
118 _dpi: f32,
119 missing: &mut Vec<String>,
120 ) -> Self {
121 Self {
122 $($(
123 $opt_field: crate::resolve::validate_helpers::require(
124 &source.$opt_field,
125 &format!("{}.{}", prefix, stringify!($opt_field)),
126 missing,
127 ),
128 )*)?
129 $($(
130 $so_field: source.$so_field,
131 )*)?
132 $($(
133 $on_field: <$on_opt_type as crate::resolve::validate_helpers::ValidateNested>::validate_nested(
134 &source.$on_field,
135 &format!("{}.{}", prefix, stringify!($on_field)),
136 _dpi,
137 missing,
138 ),
139 )*)?
140 $($(
141 $bp_field: crate::resolve::validate_helpers::require_border_partial(
142 &source.$bp_field,
143 &format!("{}.{}", prefix, stringify!($bp_field)),
144 missing,
145 ),
146 )*)?
147 $($(
148 $bo_field: crate::resolve::validate_helpers::border_all_optional(
149 &source.$bo_field,
150 ),
151 )*)?
152 }
153 }
154 }
155 };
156}
157
158define_widget_pair! {
161 WindowTheme / ResolvedWindowTheme {
163 option {
164 background_color: Rgba,
166 title_bar_background: Rgba,
168 inactive_title_bar_background: Rgba,
170 inactive_title_bar_text_color: Rgba,
172 }
173 optional_nested {
174 title_bar_font: [FontSpec, ResolvedFontSpec],
176 border: [BorderSpec, ResolvedBorderSpec],
178 }
179 }
180}
181
182define_widget_pair! {
185 ButtonTheme / ResolvedButtonTheme {
187 option {
188 background_color: Rgba,
190 primary_background: Rgba,
192 primary_text_color: Rgba,
194 min_width as "min_width_px": f32,
196 min_height as "min_height_px": f32,
198 icon_text_gap as "icon_text_gap_px": f32,
200 disabled_opacity: f32,
202 hover_background: Rgba,
204 hover_text_color: Rgba,
206 active_text_color: Rgba,
208 disabled_text_color: Rgba,
210 }
211 soft_option {
212 active_background: Rgba,
214 disabled_background: Rgba,
216 }
217 optional_nested {
218 font: [FontSpec, ResolvedFontSpec],
220 border: [BorderSpec, ResolvedBorderSpec],
222 }
223 }
224}
225
226define_widget_pair! {
229 InputTheme / ResolvedInputTheme {
231 option {
232 background_color: Rgba,
234 placeholder_color: Rgba,
236 caret_color: Rgba,
238 selection_background: Rgba,
240 selection_text_color: Rgba,
242 min_height as "min_height_px": f32,
244 disabled_opacity: f32,
246 disabled_text_color: Rgba,
248 }
249 soft_option {
250 hover_border_color: Rgba,
252 focus_border_color: Rgba,
254 disabled_background: Rgba,
256 }
257 optional_nested {
258 font: [FontSpec, ResolvedFontSpec],
260 border: [BorderSpec, ResolvedBorderSpec],
262 }
263 }
264}
265
266define_widget_pair! {
269 CheckboxTheme / ResolvedCheckboxTheme {
271 option {
272 background_color: Rgba,
274 checked_background: Rgba,
276 indicator_color: Rgba,
278 indicator_width as "indicator_width_px": f32,
280 label_gap as "label_gap_px": f32,
282 disabled_opacity: f32,
284 disabled_text_color: Rgba,
286 }
287 soft_option {
288 hover_background: Rgba,
290 disabled_background: Rgba,
292 unchecked_background: Rgba,
294 unchecked_border_color: Rgba,
296 }
297 optional_nested {
298 font: [FontSpec, ResolvedFontSpec],
300 border: [BorderSpec, ResolvedBorderSpec],
302 }
303 }
304}
305
306define_widget_pair! {
309 MenuTheme / ResolvedMenuTheme {
311 option {
312 background_color: Rgba,
314 separator_color: Rgba,
316 row_height as "row_height_px": f32,
318 icon_text_gap as "icon_text_gap_px": f32,
320 icon_size as "icon_size_px": f32,
322 hover_background: Rgba,
324 hover_text_color: Rgba,
326 disabled_text_color: Rgba,
328 }
329 optional_nested {
330 font: [FontSpec, ResolvedFontSpec],
332 }
333 border_optional {
334 border: [BorderSpec, ResolvedBorderSpec],
336 }
337 }
338}
339
340define_widget_pair! {
343 TooltipTheme / ResolvedTooltipTheme {
345 option {
346 background_color: Rgba,
348 max_width as "max_width_px": f32,
350 }
351 optional_nested {
352 font: [FontSpec, ResolvedFontSpec],
354 border: [BorderSpec, ResolvedBorderSpec],
356 }
357 }
358}
359
360define_widget_pair! {
363 ScrollbarTheme / ResolvedScrollbarTheme {
365 option {
366 track_color: Rgba,
368 thumb_color: Rgba,
370 thumb_hover_color: Rgba,
372 groove_width as "groove_width_px": f32,
374 min_thumb_length as "min_thumb_length_px": f32,
376 thumb_width as "thumb_width_px": f32,
378 overlay_mode: bool,
380 }
381 soft_option {
382 thumb_active_color: Rgba,
384 }
385 }
386}
387
388define_widget_pair! {
391 SliderTheme / ResolvedSliderTheme {
393 option {
394 fill_color: Rgba,
396 track_color: Rgba,
398 thumb_color: Rgba,
400 track_height as "track_height_px": f32,
402 thumb_diameter as "thumb_diameter_px": f32,
404 tick_mark_length as "tick_mark_length_px": f32,
406 disabled_opacity: f32,
408 }
409 soft_option {
410 thumb_hover_color: Rgba,
412 disabled_fill_color: Rgba,
414 disabled_track_color: Rgba,
416 disabled_thumb_color: Rgba,
418 }
419 }
420}
421
422define_widget_pair! {
425 ProgressBarTheme / ResolvedProgressBarTheme {
427 option {
428 fill_color: Rgba,
430 track_color: Rgba,
432 track_height as "track_height_px": f32,
434 min_width as "min_width_px": f32,
436 }
437 optional_nested {
438 border: [BorderSpec, ResolvedBorderSpec],
440 }
441 }
442}
443
444define_widget_pair! {
447 TabTheme / ResolvedTabTheme {
449 option {
450 background_color: Rgba,
452 active_background: Rgba,
454 active_text_color: Rgba,
456 bar_background: Rgba,
458 min_width as "min_width_px": f32,
460 min_height as "min_height_px": f32,
462 hover_text_color: Rgba,
464 }
465 soft_option {
466 hover_background: Rgba,
468 }
469 optional_nested {
470 font: [FontSpec, ResolvedFontSpec],
472 }
473 border_optional {
474 border: [BorderSpec, ResolvedBorderSpec],
476 }
477 }
478}
479
480define_widget_pair! {
483 SidebarTheme / ResolvedSidebarTheme {
485 option {
486 background_color: Rgba,
488 selection_background: Rgba,
490 selection_text_color: Rgba,
492 hover_background: Rgba,
494 }
495 optional_nested {
496 font: [FontSpec, ResolvedFontSpec],
498 }
499 border_partial {
500 border: [BorderSpec, ResolvedBorderSpec],
502 }
503 }
504}
505
506define_widget_pair! {
509 ToolbarTheme / ResolvedToolbarTheme {
511 option {
512 background_color: Rgba,
514 bar_height as "bar_height_px": f32,
516 item_gap as "item_gap_px": f32,
518 icon_size as "icon_size_px": f32,
520 }
521 optional_nested {
522 font: [FontSpec, ResolvedFontSpec],
524 border: [BorderSpec, ResolvedBorderSpec],
526 }
527 }
528}
529
530define_widget_pair! {
533 StatusBarTheme / ResolvedStatusBarTheme {
535 option {
536 background_color: Rgba,
538 }
539 optional_nested {
540 font: [FontSpec, ResolvedFontSpec],
542 }
543 border_partial {
544 border: [BorderSpec, ResolvedBorderSpec],
546 }
547 }
548}
549
550define_widget_pair! {
553 ListTheme / ResolvedListTheme {
555 option {
556 background_color: Rgba,
558 alternate_row_background: Rgba,
560 selection_background: Rgba,
562 selection_text_color: Rgba,
564 header_background: Rgba,
566 grid_color: Rgba,
568 row_height as "row_height_px": f32,
570 hover_background: Rgba,
572 hover_text_color: Rgba,
574 disabled_text_color: Rgba,
576 }
577 optional_nested {
578 item_font: [FontSpec, ResolvedFontSpec],
580 header_font: [FontSpec, ResolvedFontSpec],
582 border: [BorderSpec, ResolvedBorderSpec],
584 }
585 }
586}
587
588define_widget_pair! {
591 PopoverTheme / ResolvedPopoverTheme {
593 option {
594 background_color: Rgba,
596 }
597 optional_nested {
598 font: [FontSpec, ResolvedFontSpec],
600 border: [BorderSpec, ResolvedBorderSpec],
602 }
603 }
604}
605
606define_widget_pair! {
609 SplitterTheme / ResolvedSplitterTheme {
611 option {
612 divider_width as "divider_width_px": f32,
614 divider_color: Rgba,
616 hover_color: Rgba,
618 }
619 }
620}
621
622define_widget_pair! {
625 SeparatorTheme / ResolvedSeparatorTheme {
627 option {
628 line_color: Rgba,
630 line_width as "line_width_px": f32,
632 }
633 }
634}
635
636define_widget_pair! {
639 SwitchTheme / ResolvedSwitchTheme {
641 option {
642 checked_background: Rgba,
644 unchecked_background: Rgba,
646 thumb_background: Rgba,
648 track_width as "track_width_px": f32,
650 track_height as "track_height_px": f32,
652 thumb_diameter as "thumb_diameter_px": f32,
654 track_radius as "track_radius_px": f32,
656 disabled_opacity: f32,
658 }
659 soft_option {
660 hover_checked_background: Rgba,
662 hover_unchecked_background: Rgba,
664 disabled_checked_background: Rgba,
666 disabled_unchecked_background: Rgba,
668 disabled_thumb_color: Rgba,
670 }
671 }
672}
673
674define_widget_pair! {
677 DialogTheme / ResolvedDialogTheme {
679 option {
680 background_color: Rgba,
682 min_width as "min_width_px": f32,
684 max_width as "max_width_px": f32,
686 min_height as "min_height_px": f32,
688 max_height as "max_height_px": f32,
690 button_gap as "button_gap_px": f32,
692 icon_size as "icon_size_px": f32,
694 button_order: DialogButtonOrder,
696 }
697 optional_nested {
698 title_font: [FontSpec, ResolvedFontSpec],
700 body_font: [FontSpec, ResolvedFontSpec],
702 border: [BorderSpec, ResolvedBorderSpec],
704 }
705 }
706}
707
708define_widget_pair! {
711 SpinnerTheme / ResolvedSpinnerTheme {
713 option {
714 fill_color: Rgba,
716 diameter as "diameter_px": f32,
718 min_diameter as "min_diameter_px": f32,
720 stroke_width as "stroke_width_px": f32,
722 }
723 }
724}
725
726define_widget_pair! {
729 ComboBoxTheme / ResolvedComboBoxTheme {
731 option {
732 background_color: Rgba,
734 min_height as "min_height_px": f32,
736 min_width as "min_width_px": f32,
738 arrow_icon_size as "arrow_icon_size_px": f32,
740 arrow_area_width as "arrow_area_width_px": f32,
742 disabled_opacity: f32,
744 disabled_text_color: Rgba,
746 }
747 soft_option {
748 hover_background: Rgba,
750 disabled_background: Rgba,
752 }
753 optional_nested {
754 font: [FontSpec, ResolvedFontSpec],
756 border: [BorderSpec, ResolvedBorderSpec],
758 }
759 }
760}
761
762define_widget_pair! {
765 SegmentedControlTheme / ResolvedSegmentedControlTheme {
767 option {
768 background_color: Rgba,
770 active_background: Rgba,
772 active_text_color: Rgba,
774 segment_height as "segment_height_px": f32,
776 separator_width as "separator_width_px": f32,
778 disabled_opacity: f32,
780 }
781 soft_option {
782 hover_background: Rgba,
784 }
785 optional_nested {
786 font: [FontSpec, ResolvedFontSpec],
788 border: [BorderSpec, ResolvedBorderSpec],
790 }
791 }
792}
793
794define_widget_pair! {
797 CardTheme / ResolvedCardTheme {
799 option {
800 background_color: Rgba,
802 }
803 border_optional {
804 border: [BorderSpec, ResolvedBorderSpec],
806 }
807 }
808}
809
810define_widget_pair! {
813 ExpanderTheme / ResolvedExpanderTheme {
815 option {
816 header_height as "header_height_px": f32,
818 arrow_icon_size as "arrow_icon_size_px": f32,
820 }
821 soft_option {
822 hover_background: Rgba,
824 arrow_color: Rgba,
826 }
827 optional_nested {
828 font: [FontSpec, ResolvedFontSpec],
830 border: [BorderSpec, ResolvedBorderSpec],
832 }
833 }
834}
835
836define_widget_pair! {
839 LinkTheme / ResolvedLinkTheme {
841 option {
842 visited_text_color: Rgba,
844 underline_enabled: bool,
846 background_color: Rgba,
848 hover_background: Rgba,
850 hover_text_color: Rgba,
852 active_text_color: Rgba,
854 disabled_text_color: Rgba,
856 }
857 optional_nested {
858 font: [FontSpec, ResolvedFontSpec],
860 }
861 }
862}
863
864define_widget_pair! {
867 LayoutTheme / ResolvedLayoutTheme {
872 option {
873 widget_gap as "widget_gap_px": f32,
875 container_margin as "container_margin_px": f32,
877 window_margin as "window_margin_px": f32,
879 section_gap as "section_gap_px": f32,
881 }
882 }
883}
884
885use crate::resolve::validate_helpers::{
888 check_min_max, check_non_negative, check_positive, check_range_f32, check_range_u16,
889};
890
891impl ResolvedWindowTheme {
892 pub(crate) fn check_ranges(&self, prefix: &str, errors: &mut Vec<String>) {
893 check_positive(
894 self.title_bar_font.size,
895 &format!("{prefix}.title_bar_font.size"),
896 errors,
897 );
898 check_range_u16(
899 self.title_bar_font.weight,
900 100,
901 900,
902 &format!("{prefix}.title_bar_font.weight"),
903 errors,
904 );
905 }
906}
907
908impl ResolvedButtonTheme {
909 pub(crate) fn check_ranges(&self, prefix: &str, errors: &mut Vec<String>) {
910 check_non_negative(self.min_width, &format!("{prefix}.min_width"), errors);
911 check_non_negative(self.min_height, &format!("{prefix}.min_height"), errors);
912 check_non_negative(
913 self.icon_text_gap,
914 &format!("{prefix}.icon_text_gap"),
915 errors,
916 );
917 check_range_f32(
918 self.disabled_opacity,
919 0.0,
920 1.0,
921 &format!("{prefix}.disabled_opacity"),
922 errors,
923 );
924 check_positive(self.font.size, &format!("{prefix}.font.size"), errors);
925 check_range_u16(
926 self.font.weight,
927 100,
928 900,
929 &format!("{prefix}.font.weight"),
930 errors,
931 );
932 }
933}
934
935impl ResolvedInputTheme {
936 pub(crate) fn check_ranges(&self, prefix: &str, errors: &mut Vec<String>) {
937 check_non_negative(self.min_height, &format!("{prefix}.min_height"), errors);
938 check_range_f32(
939 self.disabled_opacity,
940 0.0,
941 1.0,
942 &format!("{prefix}.disabled_opacity"),
943 errors,
944 );
945 check_positive(self.font.size, &format!("{prefix}.font.size"), errors);
946 check_range_u16(
947 self.font.weight,
948 100,
949 900,
950 &format!("{prefix}.font.weight"),
951 errors,
952 );
953 }
954}
955
956impl ResolvedCheckboxTheme {
957 pub(crate) fn check_ranges(&self, prefix: &str, errors: &mut Vec<String>) {
958 check_non_negative(
959 self.indicator_width,
960 &format!("{prefix}.indicator_width"),
961 errors,
962 );
963 check_non_negative(self.label_gap, &format!("{prefix}.label_gap"), errors);
964 check_range_f32(
965 self.disabled_opacity,
966 0.0,
967 1.0,
968 &format!("{prefix}.disabled_opacity"),
969 errors,
970 );
971 check_positive(self.font.size, &format!("{prefix}.font.size"), errors);
972 check_range_u16(
973 self.font.weight,
974 100,
975 900,
976 &format!("{prefix}.font.weight"),
977 errors,
978 );
979 }
980}
981
982impl ResolvedMenuTheme {
983 pub(crate) fn check_ranges(&self, prefix: &str, errors: &mut Vec<String>) {
984 check_non_negative(self.row_height, &format!("{prefix}.row_height"), errors);
985 check_non_negative(
986 self.icon_text_gap,
987 &format!("{prefix}.icon_text_gap"),
988 errors,
989 );
990 check_non_negative(self.icon_size, &format!("{prefix}.icon_size"), errors);
991 check_positive(self.font.size, &format!("{prefix}.font.size"), errors);
992 check_range_u16(
993 self.font.weight,
994 100,
995 900,
996 &format!("{prefix}.font.weight"),
997 errors,
998 );
999 }
1000}
1001
1002impl ResolvedTooltipTheme {
1003 pub(crate) fn check_ranges(&self, prefix: &str, errors: &mut Vec<String>) {
1004 check_non_negative(self.max_width, &format!("{prefix}.max_width"), errors);
1005 check_positive(self.font.size, &format!("{prefix}.font.size"), errors);
1006 check_range_u16(
1007 self.font.weight,
1008 100,
1009 900,
1010 &format!("{prefix}.font.weight"),
1011 errors,
1012 );
1013 }
1014}
1015
1016impl ResolvedScrollbarTheme {
1017 pub(crate) fn check_ranges(&self, prefix: &str, errors: &mut Vec<String>) {
1018 check_non_negative(self.groove_width, &format!("{prefix}.groove_width"), errors);
1019 check_non_negative(
1020 self.min_thumb_length,
1021 &format!("{prefix}.min_thumb_length"),
1022 errors,
1023 );
1024 check_non_negative(self.thumb_width, &format!("{prefix}.thumb_width"), errors);
1025 }
1026}
1027
1028impl ResolvedSliderTheme {
1029 pub(crate) fn check_ranges(&self, prefix: &str, errors: &mut Vec<String>) {
1030 check_non_negative(self.track_height, &format!("{prefix}.track_height"), errors);
1031 check_non_negative(
1032 self.thumb_diameter,
1033 &format!("{prefix}.thumb_diameter"),
1034 errors,
1035 );
1036 check_non_negative(
1037 self.tick_mark_length,
1038 &format!("{prefix}.tick_mark_length"),
1039 errors,
1040 );
1041 check_range_f32(
1042 self.disabled_opacity,
1043 0.0,
1044 1.0,
1045 &format!("{prefix}.disabled_opacity"),
1046 errors,
1047 );
1048 }
1049}
1050
1051impl ResolvedProgressBarTheme {
1052 pub(crate) fn check_ranges(&self, prefix: &str, errors: &mut Vec<String>) {
1053 check_non_negative(self.track_height, &format!("{prefix}.track_height"), errors);
1054 check_non_negative(self.min_width, &format!("{prefix}.min_width"), errors);
1055 }
1056}
1057
1058impl ResolvedTabTheme {
1059 pub(crate) fn check_ranges(&self, prefix: &str, errors: &mut Vec<String>) {
1060 check_non_negative(self.min_width, &format!("{prefix}.min_width"), errors);
1061 check_non_negative(self.min_height, &format!("{prefix}.min_height"), errors);
1062 check_positive(self.font.size, &format!("{prefix}.font.size"), errors);
1063 check_range_u16(
1064 self.font.weight,
1065 100,
1066 900,
1067 &format!("{prefix}.font.weight"),
1068 errors,
1069 );
1070 }
1071}
1072
1073impl ResolvedSidebarTheme {
1074 pub(crate) fn check_ranges(&self, prefix: &str, errors: &mut Vec<String>) {
1075 check_positive(self.font.size, &format!("{prefix}.font.size"), errors);
1076 check_range_u16(
1077 self.font.weight,
1078 100,
1079 900,
1080 &format!("{prefix}.font.weight"),
1081 errors,
1082 );
1083 }
1084}
1085
1086impl ResolvedToolbarTheme {
1087 pub(crate) fn check_ranges(&self, prefix: &str, errors: &mut Vec<String>) {
1088 check_non_negative(self.bar_height, &format!("{prefix}.bar_height"), errors);
1089 check_non_negative(self.item_gap, &format!("{prefix}.item_gap"), errors);
1090 check_non_negative(self.icon_size, &format!("{prefix}.icon_size"), errors);
1091 check_positive(self.font.size, &format!("{prefix}.font.size"), errors);
1092 check_range_u16(
1093 self.font.weight,
1094 100,
1095 900,
1096 &format!("{prefix}.font.weight"),
1097 errors,
1098 );
1099 }
1100}
1101
1102impl ResolvedStatusBarTheme {
1103 pub(crate) fn check_ranges(&self, prefix: &str, errors: &mut Vec<String>) {
1104 check_positive(self.font.size, &format!("{prefix}.font.size"), errors);
1105 check_range_u16(
1106 self.font.weight,
1107 100,
1108 900,
1109 &format!("{prefix}.font.weight"),
1110 errors,
1111 );
1112 }
1113}
1114
1115impl ResolvedListTheme {
1116 pub(crate) fn check_ranges(&self, prefix: &str, errors: &mut Vec<String>) {
1117 check_non_negative(self.row_height, &format!("{prefix}.row_height"), errors);
1118 check_positive(
1119 self.item_font.size,
1120 &format!("{prefix}.item_font.size"),
1121 errors,
1122 );
1123 check_range_u16(
1124 self.item_font.weight,
1125 100,
1126 900,
1127 &format!("{prefix}.item_font.weight"),
1128 errors,
1129 );
1130 check_positive(
1131 self.header_font.size,
1132 &format!("{prefix}.header_font.size"),
1133 errors,
1134 );
1135 check_range_u16(
1136 self.header_font.weight,
1137 100,
1138 900,
1139 &format!("{prefix}.header_font.weight"),
1140 errors,
1141 );
1142 }
1143}
1144
1145impl ResolvedPopoverTheme {
1146 pub(crate) fn check_ranges(&self, prefix: &str, errors: &mut Vec<String>) {
1147 check_positive(self.font.size, &format!("{prefix}.font.size"), errors);
1148 check_range_u16(
1149 self.font.weight,
1150 100,
1151 900,
1152 &format!("{prefix}.font.weight"),
1153 errors,
1154 );
1155 }
1156}
1157
1158impl ResolvedSplitterTheme {
1159 pub(crate) fn check_ranges(&self, prefix: &str, errors: &mut Vec<String>) {
1160 check_non_negative(
1161 self.divider_width,
1162 &format!("{prefix}.divider_width"),
1163 errors,
1164 );
1165 }
1166}
1167
1168impl ResolvedSeparatorTheme {
1169 pub(crate) fn check_ranges(&self, prefix: &str, errors: &mut Vec<String>) {
1170 check_non_negative(self.line_width, &format!("{prefix}.line_width"), errors);
1171 }
1172}
1173
1174impl ResolvedSwitchTheme {
1175 pub(crate) fn check_ranges(&self, prefix: &str, errors: &mut Vec<String>) {
1176 check_non_negative(self.track_width, &format!("{prefix}.track_width"), errors);
1177 check_non_negative(self.track_height, &format!("{prefix}.track_height"), errors);
1178 check_non_negative(
1179 self.thumb_diameter,
1180 &format!("{prefix}.thumb_diameter"),
1181 errors,
1182 );
1183 check_non_negative(self.track_radius, &format!("{prefix}.track_radius"), errors);
1184 check_range_f32(
1185 self.disabled_opacity,
1186 0.0,
1187 1.0,
1188 &format!("{prefix}.disabled_opacity"),
1189 errors,
1190 );
1191 }
1192}
1193
1194impl ResolvedDialogTheme {
1195 pub(crate) fn check_ranges(&self, prefix: &str, errors: &mut Vec<String>) {
1196 check_non_negative(self.min_width, &format!("{prefix}.min_width"), errors);
1197 check_non_negative(self.max_width, &format!("{prefix}.max_width"), errors);
1198 check_non_negative(self.min_height, &format!("{prefix}.min_height"), errors);
1199 check_non_negative(self.max_height, &format!("{prefix}.max_height"), errors);
1200 check_non_negative(self.button_gap, &format!("{prefix}.button_gap"), errors);
1201 check_non_negative(self.icon_size, &format!("{prefix}.icon_size"), errors);
1202 check_positive(
1203 self.title_font.size,
1204 &format!("{prefix}.title_font.size"),
1205 errors,
1206 );
1207 check_range_u16(
1208 self.title_font.weight,
1209 100,
1210 900,
1211 &format!("{prefix}.title_font.weight"),
1212 errors,
1213 );
1214 check_positive(
1215 self.body_font.size,
1216 &format!("{prefix}.body_font.size"),
1217 errors,
1218 );
1219 check_range_u16(
1220 self.body_font.weight,
1221 100,
1222 900,
1223 &format!("{prefix}.body_font.weight"),
1224 errors,
1225 );
1226 check_min_max(
1227 self.min_width,
1228 self.max_width,
1229 &format!("{prefix}.min_width"),
1230 &format!("{prefix}.max_width"),
1231 errors,
1232 );
1233 check_min_max(
1234 self.min_height,
1235 self.max_height,
1236 &format!("{prefix}.min_height"),
1237 &format!("{prefix}.max_height"),
1238 errors,
1239 );
1240 }
1241}
1242
1243impl ResolvedSpinnerTheme {
1244 pub(crate) fn check_ranges(&self, prefix: &str, errors: &mut Vec<String>) {
1245 check_non_negative(self.diameter, &format!("{prefix}.diameter"), errors);
1246 check_non_negative(self.min_diameter, &format!("{prefix}.min_diameter"), errors);
1247 check_non_negative(self.stroke_width, &format!("{prefix}.stroke_width"), errors);
1248 }
1249}
1250
1251impl ResolvedLinkTheme {
1252 pub(crate) fn check_ranges(&self, prefix: &str, errors: &mut Vec<String>) {
1253 check_positive(self.font.size, &format!("{prefix}.font.size"), errors);
1254 check_range_u16(
1255 self.font.weight,
1256 100,
1257 900,
1258 &format!("{prefix}.font.weight"),
1259 errors,
1260 );
1261 }
1262}
1263
1264impl ResolvedComboBoxTheme {
1265 pub(crate) fn check_ranges(&self, prefix: &str, errors: &mut Vec<String>) {
1266 check_non_negative(self.min_height, &format!("{prefix}.min_height"), errors);
1267 check_non_negative(self.min_width, &format!("{prefix}.min_width"), errors);
1268 check_non_negative(
1269 self.arrow_icon_size,
1270 &format!("{prefix}.arrow_icon_size"),
1271 errors,
1272 );
1273 check_non_negative(
1274 self.arrow_area_width,
1275 &format!("{prefix}.arrow_area_width"),
1276 errors,
1277 );
1278 check_range_f32(
1279 self.disabled_opacity,
1280 0.0,
1281 1.0,
1282 &format!("{prefix}.disabled_opacity"),
1283 errors,
1284 );
1285 check_positive(self.font.size, &format!("{prefix}.font.size"), errors);
1286 check_range_u16(
1287 self.font.weight,
1288 100,
1289 900,
1290 &format!("{prefix}.font.weight"),
1291 errors,
1292 );
1293 }
1294}
1295
1296impl ResolvedSegmentedControlTheme {
1297 pub(crate) fn check_ranges(&self, prefix: &str, errors: &mut Vec<String>) {
1298 check_non_negative(
1299 self.segment_height,
1300 &format!("{prefix}.segment_height"),
1301 errors,
1302 );
1303 check_non_negative(
1304 self.separator_width,
1305 &format!("{prefix}.separator_width"),
1306 errors,
1307 );
1308 check_range_f32(
1309 self.disabled_opacity,
1310 0.0,
1311 1.0,
1312 &format!("{prefix}.disabled_opacity"),
1313 errors,
1314 );
1315 check_positive(self.font.size, &format!("{prefix}.font.size"), errors);
1316 check_range_u16(
1317 self.font.weight,
1318 100,
1319 900,
1320 &format!("{prefix}.font.weight"),
1321 errors,
1322 );
1323 }
1324}
1325
1326impl ResolvedExpanderTheme {
1327 pub(crate) fn check_ranges(&self, prefix: &str, errors: &mut Vec<String>) {
1328 check_non_negative(
1329 self.header_height,
1330 &format!("{prefix}.header_height"),
1331 errors,
1332 );
1333 check_non_negative(
1334 self.arrow_icon_size,
1335 &format!("{prefix}.arrow_icon_size"),
1336 errors,
1337 );
1338 check_positive(self.font.size, &format!("{prefix}.font.size"), errors);
1339 check_range_u16(
1340 self.font.weight,
1341 100,
1342 900,
1343 &format!("{prefix}.font.weight"),
1344 errors,
1345 );
1346 }
1347}
1348
1349#[cfg(test)]
1350#[allow(clippy::unwrap_used, clippy::expect_used, dead_code)]
1351mod tests {
1352 use super::*;
1353 use crate::Rgba;
1354 use crate::model::border::{BorderSpec, ResolvedBorderSpec};
1355 use crate::model::font::FontSize;
1356 use crate::model::{DialogButtonOrder, FontSpec};
1357
1358 define_widget_pair! {
1360 TestWidget / ResolvedTestWidget {
1362 option {
1363 size: f32,
1364 label: String,
1365 }
1366 optional_nested {
1367 font: [FontSpec, ResolvedFontSpec],
1368 }
1369 }
1370 }
1371
1372 #[test]
1375 fn resolved_font_spec_fields_are_concrete() {
1376 let rfs = ResolvedFontSpec {
1377 family: "Inter".into(),
1378 size: 14.0,
1379 weight: 400,
1380 style: crate::model::font::FontStyle::Normal,
1381 color: crate::Rgba::rgb(0, 0, 0),
1382 };
1383 assert_eq!(rfs.family, "Inter");
1384 assert_eq!(rfs.size, 14.0);
1385 assert_eq!(rfs.weight, 400);
1386 }
1387
1388 #[test]
1391 fn generated_option_struct_has_option_fields() {
1392 let w = TestWidget::default();
1393 assert!(w.size.is_none());
1394 assert!(w.label.is_none());
1395 assert!(w.font.is_none());
1396 }
1397
1398 #[test]
1399 fn generated_option_struct_is_empty_by_default() {
1400 assert!(TestWidget::default().is_empty());
1401 }
1402
1403 #[test]
1404 fn generated_option_struct_not_empty_when_size_set() {
1405 let w = TestWidget {
1406 size: Some(24.0),
1407 ..Default::default()
1408 };
1409 assert!(!w.is_empty());
1410 }
1411
1412 #[test]
1413 fn generated_option_struct_not_empty_when_font_set() {
1414 let w = TestWidget {
1415 font: Some(FontSpec {
1416 size: Some(FontSize::Px(14.0)),
1417 ..Default::default()
1418 }),
1419 ..Default::default()
1420 };
1421 assert!(!w.is_empty());
1422 }
1423
1424 #[test]
1425 fn generated_resolved_struct_has_concrete_fields() {
1426 let resolved = ResolvedTestWidget {
1427 size: 24.0,
1428 label: "Click me".into(),
1429 font: ResolvedFontSpec {
1430 family: "Inter".into(),
1431 size: 14.0,
1432 weight: 400,
1433 style: crate::model::font::FontStyle::Normal,
1434 color: crate::Rgba::rgb(0, 0, 0),
1435 },
1436 };
1437 assert_eq!(resolved.size, 24.0);
1438 assert_eq!(resolved.label, "Click me");
1439 assert_eq!(resolved.font.family, "Inter");
1440 }
1441
1442 #[test]
1445 fn generated_merge_option_field_overlay_wins() {
1446 let mut base = TestWidget {
1447 size: Some(20.0),
1448 ..Default::default()
1449 };
1450 let overlay = TestWidget {
1451 size: Some(24.0),
1452 ..Default::default()
1453 };
1454 base.merge(&overlay);
1455 assert_eq!(base.size, Some(24.0));
1456 }
1457
1458 #[test]
1459 fn generated_merge_option_field_none_preserves_base() {
1460 let mut base = TestWidget {
1461 size: Some(20.0),
1462 ..Default::default()
1463 };
1464 let overlay = TestWidget::default();
1465 base.merge(&overlay);
1466 assert_eq!(base.size, Some(20.0));
1467 }
1468
1469 #[test]
1470 fn generated_merge_optional_nested_both_some_merges_inner() {
1471 let mut base = TestWidget {
1472 font: Some(FontSpec {
1473 family: Some("Noto Sans".into()),
1474 size: Some(FontSize::Px(12.0)),
1475 weight: None,
1476 ..Default::default()
1477 }),
1478 ..Default::default()
1479 };
1480 let overlay = TestWidget {
1481 font: Some(FontSpec {
1482 family: None,
1483 size: None,
1484 weight: Some(700),
1485 ..Default::default()
1486 }),
1487 ..Default::default()
1488 };
1489 base.merge(&overlay);
1490 let font = base.font.as_ref().unwrap();
1491 assert_eq!(font.family.as_deref(), Some("Noto Sans")); assert_eq!(font.size, Some(FontSize::Px(12.0))); assert_eq!(font.weight, Some(700)); }
1495
1496 #[test]
1497 fn generated_merge_optional_nested_none_plus_some_clones() {
1498 let mut base = TestWidget::default();
1499 let overlay = TestWidget {
1500 font: Some(FontSpec {
1501 family: Some("Inter".into()),
1502 size: Some(FontSize::Px(14.0)),
1503 weight: Some(400),
1504 ..Default::default()
1505 }),
1506 ..Default::default()
1507 };
1508 base.merge(&overlay);
1509 let font = base.font.as_ref().unwrap();
1510 assert_eq!(font.family.as_deref(), Some("Inter"));
1511 assert_eq!(font.size, Some(FontSize::Px(14.0)));
1512 assert_eq!(font.weight, Some(400));
1513 }
1514
1515 #[test]
1516 fn generated_merge_optional_nested_some_plus_none_preserves_base() {
1517 let mut base = TestWidget {
1518 font: Some(FontSpec {
1519 family: Some("Inter".into()),
1520 size: Some(FontSize::Px(14.0)),
1521 weight: Some(400),
1522 ..Default::default()
1523 }),
1524 ..Default::default()
1525 };
1526 let overlay = TestWidget::default();
1527 base.merge(&overlay);
1528 let font = base.font.as_ref().unwrap();
1529 assert_eq!(font.family.as_deref(), Some("Inter"));
1530 }
1531
1532 #[test]
1533 fn generated_merge_optional_nested_none_plus_none_stays_none() {
1534 let mut base = TestWidget::default();
1535 let overlay = TestWidget::default();
1536 base.merge(&overlay);
1537 assert!(base.font.is_none());
1538 }
1539
1540 #[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
1544 struct WithFont {
1545 name: Option<String>,
1546 font: Option<FontSpec>,
1547 }
1548
1549 impl_merge!(WithFont {
1550 option { name }
1551 optional_nested { font }
1552 });
1553
1554 #[test]
1555 fn impl_merge_optional_nested_none_none_stays_none() {
1556 let mut base = WithFont::default();
1557 let overlay = WithFont::default();
1558 base.merge(&overlay);
1559 assert!(base.font.is_none());
1560 }
1561
1562 #[test]
1563 fn impl_merge_optional_nested_some_none_preserves_base() {
1564 let mut base = WithFont {
1565 font: Some(FontSpec {
1566 size: Some(FontSize::Px(12.0)),
1567 ..Default::default()
1568 }),
1569 ..Default::default()
1570 };
1571 let overlay = WithFont::default();
1572 base.merge(&overlay);
1573 assert_eq!(base.font.as_ref().unwrap().size, Some(FontSize::Px(12.0)));
1574 }
1575
1576 #[test]
1577 fn impl_merge_optional_nested_none_some_clones_overlay() {
1578 let mut base = WithFont::default();
1579 let overlay = WithFont {
1580 font: Some(FontSpec {
1581 family: Some("Inter".into()),
1582 ..Default::default()
1583 }),
1584 ..Default::default()
1585 };
1586 base.merge(&overlay);
1587 assert_eq!(base.font.as_ref().unwrap().family.as_deref(), Some("Inter"));
1588 }
1589
1590 #[test]
1591 fn impl_merge_optional_nested_some_some_merges_inner() {
1592 let mut base = WithFont {
1593 font: Some(FontSpec {
1594 family: Some("Noto".into()),
1595 size: Some(FontSize::Px(11.0)),
1596 weight: None,
1597 ..Default::default()
1598 }),
1599 ..Default::default()
1600 };
1601 let overlay = WithFont {
1602 font: Some(FontSpec {
1603 family: None,
1604 size: Some(FontSize::Px(14.0)),
1605 weight: Some(400),
1606 ..Default::default()
1607 }),
1608 ..Default::default()
1609 };
1610 base.merge(&overlay);
1611 let f = base.font.as_ref().unwrap();
1612 assert_eq!(f.family.as_deref(), Some("Noto")); assert_eq!(f.size, Some(FontSize::Px(14.0))); assert_eq!(f.weight, Some(400)); }
1616
1617 #[test]
1618 fn impl_merge_optional_nested_is_empty_none() {
1619 let w = WithFont::default();
1620 assert!(w.is_empty());
1621 }
1622
1623 #[test]
1624 fn impl_merge_optional_nested_is_empty_some_default() {
1625 let w = WithFont {
1627 font: Some(FontSpec::default()),
1628 ..Default::default()
1629 };
1630 assert!(w.is_empty());
1631 }
1632
1633 #[test]
1634 fn impl_merge_optional_nested_is_not_empty_when_populated() {
1635 let w = WithFont {
1636 font: Some(FontSpec {
1637 size: Some(FontSize::Px(14.0)),
1638 ..Default::default()
1639 }),
1640 ..Default::default()
1641 };
1642 assert!(!w.is_empty());
1643 }
1644
1645 #[test]
1648 fn button_theme_default_is_empty() {
1649 assert!(ButtonTheme::default().is_empty());
1650 }
1651
1652 #[test]
1653 fn button_theme_not_empty_when_set() {
1654 let b = ButtonTheme {
1655 background_color: Some(Rgba::rgb(200, 200, 200)),
1656 min_width: Some(64.0),
1657 ..Default::default()
1658 };
1659 assert!(!b.is_empty());
1660 }
1661
1662 #[test]
1663 fn button_theme_merge_font_optional_nested() {
1664 let mut base = ButtonTheme {
1665 font: Some(FontSpec {
1666 family: Some("Noto Sans".into()),
1667 size: Some(FontSize::Px(11.0)),
1668 weight: None,
1669 ..Default::default()
1670 }),
1671 ..Default::default()
1672 };
1673 let overlay = ButtonTheme {
1674 font: Some(FontSpec {
1675 family: None,
1676 weight: Some(700),
1677 ..Default::default()
1678 }),
1679 ..Default::default()
1680 };
1681 base.merge(&overlay);
1682 let f = base.font.as_ref().unwrap();
1683 assert_eq!(f.family.as_deref(), Some("Noto Sans")); assert_eq!(f.weight, Some(700)); }
1686
1687 #[test]
1688 fn button_theme_toml_round_trip_with_font_and_border() {
1689 let b = ButtonTheme {
1690 background_color: Some(Rgba::rgb(200, 200, 200)),
1691 font: Some(FontSpec {
1692 family: Some("Inter".into()),
1693 size: Some(FontSize::Px(14.0)),
1694 weight: Some(400),
1695 ..Default::default()
1696 }),
1697 border: Some(BorderSpec {
1698 corner_radius: Some(4.0),
1699 ..Default::default()
1700 }),
1701 ..Default::default()
1702 };
1703 let toml_str = toml::to_string(&b).unwrap();
1704 let b2: ButtonTheme = toml::from_str(&toml_str).unwrap();
1705 assert_eq!(b, b2);
1706 }
1707
1708 #[test]
1711 fn window_theme_has_new_fields() {
1712 let w = WindowTheme {
1713 inactive_title_bar_background: Some(Rgba::rgb(180, 180, 180)),
1714 inactive_title_bar_text_color: Some(Rgba::rgb(120, 120, 120)),
1715 title_bar_font: Some(FontSpec {
1716 weight: Some(700),
1717 ..Default::default()
1718 }),
1719 border: Some(BorderSpec {
1720 corner_radius: Some(4.0),
1721 shadow_enabled: Some(true),
1722 ..Default::default()
1723 }),
1724 ..Default::default()
1725 };
1726 assert!(!w.is_empty());
1727 assert!(w.inactive_title_bar_background.is_some());
1728 assert!(w.inactive_title_bar_text_color.is_some());
1729 assert!(w.title_bar_font.is_some());
1730 assert!(w.border.is_some());
1731 }
1732
1733 #[test]
1734 fn window_theme_default_is_empty() {
1735 assert!(WindowTheme::default().is_empty());
1736 }
1737
1738 #[test]
1741 fn dialog_theme_button_order_works() {
1742 let d = DialogTheme {
1743 button_order: Some(DialogButtonOrder::PrimaryRight),
1744 min_width: Some(300.0),
1745 ..Default::default()
1746 };
1747 assert_eq!(d.button_order, Some(DialogButtonOrder::PrimaryRight));
1748 assert_eq!(d.min_width, Some(300.0));
1749 assert!(!d.is_empty());
1750 }
1751
1752 #[test]
1753 fn dialog_theme_button_order_toml_round_trip() {
1754 let d = DialogTheme {
1755 button_order: Some(DialogButtonOrder::PrimaryLeft),
1756 ..Default::default()
1757 };
1758 let toml_str = toml::to_string(&d).unwrap();
1759 let d2: DialogTheme = toml::from_str(&toml_str).unwrap();
1760 assert_eq!(d, d2);
1761 }
1762
1763 #[test]
1764 fn dialog_theme_default_is_empty() {
1765 assert!(DialogTheme::default().is_empty());
1766 }
1767
1768 #[test]
1771 fn splitter_theme_single_field_merge() {
1772 let mut base = SplitterTheme {
1773 divider_width: Some(4.0),
1774 ..Default::default()
1775 };
1776 let overlay = SplitterTheme {
1777 divider_width: Some(6.0),
1778 ..Default::default()
1779 };
1780 base.merge(&overlay);
1781 assert_eq!(base.divider_width, Some(6.0));
1782 }
1783
1784 #[test]
1785 fn splitter_theme_merge_none_preserves_base() {
1786 let mut base = SplitterTheme {
1787 divider_width: Some(4.0),
1788 ..Default::default()
1789 };
1790 let overlay = SplitterTheme::default();
1791 base.merge(&overlay);
1792 assert_eq!(base.divider_width, Some(4.0));
1793 }
1794
1795 #[test]
1796 fn splitter_theme_default_is_empty() {
1797 assert!(SplitterTheme::default().is_empty());
1798 }
1799
1800 #[test]
1801 fn splitter_theme_not_empty_when_set() {
1802 assert!(
1803 !SplitterTheme {
1804 divider_width: Some(4.0),
1805 ..Default::default()
1806 }
1807 .is_empty()
1808 );
1809 }
1810
1811 #[test]
1814 fn separator_theme_single_field() {
1815 let s = SeparatorTheme {
1816 line_color: Some(Rgba::rgb(200, 200, 200)),
1817 ..Default::default()
1818 };
1819 assert!(!s.is_empty());
1820 }
1821
1822 #[test]
1825 fn all_widget_theme_defaults_are_empty() {
1826 assert!(WindowTheme::default().is_empty());
1827 assert!(ButtonTheme::default().is_empty());
1828 assert!(InputTheme::default().is_empty());
1829 assert!(CheckboxTheme::default().is_empty());
1830 assert!(MenuTheme::default().is_empty());
1831 assert!(TooltipTheme::default().is_empty());
1832 assert!(ScrollbarTheme::default().is_empty());
1833 assert!(SliderTheme::default().is_empty());
1834 assert!(ProgressBarTheme::default().is_empty());
1835 assert!(TabTheme::default().is_empty());
1836 assert!(SidebarTheme::default().is_empty());
1837 assert!(ToolbarTheme::default().is_empty());
1838 assert!(StatusBarTheme::default().is_empty());
1839 assert!(ListTheme::default().is_empty());
1840 assert!(PopoverTheme::default().is_empty());
1841 assert!(SplitterTheme::default().is_empty());
1842 assert!(SeparatorTheme::default().is_empty());
1843 assert!(SwitchTheme::default().is_empty());
1844 assert!(DialogTheme::default().is_empty());
1845 assert!(SpinnerTheme::default().is_empty());
1846 assert!(ComboBoxTheme::default().is_empty());
1847 assert!(SegmentedControlTheme::default().is_empty());
1848 assert!(CardTheme::default().is_empty());
1849 assert!(ExpanderTheme::default().is_empty());
1850 assert!(LinkTheme::default().is_empty());
1851 }
1852
1853 #[test]
1856 fn input_theme_toml_round_trip() {
1857 let t = InputTheme {
1858 background_color: Some(Rgba::rgb(255, 255, 255)),
1859 font: Some(FontSpec {
1860 family: Some("Noto Sans".into()),
1861 ..Default::default()
1862 }),
1863 border: Some(BorderSpec {
1864 color: Some(Rgba::rgb(180, 180, 180)),
1865 corner_radius: Some(4.0),
1866 ..Default::default()
1867 }),
1868 ..Default::default()
1869 };
1870 let toml_str = toml::to_string(&t).unwrap();
1871 let t2: InputTheme = toml::from_str(&toml_str).unwrap();
1872 assert_eq!(t, t2);
1873 }
1874
1875 #[test]
1876 fn switch_theme_toml_round_trip() {
1877 let s = SwitchTheme {
1878 checked_background: Some(Rgba::rgb(0, 120, 215)),
1879 track_width: Some(40.0),
1880 track_height: Some(20.0),
1881 thumb_diameter: Some(14.0),
1882 track_radius: Some(10.0),
1883 ..Default::default()
1884 };
1885 let toml_str = toml::to_string(&s).unwrap();
1886 let s2: SwitchTheme = toml::from_str(&toml_str).unwrap();
1887 assert_eq!(s, s2);
1888 }
1889
1890 #[test]
1891 fn card_theme_with_border() {
1892 let c = CardTheme {
1893 background_color: Some(Rgba::rgb(255, 255, 255)),
1894 border: Some(BorderSpec {
1895 corner_radius: Some(8.0),
1896 shadow_enabled: Some(true),
1897 ..Default::default()
1898 }),
1899 };
1900 assert!(!c.is_empty());
1901 }
1902
1903 #[test]
1904 fn link_theme_has_underline_enabled_bool_field() {
1905 let l = LinkTheme {
1906 visited_text_color: Some(Rgba::rgb(100, 0, 200)),
1907 underline_enabled: Some(true),
1908 ..Default::default()
1909 };
1910 assert!(!l.is_empty());
1911 assert_eq!(l.underline_enabled, Some(true));
1912 }
1913
1914 #[test]
1915 fn status_bar_theme_has_font_and_background() {
1916 let s = StatusBarTheme {
1917 background_color: Some(Rgba::rgb(240, 240, 240)),
1918 font: Some(FontSpec {
1919 size: Some(FontSize::Px(11.0)),
1920 ..Default::default()
1921 }),
1922 ..Default::default()
1923 };
1924 assert!(!s.is_empty());
1925 }
1926
1927 define_widget_pair! {
1931 DualNestedTestWidget / ResolvedDualNestedTestWidget {
1933 option {
1934 background: Rgba,
1935 min_height: f32,
1936 }
1937 optional_nested {
1938 font: [FontSpec, ResolvedFontSpec],
1939 border: [BorderSpec, ResolvedBorderSpec],
1940 }
1941 }
1942 }
1943
1944 #[test]
1945 fn dual_nested_default_is_empty() {
1946 assert!(DualNestedTestWidget::default().is_empty());
1947 }
1948
1949 #[test]
1950 fn dual_nested_field_names() {
1951 assert_eq!(DualNestedTestWidget::FIELD_NAMES.len(), 4);
1952 assert!(DualNestedTestWidget::FIELD_NAMES.contains(&"background"));
1953 assert!(DualNestedTestWidget::FIELD_NAMES.contains(&"min_height"));
1954 assert!(DualNestedTestWidget::FIELD_NAMES.contains(&"font"));
1955 assert!(DualNestedTestWidget::FIELD_NAMES.contains(&"border"));
1956 }
1957
1958 #[test]
1959 fn dual_nested_not_empty_when_font_set() {
1960 let w = DualNestedTestWidget {
1961 font: Some(FontSpec {
1962 family: Some("Inter".into()),
1963 ..Default::default()
1964 }),
1965 ..Default::default()
1966 };
1967 assert!(!w.is_empty());
1968 }
1969
1970 #[test]
1971 fn dual_nested_not_empty_when_border_set() {
1972 let w = DualNestedTestWidget {
1973 border: Some(BorderSpec {
1974 color: Some(Rgba::rgb(100, 100, 100)),
1975 ..Default::default()
1976 }),
1977 ..Default::default()
1978 };
1979 assert!(!w.is_empty());
1980 }
1981
1982 #[test]
1983 fn dual_nested_merge_both_nested() {
1984 let mut base = DualNestedTestWidget {
1985 font: Some(FontSpec {
1986 family: Some("Noto Sans".into()),
1987 ..Default::default()
1988 }),
1989 ..Default::default()
1990 };
1991 let overlay = DualNestedTestWidget {
1992 border: Some(BorderSpec {
1993 corner_radius: Some(4.0),
1994 ..Default::default()
1995 }),
1996 ..Default::default()
1997 };
1998 base.merge(&overlay);
1999 assert!(base.font.is_some());
2000 assert!(base.border.is_some());
2001 assert_eq!(
2002 base.font.as_ref().and_then(|f| f.family.as_deref()),
2003 Some("Noto Sans")
2004 );
2005 assert_eq!(
2006 base.border.as_ref().and_then(|b| b.corner_radius),
2007 Some(4.0)
2008 );
2009 }
2010
2011 #[test]
2012 fn dual_nested_merge_inner_font_fields() {
2013 let mut base = DualNestedTestWidget {
2014 font: Some(FontSpec {
2015 family: Some("Noto Sans".into()),
2016 ..Default::default()
2017 }),
2018 ..Default::default()
2019 };
2020 let overlay = DualNestedTestWidget {
2021 font: Some(FontSpec {
2022 size: Some(FontSize::Px(14.0)),
2023 ..Default::default()
2024 }),
2025 ..Default::default()
2026 };
2027 base.merge(&overlay);
2028 let font = base.font.as_ref().unwrap();
2029 assert_eq!(font.family.as_deref(), Some("Noto Sans")); assert_eq!(font.size, Some(FontSize::Px(14.0))); }
2032
2033 #[test]
2034 fn dual_nested_toml_round_trip() {
2035 let w = DualNestedTestWidget {
2036 background: Some(Rgba::rgb(240, 240, 240)),
2037 min_height: Some(32.0),
2038 font: Some(FontSpec {
2039 family: Some("Inter".into()),
2040 size: Some(FontSize::Px(14.0)),
2041 weight: Some(400),
2042 ..Default::default()
2043 }),
2044 border: Some(BorderSpec {
2045 color: Some(Rgba::rgb(180, 180, 180)),
2046 corner_radius: Some(4.0),
2047 line_width: Some(1.0),
2048 ..Default::default()
2049 }),
2050 };
2051 let toml_str = toml::to_string(&w).unwrap();
2052 let w2: DualNestedTestWidget = toml::from_str(&toml_str).unwrap();
2053 assert_eq!(w, w2);
2054 }
2055
2056 #[test]
2061 fn button_validate_widget_extracts_all_fields() {
2062 let button = ButtonTheme {
2063 background_color: Some(Rgba::rgb(200, 200, 200)),
2064 primary_background: Some(Rgba::rgb(0, 120, 215)),
2065 primary_text_color: Some(Rgba::rgb(255, 255, 255)),
2066 min_width: Some(80.0),
2067 min_height: Some(32.0),
2068 icon_text_gap: Some(8.0),
2069 disabled_opacity: Some(0.4),
2070 hover_background: Some(Rgba::rgb(210, 210, 210)),
2071 hover_text_color: Some(Rgba::rgb(0, 0, 0)),
2072 active_text_color: Some(Rgba::rgb(0, 0, 0)),
2073 disabled_text_color: Some(Rgba::rgb(128, 128, 128)),
2074 active_background: Some(Rgba::rgb(180, 180, 180)),
2075 disabled_background: Some(Rgba::rgb(220, 220, 220)),
2076 font: Some(FontSpec {
2077 family: Some("Inter".into()),
2078 size: Some(FontSize::Px(14.0)),
2079 weight: Some(400),
2080 style: Some(crate::model::font::FontStyle::Normal),
2081 color: Some(Rgba::rgb(0, 0, 0)),
2082 }),
2083 border: Some(BorderSpec {
2084 color: Some(Rgba::rgb(100, 100, 100)),
2085 corner_radius: Some(4.0),
2086 corner_radius_lg: Some(8.0),
2087 line_width: Some(1.0),
2088 opacity: Some(0.8),
2089 shadow_enabled: Some(false),
2090 padding_horizontal: Some(12.0),
2091 padding_vertical: Some(6.0),
2092 }),
2093 };
2094 let mut missing = Vec::new();
2095 let resolved = ResolvedButtonTheme::validate_widget(&button, "button", 96.0, &mut missing);
2096 assert!(missing.is_empty(), "unexpected missing: {missing:?}");
2097 assert_eq!(resolved.background_color, Rgba::rgb(200, 200, 200));
2098 assert_eq!(resolved.min_width, 80.0);
2099 assert_eq!(resolved.font.family, "Inter");
2100 assert_eq!(resolved.font.size, 14.0);
2101 assert_eq!(resolved.border.corner_radius, 4.0);
2102 assert_eq!(resolved.active_background, Some(Rgba::rgb(180, 180, 180)));
2104 assert_eq!(resolved.disabled_background, Some(Rgba::rgb(220, 220, 220)));
2105 }
2106
2107 #[test]
2108 fn button_validate_widget_records_missing_fields() {
2109 let button = ButtonTheme::default(); let mut missing = Vec::new();
2111 let _ = ResolvedButtonTheme::validate_widget(&button, "button", 96.0, &mut missing);
2112 assert!(missing.contains(&"button.background_color".to_string()));
2114 assert!(missing.contains(&"button.min_width".to_string()));
2115 assert!(missing.contains(&"button.font".to_string()));
2117 assert!(missing.contains(&"button.border".to_string()));
2119 assert!(!missing.iter().any(|m| m.contains("active_background")));
2121 assert!(!missing.iter().any(|m| m.contains("disabled_background")));
2122 }
2123
2124 #[test]
2127 fn layout_theme_default_is_empty() {
2128 assert!(LayoutTheme::default().is_empty());
2129 }
2130
2131 #[test]
2132 fn layout_theme_not_empty_when_widget_gap_set() {
2133 let l = LayoutTheme {
2134 widget_gap: Some(8.0),
2135 ..Default::default()
2136 };
2137 assert!(!l.is_empty());
2138 }
2139
2140 #[test]
2141 fn layout_theme_field_names() {
2142 assert_eq!(LayoutTheme::FIELD_NAMES.len(), 4);
2143 assert!(LayoutTheme::FIELD_NAMES.contains(&"widget_gap_px"));
2144 assert!(LayoutTheme::FIELD_NAMES.contains(&"container_margin_px"));
2145 assert!(LayoutTheme::FIELD_NAMES.contains(&"window_margin_px"));
2146 assert!(LayoutTheme::FIELD_NAMES.contains(&"section_gap_px"));
2147 }
2148
2149 #[test]
2150 fn layout_theme_toml_round_trip() {
2151 let l = LayoutTheme {
2152 widget_gap: Some(8.0),
2153 container_margin: Some(12.0),
2154 window_margin: Some(16.0),
2155 section_gap: Some(24.0),
2156 };
2157 let toml_str = toml::to_string(&l).unwrap();
2158 let l2: LayoutTheme = toml::from_str(&toml_str).unwrap();
2159 assert_eq!(l, l2);
2160 }
2161
2162 #[test]
2163 fn layout_theme_merge() {
2164 let mut base = LayoutTheme {
2165 widget_gap: Some(6.0),
2166 container_margin: Some(10.0),
2167 ..Default::default()
2168 };
2169 let overlay = LayoutTheme {
2170 widget_gap: Some(8.0),
2171 section_gap: Some(24.0),
2172 ..Default::default()
2173 };
2174 base.merge(&overlay);
2175 assert_eq!(base.widget_gap, Some(8.0));
2177 assert_eq!(base.container_margin, Some(10.0));
2179 assert_eq!(base.section_gap, Some(24.0));
2181 assert!(base.window_margin.is_none());
2183 }
2184}