orinium_browser/engine/layouter/
builder.rs

1//! Layout builder, which transforms a DOM tree into a UI layout.
2
3use crate::engine::bridge::text;
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, ItemFragment, JustifyContent,
17    LayoutNode, Length, Style,
18};
19
20use super::css_resolver::ResolvedStyles;
21use super::types::{
22    BorderStyle, Color, ContainerRole, ContainerStyle, FontStyle, FontWeight, InfoNode, NodeKind,
23    TextAlign, TextDecoration, TextStyle,
24};
25
26/// Builds a layout tree (`LayoutNode`) and a render info tree (`InfoNode`) from the DOM.
27///
28/// # Overview
29/// - Recursively traverses the HTML DOM
30/// - Applies resolved CSS declarations
31/// - Computes layout-related styles
32/// - Collects render-time information (color, font size, text)
33///
34/// # Style resolution order (low → high priority)
35///
36/// 1. **Inherited values from parent**
37///    - `text_style`
38///
39/// 2. **Resolved CSS declarations**
40///    - Overrides inherited values when specified
41///
42/// 3. **HTML defaults / semantics**
43///    - `display` (block, inline, etc.)
44///    - Text measurement for text nodes
45///
46/// # Inherited properties
47///
48/// Only the following properties are inherited explicitly:
49///
50/// - `text_style`
51///
52/// All other style fields are initialized per node and are **not inherited**.
53///
54/// # Parameters
55///
56/// - `parent_text_style`
57///
58/// These values must be passed from the computed result of the parent when
59/// calling this function recursively.
60///
61/// # Returns
62///
63/// A tuple of:
64/// - `LayoutNode`: used by the layout engine
65/// - `InfoNode`: used for rendering (text, color, font size)
66pub fn build_layout_and_info(
67    dom: &Rc<RefCell<TreeNode<HtmlNodeType>>>,
68    resolved_styles: &ResolvedStyles,
69    measurer: &dyn text::TextMeasurer<TextStyle>,
70    parent_text_style: TextStyle,
71    mut chain: ElementChain,
72) -> (LayoutNode, InfoNode) {
73    let html_node = dom.borrow().value.clone();
74
75    /* -----------------------------
76       Initial values (inheritance)
77    ----------------------------- */
78    let mut style = Style::default();
79
80    let mut text_style = parent_text_style;
81    let mut container_style = ContainerStyle::default();
82
83    /* -----------------------------
84       Apply resolved CSS
85    ----------------------------- */
86    if let HtmlNodeType::Element {
87        tag_name,
88        attributes,
89        ..
90    } = &html_node
91    {
92        let id = attributes
93            .iter()
94            .find(|a| a.name == "id")
95            .map(|a| a.value.clone());
96
97        let class_list: Vec<String> = attributes
98            .iter()
99            .find(|attr| attr.name == "class")
100            .map(|attr| {
101                attr.value
102                    .split_whitespace()
103                    .map(|s| s.to_string())
104                    .collect()
105            })
106            .unwrap_or_default();
107
108        chain.insert(
109            0,
110            ElementInfo {
111                tag_name: tag_name.clone(),
112                id,
113                classes: class_list,
114            },
115        );
116
117        let candidates = collect_candidates(resolved_styles, &chain);
118
119        for (name, (value, _, _)) in candidates {
120            if name.starts_with("--") {
121                continue;
122            }
123            apply_declaration(
124                &name,
125                &value,
126                &mut style,
127                &mut container_style,
128                &mut text_style,
129            );
130        }
131    }
132
133    let (mut kind, inline_fragments_opt) = if let HtmlNodeType::Text(t) = &html_node {
134        let t = normalize_whitespace(t);
135
136        let mut kind = NodeKind::Text {
137            texts: split_fragments(&t),
138            style: text_style,
139        };
140
141        let inline_fragments = build_inline_fragments(&mut kind, measurer);
142
143        (kind, Some(inline_fragments))
144    } else if let Some(name) = html_node.tag_name()
145        && name == "a"
146        && let Some(href) = html_node.get_attr("href")
147    {
148        (
149            NodeKind::Container {
150                scroll_x: false,
151                scroll_y: false,
152                scroll_offset_x: 0.0,
153                scroll_offset_y: 0.0,
154                style: container_style,
155                role: ContainerRole::Link {
156                    href: href.to_string(),
157                },
158            },
159            None,
160        )
161    } else {
162        (
163            NodeKind::Container {
164                scroll_x: false,
165                scroll_y: false,
166                scroll_offset_x: 0.0,
167                scroll_offset_y: 0.0,
168                style: container_style,
169                role: ContainerRole::Normal,
170            },
171            None,
172        )
173    };
174
175    // Process Children if there are no inline fragments (i.e. text nodes).
176    let (layout, info) = if let Some(inline_fragments) = inline_fragments_opt {
177        /* -----------------------------
178           Text Node with inline fragments
179        ----------------------------- */
180
181        let style = Style {
182            display: Display::Inline,
183            ..style
184        };
185
186        let mut layout = LayoutNode::new(style);
187
188        layout.set_fragments(inline_fragments);
189
190        let info = InfoNode {
191            kind,
192            children: vec![],
193        };
194
195        (layout, info)
196    } else {
197        /* -----------------------------
198           Children
199        ----------------------------- */
200
201        // NOTE:
202        // Table 要素は未実装。
203        // 暫定的に Flex に置き換える。
204        // TODO: 将来的には TableLayout 実装に置き換える。
205        let mut layout_children = Vec::new();
206        let mut info_children = Vec::new();
207
208        if !matches!(style.display, Display::None) {
209            let mut has_text_child = false;
210
211            for child_dom in dom.borrow().children() {
212                if matches!(child_dom.borrow().value, HtmlNodeType::Text(_)) {
213                    has_text_child = true;
214                    break;
215                }
216            }
217
218            // 子に TextNode がある Block 要素は Flex(row) に変換
219            if has_text_child && matches!(style.display, Display::Block) {
220                style.display = Display::Flex {
221                    flex_direction: FlexDirection::Row,
222                };
223            }
224
225            // Table 要素は暫定的に Flex に置き換える。
226            match &html_node {
227                HtmlNodeType::Element { tag_name, .. }
228                    if tag_name == "table"
229                        || tag_name == "tbody"
230                        || tag_name == "thead"
231                        || tag_name == "tfoot" =>
232                {
233                    style.display = Display::Flex {
234                        flex_direction: FlexDirection::Column,
235                    };
236                }
237                HtmlNodeType::Element { tag_name, .. } if tag_name == "tr" => {
238                    style.display = Display::Flex {
239                        flex_direction: FlexDirection::Row,
240                    };
241                }
242                _ => {}
243            }
244
245            for child_dom in dom.borrow().children() {
246                let (child_layout, child_info) = build_layout_and_info(
247                    child_dom,
248                    resolved_styles,
249                    measurer,
250                    text_style,
251                    chain.clone(),
252                );
253
254                if dom.borrow().value.tag_name() == Some("html")
255                    && child_dom.borrow().value.tag_name() == Some("body")
256                    && let NodeKind::Container { style, .. } = &mut kind
257                    && style.background_color == Color(0, 0, 0, 0)
258                {
259                    let background_color = {
260                        let NodeKind::Container { style, .. } = &child_info.kind else {
261                            continue;
262                        };
263                        style.background_color
264                    };
265                    // html 要素の body 子要素に背景色が指定されていない場合、
266                    // body の背景色を html の背景色で上書きする
267                    style.background_color = background_color;
268                }
269
270                layout_children.push(child_layout);
271                info_children.push(child_info);
272            }
273        }
274
275        let layout = LayoutNode::with_children(style, layout_children);
276
277        let info = InfoNode {
278            kind,
279            children: info_children,
280        };
281
282        (layout, info)
283    };
284
285    (layout, info)
286}
287
288/// Splits text into text fragments for measurement. Each fragment is a word or a space.
289fn split_fragments(text: &str) -> Vec<String> {
290    let mut out = Vec::new();
291    let mut buf = String::new();
292
293    for c in text.chars() {
294        buf.push(c);
295
296        if c.is_whitespace() || c == '-' || !c.is_ascii() {
297            out.push(buf.clone());
298            buf.clear();
299        }
300    }
301
302    if !buf.is_empty() {
303        out.push(buf);
304    }
305
306    out
307}
308
309fn build_inline_fragments(
310    kind: &mut NodeKind,
311    measurer: &dyn text::TextMeasurer<TextStyle>,
312) -> Vec<ItemFragment> {
313    let NodeKind::Text { texts, style } = kind else {
314        return vec![];
315    };
316
317    let mut inline_fragments = Vec::with_capacity(texts.len());
318
319    for text in texts {
320        let req = text::TextMeasureRequest {
321            text: text.clone(),
322            style: *style,
323            max_width: None,
324            wrap: false,
325        };
326
327        let (width, height) = measurer
328            .measure(&req)
329            .map(|m| (m.width, m.height))
330            .unwrap_or((800.0, style.font_size * 1.2));
331
332        let fragment = ItemFragment::Fragment(Fragment { width, height });
333
334        inline_fragments.push(fragment);
335    }
336
337    inline_fragments
338}
339
340fn normalize_whitespace(text: &str) -> String {
341    let mut result = String::new();
342    let mut prev_was_space = false;
343
344    for c in text.chars() {
345        if c.is_whitespace() {
346            if !prev_was_space {
347                result.push(' ');
348                prev_was_space = true;
349            }
350        } else {
351            result.push(c);
352            prev_was_space = false;
353        }
354    }
355
356    result
357}
358
359fn collect_candidates(
360    resolved_styles: &ResolvedStyles,
361    chain: &ElementChain,
362) -> HashMap<String, (CssValue, (u32, u32, u32), usize)> {
363    let mut candidates: HashMap<String, (CssValue, (u32, u32, u32), usize)> = HashMap::new();
364
365    for decl in resolved_styles {
366        if decl.selector.matches(chain) {
367            let entry = candidates.get(&decl.name);
368
369            let should_replace = match entry {
370                None => true,
371                Some((_, spec, order)) => {
372                    decl.specificity > *spec || (decl.specificity == *spec && decl.order > *order)
373                }
374            };
375
376            if should_replace {
377                candidates.insert(
378                    decl.name.clone(),
379                    (decl.value.clone(), decl.specificity, decl.order),
380                );
381            }
382        }
383    }
384
385    candidates
386}
387
388fn apply_declaration(
389    name: &str,
390    value: &CssValue,
391    style: &mut Style,
392    container_style: &mut ContainerStyle,
393    text_style: &mut TextStyle,
394) -> Option<()> {
395    fn expand_box<F>(
396        value: &CssValue,
397        text_style: &TextStyle,
398        resolve_css_len: &impl Fn(&CssValue, &TextStyle) -> Option<Length>,
399        mut set: F,
400    ) -> Option<()>
401    where
402        F: FnMut(Length, Length, Length, Length),
403    {
404        let resolve = |v: &CssValue| -> Option<Length> { resolve_css_len(v, text_style) };
405
406        match value {
407            CssValue::List(values) => {
408                let vals: Vec<Length> = values.iter().map(resolve).collect::<Option<_>>()?;
409
410                match vals.as_slice() {
411                    [a] => set(a.clone(), a.clone(), a.clone(), a.clone()),
412                    [v, h] => set(v.clone(), h.clone(), v.clone(), h.clone()),
413                    [t, h, b] => set(t.clone(), h.clone(), b.clone(), h.clone()),
414                    [t, r, b, l] => set(t.clone(), r.clone(), b.clone(), l.clone()),
415                    _ => return None,
416                }
417            }
418
419            _ => {
420                let v = resolve_css_len(value, text_style)?;
421                set(v.clone(), v.clone(), v.clone(), v);
422            }
423        }
424
425        Some(())
426    }
427
428    fn parse_border_shorthand(
429        value: &CssValue,
430        text_style: &TextStyle,
431    ) -> Option<(Option<Length>, Option<BorderStyle>, Option<Color>)> {
432        let mut width: Option<Length> = None;
433        let mut style_v: Option<BorderStyle> = None;
434        let mut color_v: Option<Color> = None;
435
436        let items: Vec<&CssValue> = match value {
437            CssValue::List(values) => values.iter().collect(),
438            _ => vec![value],
439        };
440
441        for v in items {
442            let token = v;
443
444            // try as length (numeric lengths)
445            if width.is_none()
446                && let Some(l) = resolve_css_len(token, text_style)
447            {
448                width = Some(l);
449                continue;
450            }
451
452            // try as width keyword (thin/medium/thick). Check keywords before style keywords.
453            if width.is_none()
454                && let CssValue::Keyword(s) = token
455            {
456                match s.as_str().to_ascii_lowercase().as_str() {
457                    "thin" => {
458                        width = Some(Length::Px(1.0));
459                        continue;
460                    }
461                    "medium" => {
462                        width = Some(Length::Px(3.0));
463                        continue;
464                    }
465                    "midium" => {
466                        width = Some(Length::Px(3.0));
467                        continue;
468                    } // common misspelling
469                    "thick" => {
470                        width = Some(Length::Px(5.0));
471                        continue;
472                    }
473                    _ => {}
474                }
475            }
476
477            // try as style keyword
478            if style_v.is_none()
479                && let CssValue::Keyword(s) = token
480            {
481                let s_lower = s.as_str();
482                let parsed = match s_lower {
483                    "none" => Some(BorderStyle::None),
484                    "solid" => Some(BorderStyle::Solid),
485                    "dashed" => Some(BorderStyle::Dashed),
486                    "dotted" => Some(BorderStyle::Dotted),
487                    _ => None,
488                };
489
490                if let Some(p) = parsed {
491                    style_v = Some(p);
492                    continue;
493                }
494            }
495
496            // try as color
497            if color_v.is_none()
498                && let Some(c) = resolve_css_color(token)
499            {
500                color_v = Some(c);
501                continue;
502            }
503
504            // unknown token: ignore
505        }
506
507        Some((width, style_v, color_v))
508    }
509
510    match (name, value) {
511        /* ======================
512         * Display
513         * ====================== */
514        ("display", CssValue::Keyword(v)) => {
515            style.display = match v.as_str() {
516                "block" => Display::Block,
517                "flex" => Display::Flex {
518                    flex_direction: FlexDirection::Row,
519                },
520                "inline" => Display::Inline,
521                "none" => Display::None,
522                _ => style.display,
523            };
524        }
525
526        /* ======================
527         * Color / Text
528         * ====================== */
529        ("background-color", _) => {
530            container_style.background_color = match value {
531                CssValue::Keyword(kw) if kw.eq_ignore_ascii_case("inherit") => {
532                    // inherit: use parent's text color
533                    text_style.color
534                }
535                CssValue::Keyword(kw) if kw.eq_ignore_ascii_case("currentColor") => {
536                    text_style.color
537                }
538                _ => resolve_css_color(value)?,
539            };
540        }
541
542        ("background", _) => {
543            container_style.background_color = match value {
544                CssValue::Keyword(kw) if kw.eq_ignore_ascii_case("inherit") => {
545                    // inherit: use parent's text color
546                    text_style.color
547                }
548                CssValue::Keyword(kw) if kw.eq_ignore_ascii_case("currentColor") => {
549                    text_style.color
550                }
551                _ => resolve_css_color(value)?,
552            };
553        }
554
555        ("color", _) => {
556            text_style.color = match value {
557                CssValue::Keyword(kw) if kw.eq_ignore_ascii_case("inherit") => {
558                    // inherit: use parent's color
559                    text_style.color
560                }
561                CssValue::Keyword(kw) if kw.eq_ignore_ascii_case("currentColor") => {
562                    text_style.color
563                }
564                _ => resolve_css_color(value)?,
565            }
566        }
567
568        ("font-size", CssValue::Length(_, _)) => {
569            // TODO: Add other size
570            let len = resolve_css_len(value, text_style)?;
571            let px = match &len {
572                Length::Px(v) => *v,
573                Length::Percent(v) => *v * text_style.font_size / 100.0,
574                _ => {
575                    log::error!(target: "Layouter", "Unknown size type for font-size: {:?}", len);
576                    return None;
577                }
578            };
579            text_style.font_size = px;
580        }
581
582        ("font-weight", CssValue::Keyword(v)) => {
583            text_style.font_weight = match v.as_str() {
584                "normal" => FontWeight::NORMAL,
585                "bold" => FontWeight::BOLD,
586                _ => text_style.font_weight,
587            };
588        }
589        ("font-weight", CssValue::Number(v)) => {
590            text_style.font_weight = FontWeight(*v as u16);
591        }
592
593        ("font-style", CssValue::Keyword(v)) => {
594            text_style.font_style = match v.as_str() {
595                "normal" => FontStyle::Normal,
596                "italic" => FontStyle::Italic,
597                "oblique" => FontStyle::Oblique,
598                _ => text_style.font_style,
599            };
600        }
601
602        ("text-decoration", CssValue::Keyword(v)) => {
603            text_style.text_decoration = match v.as_str() {
604                "none" => TextDecoration::None,
605                "underline" => TextDecoration::Underline,
606                "line-through" => TextDecoration::LineThrough,
607                "overline" => TextDecoration::Overline,
608                _ => TextDecoration::None,
609            };
610        }
611
612        ("text-align", CssValue::Keyword(v)) if v == "left" => {
613            text_style.text_align = TextAlign::Left;
614        }
615        ("text-align", CssValue::Keyword(v)) if v == "center" => {
616            text_style.text_align = TextAlign::Center;
617        }
618        ("text-align", CssValue::Keyword(v)) if v == "right" => {
619            text_style.text_align = TextAlign::Right;
620        }
621
622        /* ======================
623         * Box Model
624         * ====================== */
625        ("box-sizing", CssValue::Keyword(v)) => {
626            style.box_sizing = match v.as_str() {
627                "content-box" => BoxSizing::ContentBox,
628                "border-box" => BoxSizing::BorderBox,
629                _ => BoxSizing::ContentBox,
630            };
631        }
632
633        ("border-style", CssValue::Keyword(v)) => {
634            let s = match v.as_str() {
635                "none" => BorderStyle::None,
636                "solid" => BorderStyle::Solid,
637                "dashed" => BorderStyle::Dashed,
638                "dotted" => BorderStyle::Dotted,
639                _ => BorderStyle::None,
640            };
641
642            container_style.border_style.top = s;
643            container_style.border_style.right = s;
644            container_style.border_style.bottom = s;
645            container_style.border_style.left = s;
646        }
647
648        ("margin", v) => {
649            expand_box(v, text_style, &resolve_css_len, |t, r, b, l| {
650                style.spacing.margin_top = t;
651                style.spacing.margin_right = r;
652                style.spacing.margin_bottom = b;
653                style.spacing.margin_left = l;
654            })?;
655        }
656        ("margin-top", _) => {
657            style.spacing.margin_top = resolve_css_len(value, text_style)?;
658        }
659        ("margin-right", _) => {
660            style.spacing.margin_right = resolve_css_len(value, text_style)?;
661        }
662        ("margin-bottom", _) => {
663            style.spacing.margin_bottom = resolve_css_len(value, text_style)?;
664        }
665        ("margin-left", _) => {
666            style.spacing.margin_left = resolve_css_len(value, text_style)?;
667        }
668
669        ("border", v) => {
670            let (maybe_width, maybe_style, maybe_color) = parse_border_shorthand(v, text_style)?;
671
672            if let Some(w) = maybe_width {
673                style.spacing.border_top = w.clone();
674                style.spacing.border_right = w.clone();
675                style.spacing.border_bottom = w.clone();
676                style.spacing.border_left = w;
677            }
678
679            if let Some(s) = maybe_style {
680                container_style.border_style.top = s;
681                container_style.border_style.right = s;
682                container_style.border_style.bottom = s;
683                container_style.border_style.left = s;
684            }
685
686            if let Some(c) = maybe_color {
687                container_style.border_color.top = c;
688                container_style.border_color.right = c;
689                container_style.border_color.bottom = c;
690                container_style.border_color.left = c;
691            }
692        }
693        ("border-top", _) => {
694            if let CssValue::List(_) = value {
695                let (maybe_width, maybe_style, maybe_color) =
696                    parse_border_shorthand(value, text_style)?;
697                if let Some(w) = maybe_width {
698                    style.spacing.border_top = w;
699                }
700                if let Some(s) = maybe_style {
701                    container_style.border_style.top = s;
702                }
703                if let Some(c) = maybe_color {
704                    container_style.border_color.top = c;
705                }
706            } else {
707                style.spacing.border_top = resolve_css_len(value, text_style)?;
708            }
709        }
710        ("border-right", _) => {
711            if let CssValue::List(_) = value {
712                let (maybe_width, maybe_style, maybe_color) =
713                    parse_border_shorthand(value, text_style)?;
714                if let Some(w) = maybe_width {
715                    style.spacing.border_right = w;
716                }
717                if let Some(s) = maybe_style {
718                    container_style.border_style.right = s;
719                }
720                if let Some(c) = maybe_color {
721                    container_style.border_color.right = c;
722                }
723            } else {
724                style.spacing.border_right = resolve_css_len(value, text_style)?;
725            }
726        }
727        ("border-bottom", _) => {
728            if let CssValue::List(_) = value {
729                let (maybe_width, maybe_style, maybe_color) =
730                    parse_border_shorthand(value, text_style)?;
731                if let Some(w) = maybe_width {
732                    style.spacing.border_bottom = w;
733                }
734                if let Some(s) = maybe_style {
735                    container_style.border_style.bottom = s;
736                }
737                if let Some(c) = maybe_color {
738                    container_style.border_color.bottom = c;
739                }
740            } else {
741                style.spacing.border_bottom = resolve_css_len(value, text_style)?;
742            }
743        }
744        ("border-left", _) => {
745            if let CssValue::List(_) = value {
746                let (maybe_width, maybe_style, maybe_color) =
747                    parse_border_shorthand(value, text_style)?;
748                if let Some(w) = maybe_width {
749                    style.spacing.border_left = w;
750                }
751                if let Some(s) = maybe_style {
752                    container_style.border_style.left = s;
753                }
754                if let Some(c) = maybe_color {
755                    container_style.border_color.left = c;
756                }
757            } else {
758                style.spacing.border_left = resolve_css_len(value, text_style)?;
759            }
760        }
761
762        ("padding", v) => {
763            expand_box(v, text_style, &resolve_css_len, |t, r, b, l| {
764                style.spacing.padding_top = t;
765                style.spacing.padding_right = r;
766                style.spacing.padding_bottom = b;
767                style.spacing.padding_left = l;
768            })?;
769        }
770        ("padding-top", _) => {
771            style.spacing.padding_top = resolve_css_len(value, text_style)?;
772        }
773        ("padding-right", _) => {
774            style.spacing.padding_right = resolve_css_len(value, text_style)?;
775        }
776        ("padding-bottom", _) => {
777            style.spacing.padding_bottom = resolve_css_len(value, text_style)?;
778        }
779        ("padding-left", _) => {
780            style.spacing.padding_left = resolve_css_len(value, text_style)?;
781        }
782
783        /* ======================
784         * Size
785         * ====================== */
786        ("width", _) => {
787            style.size.width = resolve_css_len(value, text_style)?;
788        }
789        ("height", _) => {
790            style.size.height = resolve_css_len(value, text_style)?;
791        }
792        ("min-width", _) => {
793            style.size.min_width = resolve_css_len(value, text_style)?;
794        }
795        ("min-height", _) => {
796            style.size.min_height = resolve_css_len(value, text_style)?;
797        }
798        ("max-width", _) => {
799            style.size.max_width = resolve_css_len(value, text_style)?;
800        }
801        ("max-height", _) => {
802            style.size.max_height = resolve_css_len(value, text_style)?;
803        }
804
805        /* ======================
806         * Flex
807         * ====================== */
808        ("flex-direction", CssValue::Keyword(v)) => {
809            if let Display::Flex { flex_direction } = &mut style.display {
810                *flex_direction = match v.as_str() {
811                    "row" => FlexDirection::Row,
812                    "column" => FlexDirection::Column,
813                    _ => return None,
814                };
815            }
816        }
817
818        ("justify-content", CssValue::Keyword(v)) => {
819            style.justify_content = match v.as_str() {
820                "flex-start" => JustifyContent::Start,
821                "center" => JustifyContent::Center,
822                "flex-end" => JustifyContent::End,
823                "space-between" => JustifyContent::SpaceBetween,
824                "space-around" => JustifyContent::SpaceAround,
825                _ => return None,
826            };
827        }
828
829        ("align-items", CssValue::Keyword(v)) => {
830            style.align_items = match v.as_str() {
831                "stretch" => AlignItems::Stretch,
832                "flex-start" => AlignItems::Start,
833                "center" => AlignItems::Center,
834                "flex-end" => AlignItems::End,
835                _ => return None,
836            };
837        }
838
839        ("gap", _) => {
840            let gap = resolve_css_len(value, text_style)?;
841            style.row_gap = gap.clone();
842            style.column_gap = gap;
843        }
844
845        ("align-self", CssValue::Keyword(v)) => {
846            style.item_style.align_self = match v.as_str() {
847                "stretch" => Some(AlignItems::Stretch),
848                "flex-start" => Some(AlignItems::Start),
849                "center" => Some(AlignItems::Center),
850                "flex-end" => Some(AlignItems::End),
851                _ => return None,
852            };
853        }
854
855        ("flex-grow", CssValue::Number(v)) => {
856            style.item_style.flex_grow = *v;
857        }
858
859        ("flex-basis", _) => {
860            style.item_style.flex_basis = resolve_css_len(value, text_style)?;
861        }
862
863        _ => {}
864    }
865    Some(())
866}
867
868/// Resolve CssValue to Length.
869fn resolve_css_len(css_len: &CssValue, text_style: &TextStyle) -> Option<Length> {
870    match &css_len {
871        CssValue::Length(v, Unit::Em) => Some(Length::Px(text_style.font_size * v)),
872        CssValue::Length(v, Unit::Rem) => Some(Length::Px(16.0 * v)), // html sont-size 仮値
873        CssValue::Length(v, u) => match u {
874            Unit::Percent => Some(Length::Percent(*v)),
875            Unit::Px => Some(Length::Px(*v)),
876            Unit::Vw => Some(Length::Vw(*v)),
877            Unit::Vh => Some(Length::Vh(*v)),
878            Unit::Em | Unit::Rem => unreachable!(),
879        },
880        CssValue::Number(0.0) => Some(Length::Px(0.0)),
881        CssValue::Keyword(s) => match s.as_str() {
882            "auto" => Some(Length::Auto),
883            _ => None,
884        },
885        CssValue::Function(name, args) if name == "calc" && !args.is_empty() => {
886            let mut iter = args.iter();
887            let mut result = resolve_css_len(iter.next().unwrap(), text_style)?;
888
889            while let (Some(op), Some(val)) = (iter.next(), iter.next()) {
890                match op {
891                    CssValue::Keyword(o) if o == "+" => {
892                        let val_resolved = resolve_css_len(val, text_style)?;
893                        result = Length::Add(Box::new(result), Box::new(val_resolved));
894                    }
895                    CssValue::Keyword(o) if o == "-" => {
896                        let val_resolved = resolve_css_len(val, text_style)?;
897                        result = Length::Sub(Box::new(result), Box::new(val_resolved));
898                    }
899                    CssValue::Keyword(o) if o == "*" => {
900                        if let CssValue::Number(factor) = val {
901                            result = Length::Mul(Box::new(result), *factor);
902                        } else {
903                            log::error!(target: "Layouter", "Invalid operand for multiplication in calc(): {:?}", val);
904                            return None;
905                        }
906                    }
907                    CssValue::Keyword(o) if o == "/" => {
908                        if let CssValue::Number(factor) = val {
909                            if *factor == 0.0 {
910                                log::error!(target: "Layouter", "Division by zero in calc()");
911                                return None;
912                            }
913                            result = Length::Div(Box::new(result), *factor);
914                        } else {
915                            log::error!(target: "Layouter", "Invalid operand for division in calc(): {:?}", val);
916                            return None;
917                        }
918                    }
919                    _ => {
920                        log::error!(target: "Layouter", "Unknown operator for calc function: {:?}", op);
921                        return None;
922                    }
923                }
924            }
925
926            Some(result)
927        }
928        _ => {
929            log::error!(target: "Layouter", "Unknown CSS Length type: {:?}", css_len);
930            None
931        }
932    }
933}
934
935/// Resolve a computed CssValue into a final RGBA Color.
936///
937/// Assumptions:
938/// - This function is called *after* cascade and inheritance resolution.
939/// - Keywords like `currentColor`, `inherit`, `initial`, `unset`
940///   must NOT reach this stage.
941/// - The returned Color is always absolute RGBA.
942fn resolve_css_color(css_color: &CssValue) -> Option<Color> {
943    fn keyword_color_to_color(keyword: &str) -> Option<Color> {
944        // NOTE:
945        // Keyword matching is case-insensitive according to CSS specs.
946        // Keep this list limited to commonly used CSS Color Level 3 keywords.
947        match keyword.to_ascii_lowercase().as_str() {
948            // Basic colors
949            "black" => Some(Color(0, 0, 0, 255)),
950            "white" => Some(Color(255, 255, 255, 255)),
951            "red" => Some(Color(255, 0, 0, 255)),
952            "green" => Some(Color(0, 128, 0, 255)),
953            "blue" => Some(Color(0, 0, 255, 255)),
954            "yellow" => Some(Color(255, 255, 0, 255)),
955
956            // Gray variants (US / UK spelling)
957            "gray" | "grey" => Some(Color(128, 128, 128, 255)),
958            "lightgray" | "lightgrey" => Some(Color(211, 211, 211, 255)),
959            "darkgray" | "darkgrey" => Some(Color(169, 169, 169, 255)),
960
961            // Frequently used named colors
962            "royalblue" => Some(Color(65, 105, 225, 255)),
963            "cornflowerblue" => Some(Color(100, 149, 237, 255)),
964            "skyblue" => Some(Color(135, 206, 235, 255)),
965            "lightblue" => Some(Color(173, 216, 230, 255)),
966
967            "orange" => Some(Color(255, 165, 0, 255)),
968            "pink" => Some(Color(255, 192, 203, 255)),
969            "purple" => Some(Color(128, 0, 128, 255)),
970            "brown" => Some(Color(165, 42, 42, 255)),
971
972            // Special keyword
973            "transparent" => Some(Color(0, 0, 0, 0)),
974            "initial" => Some(Color(0, 0, 0, 255)),
975
976            // CSS Level 4 system colors (approximate)
977            // stub implementations
978            "buttonface" => Some(Color(240, 240, 240, 255)),
979            "buttontext" => Some(Color(0, 0, 0, 255)),
980            "linktext" => Some(Color(0, 0, 255, 255)),
981
982            // Stub for none keyword (e.g. border-color: none, background: none, etc.)
983            "none" => Some(Color(0, 0, 0, 0)),
984
985            _ => {
986                log::error!(target: "Layouter", "Unknown CSS color keyword: {}", keyword);
987                None
988            }
989        }
990    }
991
992    /// Convert HSL to RGB (0..255)
993    fn hsla_to_rgba(h: f32, s: f32, l: f32, a: f32) -> (u8, u8, u8, u8) {
994        // 1. Compute Chroma
995        let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
996        let h_prime = h / 60.0;
997        let x = c * (1.0 - ((h_prime % 2.0) - 1.0).abs());
998
999        // 2. Determine preliminary RGB values based on hue sector
1000        let (r1, g1, b1) = match h_prime as u32 {
1001            0 => (c, x, 0.0),
1002            1 => (x, c, 0.0),
1003            2 => (0.0, c, x),
1004            3 => (0.0, x, c),
1005            4 => (x, 0.0, c),
1006            5 | 6 => (c, 0.0, x),
1007            _ => (0.0, 0.0, 0.0),
1008        };
1009
1010        // 3. Add m to match the lightness
1011        let m = l - c / 2.0;
1012        let r = ((r1 + m) * 255.0).round().clamp(0.0, 255.0) as u8;
1013        let g = ((g1 + m) * 255.0).round().clamp(0.0, 255.0) as u8;
1014        let b = ((b1 + m) * 255.0).round().clamp(0.0, 255.0) as u8;
1015        let a = (a * 255.0).round().clamp(0.0, 255.0) as u8;
1016
1017        (r, g, b, a)
1018    }
1019
1020    match css_color {
1021        // Already parsed as an absolute color (rgb/rgba/hex, etc.)
1022        CssValue::Color(_) => {
1023            let (r, g, b, a) = css_color.to_rgba_tuple()?;
1024            Some(Color(r, g, b, a))
1025        }
1026
1027        // Named color keyword
1028        CssValue::Keyword(value) => keyword_color_to_color(value),
1029
1030        // rgb() / rgba() unified
1031        CssValue::Function(func, args) if func == "rgb" || func == "rgba" => {
1032            // Extract numeric components, ignoring commas and handling '/'
1033            let mut numbers = Vec::new();
1034            let mut alpha: Option<f32> = None;
1035            let mut after_slash = false;
1036
1037            for arg in args {
1038                match arg {
1039                    CssValue::Keyword(k) if k == "/" => {
1040                        after_slash = true;
1041                    }
1042                    CssValue::Number(n) => {
1043                        if after_slash {
1044                            alpha = Some(*n);
1045                        } else {
1046                            numbers.push(*n);
1047                        }
1048                    }
1049                    _ => return None,
1050                }
1051            }
1052
1053            if numbers.len() != 3 {
1054                return None;
1055            }
1056
1057            let a = alpha.unwrap_or(1.0);
1058
1059            Some(Color(
1060                (numbers[0] * 255.0).round() as u8,
1061                (numbers[1] * 255.0).round() as u8,
1062                (numbers[2] * 255.0).round() as u8,
1063                (a * 255.0).round() as u8,
1064            ))
1065        }
1066
1067        // hsl() / hsla() unified
1068        CssValue::Function(func, args) if func == "hsl" || func == "hsla" => {
1069            // Collect h, s, l and optional alpha
1070            let mut numbers = Vec::new();
1071            let mut alpha: Option<f32> = None;
1072            let mut after_slash = false;
1073
1074            for arg in args {
1075                match arg {
1076                    CssValue::Keyword(k) if k == "/" => {
1077                        after_slash = true;
1078                    }
1079                    CssValue::Number(n) => {
1080                        if after_slash {
1081                            alpha = Some(*n);
1082                        } else {
1083                            numbers.push(*n);
1084                        }
1085                    }
1086                    _ => return None,
1087                }
1088            }
1089
1090            if numbers.len() != 3 {
1091                return None;
1092            }
1093
1094            let a = alpha.unwrap_or(1.0);
1095            let (r, g, b, a) = hsla_to_rgba(numbers[0], numbers[1], numbers[2], a);
1096
1097            Some(Color(r, g, b, a))
1098        }
1099
1100        // Any other value reaching here is a pipeline error
1101        _ => {
1102            log::error!(
1103                target: "Layouter",
1104                "Unexpected CSS color value at layout stage: {:?}",
1105                css_color
1106            );
1107            None
1108        }
1109    }
1110}