use egui::{
epaint::text::{FontInsert, FontPriority, InsertFontFamily},
style::{Selection, Widgets},
Color32, Context, CornerRadius, FontData, FontFamily, FontId, Id, Margin, Stroke, Style,
TextStyle, Vec2, Visuals, WidgetText,
};
const SYMBOLS_FONT_BYTES: &[u8] = include_bytes!("../assets/elegance-symbols.ttf");
const SYMBOLS_FONT_KEY: &str = "elegance-symbols";
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Accent {
Blue,
Green,
Red,
Purple,
Amber,
Sky,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Palette {
pub is_dark: bool,
pub bg: Color32,
pub card: Color32,
pub input_bg: Color32,
pub border: Color32,
pub text: Color32,
pub text_muted: Color32,
pub text_faint: Color32,
pub blue: Color32,
pub blue_hover: Color32,
pub green: Color32,
pub green_hover: Color32,
pub red: Color32,
pub red_hover: Color32,
pub purple: Color32,
pub purple_hover: Color32,
pub amber: Color32,
pub amber_hover: Color32,
pub sky: Color32,
pub success: Color32,
pub danger: Color32,
pub warning: Color32,
}
impl Palette {
pub fn slate() -> Self {
Self {
is_dark: true,
bg: rgb(0x0f, 0x17, 0x2a),
card: rgb(0x1e, 0x29, 0x3b),
input_bg: rgb(0x0f, 0x17, 0x2a),
border: rgb(0x33, 0x41, 0x55),
text: rgb(0xe2, 0xe8, 0xf0),
text_muted: rgb(0x94, 0xa3, 0xb8),
text_faint: rgb(0x64, 0x74, 0x8b),
blue: rgb(0x25, 0x63, 0xeb),
blue_hover: rgb(0x1d, 0x4e, 0xd8),
green: rgb(0x16, 0xa3, 0x4a),
green_hover: rgb(0x15, 0x80, 0x3d),
red: rgb(0xdc, 0x26, 0x26),
red_hover: rgb(0xb9, 0x1c, 0x1c),
purple: rgb(0x7c, 0x3a, 0xed),
purple_hover: rgb(0x6d, 0x28, 0xd9),
amber: rgb(0xd9, 0x77, 0x06),
amber_hover: rgb(0xb4, 0x53, 0x09),
sky: rgb(0x38, 0xbd, 0xf8),
success: rgb(0x4a, 0xde, 0x80),
danger: rgb(0xf8, 0x71, 0x71),
warning: rgb(0xfb, 0xbf, 0x24),
}
}
pub fn charcoal() -> Self {
Self {
is_dark: true,
bg: rgb(0x0f, 0x0f, 0x10),
card: rgb(0x1c, 0x1c, 0x1e),
input_bg: rgb(0x0f, 0x0f, 0x10),
border: rgb(0x38, 0x38, 0x3a),
text: rgb(0xfa, 0xfa, 0xfa),
text_muted: rgb(0xa1, 0xa1, 0xaa),
text_faint: rgb(0x71, 0x71, 0x7a),
blue: rgb(0x3b, 0x82, 0xf6),
blue_hover: rgb(0x25, 0x63, 0xeb),
green: rgb(0x22, 0xc5, 0x5e),
green_hover: rgb(0x16, 0xa3, 0x4a),
red: rgb(0xef, 0x44, 0x44),
red_hover: rgb(0xdc, 0x26, 0x26),
purple: rgb(0x8b, 0x5c, 0xf6),
purple_hover: rgb(0x7c, 0x3a, 0xed),
amber: rgb(0xf5, 0x9e, 0x0b),
amber_hover: rgb(0xd9, 0x77, 0x06),
sky: rgb(0x22, 0xd3, 0xee),
success: rgb(0x4a, 0xde, 0x80),
danger: rgb(0xf8, 0x71, 0x71),
warning: rgb(0xfb, 0xbf, 0x24),
}
}
pub fn frost() -> Self {
Self {
is_dark: false,
bg: rgb(0xe2, 0xe8, 0xf0),
card: rgb(0xf8, 0xfa, 0xfc),
input_bg: rgb(0xff, 0xff, 0xff),
border: rgb(0x94, 0xa3, 0xb8),
text: rgb(0x0f, 0x17, 0x2a),
text_muted: rgb(0x47, 0x55, 0x69),
text_faint: rgb(0x64, 0x74, 0x8b),
blue: rgb(0x25, 0x63, 0xeb),
blue_hover: rgb(0x1d, 0x4e, 0xd8),
green: rgb(0x16, 0xa3, 0x4a),
green_hover: rgb(0x15, 0x80, 0x3d),
red: rgb(0xdc, 0x26, 0x26),
red_hover: rgb(0xb9, 0x1c, 0x1c),
purple: rgb(0x7c, 0x3a, 0xed),
purple_hover: rgb(0x6d, 0x28, 0xd9),
amber: rgb(0xd9, 0x77, 0x06),
amber_hover: rgb(0xb4, 0x53, 0x09),
sky: rgb(0x03, 0x74, 0xb0),
success: rgb(0x16, 0xa3, 0x4a),
danger: rgb(0xdc, 0x26, 0x26),
warning: rgb(0xd9, 0x77, 0x06),
}
}
pub fn paper() -> Self {
Self {
is_dark: false,
bg: rgb(0xec, 0xe9, 0xe4),
card: rgb(0xfa, 0xf8, 0xf3),
input_bg: rgb(0xff, 0xff, 0xff),
border: rgb(0xbc, 0xb6, 0xa8),
text: rgb(0x1c, 0x1a, 0x16),
text_muted: rgb(0x57, 0x52, 0x4a),
text_faint: rgb(0x8a, 0x83, 0x77),
blue: rgb(0x25, 0x63, 0xeb),
blue_hover: rgb(0x1d, 0x4e, 0xd8),
green: rgb(0x16, 0xa3, 0x4a),
green_hover: rgb(0x15, 0x80, 0x3d),
red: rgb(0xdc, 0x26, 0x26),
red_hover: rgb(0xb9, 0x1c, 0x1c),
purple: rgb(0x7c, 0x3a, 0xed),
purple_hover: rgb(0x6d, 0x28, 0xd9),
amber: rgb(0xd9, 0x77, 0x06),
amber_hover: rgb(0xb4, 0x53, 0x09),
sky: rgb(0x0c, 0x80, 0x9e),
success: rgb(0x16, 0xa3, 0x4a),
danger: rgb(0xdc, 0x26, 0x26),
warning: rgb(0xd9, 0x77, 0x06),
}
}
pub fn depth_tint(&self, base: Color32, t: f32) -> Color32 {
let toward = if self.is_dark {
Color32::WHITE
} else {
Color32::BLACK
};
mix(base, toward, t)
}
pub fn accent_fill(&self, accent: Accent) -> Color32 {
match accent {
Accent::Blue => self.blue,
Accent::Green => self.green,
Accent::Red => self.red,
Accent::Purple => self.purple,
Accent::Amber => self.amber,
Accent::Sky => self.sky,
}
}
pub fn accent_hover(&self, accent: Accent) -> Color32 {
match accent {
Accent::Blue => self.blue_hover,
Accent::Green => self.green_hover,
Accent::Red => self.red_hover,
Accent::Purple => self.purple_hover,
Accent::Amber => self.amber_hover,
Accent::Sky => mix(self.sky, Color32::BLACK, 0.15),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Typography {
pub body: f32,
pub button: f32,
pub label: f32,
pub small: f32,
pub heading: f32,
pub monospace: f32,
}
impl Typography {
pub fn elegant() -> Self {
Self {
body: 14.0,
button: 13.5,
label: 13.0,
small: 12.0,
heading: 16.0,
monospace: 13.0,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Theme {
pub palette: Palette,
pub typography: Typography,
pub control_radius: f32,
pub card_radius: f32,
pub card_padding: f32,
pub control_padding_y: f32,
pub control_padding_x: f32,
}
impl Theme {
pub fn slate() -> Self {
Self {
palette: Palette::slate(),
typography: Typography::elegant(),
control_radius: 6.0,
card_radius: 10.0,
card_padding: 18.0,
control_padding_y: 6.5,
control_padding_x: 14.0,
}
}
pub fn charcoal() -> Self {
Self {
palette: Palette::charcoal(),
..Self::slate()
}
}
pub fn frost() -> Self {
Self {
palette: Palette::frost(),
..Self::slate()
}
}
pub fn paper() -> Self {
Self {
palette: Palette::paper(),
..Self::slate()
}
}
pub fn install(self, ctx: &Context) {
install_symbols_font(ctx);
let unchanged = ctx.data(|d| {
d.get_temp::<Theme>(Self::storage_id())
.is_some_and(|t| t == self)
});
if unchanged {
return;
}
ctx.global_style_mut(|style| self.apply_to_style(style));
ctx.data_mut(|d| d.insert_temp(Self::storage_id(), self));
}
pub fn current(ctx: &Context) -> Theme {
ctx.data(|d| {
d.get_temp::<Theme>(Self::storage_id())
.unwrap_or_else(Theme::slate)
})
}
fn storage_id() -> Id {
Id::new("elegance::theme")
}
}
fn install_symbols_font(ctx: &Context) {
ctx.add_font(FontInsert::new(
SYMBOLS_FONT_KEY,
FontData::from_static(SYMBOLS_FONT_BYTES),
vec![
InsertFontFamily {
family: FontFamily::Proportional,
priority: FontPriority::Lowest,
},
InsertFontFamily {
family: FontFamily::Monospace,
priority: FontPriority::Lowest,
},
],
));
}
impl Theme {
fn apply_to_style(&self, style: &mut Style) {
let p = &self.palette;
let t = &self.typography;
use FontFamily::{Monospace, Proportional};
style
.text_styles
.insert(TextStyle::Heading, FontId::new(t.heading, Proportional));
style
.text_styles
.insert(TextStyle::Body, FontId::new(t.body, Proportional));
style
.text_styles
.insert(TextStyle::Button, FontId::new(t.button, Proportional));
style
.text_styles
.insert(TextStyle::Small, FontId::new(t.small, Proportional));
style
.text_styles
.insert(TextStyle::Monospace, FontId::new(t.monospace, Monospace));
let sp = &mut style.spacing;
sp.item_spacing = Vec2::new(8.0, 6.0);
sp.button_padding = Vec2::new(self.control_padding_x, self.control_padding_y);
sp.interact_size = Vec2::new(24.0, 24.0);
sp.icon_width = 16.0;
sp.icon_width_inner = 10.0;
sp.icon_spacing = 6.0;
sp.combo_width = 120.0;
sp.text_edit_width = 180.0;
sp.window_margin = Margin::same(10);
sp.menu_margin = Margin::same(6);
sp.indent = 16.0;
style.interaction.tooltip_delay = 0.35;
style.interaction.tooltip_grace_time = 0.2;
let v = &mut style.visuals;
*v = if p.is_dark {
Visuals::dark()
} else {
Visuals::light()
};
v.dark_mode = p.is_dark;
v.override_text_color = Some(p.text);
v.panel_fill = p.bg;
v.window_fill = p.card;
v.window_stroke = Stroke::new(1.0, p.border);
v.window_corner_radius = CornerRadius::same(self.card_radius as u8);
v.menu_corner_radius = CornerRadius::same(8);
v.extreme_bg_color = p.input_bg;
v.faint_bg_color = p.depth_tint(p.card, 0.02);
v.code_bg_color = p.input_bg;
v.hyperlink_color = p.sky;
v.warn_fg_color = p.warning;
v.error_fg_color = p.danger;
v.button_frame = true;
v.striped = false;
v.selection = Selection {
bg_fill: with_alpha(p.sky, 70),
stroke: Stroke::new(1.0, p.sky),
};
let control_radius = CornerRadius::same(self.control_radius as u8);
v.widgets = Widgets {
noninteractive: egui::style::WidgetVisuals {
bg_fill: p.card,
weak_bg_fill: p.card,
bg_stroke: Stroke::new(1.0, p.border),
corner_radius: control_radius,
fg_stroke: Stroke::new(1.0, p.text),
expansion: 0.0,
},
inactive: egui::style::WidgetVisuals {
bg_fill: p.input_bg,
weak_bg_fill: p.input_bg,
bg_stroke: Stroke::new(1.0, p.border),
corner_radius: control_radius,
fg_stroke: Stroke::new(1.0, p.text),
expansion: 0.0,
},
hovered: egui::style::WidgetVisuals {
bg_fill: p.depth_tint(p.input_bg, 0.04),
weak_bg_fill: p.depth_tint(p.input_bg, 0.04),
bg_stroke: Stroke::new(1.0, p.text_muted),
corner_radius: control_radius,
fg_stroke: Stroke::new(1.5, p.text),
expansion: 1.0,
},
active: egui::style::WidgetVisuals {
bg_fill: mix(p.input_bg, p.sky, 0.15),
weak_bg_fill: mix(p.input_bg, p.sky, 0.15),
bg_stroke: Stroke::new(1.0, p.sky),
corner_radius: control_radius,
fg_stroke: Stroke::new(1.5, p.text),
expansion: 1.0,
},
open: egui::style::WidgetVisuals {
bg_fill: p.input_bg,
weak_bg_fill: p.input_bg,
bg_stroke: Stroke::new(1.0, p.sky),
corner_radius: control_radius,
fg_stroke: Stroke::new(1.0, p.text),
expansion: 0.0,
},
};
}
pub fn body_text(&self, text: impl Into<String>) -> WidgetText {
egui::RichText::new(text.into())
.color(self.palette.text)
.size(self.typography.body)
.into()
}
pub fn heading_text(&self, text: impl Into<String>) -> WidgetText {
egui::RichText::new(text.into())
.color(self.palette.text)
.size(self.typography.heading)
.strong()
.into()
}
pub fn muted_text(&self, text: impl Into<String>) -> WidgetText {
egui::RichText::new(text.into())
.color(self.palette.text_muted)
.size(self.typography.label)
.into()
}
pub fn faint_text(&self, text: impl Into<String>) -> WidgetText {
egui::RichText::new(text.into())
.color(self.palette.text_faint)
.size(self.typography.small)
.into()
}
}
impl Default for Theme {
fn default() -> Self {
Self::slate()
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum BuiltInTheme {
#[default]
Slate,
Charcoal,
Frost,
Paper,
}
impl BuiltInTheme {
pub const fn label(self) -> &'static str {
match self {
Self::Slate => "Slate",
Self::Charcoal => "Charcoal",
Self::Frost => "Frost",
Self::Paper => "Paper",
}
}
pub fn theme(self) -> Theme {
match self {
Self::Slate => Theme::slate(),
Self::Charcoal => Theme::charcoal(),
Self::Frost => Theme::frost(),
Self::Paper => Theme::paper(),
}
}
pub const fn all() -> [BuiltInTheme; 4] {
[Self::Slate, Self::Charcoal, Self::Frost, Self::Paper]
}
}
#[inline]
const fn rgb(r: u8, g: u8, b: u8) -> Color32 {
Color32::from_rgb(r, g, b)
}
pub(crate) fn with_alpha(c: Color32, alpha: u8) -> Color32 {
Color32::from_rgba_unmultiplied(c.r(), c.g(), c.b(), alpha)
}
pub(crate) const BASELINE_FRAC: f32 = 0.78;
pub(crate) fn with_themed_visuals<R>(ui: &mut egui::Ui, f: impl FnOnce(&mut egui::Ui) -> R) -> R {
let saved = ui.visuals().clone();
let result = f(ui);
*ui.visuals_mut() = saved;
result
}
pub(crate) fn themed_input_visuals(v: &mut Visuals, theme: &Theme, bg_fill: Color32) {
let p = &theme.palette;
let radius = CornerRadius::same(theme.control_radius as u8);
for w in [
&mut v.widgets.inactive,
&mut v.widgets.hovered,
&mut v.widgets.active,
&mut v.widgets.open,
] {
w.bg_fill = bg_fill;
w.weak_bg_fill = bg_fill;
w.corner_radius = radius;
w.expansion = 0.0;
}
v.widgets.inactive.bg_stroke = Stroke::new(1.0, p.border);
v.widgets.hovered.bg_stroke = Stroke::new(1.0, p.text_muted);
v.widgets.active.bg_stroke = Stroke::new(1.5, p.sky);
v.widgets.open.bg_stroke = Stroke::new(1.5, p.sky);
}
pub(crate) fn placeholder_galley(
ui: &egui::Ui,
text: &str,
font_size: f32,
strong: bool,
wrap_width: f32,
) -> std::sync::Arc<egui::Galley> {
let mut rt = egui::RichText::new(text)
.size(font_size)
.color(Color32::PLACEHOLDER);
if strong {
rt = rt.strong();
}
egui::WidgetText::from(rt).into_galley(
ui,
Some(egui::TextWrapMode::Extend),
wrap_width,
egui::FontSelection::FontId(egui::FontId::proportional(font_size)),
)
}
pub(crate) fn mix(a: Color32, b: Color32, t: f32) -> Color32 {
let t = t.clamp(0.0, 1.0);
let lerp = |x: u8, y: u8| -> u8 {
let xf = x as f32;
let yf = y as f32;
(xf + (yf - xf) * t).round().clamp(0.0, 255.0) as u8
};
Color32::from_rgba_unmultiplied(
lerp(a.r(), b.r()),
lerp(a.g(), b.g()),
lerp(a.b(), b.b()),
lerp(a.a().max(1), b.a().max(1)),
)
}