Skip to main content

zeus_theme/
lib.rs

1use egui::{Color32, Context, Frame, Id, LayerId, Order, Rect, Style};
2use std::sync::{Arc, RwLock};
3
4const PANIC_MSG: &str = "Custom theme not supported, use Theme::from_custom() instead";
5
6pub mod editor;
7pub mod hsla;
8pub mod themes;
9pub mod utils;
10pub mod visuals;
11pub mod window;
12
13pub use editor::ThemeEditor;
14use themes::*;
15pub use visuals::*;
16
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum ThemeKind {
20   Dark,
21
22   TokyoNight,
23
24   /// WIP
25   // Light,
26
27   /// A custom theme
28   Custom,
29}
30
31impl ThemeKind {
32   pub fn to_str(&self) -> &str {
33      match self {
34         ThemeKind::Dark => "Dark",
35         ThemeKind::TokyoNight => "Tokyo Night",
36         // ThemeKind::Light => "Light",
37         ThemeKind::Custom => "Custom",
38      }
39   }
40
41   pub fn to_vec() -> Vec<Self> {
42      vec![Self::Dark, Self::TokyoNight]
43   }
44}
45
46#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
47#[derive(Debug, Clone)]
48pub struct Theme {
49   /// True if the theme is dark
50   pub dark_mode: bool,
51   #[cfg_attr(feature = "serde", serde(skip))]
52   pub overlay_manager: OverlayManager,
53
54   /// True if a tint is recomended to be applied to images
55   /// to soften the contrast between the image and the background
56   ///
57   /// This is usually true for themes with very dark background
58   pub image_tint_recommended: bool,
59   pub kind: ThemeKind,
60   pub style: Style,
61   pub colors: ThemeColors,
62   pub text_sizes: TextSizes,
63   /// Used for [window::window_frame]
64   pub window_frame: Frame,
65   /// Base container frame for major UI sections.
66   pub frame1: Frame,
67   /// Frame for nested elements, like individual list items.
68   pub frame2: Frame,
69
70   pub frame1_visuals: FrameVisuals,
71   pub frame2_visuals: FrameVisuals,
72}
73
74impl PartialEq for Theme {
75   fn eq(&self, other: &Self) -> bool {
76      self.dark_mode == other.dark_mode
77         && self.kind == other.kind
78         && self.style == other.style
79         && self.colors == other.colors
80         && self.text_sizes == other.text_sizes
81         && self.window_frame == other.window_frame
82         && self.frame1 == other.frame1
83         && self.frame2 == other.frame2
84         && self.frame1_visuals == other.frame1_visuals
85         && self.frame2_visuals == other.frame2_visuals
86   }
87}
88
89impl Eq for Theme {}
90
91impl Theme {
92   /// Panics if the kind is [ThemeKind::Custom]
93   ///
94   /// Use [Theme::from_custom()] instead
95   pub fn new(kind: ThemeKind) -> Self {
96      let theme = match kind {
97         ThemeKind::Dark => dark::theme(),
98         ThemeKind::TokyoNight => tokyo_night::theme(),
99         // ThemeKind::Light => light::theme(),
100         ThemeKind::Custom => panic!("{}", PANIC_MSG),
101      };
102
103      theme
104   }
105
106   pub fn set_window_frame_colors(&mut self) {
107      match self.kind {
108         ThemeKind::Dark => self.window_frame = dark::window_frame(&self.colors),
109         ThemeKind::TokyoNight => self.window_frame = tokyo_night::window_frame(&self.colors),
110         // ThemeKind::Light => self.window_frame = light::window_frame(&self.colors),
111         ThemeKind::Custom => panic!("{}", PANIC_MSG),
112      }
113   }
114
115   pub fn set_frame1_colors(&mut self) {
116      match self.kind {
117         ThemeKind::Dark => self.frame1 = dark::frame1(&self.colors),
118         ThemeKind::TokyoNight => self.frame1 = tokyo_night::frame1(&self.colors),
119         // ThemeKind::Light => self.frame1 = light::frame1(&self.colors),
120         ThemeKind::Custom => panic!("{}", PANIC_MSG),
121      }
122   }
123
124   pub fn set_frame2_colors(&mut self) {
125      match self.kind {
126         ThemeKind::Dark => self.frame2 = dark::frame2(&self.colors),
127         ThemeKind::TokyoNight => self.frame2 = tokyo_night::frame2(&self.colors),
128         // ThemeKind::Light => self.frame2 = light::frame2(&self.colors),
129         ThemeKind::Custom => panic!("{}", PANIC_MSG),
130      }
131   }
132
133   pub fn button_visuals(&self) -> ButtonVisuals {
134      match self.kind {
135         ThemeKind::Dark => self.colors.button_visuals,
136         ThemeKind::TokyoNight => self.colors.button_visuals,
137         // ThemeKind::Light => self.colors.button_visuals,
138         ThemeKind::Custom => panic!("{}", PANIC_MSG),
139      }
140   }
141
142   pub fn label_visuals(&self) -> LabelVisuals {
143      match self.kind {
144         ThemeKind::Dark => self.colors.label_visuals,
145         ThemeKind::TokyoNight => self.colors.label_visuals,
146         // ThemeKind::Light => self.colors.label_visuals,
147         ThemeKind::Custom => panic!("{}", PANIC_MSG),
148      }
149   }
150
151   pub fn combo_box_visuals(&self) -> ComboBoxVisuals {
152      match self.kind {
153         ThemeKind::Dark => self.colors.combo_box_visuals,
154         ThemeKind::TokyoNight => self.colors.combo_box_visuals,
155         // ThemeKind::Light => self.colors.combo_box_visuals,
156         ThemeKind::Custom => panic!("{}", PANIC_MSG),
157      }
158   }
159
160   pub fn text_edit_visuals(&self) -> TextEditVisuals {
161      match self.kind {
162         ThemeKind::Dark => self.colors.text_edit_visuals,
163         ThemeKind::TokyoNight => self.colors.text_edit_visuals,
164         // ThemeKind::Light => self.colors.text_edit_visuals,
165         ThemeKind::Custom => panic!("{}", PANIC_MSG),
166      }
167   }
168
169   /// Install this theme into the given egui context
170   pub fn install(self, ctx: &Context) {
171      let unchanged =
172         ctx.data(|d| d.get_temp::<Theme>(Self::storage_id()).is_some_and(|t| t == self));
173
174      if unchanged {
175         return;
176      }
177
178      ctx.set_global_style(self.style.clone());
179      ctx.data_mut(|d| d.insert_temp(Self::storage_id(), self));
180   }
181
182   /// Read the current theme from the context
183   /// if it exists, otherwise return the default theme
184   pub fn current(ctx: &Context) -> Theme {
185      ctx.data(|d| {
186         d.get_temp::<Theme>(Self::storage_id())
187            .unwrap_or_else(|| Theme::new(ThemeKind::TokyoNight))
188      })
189   }
190
191   fn storage_id() -> Id {
192      Id::new("zeus::theme")
193   }
194}
195
196/// This is the color palette of the theme
197#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
198#[derive(Copy, Clone, Debug, PartialEq, Eq)]
199pub struct ThemeColors {
200   pub button_visuals: ButtonVisuals,
201
202   pub label_visuals: LabelVisuals,
203
204   pub combo_box_visuals: ComboBoxVisuals,
205
206   pub text_edit_visuals: TextEditVisuals,
207
208   /// The color for the title bar of the app (if using custom window frame)
209   pub title_bar: Color32,
210
211   /// Main BG color of the theme
212   pub bg: Color32,
213
214   /// Widget BG color
215   ///
216   /// This is the color of the widget backgrounds
217   pub widget_bg: Color32,
218
219   /// The color to use when hovering over a widget
220   pub hover: Color32,
221
222   /// Main text color
223   pub text: Color32,
224
225   /// Muted text color
226   ///
227   /// For example a hint inside a text field
228   pub text_muted: Color32,
229
230   /// Highlight color
231   pub highlight: Color32,
232
233   /// Border color
234   pub border: Color32,
235
236   /// Accent color
237   pub accent: Color32,
238
239   /// Error color
240   ///
241   /// Can be used to indicate something bad or to highlight a dangerous action
242   pub error: Color32,
243
244   /// Warning color
245   pub warning: Color32,
246
247   /// Success color
248   ///
249   /// Can be used to indicate something good or to highlight a successful action
250   pub success: Color32,
251
252   /// Info color
253   ///
254   /// Can be used for hyperlinks or to highlight something important
255   pub info: Color32,
256}
257
258#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
259#[derive(Clone, Default, Debug, PartialEq)]
260pub struct TextSizes {
261   pub very_small: f32,
262   pub small: f32,
263   pub normal: f32,
264   pub large: f32,
265   pub very_large: f32,
266   pub heading: f32,
267}
268
269impl TextSizes {
270   pub fn new(
271      very_small: f32,
272      small: f32,
273      normal: f32,
274      large: f32,
275      very_large: f32,
276      heading: f32,
277   ) -> Self {
278      Self {
279         very_small,
280         small,
281         normal,
282         large,
283         very_large,
284         heading,
285      }
286   }
287}
288
289#[derive(Clone, Debug, Default)]
290pub struct OverlayManager(Arc<RwLock<OverlayCounter>>);
291
292impl OverlayManager {
293   pub fn new() -> Self {
294      Self(Arc::new(RwLock::new(OverlayCounter::new())))
295   }
296
297   pub fn tint_0(&self) -> Color32 {
298      Color32::from_black_alpha(40)
299   }
300
301   pub fn tint_1(&self) -> Color32 {
302      Color32::from_black_alpha(60)
303   }
304
305   pub fn tint_2(&self) -> Color32 {
306      Color32::from_black_alpha(80)
307   }
308
309   pub fn tint_3(&self) -> Color32 {
310      Color32::from_black_alpha(100)
311   }
312
313   pub fn counter(&self) -> u8 {
314      self.0.read().unwrap().counter()
315   }
316
317   pub fn order(&self) -> Order {
318      self.0.read().unwrap().order()
319   }
320
321   pub fn paint_background(&self) {
322      self.0.write().unwrap().paint_background()
323   }
324
325   pub fn paint_middle(&self) {
326      self.0.write().unwrap().paint_middle()
327   }
328
329   pub fn paint_foreground(&self) {
330      self.0.write().unwrap().paint_foreground()
331   }
332
333   pub fn paint_tooltip(&self) {
334      self.0.write().unwrap().paint_tooltip()
335   }
336
337   pub fn paint_debug(&self) {
338      self.0.write().unwrap().paint_debug()
339   }
340
341   /// Call this when you open a window
342   pub fn window_opened(&self) {
343      self.0.write().unwrap().window_opened();
344   }
345
346   /// Call this when you close a window
347   pub fn window_closed(&self) {
348      self.0.write().unwrap().window_closed();
349   }
350
351   pub fn recommended_order(&self) -> Order {
352      self.0.read().unwrap().recommended_order()
353   }
354
355   pub fn calculate_alpha(&self) -> u8 {
356      self.0.read().unwrap().calculate_alpha()
357   }
358
359   /// Returns the tint color based on the counter
360   pub fn overlay_tint(&self) -> Color32 {
361      self.0.read().unwrap().overlay_tint()
362   }
363
364   /// Paints a full-screen darkening overlay up to Foreground layer if needed
365   ///
366   /// If `recommend_order` is true, it will choose an order based on the counter
367   pub fn paint_overlay(&self, ctx: &Context, recommend_order: bool) {
368      self.0.read().unwrap().paint_overlay(ctx, recommend_order);
369   }
370
371   /// Paints an overlay at a specific screen position
372   pub fn paint_overlay_at(&self, ctx: &Context, rect: Rect, order: Order, id: Id, tint: Color32) {
373      self.0.read().unwrap().paint_overlay_at(ctx, rect, order, id, tint);
374   }
375}
376
377#[derive(Clone, Debug)]
378struct OverlayCounter {
379   counter: u8,
380   order: Order,
381}
382
383impl Default for OverlayCounter {
384   fn default() -> Self {
385      Self::new()
386   }
387}
388
389impl OverlayCounter {
390   pub fn new() -> Self {
391      Self {
392         counter: 0,
393         order: Order::Background,
394      }
395   }
396
397   pub fn counter(&self) -> u8 {
398      self.counter
399   }
400
401   pub fn order(&self) -> Order {
402      self.order
403   }
404
405   fn paint_background(&mut self) {
406      self.order = Order::Background;
407   }
408
409   fn paint_middle(&mut self) {
410      self.order = Order::Middle;
411   }
412
413   fn paint_foreground(&mut self) {
414      self.order = Order::Foreground;
415   }
416
417   fn paint_tooltip(&mut self) {
418      self.order = Order::Tooltip;
419   }
420
421   fn paint_debug(&mut self) {
422      self.order = Order::Debug;
423   }
424
425   fn window_opened(&mut self) {
426      self.counter += 1;
427   }
428
429   fn window_closed(&mut self) {
430      if self.counter > 0 {
431         self.counter -= 1;
432      }
433   }
434
435   fn calculate_alpha(&self) -> u8 {
436      let counter = self.counter;
437
438      if counter == 0 {
439         return 0;
440      }
441
442      let mut a = 60;
443      for _ in 1..counter {
444         a += 20;
445      }
446
447      a
448   }
449
450   fn overlay_tint(&self) -> Color32 {
451      let counter = self.counter();
452
453      if counter == 1 {
454         return Color32::from_black_alpha(60);
455      }
456
457      let alpha = self.calculate_alpha();
458      Color32::from_black_alpha(alpha)
459   }
460
461   fn recommended_order(&self) -> Order {
462      if self.counter() == 1 {
463         Order::Middle
464      } else if self.counter() == 2 {
465         Order::Foreground
466      } else {
467         Order::Tooltip
468      }
469   }
470
471   fn paint_overlay(&self, ctx: &Context, recommend_order: bool) {
472      let counter = self.counter();
473      if counter == 0 {
474         return;
475      }
476
477      let order = if recommend_order {
478         if counter == 1 {
479            Order::Middle
480         } else if counter == 2 {
481            Order::Foreground
482         } else {
483            Order::Tooltip
484         }
485      } else {
486         self.order()
487      };
488
489      let layer_id = LayerId::new(order, Id::new("darkening_overlay"));
490
491      let painter = ctx.layer_painter(layer_id);
492      painter.rect_filled(ctx.content_rect(), 0.0, self.overlay_tint());
493   }
494
495   pub fn paint_overlay_at(&self, ctx: &Context, rect: Rect, order: Order, id: Id, tint: Color32) {
496      let layer_id = LayerId::new(order, id);
497
498      let painter = ctx.layer_painter(layer_id);
499      painter.rect_filled(rect, 0.0, tint);
500   }
501}