corundum_framework_gui/theme/
visuals.rs1use egui::Visuals;
4
5use super::{Swatch, Theme, Tone};
6
7impl Theme {
8 pub fn visuals(&self) -> Visuals {
11 let mut v = if self.is_dark() {
12 Visuals::dark()
13 } else {
14 Visuals::light()
15 };
16 let grey = |tone| self.color(tone, Swatch::Neutral);
17 let base = grey(Tone::Base);
18 let border = grey(Tone::Border);
19 let text = grey(Tone::Text);
20 let strong = grey(Tone::Strong);
21
22 v.dark_mode = self.is_dark();
23 v.panel_fill = base;
24 v.window_fill = base;
25 v.window_stroke.color = border;
26 v.extreme_bg_color = grey(Tone::Sunken);
27 v.faint_bg_color = grey(Tone::Raised);
28 v.code_bg_color = grey(Tone::Raised);
29 v.weak_text_color = Some(grey(Tone::Muted));
30 v.hyperlink_color = self.text(Swatch::Accent);
31 v.warn_fg_color = self.text(Swatch::WARNING);
32 v.error_fg_color = self.text(Swatch::DANGER);
33 v.selection.bg_fill = self.color(Tone::Pressed, Swatch::Accent);
34 v.selection.stroke.color = self.text(Swatch::Accent);
35
36 let w = &mut v.widgets;
37 w.noninteractive.bg_fill = base;
38 w.noninteractive.weak_bg_fill = base;
39 w.noninteractive.bg_stroke.color = border;
40 w.noninteractive.fg_stroke.color = text;
41 for (widget, fill, fg) in [
42 (&mut w.inactive, Tone::Raised, text),
43 (&mut w.hovered, Tone::Hover, strong),
44 (&mut w.active, Tone::Pressed, strong),
45 (&mut w.open, Tone::Hover, strong),
46 ] {
47 widget.bg_fill = grey(fill);
48 widget.weak_bg_fill = grey(fill);
49 widget.fg_stroke.color = fg;
50 }
51 w.hovered.bg_stroke.color = border;
52 v
53 }
54}
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59 use crate::theme::color::contrast_ratio;
60 use crate::theme::{Contrast, Tint};
61
62 fn both() -> [Theme; 2] {
63 [true, false].map(|dark| Theme::new(dark, Contrast::Medium, Tint::Swatch(Swatch::Blue)))
64 }
65
66 #[test]
67 fn visuals_take_the_theme_colours() {
68 for t in both() {
69 let v = t.visuals();
70 assert_eq!(v.dark_mode, t.is_dark());
71 assert_eq!(v.panel_fill, t.color(Tone::Base, Swatch::Neutral));
72 assert_eq!(v.extreme_bg_color, t.color(Tone::Sunken, Swatch::Neutral));
73 assert_eq!(
74 v.widgets.inactive.weak_bg_fill,
75 t.color(Tone::Raised, Swatch::Neutral)
76 );
77 assert_eq!(v.selection.bg_fill, t.color(Tone::Pressed, Swatch::Accent));
78 assert_eq!(v.hyperlink_color, t.text(Swatch::Accent));
79 assert_eq!(v.error_fg_color, t.text(Swatch::DANGER));
80 assert_eq!(v.warn_fg_color, t.text(Swatch::WARNING));
81 assert_eq!(
82 v.weak_text_color,
83 Some(t.color(Tone::Muted, Swatch::Neutral))
84 );
85 }
86 }
87
88 #[test]
89 fn non_colour_fields_are_eguis() {
90 for t in both() {
91 let v = t.visuals();
92 let egui = if t.is_dark() {
93 Visuals::dark()
94 } else {
95 Visuals::light()
96 };
97 assert_eq!(v.window_corner_radius, egui.window_corner_radius);
98 assert_eq!(v.window_shadow.offset, egui.window_shadow.offset);
99 }
100 }
101
102 #[test]
103 fn selected_text_is_readable() {
104 for contrast in [Contrast::Low, Contrast::Medium, Contrast::High] {
105 let target = match contrast {
106 Contrast::Low => 3.0,
107 Contrast::Medium => 4.5,
108 Contrast::High => 7.0,
109 };
110 let accents =
111 std::iter::once(Tint::Swatch(Swatch::Blue)).chain((0..360).step_by(30).map(|h| {
112 Tint::Custom {
113 chroma: Tint::MAX_CHROMA,
114 hue: h as f32,
115 }
116 }));
117 for accent in accents {
118 for dark in [true, false] {
119 let v = Theme::new(dark, contrast, accent).visuals();
120 let r = contrast_ratio(v.selection.stroke.color, v.selection.bg_fill);
121 assert!(r >= target, "dark {dark} {contrast:?} {accent:?}: {r}");
122 }
123 }
124 }
125 }
126}