1use super::widgets::ResolvedFontSpec;
8use crate::Rgba;
9
10#[derive(Clone, Debug, PartialEq)]
14pub struct ResolvedSpacing {
15 pub xxs: f32,
17 pub xs: f32,
19 pub s: f32,
21 pub m: f32,
23 pub l: f32,
25 pub xl: f32,
27 pub xxl: f32,
29}
30
31#[derive(Clone, Debug, PartialEq)]
35pub struct ResolvedIconSizes {
36 pub toolbar: f32,
38 pub small: f32,
40 pub large: f32,
42 pub dialog: f32,
44 pub panel: f32,
46}
47
48#[derive(Clone, Debug, Default, PartialEq)]
52pub struct ResolvedTextScaleEntry {
53 pub size: f32,
55 pub weight: u16,
57 pub line_height: f32,
59}
60
61#[derive(Clone, Debug, PartialEq)]
65pub struct ResolvedTextScale {
66 pub caption: ResolvedTextScaleEntry,
68 pub section_heading: ResolvedTextScaleEntry,
70 pub dialog_title: ResolvedTextScaleEntry,
72 pub display: ResolvedTextScaleEntry,
74}
75
76#[derive(Clone, Debug, PartialEq)]
83pub struct ResolvedDefaults {
84 pub font: ResolvedFontSpec,
87 pub line_height: f32,
89 pub mono_font: ResolvedFontSpec,
91
92 pub background: Rgba,
95 pub foreground: Rgba,
97 pub accent: Rgba,
99 pub accent_foreground: Rgba,
101 pub surface: Rgba,
103 pub border: Rgba,
105 pub muted: Rgba,
107 pub shadow: Rgba,
109 pub link: Rgba,
111 pub selection: Rgba,
113 pub selection_foreground: Rgba,
115 pub selection_inactive: Rgba,
117 pub disabled_foreground: Rgba,
119
120 pub danger: Rgba,
123 pub danger_foreground: Rgba,
125 pub warning: Rgba,
127 pub warning_foreground: Rgba,
129 pub success: Rgba,
131 pub success_foreground: Rgba,
133 pub info: Rgba,
135 pub info_foreground: Rgba,
137
138 pub radius: f32,
141 pub radius_lg: f32,
143 pub frame_width: f32,
145 pub disabled_opacity: f32,
147 pub border_opacity: f32,
149 pub shadow_enabled: bool,
151
152 pub focus_ring_color: Rgba,
155 pub focus_ring_width: f32,
157 pub focus_ring_offset: f32,
159
160 pub spacing: ResolvedSpacing,
163
164 pub icon_sizes: ResolvedIconSizes,
167
168 pub text_scaling_factor: f32,
171 pub reduce_motion: bool,
173 pub high_contrast: bool,
175 pub reduce_transparency: bool,
177}
178
179#[derive(Clone, Debug, PartialEq)]
187pub struct ResolvedTheme {
188 pub defaults: ResolvedDefaults,
190 pub text_scale: ResolvedTextScale,
192
193 pub window: super::widgets::ResolvedWindow,
196 pub button: super::widgets::ResolvedButton,
198 pub input: super::widgets::ResolvedInput,
200 pub checkbox: super::widgets::ResolvedCheckbox,
202 pub menu: super::widgets::ResolvedMenu,
204 pub tooltip: super::widgets::ResolvedTooltip,
206 pub scrollbar: super::widgets::ResolvedScrollbar,
208 pub slider: super::widgets::ResolvedSlider,
210 pub progress_bar: super::widgets::ResolvedProgressBar,
212 pub tab: super::widgets::ResolvedTab,
214 pub sidebar: super::widgets::ResolvedSidebar,
216 pub toolbar: super::widgets::ResolvedToolbar,
218 pub status_bar: super::widgets::ResolvedStatusBar,
220 pub list: super::widgets::ResolvedList,
222 pub popover: super::widgets::ResolvedPopover,
224 pub splitter: super::widgets::ResolvedSplitter,
226 pub separator: super::widgets::ResolvedSeparator,
228 pub switch: super::widgets::ResolvedSwitch,
230 pub dialog: super::widgets::ResolvedDialog,
232 pub spinner: super::widgets::ResolvedSpinner,
234 pub combo_box: super::widgets::ResolvedComboBox,
236 pub segmented_control: super::widgets::ResolvedSegmentedControl,
238 pub card: super::widgets::ResolvedCard,
240 pub expander: super::widgets::ResolvedExpander,
242 pub link: super::widgets::ResolvedLink,
244
245 pub icon_set: String,
247}
248
249#[cfg(test)]
250#[allow(
251 clippy::unwrap_used,
252 clippy::expect_used,
253 clippy::bool_assert_comparison
254)]
255mod tests {
256 use super::*;
257 use crate::Rgba;
258 use crate::model::DialogButtonOrder;
259 use crate::model::widgets::{
260 ResolvedButton, ResolvedCard, ResolvedCheckbox, ResolvedComboBox, ResolvedDialog,
261 ResolvedExpander, ResolvedFontSpec, ResolvedInput, ResolvedLink, ResolvedList,
262 ResolvedMenu, ResolvedPopover, ResolvedProgressBar, ResolvedScrollbar,
263 ResolvedSegmentedControl, ResolvedSeparator, ResolvedSidebar, ResolvedSlider,
264 ResolvedSpinner, ResolvedSplitter, ResolvedStatusBar, ResolvedSwitch, ResolvedTab,
265 ResolvedToolbar, ResolvedTooltip, ResolvedWindow,
266 };
267
268 fn sample_font() -> ResolvedFontSpec {
269 ResolvedFontSpec {
270 family: "Inter".into(),
271 size: 14.0,
272 weight: 400,
273 }
274 }
275
276 fn sample_spacing() -> ResolvedSpacing {
277 ResolvedSpacing {
278 xxs: 2.0,
279 xs: 4.0,
280 s: 6.0,
281 m: 12.0,
282 l: 18.0,
283 xl: 24.0,
284 xxl: 36.0,
285 }
286 }
287
288 fn sample_icon_sizes() -> ResolvedIconSizes {
289 ResolvedIconSizes {
290 toolbar: 24.0,
291 small: 16.0,
292 large: 32.0,
293 dialog: 22.0,
294 panel: 20.0,
295 }
296 }
297
298 fn sample_text_scale_entry() -> ResolvedTextScaleEntry {
299 ResolvedTextScaleEntry {
300 size: 12.0,
301 weight: 400,
302 line_height: 1.4,
303 }
304 }
305
306 fn sample_defaults() -> ResolvedDefaults {
307 let c = Rgba::rgb(128, 128, 128);
308 ResolvedDefaults {
309 font: sample_font(),
310 line_height: 1.4,
311 mono_font: ResolvedFontSpec {
312 family: "JetBrains Mono".into(),
313 size: 12.0,
314 weight: 400,
315 },
316 background: c,
317 foreground: c,
318 accent: c,
319 accent_foreground: c,
320 surface: c,
321 border: c,
322 muted: c,
323 shadow: c,
324 link: c,
325 selection: c,
326 selection_foreground: c,
327 selection_inactive: c,
328 disabled_foreground: c,
329 danger: c,
330 danger_foreground: c,
331 warning: c,
332 warning_foreground: c,
333 success: c,
334 success_foreground: c,
335 info: c,
336 info_foreground: c,
337 radius: 4.0,
338 radius_lg: 8.0,
339 frame_width: 1.0,
340 disabled_opacity: 0.5,
341 border_opacity: 0.15,
342 shadow_enabled: true,
343 focus_ring_color: c,
344 focus_ring_width: 2.0,
345 focus_ring_offset: 1.0,
346 spacing: sample_spacing(),
347 icon_sizes: sample_icon_sizes(),
348 text_scaling_factor: 1.0,
349 reduce_motion: false,
350 high_contrast: false,
351 reduce_transparency: false,
352 }
353 }
354
355 #[test]
358 fn resolved_spacing_has_7_concrete_fields() {
359 let s = sample_spacing();
360 assert_eq!(s.xxs, 2.0);
361 assert_eq!(s.xs, 4.0);
362 assert_eq!(s.s, 6.0);
363 assert_eq!(s.m, 12.0);
364 assert_eq!(s.l, 18.0);
365 assert_eq!(s.xl, 24.0);
366 assert_eq!(s.xxl, 36.0);
367 }
368
369 #[test]
370 fn resolved_spacing_derives_clone_debug_partialeq() {
371 let s = sample_spacing();
372 let s2 = s.clone();
373 assert_eq!(s, s2);
374 let dbg = format!("{s:?}");
375 assert!(dbg.contains("ResolvedSpacing"));
376 }
377
378 #[test]
381 fn resolved_icon_sizes_has_5_concrete_fields() {
382 let i = sample_icon_sizes();
383 assert_eq!(i.toolbar, 24.0);
384 assert_eq!(i.small, 16.0);
385 assert_eq!(i.large, 32.0);
386 assert_eq!(i.dialog, 22.0);
387 assert_eq!(i.panel, 20.0);
388 }
389
390 #[test]
391 fn resolved_icon_sizes_derives_clone_debug_partialeq() {
392 let i = sample_icon_sizes();
393 let i2 = i.clone();
394 assert_eq!(i, i2);
395 let dbg = format!("{i:?}");
396 assert!(dbg.contains("ResolvedIconSizes"));
397 }
398
399 #[test]
402 fn resolved_text_scale_entry_has_3_concrete_fields() {
403 let e = sample_text_scale_entry();
404 assert_eq!(e.size, 12.0);
405 assert_eq!(e.weight, 400);
406 assert_eq!(e.line_height, 1.4);
407 }
408
409 #[test]
410 fn resolved_text_scale_entry_derives_clone_debug_partialeq() {
411 let e = sample_text_scale_entry();
412 let e2 = e.clone();
413 assert_eq!(e, e2);
414 let dbg = format!("{e:?}");
415 assert!(dbg.contains("ResolvedTextScaleEntry"));
416 }
417
418 #[test]
421 fn resolved_text_scale_has_4_entries() {
422 let ts = ResolvedTextScale {
423 caption: ResolvedTextScaleEntry {
424 size: 11.0,
425 weight: 400,
426 line_height: 1.3,
427 },
428 section_heading: ResolvedTextScaleEntry {
429 size: 14.0,
430 weight: 600,
431 line_height: 1.4,
432 },
433 dialog_title: ResolvedTextScaleEntry {
434 size: 16.0,
435 weight: 700,
436 line_height: 1.2,
437 },
438 display: ResolvedTextScaleEntry {
439 size: 24.0,
440 weight: 300,
441 line_height: 1.1,
442 },
443 };
444 assert_eq!(ts.caption.size, 11.0);
445 assert_eq!(ts.section_heading.weight, 600);
446 assert_eq!(ts.dialog_title.size, 16.0);
447 assert_eq!(ts.display.weight, 300);
448 }
449
450 #[test]
451 fn resolved_text_scale_derives_clone_debug_partialeq() {
452 let e = sample_text_scale_entry();
453 let ts = ResolvedTextScale {
454 caption: e.clone(),
455 section_heading: e.clone(),
456 dialog_title: e.clone(),
457 display: e,
458 };
459 let ts2 = ts.clone();
460 assert_eq!(ts, ts2);
461 let dbg = format!("{ts:?}");
462 assert!(dbg.contains("ResolvedTextScale"));
463 }
464
465 #[test]
468 fn resolved_defaults_all_fields_concrete() {
469 let d = sample_defaults();
470 assert_eq!(d.font.family, "Inter");
472 assert_eq!(d.mono_font.family, "JetBrains Mono");
473 assert_eq!(d.line_height, 1.4);
474 assert_eq!(d.background, Rgba::rgb(128, 128, 128));
476 assert_eq!(d.accent, Rgba::rgb(128, 128, 128));
477 assert_eq!(d.radius, 4.0);
479 assert_eq!(d.shadow_enabled, true);
480 assert_eq!(d.focus_ring_width, 2.0);
482 assert_eq!(d.spacing.m, 12.0);
484 assert_eq!(d.icon_sizes.toolbar, 24.0);
485 assert_eq!(d.text_scaling_factor, 1.0);
487 assert_eq!(d.reduce_motion, false);
488 }
489
490 #[test]
491 fn resolved_defaults_derives_clone_debug_partialeq() {
492 let d = sample_defaults();
493 let d2 = d.clone();
494 assert_eq!(d, d2);
495 let dbg = format!("{d:?}");
496 assert!(dbg.contains("ResolvedDefaults"));
497 }
498
499 #[test]
502 fn resolved_theme_construction_with_all_widgets() {
503 let c = Rgba::rgb(100, 100, 100);
504 let f = sample_font();
505 let e = sample_text_scale_entry();
506
507 let theme = ResolvedTheme {
508 defaults: sample_defaults(),
509 text_scale: ResolvedTextScale {
510 caption: e.clone(),
511 section_heading: e.clone(),
512 dialog_title: e.clone(),
513 display: e,
514 },
515 window: ResolvedWindow {
516 background: c,
517 foreground: c,
518 border: c,
519 title_bar_background: c,
520 title_bar_foreground: c,
521 inactive_title_bar_background: c,
522 inactive_title_bar_foreground: c,
523 radius: 4.0,
524 shadow: true,
525 title_bar_font: f.clone(),
526 },
527 button: ResolvedButton {
528 background: c,
529 foreground: c,
530 border: c,
531 primary_bg: c,
532 primary_fg: c,
533 min_width: 64.0,
534 min_height: 28.0,
535 padding_horizontal: 12.0,
536 padding_vertical: 6.0,
537 radius: 4.0,
538 icon_spacing: 6.0,
539 disabled_opacity: 0.5,
540 shadow: false,
541 font: f.clone(),
542 },
543 input: ResolvedInput {
544 background: c,
545 foreground: c,
546 border: c,
547 placeholder: c,
548 caret: c,
549 selection: c,
550 selection_foreground: c,
551 min_height: 28.0,
552 padding_horizontal: 8.0,
553 padding_vertical: 4.0,
554 radius: 4.0,
555 border_width: 1.0,
556 font: f.clone(),
557 },
558 checkbox: ResolvedCheckbox {
559 checked_bg: c,
560 indicator_size: 18.0,
561 spacing: 6.0,
562 radius: 2.0,
563 border_width: 1.0,
564 },
565 menu: ResolvedMenu {
566 background: c,
567 foreground: c,
568 separator: c,
569 item_height: 28.0,
570 padding_horizontal: 8.0,
571 padding_vertical: 4.0,
572 icon_spacing: 6.0,
573 font: f.clone(),
574 },
575 tooltip: ResolvedTooltip {
576 background: c,
577 foreground: c,
578 padding_horizontal: 6.0,
579 padding_vertical: 4.0,
580 max_width: 300.0,
581 radius: 4.0,
582 font: f.clone(),
583 },
584 scrollbar: ResolvedScrollbar {
585 track: c,
586 thumb: c,
587 thumb_hover: c,
588 width: 14.0,
589 min_thumb_height: 20.0,
590 slider_width: 8.0,
591 overlay_mode: false,
592 },
593 slider: ResolvedSlider {
594 fill: c,
595 track: c,
596 thumb: c,
597 track_height: 4.0,
598 thumb_size: 16.0,
599 tick_length: 6.0,
600 },
601 progress_bar: ResolvedProgressBar {
602 fill: c,
603 track: c,
604 height: 6.0,
605 min_width: 100.0,
606 radius: 3.0,
607 },
608 tab: ResolvedTab {
609 background: c,
610 foreground: c,
611 active_background: c,
612 active_foreground: c,
613 bar_background: c,
614 min_width: 60.0,
615 min_height: 32.0,
616 padding_horizontal: 12.0,
617 padding_vertical: 6.0,
618 },
619 sidebar: ResolvedSidebar {
620 background: c,
621 foreground: c,
622 },
623 toolbar: ResolvedToolbar {
624 height: 40.0,
625 item_spacing: 4.0,
626 padding: 4.0,
627 font: f.clone(),
628 },
629 status_bar: ResolvedStatusBar { font: f.clone() },
630 list: ResolvedList {
631 background: c,
632 foreground: c,
633 alternate_row: c,
634 selection: c,
635 selection_foreground: c,
636 header_background: c,
637 header_foreground: c,
638 grid_color: c,
639 item_height: 28.0,
640 padding_horizontal: 8.0,
641 padding_vertical: 4.0,
642 },
643 popover: ResolvedPopover {
644 background: c,
645 foreground: c,
646 border: c,
647 radius: 6.0,
648 },
649 splitter: ResolvedSplitter { width: 4.0 },
650 separator: ResolvedSeparator { color: c },
651 switch: ResolvedSwitch {
652 checked_bg: c,
653 unchecked_bg: c,
654 thumb_bg: c,
655 track_width: 40.0,
656 track_height: 20.0,
657 thumb_size: 14.0,
658 track_radius: 10.0,
659 },
660 dialog: ResolvedDialog {
661 min_width: 320.0,
662 max_width: 600.0,
663 min_height: 200.0,
664 max_height: 800.0,
665 content_padding: 16.0,
666 button_spacing: 8.0,
667 radius: 8.0,
668 icon_size: 22.0,
669 button_order: DialogButtonOrder::TrailingAffirmative,
670 title_font: f.clone(),
671 },
672 spinner: ResolvedSpinner {
673 fill: c,
674 diameter: 24.0,
675 min_size: 16.0,
676 stroke_width: 2.0,
677 },
678 combo_box: ResolvedComboBox {
679 min_height: 28.0,
680 min_width: 80.0,
681 padding_horizontal: 8.0,
682 arrow_size: 12.0,
683 arrow_area_width: 20.0,
684 radius: 4.0,
685 },
686 segmented_control: ResolvedSegmentedControl {
687 segment_height: 28.0,
688 separator_width: 1.0,
689 padding_horizontal: 12.0,
690 radius: 4.0,
691 },
692 card: ResolvedCard {
693 background: c,
694 border: c,
695 radius: 8.0,
696 padding: 12.0,
697 shadow: true,
698 },
699 expander: ResolvedExpander {
700 header_height: 32.0,
701 arrow_size: 12.0,
702 content_padding: 8.0,
703 radius: 4.0,
704 },
705 link: ResolvedLink {
706 color: c,
707 visited: c,
708 background: c,
709 hover_bg: c,
710 underline: true,
711 },
712 icon_set: "freedesktop".into(),
713 };
714
715 assert_eq!(theme.defaults.font.family, "Inter");
717 assert_eq!(theme.window.radius, 4.0);
718 assert_eq!(theme.button.min_height, 28.0);
719 assert_eq!(theme.icon_set, "freedesktop");
720 assert_eq!(theme.text_scale.caption.size, 12.0);
721 }
722
723 #[test]
724 fn resolved_theme_derives_clone_debug_partialeq() {
725 let c = Rgba::rgb(100, 100, 100);
726 let f = sample_font();
727 let e = sample_text_scale_entry();
728
729 let theme = ResolvedTheme {
730 defaults: sample_defaults(),
731 text_scale: ResolvedTextScale {
732 caption: e.clone(),
733 section_heading: e.clone(),
734 dialog_title: e.clone(),
735 display: e,
736 },
737 window: ResolvedWindow {
738 background: c,
739 foreground: c,
740 border: c,
741 title_bar_background: c,
742 title_bar_foreground: c,
743 inactive_title_bar_background: c,
744 inactive_title_bar_foreground: c,
745 radius: 4.0,
746 shadow: true,
747 title_bar_font: f.clone(),
748 },
749 button: ResolvedButton {
750 background: c,
751 foreground: c,
752 border: c,
753 primary_bg: c,
754 primary_fg: c,
755 min_width: 64.0,
756 min_height: 28.0,
757 padding_horizontal: 12.0,
758 padding_vertical: 6.0,
759 radius: 4.0,
760 icon_spacing: 6.0,
761 disabled_opacity: 0.5,
762 shadow: false,
763 font: f.clone(),
764 },
765 input: ResolvedInput {
766 background: c,
767 foreground: c,
768 border: c,
769 placeholder: c,
770 caret: c,
771 selection: c,
772 selection_foreground: c,
773 min_height: 28.0,
774 padding_horizontal: 8.0,
775 padding_vertical: 4.0,
776 radius: 4.0,
777 border_width: 1.0,
778 font: f.clone(),
779 },
780 checkbox: ResolvedCheckbox {
781 checked_bg: c,
782 indicator_size: 18.0,
783 spacing: 6.0,
784 radius: 2.0,
785 border_width: 1.0,
786 },
787 menu: ResolvedMenu {
788 background: c,
789 foreground: c,
790 separator: c,
791 item_height: 28.0,
792 padding_horizontal: 8.0,
793 padding_vertical: 4.0,
794 icon_spacing: 6.0,
795 font: f.clone(),
796 },
797 tooltip: ResolvedTooltip {
798 background: c,
799 foreground: c,
800 padding_horizontal: 6.0,
801 padding_vertical: 4.0,
802 max_width: 300.0,
803 radius: 4.0,
804 font: f.clone(),
805 },
806 scrollbar: ResolvedScrollbar {
807 track: c,
808 thumb: c,
809 thumb_hover: c,
810 width: 14.0,
811 min_thumb_height: 20.0,
812 slider_width: 8.0,
813 overlay_mode: false,
814 },
815 slider: ResolvedSlider {
816 fill: c,
817 track: c,
818 thumb: c,
819 track_height: 4.0,
820 thumb_size: 16.0,
821 tick_length: 6.0,
822 },
823 progress_bar: ResolvedProgressBar {
824 fill: c,
825 track: c,
826 height: 6.0,
827 min_width: 100.0,
828 radius: 3.0,
829 },
830 tab: ResolvedTab {
831 background: c,
832 foreground: c,
833 active_background: c,
834 active_foreground: c,
835 bar_background: c,
836 min_width: 60.0,
837 min_height: 32.0,
838 padding_horizontal: 12.0,
839 padding_vertical: 6.0,
840 },
841 sidebar: ResolvedSidebar {
842 background: c,
843 foreground: c,
844 },
845 toolbar: ResolvedToolbar {
846 height: 40.0,
847 item_spacing: 4.0,
848 padding: 4.0,
849 font: f.clone(),
850 },
851 status_bar: ResolvedStatusBar { font: f.clone() },
852 list: ResolvedList {
853 background: c,
854 foreground: c,
855 alternate_row: c,
856 selection: c,
857 selection_foreground: c,
858 header_background: c,
859 header_foreground: c,
860 grid_color: c,
861 item_height: 28.0,
862 padding_horizontal: 8.0,
863 padding_vertical: 4.0,
864 },
865 popover: ResolvedPopover {
866 background: c,
867 foreground: c,
868 border: c,
869 radius: 6.0,
870 },
871 splitter: ResolvedSplitter { width: 4.0 },
872 separator: ResolvedSeparator { color: c },
873 switch: ResolvedSwitch {
874 checked_bg: c,
875 unchecked_bg: c,
876 thumb_bg: c,
877 track_width: 40.0,
878 track_height: 20.0,
879 thumb_size: 14.0,
880 track_radius: 10.0,
881 },
882 dialog: ResolvedDialog {
883 min_width: 320.0,
884 max_width: 600.0,
885 min_height: 200.0,
886 max_height: 800.0,
887 content_padding: 16.0,
888 button_spacing: 8.0,
889 radius: 8.0,
890 icon_size: 22.0,
891 button_order: DialogButtonOrder::TrailingAffirmative,
892 title_font: f.clone(),
893 },
894 spinner: ResolvedSpinner {
895 fill: c,
896 diameter: 24.0,
897 min_size: 16.0,
898 stroke_width: 2.0,
899 },
900 combo_box: ResolvedComboBox {
901 min_height: 28.0,
902 min_width: 80.0,
903 padding_horizontal: 8.0,
904 arrow_size: 12.0,
905 arrow_area_width: 20.0,
906 radius: 4.0,
907 },
908 segmented_control: ResolvedSegmentedControl {
909 segment_height: 28.0,
910 separator_width: 1.0,
911 padding_horizontal: 12.0,
912 radius: 4.0,
913 },
914 card: ResolvedCard {
915 background: c,
916 border: c,
917 radius: 8.0,
918 padding: 12.0,
919 shadow: true,
920 },
921 expander: ResolvedExpander {
922 header_height: 32.0,
923 arrow_size: 12.0,
924 content_padding: 8.0,
925 radius: 4.0,
926 },
927 link: ResolvedLink {
928 color: c,
929 visited: c,
930 background: c,
931 hover_bg: c,
932 underline: true,
933 },
934 icon_set: "freedesktop".into(),
935 };
936
937 let theme2 = theme.clone();
938 assert_eq!(theme, theme2);
939 let dbg = format!("{theme:?}");
940 assert!(dbg.contains("ResolvedTheme"));
941 }
942}