use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Color {
pub r: u8,
pub g: u8,
pub b: u8,
}
impl Color {
pub const fn new(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b }
}
pub fn from_hex(hex: &str) -> Option<Self> {
let hex = hex.trim_start_matches('#');
if hex.len() != 6 {
return None;
}
let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
Some(Self { r, g, b })
}
pub fn to_hex(&self) -> String {
format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
}
fn linearize(val: u8) -> f64 {
let v = val as f64 / 255.0;
if v <= 0.03928 {
v / 12.92
} else {
((v + 0.055) / 1.055).powf(2.4)
}
}
pub fn luminance(&self) -> f64 {
0.2126 * Self::linearize(self.r)
+ 0.7152 * Self::linearize(self.g)
+ 0.0722 * Self::linearize(self.b)
}
pub fn contrast_ratio(&self, other: &Color) -> f64 {
let l1 = self.luminance();
let l2 = other.luminance();
let lighter = l1.max(l2);
let darker = l1.min(l2);
(lighter + 0.05) / (darker + 0.05)
}
}
impl Default for Color {
fn default() -> Self {
Self::new(128, 128, 128)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ThemeColors {
pub bg: Color,
pub dialog_bg: Color,
pub fg: Color,
pub accent: Color,
pub accent_secondary: Color,
pub highlight: Color,
pub muted: Color,
pub success: Color,
pub warning: Color,
pub danger: Color,
pub border: Color,
pub selection_bg: Color,
pub selection_fg: Color,
pub graph_line: Color,
}
impl Default for ThemeColors {
fn default() -> Self {
Self {
bg: Color::from_hex("#16161e").unwrap(),
dialog_bg: Color::from_hex("#23232d").unwrap(),
fg: Color::from_hex("#e6e6f0").unwrap(),
accent: Color::from_hex("#8ab4f8").unwrap(),
accent_secondary: Color::from_hex("#bb86fc").unwrap(),
highlight: Color::from_hex("#ffcb6b").unwrap(),
muted: Color::from_hex("#80808c").unwrap(),
success: Color::from_hex("#81c784").unwrap(),
warning: Color::from_hex("#ffb74d").unwrap(),
danger: Color::from_hex("#ef5350").unwrap(),
border: Color::from_hex("#3c3c50").unwrap(),
selection_bg: Color::from_hex("#323246").unwrap(),
selection_fg: Color::from_hex("#ffffff").unwrap(),
graph_line: Color::from_hex("#8ab4f8").unwrap(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ThemeVariants {
pub dark: Option<ThemeColors>,
pub light: Option<ThemeColors>,
}
#[derive(Debug, Clone)]
pub struct NamedTheme {
pub id: String,
pub name: String,
pub variants: ThemeVariants,
pub is_builtin: bool,
}
impl NamedTheme {
pub fn get_colors(&self, is_dark: bool) -> ThemeColors {
if is_dark {
self.variants
.dark
.or(self.variants.light)
.expect("Theme must have at least one variant")
} else {
self.variants
.light
.or(self.variants.dark)
.expect("Theme must have at least one variant")
}
}
pub fn has_dark(&self) -> bool {
self.variants.dark.is_some()
}
pub fn has_light(&self) -> bool {
self.variants.light.is_some()
}
pub fn variants_label(&self) -> &'static str {
match (self.has_dark(), self.has_light()) {
(true, true) => "dark + light",
(true, false) => "dark only",
(false, true) => "light only",
_ => "unknown",
}
}
}