1use std::time::Duration;
2
3use floem::peniko::{Brush, Color};
4use floem::prop;
5use floem::style::{
6 AlignContentProp, AlignItemsProp, AlignSelf, AspectRatio, Background, BorderBottom,
7 BorderColor, BorderLeft, BorderRadius, BorderRight, BorderTop, BoxShadow, BoxShadowProp,
8 ColGap, Cursor, CursorColor, CursorStyle, DisplayProp, FlexBasis, FlexDirectionProp, FlexGrow,
9 FlexShrink, FlexWrapProp, FontFamily, FontSize, FontStyle, FontWeight, Height, InsetBottom,
10 InsetLeft, InsetRight, InsetTop, JustifyContentProp, JustifySelf, LineHeight, MarginBottom,
11 MarginLeft, MarginRight, MarginTop, MaxHeight, MaxWidth, MinHeight, MinWidth, Outline,
12 OutlineColor, PaddingBottom, PaddingLeft, PaddingRight, PaddingTop, PositionProp, RowGap,
13 Selectable, Style, StylePropValue, TextColor, TextOverflow, TextOverflowProp, Transition,
14 Width, ZIndex,
15};
16use floem::taffy::{
17 AlignContent, AlignItems, Display, FlexDirection, FlexWrap, JustifyContent, Position,
18};
19use floem::text::Weight;
20use floem::unit::{Pct, Px, PxPct, PxPctAuto};
21use floem::views::scroll::Border;
22use floem_css_macros::StyleParser;
23use smallvec::SmallVec;
24
25#[derive(Clone, Debug, Default, PartialEq)]
26pub struct BorderDef {
27 width: Option<Px>,
28 color: Option<Color>,
29}
30
31impl StylePropValue for BorderDef {}
32
33prop!(pub Padding: PxPctAuto {} = PxPctAuto::Px(0.0));
34prop!(pub Margin: PxPctAuto {} = PxPctAuto::Px(0.0));
35prop!(pub TransitionProp: f64 {} = 0.0);
36prop!(pub BorderProp: BorderDef {} = BorderDef::default());
37
38#[derive(StyleParser)]
39pub enum Declaration {
40 #[property("display")]
41 #[parser("parse_display")]
42 #[style_class(DisplayProp)]
43 Display(Display),
44
45 #[property("position")]
46 #[parser("parse_position")]
47 #[style_class(PositionProp)]
48 Position(Position),
49
50 #[property("width")]
51 #[parser("parse_pxpctauto")]
52 #[style_class(Width)]
53 Width(PxPctAuto),
54
55 #[property("height")]
56 #[parser("parse_pxpctauto")]
57 #[style_class(Height)]
58 Height(PxPctAuto),
59
60 #[property("min-width")]
61 #[parser("parse_pxpctauto")]
62 #[style_class(MinWidth)]
63 MinWidth(PxPctAuto),
64
65 #[property("min-height")]
66 #[parser("parse_pxpctauto")]
67 #[style_class(MinHeight)]
68 MinHeight(PxPctAuto),
69
70 #[property("max-width")]
71 #[parser("parse_pxpctauto")]
72 #[style_class(MaxWidth)]
73 MaxWidth(PxPctAuto),
74
75 #[property("max-height")]
76 #[parser("parse_pxpctauto")]
77 #[style_class(MaxHeight)]
78 MaxHeight(PxPctAuto),
79
80 #[property("flex-direction")]
81 #[parser("parse_flex_direction")]
82 #[style_class(FlexDirectionProp)]
83 FlexDirection(FlexDirection),
84
85 #[property("flex-wrap")]
86 #[parser("parse_flex_wrap")]
87 #[style_class(FlexWrapProp)]
88 FlexWrap(FlexWrap),
89
90 #[property("flex-grow")]
91 #[parser("parse_f32")]
92 #[style_class(FlexGrow)]
93 FlexGrow(f32),
94
95 #[property("flex-shrink")]
96 #[parser("parse_f32")]
97 #[style_class(FlexShrink)]
98 FlexShrink(f32),
99
100 #[property("flex-basis")]
101 #[parser("parse_pxpctauto")]
102 #[style_class(FlexBasis)]
103 FlexBasis(PxPctAuto),
104
105 #[property("justify-content")]
106 #[parser("parse_justify_content")]
107 #[style_class(JustifyContentProp)]
108 JustifyContent(JustifyContent),
109
110 #[property("justify-self")]
111 #[parser("parse_align_items")]
112 #[style_class(JustifySelf)]
113 JustifySelf(AlignItems),
114
115 #[property("align-items")]
116 #[parser("parse_align_items")]
117 #[style_class(AlignItemsProp)]
118 AlignItems(AlignItems),
119
120 #[property("align-content")]
121 #[parser("parse_align_content")]
122 #[style_class(AlignContentProp)]
123 AlignContent(AlignContent),
124
125 #[property("align-self")]
126 #[parser("parse_align_items")]
127 #[style_class(AlignSelf)]
128 AlignSelf(AlignItems),
129
130 #[property("border")]
131 #[parser("parse_border")]
132 #[style_class(BorderProp)]
133 Border(BorderDef),
134
135 #[property("border-width")]
136 #[parser("parse_px")]
137 #[style_class(Border)]
138 BorderWidth(Px),
139
140 #[property("border-left")]
141 #[parser("parse_px")]
142 #[style_class(BorderLeft)]
143 BorderLeft(Px),
144
145 #[property("border-top")]
146 #[parser("parse_px")]
147 #[style_class(BorderTop)]
148 BorderTop(Px),
149
150 #[property("border-right")]
151 #[parser("parse_px")]
152 #[style_class(BorderRight)]
153 BorderRight(Px),
154
155 #[property("border-bottom")]
156 #[parser("parse_px")]
157 #[style_class(BorderBottom)]
158 BorderBottom(Px),
159
160 #[property("border-radius")]
161 #[parser("parse_px_pct")]
162 #[style_class(BorderRadius)]
163 BorderRadius(PxPct),
164
165 #[property("outline-color")]
166 #[parser("parse_color")]
167 #[style_class(OutlineColor)]
168 OutlineColor(Color),
169
170 #[property("outline")]
171 #[parser("parse_px")]
172 #[style_class(Outline)]
173 Outline(Px),
174
175 #[property("border-color")]
176 #[parser("parse_color")]
177 #[style_class(BorderColor)]
178 BorderColor(Color),
179
180 #[property("padding")]
181 #[parser("parse_px_pct")]
182 #[style_class(Padding)]
183 Padding(PxPct),
184
185 #[property("padding-left")]
186 #[parser("parse_px_pct")]
187 #[style_class(PaddingLeft)]
188 PaddingLeft(PxPct),
189
190 #[property("padding-top")]
191 #[parser("parse_px_pct")]
192 #[style_class(PaddingTop)]
193 PaddingTop(PxPct),
194
195 #[property("padding-right")]
196 #[parser("parse_px_pct")]
197 #[style_class(PaddingRight)]
198 PaddingRight(PxPct),
199
200 #[property("padding-bottom")]
201 #[parser("parse_px_pct")]
202 #[style_class(PaddingBottom)]
203 PaddingBottom(PxPct),
204
205 #[property("margin")]
206 #[parser("parse_pxpctauto")]
207 #[style_class(Margin)]
208 Margin(PxPctAuto),
209
210 #[property("margin-left")]
211 #[parser("parse_pxpctauto")]
212 #[style_class(MarginLeft)]
213 MarginLeft(PxPctAuto),
214
215 #[property("margin-top")]
216 #[parser("parse_pxpctauto")]
217 #[style_class(MarginTop)]
218 MarginTop(PxPctAuto),
219
220 #[property("margin-right")]
221 #[parser("parse_pxpctauto")]
222 #[style_class(MarginRight)]
223 MarginRight(PxPctAuto),
224
225 #[property("margin-bottom")]
226 #[parser("parse_pxpctauto")]
227 #[style_class(MarginBottom)]
228 MarginBottom(PxPctAuto),
229
230 #[property("left")]
231 #[parser("parse_pxpctauto")]
232 #[style_class(InsetLeft)]
233 InsetLeft(PxPctAuto),
234
235 #[property("top")]
236 #[parser("parse_pxpctauto")]
237 #[style_class(InsetTop)]
238 InsetTop(PxPctAuto),
239
240 #[property("right")]
241 #[parser("parse_pxpctauto")]
242 #[style_class(InsetRight)]
243 InsetRight(PxPctAuto),
244
245 #[property("bottom")]
246 #[parser("parse_pxpctauto")]
247 #[style_class(InsetBottom)]
248 InsetBottom(PxPctAuto),
249
250 #[property("z-index")]
251 #[parser("parse_i32")]
252 #[style_class(ZIndex)]
253 ZIndex(i32),
254
255 #[property("cursor")]
256 #[parser("parse_cursor_style")]
257 #[style_class(Cursor)]
258 Cursor(CursorStyle),
259
260 #[property("color")]
261 #[parser("parse_color")]
262 #[style_class(TextColor)]
263 Color(Color),
264
265 #[property("background-color")]
266 #[parser("parse_color")]
267 #[style_class(Background)]
268 BackgroundColor(Color),
269
270 #[property("box-shadow")]
271 #[parser("parse_box_shadow")]
272 #[style_class(BoxShadowProp)]
273 BoxShadow(BoxShadow),
274
275 #[property("font-size")]
276 #[parser("parse_px")]
277 #[style_class(FontSize)]
278 FontSize(Px),
279
280 #[property("font-family")]
281 #[parser("to_owned")]
282 #[style_class(FontFamily)]
283 FontFamily(String),
284
285 #[property("font-weight")]
286 #[parser("parse_font_weight")]
287 #[style_class(FontWeight)]
288 FontWeight(Weight),
289
290 #[property("font-style")]
291 #[parser("parse_font_style")]
292 #[style_class(FontStyle)]
293 FontStyle(floem::text::Style),
294
295 #[property("caret-color")]
296 #[parser("parse_color")]
297 #[style_class(CursorColor)]
298 CursorColor(Color),
299
300 #[property("text-wrap")]
301 #[parser("parse_text_overflow")]
302 #[style_class(TextOverflowProp)]
303 TextOverflow(TextOverflow),
304
305 #[property("line-height")]
306 #[parser("parse_f32")]
307 #[style_class(LineHeight)]
308 LineHeight(f32),
309
310 #[property("aspect-ratio")]
311 #[parser("parse_f32")]
312 #[style_class(AspectRatio)]
313 AspectRatio(f32),
314
315 #[property("column-gap")]
316 #[parser("parse_px_pct")]
317 #[style_class(ColGap)]
318 ColGap(PxPct),
319
320 #[property("row-gap")]
321 #[parser("parse_px_pct")]
322 #[style_class(RowGap)]
323 RowGap(PxPct),
324
325 #[property("gap")]
326 #[parser("parse_gap")]
327 #[style_class(RowGap)]
328 Gap((PxPct, Option<PxPct>)),
329
330 #[property("transition")]
331 #[parser("parse_transition")]
332 #[style_class(TransitionProp)]
333 Transition((String, Transition)),
334
335 #[property("user-select")]
336 #[parser("parse_user_select")]
337 #[style_class(Selectable)]
338 UserSelect(bool),
339}
340
341impl Declaration {
342 #[inline(never)]
343 pub fn apply_style(self, s: Style) -> Style {
344 match self {
345 Self::Display(d) => s.display(d),
346 Self::Position(p) => s.position(p),
347 Self::Width(v) => s.width(v),
348 Self::Height(v) => s.height(v),
349 Self::MinWidth(v) => s.min_width(v),
350 Self::MinHeight(v) => s.min_height(v),
351 Self::MaxWidth(v) => s.max_width(v),
352 Self::MaxHeight(v) => s.max_height(v),
353 Self::FlexDirection(f) => s.flex_direction(f),
354 Self::FlexWrap(f) => s.flex_wrap(f),
355 Self::FlexGrow(f) => s.flex_grow(f),
356 Self::FlexShrink(f) => s.flex_shrink(f),
357 Self::FlexBasis(v) => s.flex_basis(v),
358 Self::JustifyContent(j) => s.justify_content(j),
359 Self::JustifySelf(a) => s.justify_self(a),
360 Self::AlignItems(a) => s.align_items(a),
361 Self::AlignContent(v) => s.align_content(v),
362 Self::AlignSelf(v) => s.align_self(v),
363 Self::Border(b) => s
364 .apply_opt(b.width, |s, v| s.border(v.0))
365 .apply_opt(b.color, Style::border_color),
366 Self::BorderWidth(v) => s.border(v.0),
367 Self::BorderLeft(v) => s.border_left(v.0),
368 Self::BorderTop(v) => s.border_top(v.0),
369 Self::BorderRight(v) => s.border_right(v.0),
370 Self::BorderBottom(v) => s.border_bottom(v.0),
371 Self::BorderRadius(v) => s.border_radius(v),
372 Self::OutlineColor(v) => s.outline_color(v),
373 Self::Outline(v) => s.outline(v.0),
374 Self::BorderColor(v) => s.border_color(v),
375 Self::Padding(v) => s.padding(v),
376 Self::PaddingLeft(v) => s.padding_left(v),
377 Self::PaddingTop(v) => s.padding_top(v),
378 Self::PaddingRight(v) => s.padding_right(v),
379 Self::PaddingBottom(v) => s.padding_bottom(v),
380 Self::Margin(v) => s.margin(v),
381 Self::MarginLeft(v) => s.margin_left(v),
382 Self::MarginTop(v) => s.margin_top(v),
383 Self::MarginRight(v) => s.margin_right(v),
384 Self::MarginBottom(v) => s.margin_bottom(v),
385 Self::InsetLeft(v) => s.inset_left(v),
386 Self::InsetTop(v) => s.inset_top(v),
387 Self::InsetRight(v) => s.inset_right(v),
388 Self::InsetBottom(v) => s.inset_bottom(v),
389 Self::ZIndex(v) => s.z_index(v),
390 Self::Cursor(v) => s.cursor(v),
391 Self::Color(v) => s.color(v),
392 Self::BackgroundColor(v) => s.background(v),
393 Self::BoxShadow(b) => s
394 .box_shadow_blur(b.blur_radius)
395 .box_shadow_color(b.color)
396 .box_shadow_spread(b.spread)
397 .box_shadow_h_offset(b.h_offset)
398 .box_shadow_v_offset(b.v_offset),
399 Self::FontSize(v) => s.font_size(v),
400 Self::FontFamily(v) => s.font_family(v),
401 Self::FontWeight(v) => s.font_weight(v),
402 Self::FontStyle(v) => s.font_style(v),
403 Self::CursorColor(v) => s.cursor_color(Brush::Solid(v)),
404 Self::TextOverflow(v) => s.text_overflow(v),
405 Self::LineHeight(v) => s.line_height(v),
406 Self::AspectRatio(v) => s.aspect_ratio(v),
407 Self::ColGap(v) => s.column_gap(v),
408 Self::RowGap(v) => s.row_gap(v),
409 Self::Gap(v) => s.row_gap(v.0).apply_opt(v.1, Style::column_gap),
410 Self::Transition((key, t)) => Self::apply_transition(s, &key, t),
411 Self::UserSelect(v) => s.selectable(v),
412 }
413 }
414}
415
416#[derive(Debug)]
417pub struct ParseError<'a> {
418 pub error: &'static str,
419 pub value: &'a str,
420}
421
422impl<'a> ParseError<'a> {
423 pub const fn new(error: &'static str, value: &'a str) -> Self {
424 Self { error, value }
425 }
426}
427
428impl std::fmt::Display for ParseError<'_> {
429 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
430 write!(f, "{}: {}", self.error, self.value)
431 }
432}
433
434const fn parse_display(s: &str) -> Option<Display> {
435 match s.as_bytes() {
436 b"block" => Some(Display::Block),
437 b"flex" => Some(Display::Flex),
438 b"grid" => Some(Display::Grid),
439 b"none" => Some(Display::None),
440 _ => None,
441 }
442}
443
444const fn parse_justify_content(s: &str) -> Option<JustifyContent> {
445 match s.as_bytes() {
446 b"start" => Some(JustifyContent::Start),
447 b"end" => Some(JustifyContent::End),
448 b"flex-start" => Some(JustifyContent::FlexStart),
449 b"flex-end" => Some(JustifyContent::FlexEnd),
450 b"center" => Some(JustifyContent::Center),
451 b"stretch" => Some(JustifyContent::Stretch),
452 b"space-between" => Some(JustifyContent::SpaceBetween),
453 b"space-evenly" => Some(JustifyContent::SpaceEvenly),
454 b"space-around" => Some(JustifyContent::SpaceAround),
455 _ => None,
456 }
457}
458
459const fn parse_align_items(s: &str) -> Option<AlignItems> {
460 match s.as_bytes() {
461 b"center" => Some(AlignItems::Center),
462 b"start" => Some(AlignItems::Start),
463 b"end" => Some(AlignItems::End),
464 b"flex-start" => Some(AlignItems::FlexStart),
465 b"flex-end" => Some(AlignItems::FlexEnd),
466 b"baseline" => Some(AlignItems::Baseline),
467 b"stretch" => Some(AlignItems::Stretch),
468 _ => None,
469 }
470}
471
472pub const fn parse_align_content(s: &str) -> Option<AlignContent> {
473 match s.as_bytes() {
474 b"center" => Some(AlignContent::Center),
475 b"start" => Some(AlignContent::Start),
476 b"end" => Some(AlignContent::End),
477 b"flex-start" => Some(AlignContent::FlexStart),
478 b"flex-end" => Some(AlignContent::FlexEnd),
479 b"stretch" => Some(AlignContent::Stretch),
480 b"space-between" => Some(AlignContent::SpaceBetween),
481 b"space-evenly" => Some(AlignContent::SpaceEvenly),
482 b"space-around" => Some(AlignContent::SpaceAround),
483 _ => None,
484 }
485}
486
487pub const fn parse_position(s: &str) -> Option<Position> {
488 match s.as_bytes() {
489 b"absolute" => Some(Position::Absolute),
490 b"relative" => Some(Position::Relative),
491 _ => None,
492 }
493}
494
495pub const fn parse_flex_direction(s: &str) -> Option<FlexDirection> {
496 match s.as_bytes() {
497 b"row" => Some(FlexDirection::Row),
498 b"column" => Some(FlexDirection::Column),
499 b"row-reverse" => Some(FlexDirection::RowReverse),
500 b"column-reverse" => Some(FlexDirection::ColumnReverse),
501 _ => None,
502 }
503}
504
505pub const fn parse_flex_wrap(s: &str) -> Option<FlexWrap> {
506 match s.as_bytes() {
507 b"wrap" => Some(FlexWrap::Wrap),
508 b"no-wrap" => Some(FlexWrap::NoWrap),
509 b"wrap-reverse" => Some(FlexWrap::WrapReverse),
510 _ => None,
511 }
512}
513
514fn parse_f32(s: &str) -> Option<f32> {
515 s.parse::<f32>().ok()
516}
517
518fn parse_px(s: &str) -> Option<Px> {
519 let pixels = s.strip_suffix("px")?;
520 match pixels.trim_end().parse::<f64>() {
521 Ok(value) => Some(Px(value)),
522 Err(_) => None,
523 }
524}
525
526fn parse_pct(s: &str) -> Option<Pct> {
527 let percents = s.strip_suffix('%')?;
528 match percents.trim_end().parse::<f64>() {
529 Ok(value) => Some(Pct(value)),
530 Err(_) => None,
531 }
532}
533
534fn parse_px_pct(s: &str) -> Option<PxPct> {
535 if let Some(px) = parse_px(s) {
536 return Some(PxPct::Px(px.0));
537 }
538 if let Some(pct) = parse_pct(s) {
539 return Some(PxPct::Pct(pct.0));
540 }
541 None
542}
543
544fn parse_pxpctauto(s: &str) -> Option<PxPctAuto> {
545 if s == "auto" {
546 return Some(PxPctAuto::Auto);
547 }
548 match parse_px_pct(s) {
549 Some(PxPct::Px(px)) => Some(PxPctAuto::Px(px)),
550 Some(PxPct::Pct(pct)) => Some(PxPctAuto::Pct(pct)),
551 None => None,
552 }
553}
554
555fn get_rgb_value(s: &str) -> Option<(usize, usize)> {
556 let start = s.find('(').unwrap_or(0);
557 let end = s[start..].find(')').unwrap_or(0);
558 if end > start {
559 Some((start + 1, end + 1))
560 } else {
561 None
562 }
563}
564
565fn parse_color(s: &str) -> Option<Color> {
566 if s.starts_with('#') {
567 return Color::parse(s);
568 }
569 if s.starts_with("rgba") {
570 let (start, end) = get_rgb_value(s)?;
571 return parse_rgba(&s[start..end]);
572 }
573 if s.starts_with("rgb") {
574 let (start, end) = get_rgb_value(s)?;
575 return parse_rgb(&s[start..end]);
576 }
577 if s.starts_with("hsl") || s.starts_with("hwb") {
578 return None;
580 }
581 Color::parse(s)
582}
583
584fn parse_i32(s: &str) -> Option<i32> {
585 s.parse::<i32>().ok()
586}
587
588pub const fn parse_cursor_style(s: &str) -> Option<CursorStyle> {
589 match s.as_bytes() {
590 b"default" => Some(CursorStyle::Default),
591 b"pointer" => Some(CursorStyle::Pointer),
592 b"text" => Some(CursorStyle::Text),
593 b"col-resize" => Some(CursorStyle::ColResize),
594 b"row-resize" => Some(CursorStyle::RowResize),
595 b"w-resize" => Some(CursorStyle::WResize),
596 b"e-resize" => Some(CursorStyle::EResize),
597 b"s-resize" => Some(CursorStyle::SResize),
598 b"n-resize" => Some(CursorStyle::NResize),
599 b"nw-resize" => Some(CursorStyle::NwResize),
600 b"ne-resize" => Some(CursorStyle::NeResize),
601 b"sw-resize" => Some(CursorStyle::SwResize),
602 b"se-resize" => Some(CursorStyle::SeResize),
603 b"nesw-resize" => Some(CursorStyle::NeswResize),
604 b"nwse-resize" => Some(CursorStyle::NwseResize),
605 _ => None,
606 }
607}
608
609fn to_owned(s: &str) -> Option<String> {
610 Some(s.to_string())
611}
612
613pub const fn parse_font_weight(s: &str) -> Option<Weight> {
614 match s.as_bytes() {
615 b"100" | b"thin" => Some(Weight(100)),
616 b"200" => Some(Weight(200)),
617 b"300" => Some(Weight(300)),
618 b"400" | b"normal" => Some(Weight(400)),
619 b"500" => Some(Weight(500)),
620 b"600" => Some(Weight(600)),
621 b"700" | b"bold" => Some(Weight(700)),
622 b"800" => Some(Weight(800)),
623 b"900" => Some(Weight(900)),
624 _ => None,
625 }
626}
627
628pub const fn parse_font_style(s: &str) -> Option<floem::text::Style> {
629 match s.as_bytes() {
630 b"normal" => Some(floem::text::Style::Normal),
631 b"italic" => Some(floem::text::Style::Italic),
632 b"oblique" => Some(floem::text::Style::Oblique),
633 _ => None,
634 }
635}
636
637pub const fn parse_text_overflow(s: &str) -> Option<TextOverflow> {
638 match s.as_bytes() {
639 b"clip" => Some(TextOverflow::Clip),
640 b"ellipsis" => Some(TextOverflow::Ellipsis),
641 b"wrap" => Some(TextOverflow::Wrap),
642 _ => None,
643 }
644}
645
646pub fn parse_gap(s: &str) -> Option<(PxPct, Option<PxPct>)> {
647 let mut st = s.split_whitespace();
648 let row_val = st.next()?;
649 let row_px_pct = parse_px_pct(row_val)?;
650 let col_val = st.next()?;
651 let col_px_pct = parse_px_pct(col_val);
652 Some((row_px_pct, col_px_pct))
653}
654#[allow(clippy::many_single_char_names)]
655fn parse_box_shadow(s: &str) -> Option<BoxShadow> {
656 let mut parts = SmallVec::<[&str; 5]>::new_const();
657 let mut start = 0;
658 let mut after_wp = false;
659 for (i, c) in s.char_indices() {
660 if c.is_whitespace() {
661 parts.push(&s[start..i]);
662 after_wp = true;
663 start = i + 1;
664 } else if after_wp && c.is_alphabetic() {
665 break;
666 } else {
667 after_wp = false;
668 }
669 }
670 parts.push(&s[start..]);
671 match parts.as_slice() {
672 ["none"] => Some(BoxShadow::default()),
673 [a, b] => parse_box_shadow_2([a, b]),
674 [a, b, c] => parse_box_shadow_3([a, b, c]),
675 [a, b, c, d] => parse_box_shadow_4([a, b, c, d]),
676 [a, b, c, d, e] => parse_box_shadow_5([a, b, c, d, e]),
677 _ => None,
678 }
679}
680
681fn parse_box_shadow_2([a, b]: [&str; 2]) -> Option<BoxShadow> {
682 if let (Some(h_offset), Some(v_offset)) = (parse_px_pct(a), parse_px_pct(b)) {
683 return Some(BoxShadow {
684 h_offset,
685 v_offset,
686 ..BoxShadow::default()
687 });
688 };
689 None
690}
691
692fn parse_box_shadow_3([a, b, c]: [&str; 3]) -> Option<BoxShadow> {
693 if let (Some(h_offset), Some(v_offset), Some(color)) =
695 (parse_px_pct(a), parse_px_pct(b), parse_color(c))
696 {
697 return Some(BoxShadow {
698 color,
699 h_offset,
700 v_offset,
701 ..BoxShadow::default()
702 });
703 }
704
705 if let (Some(color), Some(h_offset), Some(v_offset)) =
707 (parse_color(a), parse_px_pct(b), parse_px_pct(c))
708 {
709 return Some(BoxShadow {
710 color,
711 h_offset,
712 v_offset,
713 ..BoxShadow::default()
714 });
715 }
716 if let (Some(h_offset), Some(v_offset), Some(blur_radius)) =
718 (parse_px_pct(a), parse_px_pct(b), parse_px_pct(c))
719 {
720 return Some(BoxShadow {
721 blur_radius,
722 h_offset,
723 v_offset,
724 ..BoxShadow::default()
725 });
726 }
727
728 None
729}
730#[allow(clippy::many_single_char_names)]
731fn parse_box_shadow_4([a, b, c, d]: [&str; 4]) -> Option<BoxShadow> {
732 if let (Some(h_offset), Some(v_offset), Some(blur_radius), Some(color)) = (
734 parse_px_pct(a),
735 parse_px_pct(b),
736 parse_px_pct(c),
737 parse_color(d),
738 ) {
739 return Some(BoxShadow {
740 color,
741 blur_radius,
742 h_offset,
743 v_offset,
744 ..BoxShadow::default()
745 });
746 }
747 if let (Some(color), Some(h_offset), Some(v_offset), Some(blur_radius)) = (
749 parse_color(a),
750 parse_px_pct(b),
751 parse_px_pct(c),
752 parse_px_pct(d),
753 ) {
754 return Some(BoxShadow {
755 color,
756 blur_radius,
757 h_offset,
758 v_offset,
759 ..BoxShadow::default()
760 });
761 }
762 if let (Some(h_offset), Some(v_offset), Some(blur_radius), Some(spread)) = (
764 parse_px_pct(a),
765 parse_px_pct(b),
766 parse_px_pct(c),
767 parse_px_pct(d),
768 ) {
769 return Some(BoxShadow {
770 blur_radius,
771 spread,
772 h_offset,
773 v_offset,
774 ..BoxShadow::default()
775 });
776 }
777 None
778}
779#[allow(clippy::many_single_char_names)]
780fn parse_box_shadow_5([a, b, c, d, e]: [&str; 5]) -> Option<BoxShadow> {
781 if let (Some(h_offset), Some(v_offset), Some(blur_radius), Some(spread), Some(color)) = (
783 parse_px_pct(a),
784 parse_px_pct(b),
785 parse_px_pct(c),
786 parse_px_pct(d),
787 parse_color(e),
788 ) {
789 return Some(BoxShadow {
790 h_offset,
791 v_offset,
792 blur_radius,
793 spread,
794 color,
795 });
796 }
797 if let (Some(color), Some(h_offset), Some(v_offset), Some(blur_radius), Some(spread)) = (
799 parse_color(a),
800 parse_px_pct(b),
801 parse_px_pct(c),
802 parse_px_pct(d),
803 parse_px_pct(e),
804 ) {
805 return Some(BoxShadow {
806 h_offset,
807 v_offset,
808 blur_radius,
809 spread,
810 color,
811 });
812 }
813 None
814}
815
816fn parse_rgba(s: &str) -> Option<Color> {
817 let mut parts = SmallVec::<[&str; 4]>::new_const();
818 parts.extend(s.split(',').map(str::trim));
819 if let [r, g, b, a] = parts.as_slice() {
820 if let (Some(r), Some(g), Some(b), Some(a)) = (
821 parse_rgb_value(r),
822 parse_rgb_value(g),
823 parse_rgb_value(b),
824 parse_rgb_alpha(a),
825 ) {
826 return Some(Color::rgba8(r, g, b, a));
827 }
828 }
829 None
830}
831
832fn parse_rgb(s: &str) -> Option<Color> {
833 let mut parts = SmallVec::<[&str; 3]>::new_const();
834 parts.extend(s.split(',').map(str::trim));
835 if let [r, g, b] = parts.as_slice() {
836 if let (Some(r), Some(g), Some(b)) =
837 (parse_rgb_value(r), parse_rgb_value(g), parse_rgb_value(b))
838 {
839 return Some(Color::rgb8(r, g, b));
840 }
841 }
842 None
843}
844
845fn parse_rgb_value(s: &str) -> Option<u8> {
846 s.parse::<u8>().ok()
847}
848
849fn parse_rgb_alpha(s: &str) -> Option<u8> {
850 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
851 s.parse::<f64>()
852 .map(|v| (v.clamp(0.0, 1.0) * 255.) as u8)
853 .ok()
854}
855
856fn parse_transition(s: &str) -> Option<(String, Transition)> {
857 let mut parts = s.split_whitespace();
858 let key = parts.next()?;
859 let duration_str = parts.next()?;
860 let duration = parse_duration(duration_str)?;
861 let transition = Transition::linear(duration);
862 Some((key.to_string(), transition))
863}
864
865fn parse_duration(s: &str) -> Option<Duration> {
866 if let Some(ms) = s.strip_suffix("ms") {
867 if let Ok(d) = ms.parse::<u64>() {
868 return Some(Duration::from_millis(d));
869 }
870 }
871 if let Some(seconds) = s.strip_suffix('s') {
872 if let Ok(f) = seconds.parse::<f64>() {
873 if f > 0. {
874 let ms = (f * 1000.) as u64;
875 return Some(Duration::from_millis(ms));
876 }
877 }
878 }
879 None
880}
881
882const fn parse_user_select(s: &str) -> Option<bool> {
883 match s.as_bytes() {
884 b"none" => Some(false),
885 b"auto" => Some(true),
886 _ => None,
887 }
888}
889
890fn parse_border(s: &str) -> Option<BorderDef> {
891 let mut parts = s.split_whitespace();
892 let first = parts.next();
893 let second = parts.next();
894 let mut retval = BorderDef {
895 width: None,
896 color: None,
897 };
898 let mut parse_val = |val: &str| {
899 if let Some(px) = parse_px(val) {
900 retval.width = Some(px);
901 return Some(());
902 } else if let Some(color) = parse_color(val) {
903 retval.color = Some(color);
904 return Some(());
905 }
906 None
907 };
908 match (first, second) {
909 (Some(val), None) => {
910 parse_val(val)?;
911 }
912 (Some(f), Some(s)) => {
913 parse_val(f)?;
914 parse_val(s)?;
915 }
916 _ => return None,
917 }
918 Some(retval)
919}
920
921#[cfg(test)]
922mod tests {
923 use std::time::Duration;
924
925 use floem::{
926 peniko::Color,
927 unit::{Px, PxPct},
928 };
929
930 use crate::declaration::{
931 get_rgb_value, parse_box_shadow_5, parse_rgb, parse_rgb_value, parse_rgba, BorderDef,
932 };
933
934 use super::{parse_border, parse_duration, parse_rgb_alpha};
935
936 #[test]
937 fn duration() {
938 let sec = parse_duration("1s").unwrap();
939 assert!(sec == Duration::from_secs(1));
940 let tenth_sec = parse_duration("0.1s").unwrap();
941 assert!(tenth_sec == Duration::from_millis(100));
942 let ms = parse_duration("150ms").unwrap();
943 assert!(ms == Duration::from_millis(150));
944 let value = parse_duration("1");
946 assert!(value.is_none());
947 }
948
949 #[test]
950 #[rustfmt::skip]
951 fn border() {
952 let v = parse_border("10px").unwrap();
953 assert!(v == BorderDef { width: Some(Px(10.0)), color: None });
954 let v = parse_border("10px red").unwrap();
955 assert!(v == BorderDef { width: Some(Px(10.0)), color: Some(Color::RED) });
956 let v = parse_border("red").unwrap();
957 assert!(v == BorderDef { width: None, color: Some(Color::RED) });
958 }
959
960 #[test]
961 fn rgb_alpha() {
962 let v = parse_rgb_alpha("0.1").unwrap();
963 assert!(v == 25);
964 let v = parse_rgb_alpha("1.1").unwrap();
965 assert!(v == 255);
966 let v = parse_rgb_alpha("0").unwrap();
967 assert!(v == 0);
968 }
969
970 #[test]
971 fn rgb_value() {
972 let v = parse_rgb_value("100").unwrap();
973 assert!(v == 100);
974 assert!(parse_rgb_value("300").is_none());
975 }
976
977 #[test]
978 #[rustfmt::skip]
979 fn rgb() {
980 let v = parse_rgb("21, 22, 23").unwrap();
981 assert!(v == Color {r: 21, g: 22, b: 23, a: 255 });
982 assert!(parse_rgb("21, 22, 280").is_none());
983 }
984
985 #[test]
986 #[rustfmt::skip]
987 fn rgba() {
988 let v = parse_rgba("21, 22, 23, 0.65").unwrap();
989 assert!(v == Color {r: 21, g: 22, b: 23, a: 165 });
990 assert!(parse_rgba("21, 22, 280, 0.1").is_none());
991 }
992
993 #[test]
994 fn find_rgba_value() {
995 let (start, end) = get_rgb_value("rgba(21, 22, 23, 0.65)").unwrap();
996 assert!(start == 5);
997 assert!(end == 18);
998 let (start, end) = get_rgb_value("rgb(21, 22, 23)").unwrap();
999 assert!(start == 4);
1000 assert!(end == 12);
1001 assert!(get_rgb_value("rgb(21, 22, 23").is_none());
1002 }
1003
1004 #[test]
1005 fn box_shadow_5() {
1006 let v = parse_box_shadow_5(["4px", "8px", "10px", "15px", "black"]).unwrap();
1007 assert!(v.h_offset == PxPct::Px(4.0));
1008 assert!(v.v_offset == PxPct::Px(8.0));
1009 assert!(v.blur_radius == PxPct::Px(10.0));
1010 assert!(v.spread == PxPct::Px(15.0));
1011 assert!(v.color == Color::BLACK);
1012 let v = parse_box_shadow_5(["green", "4px", "8px", "10px", "15px"]).unwrap();
1013 assert!(v.h_offset == PxPct::Px(4.0));
1014 assert!(v.v_offset == PxPct::Px(8.0));
1015 assert!(v.blur_radius == PxPct::Px(10.0));
1016 assert!(v.spread == PxPct::Px(15.0));
1017 assert!(v.color == Color::GREEN);
1018 }
1019}