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,
26
27 Custom,
32}
33
34impl ThemeKind {
35 pub fn to_str(&self) -> &str {
36 match self {
37 ThemeKind::Dark => "Dark",
38 ThemeKind::TokyoNight => "Tokyo Night",
39 ThemeKind::Custom => "Custom",
41 }
42 }
43
44 pub fn to_vec() -> Vec<Self> {
45 vec![Self::Dark, Self::TokyoNight]
46 }
47}
48
49#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
50#[derive(Debug, Clone)]
51pub struct Theme {
52 pub dark_mode: bool,
54 #[cfg_attr(feature = "serde", serde(skip))]
55 pub overlay_manager: OverlayManager,
56
57 pub image_tint_recommended: bool,
62 pub kind: ThemeKind,
63 pub style: Style,
64 pub colors: ThemeColors,
65 pub text_sizes: TextSizes,
66 pub window_frame: Frame,
68 pub frame1: Frame,
70 pub frame2: Frame,
72
73 pub frame1_visuals: FrameVisuals,
74 pub frame2_visuals: FrameVisuals,
75}
76
77impl PartialEq for Theme {
78 fn eq(&self, other: &Self) -> bool {
79 self.dark_mode == other.dark_mode
80 && self.kind == other.kind
81 && self.style == other.style
82 && self.colors == other.colors
83 && self.text_sizes == other.text_sizes
84 && self.window_frame == other.window_frame
85 && self.frame1 == other.frame1
86 && self.frame2 == other.frame2
87 && self.frame1_visuals == other.frame1_visuals
88 && self.frame2_visuals == other.frame2_visuals
89 }
90}
91
92impl Eq for Theme {}
93
94impl Theme {
95 pub fn new(kind: ThemeKind) -> Self {
99 let theme = match kind {
100 ThemeKind::Dark => dark::theme(),
101 ThemeKind::TokyoNight => tokyo_night::theme(),
102 ThemeKind::Custom => panic!("{}", PANIC_MSG),
104 };
105
106 theme
107 }
108
109 pub fn remap_derived_frames(&mut self, old: &ThemeColors) {
115 let new = self.colors;
116 if !frame_palette_changed(old, &new) {
117 return;
118 }
119
120 remap_frame(
121 &mut self.window_frame,
122 old.title_bar,
123 new.title_bar,
124 old.border,
125 new.border,
126 );
127 remap_frame(
128 &mut self.frame1,
129 old.widget_bg,
130 new.widget_bg,
131 old.border,
132 new.border,
133 );
134 remap_frame(
135 &mut self.frame2,
136 old.bg,
137 new.bg,
138 old.border,
139 new.border,
140 );
141 remap_frame_visuals(
142 &mut self.frame1_visuals,
143 old.hover,
144 new.hover,
145 old.widget_bg,
146 new.widget_bg,
147 old.highlight,
148 new.highlight,
149 );
150 remap_frame_visuals(
151 &mut self.frame2_visuals,
152 old.hover,
153 new.hover,
154 old.bg,
155 new.bg,
156 old.highlight,
157 new.highlight,
158 );
159 }
160
161 pub fn button_visuals(&self) -> ButtonVisuals {
162 match self.kind {
163 ThemeKind::Dark => self.colors.button_visuals,
164 ThemeKind::TokyoNight => self.colors.button_visuals,
165 ThemeKind::Custom => panic!("{}", PANIC_MSG),
167 }
168 }
169
170 pub fn label_visuals(&self) -> LabelVisuals {
171 match self.kind {
172 ThemeKind::Dark => self.colors.label_visuals,
173 ThemeKind::TokyoNight => self.colors.label_visuals,
174 ThemeKind::Custom => panic!("{}", PANIC_MSG),
176 }
177 }
178
179 pub fn combo_box_visuals(&self) -> ComboBoxVisuals {
180 match self.kind {
181 ThemeKind::Dark => self.colors.combo_box_visuals,
182 ThemeKind::TokyoNight => self.colors.combo_box_visuals,
183 ThemeKind::Custom => panic!("{}", PANIC_MSG),
185 }
186 }
187
188 pub fn text_edit_visuals(&self) -> TextEditVisuals {
189 match self.kind {
190 ThemeKind::Dark => self.colors.text_edit_visuals,
191 ThemeKind::TokyoNight => self.colors.text_edit_visuals,
192 ThemeKind::Custom => panic!("{}", PANIC_MSG),
194 }
195 }
196
197 pub fn install(self, ctx: &Context) {
199 let unchanged =
200 ctx.data(|d| d.get_temp::<Theme>(Self::storage_id()).is_some_and(|t| t == self));
201
202 if unchanged {
203 return;
204 }
205
206 ctx.set_global_style(self.style.clone());
207 ctx.data_mut(|d| d.insert_temp(Self::storage_id(), self));
208 }
209
210 pub fn current(ctx: &Context) -> Theme {
213 ctx.data(|d| {
214 d.get_temp::<Theme>(Self::storage_id())
215 .unwrap_or_else(|| Theme::new(ThemeKind::TokyoNight))
216 })
217 }
218
219 fn storage_id() -> Id {
220 Id::new("zeus::theme")
221 }
222}
223
224fn frame_palette_changed(old: &ThemeColors, new: &ThemeColors) -> bool {
225 old.title_bar != new.title_bar
226 || old.bg != new.bg
227 || old.widget_bg != new.widget_bg
228 || old.hover != new.hover
229 || old.highlight != new.highlight
230 || old.border != new.border
231}
232
233fn remap_if_eq(slot: &mut Color32, old: Color32, new: Color32) {
234 if *slot == old {
235 *slot = new;
236 }
237}
238
239fn remap_frame(
240 frame: &mut Frame,
241 old_fill: Color32,
242 new_fill: Color32,
243 old_border: Color32,
244 new_border: Color32,
245) {
246 remap_if_eq(&mut frame.fill, old_fill, new_fill);
247 remap_if_eq(&mut frame.stroke.color, old_border, new_border);
248 remap_if_eq(&mut frame.shadow.color, old_border, new_border);
249}
250
251fn remap_frame_visuals(
252 visuals: &mut FrameVisuals,
253 old_hover: Color32,
254 new_hover: Color32,
255 old_click: Color32,
256 new_click: Color32,
257 old_highlight: Color32,
258 new_highlight: Color32,
259) {
260 remap_if_eq(&mut visuals.bg_on_hover, old_hover, new_hover);
261 remap_if_eq(&mut visuals.bg_on_click, old_click, new_click);
262 remap_if_eq(
263 &mut visuals.border_on_hover.1,
264 old_highlight,
265 new_highlight,
266 );
267 remap_if_eq(
268 &mut visuals.border_on_click.1,
269 old_highlight,
270 new_highlight,
271 );
272}
273
274#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
276#[derive(Copy, Clone, Debug, PartialEq, Eq)]
277pub struct ThemeColors {
278 pub button_visuals: ButtonVisuals,
279
280 pub label_visuals: LabelVisuals,
281
282 pub combo_box_visuals: ComboBoxVisuals,
283
284 pub text_edit_visuals: TextEditVisuals,
285
286 pub title_bar: Color32,
288
289 pub bg: Color32,
291
292 pub widget_bg: Color32,
296
297 pub hover: Color32,
299
300 pub text: Color32,
302
303 pub text_muted: Color32,
307
308 pub highlight: Color32,
310
311 pub border: Color32,
313
314 pub accent: Color32,
316
317 pub error: Color32,
321
322 pub warning: Color32,
324
325 pub success: Color32,
329
330 pub info: Color32,
334}
335
336#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
337#[derive(Clone, Default, Debug, PartialEq)]
338pub struct TextSizes {
339 pub very_small: f32,
340 pub small: f32,
341 pub normal: f32,
342 pub large: f32,
343 pub very_large: f32,
344 pub heading: f32,
345}
346
347impl TextSizes {
348 pub fn new(
349 very_small: f32,
350 small: f32,
351 normal: f32,
352 large: f32,
353 very_large: f32,
354 heading: f32,
355 ) -> Self {
356 Self {
357 very_small,
358 small,
359 normal,
360 large,
361 very_large,
362 heading,
363 }
364 }
365}
366
367#[derive(Clone, Debug, Default)]
368pub struct OverlayManager(Arc<RwLock<OverlayCounter>>);
369
370impl OverlayManager {
371 pub fn new() -> Self {
372 Self(Arc::new(RwLock::new(OverlayCounter::new())))
373 }
374
375 pub fn tint_0(&self) -> Color32 {
376 Color32::from_black_alpha(40)
377 }
378
379 pub fn tint_1(&self) -> Color32 {
380 Color32::from_black_alpha(60)
381 }
382
383 pub fn tint_2(&self) -> Color32 {
384 Color32::from_black_alpha(80)
385 }
386
387 pub fn tint_3(&self) -> Color32 {
388 Color32::from_black_alpha(100)
389 }
390
391 pub fn counter(&self) -> u8 {
392 self.0.read().unwrap().counter()
393 }
394
395 pub fn order(&self) -> Order {
396 self.0.read().unwrap().order()
397 }
398
399 pub fn paint_background(&self) {
400 self.0.write().unwrap().paint_background()
401 }
402
403 pub fn paint_middle(&self) {
404 self.0.write().unwrap().paint_middle()
405 }
406
407 pub fn paint_foreground(&self) {
408 self.0.write().unwrap().paint_foreground()
409 }
410
411 pub fn paint_tooltip(&self) {
412 self.0.write().unwrap().paint_tooltip()
413 }
414
415 pub fn paint_debug(&self) {
416 self.0.write().unwrap().paint_debug()
417 }
418
419 pub fn window_opened(&self) {
421 self.0.write().unwrap().window_opened();
422 }
423
424 pub fn window_closed(&self) {
426 self.0.write().unwrap().window_closed();
427 }
428
429 pub fn recommended_order(&self) -> Order {
430 self.0.read().unwrap().recommended_order()
431 }
432
433 pub fn calculate_alpha(&self) -> u8 {
434 self.0.read().unwrap().calculate_alpha()
435 }
436
437 pub fn overlay_tint(&self) -> Color32 {
439 self.0.read().unwrap().overlay_tint()
440 }
441
442 pub fn paint_overlay(&self, ctx: &Context, recommend_order: bool) {
446 self.0.read().unwrap().paint_overlay(ctx, recommend_order);
447 }
448
449 pub fn paint_overlay_at(&self, ctx: &Context, rect: Rect, order: Order, id: Id, tint: Color32) {
451 self.0.read().unwrap().paint_overlay_at(ctx, rect, order, id, tint);
452 }
453}
454
455#[derive(Clone, Debug)]
456struct OverlayCounter {
457 counter: u8,
458 order: Order,
459}
460
461impl Default for OverlayCounter {
462 fn default() -> Self {
463 Self::new()
464 }
465}
466
467impl OverlayCounter {
468 pub fn new() -> Self {
469 Self {
470 counter: 0,
471 order: Order::Background,
472 }
473 }
474
475 pub fn counter(&self) -> u8 {
476 self.counter
477 }
478
479 pub fn order(&self) -> Order {
480 self.order
481 }
482
483 fn paint_background(&mut self) {
484 self.order = Order::Background;
485 }
486
487 fn paint_middle(&mut self) {
488 self.order = Order::Middle;
489 }
490
491 fn paint_foreground(&mut self) {
492 self.order = Order::Foreground;
493 }
494
495 fn paint_tooltip(&mut self) {
496 self.order = Order::Tooltip;
497 }
498
499 fn paint_debug(&mut self) {
500 self.order = Order::Debug;
501 }
502
503 fn window_opened(&mut self) {
504 self.counter += 1;
505 }
506
507 fn window_closed(&mut self) {
508 if self.counter > 0 {
509 self.counter -= 1;
510 }
511 }
512
513 fn calculate_alpha(&self) -> u8 {
514 let counter = self.counter;
515
516 if counter == 0 {
517 return 0;
518 }
519
520 let mut a = 80;
521 for _ in 1..counter {
522 a += 40;
523 }
524
525 a
526 }
527
528 fn overlay_tint(&self) -> Color32 {
529 let counter = self.counter();
530
531 if counter == 1 {
532 return Color32::from_black_alpha(80);
533 }
534
535 let alpha = self.calculate_alpha();
536 Color32::from_black_alpha(alpha)
537 }
538
539 fn recommended_order(&self) -> Order {
540 if self.counter() == 1 {
541 Order::Background
542 } else if self.counter() == 2 {
543 Order::Middle
544 } else {
545 Order::Foreground
546 }
547 }
548
549 fn paint_overlay(&self, ctx: &Context, recommend_order: bool) {
550 let counter = self.counter();
551 if counter == 0 {
552 return;
553 }
554
555 let order = if recommend_order {
556 if counter == 1 {
557 Order::Background
558 } else if counter == 2 {
559 Order::Middle
560 } else {
561 Order::Foreground
562 }
563 } else {
564 self.order()
565 };
566
567 let layer_id = LayerId::new(order, Id::new("darkening_overlay"));
568
569 let painter = ctx.layer_painter(layer_id);
570 painter.rect_filled(ctx.content_rect(), 0.0, self.overlay_tint());
571 }
572
573 pub fn paint_overlay_at(&self, ctx: &Context, rect: Rect, order: Order, id: Id, tint: Color32) {
574 let layer_id = LayerId::new(order, id);
575
576 let painter = ctx.layer_painter(layer_id);
577 painter.rect_filled(rect, 0.0, tint);
578 }
579}
580
581#[cfg(test)]
582mod tests {
583 use super::*;
584 use egui::{Margin, Stroke};
585
586 #[test]
587 fn custom_frame_fill_survives_palette_remap() {
588 let mut theme = Theme::new(ThemeKind::Dark);
589 let old = theme.colors;
590 let custom = Color32::from_rgb(255, 0, 0);
591 theme.frame1.fill = custom;
592
593 theme.colors.widget_bg = Color32::from_rgb(1, 2, 3);
594 theme.remap_derived_frames(&old);
595
596 assert_eq!(theme.frame1.fill, custom);
597 }
598
599 #[test]
600 fn palette_change_updates_unmodified_frame_fill() {
601 let mut theme = Theme::new(ThemeKind::Dark);
602 let old = theme.colors;
603 assert_eq!(theme.frame1.fill, old.widget_bg);
604
605 let next = Color32::from_rgb(1, 2, 3);
606 theme.colors.widget_bg = next;
607 theme.remap_derived_frames(&old);
608
609 assert_eq!(theme.frame1.fill, next);
610 }
611
612 #[test]
613 fn palette_remap_preserves_frame_structure() {
614 let mut theme = Theme::new(ThemeKind::Dark);
615 let old = theme.colors;
616 theme.frame1.inner_margin = Margin::same(42);
617 theme.frame1.stroke = Stroke::new(3.0, old.border);
618
619 theme.colors.widget_bg = Color32::from_rgb(9, 9, 9);
620 theme.colors.border = Color32::from_rgb(8, 8, 8);
621 theme.remap_derived_frames(&old);
622
623 assert_eq!(theme.frame1.inner_margin, Margin::same(42));
624 assert_eq!(theme.frame1.stroke.width, 3.0);
625 assert_eq!(
626 theme.frame1.stroke.color,
627 Color32::from_rgb(8, 8, 8)
628 );
629 assert_eq!(theme.frame1.fill, Color32::from_rgb(9, 9, 9));
630 }
631}