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   /// Button visuals from [`ThemeVisuals`].
221   pub fn button_visuals(&self) -> ButtonVisuals {
222      self.visuals.button_visuals
223   }
224
225   /// Label visuals from [`ThemeVisuals`] (same shape as button visuals).
226   pub fn label_visuals(&self) -> LabelVisuals {
227      self.visuals.label_visuals
228   }
229
230   /// Combo-box visuals from [`ThemeVisuals`].
231   pub fn combo_box_visuals(&self) -> ComboBoxVisuals {
232      self.visuals.combo_box_visuals
233   }
234
235   /// Text-edit visuals from [`ThemeVisuals`].
236   pub fn text_edit_visuals(&self) -> TextEditVisuals {
237      self.visuals.text_edit_visuals
238   }
239
240   fn storage_id() -> Id {
241      Id::new("elements::theme")
242   }
243
244   fn button_visuals_id() -> Id {
245      Id::new("elements::button_visuals")
246   }
247
248   fn label_visuals_id() -> Id {
249      Id::new("elements::label_visuals")
250   }
251
252   fn combo_box_visuals_id() -> Id {
253      Id::new("elements::combo_box_visuals")
254   }
255
256   fn text_edit_visuals_id() -> Id {
257      Id::new("elements::text_edit_visuals")
258   }
259
260   /// Widget visuals stored by [`Theme::install`], if any.
261   ///
262   /// Widgets resolve in this order: `self.visuals` → context → [`egui::Style`].
263   pub fn button_visuals_from_ctx(ctx: &Context) -> Option<ButtonVisuals> {
264      ctx.data(|d| d.get_temp(Self::button_visuals_id()))
265   }
266
267   /// Same as [`Self::button_visuals_from_ctx`] for labels.
268   pub fn label_visuals_from_ctx(ctx: &Context) -> Option<LabelVisuals> {
269      ctx.data(|d| d.get_temp(Self::label_visuals_id()))
270   }
271
272   /// Same as [`Self::button_visuals_from_ctx`] for combo boxes.
273   pub fn combo_box_visuals_from_ctx(ctx: &Context) -> Option<ComboBoxVisuals> {
274      ctx.data(|d| d.get_temp(Self::combo_box_visuals_id()))
275   }
276
277   /// Same as [`Self::button_visuals_from_ctx`] for text edits.
278   pub fn text_edit_visuals_from_ctx(ctx: &Context) -> Option<TextEditVisuals> {
279      ctx.data(|d| d.get_temp(Self::text_edit_visuals_id()))
280   }
281
282   /// Install this theme into the given egui context.
283   ///
284   /// Widgets then pick up their visuals from `ctx` automatically.
285   pub fn install(&mut self, ctx: &Context) {
286      #[cfg(feature = "elegance")]
287      self.install_elegance_theme(ctx);
288
289      let button_visuals = self.button_visuals();
290      let label_visuals = self.label_visuals();
291      let combo_box_visuals = self.combo_box_visuals();
292      let text_edit_visuals = self.text_edit_visuals();
293      let style = self.style();
294      let theme = self.clone();
295
296      ctx.set_global_style(style);
297      ctx.data_mut(|d| d.insert_temp(Self::storage_id(), theme));
298
299      ctx.data_mut(|d| d.insert_temp(Self::button_visuals_id(), button_visuals));
300      ctx.data_mut(|d| d.insert_temp(Self::label_visuals_id(), label_visuals));
301      ctx.data_mut(|d| d.insert_temp(Self::combo_box_visuals_id(), combo_box_visuals));
302      ctx.data_mut(|d| d.insert_temp(Self::text_edit_visuals_id(), text_edit_visuals));
303   }
304
305   #[cfg(feature = "elegance")]
306   /// Convert the current theme into an elegance theme and install it
307   ///
308   /// If the elegance theme is already installed, this is no-op.
309   pub fn install_elegance_theme(&mut self, ctx: &Context) {
310      let key = EleganceThemeKey::from_theme(&self);
311      if self.elegance_key == Some(key) {
312         return;
313      }
314
315      let c = &self.colors;
316      let mut pal = if key.dark {
317         Palette::charcoal()
318      } else {
319         Palette::frost()
320      };
321
322      pal.is_dark = key.dark;
323      pal.bg = c.bg;
324      pal.card = c.widget_bg;
325      pal.input_bg = c.widget_bg;
326      pal.border = c.border;
327      pal.text = c.text;
328      pal.text_muted = c.text_muted;
329      pal.text_faint = c.text_muted;
330      pal.focus = c.accent;
331      pal.blue = c.info;
332      pal.green = c.success;
333      pal.green_hover = c.success;
334      pal.red = c.error;
335      pal.red_hover = c.error;
336      pal.amber = c.warning;
337      pal.amber_hover = c.warning;
338      pal.purple = c.accent;
339      pal.purple_hover = c.accent;
340      pal.success = c.success;
341      pal.danger = c.error;
342      pal.warning = c.warning;
343
344      let elegance_typography = EleganceTypography {
345         body: self.typography.normal,
346         button: self.typography.normal,
347         label: self.typography.normal,
348         small: self.typography.small,
349         heading: self.typography.heading,
350         monospace: self.typography.small,
351      };
352
353      let elegance_theme = EleganceTheme {
354         palette: pal,
355         control_radius: self.corner_radius as f32,
356         card_radius: self.corner_radius as f32,
357         card_padding: self.frame1.inner_margin.top as f32,
358         control_padding_y: self.button_padding.y as f32,
359         control_padding_x: self.button_padding.x as f32,
360         typography: elegance_typography,
361      };
362
363      ctx.data_mut(|d| d.insert_temp(elegance_theme_key(), elegance_theme));
364      self.elegance_key = Some(key);
365   }
366
367   /// Read the current theme from the context
368   /// if it exists, otherwise return the default theme
369   pub fn current(ctx: &Context) -> Theme {
370      ctx.data(|d| {
371         d.get_temp::<Theme>(Self::storage_id())
372            .unwrap_or_else(|| Theme::new(ThemeKind::TokyoNight))
373      })
374   }
375
376   /// Keep derived frame colors in sync with a palette change.
377   ///
378   /// Only updates a color if it still matches the previous palette slot
379   /// (e.g. `frame1.fill == old.widget_bg`). Custom colors and structural
380   /// frame properties (margins, rounding, shadow offsets) are left alone.
381   pub fn remap_derived_frames(&mut self, old: &ThemeColors) {
382      let new = self.colors;
383      if !frame_palette_changed(old, &new) {
384         return;
385      }
386
387      remap_frame(
388         &mut self.window_frame,
389         old.title_bar,
390         new.title_bar,
391         old.border,
392         new.border,
393      );
394      remap_frame(
395         &mut self.frame1,
396         old.widget_bg,
397         new.widget_bg,
398         old.border,
399         new.border,
400      );
401      remap_frame(
402         &mut self.frame2,
403         old.bg,
404         new.bg,
405         old.border,
406         new.border,
407      );
408      remap_frame_visuals(
409         &mut self.visuals.frame1_visuals,
410         old.hover,
411         new.hover,
412         old.widget_bg,
413         new.widget_bg,
414         old.highlight,
415         new.highlight,
416      );
417      remap_frame_visuals(
418         &mut self.visuals.frame2_visuals,
419         old.hover,
420         new.hover,
421         old.bg,
422         new.bg,
423         old.highlight,
424         new.highlight,
425      );
426   }
427}
428
429/// Theme visuals
430#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
431#[derive(Copy, Clone, Debug, PartialEq, Eq)]
432pub struct ThemeVisuals {
433   /// Visuals for [`crate::widgets::Button`].
434   pub button_visuals: ButtonVisuals,
435   /// Visuals for [`crate::widgets::Label`].
436   pub label_visuals: LabelVisuals,
437   /// Visuals for [`crate::widgets::ComboBox`].
438   pub combo_box_visuals: ComboBoxVisuals,
439   /// Visuals for [`crate::widgets::SecureTextEdit`].
440   pub text_edit_visuals: TextEditVisuals,
441   /// Hover/click fills for [`Theme::frame1`].
442   pub frame1_visuals: FrameVisuals,
443   /// Hover/click fills for [`Theme::frame2`].
444   pub frame2_visuals: FrameVisuals,
445}
446
447/// This is the color palette of the theme
448#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
449#[derive(Copy, Clone, Debug, PartialEq, Eq)]
450pub struct ThemeColors {
451   /// The color for the title bar of the app (if using custom window frame)
452   pub title_bar: Color32,
453
454   /// Main BG color of the theme
455   pub bg: Color32,
456
457   /// Widget BG color
458   ///
459   /// This is the color of the widget backgrounds
460   pub widget_bg: Color32,
461
462   /// The color to use when hovering over a widget
463   pub hover: Color32,
464
465   /// Main text color
466   pub text: Color32,
467
468   /// Muted text color
469   ///
470   /// For example a hint inside a text field
471   pub text_muted: Color32,
472
473   /// Selected / emphasis fill. Distinct from [`Self::hover`].
474   pub highlight: Color32,
475
476   /// Border color
477   pub border: Color32,
478
479   /// Accent color
480   pub accent: Color32,
481
482   /// Error color
483   ///
484   /// Can be used to indicate something bad or to highlight a dangerous action
485   pub error: Color32,
486
487   /// Warning color
488   pub warning: Color32,
489
490   /// Success color
491   ///
492   /// Can be used to indicate something good or to highlight a successful action
493   pub success: Color32,
494
495   /// Info color
496   ///
497   /// Can be used for hyperlinks or to highlight something important
498   pub info: Color32,
499}
500
501/// Font sizes in points.
502#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
503#[derive(Clone, Default, Debug, PartialEq)]
504pub struct Typography {
505   /// Tiny captions.
506   pub very_small: f32,
507   /// Secondary / hint text.
508   pub small: f32,
509   /// Body and button text.
510   pub normal: f32,
511   /// Emphasized body.
512   pub large: f32,
513   /// Section titles below a heading.
514   pub very_large: f32,
515   /// Page / modal headings.
516   pub heading: f32,
517}