1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
11pub struct StyleProperties {
12 pub background: Option<Background>,
14 pub color: Option<Color>,
16 pub border: Option<Border>,
18 pub shadow: Option<Shadow>,
20 pub opacity: Option<f32>,
22 pub transform: Option<Transform>,
24}
25
26impl StyleProperties {
27 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
57pub enum Background {
58 Color(Color),
60 Gradient(Gradient),
62 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
78pub enum ImageFit {
79 Fill,
80 Contain,
81 Cover,
82 ScaleDown,
83}
84
85#[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 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 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 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 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 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 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 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 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#[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 pub fn validate(&self) -> Result<(), String> {
284 let stops = match self {
285 Gradient::Linear { angle, stops } => {
286 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#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
328pub struct ColorStop {
329 pub color: Color,
330 pub offset: f32,
332}
333
334#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
336pub enum RadialShape {
337 Circle,
338 Ellipse,
339}
340
341#[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#[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 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#[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#[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 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 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
511pub enum Transform {
512 Scale(f32),
514 ScaleXY { x: f32, y: f32 },
516 Rotate(f32),
518 Translate { x: f32, y: f32 },
520 Matrix([f32; 6]),
522 Multiple(Vec<Transform>),
524}
525
526impl Transform {
527 pub fn parse(s: &str) -> Result<Self, String> {
538 let s = s.trim();
539
540 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 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 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 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}