Skip to main content

orinium_browser/engine/layouter/
builder.rs

1//! Layout builder, which transforms a DOM tree into a UI layout.
2
3use crate::engine::bridge::text::{self, FallbackTextMeasurer, MeasuredFragment, TextMeasurer};
4use crate::engine::css::{
5    matcher::{ElementChain, ElementInfo},
6    values::{CssValue, Unit},
7};
8use crate::engine::html::HtmlNodeType;
9use crate::engine::tree::TreeNode;
10
11use std::cell::RefCell;
12use std::collections::HashMap;
13use std::rc::Rc;
14
15use ui_layout::{
16    AlignItems, BoxSizing, Display, FlexDirection, Fragment, InnerDisplay, ItemFragment,
17    JustifyContent, LayoutChild, LayoutNode, Length, LengthOrAuto, OuterDisplay, Style,
18};
19
20use super::css_resolver::ResolvedStyles;
21use super::types::{
22    Background, BorderStyle, Color, ColorStop, ContainerRole, ContainerStyle, FontStyle,
23    FontWeight, Gradient, GradientKind, InfoNode, LineHeight, NodeKind, RadialShape,
24    RadialSizeKind, TextAlign, TextDecoration, TextStyle, TextTransform,
25};
26
27const DEFAULT_LINE_FACTOR: f32 = 1.2;
28
29/// Inherited values from parent, passed down through the tree.
30///
31/// `text_style` carries all inherited text/line-height values.
32/// Add new fields here when additional deferred-resolution properties arise.
33#[derive(Clone, Copy)]
34pub struct InheritedCss {
35    pub text_style: TextStyle,
36}
37
38/// Convert a resolved `Length` to an absolute pixel value for `LineHeight::Px`.
39fn length_to_px(len: &Length, font_size: f32) -> f32 {
40    match len {
41        Length::Px(v) => *v,
42        Length::Percent(v) => v * font_size / 100.0,
43        Length::Add(a, b) => length_to_px(a, font_size) + length_to_px(b, font_size),
44        Length::Sub(a, b) => length_to_px(a, font_size) - length_to_px(b, font_size),
45        Length::Mul(a, f) => length_to_px(a, font_size) * f,
46        Length::Div(a, f) => length_to_px(a, font_size) / f,
47        _ => font_size * DEFAULT_LINE_FACTOR,
48    }
49}
50
51/// Builds a layout tree (`LayoutNode`) and a render info tree (`InfoNode`) from the DOM.
52///
53/// # Overview
54/// - Recursively traverses the HTML DOM
55/// - Applies resolved CSS declarations
56/// - Computes layout-related styles
57/// - Collects render-time information (color, font size, text)
58///
59/// # Style resolution order (low → high priority)
60///
61/// 1. **Inherited values from parent**
62///    - `text_style`
63///
64/// 2. **Resolved CSS declarations**
65///    - Overrides inherited values when specified
66///
67/// 3. **HTML defaults / semantics**
68///    - `display` (block, inline, etc.)
69///    - Text measurement for text nodes
70///
71/// # Inherited properties
72///
73/// Only the following properties are inherited explicitly:
74///
75/// - `text_style`
76///
77/// All other style fields are initialized per node and are **not inherited**.
78///
79/// # Parameters
80///
81/// - `parent_text_style`
82///
83/// These values must be passed from the computed result of the parent when
84/// calling this function recursively.
85///
86/// # Returns
87///
88/// A tuple of:
89/// - `LayoutNode`: used by the layout engine
90/// - `InfoNode`: used for rendering (text, color, font size)
91pub fn build_layout_and_info(
92    dom: &Rc<RefCell<TreeNode<HtmlNodeType>>>,
93    resolved_styles: &ResolvedStyles,
94    measurer: &dyn text::TextMeasurer<TextStyle>,
95    parent: InheritedCss,
96    mut chain: ElementChain,
97) -> (LayoutNode, InfoNode) {
98    let html_node = dom.borrow().value.clone();
99
100    let mut text_style = parent.text_style;
101    let mut container_style = ContainerStyle::default();
102    let mut style = Style::default();
103
104    /* -----------------------------
105       Build element chain
106    ----------------------------- */
107    if let HtmlNodeType::Element {
108        tag_name,
109        attributes,
110        ..
111    } = &html_node
112    {
113        let id = attributes
114            .iter()
115            .find(|a| a.name == "id")
116            .map(|a| a.value.clone());
117
118        let class_list: Vec<String> = attributes
119            .iter()
120            .find(|attr| attr.name == "class")
121            .map(|attr| {
122                attr.value
123                    .split_whitespace()
124                    .map(|s| s.to_string())
125                    .collect()
126            })
127            .unwrap_or_default();
128
129        chain.insert(
130            0,
131            ElementInfo {
132                tag_name: tag_name.clone(),
133                id,
134                classes: class_list,
135            },
136        );
137    }
138
139    /* -----------------------------
140       Collect CSS candidates
141    ----------------------------- */
142    let candidates: Option<HashMap<String, (CssValue, (u32, u32, u32), usize)>> =
143        if let HtmlNodeType::Element { .. } = &html_node {
144            Some(collect_candidates(resolved_styles, &chain))
145        } else {
146            None
147        };
148
149    /* -----------------------------
150       Phase 1: Apply element-specific CSS declarations
151    ----------------------------- */
152    if let Some(candidates) = &candidates {
153        for (name, (value, _, _)) in candidates {
154            if name.starts_with("--") {
155                continue;
156            }
157            apply_declaration(
158                name,
159                value,
160                &mut style,
161                &mut container_style,
162                &mut text_style,
163            );
164        }
165    }
166
167    /* -----------------------------
168       Phase 2: Resolve line-height using final font_size.
169       text_style.line_height was either inherited from parent
170       (via parent.text_style) or set by an explicit declaration
171       in Phase 1 (via apply_declaration).
172    ----------------------------- */
173    style.line_height = match text_style.line_height {
174        LineHeight::Number(factor) => Length::Px(text_style.font_size * factor),
175        LineHeight::Normal => Length::Px(text_style.font_size * DEFAULT_LINE_FACTOR),
176        LineHeight::Px(px) => Length::Px(px),
177    };
178
179    let child = InheritedCss { text_style };
180
181    let (mut kind, inline_fragments_opt) = if let HtmlNodeType::Text(t) = &html_node {
182        let t = normalize_whitespace(t);
183        let t = match text_style.text_transform {
184            TextTransform::None => t,
185            TextTransform::Uppercase => t.to_ascii_uppercase(),
186            TextTransform::Lowercase => t.to_ascii_lowercase(),
187        };
188
189        let _t = std::time::Instant::now();
190        let measured = measurer
191            .measure(&text::TextMeasureRequest {
192                text: t.clone(),
193                style: text_style,
194            })
195            .expect("text measure failed");
196        let preview = if t.len() > 40 {
197            let cut = t.floor_char_boundary(40);
198            format!("{}...", &t[..cut])
199        } else {
200            t.clone()
201        };
202        log::info!(
203            target: "Layouter",
204            "measure inline: text={:?} len={} took={:?}",
205            preview,
206            t.len(),
207            _t.elapsed(),
208        );
209
210        let kind = NodeKind::Text {
211            texts: measured.iter().map(|f| f.text.clone()).collect(),
212            style: text_style,
213        };
214
215        let inline_fragments: Vec<ItemFragment> = measured
216            .into_iter()
217            .map(|f| {
218                ItemFragment::Fragment(Fragment {
219                    width: f.width,
220                    height: f.height,
221                })
222            })
223            .collect();
224
225        (kind, Some(inline_fragments))
226    } else if let Some(name) = html_node.tag_name()
227        && name == "a"
228        && let Some(href) = html_node.get_attr("href")
229    {
230        (
231            NodeKind::Container {
232                scroll_x: false,
233                scroll_y: false,
234                scroll_offset_x: 0.0,
235                scroll_offset_y: 0.0,
236                style: container_style,
237                role: ContainerRole::Link {
238                    href: href.to_string(),
239                },
240            },
241            None,
242        )
243    } else {
244        (
245            NodeKind::Container {
246                scroll_x: false,
247                scroll_y: false,
248                scroll_offset_x: 0.0,
249                scroll_offset_y: 0.0,
250                style: container_style,
251                role: ContainerRole::Normal,
252            },
253            None,
254        )
255    };
256
257    // Process Children if there are no inline fragments (i.e. text nodes).
258    let (layout, info) = if let Some(inline_fragments) = inline_fragments_opt {
259        /* -----------------------------
260           Text Node with inline fragments
261        ----------------------------- */
262
263        let style = Style {
264            display: Display {
265                outer: OuterDisplay::Inline,
266                inner: InnerDisplay::Flow,
267            },
268            ..style
269        };
270
271        let layout = LayoutNode::with_children(style, inline_fragments);
272
273        let info = InfoNode {
274            kind,
275            children: vec![],
276        };
277
278        (layout, info)
279    } else {
280        /* -----------------------------
281           Children
282        ----------------------------- */
283
284        // NOTE:
285        // Table 要素は未実装。
286        // 暫定的に Flex に置き換える。
287        // TODO: 将来的には TableLayout 実装に置き換える。
288        let mut layout_children: Vec<LayoutChild> = Vec::new();
289        let mut info_children = Vec::new();
290
291        if style.display.outer != OuterDisplay::None {
292            // Table 要素は暫定的に Flex に置き換える。
293            match &html_node {
294                HtmlNodeType::Element { tag_name, .. }
295                    if tag_name == "table"
296                        || tag_name == "tbody"
297                        || tag_name == "thead"
298                        || tag_name == "tfoot" =>
299                {
300                    style.display = Display {
301                        outer: OuterDisplay::Block,
302                        inner: InnerDisplay::Flex,
303                    };
304                    style.flex_direction = FlexDirection::Column;
305                }
306                HtmlNodeType::Element { tag_name, .. } if tag_name == "tr" => {
307                    style.display = Display {
308                        outer: OuterDisplay::Block,
309                        inner: InnerDisplay::Flex,
310                    };
311                    style.flex_direction = FlexDirection::Row;
312                }
313                _ => {}
314            }
315
316            for child_dom in dom.borrow().children() {
317                let child_node = child_dom.borrow().value.clone();
318
319                if let HtmlNodeType::Text(t) = &child_node {
320                    let t = normalize_whitespace(t);
321                    let t = match text_style.text_transform {
322                        TextTransform::None => t,
323                        TextTransform::Uppercase => t.to_ascii_uppercase(),
324                        TextTransform::Lowercase => t.to_ascii_lowercase(),
325                    };
326
327                    let _t = std::time::Instant::now();
328                    let request = &text::TextMeasureRequest {
329                        text: t.clone(),
330                        style: text_style,
331                    };
332                    let measured = measurer.measure(request).unwrap_or_else(|_|
333                            // FallbackTextMeasurer won't return any errors.
334                            FallbackTextMeasurer.measure(request).unwrap());
335                    let preview = if t.len() > 40 {
336                        let cut = t.floor_char_boundary(40);
337                        format!("{}...", &t[..cut])
338                    } else {
339                        t.clone()
340                    };
341                    log::info!(
342                        target: "Layouter",
343                        "measure child: text={:?} len={} took={:?}",
344                        preview,
345                        t.len(),
346                        _t.elapsed(),
347                    );
348
349                    let text_kind = NodeKind::Text {
350                        texts: measured.iter().map(|f| f.text.clone()).collect(),
351                        style: text_style,
352                    };
353
354                    for fragment in &measured {
355                        layout_children.push(generate_fragment_node(fragment).into());
356                    }
357
358                    info_children.push(InfoNode {
359                        kind: text_kind,
360                        children: vec![],
361                    });
362                } else {
363                    if child_dom.borrow().value.tag_name() == Some("br") {
364                        layout_children.push(ItemFragment::LineBreak.into());
365                        info_children.push(InfoNode {
366                            kind: NodeKind::LineBreak,
367                            children: vec![],
368                        });
369                        continue;
370                    }
371
372                    let (child_layout, child_info) = build_layout_and_info(
373                        child_dom,
374                        resolved_styles,
375                        measurer,
376                        child,
377                        chain.clone(),
378                    );
379
380                    if dom.borrow().value.tag_name() == Some("html")
381                        && child_dom.borrow().value.tag_name() == Some("body")
382                        && let NodeKind::Container { style, .. } = &mut kind
383                        && style.background == Background::Color(Color(0, 0, 0, 0))
384                    {
385                        let background = {
386                            let NodeKind::Container { style, .. } = &child_info.kind else {
387                                continue;
388                            };
389                            style.background.clone()
390                        };
391                        // html 要素の body 子要素に背景色が指定されていない場合、
392                        // body の背景色を html の背景色で上書きする
393                        style.background = background;
394                    }
395
396                    layout_children.push(child_layout.into());
397                    info_children.push(child_info);
398                }
399            }
400        }
401
402        let layout = LayoutNode::with_children(style, layout_children);
403
404        let info = InfoNode {
405            kind,
406            children: info_children,
407        };
408
409        (layout, info)
410    };
411
412    (layout, info)
413}
414
415fn normalize_whitespace(text: &str) -> String {
416    let mut result = String::new();
417    let mut prev_was_space = false;
418
419    for c in text.chars() {
420        if c.is_whitespace() {
421            if !prev_was_space {
422                result.push(' ');
423                prev_was_space = true;
424            }
425        } else {
426            result.push(c);
427            prev_was_space = false;
428        }
429    }
430
431    result
432}
433
434fn generate_fragment_node(fragment: &MeasuredFragment) -> ItemFragment {
435    if fragment.text == "\n" {
436        ItemFragment::LineBreak
437    } else {
438        ItemFragment::Fragment(Fragment {
439            width: fragment.width,
440            height: fragment.height,
441        })
442    }
443}
444
445fn collect_candidates(
446    resolved_styles: &ResolvedStyles,
447    chain: &ElementChain,
448) -> HashMap<String, (CssValue, (u32, u32, u32), usize)> {
449    let mut candidates: HashMap<String, (CssValue, (u32, u32, u32), usize)> = HashMap::new();
450
451    for decl in resolved_styles {
452        if decl.selector.matches(chain) {
453            let entry = candidates.get(&decl.name);
454
455            let should_replace = match entry {
456                None => true,
457                Some((_, spec, order)) => {
458                    decl.specificity > *spec || (decl.specificity == *spec && decl.order > *order)
459                }
460            };
461
462            if should_replace {
463                candidates.insert(
464                    decl.name.clone(),
465                    (decl.value.clone(), decl.specificity, decl.order),
466                );
467            }
468        }
469    }
470
471    candidates
472}
473
474fn apply_declaration(
475    name: &str,
476    value: &CssValue,
477    style: &mut Style,
478    container_style: &mut ContainerStyle,
479    text_style: &mut TextStyle,
480) -> Option<()> {
481    fn expand_box<T: Clone, F>(
482        name: &str,
483        value: &CssValue,
484        text_style: &TextStyle,
485        resolve: &impl Fn(&str, &CssValue, &TextStyle) -> Option<T>,
486        mut set: F,
487    ) -> Option<()>
488    where
489        F: FnMut(T, T, T, T),
490    {
491        let resolve = |v: &CssValue| -> Option<T> { resolve(name, v, text_style) };
492
493        match value {
494            CssValue::List(values) => {
495                let vals: Vec<T> = values.iter().map(resolve).collect::<Option<_>>()?;
496
497                match vals.as_slice() {
498                    [a] => set(a.clone(), a.clone(), a.clone(), a.clone()),
499                    [v, h] => set(v.clone(), h.clone(), v.clone(), h.clone()),
500                    [t, h, b] => set(t.clone(), h.clone(), b.clone(), h.clone()),
501                    [t, r, b, l] => set(t.clone(), r.clone(), b.clone(), l.clone()),
502                    _ => return None,
503                }
504            }
505
506            _ => {
507                let v = resolve(value)?;
508                set(v.clone(), v.clone(), v.clone(), v);
509            }
510        }
511
512        Some(())
513    }
514
515    fn parse_border_shorthand(
516        name: &str,
517        value: &CssValue,
518        text_style: &TextStyle,
519    ) -> Option<(Option<Length>, Option<BorderStyle>, Option<Color>)> {
520        let mut width: Option<Length> = None;
521        let mut style_v: Option<BorderStyle> = None;
522        let mut color_v: Option<Color> = None;
523
524        let items: Vec<&CssValue> = match value {
525            CssValue::List(values) => values.iter().collect(),
526            _ => vec![value],
527        };
528
529        for v in items {
530            let token = v;
531
532            // try as length (numeric lengths)
533            if width.is_none()
534                && let Some(l) = resolve_css_len(name, token, text_style)
535            {
536                width = Some(l);
537                continue;
538            }
539
540            // try as width keyword (thin/medium/thick). Check keywords before style keywords.
541            if width.is_none()
542                && let CssValue::Keyword(s) = token
543            {
544                match s.as_str().to_ascii_lowercase().as_str() {
545                    "thin" => {
546                        width = Some(Length::Px(1.0));
547                        continue;
548                    }
549                    "medium" => {
550                        width = Some(Length::Px(3.0));
551                        continue;
552                    }
553                    "midium" => {
554                        width = Some(Length::Px(3.0));
555                        continue;
556                    } // common misspelling
557                    "thick" => {
558                        width = Some(Length::Px(5.0));
559                        continue;
560                    }
561                    _ => {}
562                }
563            }
564
565            // try as style keyword
566            if style_v.is_none()
567                && let CssValue::Keyword(s) = token
568            {
569                let s_lower = s.as_str();
570                let parsed = match s_lower {
571                    "none" => Some(BorderStyle::None),
572                    "solid" => Some(BorderStyle::Solid),
573                    "dashed" => Some(BorderStyle::Dashed),
574                    "dotted" => Some(BorderStyle::Dotted),
575                    "inset" | "outset" | "groove" | "ridge" | "double" | "hidden" => {
576                        // stub
577                        style_v = Some(BorderStyle::Solid);
578                        continue;
579                    }
580                    _ => None,
581                };
582
583                if let Some(p) = parsed {
584                    style_v = Some(p);
585                    continue;
586                }
587            }
588
589            // try as color
590            if color_v.is_none()
591                && let Some(c) = resolve_css_color(name, token)
592            {
593                color_v = Some(c);
594                continue;
595            }
596
597            // unknown token: ignore
598        }
599
600        Some((width, style_v, color_v))
601    }
602
603    match (name, value) {
604        /* ======================
605         * Display
606         * ====================== */
607        ("display", CssValue::Keyword(v)) => {
608            if let Some(parsed_display) = Display::from_css_name(v.as_str()) {
609                style.display = parsed_display;
610            }
611        }
612
613        /* ======================
614         * Color / Text
615         * ====================== */
616        ("background-color", _) => {
617            container_style.background = match value {
618                CssValue::Keyword(kw) if kw.eq_ignore_ascii_case("inherit") => {
619                    Background::Color(text_style.color)
620                }
621                CssValue::Keyword(kw) if kw.eq_ignore_ascii_case("currentColor") => {
622                    Background::Color(text_style.color)
623                }
624                CssValue::Keyword(kw) if kw.eq_ignore_ascii_case("initial") => {
625                    Background::Color(Color(0, 0, 0, 0))
626                }
627                _ => Background::Color(resolve_css_color(name, value)?),
628            };
629        }
630
631        ("background", _) => {
632            container_style.background = parse_background_shorthand(name, value, text_style)?;
633        }
634
635        ("color", _) => {
636            text_style.color = match value {
637                CssValue::Keyword(kw) if kw.eq_ignore_ascii_case("inherit") => {
638                    // inherit: use parent's color
639                    text_style.color
640                }
641                CssValue::Keyword(kw) if kw.eq_ignore_ascii_case("currentColor") => {
642                    text_style.color
643                }
644                _ => resolve_css_color(name, value)?,
645            }
646        }
647
648        ("font-size", CssValue::Length(_, _)) => {
649            // TODO: Add other size
650            let len = resolve_css_len(name, value, text_style)?;
651            let px = match &len {
652                Length::Px(v) => *v,
653                Length::Percent(v) => *v * text_style.font_size / 100.0,
654                _ => {
655                    log::error!(target: "Layouter", "Unknown size type for `{}`: {:?}", name, len);
656                    return None;
657                }
658            };
659            text_style.font_size = px;
660        }
661
662        ("line-height", CssValue::Number(factor)) => {
663            text_style.line_height = LineHeight::Number(*factor);
664            style.line_height = Length::Px(text_style.font_size * factor);
665        }
666        ("line-height", CssValue::Keyword(v)) if v == "normal" => {
667            text_style.line_height = LineHeight::Normal;
668            style.line_height = Length::Px(text_style.font_size * DEFAULT_LINE_FACTOR);
669        }
670        ("line-height", _) => {
671            let len = resolve_css_len(name, value, text_style)?;
672            text_style.line_height = LineHeight::Px(length_to_px(&len, text_style.font_size));
673            style.line_height = len;
674        }
675
676        ("font-weight", CssValue::Keyword(v)) => {
677            text_style.font_weight = match v.as_str() {
678                "normal" => FontWeight::NORMAL,
679                "bold" => FontWeight::BOLD,
680                _ => text_style.font_weight,
681            };
682        }
683        ("font-weight", CssValue::Number(v)) => {
684            text_style.font_weight = FontWeight(*v as u16);
685        }
686
687        ("font-style", CssValue::Keyword(v)) => {
688            text_style.font_style = match v.as_str() {
689                "normal" => FontStyle::Normal,
690                "italic" => FontStyle::Italic,
691                "oblique" => FontStyle::Oblique,
692                _ => text_style.font_style,
693            };
694        }
695
696        ("text-decoration", CssValue::Keyword(v)) => {
697            text_style.text_decoration = match v.as_str() {
698                "none" => TextDecoration::None,
699                "underline" => TextDecoration::Underline,
700                "line-through" => TextDecoration::LineThrough,
701                "overline" => TextDecoration::Overline,
702                _ => TextDecoration::None,
703            };
704        }
705
706        ("text-transform", CssValue::Keyword(v)) => {
707            text_style.text_transform = match v.as_str() {
708                "none" => TextTransform::None,
709                "uppercase" => TextTransform::Uppercase,
710                "lowercase" => TextTransform::Lowercase,
711                _ => TextTransform::None,
712            };
713        }
714
715        ("text-align", CssValue::Keyword(v)) if v == "left" => {
716            text_style.text_align = TextAlign::Left;
717        }
718        ("text-align", CssValue::Keyword(v)) if v == "center" => {
719            text_style.text_align = TextAlign::Center;
720        }
721        ("text-align", CssValue::Keyword(v)) if v == "right" => {
722            text_style.text_align = TextAlign::Right;
723        }
724
725        /* ======================
726         * Box Model
727         * ====================== */
728        ("box-sizing", CssValue::Keyword(v)) => {
729            style.box_sizing = match v.as_str() {
730                "content-box" => BoxSizing::ContentBox,
731                "border-box" => BoxSizing::BorderBox,
732                _ => BoxSizing::ContentBox,
733            };
734        }
735
736        ("border-style", CssValue::Keyword(v)) => {
737            let s = match v.as_str() {
738                "none" => BorderStyle::None,
739                "solid" => BorderStyle::Solid,
740                "dashed" => BorderStyle::Dashed,
741                "dotted" => BorderStyle::Dotted,
742                _ => BorderStyle::None,
743            };
744
745            container_style.border_style.top = s;
746            container_style.border_style.right = s;
747            container_style.border_style.bottom = s;
748            container_style.border_style.left = s;
749        }
750
751        ("margin", v) => {
752            expand_box(
753                name,
754                v,
755                text_style,
756                &|_, cv, ts| match cv {
757                    CssValue::Keyword(s) if s == "auto" => Some(ui_layout::LengthOrAuto::Auto),
758                    _ => resolve_css_len(name, cv, ts).map(ui_layout::LengthOrAuto::Length),
759                },
760                |t, r, b, l| {
761                    style.spacing.margin_top = t;
762                    style.spacing.margin_right = r;
763                    style.spacing.margin_bottom = b;
764                    style.spacing.margin_left = l;
765                },
766            )?;
767        }
768        ("margin-top", _) => {
769            style.spacing.margin_top = resolve_css_len_auto(name, value, text_style)?;
770        }
771        ("margin-right", _) => {
772            style.spacing.margin_right = resolve_css_len_auto(name, value, text_style)?;
773        }
774        ("margin-bottom", _) => {
775            style.spacing.margin_bottom = resolve_css_len_auto(name, value, text_style)?;
776        }
777        ("margin-left", _) => {
778            style.spacing.margin_left = resolve_css_len_auto(name, value, text_style)?;
779        }
780
781        ("border", v) => {
782            let (maybe_width, maybe_style, maybe_color) = if let CssValue::Keyword(k) = v
783                && (k.eq_ignore_ascii_case("inset") || k.eq_ignore_ascii_case("initial"))
784            {
785                (Some(Length::Px(0.0)), None, None)
786            } else {
787                parse_border_shorthand(name, v, text_style)?
788            };
789
790            if let Some(w) = maybe_width {
791                style.spacing.border_top = w.clone();
792                style.spacing.border_right = w.clone();
793                style.spacing.border_bottom = w.clone();
794                style.spacing.border_left = w;
795            }
796
797            if let Some(s) = maybe_style {
798                container_style.border_style.top = s;
799                container_style.border_style.right = s;
800                container_style.border_style.bottom = s;
801                container_style.border_style.left = s;
802            }
803
804            if let Some(c) = maybe_color {
805                container_style.border_color.top = c;
806                container_style.border_color.right = c;
807                container_style.border_color.bottom = c;
808                container_style.border_color.left = c;
809            }
810        }
811        ("border-top", _) => {
812            let (maybe_width, maybe_style, maybe_color) =
813                parse_border_shorthand(name, value, text_style)?;
814            if let Some(w) = maybe_width {
815                style.spacing.border_top = w;
816            }
817            if let Some(s) = maybe_style {
818                container_style.border_style.top = s;
819            }
820            if let Some(c) = maybe_color {
821                container_style.border_color.top = c;
822            }
823        }
824        ("border-right", _) => {
825            let (maybe_width, maybe_style, maybe_color) =
826                parse_border_shorthand(name, value, text_style)?;
827            if let Some(w) = maybe_width {
828                style.spacing.border_right = w;
829            }
830            if let Some(s) = maybe_style {
831                container_style.border_style.right = s;
832            }
833            if let Some(c) = maybe_color {
834                container_style.border_color.right = c;
835            }
836        }
837        ("border-bottom", _) => {
838            let (maybe_width, maybe_style, maybe_color) =
839                parse_border_shorthand(name, value, text_style)?;
840            if let Some(w) = maybe_width {
841                style.spacing.border_bottom = w;
842            }
843            if let Some(s) = maybe_style {
844                container_style.border_style.bottom = s;
845            }
846            if let Some(c) = maybe_color {
847                container_style.border_color.bottom = c;
848            }
849        }
850        ("border-left", _) => {
851            let (maybe_width, maybe_style, maybe_color) =
852                parse_border_shorthand(name, value, text_style)?;
853            if let Some(w) = maybe_width {
854                style.spacing.border_left = w;
855            }
856            if let Some(s) = maybe_style {
857                container_style.border_style.left = s;
858            }
859            if let Some(c) = maybe_color {
860                container_style.border_color.left = c;
861            }
862        }
863
864        ("padding", v) => {
865            expand_box(
866                name,
867                v,
868                text_style,
869                &|_, v, ts| resolve_css_len(name, v, ts),
870                |t, r, b, l| {
871                    style.spacing.padding_top = t;
872                    style.spacing.padding_right = r;
873                    style.spacing.padding_bottom = b;
874                    style.spacing.padding_left = l;
875                },
876            )?;
877        }
878        ("padding-top", _) => {
879            style.spacing.padding_top = resolve_css_len(name, value, text_style)?;
880        }
881        ("padding-right", _) => {
882            style.spacing.padding_right = resolve_css_len(name, value, text_style)?;
883        }
884        ("padding-bottom", _) => {
885            style.spacing.padding_bottom = resolve_css_len(name, value, text_style)?;
886        }
887        ("padding-left", _) => {
888            style.spacing.padding_left = resolve_css_len(name, value, text_style)?;
889        }
890
891        /* ======================
892         * Size
893         * ====================== */
894        ("width", _) => {
895            style.size.width = resolve_css_len_auto(name, value, text_style)?;
896        }
897        ("height", _) => {
898            style.size.height = resolve_css_len_auto(name, value, text_style)?;
899        }
900        ("min-width", _) => {
901            style.size.min_width = resolve_css_len_auto(name, value, text_style)?;
902        }
903        ("min-height", _) => {
904            style.size.min_height = resolve_css_len_auto(name, value, text_style)?;
905        }
906        ("max-width", _) => {
907            style.size.max_width = resolve_css_len_auto(name, value, text_style)?;
908        }
909        ("max-height", _) => {
910            style.size.max_height = resolve_css_len_auto(name, value, text_style)?;
911        }
912
913        /* ======================
914         * Flex
915         * ====================== */
916        ("flex-direction", CssValue::Keyword(v)) => {
917            style.flex_direction = match v.as_str() {
918                "row" => FlexDirection::Row,
919                "column" => FlexDirection::Column,
920                "row-reverse" => FlexDirection::RowReverse,
921                "column-reverse" => FlexDirection::ColumnReverse,
922                _ => return None,
923            };
924        }
925
926        ("justify-content", CssValue::Keyword(v)) => {
927            style.justify_content = match v.as_str() {
928                "flex-start" | "start" => JustifyContent::Start,
929                "center" => JustifyContent::Center,
930                "flex-end" | "end" => JustifyContent::End,
931                "space-between" => JustifyContent::SpaceBetween,
932                "space-around" => JustifyContent::SpaceAround,
933                "space-evenly" => JustifyContent::SpaceEvenly,
934                _ => return None,
935            };
936        }
937
938        ("align-items", CssValue::Keyword(v)) => {
939            style.align_items = match v.as_str() {
940                "stretch" => AlignItems::Stretch,
941                "flex-start" | "start" => AlignItems::Start,
942                "center" => AlignItems::Center,
943                "flex-end" | "end" => AlignItems::End,
944                _ => return None,
945            };
946        }
947
948        ("gap", _) => match value {
949            CssValue::List(l) if l.len() == 2 => {
950                let mut l = l.iter();
951                let gap = resolve_css_len_auto(name, l.next()?, text_style)?;
952                style.row_gap = gap;
953                let gap = resolve_css_len_auto(name, l.next()?, text_style)?;
954                style.column_gap = gap;
955            }
956            CssValue::Length(_, _) => {
957                let gap = resolve_css_len_auto(name, value, text_style)?;
958                style.row_gap = gap.clone();
959                style.column_gap = gap;
960            }
961            _ => {}
962        },
963
964        ("flex-grow", CssValue::Number(v)) => {
965            style.item_style.flex_grow = *v;
966        }
967
968        ("flex-basis", _) => {
969            style.item_style.flex_basis = resolve_css_len_auto(name, value, text_style)?;
970        }
971
972        _ => {
973            // log::error!("{name}, {value:?}");
974        }
975    }
976    Some(())
977}
978
979// =========================
980//   Background Shorthand
981// =========================
982
983fn parse_background_shorthand(
984    name: &str,
985    value: &CssValue,
986    text_style: &TextStyle,
987) -> Option<Background> {
988    let items: Vec<&CssValue> = match value {
989        CssValue::List(values) => values.iter().collect(),
990        _ => vec![value],
991    };
992
993    let mut maybe_color: Option<Color> = None;
994    let mut maybe_gradient: Option<Gradient> = None;
995
996    for v in items {
997        // inherit
998        if let CssValue::Keyword(kw) = v {
999            if kw.eq_ignore_ascii_case("inherit") {
1000                maybe_color = Some(text_style.color);
1001                continue;
1002            }
1003            if kw.eq_ignore_ascii_case("currentColor") {
1004                maybe_color = Some(text_style.color);
1005                continue;
1006            }
1007        }
1008
1009        if let CssValue::Number(0.0) = v {
1010            maybe_color = Some(Color(0, 0, 0, 0));
1011            continue;
1012        }
1013
1014        // gradient
1015        if let CssValue::Function(fn_name, args) = v {
1016            if fn_name == "linear-gradient" || fn_name == "radial-gradient" {
1017                maybe_gradient = Some(parse_gradient(fn_name, args, text_style)?);
1018                continue;
1019            }
1020        }
1021
1022        // color
1023        if let Some(c) = resolve_css_color(name, v) {
1024            maybe_color = Some(c);
1025            continue;
1026        }
1027    }
1028
1029    if let Some(g) = maybe_gradient {
1030        return Some(Background::Gradient(g));
1031    }
1032    if let Some(c) = maybe_color {
1033        return Some(Background::Color(c));
1034    }
1035
1036    None
1037}
1038
1039// =========================
1040//   Gradient Parsing
1041// =========================
1042
1043fn parse_gradient(fn_name: &str, args: &[CssValue], text_style: &TextStyle) -> Option<Gradient> {
1044    match fn_name {
1045        "linear-gradient" => parse_linear_gradient(args, text_style),
1046        "radial-gradient" => parse_radial_gradient(args, text_style),
1047        _ => None,
1048    }
1049}
1050
1051fn parse_linear_gradient(args: &[CssValue], _text_style: &TextStyle) -> Option<Gradient> {
1052    if args.is_empty() {
1053        return None;
1054    }
1055
1056    let (skip, angle) = parse_linear_direction(args);
1057    let angle = angle.unwrap_or(180.0);
1058    let stops = parse_color_stops(&args[skip..])?;
1059
1060    Some(Gradient {
1061        kind: GradientKind::Linear { angle },
1062        stops,
1063    })
1064}
1065
1066/// Returns (number_of_consumed_args, optional_angle_in_degrees).
1067fn parse_linear_direction(args: &[CssValue]) -> (usize, Option<f32>) {
1068    if args.is_empty() {
1069        return (0, None);
1070    }
1071
1072    // <angle>
1073    if let CssValue::Length(v, Unit::Deg) = &args[0] {
1074        return (1, Some(*v));
1075    }
1076
1077    // "to" <side-or-corner>
1078    if let CssValue::Keyword(k) = &args[0] {
1079        if k.as_str() == "to" && args.len() > 1 {
1080            let mut idx = 1;
1081            let mut sides: Vec<&str> = Vec::new();
1082            while idx < args.len() {
1083                if let CssValue::Keyword(k) = &args[idx] {
1084                    match k.as_str() {
1085                        "top" | "bottom" | "left" | "right" => {
1086                            sides.push(k.as_str());
1087                            idx += 1;
1088                        }
1089                        _ => break,
1090                    }
1091                } else {
1092                    break;
1093                }
1094            }
1095            if !sides.is_empty() {
1096                let angle = match sides.as_slice() {
1097                    ["top"] => Some(0.0),
1098                    ["top", "left"] => Some(315.0),
1099                    ["top", "right"] => Some(45.0),
1100                    ["bottom"] => Some(180.0),
1101                    ["bottom", "left"] => Some(225.0),
1102                    ["bottom", "right"] => Some(135.0),
1103                    ["left"] => Some(270.0),
1104                    ["right"] => Some(90.0),
1105                    _ => None,
1106                };
1107                return (idx, angle);
1108            }
1109        }
1110    }
1111
1112    (0, None)
1113}
1114
1115fn parse_radial_gradient(args: &[CssValue], _text_style: &TextStyle) -> Option<Gradient> {
1116    let mut shape = RadialShape::Ellipse;
1117    let mut size = RadialSizeKind::default();
1118    let mut position = (0.5f32, 0.5f32);
1119
1120    let mut idx = 0;
1121
1122    // Consume known radial keywords before color stops
1123    while idx < args.len() {
1124        if let CssValue::Keyword(k) = &args[idx] {
1125            match k.as_str() {
1126                "circle" => {
1127                    shape = RadialShape::Circle;
1128                    idx += 1;
1129                    continue;
1130                }
1131                "ellipse" => {
1132                    shape = RadialShape::Ellipse;
1133                    idx += 1;
1134                    continue;
1135                }
1136                "closest-side" => {
1137                    size = RadialSizeKind::ClosestSide;
1138                    idx += 1;
1139                    continue;
1140                }
1141                "farthest-side" => {
1142                    size = RadialSizeKind::FarthestSide;
1143                    idx += 1;
1144                    continue;
1145                }
1146                "closest-corner" => {
1147                    size = RadialSizeKind::ClosestCorner;
1148                    idx += 1;
1149                    continue;
1150                }
1151                "farthest-corner" => {
1152                    size = RadialSizeKind::FarthestCorner;
1153                    idx += 1;
1154                    continue;
1155                }
1156                _ => break,
1157            }
1158        } else {
1159            break;
1160        }
1161    }
1162
1163    // Optional "at <position>" — simplified to "at center" / "at top left" etc.
1164    if idx < args.len() && args[idx] == CssValue::Keyword("at".into()) {
1165        idx += 1; // skip "at"
1166        if idx < args.len() {
1167            if let CssValue::Keyword(k) = &args[idx] {
1168                // Parse position keywords
1169                match k.as_str() {
1170                    "center" => position = (0.5, 0.5),
1171                    "top" => position = (0.5, 0.0),
1172                    "bottom" => position = (0.5, 1.0),
1173                    "left" => position = (0.0, 0.5),
1174                    "right" => position = (1.0, 0.5),
1175                    _ => {} // ignore unknown
1176                }
1177                idx += 1;
1178                // Optional second keyword (e.g. "top left")
1179                if idx < args.len() {
1180                    if let CssValue::Keyword(k2) = &args[idx] {
1181                        match (k.as_str(), k2.as_str()) {
1182                            ("top", "left") | ("left", "top") => position = (0.0, 0.0),
1183                            ("top", "right") | ("right", "top") => position = (1.0, 0.0),
1184                            ("bottom", "left") | ("left", "bottom") => position = (0.0, 1.0),
1185                            ("bottom", "right") | ("right", "bottom") => position = (1.0, 1.0),
1186                            _ => {}
1187                        }
1188                        idx += 1;
1189                    }
1190                }
1191            }
1192        }
1193    }
1194
1195    let stops = parse_color_stops(&args[idx..])?;
1196    if stops.is_empty() {
1197        return None;
1198    }
1199    Some(Gradient {
1200        kind: GradientKind::Radial {
1201            shape,
1202            size,
1203            position,
1204        },
1205        stops,
1206    })
1207}
1208
1209fn parse_color_stops(args: &[CssValue]) -> Option<Vec<ColorStop>> {
1210    let mut stops = Vec::new();
1211    let mut i = 0;
1212
1213    while i < args.len() {
1214        let color = resolve_css_color("gradient", &args[i])?;
1215        i += 1;
1216
1217        let position = if i < args.len() {
1218            match &args[i] {
1219                CssValue::Length(v, Unit::Percent) => {
1220                    i += 1;
1221                    Some((*v / 100.0).clamp(0.0, 1.0))
1222                }
1223                CssValue::Length(_v, Unit::Px) => {
1224                    i += 1;
1225                    None
1226                }
1227                _ => None,
1228            }
1229        } else {
1230            None
1231        };
1232
1233        stops.push(ColorStop { color, position });
1234    }
1235
1236    Some(stops)
1237}
1238
1239/// Resolve CssValue to LengthOrAuto.
1240fn resolve_css_len_auto(
1241    name: &str,
1242    css_len: &CssValue,
1243    text_style: &TextStyle,
1244) -> Option<LengthOrAuto> {
1245    match &css_len {
1246        CssValue::Keyword(s) if s == "auto" => Some(LengthOrAuto::Auto),
1247        _ => resolve_css_len(name, css_len, text_style).map(|l| l.into()),
1248    }
1249}
1250
1251/// Resolve CssValue to Length.
1252fn resolve_css_len(name: &str, css_len: &CssValue, text_style: &TextStyle) -> Option<Length> {
1253    match &css_len {
1254        CssValue::Length(v, Unit::Em) => Some(Length::Px(text_style.font_size * v)),
1255        CssValue::Length(v, Unit::Rem) => Some(Length::Px(16.0 * v)), // html sont-size 仮値
1256        CssValue::Length(v, u) => match u {
1257            Unit::Percent => Some(Length::Percent(*v)),
1258            Unit::Px => Some(Length::Px(*v)),
1259            Unit::Vw => Some(Length::Vw(*v)),
1260            Unit::Vh => Some(Length::Vh(*v)),
1261            Unit::Em | Unit::Rem => unreachable!(),
1262            Unit::Deg => {
1263                log::error!(target: "Layouter", "Unexpected deg unit for `{}` (expected length)", name);
1264                return None;
1265            }
1266        },
1267        CssValue::Number(0.0) => Some(Length::Px(0.0)),
1268        CssValue::Keyword(_) => None,
1269        CssValue::Function(fn_name, args) if fn_name == "calc" && !args.is_empty() => {
1270            let mut iter = args.iter();
1271            let mut result = resolve_css_len(name, iter.next().unwrap(), text_style)?;
1272
1273            while let (Some(op), Some(val)) = (iter.next(), iter.next()) {
1274                match op {
1275                    CssValue::Keyword(o) if o == "+" => {
1276                        let val_resolved = resolve_css_len(name, val, text_style)?;
1277                        result = Length::Add(Box::new(result), Box::new(val_resolved));
1278                    }
1279                    CssValue::Keyword(o) if o == "-" => {
1280                        let val_resolved = resolve_css_len(name, val, text_style)?;
1281                        result = Length::Sub(Box::new(result), Box::new(val_resolved));
1282                    }
1283                    CssValue::Keyword(o) if o == "*" => {
1284                        if let CssValue::Number(factor) = val {
1285                            result = Length::Mul(Box::new(result), *factor);
1286                        } else {
1287                            log::error!(target: "Layouter", "Invalid operand for multiplication in calc() for `{}`: {:?}", name, val);
1288                            return None;
1289                        }
1290                    }
1291                    CssValue::Keyword(o) if o == "/" => {
1292                        if let CssValue::Number(factor) = val {
1293                            if *factor == 0.0 {
1294                                log::error!(target: "Layouter", "Division by zero in calc() for `{}`", name);
1295                                return None;
1296                            }
1297                            result = Length::Div(Box::new(result), *factor);
1298                        } else {
1299                            log::error!(target: "Layouter", "Invalid operand for division in calc() for `{}`: {:?}", name, val);
1300                            return None;
1301                        }
1302                    }
1303                    _ => {
1304                        log::error!(target: "Layouter", "Unknown operator in calc() for `{}`: {:?}", name, op);
1305                        return None;
1306                    }
1307                }
1308            }
1309
1310            Some(result)
1311        }
1312        CssValue::Color(_) => None,
1313        _ => {
1314            log::error!(target: "Layouter", "Unknown CSS Length type for `{}`: {:?}", name, css_len);
1315            None
1316        }
1317    }
1318}
1319
1320/// Resolve a computed CssValue into a final RGBA Color.
1321///
1322/// Assumptions:
1323/// - This function is called *after* cascade and inheritance resolution.
1324/// - Keywords like `currentColor`, `inherit`, `initial`, `unset`
1325///   must NOT reach this stage.
1326/// - The returned Color is always absolute RGBA.
1327fn resolve_css_color(name: &str, css_color: &CssValue) -> Option<Color> {
1328    fn keyword_color_to_color(name: &str, keyword: &str) -> Option<Color> {
1329        // NOTE:
1330        // Keyword matching is case-insensitive according to CSS specs.
1331        // Keep this list limited to commonly used CSS Color Level 3 keywords.
1332        match keyword.to_ascii_lowercase().as_str() {
1333            // ===== Basic =====
1334            "black" => Some(Color(0, 0, 0, 255)),
1335            "silver" => Some(Color(192, 192, 192, 255)),
1336            "gray" | "grey" => Some(Color(128, 128, 128, 255)),
1337            "white" => Some(Color(255, 255, 255, 255)),
1338
1339            // ===== Red =====
1340            "maroon" => Some(Color(128, 0, 0, 255)),
1341            "red" => Some(Color(255, 0, 0, 255)),
1342            "firebrick" => Some(Color(178, 34, 34, 255)),
1343            "crimson" => Some(Color(220, 20, 60, 255)),
1344            "indianred" => Some(Color(205, 92, 92, 255)),
1345            "lightcoral" => Some(Color(240, 128, 128, 255)),
1346            "salmon" => Some(Color(250, 128, 114, 255)),
1347            "darksalmon" => Some(Color(233, 150, 122, 255)),
1348            "lightsalmon" => Some(Color(255, 160, 122, 255)),
1349
1350            // ===== Pink =====
1351            "pink" => Some(Color(255, 192, 203, 255)),
1352            "lightpink" => Some(Color(255, 182, 193, 255)),
1353            "hotpink" => Some(Color(255, 105, 180, 255)),
1354            "deeppink" => Some(Color(255, 20, 147, 255)),
1355            "palevioletred" => Some(Color(219, 112, 147, 255)),
1356            "magenta" | "fuchsia" => Some(Color(255, 0, 255, 255)),
1357
1358            // ===== Orange =====
1359            "coral" => Some(Color(255, 127, 80, 255)),
1360            "tomato" => Some(Color(255, 99, 71, 255)),
1361            "orangered" => Some(Color(255, 69, 0, 255)),
1362            "orange" => Some(Color(255, 165, 0, 255)),
1363
1364            // ===== Yellow =====
1365            "gold" => Some(Color(255, 215, 0, 255)),
1366            "yellow" => Some(Color(255, 255, 0, 255)),
1367            "lightyellow" => Some(Color(255, 255, 224, 255)),
1368            "lemonchiffon" => Some(Color(255, 250, 205, 255)),
1369            "lightgoldenrodyellow" => Some(Color(250, 250, 210, 255)),
1370            "papayawhip" => Some(Color(255, 239, 213, 255)),
1371            "moccasin" => Some(Color(255, 228, 181, 255)),
1372
1373            // ===== Green =====
1374            "green" => Some(Color(0, 128, 0, 255)),
1375            "darkgreen" => Some(Color(0, 100, 0, 255)),
1376            "forestgreen" => Some(Color(34, 139, 34, 255)),
1377            "lime" => Some(Color(0, 255, 0, 255)),
1378            "limegreen" => Some(Color(50, 205, 50, 255)),
1379            "lightgreen" => Some(Color(144, 238, 144, 255)),
1380            "palegreen" => Some(Color(152, 251, 152, 255)),
1381            "springgreen" => Some(Color(0, 255, 127, 255)),
1382            "seagreen" => Some(Color(46, 139, 87, 255)),
1383            "mediumseagreen" => Some(Color(60, 179, 113, 255)),
1384            "yellowgreen" => Some(Color(154, 205, 50, 255)),
1385
1386            // ===== Cyan / Aqua =====
1387            "aqua" | "cyan" => Some(Color(0, 255, 255, 255)),
1388            "lightcyan" => Some(Color(224, 255, 255, 255)),
1389            "paleturquoise" => Some(Color(175, 238, 238, 255)),
1390            "turquoise" => Some(Color(64, 224, 208, 255)),
1391            "mediumturquoise" => Some(Color(72, 209, 204, 255)),
1392
1393            // ===== Blue =====
1394            "blue" => Some(Color(0, 0, 255, 255)),
1395            "mediumblue" => Some(Color(0, 0, 205, 255)),
1396            "darkblue" => Some(Color(0, 0, 139, 255)),
1397            "navy" => Some(Color(0, 0, 128, 255)),
1398            "royalblue" => Some(Color(65, 105, 225, 255)),
1399            "cornflowerblue" => Some(Color(100, 149, 237, 255)),
1400            "skyblue" => Some(Color(135, 206, 235, 255)),
1401            "lightblue" => Some(Color(173, 216, 230, 255)),
1402            "deepskyblue" => Some(Color(0, 191, 255, 255)),
1403
1404            // ===== Purple =====
1405            "purple" => Some(Color(128, 0, 128, 255)),
1406            "indigo" => Some(Color(75, 0, 130, 255)),
1407            "violet" => Some(Color(238, 130, 238, 255)),
1408            "plum" => Some(Color(221, 160, 221, 255)),
1409            "orchid" => Some(Color(218, 112, 214, 255)),
1410            "mediumpurple" => Some(Color(147, 112, 219, 255)),
1411            "rebeccapurple" => Some(Color(102, 51, 153, 255)),
1412
1413            // ===== Brown =====
1414            "brown" => Some(Color(165, 42, 42, 255)),
1415            "saddlebrown" => Some(Color(139, 69, 19, 255)),
1416            "sienna" => Some(Color(160, 82, 45, 255)),
1417            "chocolate" => Some(Color(210, 105, 30, 255)),
1418            "peru" => Some(Color(205, 133, 63, 255)),
1419            "burlywood" => Some(Color(222, 184, 135, 255)),
1420
1421            // ===== White variations =====
1422            "snow" => Some(Color(255, 250, 250, 255)),
1423            "honeydew" => Some(Color(240, 255, 240, 255)),
1424            "mintcream" => Some(Color(245, 255, 250, 255)),
1425            "azure" => Some(Color(240, 255, 255, 255)),
1426            "aliceblue" => Some(Color(240, 248, 255, 255)),
1427            "ghostwhite" => Some(Color(248, 248, 255, 255)),
1428
1429            // ===== Gray scale =====
1430            "gainsboro" => Some(Color(220, 220, 220, 255)),
1431            "lightgray" | "lightgrey" => Some(Color(211, 211, 211, 255)),
1432            "darkgray" | "darkgrey" => Some(Color(169, 169, 169, 255)),
1433            "dimgray" | "dimgrey" => Some(Color(105, 105, 105, 255)),
1434            "lightslategray" | "lightslategrey" => Some(Color(119, 136, 153, 255)),
1435            "slategray" | "slategrey" => Some(Color(112, 128, 144, 255)),
1436
1437            // ===== CSS System Colors =====
1438            "buttonface" => Some(Color(240, 240, 240, 255)),
1439            "buttontext" => Some(Color(0, 0, 0, 255)),
1440
1441            "linktext" => Some(Color(0, 0, 238, 255)),
1442            "visitedtext" => Some(Color(85, 26, 139, 255)),
1443            "activetext" => Some(Color(255, 0, 0, 255)),
1444
1445            "canvas" => Some(Color(255, 255, 255, 255)),
1446            "canvastext" => Some(Color(0, 0, 0, 255)),
1447
1448            "field" => Some(Color(255, 255, 255, 255)),
1449            "fieldtext" => Some(Color(0, 0, 0, 255)),
1450
1451            "highlight" => Some(Color(0, 120, 215, 255)),
1452            "highlighttext" => Some(Color(255, 255, 255, 255)),
1453
1454            "graytext" => Some(Color(128, 128, 128, 255)),
1455
1456            // ===== Special =====
1457            "transparent" => Some(Color(0, 0, 0, 0)),
1458            "none" => Some(Color(0, 0, 0, 0)),
1459
1460            _ => {
1461                log::error!(target: "Layouter", "Unknown CSS color keyword `{}` for `{}`", keyword, name);
1462                None
1463            }
1464        }
1465    }
1466
1467    /// Convert HSL to RGB (0..255)
1468    fn hsla_to_rgba(h: f32, s: f32, l: f32, a: f32) -> (u8, u8, u8, u8) {
1469        // 1. Compute Chroma
1470        let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
1471        let h_prime = h / 60.0;
1472        let x = c * (1.0 - ((h_prime % 2.0) - 1.0).abs());
1473
1474        // 2. Determine preliminary RGB values based on hue sector
1475        let (r1, g1, b1) = match h_prime as u32 {
1476            0 => (c, x, 0.0),
1477            1 => (x, c, 0.0),
1478            2 => (0.0, c, x),
1479            3 => (0.0, x, c),
1480            4 => (x, 0.0, c),
1481            5 | 6 => (c, 0.0, x),
1482            _ => (0.0, 0.0, 0.0),
1483        };
1484
1485        // 3. Add m to match the lightness
1486        let m = l - c / 2.0;
1487        let r = ((r1 + m) * 255.0).round().clamp(0.0, 255.0) as u8;
1488        let g = ((g1 + m) * 255.0).round().clamp(0.0, 255.0) as u8;
1489        let b = ((b1 + m) * 255.0).round().clamp(0.0, 255.0) as u8;
1490        let a = (a * 255.0).round().clamp(0.0, 255.0) as u8;
1491
1492        (r, g, b, a)
1493    }
1494
1495    match css_color {
1496        // Already parsed as an absolute color (rgb/rgba/hex, etc.)
1497        CssValue::Color(_) => {
1498            let (r, g, b, a) = css_color.to_rgba_tuple()?;
1499            Some(Color(r, g, b, a))
1500        }
1501
1502        // Named color keyword
1503        CssValue::Keyword(value) => keyword_color_to_color(name, value),
1504
1505        // rgb() / rgba() unified
1506        CssValue::Function(func, args) if func == "rgb" || func == "rgba" => {
1507            // Extract numeric components, ignoring commas and handling '/'
1508            let mut numbers = Vec::new();
1509            let mut alpha: Option<f32> = None;
1510            let mut after_slash = false;
1511
1512            for arg in args {
1513                match arg {
1514                    CssValue::Keyword(k) if k == "/" => {
1515                        after_slash = true;
1516                    }
1517                    CssValue::Number(n) => {
1518                        if after_slash {
1519                            alpha = Some(*n);
1520                        } else {
1521                            numbers.push(*n);
1522                        }
1523                    }
1524                    _ => return None,
1525                }
1526            }
1527
1528            if numbers.len() != 3 {
1529                return None;
1530            }
1531
1532            let a = alpha.unwrap_or(1.0);
1533
1534            Some(Color(
1535                (numbers[0] * 255.0).round() as u8,
1536                (numbers[1] * 255.0).round() as u8,
1537                (numbers[2] * 255.0).round() as u8,
1538                (a * 255.0).round() as u8,
1539            ))
1540        }
1541
1542        // hsl() / hsla() unified
1543        CssValue::Function(func, args) if func == "hsl" || func == "hsla" => {
1544            // Collect h, s, l and optional alpha
1545            let mut numbers = Vec::new();
1546            let mut alpha: Option<f32> = None;
1547            let mut after_slash = false;
1548
1549            for arg in args {
1550                match arg {
1551                    CssValue::Keyword(k) if k == "/" => {
1552                        after_slash = true;
1553                    }
1554                    CssValue::Number(n) => {
1555                        if after_slash {
1556                            alpha = Some(*n);
1557                        } else {
1558                            numbers.push(*n);
1559                        }
1560                    }
1561                    _ => return None,
1562                }
1563            }
1564
1565            if numbers.len() != 3 {
1566                return None;
1567            }
1568
1569            let a = alpha.unwrap_or(1.0);
1570            let (r, g, b, a) = hsla_to_rgba(numbers[0], numbers[1], numbers[2], a);
1571
1572            Some(Color(r, g, b, a))
1573        }
1574
1575        // Any other value reaching here is a pipeline error
1576        _ => {
1577            log::error!(
1578                target: "Layouter",
1579                "Unexpected CSS color value for `{}` at layout stage: {:?}",
1580                name,
1581                css_color
1582            );
1583            None
1584        }
1585    }
1586}