dampen_core/ir/
theme.rs

1//! Theming system types for Dampen UI framework
2//!
3//! This module defines the IR types for theme definitions, color palettes,
4//! typography, spacing scales, and style classes with inheritance.
5//! All types are backend-agnostic and serializable.
6
7use super::layout::LayoutConstraints;
8use super::style::{Color, StyleProperties};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12/// Theme definition containing all visual properties
13#[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    /// Default styles per widget type
20    pub base_styles: HashMap<String, StyleProperties>,
21}
22
23impl Theme {
24    /// Validate theme
25    ///
26    /// Returns an error if:
27    /// - Palette colors are invalid
28    /// - Typography values are invalid
29    /// - Spacing unit is non-positive
30    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/// Theme color palette
46#[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/// Typography configuration
75#[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/// Font weight
116#[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    /// Parse from string
128    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    /// Convert to CSS numeric value
144    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/// Spacing scale configuration
157#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
158pub struct SpacingScale {
159    /// Base spacing unit (e.g., 4px)
160    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    /// Get spacing for a multiplier
172    pub fn get(&self, multiplier: u8) -> f32 {
173        self.unit * multiplier as f32
174    }
175}
176
177/// Style class definition
178#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
179pub struct StyleClass {
180    pub name: String,
181    pub style: StyleProperties,
182    pub layout: Option<LayoutConstraints>,
183    /// Inherit from other classes
184    pub extends: Vec<String>,
185    /// State-specific overrides (single states)
186    pub state_variants: HashMap<WidgetState, StyleProperties>,
187    /// Combined state overrides (e.g., hover:active)
188    #[serde(default)]
189    pub combined_state_variants: HashMap<StateSelector, StyleProperties>,
190}
191
192impl StyleClass {
193    /// Validate class definition
194    ///
195    /// Returns an error if:
196    /// - Style properties are invalid
197    /// - Layout constraints are invalid
198    /// - Circular dependency detected
199    /// - Inheritance depth exceeds 5 levels
200    /// - Referenced parent classes don't exist
201    pub fn validate(&self, all_classes: &HashMap<String, StyleClass>) -> Result<(), String> {
202        // Validate own properties
203        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        // Check inheritance depth
214        self.check_inheritance_depth(all_classes, 0)?;
215
216        // Check for circular dependencies
217        self.check_circular_dependency(all_classes, &mut Vec::new())?;
218
219        // Validate state variants
220        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        // Validate combined state variants
227        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        // Verify all extended classes exist
234        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/// Widget interaction state
291#[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    /// Parse from string prefix
301    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/// State selector for style matching - can be single or combined states
313#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
314pub enum StateSelector {
315    /// Single state (e.g., "hover")
316    Single(WidgetState),
317    /// Combined states that must all be active (e.g., "hover:active")
318    /// Sorted for consistent comparison
319    Combined(Vec<WidgetState>),
320}
321
322impl StateSelector {
323    /// Create a single state selector
324    pub fn single(state: WidgetState) -> Self {
325        StateSelector::Single(state)
326    }
327
328    /// Create a combined state selector from multiple states
329    pub fn combined(mut states: Vec<WidgetState>) -> Self {
330        if states.len() == 1 {
331            StateSelector::Single(states[0])
332        } else {
333            // Sort for consistent comparison
334            states.sort();
335            states.dedup(); // Remove duplicates
336            StateSelector::Combined(states)
337        }
338    }
339
340    /// Check if this selector matches the given active states
341    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    /// Get specificity for cascade resolution (more specific = higher number)
351    pub fn specificity(&self) -> usize {
352        match self {
353            StateSelector::Single(_) => 1,
354            StateSelector::Combined(states) => states.len(),
355        }
356    }
357}