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