Skip to main content

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