dampen_core/ir/
style.rs

1//! Styling system types for Dampen UI framework
2//!
3//! This module defines the IR types for visual styling properties including
4//! backgrounds, colors, borders, shadows, opacity, and transforms.
5//! All types are backend-agnostic and serializable.
6
7use serde::{Deserialize, Serialize};
8
9/// Complete style properties for a widget
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
11pub struct StyleProperties {
12    /// Background fill
13    pub background: Option<Background>,
14    /// Foreground/text color
15    pub color: Option<Color>,
16    /// Border styling
17    pub border: Option<Border>,
18    /// Drop shadow
19    pub shadow: Option<Shadow>,
20    /// Opacity (0.0 = transparent, 1.0 = opaque)
21    pub opacity: Option<f32>,
22    /// Visual transformations
23    pub transform: Option<Transform>,
24}
25
26impl StyleProperties {
27    /// Validates all style properties
28    ///
29    /// Returns an error if:
30    /// - Opacity is not in 0.0-1.0 range
31    /// - Colors are invalid
32    pub fn validate(&self) -> Result<(), String> {
33        if let Some(opacity) = self.opacity {
34            if !(0.0..=1.0).contains(&opacity) {
35                return Err(format!("opacity must be 0.0-1.0, got {}", opacity));
36            }
37        }
38
39        if let Some(ref color) = self.color {
40            color.validate()?;
41        }
42
43        if let Some(ref background) = self.background {
44            background.validate()?;
45        }
46
47        if let Some(ref border) = self.border {
48            border.validate()?;
49        }
50
51        Ok(())
52    }
53}
54
55/// Background fill type
56#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
57pub enum Background {
58    /// Solid color
59    Color(Color),
60    /// Gradient fill
61    Gradient(Gradient),
62    /// Image background
63    Image { path: String, fit: ImageFit },
64}
65
66impl Background {
67    pub fn validate(&self) -> Result<(), String> {
68        match self {
69            Background::Color(color) => color.validate(),
70            Background::Gradient(gradient) => gradient.validate(),
71            Background::Image { .. } => Ok(()),
72        }
73    }
74}
75
76/// Image fitting strategy
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
78pub enum ImageFit {
79    Fill,
80    Contain,
81    Cover,
82    ScaleDown,
83}
84
85/// Color representation (RGBA, 0.0-1.0 range)
86#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
87pub struct Color {
88    pub r: f32,
89    pub g: f32,
90    pub b: f32,
91    pub a: f32,
92}
93
94impl Color {
95    /// Parse color from CSS string
96    ///
97    /// Supports:
98    /// - Hex: "#3498db", "#3498dbff"
99    /// - RGB: "rgb(52, 152, 219)", "rgba(52, 152, 219, 0.8)"
100    /// - HSL: "hsl(204, 70%, 53%)", "hsla(204, 70%, 53%, 0.8)"
101    /// - Named: "red", "blue", "transparent"
102    pub fn parse(s: &str) -> Result<Self, String> {
103        let css_color =
104            csscolorparser::parse(s).map_err(|e| format!("Invalid color '{}': {}", s, e))?;
105
106        let [r, g, b, a] = css_color.to_array();
107
108        Ok(Color {
109            r: r as f32,
110            g: g as f32,
111            b: b as f32,
112            a: a as f32,
113        })
114    }
115
116    /// Validate color values
117    pub fn validate(&self) -> Result<(), String> {
118        if self.r < 0.0 || self.r > 1.0 {
119            return Err(format!("Red component out of range: {}", self.r));
120        }
121        if self.g < 0.0 || self.g > 1.0 {
122            return Err(format!("Green component out of range: {}", self.g));
123        }
124        if self.b < 0.0 || self.b > 1.0 {
125            return Err(format!("Blue component out of range: {}", self.b));
126        }
127        if self.a < 0.0 || self.a > 1.0 {
128            return Err(format!("Alpha component out of range: {}", self.a));
129        }
130        Ok(())
131    }
132}
133
134/// Gradient fill
135#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
136pub enum Gradient {
137    Linear {
138        angle: f32,
139        stops: Vec<ColorStop>,
140    },
141    Radial {
142        shape: RadialShape,
143        stops: Vec<ColorStop>,
144    },
145}
146
147impl Gradient {
148    /// Validate gradient
149    ///
150    /// Returns an error if:
151    /// - Less than 2 or more than 8 color stops (Iced limitation)
152    /// - Color stop offsets not sorted or out of range
153    /// - Angle not normalized
154    pub fn validate(&self) -> Result<(), String> {
155        let stops = match self {
156            Gradient::Linear { angle, stops } => {
157                // Normalize angle to 0.0-360.0
158                if *angle < 0.0 || *angle > 360.0 {
159                    return Err(format!("Gradient angle must be 0.0-360.0, got {}", angle));
160                }
161                stops
162            }
163            Gradient::Radial { stops, .. } => stops,
164        };
165
166        if stops.len() < 2 {
167            return Err("Gradient must have at least 2 color stops".to_string());
168        }
169
170        if stops.len() > 8 {
171            return Err(
172                "Gradient cannot have more than 8 color stops (Iced limitation)".to_string(),
173            );
174        }
175
176        let mut last_offset = -1.0;
177        for stop in stops {
178            if stop.offset < 0.0 || stop.offset > 1.0 {
179                return Err(format!(
180                    "Color stop offset must be 0.0-1.0, got {}",
181                    stop.offset
182                ));
183            }
184
185            if stop.offset <= last_offset {
186                return Err("Color stop offsets must be in ascending order".to_string());
187            }
188
189            stop.color.validate()?;
190            last_offset = stop.offset;
191        }
192
193        Ok(())
194    }
195}
196
197/// Color stop for gradients
198#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
199pub struct ColorStop {
200    pub color: Color,
201    /// Offset in gradient (0.0 = start, 1.0 = end)
202    pub offset: f32,
203}
204
205/// Radial gradient shape
206#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
207pub enum RadialShape {
208    Circle,
209    Ellipse,
210}
211
212/// Border styling
213#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
214pub struct Border {
215    pub width: f32,
216    pub color: Color,
217    pub radius: BorderRadius,
218    pub style: BorderStyle,
219}
220
221impl Border {
222    pub fn validate(&self) -> Result<(), String> {
223        if self.width < 0.0 {
224            return Err(format!(
225                "Border width must be non-negative, got {}",
226                self.width
227            ));
228        }
229        self.color.validate()?;
230        self.radius.validate()?;
231        Ok(())
232    }
233}
234
235/// Border radius (corner rounding)
236#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
237pub struct BorderRadius {
238    pub top_left: f32,
239    pub top_right: f32,
240    pub bottom_right: f32,
241    pub bottom_left: f32,
242}
243
244impl BorderRadius {
245    /// Parse from string
246    ///
247    /// # Formats
248    /// - `"<all>"`: All corners (e.g., "8")
249    /// - `"<tl> <tr> <br> <bl>"`: Individual corners
250    pub fn parse(s: &str) -> Result<Self, String> {
251        let parts: Vec<&str> = s.split_whitespace().collect();
252
253        match parts.len() {
254            1 => {
255                let all: f32 = parts[0]
256                    .parse()
257                    .map_err(|_| format!("Invalid border radius: {}", s))?;
258                Ok(BorderRadius {
259                    top_left: all,
260                    top_right: all,
261                    bottom_right: all,
262                    bottom_left: all,
263                })
264            }
265            4 => {
266                let tl: f32 = parts[0]
267                    .parse()
268                    .map_err(|_| format!("Invalid top-left radius: {}", parts[0]))?;
269                let tr: f32 = parts[1]
270                    .parse()
271                    .map_err(|_| format!("Invalid top-right radius: {}", parts[1]))?;
272                let br: f32 = parts[2]
273                    .parse()
274                    .map_err(|_| format!("Invalid bottom-right radius: {}", parts[2]))?;
275                let bl: f32 = parts[3]
276                    .parse()
277                    .map_err(|_| format!("Invalid bottom-left radius: {}", parts[3]))?;
278                Ok(BorderRadius {
279                    top_left: tl,
280                    top_right: tr,
281                    bottom_right: br,
282                    bottom_left: bl,
283                })
284            }
285            _ => Err(format!(
286                "Invalid border radius format: '{}'. Expected 1 or 4 values",
287                s
288            )),
289        }
290    }
291
292    pub fn validate(&self) -> Result<(), String> {
293        if self.top_left < 0.0
294            || self.top_right < 0.0
295            || self.bottom_right < 0.0
296            || self.bottom_left < 0.0
297        {
298            return Err("Border radius values must be non-negative".to_string());
299        }
300        Ok(())
301    }
302}
303
304/// Border line style
305#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
306pub enum BorderStyle {
307    Solid,
308    Dashed,
309    Dotted,
310}
311
312impl BorderStyle {
313    pub fn parse(s: &str) -> Result<Self, String> {
314        match s.trim().to_lowercase().as_str() {
315            "solid" => Ok(BorderStyle::Solid),
316            "dashed" => Ok(BorderStyle::Dashed),
317            "dotted" => Ok(BorderStyle::Dotted),
318            _ => Err(format!(
319                "Invalid border style: '{}'. Expected solid, dashed, or dotted",
320                s
321            )),
322        }
323    }
324}
325
326/// Drop shadow
327#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
328pub struct Shadow {
329    pub offset_x: f32,
330    pub offset_y: f32,
331    pub blur_radius: f32,
332    pub color: Color,
333}
334
335impl Shadow {
336    /// Parse from string format: "offset_x offset_y blur color"
337    ///
338    /// # Example
339    /// ```rust
340    /// use dampen_core::ir::style::Shadow;
341    ///
342    /// let shadow = Shadow::parse("2 2 4 #00000040").unwrap();
343    /// assert_eq!(shadow.offset_x, 2.0);
344    /// assert_eq!(shadow.offset_y, 2.0);
345    /// assert_eq!(shadow.blur_radius, 4.0);
346    /// ```
347    pub fn parse(s: &str) -> Result<Self, String> {
348        let parts: Vec<&str> = s.split_whitespace().collect();
349
350        if parts.len() < 4 {
351            return Err(format!(
352                "Invalid shadow format: '{}'. Expected: offset_x offset_y blur color",
353                s
354            ));
355        }
356
357        let offset_x: f32 = parts[0]
358            .parse()
359            .map_err(|_| format!("Invalid offset_x: {}", parts[0]))?;
360        let offset_y: f32 = parts[1]
361            .parse()
362            .map_err(|_| format!("Invalid offset_y: {}", parts[1]))?;
363        let blur_radius: f32 = parts[2]
364            .parse()
365            .map_err(|_| format!("Invalid blur_radius: {}", parts[2]))?;
366
367        // Color is everything after the first 3 parts
368        let color_str = parts[3..].join(" ");
369        let color = Color::parse(&color_str)?;
370
371        Ok(Shadow {
372            offset_x,
373            offset_y,
374            blur_radius,
375            color,
376        })
377    }
378}
379
380/// Visual transformation
381#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
382pub enum Transform {
383    /// Uniform scale
384    Scale(f32),
385    /// Non-uniform scale
386    ScaleXY { x: f32, y: f32 },
387    /// Rotation in degrees
388    Rotate(f32),
389    /// Translation in pixels
390    Translate { x: f32, y: f32 },
391    /// Multiple composed transforms
392    Multiple(Vec<Transform>),
393}
394
395impl Transform {
396    /// Parse from string
397    ///
398    /// # Examples
399    /// ```rust
400    /// use dampen_core::ir::style::Transform;
401    ///
402    /// assert_eq!(Transform::parse("scale(1.2)"), Ok(Transform::Scale(1.2)));
403    /// assert_eq!(Transform::parse("rotate(45)"), Ok(Transform::Rotate(45.0)));
404    /// assert_eq!(Transform::parse("translate(10, 20)"), Ok(Transform::Translate { x: 10.0, y: 20.0 }));
405    /// ```
406    pub fn parse(s: &str) -> Result<Self, String> {
407        let s = s.trim();
408
409        // Scale
410        if s.starts_with("scale(") && s.ends_with(')') {
411            let inner = &s[6..s.len() - 1];
412            let value: f32 = inner
413                .parse()
414                .map_err(|_| format!("Invalid scale value: {}", s))?;
415            return Ok(Transform::Scale(value));
416        }
417
418        // ScaleXY
419        if s.starts_with("scale(") && s.ends_with(')') {
420            let inner = &s[6..s.len() - 1];
421            let parts: Vec<&str> = inner.split(',').collect();
422            if parts.len() == 2 {
423                let x: f32 = parts[0]
424                    .trim()
425                    .parse()
426                    .map_err(|_| format!("Invalid scale x: {}", parts[0]))?;
427                let y: f32 = parts[1]
428                    .trim()
429                    .parse()
430                    .map_err(|_| format!("Invalid scale y: {}", parts[1]))?;
431                return Ok(Transform::ScaleXY { x, y });
432            }
433        }
434
435        // Rotate
436        if s.starts_with("rotate(") && s.ends_with(')') {
437            let inner = &s[7..s.len() - 1];
438            let value: f32 = inner
439                .parse()
440                .map_err(|_| format!("Invalid rotate value: {}", s))?;
441            return Ok(Transform::Rotate(value));
442        }
443
444        // Translate
445        if s.starts_with("translate(") && s.ends_with(')') {
446            let inner = &s[10..s.len() - 1];
447            let parts: Vec<&str> = inner.split(',').collect();
448            if parts.len() == 2 {
449                let x: f32 = parts[0]
450                    .trim()
451                    .parse()
452                    .map_err(|_| format!("Invalid translate x: {}", parts[0]))?;
453                let y: f32 = parts[1]
454                    .trim()
455                    .parse()
456                    .map_err(|_| format!("Invalid translate y: {}", parts[1]))?;
457                return Ok(Transform::Translate { x, y });
458            }
459        }
460
461        Err(format!(
462            "Invalid transform format: '{}'. Expected scale(n), rotate(n), or translate(x, y)",
463            s
464        ))
465    }
466}