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 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#[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 pub fn validate(&self) -> Result<(), String> {
155 let stops = match self {
156 Gradient::Linear { angle, stops } => {
157 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#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
199pub struct ColorStop {
200 pub color: Color,
201 pub offset: f32,
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
207pub enum RadialShape {
208 Circle,
209 Ellipse,
210}
211
212#[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#[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 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#[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#[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 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 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
382pub enum Transform {
383 Scale(f32),
385 ScaleXY { x: f32, y: f32 },
387 Rotate(f32),
389 Translate { x: f32, y: f32 },
391 Multiple(Vec<Transform>),
393}
394
395impl Transform {
396 pub fn parse(s: &str) -> Result<Self, String> {
407 let s = s.trim();
408
409 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 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 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 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}