corundum_framework_gui/theme/
mod.rs1use std::sync::Arc;
13
14mod color;
15mod palette;
16mod picker;
17mod tint;
18mod ui;
19mod visuals;
20
21pub use palette::{Swatch, Theme, Tone};
22pub use picker::TintPicker;
23pub use tint::Tint;
24pub use ui::settings_ui;
25
26#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
29pub enum Contrast {
30 Low,
32 #[default]
34 Medium,
35 High,
37}
38
39#[derive(Clone, PartialEq, Debug, Default)]
41#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
42#[cfg_attr(feature = "serde", serde(default))]
43pub struct ThemeSettings {
44 pub preference: egui::ThemePreference,
46 pub contrast: Contrast,
47 pub accent: Option<Tint>,
49}
50
51impl ThemeSettings {
52 pub fn accent(&self) -> Tint {
54 self.accent.unwrap_or(DEFAULT_ACCENT)
55 }
56}
57
58pub(crate) const DEFAULT_ACCENT: Tint = Tint::Swatch(Swatch::Blue);
60
61#[derive(Clone)]
63struct Installed {
64 light: Arc<Theme>,
65 dark: Arc<Theme>,
66}
67
68fn key() -> egui::Id {
69 egui::Id::unique("corundum_theme")
70}
71
72pub fn install(ctx: &egui::Context, settings: &ThemeSettings) {
75 let build = |dark| Arc::new(Theme::new(dark, settings.contrast, settings.accent()));
76 let installed = Installed {
77 light: build(false),
78 dark: build(true),
79 };
80 ctx.set_visuals_of(egui::Theme::Light, installed.light.visuals());
81 ctx.set_visuals_of(egui::Theme::Dark, installed.dark.visuals());
82 ctx.set_theme(settings.preference);
83 ctx.data_mut(|d| d.insert_temp(key(), installed));
84}
85
86pub fn is_installed(ctx: &egui::Context) -> bool {
88 ctx.data(|d| d.get_temp::<Installed>(key())).is_some()
89}
90
91pub fn current(ctx: &egui::Context) -> Arc<Theme> {
94 let dark = ctx.theme() == egui::Theme::Dark;
95 match ctx.data(|d| d.get_temp::<Installed>(key())) {
96 Some(installed) if dark => installed.dark,
97 Some(installed) => installed.light,
98 None => Arc::new(Theme::new(dark, Contrast::default(), DEFAULT_ACCENT)),
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 fn same(a: &Theme, b: &Theme) -> bool {
107 a == b
108 }
109
110 #[test]
111 fn install_follows_the_egui_theme() {
112 let ctx = egui::Context::default();
113 let settings = ThemeSettings {
114 preference: egui::ThemePreference::Dark,
115 ..Default::default()
116 };
117 install(&ctx, &settings);
118 assert!(current(&ctx).is_dark());
119 assert_eq!(
120 ctx.global_style().visuals.panel_fill,
121 current(&ctx).color(Tone::Base, Swatch::Neutral)
122 );
123 ctx.set_theme(egui::Theme::Light);
124 assert!(!current(&ctx).is_dark());
125 assert_eq!(
126 ctx.global_style().visuals.panel_fill,
127 current(&ctx).color(Tone::Base, Swatch::Neutral)
128 );
129 }
130
131 #[test]
132 fn system_preference_still_gives_a_theme() {
133 let ctx = egui::Context::default();
134 install(&ctx, &ThemeSettings::default());
135 assert_eq!(current(&ctx).is_dark(), ctx.theme() == egui::Theme::Dark);
136 }
137
138 #[test]
139 fn reinstall_replaces_both() {
140 let ctx = egui::Context::default();
141 install(&ctx, &ThemeSettings::default());
142 let accent = Tint::Custom {
143 chroma: 0.1,
144 hue: 30.0,
145 };
146 install(
147 &ctx,
148 &ThemeSettings {
149 contrast: Contrast::High,
150 accent: Some(accent),
151 ..Default::default()
152 },
153 );
154 for (theme, dark) in [(egui::Theme::Dark, true), (egui::Theme::Light, false)] {
155 ctx.set_theme(theme);
156 assert!(same(
157 ¤t(&ctx),
158 &Theme::new(dark, Contrast::High, accent)
159 ));
160 }
161 }
162
163 #[test]
164 fn before_install_current_is_the_default() {
165 let ctx = egui::Context::default();
166 let visuals = ctx.global_style().visuals.clone();
167 assert!(!is_installed(&ctx));
168 let dark = ctx.theme() == egui::Theme::Dark;
169 assert!(same(
170 ¤t(&ctx),
171 &Theme::new(dark, Contrast::Medium, Tint::Swatch(Swatch::Blue))
172 ));
173 assert_eq!(ctx.global_style().visuals, visuals);
174 }
175
176 #[test]
177 fn none_accent_is_blue() {
178 let settings = ThemeSettings::default();
179 assert_eq!(settings.accent, None);
180 assert_eq!(settings.accent(), Tint::Swatch(Swatch::Blue));
181 let ctx = egui::Context::default();
182 install(&ctx, &settings);
183 let t = current(&ctx);
184 for tone in Tone::ALL {
185 assert_eq!(t.color(tone, Swatch::Accent), t.color(tone, Swatch::Blue));
186 }
187 }
188
189 #[cfg(feature = "serde")]
190 #[test]
191 fn settings_with_no_accent_round_trip() {
192 let settings = ThemeSettings::default();
193 let json = serde_json::to_string(&settings).unwrap();
194 let back: ThemeSettings = serde_json::from_str(&json).unwrap();
195 assert_eq!(back, settings);
196 assert_eq!(back.accent(), Tint::Swatch(Swatch::Blue));
197 }
198
199 #[cfg(feature = "serde")]
200 #[test]
201 fn settings_round_trip() {
202 let settings = ThemeSettings {
203 preference: egui::ThemePreference::Light,
204 contrast: Contrast::Low,
205 accent: Some(Tint::Custom {
206 chroma: 0.2,
207 hue: 140.0,
208 }),
209 };
210 let json = serde_json::to_string(&settings).unwrap();
211 assert_eq!(
212 serde_json::from_str::<ThemeSettings>(&json).unwrap(),
213 settings
214 );
215 }
216}