use std::sync::atomic::{AtomicU8, Ordering};
use egui::Color32;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum Theme {
#[default]
System,
Dark,
Light,
}
impl Theme {
pub fn as_str(self) -> &'static str {
match self {
Theme::System => "system",
Theme::Dark => "dark",
Theme::Light => "light",
}
}
pub fn from_str(s: &str) -> Theme {
match s.trim().to_ascii_lowercase().as_str() {
"dark" => Theme::Dark,
"light" => Theme::Light,
_ => Theme::System,
}
}
pub fn label(self) -> &'static str {
match self {
Theme::System => "System",
Theme::Dark => "Dark",
Theme::Light => "Light",
}
}
}
pub struct Palette {
pub accent: Color32,
pub encrypted: Color32,
pub success: Color32,
pub warn: Color32,
pub error: Color32,
pub text: Color32,
pub text_dim: Color32,
pub bg: Color32,
pub panel: Color32,
pub select: Color32,
}
static DARK: Palette = Palette {
accent: Color32::from_rgb(0x4c, 0xc2, 0xff),
encrypted: Color32::from_rgb(0xc9, 0x8b, 0xff),
success: Color32::from_rgb(0x4a, 0xde, 0x80),
warn: Color32::from_rgb(0xfa, 0xcc, 0x15),
error: Color32::from_rgb(0xff, 0x6b, 0x6b),
text: Color32::from_rgb(0xf2, 0xf3, 0xf5),
text_dim: Color32::from_rgb(0xb4, 0xb8, 0xc4),
bg: Color32::from_rgb(0x0b, 0x0b, 0x0f),
panel: Color32::from_rgb(0x15, 0x16, 0x1c),
select: Color32::from_rgb(0x26, 0x31, 0x4a),
};
static LIGHT: Palette = Palette {
accent: Color32::from_rgb(0x03, 0x69, 0xa1),
encrypted: Color32::from_rgb(0x7e, 0x22, 0xce),
success: Color32::from_rgb(0x15, 0x80, 0x3d),
warn: Color32::from_rgb(0xb4, 0x53, 0x09),
error: Color32::from_rgb(0xb9, 0x1c, 0x1c),
text: Color32::from_rgb(0x14, 0x15, 0x1a),
text_dim: Color32::from_rgb(0x52, 0x55, 0x5e),
bg: Color32::from_rgb(0xf4, 0xf5, 0xf7),
panel: Color32::from_rgb(0xff, 0xff, 0xff),
select: Color32::from_rgb(0xcf, 0xe4, 0xff),
};
static CURRENT: AtomicU8 = AtomicU8::new(0);
static CHOICE: AtomicU8 = AtomicU8::new(0);
pub fn current() -> Theme {
match CURRENT.load(Ordering::Relaxed) {
1 => Theme::Light,
_ => Theme::Dark,
}
}
pub fn set_effective(t: Theme) {
CURRENT.store(if t == Theme::Light { 1 } else { 0 }, Ordering::Relaxed);
}
pub fn choice() -> Theme {
match CHOICE.load(Ordering::Relaxed) {
1 => Theme::Dark,
2 => Theme::Light,
_ => Theme::System,
}
}
pub fn set_choice(t: Theme) {
CHOICE.store(
match t {
Theme::System => 0,
Theme::Dark => 1,
Theme::Light => 2,
},
Ordering::Relaxed,
);
}
pub fn resolve(ctx: &egui::Context) -> Theme {
match choice() {
Theme::Dark => Theme::Dark,
Theme::Light => Theme::Light,
Theme::System => match ctx.system_theme() {
Some(egui::Theme::Light) => Theme::Light,
_ => Theme::Dark,
},
}
}
pub fn apply(ctx: &egui::Context) {
set_effective(resolve(ctx));
install(ctx);
}
pub fn palette() -> &'static Palette {
match current() {
Theme::Light => &LIGHT,
_ => &DARK,
}
}
pub fn install(ctx: &egui::Context) {
let theme = current();
let p = palette();
let mut v = match theme {
Theme::Light => egui::Visuals::light(),
_ => egui::Visuals::dark(),
};
v.panel_fill = p.panel;
v.window_fill = p.panel;
v.extreme_bg_color = p.bg;
v.faint_bg_color = match theme {
Theme::Light => Color32::from_rgb(0xec, 0xee, 0xf2),
_ => Color32::from_rgb(0x1c, 0x1c, 0x24),
};
v.override_text_color = Some(p.text);
v.hyperlink_color = p.accent;
v.selection.bg_fill = p.select;
v.selection.stroke = egui::Stroke::new(1.0, p.accent);
v.widgets.hovered.bg_fill = match theme {
Theme::Light => Color32::from_rgb(0xe3, 0xe7, 0xef),
_ => Color32::from_rgb(0x22, 0x26, 0x33),
};
v.widgets.active.bg_fill = p.select;
ctx.set_visuals(v);
let mut style = (*ctx.global_style()).clone();
style.spacing.item_spacing = egui::vec2(8.0, 6.0);
style.spacing.button_padding = egui::vec2(8.0, 4.0);
style.spacing.window_margin = egui::Margin::same(10);
ctx.set_global_style(style);
}
#[cfg(test)]
mod tests {
use super::Theme;
#[test]
fn default_is_system() {
assert_eq!(Theme::default(), Theme::System);
}
#[test]
fn from_str_pins_explicit_and_defaults_to_system() {
assert_eq!(Theme::from_str("dark"), Theme::Dark);
assert_eq!(Theme::from_str("light"), Theme::Light);
assert_eq!(Theme::from_str("system"), Theme::System);
assert_eq!(Theme::from_str(""), Theme::System);
assert_eq!(Theme::from_str(" "), Theme::System);
assert_eq!(Theme::from_str("nonsense"), Theme::System);
assert_eq!(Theme::from_str(" DARK "), Theme::Dark);
assert_eq!(Theme::from_str("Light"), Theme::Light);
}
#[test]
fn as_str_round_trips_and_labels_present() {
for t in [Theme::System, Theme::Dark, Theme::Light] {
assert_eq!(Theme::from_str(t.as_str()), t);
assert!(!t.label().is_empty());
}
}
}