egui_charts/theme/
context.rs1use egui::{Context, Ui};
4
5use super::Theme;
6
7pub trait ThemeExt {
23 fn active_theme(&self) -> Theme;
25}
26
27impl ThemeExt for Context {
28 fn active_theme(&self) -> Theme {
29 applied_theme(self).unwrap_or_default()
30 }
31}
32
33impl ThemeExt for Ui {
34 fn active_theme(&self) -> Theme {
35 self.ctx().active_theme()
36 }
37}
38
39fn active_theme_id() -> egui::Id {
40 egui::Id::NULL.with("active_theme")
41}
42
43pub(crate) fn applied_theme(ctx: &Context) -> Option<Theme> {
45 ctx.data(|d| d.get_temp::<Theme>(active_theme_id()))
46}
47
48pub(crate) fn set_active_theme(ctx: &Context, theme: Theme) {
50 ctx.data_mut(|d| d.insert_temp(active_theme_id(), theme));
51}
52
53pub(crate) struct ThemeScope {
55 ctx: Context,
56 previous: Option<Theme>,
57}
58
59impl ThemeScope {
60 pub(crate) fn new(ctx: &Context, theme: &Theme) -> Self {
61 let previous = applied_theme(ctx);
62 set_active_theme(ctx, theme.clone());
63 Self {
64 ctx: ctx.clone(),
65 previous,
66 }
67 }
68}
69
70impl Drop for ThemeScope {
71 fn drop(&mut self) {
72 if let Some(theme) = self.previous.take() {
73 set_active_theme(&self.ctx, theme);
74 } else {
75 self.ctx
76 .data_mut(|data| data.remove::<Theme>(active_theme_id()));
77 }
78 }
79}
80
81#[cfg(feature = "ui")]
83pub(crate) trait ThemeDerived {
84 fn follow(&mut self, old: &Self, new: &Self);
87}
88
89#[cfg(feature = "ui")]
91macro_rules! theme_derived {
92 ($config:ty { $($field:ident),+ $(,)? }) => {
93 impl $crate::theme::ThemeDerived for $config {
94 fn follow(&mut self, old: &Self, new: &Self) {
95 let Self { $($field),+ } = self;
97 $(if *$field == old.$field {
98 *$field = new.$field.clone();
99 })+
100 }
101 }
102 };
103}
104#[cfg(feature = "ui")]
105pub(crate) use theme_derived;
106
107#[cfg(feature = "ui")]
113pub(crate) fn follow_active_theme<C: ThemeDerived>(
114 ctx: &Context,
115 derived: &mut Option<(Theme, C)>,
116 config: &mut C,
117 from_theme: impl FnOnce(&Theme) -> C,
118) {
119 if let Some(theme) = applied_theme(ctx)
120 && let Some((derived_from, old)) = derived
121 && *derived_from != theme
122 {
123 let new = from_theme(&theme);
124 config.follow(old, &new);
125 *derived_from = theme;
126 *old = new;
127 }
128}
129
130#[cfg(all(test, feature = "ui"))]
131mod tests {
132 use super::*;
133 use crate::theme::apply_to_egui;
134 use crate::ui::timeframe_toolbar::{TimeframeToolbar, TimeframeToolbarConfig};
135 use egui::Color32;
136
137 #[test]
138 fn theme_derived_config_follows_the_active_theme_and_keeps_caller_edits() {
139 let ctx = Context::default();
140 let mut themed = TimeframeToolbar::new();
141 themed.config.panel_height = 40.0;
142 themed.config.text_color = Color32::RED;
143 let mut custom =
144 TimeframeToolbar::with_config(TimeframeToolbarConfig::from_theme(&Theme::dark()));
145
146 let mut edited_dark = Theme::dark();
149 edited_dark.ui.border = Color32::GREEN;
150 edited_dark.text.body = 17.0;
151 edited_dark.text.bottom_bar = 19.0;
152 edited_dark.spacing.md = 9.0;
153 edited_dark.radius.sm = 7.0;
154 for (name, theme) in [
155 ("light", Theme::light()),
156 ("dark", Theme::dark()),
157 ("edited dark", edited_dark),
158 ] {
159 ctx.run_ui(Default::default(), |ui| {
160 apply_to_egui(ui.ctx(), &theme);
161 egui::CentralPanel::default().show(ui, |ui| {
162 themed.show(ui);
163 custom.show(ui);
164 });
165 })
166 .drop_without_applying_deltas();
167 assert_eq!(
168 themed.config.panel_height, 40.0,
169 "{name}: edited height replaced"
170 );
171 assert_eq!(
172 themed.config.text_color,
173 Color32::RED,
174 "{name}: edited colour replaced"
175 );
176 assert_eq!(
177 themed.config.border_color, theme.ui.border,
178 "{name}: untouched colour stale"
179 );
180 assert_eq!(
181 themed.config.font_size, theme.text.bottom_bar,
182 "{name}: font stale"
183 );
184 assert_eq!(
185 themed.config.btn_padding_h, theme.spacing.md,
186 "{name}: spacing stale"
187 );
188 assert_eq!(
189 ctx.global_style().text_styles[&egui::TextStyle::Body].size,
190 theme.text.body
191 );
192 assert_eq!(
193 ctx.global_style().visuals.widgets.inactive.corner_radius,
194 egui::CornerRadius::from(theme.radius.sm)
195 );
196 assert_eq!(custom.config.font_size, Theme::dark().text.bottom_bar);
197 assert_eq!(
198 custom.config.border_color,
199 Theme::dark().ui.border,
200 "{name}: with_config replaced"
201 );
202 }
203 }
204}