use egui::{Color32, Context, Id, Ui, Visuals};
#[derive(Clone, Copy, Debug)]
pub struct Theme {
pub name: &'static str,
pub bg: Color32,
pub node_fill: Color32,
pub node_stroke: Color32,
pub edge: Color32,
pub text: Color32,
pub text_dim: Color32,
pub accent: Color32,
pub point: Color32,
pub panel_bg: Color32,
pub panel_stroke: Color32,
pub glow: Color32,
}
impl Default for Theme {
fn default() -> Self {
Self {
name: "default",
bg: Color32::from_rgb(18, 18, 24),
node_fill: Color32::from_rgb(30, 30, 40),
node_stroke: Color32::from_gray(120),
edge: Color32::from_gray(90),
text: Color32::from_gray(220),
text_dim: Color32::from_gray(150),
accent: Color32::from_rgb(120, 210, 255),
point: Color32::from_rgb(90, 200, 140),
panel_bg: Color32::from_rgba_unmultiplied(16, 26, 42, 236),
panel_stroke: Color32::from_rgb(80, 130, 180),
glow: Color32::from_rgb(120, 210, 255),
}
}
}
impl Theme {
pub const ALL: &'static [fn() -> Theme] = &[
Theme::default,
Theme::sci_fi,
Theme::nordic_aurora,
Theme::cyberpunk_neon,
Theme::amber_crt,
Theme::deep_space,
Theme::hugin_noir,
];
pub fn names() -> Vec<&'static str> {
Self::ALL.iter().map(|ctor| ctor().name).collect()
}
pub fn by_name(name: &str) -> Option<Theme> {
let norm = |s: &str| s.to_ascii_lowercase().replace(['-', ' '], "_");
let want = norm(name);
Self::ALL.iter().map(|ctor| ctor()).find(|t| norm(t.name) == want)
}
pub fn sci_fi() -> Self {
Self {
name: "sci-fi",
bg: Color32::from_rgb(6, 10, 18),
node_fill: Color32::from_rgb(14, 22, 38),
node_stroke: Color32::from_rgb(0, 200, 220),
edge: Color32::from_rgb(60, 110, 170),
text: Color32::from_rgb(180, 235, 255),
text_dim: Color32::from_rgb(90, 130, 175),
accent: Color32::from_rgb(0, 255, 225),
point: Color32::from_rgb(80, 255, 170),
panel_bg: Color32::from_rgba_unmultiplied(8, 16, 28, 240),
panel_stroke: Color32::from_rgb(0, 200, 220),
glow: Color32::from_rgb(0, 255, 225),
}
}
pub fn nordic_aurora() -> Self {
Self {
name: "nordic-aurora",
bg: Color32::from_rgb(10, 18, 28),
node_fill: Color32::from_rgb(18, 32, 44),
node_stroke: Color32::from_rgb(80, 220, 180),
edge: Color32::from_rgb(50, 110, 120),
text: Color32::from_rgb(214, 240, 234),
text_dim: Color32::from_rgb(120, 165, 165),
accent: Color32::from_rgb(120, 230, 200),
point: Color32::from_rgb(150, 130, 240),
panel_bg: Color32::from_rgba_unmultiplied(12, 24, 34, 238),
panel_stroke: Color32::from_rgb(70, 180, 160),
glow: Color32::from_rgb(90, 255, 190),
}
}
pub fn cyberpunk_neon() -> Self {
Self {
name: "cyberpunk-neon",
bg: Color32::from_rgb(14, 8, 22),
node_fill: Color32::from_rgb(26, 14, 38),
node_stroke: Color32::from_rgb(0, 240, 255),
edge: Color32::from_rgb(120, 40, 140),
text: Color32::from_rgb(245, 220, 255),
text_dim: Color32::from_rgb(160, 110, 180),
accent: Color32::from_rgb(255, 50, 200),
point: Color32::from_rgb(0, 240, 255),
panel_bg: Color32::from_rgba_unmultiplied(20, 10, 30, 240),
panel_stroke: Color32::from_rgb(255, 50, 200),
glow: Color32::from_rgb(255, 60, 210),
}
}
pub fn amber_crt() -> Self {
Self {
name: "amber-crt",
bg: Color32::from_rgb(14, 10, 4),
node_fill: Color32::from_rgb(28, 20, 8),
node_stroke: Color32::from_rgb(255, 176, 64),
edge: Color32::from_rgb(120, 80, 24),
text: Color32::from_rgb(255, 200, 110),
text_dim: Color32::from_rgb(170, 120, 50),
accent: Color32::from_rgb(255, 176, 64),
point: Color32::from_rgb(255, 214, 130),
panel_bg: Color32::from_rgba_unmultiplied(20, 14, 6, 240),
panel_stroke: Color32::from_rgb(200, 130, 40),
glow: Color32::from_rgb(255, 190, 90),
}
}
pub fn deep_space() -> Self {
Self {
name: "deep-space",
bg: Color32::from_rgb(6, 7, 16),
node_fill: Color32::from_rgb(16, 18, 34),
node_stroke: Color32::from_rgb(130, 150, 255),
edge: Color32::from_rgb(54, 60, 110),
text: Color32::from_rgb(226, 230, 248),
text_dim: Color32::from_rgb(128, 138, 180),
accent: Color32::from_rgb(150, 130, 255),
point: Color32::from_rgb(110, 200, 255),
panel_bg: Color32::from_rgba_unmultiplied(10, 12, 26, 240),
panel_stroke: Color32::from_rgb(90, 100, 190),
glow: Color32::from_rgb(170, 140, 255),
}
}
pub fn hugin_noir() -> Self {
Self {
name: "hugin-noir",
bg: Color32::from_rgb(10, 10, 11),
node_fill: Color32::from_rgb(22, 22, 24),
node_stroke: Color32::from_rgb(232, 226, 214), edge: Color32::from_rgb(70, 70, 74),
text: Color32::from_rgb(236, 230, 218), text_dim: Color32::from_rgb(140, 138, 134),
accent: Color32::from_rgb(196, 30, 38), point: Color32::from_rgb(196, 30, 38),
panel_bg: Color32::from_rgba_unmultiplied(16, 16, 17, 242),
panel_stroke: Color32::from_rgb(150, 24, 30),
glow: Color32::from_rgb(220, 40, 48),
}
}
pub fn visuals(&self) -> Visuals {
let mut v = Visuals::dark();
v.override_text_color = Some(self.text);
v.hyperlink_color = self.accent;
v.panel_fill = self.bg;
v.window_fill = self.panel_bg;
v.extreme_bg_color = self.bg;
v.faint_bg_color = self.node_fill;
v.selection.bg_fill = self.accent.linear_multiply(0.35);
v.selection.stroke.color = self.accent;
v.widgets.noninteractive.bg_fill = self.node_fill;
v.widgets.inactive.bg_fill = self.node_fill;
v.widgets.hovered.bg_stroke.color = self.accent;
v.widgets.active.bg_stroke.color = self.accent;
v
}
}
const THEME_ID: &str = "facett_theme";
pub fn set_theme(ctx: &Context, theme: Theme) {
ctx.set_visuals(theme.visuals());
ctx.data_mut(|d| d.insert_temp(Id::new(THEME_ID), theme));
}
pub fn theme(ui: &Ui) -> Theme {
let t = ui.data(|d| d.get_temp::<Theme>(Id::new(THEME_ID))).unwrap_or_default();
probe::record(t.name);
t
}
pub mod probe {
use std::cell::Cell;
thread_local! {
static PAINTED: Cell<Option<&'static str>> = const { Cell::new(None) };
}
pub fn record(name: &'static str) {
PAINTED.with(|p| p.set(Some(name)));
}
pub fn reset() {
PAINTED.with(|p| p.set(None));
}
pub fn painted() -> Option<&'static str> {
PAINTED.with(|p| p.get())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn set_and_read_theme_round_trips() {
let ctx = Context::default();
set_theme(&ctx, Theme::sci_fi());
let mut got = "";
let _ = ctx.run(egui::RawInput::default(), |ctx| {
egui::CentralPanel::default().show(ctx, |ui| {
got = theme(ui).name;
});
});
assert_eq!(got, "sci-fi");
}
#[test]
fn all_themes_have_unique_names_and_are_lookupable() {
let names = Theme::names();
assert_eq!(names.len(), Theme::ALL.len());
let mut sorted = names.clone();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(sorted.len(), names.len(), "theme names must be unique");
assert!(names.contains(&"default"));
assert!(names.contains(&"sci-fi"));
for n in &names {
assert_eq!(Theme::by_name(n).map(|t| t.name), Some(*n));
}
assert_eq!(Theme::by_name("Nordic Aurora").map(|t| t.name), Some("nordic-aurora"));
assert_eq!(Theme::by_name("AMBER_CRT").map(|t| t.name), Some("amber-crt"));
assert!(Theme::by_name("nonesuch").is_none());
}
}