Skip to main content

corundum_framework_gui/theme/
visuals.rs

1//! [`Theme::visuals`]: the theme as egui's [`Visuals`].
2
3use egui::Visuals;
4
5use super::{Swatch, Theme, Tone};
6
7impl Theme {
8    /// egui's look in this theme's colours. Everything else — corner radii, shadows,
9    /// spacing — is egui's own dark or light.
10    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}