1use velox_dom::style::Length;
6
7#[derive(Debug, Clone, Copy, PartialEq)]
9pub struct BorderRadius {
10 pub top_left: Length,
11 pub top_right: Length,
12 pub bottom_right: Length,
13 pub bottom_left: Length,
14}
15
16impl BorderRadius {
17 pub fn parse(value: &str) -> Option<Self> {
18 let parts: Vec<&str> = value.split_whitespace().collect();
19
20 match parts.len() {
21 1 => {
22 let v = Length::parse(parts[0])?;
23 Some(Self {
24 top_left: v,
25 top_right: v,
26 bottom_right: v,
27 bottom_left: v,
28 })
29 }
30 2 => {
31 let h = Length::parse(parts[0])?;
32 let v = Length::parse(parts[1])?;
33 Some(Self {
34 top_left: h,
35 top_right: v,
36 bottom_right: h,
37 bottom_left: v,
38 })
39 }
40 3 => {
41 let tl = Length::parse(parts[0])?;
42 let tr = Length::parse(parts[1])?;
43 let br = Length::parse(parts[2])?;
44 Some(Self {
45 top_left: tl,
46 top_right: tr,
47 bottom_right: br,
48 bottom_left: tr,
49 })
50 }
51 4 => {
52 let tl = Length::parse(parts[0])?;
53 let tr = Length::parse(parts[1])?;
54 let br = Length::parse(parts[2])?;
55 let bl = Length::parse(parts[3])?;
56 Some(Self {
57 top_left: tl,
58 top_right: tr,
59 bottom_right: br,
60 bottom_left: bl,
61 })
62 }
63 _ => None,
64 }
65 }
66
67 pub fn all(radius: Length) -> Self {
68 Self {
69 top_left: radius,
70 top_right: radius,
71 bottom_right: radius,
72 bottom_left: radius,
73 }
74 }
75}
76
77impl Default for BorderRadius {
78 fn default() -> Self {
79 Self::all(Length::Zero)
80 }
81}
82
83#[derive(Debug, Clone, Copy, PartialEq)]
85pub struct BoxShadow {
86 pub offset_x: f32,
88 pub offset_y: f32,
90 pub blur: f32,
92 pub spread: f32,
94 pub color: (u8, u8, u8, u8),
96 pub inset: bool,
98}
99
100impl BoxShadow {
101 pub fn new(offset_x: f32, offset_y: f32, blur: f32, color: (u8, u8, u8, u8)) -> Self {
102 Self {
103 offset_x,
104 offset_y,
105 blur,
106 spread: 0.0,
107 color,
108 inset: false,
109 }
110 }
111
112 pub fn with_spread(mut self, spread: f32) -> Self {
113 self.spread = spread;
114 self
115 }
116
117 pub fn inset(mut self) -> Self {
118 self.inset = true;
119 self
120 }
121
122 pub fn parse(value: &str) -> Option<Self> {
125 let value = value.trim();
126 let inset = value.starts_with("inset");
127 let value = if inset {
132 value.strip_prefix("inset").map(str::trim).unwrap_or(value)
133 } else {
134 value
135 };
136
137 let mut offset_x = 0.0;
138 let mut offset_y = 0.0;
139 let mut blur = 0.0;
140 let mut spread = 0.0;
141 let mut color = (0u8, 0u8, 0u8, 128u8);
142
143 let mut parts: Vec<String> = Vec::new();
145 let mut current_part = String::new();
146 let mut paren_depth = 0;
147
148 for token in value.split_whitespace() {
149 let open_parens = token.matches('(').count();
150 let close_parens = token.matches(')').count();
151
152 if paren_depth == 0 && open_parens == 0 && close_parens == 0 {
153 parts.push(token.to_string());
155 } else {
156 if paren_depth == 0 {
158 current_part.clear();
159 }
160 if !current_part.is_empty() {
161 current_part.push(' ');
162 }
163 current_part.push_str(token);
164 paren_depth += open_parens as i32 - close_parens as i32;
165
166 if paren_depth == 0 && !current_part.is_empty() {
167 parts.push(current_part.clone());
168 }
169 }
170 }
171
172 if parts.is_empty() {
174 parts = value.split_whitespace().map(|s| s.to_string()).collect();
175 }
176
177 let mut part_idx = 0;
178
179 if part_idx < parts.len() {
181 let p = &parts[part_idx];
182 offset_x = p
183 .strip_suffix("px")
184 .and_then(|s| s.parse().ok())
185 .unwrap_or(0.0);
186 part_idx += 1;
187 }
188 if part_idx < parts.len() {
189 let p = &parts[part_idx];
190 offset_y = p
191 .strip_suffix("px")
192 .and_then(|s| s.parse().ok())
193 .unwrap_or(0.0);
194 part_idx += 1;
195 }
196 if part_idx < parts.len() {
197 let p = &parts[part_idx];
198 blur = p
199 .strip_suffix("px")
200 .and_then(|s| s.parse().ok())
201 .unwrap_or(0.0);
202 part_idx += 1;
203 }
204 if part_idx < parts.len() {
206 let p = &parts[part_idx];
207 if p.ends_with("px") {
208 spread = p
209 .strip_suffix("px")
210 .and_then(|s| s.parse().ok())
211 .unwrap_or(0.0);
212 part_idx += 1;
213 }
214 }
215 if part_idx < parts.len() {
217 let color_str = parts[part_idx..].join(" ");
218 if let Some(c) = velox_dom::style::Color::parse(&color_str) {
219 color = (c.r, c.g, c.b, c.a);
220 }
221 }
222
223 Some(BoxShadow {
224 offset_x,
225 offset_y,
226 blur,
227 spread,
228 color,
229 inset,
230 })
231 }
232}
233
234impl Default for BoxShadow {
235 fn default() -> Self {
236 Self {
237 offset_x: 0.0,
238 offset_y: 0.0,
239 blur: 0.0,
240 spread: 0.0,
241 color: (0, 0, 0, 128),
242 inset: false,
243 }
244 }
245}
246
247#[derive(Debug, Clone, Copy, PartialEq)]
249pub struct TextShadow {
250 pub offset_x: f32,
252 pub offset_y: f32,
254 pub blur: f32,
256 pub color: (u8, u8, u8, u8),
258}
259
260impl TextShadow {
261 pub fn new(offset_x: f32, offset_y: f32, blur: f32, color: (u8, u8, u8, u8)) -> Self {
262 Self {
263 offset_x,
264 offset_y,
265 blur,
266 color,
267 }
268 }
269
270 pub fn parse(value: &str) -> Option<Self> {
272 let value = value.trim();
273 let mut offset_x = 0.0;
274 let mut offset_y = 0.0;
275 let mut blur = 0.0;
276 let mut color = (0u8, 0u8, 0u8, 128u8);
277
278 let mut parts: Vec<String> = Vec::new();
280 let mut current_part = String::new();
281 let mut paren_depth = 0;
282
283 for token in value.split_whitespace() {
284 let open_parens = token.matches('(').count();
285 let close_parens = token.matches(')').count();
286
287 if paren_depth == 0 && open_parens == 0 && close_parens == 0 {
288 parts.push(token.to_string());
289 } else {
290 if paren_depth == 0 {
291 current_part.clear();
292 }
293 if !current_part.is_empty() {
294 current_part.push(' ');
295 }
296 current_part.push_str(token);
297 paren_depth += open_parens as i32 - close_parens as i32;
298
299 if paren_depth == 0 && !current_part.is_empty() {
300 parts.push(current_part.clone());
301 }
302 }
303 }
304
305 if parts.is_empty() {
306 parts = value.split_whitespace().map(|s| s.to_string()).collect();
307 }
308
309 let mut part_idx = 0;
310
311 if part_idx < parts.len() {
312 let p = &parts[part_idx];
313 offset_x = p
314 .strip_suffix("px")
315 .and_then(|s| s.parse().ok())
316 .unwrap_or(0.0);
317 part_idx += 1;
318 }
319 if part_idx < parts.len() {
320 let p = &parts[part_idx];
321 offset_y = p
322 .strip_suffix("px")
323 .and_then(|s| s.parse().ok())
324 .unwrap_or(0.0);
325 part_idx += 1;
326 }
327 if part_idx < parts.len() {
329 let p = &parts[part_idx];
330 if p.ends_with("px") {
331 blur = p
332 .strip_suffix("px")
333 .and_then(|s| s.parse().ok())
334 .unwrap_or(0.0);
335 part_idx += 1;
336 }
337 }
338 if part_idx < parts.len() {
340 let color_str = parts[part_idx..].join(" ");
341 if let Some(c) = velox_dom::style::Color::parse(&color_str) {
342 color = (c.r, c.g, c.b, c.a);
343 }
344 }
345
346 Some(TextShadow {
347 offset_x,
348 offset_y,
349 blur,
350 color,
351 })
352 }
353}
354
355impl Default for TextShadow {
356 fn default() -> Self {
357 Self {
358 offset_x: 0.0,
359 offset_y: 0.0,
360 blur: 0.0,
361 color: (0, 0, 0, 128),
362 }
363 }
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369
370 #[test]
371 fn test_border_radius_single() {
372 let br = BorderRadius::parse("8px").unwrap();
373 assert_eq!(br.top_left, Length::Px(8.0));
374 assert_eq!(br.bottom_right, Length::Px(8.0));
375 }
376
377 #[test]
378 fn test_border_radius_four() {
379 let br = BorderRadius::parse("4px 8px 12px 16px").unwrap();
380 assert_eq!(br.top_left, Length::Px(4.0));
381 assert_eq!(br.top_right, Length::Px(8.0));
382 assert_eq!(br.bottom_right, Length::Px(12.0));
383 assert_eq!(br.bottom_left, Length::Px(16.0));
384 }
385
386 #[test]
387 fn test_box_shadow() {
388 let shadow = BoxShadow::new(2.0, 2.0, 5.0, (0, 0, 0, 200));
389 assert_eq!(shadow.offset_x, 2.0);
390 assert_eq!(shadow.blur, 5.0);
391 assert!(!shadow.inset);
392 }
393}