1#[derive(Debug, Clone)]
8pub struct ThemeDefinition {
9 pub name: String,
11 pub display_name: String,
13 pub base: ThemeBase,
15 pub colors: ThemeColors,
17}
18
19impl ThemeDefinition {
20 pub fn new(name: impl Into<String>, display_name: impl Into<String>, base: ThemeBase) -> Self {
22 Self {
23 name: name.into(),
24 display_name: display_name.into(),
25 base,
26 colors: ThemeColors::default(),
27 }
28 }
29
30 pub fn with_backgrounds(
32 mut self,
33 base: Option<u32>,
34 surface: Option<u32>,
35 elevated: Option<u32>,
36 ) -> Self {
37 self.colors.bg_base = base;
38 self.colors.bg_surface = surface;
39 self.colors.bg_elevated = elevated;
40 self
41 }
42
43 pub fn with_text(
45 mut self,
46 primary: Option<u32>,
47 secondary: Option<u32>,
48 muted: Option<u32>,
49 ) -> Self {
50 self.colors.text_primary = primary;
51 self.colors.text_secondary = secondary;
52 self.colors.text_muted = muted;
53 self
54 }
55
56 pub fn with_accents(
58 mut self,
59 primary: Option<u32>,
60 hover: Option<u32>,
61 muted: Option<u32>,
62 ) -> Self {
63 self.colors.accent_primary = primary;
64 self.colors.accent_hover = hover;
65 self.colors.accent_muted = muted;
66 self
67 }
68
69 pub fn with_borders(mut self, subtle: Option<u32>, strong: Option<u32>) -> Self {
71 self.colors.border_subtle = subtle;
72 self.colors.border_strong = strong;
73 self
74 }
75
76 pub fn with_semantic(
78 mut self,
79 success: Option<u32>,
80 warning: Option<u32>,
81 error: Option<u32>,
82 info: Option<u32>,
83 ) -> Self {
84 self.colors.success = success;
85 self.colors.warning = warning;
86 self.colors.error = error;
87 self.colors.info = info;
88 self
89 }
90
91 pub fn with_chart_palette(mut self, palette: Vec<u32>) -> Self {
93 self.colors.chart_palette = palette;
94 self
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
100pub enum ThemeBase {
101 #[default]
103 Dark,
104 Light,
106}
107
108impl ThemeBase {
109 pub fn parse(s: &str) -> Self {
111 match s.to_lowercase().as_str() {
112 "light" | "l" => Self::Light,
113 _ => Self::Dark,
114 }
115 }
116}
117
118#[derive(Debug, Clone, Default)]
123pub struct ThemeColors {
124 pub bg_base: Option<u32>,
127 pub bg_surface: Option<u32>,
129 pub bg_elevated: Option<u32>,
131
132 pub text_primary: Option<u32>,
135 pub text_secondary: Option<u32>,
137 pub text_muted: Option<u32>,
139
140 pub accent_primary: Option<u32>,
143 pub accent_hover: Option<u32>,
145 pub accent_muted: Option<u32>,
147
148 pub border_subtle: Option<u32>,
151 pub border_strong: Option<u32>,
153
154 pub success: Option<u32>,
157 pub warning: Option<u32>,
159 pub error: Option<u32>,
161 pub info: Option<u32>,
163
164 pub chart_palette: Vec<u32>,
167}
168
169impl ThemeColors {
170 pub fn parse_hex(s: &str) -> Option<u32> {
172 let hex = s.trim().trim_start_matches('#');
173 if hex.len() != 6 {
174 return None;
175 }
176 u32::from_str_radix(hex, 16).ok()
177 }
178
179 pub fn to_rgb(color: u32) -> (u8, u8, u8) {
181 let r = ((color >> 16) & 0xFF) as u8;
182 let g = ((color >> 8) & 0xFF) as u8;
183 let b = (color & 0xFF) as u8;
184 (r, g, b)
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 #[test]
193 fn test_theme_base_parse() {
194 assert_eq!(ThemeBase::parse("dark"), ThemeBase::Dark);
195 assert_eq!(ThemeBase::parse("light"), ThemeBase::Light);
196 assert_eq!(ThemeBase::parse("l"), ThemeBase::Light);
197 assert_eq!(ThemeBase::parse("LIGHT"), ThemeBase::Light);
198 assert_eq!(ThemeBase::parse("unknown"), ThemeBase::Dark); }
200
201 #[test]
202 fn test_parse_hex() {
203 assert_eq!(ThemeColors::parse_hex("#1a1b26"), Some(0x1a1b26));
204 assert_eq!(ThemeColors::parse_hex("1a1b26"), Some(0x1a1b26));
205 assert_eq!(ThemeColors::parse_hex("#FFFFFF"), Some(0xFFFFFF));
206 assert_eq!(ThemeColors::parse_hex("invalid"), None);
207 assert_eq!(ThemeColors::parse_hex("#12345"), None); }
209
210 #[test]
211 fn test_to_rgb() {
212 assert_eq!(ThemeColors::to_rgb(0xFF0000), (255, 0, 0));
213 assert_eq!(ThemeColors::to_rgb(0x00FF00), (0, 255, 0));
214 assert_eq!(ThemeColors::to_rgb(0x0000FF), (0, 0, 255));
215 assert_eq!(ThemeColors::to_rgb(0x1a1b26), (26, 27, 38));
216 }
217
218 #[test]
219 fn test_theme_definition_builder() {
220 let theme = ThemeDefinition::new("tokyo-night", "Tokyo Night", ThemeBase::Dark)
221 .with_backgrounds(Some(0x1a1b26), Some(0x24283b), Some(0x414868))
222 .with_accents(Some(0x7aa2f7), Some(0x89b4fa), None)
223 .with_chart_palette(vec![0x7aa2f7, 0x9ece6a, 0xe0af68]);
224
225 assert_eq!(theme.name, "tokyo-night");
226 assert_eq!(theme.display_name, "Tokyo Night");
227 assert_eq!(theme.base, ThemeBase::Dark);
228 assert_eq!(theme.colors.bg_base, Some(0x1a1b26));
229 assert_eq!(theme.colors.accent_primary, Some(0x7aa2f7));
230 assert_eq!(theme.colors.chart_palette.len(), 3);
231 }
232
233 #[test]
234 fn test_theme_default() {
235 assert_eq!(ThemeBase::default(), ThemeBase::Dark);
236 }
237}