floem_css_parser/
declaration.rs

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        // TODO Support these maybe
579        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    // <h_offset> <v_offset> <color>
694    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    // <color> <h_offset> <v_offset>
706    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    // <h_offset> <v_offset> <blur>
717    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    // <h_offset> <v_offset> <blur_radius> <color>
733    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    // <color> <h_offset> <v_offset> <blur_radius>
748    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    // <h_offset> <v_offset> <blur_radius> <blur_spread>
763    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    // <h_offset> <v_offset> <blur_radius> <blur_spread> <color>
782    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    // <color> <h_offset> <v_offset> <blur_radius> <blur_spread>
798    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        // This should fail
945        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}