Skip to main content

egui_elements/
theme.rs

1//! Installed look: palette, typography, frames, and per-widget visuals.
2//!
3//! Construct with [`Theme::new`], then [`Theme::install`] so custom widgets
4//! can read visuals from [`egui::Context`]. [`Theme::current`] reads that
5//! stored copy (or falls back to Tokyo Night).
6//!
7//! Built-in palettes live in [`crate::themes`]. Slot roles are documented
8//! in the crate `docs/theme.md`.
9
10use crate::overlay::OverlayManager;
11use crate::themes::{
12   mclaren_650gts_gt3, reverie, shade_sanctuary, tokyo_night, tokyo_night_light, wasp, wasp_light,
13};
14use crate::utils::*;
15use crate::visuals::*;
16use egui::{Color32, Context, Frame, Id, Style, Vec2};
17
18#[cfg(feature = "elegance")]
19use elegance::{Palette, Theme as EleganceTheme, Typography as EleganceTypography};
20
21/// Temp-data key used to store the mirrored `egui-elegance` theme.
22#[cfg(feature = "elegance")]
23pub fn elegance_theme_key() -> Id {
24   Id::new("elegance::theme")
25}
26
27/// Theme colors to convert [Theme] to [EleganceTheme]
28#[cfg(feature = "elegance")]
29#[derive(Clone, Copy, PartialEq, Eq)]
30pub struct EleganceThemeKey {
31   dark: bool,
32   bg: Color32,
33   widget_bg: Color32,
34   border: Color32,
35   text: Color32,
36   text_muted: Color32,
37   accent: Color32,
38   info: Color32,
39   success: Color32,
40   error: Color32,
41   warning: Color32,
42}
43
44#[cfg(feature = "elegance")]
45impl EleganceThemeKey {
46   fn from_theme(theme: &Theme) -> Self {
47      let c = &theme.colors;
48      Self {
49         dark: theme.dark,
50         bg: c.bg,
51         widget_bg: c.widget_bg,
52         border: c.border,
53         text: c.text,
54         text_muted: c.text_muted,
55         accent: c.accent,
56         info: c.info,
57         success: c.success,
58         error: c.error,
59         warning: c.warning,
60      }
61   }
62}
63
64/// Built-in palette. See each variant for the source look.
65#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub enum ThemeKind {
68   /// Based on <https://github.com/tokyo-night/tokyo-night-vscode-theme>
69   TokyoNight,
70   /// Based on <https://github.com/tokyo-night/tokyo-night-vscode-theme> (light)
71   TokyoNightLight,
72   /// Based on the 2015 McLaren 650S GT3
73   McLaren650Gts,
74   /// Based on <https://github.com/santiyounger/Reverie-Obsidian-Theme>
75   Reverie,
76   /// Based on <https://github.com/Elevict/Shade-Sanctuary>
77   ShadeSanctuary,
78   /// Based on <https://github.com/santiyounger/Wasp-Obsidian-Theme> (dark)
79   Wasp,
80   /// Based on <https://github.com/santiyounger/Wasp-Obsidian-Theme> (light)
81   WaspLight,
82}
83
84impl ThemeKind {
85   /// Human-readable name, used by [`crate::utils::theme_switcher`].
86   pub fn to_str(&self) -> &str {
87      match self {
88         ThemeKind::TokyoNight => "Tokyo Night",
89         ThemeKind::TokyoNightLight => "Tokyo Night Light",
90         ThemeKind::McLaren650Gts => "McLaren 650GTS",
91         ThemeKind::Reverie => "Reverie",
92         ThemeKind::ShadeSanctuary => "Shade Sanctuary",
93         ThemeKind::Wasp => "Wasp",
94         ThemeKind::WaspLight => "Wasp Light",
95      }
96   }
97
98   /// Every built-in kind, in switcher order.
99   pub fn to_vec() -> Vec<Self> {
100      vec![
101         Self::TokyoNight,
102         Self::TokyoNightLight,
103         Self::McLaren650Gts,
104         Self::Reverie,
105         Self::ShadeSanctuary,
106         Self::Wasp,
107         Self::WaspLight,
108      ]
109   }
110}
111
112/// Complete look: colors, typography, frames, and widget visuals.
113///
114/// `Theme` is `Clone`. [`Theme::install`] takes `&mut self` (the elegance
115/// path records a key on `self`). Clone before install if you need to keep
116/// an unmodified copy:
117///
118/// ```no_run
119/// # use egui::Context;
120/// # use egui_elements::theme::{Theme, ThemeKind};
121/// # let ctx = Context::default();
122/// let mut theme = Theme::new(ThemeKind::TokyoNight);
123/// theme.install(&ctx);
124/// ```
125#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
126#[derive(Clone)]
127pub struct Theme {
128   /// True if the theme is dark
129   pub dark: bool,
130   /// Tracks open windows so [`OverlayManager::paint_overlay`] can dim the canvas.
131   #[cfg_attr(feature = "serde", serde(skip))]
132   pub overlay_manager: OverlayManager,
133
134   /// True if a tint is recomended to be applied to images
135   /// to soften the contrast between the image and the background
136   ///
137   /// This is usually true for themes with very dark background
138   pub image_tint_recommended: bool,
139
140   /// Which built-in palette this theme was constructed from.
141   pub kind: ThemeKind,
142   /// Semantic color slots.
143   pub colors: ThemeColors,
144   /// Per-widget chrome (button, label, combo, text edit, frames).
145   pub visuals: ThemeVisuals,
146   /// Font sizes used by this crate's widgets and the demo.
147   pub typography: Typography,
148
149   /// Used for [Frame] not native windows
150   pub window_frame: Frame,
151   /// Base container frame for major UI sections.
152   pub frame1: Frame,
153   /// Frame for nested elements, like individual list items.
154   pub frame2: Frame,
155
156   /// Corner radius for widgets
157   pub corner_radius: u8,
158
159   /// Inner margin for widgets
160   pub inner_margin: i8,
161
162   /// Outer margin for widgets
163   pub outer_margin: i8,
164
165   /// Padding inside buttons
166   ///
167   /// Must use this from [egui::Ui::spacing_mut] to change the padding
168   pub button_padding: Vec2,
169
170   #[cfg(feature = "elegance")]
171   #[cfg_attr(feature = "serde", serde(skip))]
172   /// Snapshot used to skip reinstalling an unchanged elegance theme.
173   pub elegance_key: Option<EleganceThemeKey>,
174}
175
176impl PartialEq for Theme {
177   fn eq(&self, other: &Self) -> bool {
178      self.dark == other.dark
179         && self.kind == other.kind
180         && self.colors == other.colors
181         && self.typography == other.typography
182         && self.window_frame == other.window_frame
183         && self.frame1 == other.frame1
184         && self.frame2 == other.frame2
185         && self.visuals == other.visuals
186   }
187}
188
189impl Eq for Theme {}
190
191impl Theme {
192   /// Construct a built-in theme.
193   pub fn new(kind: ThemeKind) -> Self {
194      let theme = match kind {
195         ThemeKind::TokyoNight => tokyo_night::theme(),
196         ThemeKind::TokyoNightLight => tokyo_night_light::theme(),
197         ThemeKind::McLaren650Gts => mclaren_650gts_gt3::theme(),
198         ThemeKind::Reverie => reverie::theme(),
199         ThemeKind::ShadeSanctuary => shade_sanctuary::theme(),
200         ThemeKind::Wasp => wasp::theme(),
201         ThemeKind::WaspLight => wasp_light::theme(),
202      };
203
204      theme
205   }
206
207   /// Stock [`egui::Style`] for this kind (widgets, selection, window fill).
208   pub fn style(&self) -> Style {
209      match self.kind {
210         ThemeKind::TokyoNight => tokyo_night::style(),
211         ThemeKind::TokyoNightLight => tokyo_night_light::style(),
212         ThemeKind::McLaren650Gts => mclaren_650gts_gt3::style(),
213         ThemeKind::Reverie => reverie::style(),
214         ThemeKind::ShadeSanctuary => shade_sanctuary::style(),
215         ThemeKind::Wasp => wasp::style(),
216         ThemeKind::WaspLight => wasp_light::style(),
217      }
218   }
219
220   /// Frame 1 visuals from [`ThemeVisuals`].
221   pub fn frame1_visuals(&self) -> FrameVisuals {
222      self.visuals.frame1_visuals
223   }
224
225   /// Frame 2 visuals from [`ThemeVisuals`].
226   pub fn frame2_visuals(&self) -> FrameVisuals {
227      self.visuals.frame2_visuals
228   }
229
230   /// Button visuals from [`ThemeVisuals`].
231   pub fn button_visuals(&self) -> ButtonVisuals {
232      self.visuals.button_visuals
233   }
234
235   /// Label visuals from [`ThemeVisuals`] (same shape as button visuals).
236   pub fn label_visuals(&self) -> LabelVisuals {
237      self.visuals.label_visuals
238   }
239
240   /// Combo-box visuals from [`ThemeVisuals`].
241   pub fn combo_box_visuals(&self) -> ComboBoxVisuals {
242      self.visuals.combo_box_visuals
243   }
244
245   /// Text-edit visuals from [`ThemeVisuals`].
246   pub fn text_edit_visuals(&self) -> TextEditVisuals {
247      self.visuals.text_edit_visuals
248   }
249
250   fn storage_id() -> Id {
251      Id::new("elements::theme")
252   }
253
254   fn button_visuals_id() -> Id {
255      Id::new("elements::button_visuals")
256   }
257
258   fn label_visuals_id() -> Id {
259      Id::new("elements::label_visuals")
260   }
261
262   fn combo_box_visuals_id() -> Id {
263      Id::new("elements::combo_box_visuals")
264   }
265
266   fn text_edit_visuals_id() -> Id {
267      Id::new("elements::text_edit_visuals")
268   }
269
270   /// Widget visuals stored by [`Theme::install`], if any.
271   ///
272   /// Widgets resolve in this order: `self.visuals` → context → [`egui::Style`].
273   pub fn button_visuals_from_ctx(ctx: &Context) -> Option<ButtonVisuals> {
274      ctx.data(|d| d.get_temp(Self::button_visuals_id()))
275   }
276
277   /// Same as [`Self::button_visuals_from_ctx`] for labels.
278   pub fn label_visuals_from_ctx(ctx: &Context) -> Option<LabelVisuals> {
279      ctx.data(|d| d.get_temp(Self::label_visuals_id()))
280   }
281
282   /// Same as [`Self::button_visuals_from_ctx`] for combo boxes.
283   pub fn combo_box_visuals_from_ctx(ctx: &Context) -> Option<ComboBoxVisuals> {
284      ctx.data(|d| d.get_temp(Self::combo_box_visuals_id()))
285   }
286
287   /// Same as [`Self::button_visuals_from_ctx`] for text edits.
288   pub fn text_edit_visuals_from_ctx(ctx: &Context) -> Option<TextEditVisuals> {
289      ctx.data(|d| d.get_temp(Self::text_edit_visuals_id()))
290   }
291
292   /// Install this theme into the given egui context.
293   ///
294   /// Widgets then pick up their visuals from `ctx` automatically.
295   pub fn install(&mut self, ctx: &Context) {
296      #[cfg(feature = "elegance")]
297      self.install_elegance_theme(ctx);
298
299      let button_visuals = self.button_visuals();
300      let label_visuals = self.label_visuals();
301      let combo_box_visuals = self.combo_box_visuals();
302      let text_edit_visuals = self.text_edit_visuals();
303      let style = self.style();
304      let theme = self.clone();
305
306      ctx.set_global_style(style);
307      ctx.data_mut(|d| d.insert_temp(Self::storage_id(), theme));
308
309      ctx.data_mut(|d| d.insert_temp(Self::button_visuals_id(), button_visuals));
310      ctx.data_mut(|d| d.insert_temp(Self::label_visuals_id(), label_visuals));
311      ctx.data_mut(|d| d.insert_temp(Self::combo_box_visuals_id(), combo_box_visuals));
312      ctx.data_mut(|d| d.insert_temp(Self::text_edit_visuals_id(), text_edit_visuals));
313   }
314
315   #[cfg(feature = "elegance")]
316   /// Convert the current theme into an elegance theme and install it
317   ///
318   /// If the elegance theme is already installed, this is no-op.
319   pub fn install_elegance_theme(&mut self, ctx: &Context) {
320      let key = EleganceThemeKey::from_theme(&self);
321      if self.elegance_key == Some(key) {
322         return;
323      }
324
325      let c = &self.colors;
326      let mut pal = if key.dark {
327         Palette::charcoal()
328      } else {
329         Palette::frost()
330      };
331
332      pal.is_dark = key.dark;
333      pal.bg = c.bg;
334      pal.card = c.widget_bg;
335      pal.input_bg = c.widget_bg;
336      pal.border = c.border;
337      pal.text = c.text;
338      pal.text_muted = c.text_muted;
339      pal.text_faint = c.text_muted;
340      pal.focus = c.accent;
341      pal.blue = c.info;
342      pal.green = c.success;
343      pal.green_hover = c.success;
344      pal.red = c.error;
345      pal.red_hover = c.error;
346      pal.amber = c.warning;
347      pal.amber_hover = c.warning;
348      pal.purple = c.accent;
349      pal.purple_hover = c.accent;
350      pal.success = c.success;
351      pal.danger = c.error;
352      pal.warning = c.warning;
353
354      let elegance_typography = EleganceTypography {
355         body: self.typography.normal,
356         button: self.typography.normal,
357         label: self.typography.normal,
358         small: self.typography.small,
359         heading: self.typography.heading,
360         monospace: self.typography.small,
361      };
362
363      let elegance_theme = EleganceTheme {
364         palette: pal,
365         control_radius: self.corner_radius as f32,
366         card_radius: self.corner_radius as f32,
367         card_padding: self.frame1.inner_margin.top as f32,
368         control_padding_y: self.button_padding.y as f32,
369         control_padding_x: self.button_padding.x as f32,
370         typography: elegance_typography,
371      };
372
373      ctx.data_mut(|d| d.insert_temp(elegance_theme_key(), elegance_theme));
374      self.elegance_key = Some(key);
375   }
376
377   /// Read the current theme from the context
378   /// if it exists, otherwise return the default theme
379   pub fn current(ctx: &Context) -> Theme {
380      ctx.data(|d| {
381         d.get_temp::<Theme>(Self::storage_id())
382            .unwrap_or_else(|| Theme::new(ThemeKind::TokyoNight))
383      })
384   }
385
386   /// Keep derived frame colors in sync with a palette change.
387   ///
388   /// Only updates a color if it still matches the previous palette slot
389   /// (e.g. `frame1.fill == old.widget_bg`). Custom colors and structural
390   /// frame properties (margins, rounding, shadow offsets) are left alone.
391   pub fn remap_derived_frames(&mut self, old: &ThemeColors) {
392      let new = self.colors;
393      if !frame_palette_changed(old, &new) {
394         return;
395      }
396
397      remap_frame(
398         &mut self.window_frame,
399         old.title_bar,
400         new.title_bar,
401         old.border,
402         new.border,
403      );
404      remap_frame(
405         &mut self.frame1,
406         old.widget_bg,
407         new.widget_bg,
408         old.border,
409         new.border,
410      );
411      remap_frame(
412         &mut self.frame2,
413         old.bg,
414         new.bg,
415         old.border,
416         new.border,
417      );
418      remap_frame_visuals(
419         &mut self.visuals.frame1_visuals,
420         old.hover,
421         new.hover,
422         old.widget_bg,
423         new.widget_bg,
424         old.highlight,
425         new.highlight,
426      );
427      remap_frame_visuals(
428         &mut self.visuals.frame2_visuals,
429         old.hover,
430         new.hover,
431         old.bg,
432         new.bg,
433         old.highlight,
434         new.highlight,
435      );
436   }
437}
438
439/// Theme visuals
440#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
441#[derive(Copy, Clone, Debug, PartialEq, Eq)]
442pub struct ThemeVisuals {
443   /// Visuals for [`crate::widgets::Button`].
444   pub button_visuals: ButtonVisuals,
445   /// Visuals for [`crate::widgets::Label`].
446   pub label_visuals: LabelVisuals,
447   /// Visuals for [`crate::widgets::ComboBox`].
448   pub combo_box_visuals: ComboBoxVisuals,
449   /// Visuals for [`crate::widgets::SecureTextEdit`].
450   pub text_edit_visuals: TextEditVisuals,
451   /// Hover/click fills for [`Theme::frame1`].
452   pub frame1_visuals: FrameVisuals,
453   /// Hover/click fills for [`Theme::frame2`].
454   pub frame2_visuals: FrameVisuals,
455}
456
457/// This is the color palette of the theme
458#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
459#[derive(Copy, Clone, Debug, PartialEq, Eq)]
460pub struct ThemeColors {
461   /// The color for the title bar of the app (if using custom window frame)
462   pub title_bar: Color32,
463
464   /// Main BG color of the theme
465   pub bg: Color32,
466
467   /// Widget BG color
468   ///
469   /// This is the color of the widget backgrounds
470   pub widget_bg: Color32,
471
472   /// The color to use when hovering over a widget
473   pub hover: Color32,
474
475   /// Main text color
476   pub text: Color32,
477
478   /// Muted text color
479   ///
480   /// For example a hint inside a text field
481   pub text_muted: Color32,
482
483   /// Selected / emphasis fill. Distinct from [`Self::hover`].
484   pub highlight: Color32,
485
486   /// Border color
487   pub border: Color32,
488
489   /// Accent color
490   pub accent: Color32,
491
492   /// Error color
493   ///
494   /// Can be used to indicate something bad or to highlight a dangerous action
495   pub error: Color32,
496
497   /// Warning color
498   pub warning: Color32,
499
500   /// Success color
501   ///
502   /// Can be used to indicate something good or to highlight a successful action
503   pub success: Color32,
504
505   /// Info color
506   ///
507   /// Can be used for hyperlinks or to highlight something important
508   pub info: Color32,
509}
510
511/// Font sizes in points.
512#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
513#[derive(Clone, Default, Debug, PartialEq)]
514pub struct Typography {
515   /// Tiny captions.
516   pub very_small: f32,
517   /// Secondary / hint text.
518   pub small: f32,
519   /// Body and button text.
520   pub normal: f32,
521   /// Emphasized body.
522   pub large: f32,
523   /// Section titles below a heading.
524   pub very_large: f32,
525   /// Page / modal headings.
526   pub heading: f32,
527}