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    /// Parse color from hex string
117    ///
118    /// Supports:
119    /// - "#RGB" (e.g., "#f00" → red)
120    /// - "#RRGGBB" (e.g., "#ff0000" → red)
121    /// - "#RRGGBBAA" (e.g., "#ff000080" → semi-transparent red)
122    pub fn from_hex(s: &str) -> Result<Self, String> {
123        let s = s.trim();
124        if !s.starts_with('#') {
125            return Err(format!("Invalid hex color '{}': must start with '#'", s));
126        }
127
128        let hex = &s[1..];
129        let (r, g, b, a) = match hex.len() {
130            3 => {
131                // Expand each character to two (e.g., "f" -> "ff")
132                let r = u8::from_str_radix(&format!("{}{}", &hex[0..1], &hex[0..1]), 16)
133                    .map_err(|_| format!("Invalid hex color '{}'", s))?;
134                let g = u8::from_str_radix(&format!("{}{}", &hex[1..2], &hex[1..2]), 16)
135                    .map_err(|_| format!("Invalid hex color '{}'", s))?;
136                let b = u8::from_str_radix(&format!("{}{}", &hex[2..3], &hex[2..3]), 16)
137                    .map_err(|_| format!("Invalid hex color '{}'", s))?;
138                (r, g, b, 255)
139            }
140            6 => {
141                let r = u8::from_str_radix(&hex[0..2], 16)
142                    .map_err(|_| format!("Invalid hex color '{}'", s))?;
143                let g = u8::from_str_radix(&hex[2..4], 16)
144                    .map_err(|_| format!("Invalid hex color '{}'", s))?;
145                let b = u8::from_str_radix(&hex[4..6], 16)
146                    .map_err(|_| format!("Invalid hex color '{}'", s))?;
147                (r, g, b, 255)
148            }
149            8 => {
150                let r = u8::from_str_radix(&hex[0..2], 16)
151                    .map_err(|_| format!("Invalid hex color '{}'", s))?;
152                let g = u8::from_str_radix(&hex[2..4], 16)
153                    .map_err(|_| format!("Invalid hex color '{}'", s))?;
154                let b = u8::from_str_radix(&hex[4..6], 16)
155                    .map_err(|_| format!("Invalid hex color '{}'", s))?;
156                let a = u8::from_str_radix(&hex[6..8], 16)
157                    .map_err(|_| format!("Invalid hex color '{}'", s))?;
158                (r, g, b, a)
159            }
160            _ => {
161                return Err(format!(
162                    "Invalid hex color '{}': expected 3, 6, or 8 hex digits",
163                    s
164                ));
165            }
166        };
167
168        Ok(Color {
169            r: r as f32 / 255.0,
170            g: g as f32 / 255.0,
171            b: b as f32 / 255.0,
172            a: a as f32 / 255.0,
173        })
174    }
175
176    /// Convert to hex string
177    pub fn to_hex(&self) -> String {
178        let r = (self.r.clamp(0.0, 1.0) * 255.0) as u8;
179        let g = (self.g.clamp(0.0, 1.0) * 255.0) as u8;
180        let b = (self.b.clamp(0.0, 1.0) * 255.0) as u8;
181        format!("#{:02x}{:02x}{:02x}", r, g, b)
182    }
183
184    /// Create color from RGB bytes (0-255 range)
185    ///
186    /// # Arguments
187    ///
188    /// * `r` - Red component (0-255)
189    /// * `g` - Green component (0-255)
190    /// * `b` - Blue component (0-255)
191    ///
192    /// # Example
193    ///
194    /// ```rust
195    /// use dampen_core::ir::style::Color;
196    ///
197    /// let color = Color::from_rgb8(52, 152, 219);
198    /// assert_eq!(color.r, 52.0 / 255.0);
199    /// ```
200    pub fn from_rgb8(r: u8, g: u8, b: u8) -> Self {
201        Self {
202            r: r as f32 / 255.0,
203            g: g as f32 / 255.0,
204            b: b as f32 / 255.0,
205            a: 1.0,
206        }
207    }
208
209    /// Create color from RGBA bytes (0-255 range)
210    ///
211    /// # Arguments
212    ///
213    /// * `r` - Red component (0-255)
214    /// * `g` - Green component (0-255)
215    /// * `b` - Blue component (0-255)
216    /// * `a` - Alpha component (0-255)
217    ///
218    /// # Example
219    ///
220    /// ```rust
221    /// use dampen_core::ir::style::Color;
222    ///
223    /// let color = Color::from_rgba8(52, 152, 219, 200);
224    /// assert_eq!(color.r, 52.0 / 255.0);
225    /// assert_eq!(color.a, 200.0 / 255.0);
226    /// ```
227    pub fn from_rgba8(r: u8, g: u8, b: u8, a: u8) -> Self {
228        Self {
229            r: r as f32 / 255.0,
230            g: g as f32 / 255.0,
231            b: b as f32 / 255.0,
232            a: a as f32 / 255.0,
233        }
234    }
235
236    /// Validate color values
237    pub fn validate(&self) -> Result<(), String> {
238        if self.r < 0.0 || self.r > 1.0 {
239            return Err(format!("Red component out of range: {}", self.r));
240        }
241        if self.g < 0.0 || self.g > 1.0 {
242            return Err(format!("Green component out of range: {}", self.g));
243        }
244        if self.b < 0.0 || self.b > 1.0 {
245            return Err(format!("Blue component out of range: {}", self.b));
246        }
247        if self.a < 0.0 || self.a > 1.0 {
248            return Err(format!("Alpha component out of range: {}", self.a));
249        }
250        Ok(())
251    }
252}
253
254/// Gradient fill
255#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
256pub enum Gradient {
257    Linear {
258        angle: f32,
259        stops: Vec<ColorStop>,
260    },
261    Radial {
262        shape: RadialShape,
263        stops: Vec<ColorStop>,
264    },
265}
266
267impl Gradient {
268    /// Validate gradient
269    ///
270    /// Returns an error if:
271    /// - Less than 2 or more than 8 color stops (Iced limitation)
272    /// - Color stop offsets not sorted or out of range
273    /// - Angle not normalized
274    pub fn validate(&self) -> Result<(), String> {
275        let stops = match self {
276            Gradient::Linear { angle, stops } => {
277                // Normalize angle to 0.0-360.0
278                if *angle < 0.0 || *angle > 360.0 {
279                    return Err(format!("Gradient angle must be 0.0-360.0, got {}", angle));
280                }
281                stops
282            }
283            Gradient::Radial { stops, .. } => stops,
284        };
285
286        if stops.len() < 2 {
287            return Err("Gradient must have at least 2 color stops".to_string());
288        }
289
290        if stops.len() > 8 {
291            return Err(
292                "Gradient cannot have more than 8 color stops (Iced limitation)".to_string(),
293            );
294        }
295
296        let mut last_offset = -1.0;
297        for stop in stops {
298            if stop.offset < 0.0 || stop.offset > 1.0 {
299                return Err(format!(
300                    "Color stop offset must be 0.0-1.0, got {}",
301                    stop.offset
302                ));
303            }
304
305            if stop.offset <= last_offset {
306                return Err("Color stop offsets must be in ascending order".to_string());
307            }
308
309            stop.color.validate()?;
310            last_offset = stop.offset;
311        }
312
313        Ok(())
314    }
315}
316
317/// Color stop for gradients
318#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
319pub struct ColorStop {
320    pub color: Color,
321    /// Offset in gradient (0.0 = start, 1.0 = end)
322    pub offset: f32,
323}
324
325/// Radial gradient shape
326#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
327pub enum RadialShape {
328    Circle,
329    Ellipse,
330}
331
332/// Border styling
333#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
334pub struct Border {
335    pub width: f32,
336    pub color: Color,
337    pub radius: BorderRadius,
338    pub style: BorderStyle,
339}
340
341impl Border {
342    pub fn validate(&self) -> Result<(), String> {
343        if self.width < 0.0 {
344            return Err(format!(
345                "Border width must be non-negative, got {}",
346                self.width
347            ));
348        }
349        self.color.validate()?;
350        self.radius.validate()?;
351        Ok(())
352    }
353}
354
355/// Border radius (corner rounding)
356#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
357pub struct BorderRadius {
358    pub top_left: f32,
359    pub top_right: f32,
360    pub bottom_right: f32,
361    pub bottom_left: f32,
362}
363
364impl BorderRadius {
365    /// Parse from string
366    ///
367    /// # Formats
368    /// - `"<all>"`: All corners (e.g., "8")
369    /// - `"<tl> <tr> <br> <bl>"`: Individual corners
370    pub fn parse(s: &str) -> Result<Self, String> {
371        let parts: Vec<&str> = s.split_whitespace().collect();
372
373        match parts.len() {
374            1 => {
375                let all: f32 = parts[0]
376                    .parse()
377                    .map_err(|_| format!("Invalid border radius: {}", s))?;
378                Ok(BorderRadius {
379                    top_left: all,
380                    top_right: all,
381                    bottom_right: all,
382                    bottom_left: all,
383                })
384            }
385            4 => {
386                let tl: f32 = parts[0]
387                    .parse()
388                    .map_err(|_| format!("Invalid top-left radius: {}", parts[0]))?;
389                let tr: f32 = parts[1]
390                    .parse()
391                    .map_err(|_| format!("Invalid top-right radius: {}", parts[1]))?;
392                let br: f32 = parts[2]
393                    .parse()
394                    .map_err(|_| format!("Invalid bottom-right radius: {}", parts[2]))?;
395                let bl: f32 = parts[3]
396                    .parse()
397                    .map_err(|_| format!("Invalid bottom-left radius: {}", parts[3]))?;
398                Ok(BorderRadius {
399                    top_left: tl,
400                    top_right: tr,
401                    bottom_right: br,
402                    bottom_left: bl,
403                })
404            }
405            _ => Err(format!(
406                "Invalid border radius format: '{}'. Expected 1 or 4 values",
407                s
408            )),
409        }
410    }
411
412    pub fn validate(&self) -> Result<(), String> {
413        if self.top_left < 0.0
414            || self.top_right < 0.0
415            || self.bottom_right < 0.0
416            || self.bottom_left < 0.0
417        {
418            return Err("Border radius values must be non-negative".to_string());
419        }
420        Ok(())
421    }
422}
423
424/// Border line style
425#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
426pub enum BorderStyle {
427    Solid,
428    Dashed,
429    Dotted,
430}
431
432impl BorderStyle {
433    pub fn parse(s: &str) -> Result<Self, String> {
434        match s.trim().to_lowercase().as_str() {
435            "solid" => Ok(BorderStyle::Solid),
436            "dashed" => Ok(BorderStyle::Dashed),
437            "dotted" => Ok(BorderStyle::Dotted),
438            _ => Err(format!(
439                "Invalid border style: '{}'. Expected solid, dashed, or dotted",
440                s
441            )),
442        }
443    }
444}
445
446/// Drop shadow
447#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
448pub struct Shadow {
449    pub offset_x: f32,
450    pub offset_y: f32,
451    pub blur_radius: f32,
452    pub color: Color,
453}
454
455impl Shadow {
456    /// Parse from string format: "offset_x offset_y blur color"
457    ///
458    /// # Example
459    /// ```rust
460    /// use dampen_core::ir::style::Shadow;
461    ///
462    /// let shadow = Shadow::parse("2 2 4 #00000040").unwrap();
463    /// assert_eq!(shadow.offset_x, 2.0);
464    /// assert_eq!(shadow.offset_y, 2.0);
465    /// assert_eq!(shadow.blur_radius, 4.0);
466    /// ```
467    pub fn parse(s: &str) -> Result<Self, String> {
468        let parts: Vec<&str> = s.split_whitespace().collect();
469
470        if parts.len() < 4 {
471            return Err(format!(
472                "Invalid shadow format: '{}'. Expected: offset_x offset_y blur color",
473                s
474            ));
475        }
476
477        let offset_x: f32 = parts[0]
478            .parse()
479            .map_err(|_| format!("Invalid offset_x: {}", parts[0]))?;
480        let offset_y: f32 = parts[1]
481            .parse()
482            .map_err(|_| format!("Invalid offset_y: {}", parts[1]))?;
483        let blur_radius: f32 = parts[2]
484            .parse()
485            .map_err(|_| format!("Invalid blur_radius: {}", parts[2]))?;
486
487        // Color is everything after the first 3 parts
488        let color_str = parts[3..].join(" ");
489        let color = Color::parse(&color_str)?;
490
491        Ok(Shadow {
492            offset_x,
493            offset_y,
494            blur_radius,
495            color,
496        })
497    }
498}
499
500/// Visual transformation
501#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
502pub enum Transform {
503    /// Uniform scale
504    Scale(f32),
505    /// Non-uniform scale
506    ScaleXY { x: f32, y: f32 },
507    /// Rotation in degrees
508    Rotate(f32),
509    /// Translation in pixels
510    Translate { x: f32, y: f32 },
511    /// Multiple composed transforms
512    Multiple(Vec<Transform>),
513}
514
515impl Transform {
516    /// Parse from string
517    ///
518    /// # Examples
519    /// ```rust
520    /// use dampen_core::ir::style::Transform;
521    ///
522    /// assert_eq!(Transform::parse("scale(1.2)"), Ok(Transform::Scale(1.2)));
523    /// assert_eq!(Transform::parse("rotate(45)"), Ok(Transform::Rotate(45.0)));
524    /// assert_eq!(Transform::parse("translate(10, 20)"), Ok(Transform::Translate { x: 10.0, y: 20.0 }));
525    /// ```
526    pub fn parse(s: &str) -> Result<Self, String> {
527        let s = s.trim();
528
529        // Scale
530        if s.starts_with("scale(") && s.ends_with(')') {
531            let inner = &s[6..s.len() - 1];
532            let value: f32 = inner
533                .parse()
534                .map_err(|_| format!("Invalid scale value: {}", s))?;
535            return Ok(Transform::Scale(value));
536        }
537
538        // ScaleXY
539        if s.starts_with("scale(") && s.ends_with(')') {
540            let inner = &s[6..s.len() - 1];
541            let parts: Vec<&str> = inner.split(',').collect();
542            if parts.len() == 2 {
543                let x: f32 = parts[0]
544                    .trim()
545                    .parse()
546                    .map_err(|_| format!("Invalid scale x: {}", parts[0]))?;
547                let y: f32 = parts[1]
548                    .trim()
549                    .parse()
550                    .map_err(|_| format!("Invalid scale y: {}", parts[1]))?;
551                return Ok(Transform::ScaleXY { x, y });
552            }
553        }
554
555        // Rotate
556        if s.starts_with("rotate(") && s.ends_with(')') {
557            let inner = &s[7..s.len() - 1];
558            let value: f32 = inner
559                .parse()
560                .map_err(|_| format!("Invalid rotate value: {}", s))?;
561            return Ok(Transform::Rotate(value));
562        }
563
564        // Translate
565        if s.starts_with("translate(") && s.ends_with(')') {
566            let inner = &s[10..s.len() - 1];
567            let parts: Vec<&str> = inner.split(',').collect();
568            if parts.len() == 2 {
569                let x: f32 = parts[0]
570                    .trim()
571                    .parse()
572                    .map_err(|_| format!("Invalid translate x: {}", parts[0]))?;
573                let y: f32 = parts[1]
574                    .trim()
575                    .parse()
576                    .map_err(|_| format!("Invalid translate y: {}", parts[1]))?;
577                return Ok(Transform::Translate { x, y });
578            }
579        }
580
581        Err(format!(
582            "Invalid transform format: '{}'. Expected scale(n), rotate(n), or translate(x, y)",
583            s
584        ))
585    }
586}