1use egui::{
11 epaint::text::{FontInsert, FontPriority, InsertFontFamily},
12 style::{Selection, Widgets},
13 Color32, Context, CornerRadius, FontData, FontFamily, FontId, Id, Margin, Stroke, Style,
14 TextStyle, Vec2, Visuals, WidgetText,
15};
16
17const SYMBOLS_FONT_BYTES: &[u8] = include_bytes!("../assets/elegance-symbols.ttf");
21const SYMBOLS_FONT_KEY: &str = "elegance-symbols";
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
31pub enum Accent {
32 Blue,
34 Green,
36 Red,
38 Purple,
40 Amber,
42 Sky,
44}
45
46#[derive(Clone, Debug, PartialEq)]
51pub struct Palette {
52 pub is_dark: bool,
59
60 pub bg: Color32,
62 pub card: Color32,
64 pub input_bg: Color32,
66 pub border: Color32,
68
69 pub text: Color32,
71 pub text_muted: Color32,
73 pub text_faint: Color32,
75
76 pub blue: Color32,
78 pub blue_hover: Color32,
80 pub green: Color32,
82 pub green_hover: Color32,
84 pub red: Color32,
86 pub red_hover: Color32,
88 pub purple: Color32,
90 pub purple_hover: Color32,
92 pub amber: Color32,
94 pub amber_hover: Color32,
96 pub sky: Color32,
98
99 pub success: Color32,
101 pub danger: Color32,
103 pub warning: Color32,
105}
106
107impl Palette {
108 pub fn slate() -> Self {
111 Self {
112 is_dark: true,
113 bg: rgb(0x0f, 0x17, 0x2a),
114 card: rgb(0x1e, 0x29, 0x3b),
115 input_bg: rgb(0x0f, 0x17, 0x2a),
116 border: rgb(0x33, 0x41, 0x55),
117
118 text: rgb(0xe2, 0xe8, 0xf0),
119 text_muted: rgb(0x94, 0xa3, 0xb8),
120 text_faint: rgb(0x64, 0x74, 0x8b),
121
122 blue: rgb(0x25, 0x63, 0xeb),
123 blue_hover: rgb(0x1d, 0x4e, 0xd8),
124 green: rgb(0x16, 0xa3, 0x4a),
125 green_hover: rgb(0x15, 0x80, 0x3d),
126 red: rgb(0xdc, 0x26, 0x26),
127 red_hover: rgb(0xb9, 0x1c, 0x1c),
128 purple: rgb(0x7c, 0x3a, 0xed),
129 purple_hover: rgb(0x6d, 0x28, 0xd9),
130 amber: rgb(0xd9, 0x77, 0x06),
131 amber_hover: rgb(0xb4, 0x53, 0x09),
132 sky: rgb(0x38, 0xbd, 0xf8),
133
134 success: rgb(0x4a, 0xde, 0x80),
135 danger: rgb(0xf8, 0x71, 0x71),
136 warning: rgb(0xfb, 0xbf, 0x24),
137 }
138 }
139
140 pub fn charcoal() -> Self {
144 Self {
145 is_dark: true,
146 bg: rgb(0x0f, 0x0f, 0x10),
147 card: rgb(0x1c, 0x1c, 0x1e),
148 input_bg: rgb(0x0f, 0x0f, 0x10),
149 border: rgb(0x38, 0x38, 0x3a),
150
151 text: rgb(0xfa, 0xfa, 0xfa),
152 text_muted: rgb(0xa1, 0xa1, 0xaa),
153 text_faint: rgb(0x71, 0x71, 0x7a),
154
155 blue: rgb(0x3b, 0x82, 0xf6),
156 blue_hover: rgb(0x25, 0x63, 0xeb),
157 green: rgb(0x22, 0xc5, 0x5e),
158 green_hover: rgb(0x16, 0xa3, 0x4a),
159 red: rgb(0xef, 0x44, 0x44),
160 red_hover: rgb(0xdc, 0x26, 0x26),
161 purple: rgb(0x8b, 0x5c, 0xf6),
162 purple_hover: rgb(0x7c, 0x3a, 0xed),
163 amber: rgb(0xf5, 0x9e, 0x0b),
164 amber_hover: rgb(0xd9, 0x77, 0x06),
165 sky: rgb(0x22, 0xd3, 0xee),
166
167 success: rgb(0x4a, 0xde, 0x80),
168 danger: rgb(0xf8, 0x71, 0x71),
169 warning: rgb(0xfb, 0xbf, 0x24),
170 }
171 }
172
173 pub fn frost() -> Self {
178 Self {
179 is_dark: false,
180 bg: rgb(0xe2, 0xe8, 0xf0),
181 card: rgb(0xf8, 0xfa, 0xfc),
182 input_bg: rgb(0xff, 0xff, 0xff),
183 border: rgb(0x94, 0xa3, 0xb8),
184
185 text: rgb(0x0f, 0x17, 0x2a),
186 text_muted: rgb(0x47, 0x55, 0x69),
187 text_faint: rgb(0x64, 0x74, 0x8b),
188
189 blue: rgb(0x25, 0x63, 0xeb),
190 blue_hover: rgb(0x1d, 0x4e, 0xd8),
191 green: rgb(0x16, 0xa3, 0x4a),
192 green_hover: rgb(0x15, 0x80, 0x3d),
193 red: rgb(0xdc, 0x26, 0x26),
194 red_hover: rgb(0xb9, 0x1c, 0x1c),
195 purple: rgb(0x7c, 0x3a, 0xed),
196 purple_hover: rgb(0x6d, 0x28, 0xd9),
197 amber: rgb(0xd9, 0x77, 0x06),
198 amber_hover: rgb(0xb4, 0x53, 0x09),
199 sky: rgb(0x03, 0x74, 0xb0),
200
201 success: rgb(0x16, 0xa3, 0x4a),
202 danger: rgb(0xdc, 0x26, 0x26),
203 warning: rgb(0xd9, 0x77, 0x06),
204 }
205 }
206
207 pub fn paper() -> Self {
211 Self {
212 is_dark: false,
213 bg: rgb(0xec, 0xe9, 0xe4),
214 card: rgb(0xfa, 0xf8, 0xf3),
215 input_bg: rgb(0xff, 0xff, 0xff),
216 border: rgb(0xbc, 0xb6, 0xa8),
217
218 text: rgb(0x1c, 0x1a, 0x16),
219 text_muted: rgb(0x57, 0x52, 0x4a),
220 text_faint: rgb(0x8a, 0x83, 0x77),
221
222 blue: rgb(0x25, 0x63, 0xeb),
223 blue_hover: rgb(0x1d, 0x4e, 0xd8),
224 green: rgb(0x16, 0xa3, 0x4a),
225 green_hover: rgb(0x15, 0x80, 0x3d),
226 red: rgb(0xdc, 0x26, 0x26),
227 red_hover: rgb(0xb9, 0x1c, 0x1c),
228 purple: rgb(0x7c, 0x3a, 0xed),
229 purple_hover: rgb(0x6d, 0x28, 0xd9),
230 amber: rgb(0xd9, 0x77, 0x06),
231 amber_hover: rgb(0xb4, 0x53, 0x09),
232 sky: rgb(0x0c, 0x80, 0x9e),
233
234 success: rgb(0x16, 0xa3, 0x4a),
235 danger: rgb(0xdc, 0x26, 0x26),
236 warning: rgb(0xd9, 0x77, 0x06),
237 }
238 }
239
240 pub fn depth_tint(&self, base: Color32, t: f32) -> Color32 {
248 let toward = if self.is_dark {
249 Color32::WHITE
250 } else {
251 Color32::BLACK
252 };
253 mix(base, toward, t)
254 }
255
256 pub fn accent_fill(&self, accent: Accent) -> Color32 {
258 match accent {
259 Accent::Blue => self.blue,
260 Accent::Green => self.green,
261 Accent::Red => self.red,
262 Accent::Purple => self.purple,
263 Accent::Amber => self.amber,
264 Accent::Sky => self.sky,
265 }
266 }
267
268 pub fn accent_hover(&self, accent: Accent) -> Color32 {
270 match accent {
271 Accent::Blue => self.blue_hover,
272 Accent::Green => self.green_hover,
273 Accent::Red => self.red_hover,
274 Accent::Purple => self.purple_hover,
275 Accent::Amber => self.amber_hover,
276 Accent::Sky => mix(self.sky, Color32::BLACK, 0.15),
277 }
278 }
279}
280
281#[derive(Clone, Copy, Debug, PartialEq)]
286pub struct Typography {
287 pub body: f32,
289 pub button: f32,
291 pub label: f32,
293 pub small: f32,
295 pub heading: f32,
297 pub monospace: f32,
299}
300
301impl Typography {
302 pub fn elegant() -> Self {
304 Self {
305 body: 14.0,
306 button: 13.5,
307 label: 13.0,
308 small: 12.0,
309 heading: 16.0,
310 monospace: 13.0,
311 }
312 }
313}
314
315#[derive(Clone, Debug, PartialEq)]
317pub struct Theme {
318 pub palette: Palette,
320 pub typography: Typography,
322
323 pub control_radius: f32,
325 pub card_radius: f32,
327 pub card_padding: f32,
329 pub control_padding_y: f32,
331 pub control_padding_x: f32,
333}
334
335impl Theme {
336 pub fn slate() -> Self {
338 Self {
339 palette: Palette::slate(),
340 typography: Typography::elegant(),
341 control_radius: 6.0,
342 card_radius: 10.0,
343 card_padding: 18.0,
344 control_padding_y: 6.5,
345 control_padding_x: 14.0,
346 }
347 }
348
349 pub fn charcoal() -> Self {
353 Self {
354 palette: Palette::charcoal(),
355 ..Self::slate()
356 }
357 }
358
359 pub fn frost() -> Self {
363 Self {
364 palette: Palette::frost(),
365 ..Self::slate()
366 }
367 }
368
369 pub fn paper() -> Self {
373 Self {
374 palette: Palette::paper(),
375 ..Self::slate()
376 }
377 }
378
379 pub fn install(self, ctx: &Context) {
398 install_symbols_font(ctx);
399
400 let unchanged = ctx.data(|d| {
401 d.get_temp::<Theme>(Self::storage_id())
402 .is_some_and(|t| t == self)
403 });
404 if unchanged {
405 return;
406 }
407 ctx.global_style_mut(|style| self.apply_to_style(style));
408 ctx.data_mut(|d| d.insert_temp(Self::storage_id(), self));
409 }
410
411 pub fn current(ctx: &Context) -> Theme {
414 ctx.data(|d| {
415 d.get_temp::<Theme>(Self::storage_id())
416 .unwrap_or_else(Theme::slate)
417 })
418 }
419
420 fn storage_id() -> Id {
421 Id::new("elegance::theme")
422 }
423}
424
425fn install_symbols_font(ctx: &Context) {
426 ctx.add_font(FontInsert::new(
427 SYMBOLS_FONT_KEY,
428 FontData::from_static(SYMBOLS_FONT_BYTES),
429 vec![
430 InsertFontFamily {
431 family: FontFamily::Proportional,
432 priority: FontPriority::Lowest,
433 },
434 InsertFontFamily {
435 family: FontFamily::Monospace,
436 priority: FontPriority::Lowest,
437 },
438 ],
439 ));
440}
441
442impl Theme {
443 fn apply_to_style(&self, style: &mut Style) {
444 let p = &self.palette;
445 let t = &self.typography;
446
447 use FontFamily::{Monospace, Proportional};
449 style
450 .text_styles
451 .insert(TextStyle::Heading, FontId::new(t.heading, Proportional));
452 style
453 .text_styles
454 .insert(TextStyle::Body, FontId::new(t.body, Proportional));
455 style
456 .text_styles
457 .insert(TextStyle::Button, FontId::new(t.button, Proportional));
458 style
459 .text_styles
460 .insert(TextStyle::Small, FontId::new(t.small, Proportional));
461 style
462 .text_styles
463 .insert(TextStyle::Monospace, FontId::new(t.monospace, Monospace));
464
465 let sp = &mut style.spacing;
467 sp.item_spacing = Vec2::new(8.0, 6.0);
468 sp.button_padding = Vec2::new(self.control_padding_x, self.control_padding_y);
469 sp.interact_size = Vec2::new(24.0, 24.0);
470 sp.icon_width = 16.0;
471 sp.icon_width_inner = 10.0;
472 sp.icon_spacing = 6.0;
473 sp.combo_width = 120.0;
474 sp.text_edit_width = 180.0;
475 sp.window_margin = Margin::same(10);
476 sp.menu_margin = Margin::same(6);
477 sp.indent = 16.0;
478
479 let v = &mut style.visuals;
481 *v = if p.is_dark {
482 Visuals::dark()
483 } else {
484 Visuals::light()
485 };
486 v.dark_mode = p.is_dark;
487 v.override_text_color = Some(p.text);
488 v.panel_fill = p.bg;
489 v.window_fill = p.card;
490 v.window_stroke = Stroke::new(1.0, p.border);
491 v.window_corner_radius = CornerRadius::same(self.card_radius as u8);
492 v.menu_corner_radius = CornerRadius::same(8);
493 v.extreme_bg_color = p.input_bg;
494 v.faint_bg_color = p.depth_tint(p.card, 0.02);
495 v.code_bg_color = p.input_bg;
496 v.hyperlink_color = p.sky;
497 v.warn_fg_color = p.warning;
498 v.error_fg_color = p.danger;
499 v.button_frame = true;
500 v.striped = false;
501
502 v.selection = Selection {
503 bg_fill: with_alpha(p.sky, 70),
504 stroke: Stroke::new(1.0, p.sky),
505 };
506
507 let control_radius = CornerRadius::same(self.control_radius as u8);
511 v.widgets = Widgets {
512 noninteractive: egui::style::WidgetVisuals {
513 bg_fill: p.card,
514 weak_bg_fill: p.card,
515 bg_stroke: Stroke::new(1.0, p.border),
516 corner_radius: control_radius,
517 fg_stroke: Stroke::new(1.0, p.text),
518 expansion: 0.0,
519 },
520 inactive: egui::style::WidgetVisuals {
521 bg_fill: p.input_bg,
522 weak_bg_fill: p.input_bg,
523 bg_stroke: Stroke::new(1.0, p.border),
524 corner_radius: control_radius,
525 fg_stroke: Stroke::new(1.0, p.text),
526 expansion: 0.0,
527 },
528 hovered: egui::style::WidgetVisuals {
529 bg_fill: p.depth_tint(p.input_bg, 0.04),
530 weak_bg_fill: p.depth_tint(p.input_bg, 0.04),
531 bg_stroke: Stroke::new(1.0, p.text_muted),
532 corner_radius: control_radius,
533 fg_stroke: Stroke::new(1.5, p.text),
534 expansion: 1.0,
535 },
536 active: egui::style::WidgetVisuals {
537 bg_fill: mix(p.input_bg, p.sky, 0.15),
538 weak_bg_fill: mix(p.input_bg, p.sky, 0.15),
539 bg_stroke: Stroke::new(1.0, p.sky),
540 corner_radius: control_radius,
541 fg_stroke: Stroke::new(1.5, p.text),
542 expansion: 1.0,
543 },
544 open: egui::style::WidgetVisuals {
545 bg_fill: p.input_bg,
546 weak_bg_fill: p.input_bg,
547 bg_stroke: Stroke::new(1.0, p.sky),
548 corner_radius: control_radius,
549 fg_stroke: Stroke::new(1.0, p.text),
550 expansion: 0.0,
551 },
552 };
553 }
554
555 pub fn body_text(&self, text: impl Into<String>) -> WidgetText {
558 egui::RichText::new(text.into())
559 .color(self.palette.text)
560 .size(self.typography.body)
561 .into()
562 }
563
564 pub fn heading_text(&self, text: impl Into<String>) -> WidgetText {
566 egui::RichText::new(text.into())
567 .color(self.palette.text)
568 .size(self.typography.heading)
569 .strong()
570 .into()
571 }
572
573 pub fn muted_text(&self, text: impl Into<String>) -> WidgetText {
575 egui::RichText::new(text.into())
576 .color(self.palette.text_muted)
577 .size(self.typography.label)
578 .into()
579 }
580
581 pub fn faint_text(&self, text: impl Into<String>) -> WidgetText {
583 egui::RichText::new(text.into())
584 .color(self.palette.text_faint)
585 .size(self.typography.small)
586 .into()
587 }
588}
589
590impl Default for Theme {
591 fn default() -> Self {
592 Self::slate()
593 }
594}
595
596#[non_exhaustive]
611#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
612pub enum BuiltInTheme {
613 #[default]
615 Slate,
616 Charcoal,
618 Frost,
620 Paper,
622}
623
624impl BuiltInTheme {
625 pub const fn label(self) -> &'static str {
627 match self {
628 Self::Slate => "Slate",
629 Self::Charcoal => "Charcoal",
630 Self::Frost => "Frost",
631 Self::Paper => "Paper",
632 }
633 }
634
635 pub fn theme(self) -> Theme {
637 match self {
638 Self::Slate => Theme::slate(),
639 Self::Charcoal => Theme::charcoal(),
640 Self::Frost => Theme::frost(),
641 Self::Paper => Theme::paper(),
642 }
643 }
644
645 pub const fn all() -> [BuiltInTheme; 4] {
648 [Self::Slate, Self::Charcoal, Self::Frost, Self::Paper]
649 }
650}
651
652#[inline]
655const fn rgb(r: u8, g: u8, b: u8) -> Color32 {
656 Color32::from_rgb(r, g, b)
657}
658
659pub(crate) fn with_alpha(c: Color32, alpha: u8) -> Color32 {
660 Color32::from_rgba_unmultiplied(c.r(), c.g(), c.b(), alpha)
661}
662
663pub(crate) fn with_themed_visuals<R>(ui: &mut egui::Ui, f: impl FnOnce(&mut egui::Ui) -> R) -> R {
668 let saved = ui.visuals().clone();
669 let result = f(ui);
670 *ui.visuals_mut() = saved;
671 result
672}
673
674pub(crate) fn themed_input_visuals(v: &mut Visuals, theme: &Theme, bg_fill: Color32) {
683 let p = &theme.palette;
684 let radius = CornerRadius::same(theme.control_radius as u8);
685 for w in [
686 &mut v.widgets.inactive,
687 &mut v.widgets.hovered,
688 &mut v.widgets.active,
689 &mut v.widgets.open,
690 ] {
691 w.bg_fill = bg_fill;
692 w.weak_bg_fill = bg_fill;
693 w.corner_radius = radius;
694 w.expansion = 0.0;
699 }
700 v.widgets.inactive.bg_stroke = Stroke::new(1.0, p.border);
701 v.widgets.hovered.bg_stroke = Stroke::new(1.0, p.text_muted);
702 v.widgets.active.bg_stroke = Stroke::new(1.5, p.sky);
703 v.widgets.open.bg_stroke = Stroke::new(1.5, p.sky);
704}
705
706pub(crate) fn placeholder_galley(
712 ui: &egui::Ui,
713 text: &str,
714 font_size: f32,
715 strong: bool,
716 wrap_width: f32,
717) -> std::sync::Arc<egui::Galley> {
718 let mut rt = egui::RichText::new(text)
719 .size(font_size)
720 .color(Color32::PLACEHOLDER);
721 if strong {
722 rt = rt.strong();
723 }
724 egui::WidgetText::from(rt).into_galley(
725 ui,
726 Some(egui::TextWrapMode::Extend),
727 wrap_width,
728 egui::FontSelection::FontId(egui::FontId::proportional(font_size)),
729 )
730}
731
732pub(crate) fn mix(a: Color32, b: Color32, t: f32) -> Color32 {
734 let t = t.clamp(0.0, 1.0);
735 let lerp = |x: u8, y: u8| -> u8 {
736 let xf = x as f32;
737 let yf = y as f32;
738 (xf + (yf - xf) * t).round().clamp(0.0, 255.0) as u8
739 };
740 Color32::from_rgba_unmultiplied(
741 lerp(a.r(), b.r()),
742 lerp(a.g(), b.g()),
743 lerp(a.b(), b.b()),
744 lerp(a.a().max(1), b.a().max(1)),
745 )
746}