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 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#[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 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 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 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#[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 pub fn validate(&self) -> Result<(), String> {
275 let stops = match self {
276 Gradient::Linear { angle, stops } => {
277 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#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
319pub struct ColorStop {
320 pub color: Color,
321 pub offset: f32,
323}
324
325#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
327pub enum RadialShape {
328 Circle,
329 Ellipse,
330}
331
332#[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#[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 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#[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#[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 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 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
502pub enum Transform {
503 Scale(f32),
505 ScaleXY { x: f32, y: f32 },
507 Rotate(f32),
509 Translate { x: f32, y: f32 },
511 Multiple(Vec<Transform>),
513}
514
515impl Transform {
516 pub fn parse(s: &str) -> Result<Self, String> {
527 let s = s.trim();
528
529 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 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 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 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}