1use super::layout::LayoutConstraints;
8use super::style::{Color, StyleProperties};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14pub struct Theme {
15 pub name: String,
16 pub palette: ThemePalette,
17 pub typography: Typography,
18 pub spacing: SpacingScale,
19 pub base_styles: HashMap<String, StyleProperties>,
21}
22
23impl Theme {
24 pub fn validate(&self) -> Result<(), String> {
31 self.palette.validate()?;
32 self.typography.validate()?;
33 self.spacing.validate()?;
34
35 for (widget_type, style) in &self.base_styles {
36 style
37 .validate()
38 .map_err(|e| format!("Invalid base style for '{}': {}", widget_type, e))?;
39 }
40
41 Ok(())
42 }
43}
44
45#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
47pub struct ThemePalette {
48 pub primary: Color,
49 pub secondary: Color,
50 pub success: Color,
51 pub warning: Color,
52 pub danger: Color,
53 pub background: Color,
54 pub surface: Color,
55 pub text: Color,
56 pub text_secondary: Color,
57}
58
59impl ThemePalette {
60 pub fn validate(&self) -> Result<(), String> {
61 self.primary.validate()?;
62 self.secondary.validate()?;
63 self.success.validate()?;
64 self.warning.validate()?;
65 self.danger.validate()?;
66 self.background.validate()?;
67 self.surface.validate()?;
68 self.text.validate()?;
69 self.text_secondary.validate()?;
70 Ok(())
71 }
72}
73
74#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
76pub struct Typography {
77 pub font_family: String,
78 pub font_size_base: f32,
79 pub font_size_small: f32,
80 pub font_size_large: f32,
81 pub font_weight: FontWeight,
82 pub line_height: f32,
83}
84
85impl Typography {
86 pub fn validate(&self) -> Result<(), String> {
87 if self.font_size_base <= 0.0 {
88 return Err(format!(
89 "font_size_base must be positive, got {}",
90 self.font_size_base
91 ));
92 }
93 if self.font_size_small <= 0.0 {
94 return Err(format!(
95 "font_size_small must be positive, got {}",
96 self.font_size_small
97 ));
98 }
99 if self.font_size_large <= 0.0 {
100 return Err(format!(
101 "font_size_large must be positive, got {}",
102 self.font_size_large
103 ));
104 }
105 if self.line_height <= 0.0 {
106 return Err(format!(
107 "line_height must be positive, got {}",
108 self.line_height
109 ));
110 }
111 Ok(())
112 }
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
117pub enum FontWeight {
118 Thin,
119 Light,
120 Normal,
121 Medium,
122 Bold,
123 Black,
124}
125
126impl FontWeight {
127 pub fn parse(s: &str) -> Result<Self, String> {
129 match s.trim().to_lowercase().as_str() {
130 "thin" => Ok(FontWeight::Thin),
131 "light" => Ok(FontWeight::Light),
132 "normal" => Ok(FontWeight::Normal),
133 "medium" => Ok(FontWeight::Medium),
134 "bold" => Ok(FontWeight::Bold),
135 "black" => Ok(FontWeight::Black),
136 _ => Err(format!(
137 "Invalid font weight: '{}'. Expected thin, light, normal, medium, bold, or black",
138 s
139 )),
140 }
141 }
142
143 pub fn to_css(&self) -> u16 {
145 match self {
146 FontWeight::Thin => 100,
147 FontWeight::Light => 300,
148 FontWeight::Normal => 400,
149 FontWeight::Medium => 500,
150 FontWeight::Bold => 700,
151 FontWeight::Black => 900,
152 }
153 }
154}
155
156#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
158pub struct SpacingScale {
159 pub unit: f32,
161}
162
163impl SpacingScale {
164 pub fn validate(&self) -> Result<(), String> {
165 if self.unit <= 0.0 {
166 return Err(format!("spacing unit must be positive, got {}", self.unit));
167 }
168 Ok(())
169 }
170
171 pub fn get(&self, multiplier: u8) -> f32 {
173 self.unit * multiplier as f32
174 }
175}
176
177#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
179pub struct StyleClass {
180 pub name: String,
181 pub style: StyleProperties,
182 pub layout: Option<LayoutConstraints>,
183 pub extends: Vec<String>,
185 pub state_variants: HashMap<WidgetState, StyleProperties>,
187 #[serde(default)]
189 pub combined_state_variants: HashMap<StateSelector, StyleProperties>,
190}
191
192impl StyleClass {
193 pub fn validate(&self, all_classes: &HashMap<String, StyleClass>) -> Result<(), String> {
202 self.style
204 .validate()
205 .map_err(|e| format!("Invalid style: {}", e))?;
206
207 if let Some(layout) = &self.layout {
208 layout
209 .validate()
210 .map_err(|e| format!("Invalid layout: {}", e))?;
211 }
212
213 self.check_inheritance_depth(all_classes, 0)?;
215
216 self.check_circular_dependency(all_classes, &mut Vec::new())?;
218
219 for (state, style) in &self.state_variants {
221 style
222 .validate()
223 .map_err(|e| format!("Invalid style for state {:?}: {}", state, e))?;
224 }
225
226 for (selector, style) in &self.combined_state_variants {
228 style
229 .validate()
230 .map_err(|e| format!("Invalid style for state selector {:?}: {}", selector, e))?;
231 }
232
233 for parent in &self.extends {
235 if !all_classes.contains_key(parent) {
236 return Err(format!("Parent class '{}' not found", parent));
237 }
238 }
239
240 Ok(())
241 }
242
243 fn check_inheritance_depth(
244 &self,
245 all_classes: &HashMap<String, StyleClass>,
246 depth: u8,
247 ) -> Result<(), String> {
248 if depth > 5 {
249 return Err(format!(
250 "Style class inheritance depth exceeds 5 levels (class: {})",
251 self.name
252 ));
253 }
254
255 for parent_name in &self.extends {
256 if let Some(parent) = all_classes.get(parent_name) {
257 parent.check_inheritance_depth(all_classes, depth + 1)?;
258 }
259 }
260
261 Ok(())
262 }
263
264 fn check_circular_dependency(
265 &self,
266 all_classes: &HashMap<String, StyleClass>,
267 path: &mut Vec<String>,
268 ) -> Result<(), String> {
269 if path.contains(&self.name) {
270 let chain = path.join(" → ");
271 return Err(format!(
272 "Circular style class dependency detected: {} → {}",
273 chain, self.name
274 ));
275 }
276
277 path.push(self.name.clone());
278
279 for parent_name in &self.extends {
280 if let Some(parent) = all_classes.get(parent_name) {
281 parent.check_circular_dependency(all_classes, path)?;
282 }
283 }
284
285 path.pop();
286 Ok(())
287 }
288}
289
290#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
292pub enum WidgetState {
293 Hover,
294 Focus,
295 Active,
296 Disabled,
297}
298
299impl WidgetState {
300 pub fn from_prefix(s: &str) -> Option<Self> {
302 match s.trim().to_lowercase().as_str() {
303 "hover" => Some(WidgetState::Hover),
304 "focus" => Some(WidgetState::Focus),
305 "active" => Some(WidgetState::Active),
306 "disabled" => Some(WidgetState::Disabled),
307 _ => None,
308 }
309 }
310}
311
312#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
314pub enum StateSelector {
315 Single(WidgetState),
317 Combined(Vec<WidgetState>),
320}
321
322impl StateSelector {
323 pub fn single(state: WidgetState) -> Self {
325 StateSelector::Single(state)
326 }
327
328 pub fn combined(mut states: Vec<WidgetState>) -> Self {
330 if states.len() == 1 {
331 StateSelector::Single(states[0])
332 } else {
333 states.sort();
335 states.dedup(); StateSelector::Combined(states)
337 }
338 }
339
340 pub fn matches(&self, active_states: &[WidgetState]) -> bool {
342 match self {
343 StateSelector::Single(state) => active_states.contains(state),
344 StateSelector::Combined(required_states) => {
345 required_states.iter().all(|s| active_states.contains(s))
346 }
347 }
348 }
349
350 pub fn specificity(&self) -> usize {
352 match self {
353 StateSelector::Single(_) => 1,
354 StateSelector::Combined(states) => states.len(),
355 }
356 }
357}