1use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15pub struct Color {
16 pub r: u8,
17 pub g: u8,
18 pub b: u8,
19}
20
21impl Color {
22 pub const fn new(r: u8, g: u8, b: u8) -> Self {
24 Self { r, g, b }
25 }
26
27 pub fn from_hex(hex: &str) -> Option<Self> {
29 let hex = hex.trim_start_matches('#');
30 if hex.len() != 6 {
31 return None;
32 }
33 let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
34 let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
35 let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
36 Some(Self { r, g, b })
37 }
38
39 pub fn to_hex(&self) -> String {
41 format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
42 }
43
44 fn linearize(val: u8) -> f64 {
46 let v = val as f64 / 255.0;
47 if v <= 0.03928 {
48 v / 12.92
49 } else {
50 ((v + 0.055) / 1.055).powf(2.4)
51 }
52 }
53
54 pub fn luminance(&self) -> f64 {
56 0.2126 * Self::linearize(self.r)
57 + 0.7152 * Self::linearize(self.g)
58 + 0.0722 * Self::linearize(self.b)
59 }
60
61 pub fn contrast_ratio(&self, other: &Color) -> f64 {
63 let l1 = self.luminance();
64 let l2 = other.luminance();
65 let lighter = l1.max(l2);
66 let darker = l1.min(l2);
67 (lighter + 0.05) / (darker + 0.05)
68 }
69}
70
71impl Default for Color {
72 fn default() -> Self {
73 Self::new(128, 128, 128)
74 }
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub struct ThemeColors {
82 pub bg: Color,
84 pub dialog_bg: Color,
86 pub fg: Color,
88 pub accent: Color,
90 pub accent_secondary: Color,
92 pub highlight: Color,
94 pub muted: Color,
96 pub success: Color,
98 pub warning: Color,
100 pub danger: Color,
102 pub border: Color,
104 pub selection_bg: Color,
106 pub selection_fg: Color,
108 pub graph_line: Color,
110}
111
112impl Default for ThemeColors {
113 fn default() -> Self {
114 Self {
116 bg: Color::from_hex("#16161e").unwrap(),
117 dialog_bg: Color::from_hex("#23232d").unwrap(),
118 fg: Color::from_hex("#e6e6f0").unwrap(),
119 accent: Color::from_hex("#8ab4f8").unwrap(),
120 accent_secondary: Color::from_hex("#bb86fc").unwrap(),
121 highlight: Color::from_hex("#ffcb6b").unwrap(),
122 muted: Color::from_hex("#80808c").unwrap(),
123 success: Color::from_hex("#81c784").unwrap(),
124 warning: Color::from_hex("#ffb74d").unwrap(),
125 danger: Color::from_hex("#ef5350").unwrap(),
126 border: Color::from_hex("#3c3c50").unwrap(),
127 selection_bg: Color::from_hex("#323246").unwrap(),
128 selection_fg: Color::from_hex("#ffffff").unwrap(),
129 graph_line: Color::from_hex("#8ab4f8").unwrap(),
130 }
131 }
132}
133
134#[derive(Debug, Clone, Default)]
136pub struct ThemeVariants {
137 pub dark: Option<ThemeColors>,
138 pub light: Option<ThemeColors>,
139}
140
141#[derive(Debug, Clone)]
143pub struct NamedTheme {
144 pub id: String,
146 pub name: String,
148 pub variants: ThemeVariants,
150 pub is_builtin: bool,
152}
153
154impl NamedTheme {
155 pub fn get_colors(&self, is_dark: bool) -> ThemeColors {
157 if is_dark {
158 self.variants
159 .dark
160 .or(self.variants.light)
161 .expect("Theme must have at least one variant")
162 } else {
163 self.variants
164 .light
165 .or(self.variants.dark)
166 .expect("Theme must have at least one variant")
167 }
168 }
169
170 pub fn has_dark(&self) -> bool {
172 self.variants.dark.is_some()
173 }
174
175 pub fn has_light(&self) -> bool {
177 self.variants.light.is_some()
178 }
179
180 pub fn variants_label(&self) -> &'static str {
182 match (self.has_dark(), self.has_light()) {
183 (true, true) => "dark + light",
184 (true, false) => "dark only",
185 (false, true) => "light only",
186 _ => "unknown",
187 }
188 }
189}