Skip to main content

orinium_browser/engine/layouter/builder/
apply.rs

1use std::sync::LazyLock;
2
3use smol_str::SmolStr;
4
5use crate::engine::css::values::{CssValue, Unit};
6use crate::engine::layouter::builder::css_resolve::parse_grid_template;
7use crate::engine::layouter::types::{CursorKeyword, CursorStyle, CustomCursor};
8use crate::engine::renderer_model::AffineTransform;
9
10use ui_layout::{
11    AlignContent, AlignItems, BoxSizing, Display, FlexDirection, FlexWrap, JustifyContent,
12    JustifyItems, Length, LengthOrAuto, OuterDisplay, Position, Style,
13};
14
15use super::{
16    CalcValue, apply_background_shorthand_geometry, extract_font_families, length_to_px,
17    one_or_two_values, parse_background_position, parse_background_repeat,
18    parse_background_shorthand, parse_background_size, parse_grid_line, parse_grid_line_end,
19    parse_grid_placement, parse_grid_template_areas, parse_grid_tracks, resolve_calc_value,
20    resolve_css_color, resolve_css_len, resolve_css_len_auto, resolve_font_size_px,
21};
22use crate::engine::layouter::builder::parse_anchor_spec;
23use crate::engine::layouter::types::{
24    AnchorInsetSide, Background, BorderRadius, BorderStyle, ClipPath, Color, ColorScheme,
25    ContainerStyle, CornerRadius, CssClear, CssFloat, Direction, FontStyle, FontWeight, LineHeight,
26    Overflow, TextAlign, TextDecoration, TextFlowStyle, TextStyle, TextTransform, VerticalAlign,
27    Visibility, WhiteSpace, WritingMode,
28};
29
30macro_rules! apply_property {
31    ($field:ident, $target:ident, $parent:ident, $default:expr, $value:expr, $parse:expr) => {
32        $target.$field = if let CssValue::Keyword(v) = $value
33            && v == "initial"
34        {
35            $default.$field.clone()
36        } else if let CssValue::Keyword(v) = $value
37            && v == "inherit"
38        {
39            $parent.$field.clone()
40        } else if let CssValue::Keyword(v) = $value
41            && v == "unset"
42        {
43            $default.$field.clone()
44        } else {
45            $parse
46        };
47    };
48
49    ($field:ident, $target:ident, $parent:ident, $default:expr, $value:expr, $pattern:pat, $parse:expr) => {
50        $target.$field = if let CssValue::Keyword(v) = $value
51            && v == "initial"
52        {
53            $default.$field.clone()
54        } else if let CssValue::Keyword(v) = $value
55            && v == "inherit"
56        {
57            $parent.$field.clone()
58        } else if let CssValue::Keyword(v) = $value
59            && v == "unset"
60        {
61            $default.$field.clone()
62        } else if let $pattern = $value {
63            $parse
64        } else {
65            return None;
66        };
67    };
68}
69
70static DEFAULT_STYLE: LazyLock<Style> = LazyLock::new(Style::default);
71static DEFAULT_CONTAINER_STYLE: LazyLock<ContainerStyle> = LazyLock::new(ContainerStyle::default);
72static DEFAULT_TEXT_STYLE: LazyLock<TextStyle> = LazyLock::new(TextStyle::default);
73static DEFAULT_TEXT_FLOW_STYLE: LazyLock<TextFlowStyle> = LazyLock::new(TextFlowStyle::default);
74
75pub fn blockify_out_of_flow_positioned(style: &mut Style) {
76    if style.position.kind.is_out_of_flow()
77        && style.display.outer() == Some(OuterDisplay::Inline)
78        && let Display::OutsideInner { inner, .. } = style.display
79    {
80        style.display = Display::OutsideInner {
81            outer: OuterDisplay::Block,
82            inner,
83        };
84    }
85}
86
87fn resolve_flex_shorthand(
88    value: &CssValue,
89    text_flow_style: &TextFlowStyle,
90) -> Option<(f32, f32, LengthOrAuto)> {
91    let zero_basis = LengthOrAuto::Length(Length::Percent(0.0));
92
93    match value {
94        CssValue::Keyword(keyword) => match keyword.as_str() {
95            "none" => Some((0.0, 0.0, LengthOrAuto::Auto)),
96            "auto" => Some((1.0, 1.0, LengthOrAuto::Auto)),
97            "initial" => Some((0.0, 1.0, LengthOrAuto::Auto)),
98            "unset" => Some((0.0, 1.0, LengthOrAuto::Auto)),
99            _ => None,
100        },
101        CssValue::Number(grow) if *grow >= 0.0 => Some((*grow, 1.0, zero_basis)),
102        CssValue::Length(_, _) => Some((
103            1.0,
104            1.0,
105            resolve_css_len_auto("flex", value, text_flow_style)?,
106        )),
107        CssValue::List(values) => match values.as_slice() {
108            [CssValue::Number(grow), CssValue::Number(shrink)]
109                if *grow >= 0.0 && *shrink >= 0.0 =>
110            {
111                Some((*grow, *shrink, zero_basis))
112            }
113            [CssValue::Number(grow), basis] if *grow >= 0.0 => Some((
114                *grow,
115                1.0,
116                resolve_css_len_auto("flex", basis, text_flow_style)?,
117            )),
118            [CssValue::Number(grow), CssValue::Number(shrink), basis]
119                if *grow >= 0.0 && *shrink >= 0.0 =>
120            {
121                Some((
122                    *grow,
123                    *shrink,
124                    resolve_css_len_auto("flex", basis, text_flow_style)?,
125                ))
126            }
127            _ => None,
128        },
129        _ => None,
130    }
131}
132
133fn flex_direction_keyword(keyword: &str) -> Option<FlexDirection> {
134    match keyword {
135        "row" => Some(FlexDirection::Row),
136        "column" => Some(FlexDirection::Column),
137        "row-reverse" => Some(FlexDirection::RowReverse),
138        "column-reverse" => Some(FlexDirection::ColumnReverse),
139        _ => None,
140    }
141}
142
143fn flex_wrap_keyword(keyword: &str) -> Option<FlexWrap> {
144    match keyword {
145        "nowrap" => Some(FlexWrap::NoWrap),
146        "wrap" => Some(FlexWrap::Wrap),
147        "wrap-reverse" => Some(FlexWrap::WrapReverse),
148        _ => None,
149    }
150}
151
152fn resolve_flex_flow(value: &CssValue) -> Option<(FlexDirection, FlexWrap)> {
153    let values = match value {
154        CssValue::Keyword(keyword) if keyword == "initial" => {
155            return Some((FlexDirection::Row, FlexWrap::NoWrap));
156        }
157        CssValue::Keyword(_) => std::slice::from_ref(value),
158        CssValue::List(values) if (1..=2).contains(&values.len()) => values.as_slice(),
159        _ => return None,
160    };
161    let mut direction = None;
162    let mut wrap = None;
163    for value in values {
164        let CssValue::Keyword(keyword) = value else {
165            return None;
166        };
167        if let Some(parsed) = flex_direction_keyword(keyword) {
168            if direction.replace(parsed).is_some() {
169                return None;
170            }
171        } else {
172            let parsed = flex_wrap_keyword(keyword)?;
173            if wrap.replace(parsed).is_some() {
174                return None;
175            }
176        }
177    }
178
179    Some((
180        direction.unwrap_or(FlexDirection::Row),
181        wrap.unwrap_or(FlexWrap::NoWrap),
182    ))
183}
184
185fn resolve_justify_items(keyword: &str) -> Option<JustifyItems> {
186    match keyword {
187        "stretch" => Some(JustifyItems::Stretch),
188        "flex-start" | "start" => Some(JustifyItems::Start),
189        "center" => Some(JustifyItems::Center),
190        "flex-end" | "end" => Some(JustifyItems::End),
191        _ => None,
192    }
193}
194
195fn resolve_align_items(keyword: &str) -> Option<AlignItems> {
196    match keyword {
197        "stretch" => Some(AlignItems::Stretch),
198        "flex-start" | "start" => Some(AlignItems::Start),
199        "center" => Some(AlignItems::Center),
200        "flex-end" | "end" => Some(AlignItems::End),
201        _ => None,
202    }
203}
204
205/// Convert an angle-valued `CssValue` to radians.
206fn transform_angle_radians(value: &CssValue) -> Option<f32> {
207    match value {
208        CssValue::Length(v, Unit::Deg) => Some(v.to_radians()),
209        CssValue::Length(v, Unit::Rad) => Some(*v),
210        CssValue::Length(v, Unit::Grad) => Some(v * std::f32::consts::PI / 200.0),
211        CssValue::Length(v, Unit::Turn) => Some(v * 2.0 * std::f32::consts::PI),
212        CssValue::Number(n) => Some(n.to_radians()),
213        _ => None,
214    }
215}
216
217/// Resolve a length-ish transform argument to pixels, falling back to the raw
218/// value for unitless numbers.
219///
220/// `element_size` is the element's resolved (width, height) in pixels when
221/// known. Per CSS Transforms, `translate` percentages resolve against the
222/// element's own box dimensions on the appropriate axis, not against font-size.
223/// When `element_size` is `None`, fall back to `font_size` for compatibility.
224fn transform_length_px(
225    name: &str,
226    value: &CssValue,
227    text_flow_style: &TextFlowStyle,
228    element_size: Option<(f32, f32)>,
229    is_x: bool,
230) -> Option<f32> {
231    match value {
232        CssValue::Number(n) => Some(*n),
233        _ => {
234            let len = resolve_css_len(name, std::slice::from_ref(value), text_flow_style)?;
235            match len {
236                Length::Px(v) => Some(v),
237                Length::Percent(p) => {
238                    let base = if is_x {
239                        element_size
240                            .map(|(w, _)| w)
241                            .unwrap_or(text_flow_style.font_size)
242                    } else {
243                        element_size
244                            .map(|(_, h)| h)
245                            .unwrap_or(text_flow_style.font_size)
246                    };
247                    Some(base * p / 100.0)
248                }
249                _ => Some(length_to_px(&len, text_flow_style.font_size)),
250            }
251        }
252    }
253}
254
255/// Flatten the comma-separated argument groups of a transform function into a
256/// single component list, e.g. `scale(1.5, 2)` and `scale(1.5 2)` both yield
257/// `[Number(1.5), Number(2)]`.
258fn flatten_function_args(args: &[Vec<CssValue>]) -> Vec<&CssValue> {
259    args.iter().flatten().collect()
260}
261
262/// Parse a single CSS `transform()` function into an [`AffineTransform`].
263///
264/// CSS composes transform functions left-to-right (the first listed function is
265/// applied first), so callers compose results with
266/// `accumulated.then(&next)`.
267///
268/// `style` provides the element's resolved width/height so that `translate`
269/// percentages can resolve against the element's own box dimensions per CSS
270/// Transforms.
271fn parse_transform_function(
272    name: &str,
273    func: &CssValue,
274    style: &Style,
275    text_flow_style: &TextFlowStyle,
276) -> Option<AffineTransform> {
277    let CssValue::Function(fn_name, args) = func else {
278        return None;
279    };
280    let flat = flatten_function_args(args);
281
282    let element_width = match style.size.width {
283        LengthOrAuto::Length(Length::Px(v)) => Some(v),
284        _ => None,
285    };
286    let element_height = match style.size.height {
287        LengthOrAuto::Length(Length::Px(v)) => Some(v),
288        _ => None,
289    };
290    let element_size = match (element_width, element_height) {
291        (Some(w), Some(h)) => Some((w, h)),
292        _ => None,
293    };
294
295    match fn_name.to_ascii_lowercase().as_str() {
296        "none" => Some(AffineTransform::identity()),
297
298        "rotate" => {
299            let angle = transform_angle_radians(flat.first()?)?;
300            Some(AffineTransform::rotate(angle))
301        }
302
303        "scale" => {
304            let sx = transform_length_px(name, flat.first()?, text_flow_style, element_size, true)?;
305            let sy = flat
306                .get(1)
307                .and_then(|v| transform_length_px(name, v, text_flow_style, element_size, true))
308                .unwrap_or(sx);
309            Some(AffineTransform::scale(sx, sy))
310        }
311
312        "translate" => {
313            let tx = transform_length_px(name, flat.first()?, text_flow_style, element_size, true)?;
314            let ty = flat
315                .get(1)
316                .and_then(|v| transform_length_px(name, v, text_flow_style, element_size, false))
317                .unwrap_or(0.0);
318            Some(AffineTransform::translate(tx, ty))
319        }
320
321        "translatex" => {
322            let tx = transform_length_px(name, flat.first()?, text_flow_style, element_size, true)?;
323            Some(AffineTransform::translate(tx, 0.0))
324        }
325
326        "translatey" => {
327            let ty =
328                transform_length_px(name, flat.first()?, text_flow_style, element_size, false)?;
329            Some(AffineTransform::translate(0.0, ty))
330        }
331
332        _ => None,
333    }
334}
335
336/// Parse the full `transform` property value into a single composed
337/// [`AffineTransform`]. Returns `None` for `none` (identity, no transform) or
338/// when the value cannot be parsed.
339fn parse_transform(
340    name: &str,
341    value: &CssValue,
342    style: &Style,
343    text_flow_style: &TextFlowStyle,
344) -> Option<AffineTransform> {
345    match value {
346        CssValue::Keyword(k) if k.eq_ignore_ascii_case("none") => None,
347
348        CssValue::List(funcs) => {
349            let mut accumulated = AffineTransform::identity();
350            for func in funcs {
351                let t = parse_transform_function(name, func, style, text_flow_style)?;
352                accumulated = accumulated.then(&t);
353            }
354            (!accumulated.eq(&AffineTransform::identity())).then_some(accumulated)
355        }
356        CssValue::Function(..) => {
357            let t = parse_transform_function(name, value, style, text_flow_style)?;
358            (!t.eq(&AffineTransform::identity())).then_some(t)
359        }
360        _ => None,
361    }
362}
363
364/// Apply `background-color` without touching image/gradient layers. The
365/// solid color is stored as the independent bottom color of `Background`.
366fn set_background_color(container_style: &mut ContainerStyle, color: Color) {
367    container_style.background.color = Some(color);
368}
369
370#[allow(clippy::too_many_arguments)]
371pub fn apply_declaration(
372    name: &str,
373    value: &CssValue,
374    style: &mut Style,
375    container_style: &mut ContainerStyle,
376    text_style: &mut TextStyle,
377    text_flow_style: &mut TextFlowStyle,
378    parent_style: &Style,
379    parent_container_style: &ContainerStyle,
380    parent_text_style: &TextStyle,
381    parent_text_flow_style: &TextFlowStyle,
382    overflow: &mut Overflow,
383    color_scheme: ColorScheme,
384) -> Option<()> {
385    fn expand_box<T: Clone, F>(
386        name: &str,
387        value: &CssValue,
388        text_flow_style: &TextFlowStyle,
389        resolve: &impl Fn(&str, &CssValue, &TextFlowStyle) -> Option<T>,
390        mut set: F,
391    ) -> Option<()>
392    where
393        F: FnMut(T, T, T, T),
394    {
395        let resolve = |v: &CssValue| -> Option<T> { resolve(name, v, text_flow_style) };
396
397        match value {
398            CssValue::List(values) => {
399                let vals: Vec<T> = values.iter().map(resolve).collect::<Option<_>>()?;
400
401                match vals.as_slice() {
402                    [a] => set(a.clone(), a.clone(), a.clone(), a.clone()),
403                    [v, h] => set(v.clone(), h.clone(), v.clone(), h.clone()),
404                    [t, h, b] => set(t.clone(), h.clone(), b.clone(), h.clone()),
405                    [t, r, b, l] => set(t.clone(), r.clone(), b.clone(), l.clone()),
406                    _ => return None,
407                }
408            }
409
410            _ => {
411                let v = resolve(value)?;
412                set(v.clone(), v.clone(), v.clone(), v);
413            }
414        }
415
416        Some(())
417    }
418
419    /// Split a border-radius value list on the `Keyword("/")` separator into
420    /// its horizontal and vertical components. Without a slash the vertical
421    /// list is empty (meaning "use the horizontal value").
422    fn split_radius_lists<'a>(value: &'a CssValue) -> (Vec<&'a CssValue>, Vec<&'a CssValue>) {
423        match value {
424            CssValue::List(vals) => {
425                let mut horiz: Vec<&'a CssValue> = Vec::new();
426                let mut vert: Vec<&'a CssValue> = Vec::new();
427                let mut after_slash = false;
428                for v in vals {
429                    if let CssValue::Keyword(k) = v
430                        && k == "/"
431                    {
432                        after_slash = true;
433                        continue;
434                    }
435                    if after_slash {
436                        vert.push(v);
437                    } else {
438                        horiz.push(v);
439                    }
440                }
441                (horiz, vert)
442            }
443            _ => (vec![value], Vec::new()),
444        }
445    }
446
447    /// Expand a 1/2/3/4-value list to per-corner lengths in CSS order
448    /// [top-left, top-right, bottom-right, bottom-left].
449    fn expand_radius_axis(
450        name: &str,
451        values: &[&CssValue],
452        text_flow_style: &TextFlowStyle,
453    ) -> Option<[Length; 4]> {
454        let vals: Vec<Length> = values
455            .iter()
456            .map(|v| resolve_css_len(name, std::slice::from_ref(v), text_flow_style))
457            .collect::<Option<_>>()?;
458        match vals.as_slice() {
459            [a] => Some([a.clone(), a.clone(), a.clone(), a.clone()]),
460            [v, h] => Some([v.clone(), h.clone(), v.clone(), h.clone()]),
461            [t, h, b] => Some([t.clone(), h.clone(), b.clone(), h.clone()]),
462            [t, r, b, l] => Some([t.clone(), r.clone(), b.clone(), l.clone()]),
463            _ => None,
464        }
465    }
466
467    /// Parse a `border-radius` shorthand value (1-4 lengths per axis, optional
468    /// elliptical `/` form) into the four corners.
469    fn parse_border_radius_shorthand(
470        name: &str,
471        value: &CssValue,
472        text_flow_style: &TextFlowStyle,
473    ) -> Option<(CornerRadius, CornerRadius, CornerRadius, CornerRadius)> {
474        let (horiz, vert) = split_radius_lists(value);
475        let h = expand_radius_axis(name, &horiz, text_flow_style)?;
476        let v = if vert.is_empty() {
477            h.clone()
478        } else {
479            expand_radius_axis(name, &vert, text_flow_style)?
480        };
481        Some((
482            CornerRadius {
483                x: h[0].clone(),
484                y: v[0].clone(),
485            },
486            CornerRadius {
487                x: h[1].clone(),
488                y: v[1].clone(),
489            },
490            CornerRadius {
491                x: h[2].clone(),
492                y: v[2].clone(),
493            },
494            CornerRadius {
495                x: h[3].clone(),
496                y: v[3].clone(),
497            },
498        ))
499    }
500
501    /// Parse a single-corner radius value: one length, two lengths (`rx ry`)
502    /// or the elliptical `rx / ry` form.
503    fn parse_corner_radius(
504        name: &str,
505        value: &CssValue,
506        text_flow_style: &TextFlowStyle,
507    ) -> Option<CornerRadius> {
508        let (horiz, vert) = split_radius_lists(value);
509        let x = expand_radius_axis(name, &horiz, text_flow_style)?;
510        let y = if vert.is_empty() {
511            x.clone()
512        } else {
513            expand_radius_axis(name, &vert, text_flow_style)?
514        };
515        Some(CornerRadius {
516            x: x[0].clone(),
517            y: y[0].clone(),
518        })
519    }
520
521    /// Whether a single `overflow` keyword enables scrolling on an axis.
522    fn overflow_scrollable(keyword: &str) -> Option<bool> {
523        match keyword {
524            "visible" | "clip" => Some(false),
525            "hidden" | "scroll" | "auto" => Some(true),
526            _ => None,
527        }
528    }
529
530    /// Expand an `overflow` shorthand (1 or 2 keywords) into per-axis flags.
531    fn overflow_flags(value: &CssValue) -> Option<(bool, bool)> {
532        match value {
533            CssValue::Keyword(k) => overflow_scrollable(k).map(|b| (b, b)),
534            CssValue::List(l) => {
535                let x = match l.first()? {
536                    CssValue::Keyword(k) => overflow_scrollable(k)?,
537                    _ => return None,
538                };
539                let y = match l.get(1) {
540                    Some(CssValue::Keyword(k)) => overflow_scrollable(k)?,
541                    Some(_) => return None,
542                    None => x,
543                };
544                Some((x, y))
545            }
546            _ => None,
547        }
548    }
549
550    fn parse_border_shorthand(
551        name: &str,
552        value: &CssValue,
553        text_flow_style: &TextFlowStyle,
554        color_scheme: ColorScheme,
555    ) -> Option<(Option<Length>, Option<BorderStyle>, Option<Color>)> {
556        let mut width: Option<Length> = None;
557        let mut style_v: Option<BorderStyle> = None;
558        let mut color_v: Option<Color> = None;
559
560        let items: Vec<&CssValue> = match value {
561            CssValue::List(values) => values.iter().collect(),
562            _ => vec![value],
563        };
564
565        for v in items {
566            let token = v;
567
568            // try as length (numeric lengths)
569            if width.is_none()
570                && let Some(l) = resolve_css_len(name, std::slice::from_ref(token), text_flow_style)
571            {
572                width = Some(l);
573                continue;
574            }
575
576            // try as width keyword (thin/medium/thick). Check keywords before style keywords.
577            if width.is_none()
578                && let CssValue::Keyword(s) = token
579            {
580                match s.as_str().to_ascii_lowercase().as_str() {
581                    "thin" => {
582                        width = Some(Length::Px(1.0));
583                        continue;
584                    }
585                    "medium" => {
586                        width = Some(Length::Px(3.0));
587                        continue;
588                    }
589                    "midium" => {
590                        width = Some(Length::Px(3.0));
591                        continue;
592                    } // common misspelling
593                    "thick" => {
594                        width = Some(Length::Px(5.0));
595                        continue;
596                    }
597                    _ => {}
598                }
599            }
600
601            // try as style keyword
602            if style_v.is_none()
603                && let CssValue::Keyword(s) = token
604            {
605                let s_lower = s.as_str();
606                let parsed = match s_lower {
607                    "none" => Some(BorderStyle::None),
608                    "solid" => Some(BorderStyle::Solid),
609                    "dashed" => Some(BorderStyle::Dashed),
610                    "dotted" => Some(BorderStyle::Dotted),
611                    "inset" | "outset" | "groove" | "ridge" | "double" | "hidden" => {
612                        // stub
613                        style_v = Some(BorderStyle::Solid);
614                        continue;
615                    }
616                    _ => None,
617                };
618
619                if let Some(p) = parsed {
620                    style_v = Some(p);
621                    continue;
622                }
623            }
624
625            // try as color
626            if color_v.is_none()
627                && let Some(c) = resolve_css_color(name, token, color_scheme)
628            {
629                color_v = Some(c);
630                continue;
631            }
632
633            // unknown token: ignore
634        }
635
636        Some((width, style_v, color_v))
637    }
638
639    fn parse_clip_path(
640        name: &str,
641        value: &CssValue,
642        text_flow_style: &TextFlowStyle,
643    ) -> Option<ClipPath> {
644        match value {
645            CssValue::Keyword(kw) if kw.eq_ignore_ascii_case("none") => Some(ClipPath::None),
646
647            CssValue::Function(func, args) if func.eq_ignore_ascii_case("circle") => {
648                let mut radius = 0.5f32; // default: closest-side
649                let mut center_x = 0.5f32;
650                let mut center_y = 0.5f32;
651                let mut after_at = false;
652
653                for arg in args.iter().flatten() {
654                    match arg {
655                        CssValue::Keyword(k) if k.eq_ignore_ascii_case("at") => {
656                            after_at = true;
657                        }
658                        CssValue::Keyword(k) if k.eq_ignore_ascii_case("closest-side") => {
659                            if !after_at {
660                                radius = 0.5;
661                            }
662                        }
663                        CssValue::Keyword(k) if k.eq_ignore_ascii_case("farthest-side") => {
664                            if !after_at {
665                                radius = 1.0;
666                            }
667                        }
668                        CssValue::Keyword(k) if k.eq_ignore_ascii_case("farthest-corner") => {
669                            if !after_at {
670                                radius = std::f32::consts::SQRT_2;
671                            }
672                        }
673                        CssValue::Length(v, unit) => {
674                            let pct = match unit {
675                                Unit::Percent => v / 100.0,
676                                _ => resolve_css_len(
677                                    name,
678                                    std::slice::from_ref(arg),
679                                    text_flow_style,
680                                )
681                                .map(|l| length_to_px(&l, text_flow_style.font_size) / 100.0)
682                                .unwrap_or(0.0),
683                            };
684                            if after_at {
685                                // Two position values: x y
686                                if center_x != 0.5 {
687                                    // already have x, this is y
688                                    center_y = pct;
689                                } else {
690                                    center_x = pct;
691                                }
692                            } else {
693                                radius = pct;
694                            }
695                        }
696                        CssValue::Number(n) => {
697                            if after_at {
698                                if center_x != 0.5 {
699                                    center_y = *n;
700                                } else {
701                                    center_x = *n;
702                                }
703                            } else {
704                                radius = *n;
705                            }
706                        }
707                        _ => {}
708                    }
709                }
710                Some(ClipPath::Circle {
711                    radius: radius.clamp(0.0, 1.0),
712                    center_x: center_x.clamp(0.0, 1.0),
713                    center_y: center_y.clamp(0.0, 1.0),
714                })
715            }
716
717            CssValue::Function(func, args) if func.eq_ignore_ascii_case("ellipse") => {
718                let mut rx = 0.5f32;
719                let mut ry = 0.5f32;
720                let mut center_x = 0.5f32;
721                let mut center_y = 0.5f32;
722                let mut after_at = false;
723                let mut got_ry = false;
724
725                for arg in args.iter().flatten() {
726                    match arg {
727                        CssValue::Keyword(k) if k.eq_ignore_ascii_case("at") => {
728                            after_at = true;
729                        }
730                        CssValue::Keyword(k)
731                            if k.eq_ignore_ascii_case("closest-side")
732                                || k.eq_ignore_ascii_case("farthest-side")
733                                || k.eq_ignore_ascii_case("farthest-corner") =>
734                        {
735                            // keyword sizes: use defaults
736                        }
737                        CssValue::Length(v, unit) => {
738                            let pct = match unit {
739                                Unit::Percent => v / 100.0,
740                                _ => resolve_css_len(
741                                    name,
742                                    std::slice::from_ref(arg),
743                                    text_flow_style,
744                                )
745                                .map(|l| length_to_px(&l, text_flow_style.font_size) / 100.0)
746                                .unwrap_or(0.0),
747                            };
748                            if after_at {
749                                if center_x != 0.5 {
750                                    center_y = pct;
751                                } else {
752                                    center_x = pct;
753                                }
754                            } else if !got_ry {
755                                rx = pct;
756                                got_ry = true;
757                            } else {
758                                ry = pct;
759                            }
760                        }
761                        CssValue::Number(n) => {
762                            if after_at {
763                                if center_x != 0.5 {
764                                    center_y = *n;
765                                } else {
766                                    center_x = *n;
767                                }
768                            } else if !got_ry {
769                                rx = *n;
770                                got_ry = true;
771                            } else {
772                                ry = *n;
773                            }
774                        }
775                        _ => {}
776                    }
777                }
778                Some(ClipPath::Ellipse {
779                    rx: rx.clamp(0.0, 1.0),
780                    ry: ry.clamp(0.0, 1.0),
781                    center_x: center_x.clamp(0.0, 1.0),
782                    center_y: center_y.clamp(0.0, 1.0),
783                })
784            }
785
786            CssValue::Function(func, args) if func.eq_ignore_ascii_case("inset") => {
787                let mut top = 0.0f32;
788                let mut right = 0.0f32;
789                let mut bottom = 0.0f32;
790                let mut left = 0.0f32;
791                let mut idx = 0;
792
793                for arg in args.iter().flatten() {
794                    if let CssValue::Keyword(k) = arg
795                        && k.eq_ignore_ascii_case("round")
796                    {
797                        break; // round keywords / border-radius not yet supported
798                    }
799                    let pct = match arg {
800                        CssValue::Length(v, Unit::Percent) => v / 100.0,
801                        CssValue::Length(v, _) => {
802                            resolve_css_len(name, std::slice::from_ref(arg), text_flow_style)
803                                .map(|l| length_to_px(&l, text_flow_style.font_size) / 100.0)
804                                .unwrap_or(*v / 100.0)
805                        }
806                        CssValue::Number(n) => *n,
807                        _ => continue,
808                    };
809                    match idx {
810                        0 => {
811                            top = pct;
812                            right = pct;
813                            bottom = pct;
814                            left = pct;
815                        }
816                        1 => {
817                            right = pct;
818                            left = pct;
819                        }
820                        2 => bottom = pct,
821                        3 => left = pct,
822                        _ => {}
823                    }
824                    idx += 1;
825                }
826                Some(ClipPath::Inset {
827                    top: top.clamp(0.0, 1.0),
828                    right: right.clamp(0.0, 1.0),
829                    bottom: bottom.clamp(0.0, 1.0),
830                    left: left.clamp(0.0, 1.0),
831                })
832            }
833
834            // Legacy `rect(<top> <right> <bottom> <left>)` shorthand, still used
835            // by GitHub's `.sr-only` utilities (`clip-path: rect(0 0 0 0)`).
836            //
837            // Unlike `inset()`, the four numbers are offsets from the box's
838            // top-left corner: the clip rectangle spans x ∈ [left, right) and
839            // y ∈ [top, bottom). `rect(0 0 0 0)` is therefore a zero-area
840            // rectangle (unlike `inset(0 0 0 0)`, which covers the whole box).
841            CssValue::Function(func, args) if func.eq_ignore_ascii_case("rect") => {
842                let mut top = 0.0f32;
843                let mut right = 0.0f32;
844                let mut bottom = 0.0f32;
845                let mut left = 0.0f32;
846                let mut idx = 0;
847
848                for arg in args.iter().flatten() {
849                    let pct = match arg {
850                        CssValue::Length(v, Unit::Percent) => v / 100.0,
851                        CssValue::Length(v, _) => {
852                            resolve_css_len(name, std::slice::from_ref(arg), text_flow_style)
853                                .map(|l| length_to_px(&l, text_flow_style.font_size) / 100.0)
854                                .unwrap_or(*v / 100.0)
855                        }
856                        CssValue::Number(n) => *n,
857                        _ => continue,
858                    };
859                    match idx {
860                        0 => top = pct,
861                        1 => right = pct,
862                        2 => bottom = pct,
863                        3 => left = pct,
864                        _ => {}
865                    }
866                    idx += 1;
867                }
868                Some(ClipPath::Inset {
869                    top: top.clamp(0.0, 1.0),
870                    left: left.clamp(0.0, 1.0),
871                    right: (1.0 - right).clamp(0.0, 1.0),
872                    bottom: (1.0 - bottom).clamp(0.0, 1.0),
873                })
874            }
875
876            CssValue::Function(func, args) if func.eq_ignore_ascii_case("polygon") => {
877                let mut points = Vec::new();
878                let mut x_opt: Option<f32> = None;
879                for arg in args.iter().flatten() {
880                    match arg {
881                        CssValue::Length(v, Unit::Percent) => {
882                            let pct = v / 100.0;
883                            if let Some(x) = x_opt.take() {
884                                points.push((x, pct));
885                            } else {
886                                x_opt = Some(pct);
887                            }
888                        }
889                        CssValue::Length(v, _) => {
890                            let pct =
891                                resolve_css_len(name, std::slice::from_ref(arg), text_flow_style)
892                                    .map(|l| length_to_px(&l, text_flow_style.font_size) / 100.0)
893                                    .unwrap_or(*v / 100.0);
894                            if let Some(x) = x_opt.take() {
895                                points.push((x, pct));
896                            } else {
897                                x_opt = Some(pct);
898                            }
899                        }
900                        CssValue::Number(n) => {
901                            if let Some(x) = x_opt.take() {
902                                points.push((x, *n));
903                            } else {
904                                x_opt = Some(*n);
905                            }
906                        }
907                        CssValue::Keyword(k)
908                            if k.eq_ignore_ascii_case("fill")
909                                || k.eq_ignore_ascii_case("stroke")
910                                || k.eq_ignore_ascii_case("evenodd") =>
911                        {
912                            // fill-rule keywords: ignore
913                        }
914                        _ => {}
915                    }
916                }
917                if points.is_empty() {
918                    None
919                } else {
920                    Some(ClipPath::Polygon { points })
921                }
922            }
923
924            _ => None,
925        }
926    }
927
928    match (name, value) {
929        /* ======================
930         * Display
931         * ====================== */
932        ("display", _) => {
933            apply_property!(
934                display,
935                style,
936                parent_style,
937                DEFAULT_STYLE,
938                value,
939                CssValue::Keyword(v),
940                Display::parse(v.as_str())?
941            );
942        }
943
944        ("z-index", _) => {
945            let f = |v: &CssValue| match v {
946                CssValue::Number(v) => {
947                    if v.is_finite() && v.fract().abs() < f32::EPSILON {
948                        Some(Some(*v as i32))
949                    } else {
950                        None
951                    }
952                }
953                CssValue::Keyword(v) if v.eq_ignore_ascii_case("auto") => Some(None),
954                _ => None,
955            };
956
957            apply_property!(
958                z_index,
959                container_style,
960                parent_container_style,
961                DEFAULT_CONTAINER_STYLE,
962                value,
963                f(value)?
964            );
965        }
966
967        ("visibility", _) => {
968            let f = |v: &SmolStr| match v.to_ascii_lowercase().as_str() {
969                "visible" => Some(Visibility::Visible),
970                "hidden" => Some(Visibility::Hidden),
971                "collapse" => Some(Visibility::Collapse),
972                _ => None,
973            };
974
975            apply_property!(
976                visibility,
977                container_style,
978                parent_container_style,
979                DEFAULT_CONTAINER_STYLE,
980                value,
981                CssValue::Keyword(v),
982                f(v)?
983            );
984        }
985
986        ("opacity", _) => {
987            // Non-inherited. `<number>` (0..=1) or `<percentage>` per CSS Color 4;
988            // out-of-range values clamp. The `initial`/`inherit` branches of
989            // `apply_property!` restore 1.0 / carry the parent's opacity.
990            let f = |v: &CssValue| match v {
991                CssValue::Number(value) => Some(value.clamp(0.0, 1.0)),
992                CssValue::Length(value, Unit::Percent) => Some((value / 100.0).clamp(0.0, 1.0)),
993                _ => None,
994            };
995
996            apply_property!(
997                opacity,
998                container_style,
999                parent_container_style,
1000                DEFAULT_CONTAINER_STYLE,
1001                value,
1002                f(value)?
1003            );
1004        }
1005
1006        ("float", _) => {
1007            let f = |v: &SmolStr| match v.to_ascii_lowercase().as_str() {
1008                "left" => Some(CssFloat::Left),
1009                "right" => Some(CssFloat::Right),
1010                "none" => Some(CssFloat::None),
1011                _ => None,
1012            };
1013
1014            apply_property!(
1015                css_float,
1016                container_style,
1017                parent_container_style,
1018                DEFAULT_CONTAINER_STYLE,
1019                value,
1020                CssValue::Keyword(v),
1021                f(v)?
1022            );
1023        }
1024
1025        ("clear", _) => {
1026            let f = |v: &SmolStr| match v.to_ascii_lowercase().as_str() {
1027                "left" => Some(CssClear::Left),
1028                "right" => Some(CssClear::Right),
1029                "both" => Some(CssClear::Both),
1030                "none" => Some(CssClear::None),
1031                _ => None,
1032            };
1033
1034            apply_property!(
1035                css_clear,
1036                container_style,
1037                parent_container_style,
1038                DEFAULT_CONTAINER_STYLE,
1039                value,
1040                CssValue::Keyword(v),
1041                f(v)?
1042            );
1043        }
1044
1045        ("cursor", _) => {
1046            container_style.cursor = if let CssValue::Keyword(v) = value
1047                && v == "initial"
1048            {
1049                DEFAULT_CONTAINER_STYLE.cursor.clone()
1050            } else if let CssValue::Keyword(v) = value
1051                && v == "inherit"
1052            {
1053                parent_container_style.cursor.clone()
1054            } else {
1055                parse_cursor_value(value, text_flow_style.font_size)
1056            };
1057        }
1058
1059        /* ======================
1060         * Color / Text
1061         * ====================== */
1062        ("background-color", _) => {
1063            let color = match value {
1064                CssValue::Keyword(kw) if kw.eq_ignore_ascii_case("inherit") => text_style.color,
1065                CssValue::Keyword(kw) if kw.eq_ignore_ascii_case("currentColor") => {
1066                    text_style.color
1067                }
1068                CssValue::Keyword(kw)
1069                    if kw.eq_ignore_ascii_case("initial") || kw.eq_ignore_ascii_case("unset") =>
1070                {
1071                    Color(0, 0, 0, 0)
1072                }
1073                _ => resolve_css_color(name, value, color_scheme)?,
1074            };
1075            set_background_color(container_style, color);
1076        }
1077
1078        ("background", _) => {
1079            container_style.background =
1080                parse_background_shorthand(name, value, text_style, text_flow_style, color_scheme);
1081            apply_background_shorthand_geometry(value, text_flow_style.font_size, container_style);
1082        }
1083
1084        ("background-image", _) => {
1085            let existing_color = container_style.background.color;
1086            let parsed =
1087                parse_background_shorthand(name, value, text_style, text_flow_style, color_scheme);
1088            if !parsed.layers.is_empty() {
1089                container_style.background = Background {
1090                    color: existing_color,
1091                    layers: parsed.layers,
1092                };
1093            }
1094        }
1095
1096        ("background-repeat", _) => {
1097            let repeats = parse_background_repeat(value);
1098            if repeats.len() == 1 {
1099                let r = repeats[0];
1100                for layer in &mut container_style.background.layers {
1101                    layer.repeat = r;
1102                }
1103            } else {
1104                for (i, layer) in container_style.background.layers.iter_mut().enumerate() {
1105                    layer.repeat = repeats.get(i).copied().unwrap_or_default();
1106                }
1107            }
1108        }
1109
1110        ("background-size", _) => {
1111            let sizes = parse_background_size(value, text_flow_style.font_size);
1112            if sizes.len() == 1 {
1113                let s = sizes[0];
1114                for layer in &mut container_style.background.layers {
1115                    layer.size = s;
1116                }
1117            } else {
1118                for (i, layer) in container_style.background.layers.iter_mut().enumerate() {
1119                    layer.size = sizes.get(i).copied().unwrap_or_default();
1120                }
1121            }
1122        }
1123
1124        ("background-position", _) => {
1125            let positions = parse_background_position(value, text_flow_style.font_size);
1126            if positions.len() == 1 {
1127                let p = positions[0];
1128                for layer in &mut container_style.background.layers {
1129                    layer.position = p;
1130                }
1131            } else {
1132                for (i, layer) in container_style.background.layers.iter_mut().enumerate() {
1133                    layer.position = positions.get(i).copied().unwrap_or_default();
1134                }
1135            }
1136        }
1137
1138        ("color", _) => {
1139            apply_property!(
1140                color,
1141                text_style,
1142                parent_text_style,
1143                DEFAULT_TEXT_STYLE,
1144                value,
1145                resolve_css_color(name, value, color_scheme)?
1146            );
1147        }
1148
1149        // `color-scheme` is resolved separately above.
1150        ("color-scheme", _) => {}
1151
1152        ("font-size", _) => {
1153            let px = match value {
1154                CssValue::Keyword(k) => match k.to_ascii_lowercase().as_str() {
1155                    // CSS 2.1 §15.7 absolute sizes, keyed to a 16px medium.
1156                    "xx-small" => Some(9.0),
1157                    "x-small" => Some(10.0),
1158                    "small" => Some(13.0),
1159                    "medium" => Some(16.0),
1160                    "large" => Some(18.0),
1161                    "x-large" => Some(24.0),
1162                    "xx-large" => Some(32.0),
1163                    // Relative to the inherited size.
1164                    "smaller" => Some(text_flow_style.font_size * 5.0 / 6.0),
1165                    "larger" => Some(text_flow_style.font_size * 6.0 / 5.0),
1166                    _ => None,
1167                },
1168                _ => {
1169                    let len = resolve_css_len(name, std::slice::from_ref(value), text_flow_style)?;
1170                    Some(resolve_font_size_px(&len, text_flow_style.font_size)?)
1171                }
1172            }?;
1173            text_flow_style.font_size = px;
1174        }
1175
1176        ("line-height", _) => {
1177            let f = |v: &CssValue| match v {
1178                CssValue::Number(factor) => Some(LineHeight::Number(*factor)),
1179                CssValue::Keyword(v) if v.eq_ignore_ascii_case("normal") => {
1180                    Some(LineHeight::Normal)
1181                }
1182                _ => match resolve_calc_value(name, v, text_flow_style) {
1183                    Some(CalcValue::Number(factor)) => Some(LineHeight::Number(factor)),
1184                    Some(CalcValue::Length(len)) => Some(LineHeight::Px(length_to_px(
1185                        &len,
1186                        text_flow_style.font_size,
1187                    ))),
1188                    Some(CalcValue::Angle(_)) | None => None,
1189                },
1190            };
1191
1192            apply_property!(
1193                line_height,
1194                text_flow_style,
1195                parent_text_flow_style,
1196                DEFAULT_TEXT_FLOW_STYLE,
1197                value,
1198                f(value)?
1199            );
1200        }
1201
1202        ("font-weight", CssValue::Keyword(v)) => {
1203            text_style.font_weight = match v.as_str() {
1204                "normal" => FontWeight::NORMAL,
1205                "bold" => FontWeight::BOLD,
1206                _ => text_style.font_weight,
1207            };
1208        }
1209        ("font-weight", CssValue::Number(v)) => {
1210            text_style.font_weight = FontWeight(*v as u16);
1211        }
1212
1213        ("font-style", _) => {
1214            apply_property!(
1215                font_style,
1216                text_style,
1217                parent_text_style,
1218                DEFAULT_TEXT_STYLE,
1219                value,
1220                CssValue::Keyword(v),
1221                match v.as_str() {
1222                    "normal" => FontStyle::Normal,
1223                    "italic" => FontStyle::Italic,
1224                    "oblique" => FontStyle::Oblique,
1225                    _ => text_style.font_style,
1226                }
1227            );
1228        }
1229
1230        ("font-family", _) => {
1231            let families = extract_font_families(value);
1232            if !families.is_empty() {
1233                text_style.font_families = families;
1234            }
1235        }
1236
1237        ("font", v) => {
1238            // CSS `font` shorthand:
1239            // [ [ <'font-style'> || <'font-variant'> || <'font-weight'> ] font-size [/ line-height]? font-family ]
1240            let values: Vec<&CssValue> = match v {
1241                CssValue::List(list) => list.iter().collect(),
1242                CssValue::Keyword(kw) if kw.eq_ignore_ascii_case("inherit") => {
1243                    text_style.font_style = parent_text_style.font_style;
1244                    text_style.font_weight = parent_text_style.font_weight;
1245                    text_style
1246                        .font_families
1247                        .clone_from(&parent_text_style.font_families);
1248                    text_flow_style.font_size = parent_text_flow_style.font_size;
1249                    text_flow_style.line_height = parent_text_flow_style.line_height;
1250                    return Some(());
1251                }
1252                CssValue::Keyword(kw) if kw.eq_ignore_ascii_case("initial") => {
1253                    text_style.font_style = DEFAULT_TEXT_STYLE.font_style;
1254                    text_style.font_weight = DEFAULT_TEXT_STYLE.font_weight;
1255                    text_style
1256                        .font_families
1257                        .clone_from(&DEFAULT_TEXT_STYLE.font_families);
1258                    text_flow_style.font_size = DEFAULT_TEXT_FLOW_STYLE.font_size;
1259                    text_flow_style.line_height = DEFAULT_TEXT_FLOW_STYLE.line_height;
1260                    return Some(());
1261                }
1262                _ => return None,
1263            };
1264
1265            if values.is_empty() {
1266                return None;
1267            }
1268
1269            // font-size (first <length>) is required.
1270            let font_size_idx = values
1271                .iter()
1272                .position(|v| matches!(v, CssValue::Length(_, _)))?;
1273
1274            // Resolve font-size.
1275            let len = resolve_css_len(
1276                name,
1277                std::slice::from_ref(values[font_size_idx]),
1278                text_flow_style,
1279            )?;
1280            let px = resolve_font_size_px(&len, text_flow_style.font_size)?;
1281            text_flow_style.font_size = px;
1282
1283            // Everything before font-size is font-style / font-weight keywords.
1284            for v in &values[..font_size_idx] {
1285                if let CssValue::Keyword(kw) = v {
1286                    match kw.as_str() {
1287                        "italic" => text_style.font_style = FontStyle::Italic,
1288                        "oblique" => text_style.font_style = FontStyle::Oblique,
1289                        "normal" => text_style.font_style = FontStyle::Normal,
1290                        "bold" => text_style.font_weight = FontWeight::BOLD,
1291                        "bolder" | "lighter" | "small-caps" => {} // simplified: skip
1292                        s if s.len() <= 3 && s.bytes().all(|b| b.is_ascii_digit()) => {
1293                            if let Ok(w) = s.parse::<u16>()
1294                                && (100..=900).contains(&w)
1295                            {
1296                                text_style.font_weight = FontWeight(w);
1297                            }
1298                        }
1299                        _ => {}
1300                    }
1301                } else if let CssValue::Number(n) = v
1302                    && *n >= 100.0
1303                    && *n <= 900.0
1304                    && n.fract().abs() < f32::EPSILON
1305                {
1306                    text_style.font_weight = FontWeight(*n as u16);
1307                }
1308            }
1309
1310            // After font-size: optional `/` line-height, then font-family.
1311            let after = &values[font_size_idx + 1..];
1312            let family_start = if after
1313                .first()
1314                .is_some_and(|v| matches!(v, CssValue::Keyword(k) if k == "/"))
1315            {
1316                if after.len() > 1 {
1317                    match resolve_calc_value(name, after[1], text_flow_style) {
1318                        Some(CalcValue::Number(factor)) => {
1319                            text_flow_style.line_height = LineHeight::Number(factor);
1320                        }
1321                        Some(CalcValue::Length(lh_len)) => {
1322                            text_flow_style.line_height =
1323                                LineHeight::Px(length_to_px(&lh_len, text_flow_style.font_size));
1324                        }
1325                        Some(CalcValue::Angle(_)) | None => {}
1326                    }
1327                    2 // skip `/` and line-height value
1328                } else {
1329                    1 // skip bare `/`
1330                }
1331            } else {
1332                0
1333            };
1334
1335            // Rest is font-family.
1336            let family_values = &values[font_size_idx + 1 + family_start..];
1337            if !family_values.is_empty() {
1338                let families: Vec<String> = family_values
1339                    .iter()
1340                    .filter_map(|v| match v {
1341                        CssValue::Keyword(k) if !k.is_empty() => Some(k.to_string()),
1342                        CssValue::String(s) if !s.is_empty() => Some(s.clone()),
1343                        _ => None,
1344                    })
1345                    .collect();
1346                if !families.is_empty() {
1347                    text_style.font_families = families;
1348                }
1349            }
1350
1351            return Some(());
1352        }
1353
1354        ("text-decoration", v) => {
1355            let items: Vec<&CssValue> = match v {
1356                CssValue::List(list) => list.iter().collect(),
1357                _ => vec![v],
1358            };
1359            for item in items {
1360                match item {
1361                    CssValue::Keyword(k) => match k.as_str() {
1362                        "none" => text_style.text_decoration = TextDecoration::None,
1363                        "underline" => text_style.text_decoration = TextDecoration::Underline,
1364                        "line-through" => text_style.text_decoration = TextDecoration::LineThrough,
1365                        "overline" => text_style.text_decoration = TextDecoration::Overline,
1366                        _ => {}
1367                    },
1368                    _ => {
1369                        if let Some(c) = resolve_css_color(name, item, color_scheme) {
1370                            text_style.text_decoration_color = Some(c);
1371                        }
1372                    }
1373                }
1374            }
1375        }
1376
1377        ("text-decoration-color", _) => {
1378            if let Some(c) = resolve_css_color(name, value, color_scheme) {
1379                text_style.text_decoration_color = Some(c);
1380            }
1381        }
1382
1383        ("vertical-align", _) => {
1384            apply_property!(
1385                vertical_align,
1386                text_flow_style,
1387                parent_text_flow_style,
1388                DEFAULT_TEXT_FLOW_STYLE,
1389                value,
1390                CssValue::Keyword(v),
1391                match v.as_str() {
1392                    "sub" => VerticalAlign::Sub,
1393                    "super" | "sup" => VerticalAlign::Super,
1394                    _ => text_flow_style.vertical_align,
1395                }
1396            );
1397        }
1398
1399        ("text-transform", _) => {
1400            apply_property!(
1401                text_transform,
1402                text_style,
1403                parent_text_style,
1404                DEFAULT_TEXT_STYLE,
1405                value,
1406                CssValue::Keyword(v),
1407                match v.as_str() {
1408                    "none" => TextTransform::None,
1409                    "uppercase" => TextTransform::Uppercase,
1410                    "lowercase" => TextTransform::Lowercase,
1411                    _ => TextTransform::None,
1412                }
1413            );
1414        }
1415
1416        ("text-align", _) => {
1417            apply_property!(
1418                text_align,
1419                text_flow_style,
1420                parent_text_flow_style,
1421                DEFAULT_TEXT_FLOW_STYLE,
1422                value,
1423                CssValue::Keyword(v),
1424                match v.as_str() {
1425                    "left" => TextAlign::Left,
1426                    "center" => TextAlign::Center,
1427                    "right" => TextAlign::Right,
1428                    _ => return None,
1429                }
1430            );
1431        }
1432
1433        ("white-space", _) => {
1434            apply_property!(
1435                white_space,
1436                text_flow_style,
1437                parent_text_flow_style,
1438                DEFAULT_TEXT_FLOW_STYLE,
1439                value,
1440                CssValue::Keyword(v),
1441                match v.as_str() {
1442                    "normal" => WhiteSpace::Normal,
1443                    "nowrap" => WhiteSpace::Nowrap,
1444                    "pre" => WhiteSpace::Pre,
1445                    "pre-wrap" => WhiteSpace::PreWrap,
1446                    "pre-line" => WhiteSpace::PreLine,
1447                    "break-spaces" => WhiteSpace::BreakSpaces,
1448                    _ => text_flow_style.white_space,
1449                }
1450            );
1451        }
1452
1453        /* ======================
1454         * Box Model
1455         * ====================== */
1456        ("box-sizing", _) => {
1457            apply_property!(
1458                box_sizing,
1459                style,
1460                parent_style,
1461                DEFAULT_STYLE,
1462                value,
1463                CssValue::Keyword(v),
1464                match v.as_str() {
1465                    "content-box" => BoxSizing::ContentBox,
1466                    "border-box" => BoxSizing::BorderBox,
1467                    _ => BoxSizing::ContentBox,
1468                }
1469            );
1470        }
1471
1472        ("margin", v) => {
1473            expand_box(
1474                name,
1475                v,
1476                text_flow_style,
1477                &|_, cv, tfs| match cv {
1478                    CssValue::Keyword(s) if s == "auto" => Some(ui_layout::LengthOrAuto::Auto),
1479                    _ => resolve_css_len(name, std::slice::from_ref(cv), tfs)
1480                        .map(ui_layout::LengthOrAuto::Length),
1481                },
1482                |t, r, b, l| {
1483                    style.spacing.margin_top = t;
1484                    style.spacing.margin_right = r;
1485                    style.spacing.margin_bottom = b;
1486                    style.spacing.margin_left = l;
1487                },
1488            )?;
1489        }
1490        ("margin-top", _) => {
1491            style.spacing.margin_top = resolve_css_len_auto(name, value, text_flow_style)?;
1492        }
1493        ("margin-right", _) => {
1494            style.spacing.margin_right = resolve_css_len_auto(name, value, text_flow_style)?;
1495        }
1496        ("margin-bottom", _) => {
1497            style.spacing.margin_bottom = resolve_css_len_auto(name, value, text_flow_style)?;
1498        }
1499        ("margin-left", _) => {
1500            style.spacing.margin_left = resolve_css_len_auto(name, value, text_flow_style)?;
1501        }
1502        ("margin-inline", _) => {
1503            let values = one_or_two_values(value)?;
1504            style.spacing.margin_left = resolve_css_len_auto(name, values.0, text_flow_style)?;
1505            style.spacing.margin_right = resolve_css_len_auto(name, values.1, text_flow_style)?;
1506        }
1507        ("margin-inline-start", _) => {
1508            style.spacing.margin_left = resolve_css_len_auto(name, value, text_flow_style)?;
1509        }
1510        ("margin-inline-end", _) => {
1511            style.spacing.margin_right = resolve_css_len_auto(name, value, text_flow_style)?;
1512        }
1513
1514        ("border", v) => {
1515            let (maybe_width, maybe_style, maybe_color) = if let CssValue::Keyword(k) = v
1516                && (k.eq_ignore_ascii_case("inset") || k.eq_ignore_ascii_case("initial"))
1517            {
1518                (Some(Length::Px(0.0)), None, None)
1519            } else {
1520                parse_border_shorthand(name, v, text_flow_style, color_scheme)?
1521            };
1522
1523            if let Some(w) = maybe_width {
1524                style.spacing.border_top = w.clone();
1525                style.spacing.border_right = w.clone();
1526                style.spacing.border_bottom = w.clone();
1527                style.spacing.border_left = w;
1528            }
1529
1530            if let Some(s) = maybe_style {
1531                container_style.border_style.top = s;
1532                container_style.border_style.right = s;
1533                container_style.border_style.bottom = s;
1534                container_style.border_style.left = s;
1535            }
1536
1537            if let Some(c) = maybe_color {
1538                container_style.border_color.top = c;
1539                container_style.border_color.right = c;
1540                container_style.border_color.bottom = c;
1541                container_style.border_color.left = c;
1542            }
1543        }
1544        ("border-top", _) => {
1545            let (maybe_width, maybe_style, maybe_color) =
1546                parse_border_shorthand(name, value, text_flow_style, color_scheme)?;
1547            if let Some(w) = maybe_width {
1548                style.spacing.border_top = w;
1549            }
1550            if let Some(s) = maybe_style {
1551                container_style.border_style.top = s;
1552            }
1553            if let Some(c) = maybe_color {
1554                container_style.border_color.top = c;
1555            }
1556        }
1557        ("border-right", _) => {
1558            let (maybe_width, maybe_style, maybe_color) =
1559                parse_border_shorthand(name, value, text_flow_style, color_scheme)?;
1560            if let Some(w) = maybe_width {
1561                style.spacing.border_right = w;
1562            }
1563            if let Some(s) = maybe_style {
1564                container_style.border_style.right = s;
1565            }
1566            if let Some(c) = maybe_color {
1567                container_style.border_color.right = c;
1568            }
1569        }
1570        ("border-bottom", _) => {
1571            let (maybe_width, maybe_style, maybe_color) =
1572                parse_border_shorthand(name, value, text_flow_style, color_scheme)?;
1573            if let Some(w) = maybe_width {
1574                style.spacing.border_bottom = w;
1575            }
1576            if let Some(s) = maybe_style {
1577                container_style.border_style.bottom = s;
1578            }
1579            if let Some(c) = maybe_color {
1580                container_style.border_color.bottom = c;
1581            }
1582        }
1583        ("border-left", _) => {
1584            let (maybe_width, maybe_style, maybe_color) =
1585                parse_border_shorthand(name, value, text_flow_style, color_scheme)?;
1586            if let Some(w) = maybe_width {
1587                style.spacing.border_left = w;
1588            }
1589            if let Some(s) = maybe_style {
1590                container_style.border_style.left = s;
1591            }
1592            if let Some(c) = maybe_color {
1593                container_style.border_color.left = c;
1594            }
1595        }
1596
1597        ("border-radius", v) => {
1598            let (tl, tr, br, bl) = parse_border_radius_shorthand(name, v, text_flow_style)?;
1599            container_style.border_radius = BorderRadius {
1600                top_left: tl,
1601                top_right: tr,
1602                bottom_right: br,
1603                bottom_left: bl,
1604            };
1605        }
1606        ("border-top-left-radius", v) => {
1607            container_style.border_radius.top_left = parse_corner_radius(name, v, text_flow_style)?;
1608        }
1609        ("border-top-right-radius", v) => {
1610            container_style.border_radius.top_right =
1611                parse_corner_radius(name, v, text_flow_style)?;
1612        }
1613        ("border-bottom-right-radius", v) => {
1614            container_style.border_radius.bottom_right =
1615                parse_corner_radius(name, v, text_flow_style)?;
1616        }
1617        ("border-bottom-left-radius", v) => {
1618            container_style.border_radius.bottom_left =
1619                parse_corner_radius(name, v, text_flow_style)?;
1620        }
1621
1622        ("clip-path", _) => {
1623            container_style.clip_path = parse_clip_path(name, value, text_flow_style)?;
1624        }
1625
1626        ("transform", _) => {
1627            container_style.transform = parse_transform(name, value, style, text_flow_style);
1628        }
1629
1630        ("border-style", v) => {
1631            expand_box(
1632                name,
1633                v,
1634                text_flow_style,
1635                &|_, cv, _| {
1636                    let style = match cv {
1637                        CssValue::Keyword(v) => match v.as_str() {
1638                            "none" => BorderStyle::None,
1639                            "solid" => BorderStyle::Solid,
1640                            "dashed" => BorderStyle::Dashed,
1641                            "dotted" => BorderStyle::Dotted,
1642                            _ => return None,
1643                        },
1644                        _ => return None,
1645                    };
1646
1647                    Some(style)
1648                },
1649                |t, r, b, l| {
1650                    container_style.border_style.top = t;
1651                    container_style.border_style.right = r;
1652                    container_style.border_style.bottom = b;
1653                    container_style.border_style.left = l;
1654                },
1655            )?;
1656        }
1657
1658        ("border-color", v) => {
1659            expand_box(
1660                name,
1661                v,
1662                text_flow_style,
1663                &|_, cv, _| resolve_css_color(name, cv, color_scheme),
1664                |t, r, b, l| {
1665                    container_style.border_color.top = t;
1666                    container_style.border_color.right = r;
1667                    container_style.border_color.bottom = b;
1668                    container_style.border_color.left = l;
1669                },
1670            )?;
1671        }
1672        ("border-top-color", _) => {
1673            container_style.border_color.top = resolve_css_color(name, value, color_scheme)?;
1674        }
1675        ("border-right-color", _) => {
1676            container_style.border_color.right = resolve_css_color(name, value, color_scheme)?;
1677        }
1678        ("border-bottom-color", _) => {
1679            container_style.border_color.bottom = resolve_css_color(name, value, color_scheme)?;
1680        }
1681        ("border-left-color", _) => {
1682            container_style.border_color.left = resolve_css_color(name, value, color_scheme)?;
1683        }
1684
1685        ("border-width", v) => {
1686            expand_box(
1687                name,
1688                v,
1689                text_flow_style,
1690                &|_, cv, ts| match cv {
1691                    CssValue::Keyword(s) => match s.as_str() {
1692                        "thin" => Some(Length::Px(1.0)),
1693                        "medium" => Some(Length::Px(3.0)),
1694                        "thick" => Some(Length::Px(5.0)),
1695                        _ => resolve_css_len(name, std::slice::from_ref(cv), ts),
1696                    },
1697                    _ => resolve_css_len(name, std::slice::from_ref(cv), ts),
1698                },
1699                |t, r, b, l| {
1700                    style.spacing.border_top = t;
1701                    style.spacing.border_right = r;
1702                    style.spacing.border_bottom = b;
1703                    style.spacing.border_left = l;
1704                },
1705            )?;
1706        }
1707        ("border-top-width", _) => {
1708            style.spacing.border_top = match value {
1709                CssValue::Keyword(s) => match s.as_str() {
1710                    "thin" => Length::Px(1.0),
1711                    "medium" => Length::Px(3.0),
1712                    "thick" => Length::Px(5.0),
1713                    _ => resolve_css_len(name, std::slice::from_ref(value), text_flow_style)?,
1714                },
1715                _ => resolve_css_len(name, std::slice::from_ref(value), text_flow_style)?,
1716            };
1717        }
1718        ("border-right-width", _) => {
1719            style.spacing.border_right = match value {
1720                CssValue::Keyword(s) => match s.as_str() {
1721                    "thin" => Length::Px(1.0),
1722                    "medium" => Length::Px(3.0),
1723                    "thick" => Length::Px(5.0),
1724                    _ => resolve_css_len(name, std::slice::from_ref(value), text_flow_style)?,
1725                },
1726                _ => resolve_css_len(name, std::slice::from_ref(value), text_flow_style)?,
1727            };
1728        }
1729        ("border-bottom-width", _) => {
1730            style.spacing.border_bottom = match value {
1731                CssValue::Keyword(s) => match s.as_str() {
1732                    "thin" => Length::Px(1.0),
1733                    "medium" => Length::Px(3.0),
1734                    "thick" => Length::Px(5.0),
1735                    _ => resolve_css_len(name, std::slice::from_ref(value), text_flow_style)?,
1736                },
1737                _ => resolve_css_len(name, std::slice::from_ref(value), text_flow_style)?,
1738            };
1739        }
1740        ("border-left-width", _) => {
1741            style.spacing.border_left = match value {
1742                CssValue::Keyword(s) => match s.as_str() {
1743                    "thin" => Length::Px(1.0),
1744                    "medium" => Length::Px(3.0),
1745                    "thick" => Length::Px(5.0),
1746                    _ => resolve_css_len(name, std::slice::from_ref(value), text_flow_style)?,
1747                },
1748                _ => resolve_css_len(name, std::slice::from_ref(value), text_flow_style)?,
1749            };
1750        }
1751
1752        ("padding", v) => {
1753            expand_box(
1754                name,
1755                v,
1756                text_flow_style,
1757                &|_, v, ts| resolve_css_len(name, std::slice::from_ref(v), ts),
1758                |t, r, b, l| {
1759                    style.spacing.padding_top = t;
1760                    style.spacing.padding_right = r;
1761                    style.spacing.padding_bottom = b;
1762                    style.spacing.padding_left = l;
1763                },
1764            )?;
1765        }
1766        ("padding-top", _) => {
1767            style.spacing.padding_top =
1768                resolve_css_len(name, std::slice::from_ref(value), text_flow_style)?;
1769        }
1770        ("padding-right", _) => {
1771            style.spacing.padding_right =
1772                resolve_css_len(name, std::slice::from_ref(value), text_flow_style)?;
1773        }
1774        ("padding-bottom", _) => {
1775            style.spacing.padding_bottom =
1776                resolve_css_len(name, std::slice::from_ref(value), text_flow_style)?;
1777        }
1778        ("padding-left", _) => {
1779            style.spacing.padding_left =
1780                resolve_css_len(name, std::slice::from_ref(value), text_flow_style)?;
1781        }
1782        ("padding-inline", _) => {
1783            let values = one_or_two_values(value)?;
1784            style.spacing.padding_left =
1785                resolve_css_len(name, std::slice::from_ref(values.0), text_flow_style)?;
1786            style.spacing.padding_right =
1787                resolve_css_len(name, std::slice::from_ref(values.1), text_flow_style)?;
1788        }
1789        ("padding-inline-start", _) => {
1790            style.spacing.padding_left =
1791                resolve_css_len(name, std::slice::from_ref(value), text_flow_style)?;
1792        }
1793        ("padding-inline-end", _) => {
1794            style.spacing.padding_right =
1795                resolve_css_len(name, std::slice::from_ref(value), text_flow_style)?;
1796        }
1797
1798        /* ======================
1799         * Size
1800         * ====================== */
1801        ("width", _) => {
1802            style.size.width = resolve_css_len_auto(name, value, text_flow_style)?;
1803        }
1804        ("height", _) => {
1805            style.size.height = resolve_css_len_auto(name, value, text_flow_style)?;
1806        }
1807        ("min-width", _) => {
1808            style.size.min_width = resolve_css_len_auto(name, value, text_flow_style)?;
1809        }
1810        ("min-height", _) => {
1811            style.size.min_height = resolve_css_len_auto(name, value, text_flow_style)?;
1812        }
1813        ("max-width", _) => {
1814            style.size.max_width = resolve_css_len_auto(name, value, text_flow_style)?;
1815        }
1816        ("max-height", _) => {
1817            style.size.max_height = resolve_css_len_auto(name, value, text_flow_style)?;
1818        }
1819
1820        /* ======================
1821         * Position
1822         * ====================== */
1823        ("position", _) => {
1824            style.position.kind = match value {
1825                CssValue::Keyword(v) => match v.to_ascii_lowercase().as_str() {
1826                    "static" => Position::Static,
1827                    "relative" => Position::Relative,
1828                    "absolute" => Position::Absolute,
1829                    "fixed" => Position::Fixed,
1830                    "sticky" => Position::Sticky,
1831                    _ => return None,
1832                },
1833                _ => return None,
1834            };
1835        }
1836        ("anchor-name", _) => {
1837            container_style.anchor.anchors.clear();
1838            match value {
1839                CssValue::Keyword(k) if k.eq_ignore_ascii_case("none") => {}
1840                CssValue::List(list) => {
1841                    for v in list {
1842                        if let CssValue::Keyword(k) = v {
1843                            if k.eq_ignore_ascii_case("none") {
1844                                continue;
1845                            }
1846                            if k.starts_with("--") {
1847                                container_style.anchor.anchors.push(k.to_string());
1848                            }
1849                        }
1850                    }
1851                }
1852                CssValue::Keyword(k) if k.starts_with("--") => {
1853                    container_style.anchor.anchors.push(k.to_string());
1854                }
1855                _ => return None,
1856            }
1857        }
1858        ("position-anchor", _) => match value {
1859            CssValue::Keyword(k) if k.eq_ignore_ascii_case("auto") => {
1860                container_style.anchor.position_anchor = None;
1861            }
1862            CssValue::Keyword(k) if k.eq_ignore_ascii_case("none") => {
1863                container_style.anchor.position_anchor = None;
1864            }
1865            CssValue::Keyword(k) if k.starts_with("--") => {
1866                container_style.anchor.position_anchor = Some(k.to_string());
1867            }
1868            _ => return None,
1869        },
1870        ("anchor-scope", _) => match value {
1871            CssValue::Keyword(k) if k.eq_ignore_ascii_case("none") => {
1872                container_style.anchor.anchor_scope = None;
1873            }
1874            CssValue::Keyword(k) if k.starts_with("--") => {
1875                container_style.anchor.anchor_scope = Some(k.to_string());
1876            }
1877            _ => return None,
1878        },
1879        ("direction", _) => {
1880            apply_property!(
1881                direction,
1882                container_style,
1883                parent_container_style,
1884                DEFAULT_CONTAINER_STYLE,
1885                value,
1886                CssValue::Keyword(v),
1887                match v.as_str() {
1888                    "ltr" => Direction::Ltr,
1889                    "rtl" => Direction::Rtl,
1890                    _ => return None,
1891                }
1892            );
1893        }
1894        ("writing-mode", _) => {
1895            apply_property!(
1896                writing_mode,
1897                container_style,
1898                parent_container_style,
1899                DEFAULT_CONTAINER_STYLE,
1900                value,
1901                CssValue::Keyword(v),
1902                match v.as_str() {
1903                    "horizontal-tb" => WritingMode::HorizontalTb,
1904                    "vertical-rl" => WritingMode::VerticalRl,
1905                    "vertical-lr" => WritingMode::VerticalLr,
1906                    _ => return None,
1907                }
1908            );
1909        }
1910        ("top", _) => {
1911            // Check for anchor()
1912            if let CssValue::Function(name_fn, _) = value
1913                && name_fn.eq_ignore_ascii_case("anchor")
1914                && let Some(spec) = parse_anchor_spec(value)
1915            {
1916                container_style.anchor.top = Some(spec.clone());
1917                style.position.top = spec.fallback.unwrap_or(ui_layout::LengthOrAuto::Auto);
1918                return Some(());
1919            }
1920            style.position.top = resolve_css_len_auto(name, value, text_flow_style)?;
1921        }
1922        ("right", _) => {
1923            if let CssValue::Function(name_fn, _) = value
1924                && name_fn.eq_ignore_ascii_case("anchor")
1925                && let Some(spec) = parse_anchor_spec(value)
1926            {
1927                container_style.anchor.right = Some(spec.clone());
1928                style.position.right = spec.fallback.unwrap_or(ui_layout::LengthOrAuto::Auto);
1929                return Some(());
1930            }
1931            style.position.right = resolve_css_len_auto(name, value, text_flow_style)?;
1932        }
1933        ("bottom", _) => {
1934            if let CssValue::Function(name_fn, _) = value
1935                && name_fn.eq_ignore_ascii_case("anchor")
1936                && let Some(spec) = parse_anchor_spec(value)
1937            {
1938                container_style.anchor.bottom = Some(spec.clone());
1939                style.position.bottom = spec.fallback.unwrap_or(ui_layout::LengthOrAuto::Auto);
1940                return Some(());
1941            }
1942            style.position.bottom = resolve_css_len_auto(name, value, text_flow_style)?;
1943        }
1944        ("left", _) => {
1945            if let CssValue::Function(name_fn, _) = value
1946                && name_fn.eq_ignore_ascii_case("anchor")
1947                && let Some(spec) = parse_anchor_spec(value)
1948            {
1949                container_style.anchor.left = Some(spec.clone());
1950                style.position.left = spec.fallback.unwrap_or(ui_layout::LengthOrAuto::Auto);
1951                return Some(());
1952            }
1953            style.position.left = resolve_css_len_auto(name, value, text_flow_style)?;
1954        }
1955        ("inset", v) => {
1956            let (tv, rv, bv, lv): (&CssValue, &CssValue, &CssValue, &CssValue) = match v {
1957                CssValue::List(values) => match values.as_slice() {
1958                    [a] => (a, a, a, a),
1959                    [vert, horiz] => (vert, horiz, vert, horiz),
1960                    [t, h, b] => (t, h, b, h),
1961                    [t, r, b, l] => (t, r, b, l),
1962                    _ => return None,
1963                },
1964                _ => (v, v, v, v),
1965            };
1966            for (side, cv) in [
1967                (AnchorInsetSide::Top, tv),
1968                (AnchorInsetSide::Right, rv),
1969                (AnchorInsetSide::Bottom, bv),
1970                (AnchorInsetSide::Left, lv),
1971            ] {
1972                if let CssValue::Function(nf, _) = cv
1973                    && nf.eq_ignore_ascii_case("anchor")
1974                    && let Some(spec) = parse_anchor_spec(cv)
1975                {
1976                    *container_style.anchor.inset_mut(side) = Some(spec.clone());
1977                    let fallback = spec.fallback.unwrap_or(ui_layout::LengthOrAuto::Auto);
1978                    match side {
1979                        AnchorInsetSide::Top => style.position.top = fallback,
1980                        AnchorInsetSide::Right => style.position.right = fallback,
1981                        AnchorInsetSide::Bottom => style.position.bottom = fallback,
1982                        AnchorInsetSide::Left => style.position.left = fallback,
1983                    }
1984                    continue;
1985                }
1986                let resolved = match cv {
1987                    CssValue::Keyword(s) if s.eq_ignore_ascii_case("auto") => {
1988                        ui_layout::LengthOrAuto::Auto
1989                    }
1990                    _ => ui_layout::LengthOrAuto::Length(resolve_css_len(
1991                        name,
1992                        std::slice::from_ref(cv),
1993                        text_flow_style,
1994                    )?),
1995                };
1996                match side {
1997                    AnchorInsetSide::Top => style.position.top = resolved,
1998                    AnchorInsetSide::Right => style.position.right = resolved,
1999                    AnchorInsetSide::Bottom => style.position.bottom = resolved,
2000                    AnchorInsetSide::Left => style.position.left = resolved,
2001                }
2002            }
2003        }
2004
2005        /* ======================
2006         * Overflow
2007         * ====================== */
2008        ("overflow", _) => {
2009            let (x, y) = overflow_flags(value)?;
2010            overflow.x = x;
2011            overflow.y = y;
2012        }
2013        ("overflow-x", CssValue::Keyword(k)) => {
2014            overflow.x = overflow_scrollable(k)?;
2015        }
2016        ("overflow-y", CssValue::Keyword(k)) => {
2017            overflow.y = overflow_scrollable(k)?;
2018        }
2019
2020        /* ======================
2021         * Flex
2022         * ====================== */
2023        ("flex-direction", _) => {
2024            apply_property!(
2025                flex_direction,
2026                style,
2027                parent_style,
2028                DEFAULT_STYLE,
2029                value,
2030                CssValue::Keyword(v),
2031                flex_direction_keyword(v)?
2032            );
2033        }
2034
2035        ("flex-wrap", _) => {
2036            apply_property!(
2037                flex_wrap,
2038                style,
2039                parent_style,
2040                DEFAULT_STYLE,
2041                value,
2042                CssValue::Keyword(v),
2043                flex_wrap_keyword(v)?
2044            );
2045        }
2046
2047        ("flex-flow", _) => {
2048            let (direction, wrap) = resolve_flex_flow(value)?;
2049            style.flex_direction = direction;
2050            style.flex_wrap = wrap;
2051        }
2052
2053        ("place-items", value) => {
2054            let values = match value {
2055                CssValue::Keyword(_) => std::slice::from_ref(value),
2056                CssValue::List(values) if (1..=2).contains(&values.len()) => values.as_slice(),
2057                _ => return None,
2058            };
2059
2060            let align_items = match &values[0] {
2061                CssValue::Keyword(v) => resolve_align_items(v)?,
2062                _ => return None,
2063            };
2064
2065            let justify_items = match values.get(1).unwrap_or(&values[0]) {
2066                CssValue::Keyword(v) => resolve_justify_items(v)?,
2067                _ => return None,
2068            };
2069
2070            style.align_items = align_items;
2071            style.justify_items = justify_items;
2072        }
2073
2074        ("justify-items", _) => {
2075            apply_property!(
2076                justify_items,
2077                style,
2078                parent_style,
2079                DEFAULT_STYLE,
2080                value,
2081                CssValue::Keyword(v),
2082                resolve_justify_items(v)?
2083            );
2084        }
2085
2086        ("justify-content", _) => {
2087            apply_property!(
2088                justify_content,
2089                style,
2090                parent_style,
2091                DEFAULT_STYLE,
2092                value,
2093                CssValue::Keyword(v),
2094                match v.as_str() {
2095                    "flex-start" | "start" => JustifyContent::Start,
2096                    "center" => JustifyContent::Center,
2097                    "flex-end" | "end" => JustifyContent::End,
2098                    "space-between" => JustifyContent::SpaceBetween,
2099                    "space-around" => JustifyContent::SpaceAround,
2100                    "space-evenly" => JustifyContent::SpaceEvenly,
2101                    _ => return None,
2102                }
2103            );
2104        }
2105
2106        ("align-items", _) => {
2107            apply_property!(
2108                align_items,
2109                style,
2110                parent_style,
2111                DEFAULT_STYLE,
2112                value,
2113                CssValue::Keyword(v),
2114                resolve_align_items(v)?
2115            );
2116        }
2117
2118        ("align-content", _) => {
2119            apply_property!(
2120                align_content,
2121                style,
2122                parent_style,
2123                DEFAULT_STYLE,
2124                value,
2125                CssValue::Keyword(v),
2126                match v.as_str() {
2127                    "normal" | "stretch" => AlignContent::Stretch,
2128                    "flex-start" | "start" => AlignContent::Start,
2129                    "center" => AlignContent::Center,
2130                    "flex-end" | "end" => AlignContent::End,
2131                    "space-between" => AlignContent::SpaceBetween,
2132                    "space-around" => AlignContent::SpaceAround,
2133                    "space-evenly" => AlignContent::SpaceEvenly,
2134                    _ => return None,
2135                }
2136            );
2137        }
2138
2139        ("gap", _) => match value {
2140            CssValue::List(l) if l.len() == 2 => {
2141                let mut l = l.iter();
2142                let gap = resolve_css_len_auto(name, l.next()?, text_flow_style)?;
2143                style.row_gap = gap;
2144                let gap = resolve_css_len_auto(name, l.next()?, text_flow_style)?;
2145                style.column_gap = gap;
2146            }
2147            CssValue::Length(_, _) => {
2148                let gap = resolve_css_len_auto(name, value, text_flow_style)?;
2149                style.row_gap = gap.clone();
2150                style.column_gap = gap;
2151            }
2152            _ => {}
2153        },
2154
2155        ("flex", _) => {
2156            let (grow, shrink, basis) = resolve_flex_shorthand(value, text_flow_style)?;
2157            style.item_style.flex_grow = grow;
2158            style.item_style.flex_shrink = shrink;
2159            style.item_style.flex_basis = basis;
2160        }
2161
2162        ("flex-grow", value) => {
2163            style.item_style.flex_grow = match value {
2164                CssValue::Keyword(k) if k == "initial" => DEFAULT_STYLE.item_style.flex_grow,
2165                CssValue::Keyword(k) if k == "inherit" => parent_style.item_style.flex_grow,
2166                CssValue::Keyword(k) if k == "unset" => DEFAULT_STYLE.item_style.flex_grow,
2167                CssValue::Number(v) if *v >= 0.0 => *v,
2168                _ => return None,
2169            };
2170        }
2171
2172        ("flex-basis", value) => {
2173            style.item_style.flex_basis = match value {
2174                CssValue::Keyword(k) if k == "initial" => {
2175                    DEFAULT_STYLE.item_style.flex_basis.clone()
2176                }
2177                CssValue::Keyword(k) if k == "inherit" => {
2178                    parent_style.item_style.flex_basis.clone()
2179                }
2180                CssValue::Keyword(k) if k == "unset" => DEFAULT_STYLE.item_style.flex_basis.clone(),
2181                _ => resolve_css_len_auto(name, value, text_flow_style)?,
2182            };
2183        }
2184
2185        ("flex-shrink", value) => {
2186            style.item_style.flex_shrink = match value {
2187                CssValue::Keyword(k) if k == "initial" => DEFAULT_STYLE.item_style.flex_shrink,
2188                CssValue::Keyword(k) if k == "inherit" => parent_style.item_style.flex_shrink,
2189                CssValue::Keyword(k) if k == "unset" => DEFAULT_STYLE.item_style.flex_shrink,
2190                CssValue::Number(v) if *v >= 0.0 => *v,
2191                _ => return None,
2192            };
2193        }
2194
2195        ("place-self", value) => {
2196            match value {
2197                CssValue::Keyword(k) if k == "initial" => {
2198                    style.item_style.align_self = DEFAULT_STYLE.item_style.align_self;
2199                    style.item_style.justify_self = DEFAULT_STYLE.item_style.justify_self;
2200                }
2201                CssValue::Keyword(k) if k == "inherit" => {
2202                    style.item_style.align_self = parent_style.item_style.align_self;
2203                    style.item_style.justify_self = parent_style.item_style.justify_self;
2204                }
2205                CssValue::Keyword(k) if k == "unset" => {
2206                    style.item_style.align_self = DEFAULT_STYLE.item_style.align_self;
2207                    style.item_style.justify_self = DEFAULT_STYLE.item_style.justify_self;
2208                }
2209                _ => {}
2210            }
2211            let values = match value {
2212                CssValue::Keyword(_) => std::slice::from_ref(value),
2213                CssValue::List(values) if (1..=2).contains(&values.len()) => values.as_slice(),
2214                _ => return None,
2215            };
2216
2217            let align_items = match &values[0] {
2218                CssValue::Keyword(v) => resolve_align_items(v)?,
2219                _ => return None,
2220            };
2221
2222            let justify_items = match values.get(1).unwrap_or(&values[0]) {
2223                CssValue::Keyword(v) => resolve_justify_items(v)?,
2224                _ => return None,
2225            };
2226
2227            style.item_style.align_self = Some(align_items);
2228            style.item_style.justify_self = Some(justify_items);
2229        }
2230
2231        ("align-self", CssValue::Keyword(v)) => {
2232            style.item_style.align_self = Some(match v.as_str() {
2233                "stretch" => AlignItems::Stretch,
2234                "flex-start" | "start" => AlignItems::Start,
2235                "center" => AlignItems::Center,
2236                "flex-end" | "end" => AlignItems::End,
2237                "auto" => return Some(()),
2238                _ => return None,
2239            });
2240        }
2241
2242        ("justify-self", CssValue::Keyword(v)) => {
2243            style.item_style.justify_self = Some(match v.as_str() {
2244                "stretch" => JustifyItems::Stretch,
2245                "flex-start" | "start" => JustifyItems::Start,
2246                "center" => JustifyItems::Center,
2247                "flex-end" | "end" => JustifyItems::End,
2248                "auto" => return Some(()),
2249                _ => return None,
2250            });
2251        }
2252
2253        ("column-gap", _) => {
2254            style.column_gap = resolve_css_len_auto(name, value, text_flow_style)?;
2255        }
2256
2257        ("row-gap", _) => {
2258            style.row_gap = resolve_css_len_auto(name, value, text_flow_style)?;
2259        }
2260
2261        /* ======================
2262         * Grid
2263         * ====================== */
2264        ("grid-template-columns", _) => {
2265            style.grid_template_columns = parse_grid_tracks(name, value, text_flow_style)?;
2266        }
2267
2268        ("grid-template-rows", _) => {
2269            style.grid_template_rows = parse_grid_tracks(name, value, text_flow_style)?;
2270        }
2271
2272        ("grid-template", _) => {
2273            // A shorthand resets every longhand it covers, so the areas are
2274            // overwritten even when the value names none.
2275            let template = parse_grid_template(value, text_flow_style)?;
2276
2277            style.grid_template_rows = template.rows;
2278            style.grid_template_columns = template.columns;
2279            style.grid_template_areas = template.areas;
2280        }
2281
2282        ("grid-template-areas", _) => {
2283            style.grid_template_areas = parse_grid_template_areas(value)?;
2284        }
2285
2286        ("grid-area", CssValue::Keyword(area)) => {
2287            style.grid_area = Some(area.to_string());
2288        }
2289
2290        ("grid-column", _) => {
2291            style.grid_column = parse_grid_placement(value)?;
2292        }
2293
2294        ("grid-row", _) => {
2295            style.grid_row = parse_grid_placement(value)?;
2296        }
2297
2298        ("grid-column-start", _) => {
2299            style.grid_column.start = Some(parse_grid_line(value)?);
2300        }
2301
2302        ("grid-column-end", _) => {
2303            style.grid_column.end = parse_grid_line_end(value)?;
2304        }
2305
2306        ("grid-row-start", _) => {
2307            style.grid_row.start = Some(parse_grid_line(value)?);
2308        }
2309
2310        ("grid-row-end", _) => {
2311            style.grid_row.end = parse_grid_line_end(value)?;
2312        }
2313
2314        // `content` applies only to ::before / ::after pseudo-elements and is
2315        // resolved separately in build_pseudo_slot; on regular elements it is
2316        // accepted but has no visual effect.
2317        ("content", _) => {}
2318
2319        // `counter-reset` and `counter-increment` are handled in the layout
2320        // builder's enter phase (counter state threading), not here.
2321        ("counter-reset", _) => {}
2322        ("counter-increment", _) => {}
2323
2324        _ => {
2325            /*
2326            if !name.starts_with('-') {
2327                log::error!("{name}, {value:?}");
2328            }
2329            */
2330            return None;
2331        }
2332    }
2333    Some(())
2334}
2335
2336/// Parse a CSS `cursor` value into a [`CursorStyle`].
2337///
2338/// Supports:
2339/// - Keywords: `auto`, `default`, `none`, `pointer`, `text`, `move`,
2340///   `not-allowed`, `wait`, `crosshair`, `grab`, `grabbing`
2341/// - Custom images: `url("cursor.png")`, `url("cursor.png") 8 8`
2342/// - Fallback chains: `url("a.png"), url("b.png") 4 4, auto`
2343fn parse_cursor_value(value: &CssValue, _font_size: f32) -> CursorStyle {
2344    // Collect comma-separated items.
2345    let items: Vec<&CssValue> = match value {
2346        CssValue::List(items) => items.iter().collect(),
2347        _ => vec![value],
2348    };
2349
2350    let mut images: Vec<CustomCursor> = Vec::new();
2351    let mut fallback: Option<CursorKeyword> = None;
2352
2353    for item in items {
2354        match item {
2355            CssValue::Function(fn_name, args) if fn_name.eq_ignore_ascii_case("url") => {
2356                let source = args.iter().flatten().find_map(|v| match v {
2357                    CssValue::String(s) => Some(s.clone()),
2358                    CssValue::Keyword(s) => Some(s.to_string()),
2359                    _ => None,
2360                });
2361                if let Some(src) = source {
2362                    // Look for hotspot coordinates: two numbers after the url args.
2363                    let hotspot = parse_cursor_hotspot(args);
2364                    images.push(CustomCursor {
2365                        source: src,
2366                        hotspot,
2367                    });
2368                }
2369            }
2370            CssValue::Keyword(kw) => {
2371                // A keyword can only be the final fallback.
2372                if images.is_empty() && fallback.is_none() {
2373                    return CursorStyle::Keyword(CursorKeyword::from_keyword(kw));
2374                }
2375                fallback = Some(CursorKeyword::from_keyword(kw));
2376            }
2377            _ => {}
2378        }
2379    }
2380
2381    if images.is_empty() {
2382        CursorStyle::Keyword(fallback.unwrap_or(CursorKeyword::Auto))
2383    } else {
2384        CursorStyle::Custom {
2385            images,
2386            fallback: fallback.unwrap_or(CursorKeyword::Auto),
2387        }
2388    }
2389}
2390
2391/// Parse optional hotspot coordinates from `url()` args.
2392///
2393/// CSS allows `url("cursor.png") 8 8` where the two numbers are the
2394/// hotspot x and y. Looks for two `Number` values among the args.
2395fn parse_cursor_hotspot(args: &[Vec<CssValue>]) -> Option<(u32, u32)> {
2396    let mut nums: Vec<u32> = Vec::new();
2397    for group in args {
2398        for item in group {
2399            if let CssValue::Number(n) = item {
2400                nums.push(*n as u32);
2401            }
2402        }
2403    }
2404    if nums.len() >= 2 {
2405        Some((nums[0], nums[1]))
2406    } else {
2407        None
2408    }
2409}
2410
2411/// Convert a CSS keyword string to a [`CursorKeyword`].
2412impl CursorKeyword {
2413    fn from_keyword(kw: &SmolStr) -> CursorKeyword {
2414        match kw.as_str() {
2415            "auto" => CursorKeyword::Auto,
2416            "default" => CursorKeyword::Default,
2417            "none" => CursorKeyword::None,
2418            "pointer" => CursorKeyword::Pointer,
2419            "text" => CursorKeyword::Text,
2420            "move" => CursorKeyword::Move,
2421            "not-allowed" => CursorKeyword::NotAllowed,
2422            "wait" => CursorKeyword::Wait,
2423            "crosshair" => CursorKeyword::Crosshair,
2424            "grab" => CursorKeyword::Grab,
2425            "grabbing" => CursorKeyword::Grabbing,
2426            _ => CursorKeyword::Auto,
2427        }
2428    }
2429}