From 18bbc0176b809d8ca8d928f1670c1ec5f42da827 Mon Sep 17 00:00:00 2001 From: Universe Date: Fri, 24 Apr 2026 11:22:54 +0900 Subject: [PATCH] feat(htmlcss): inline-block siblings via Taffy flex-wrap emulation Taffy has no inline formatting context, so a block container holding `display: inline-block` siblings previously either stacked them vertically (when preserved as elements) or dropped their width/height (when flattened into an InlineGroup). Both broke the WPT ref pattern where two or more explicit-size inline-blocks should sit side-by-side. - collect.rs: preserve inline-block as an element when it has an explicit width or height instead of flattening into the inline path. - layout.rs: at Taffy-style time, detect containers whose element children are all inline-block (>=2, no text or other block children) and switch the container to Flex + flex-wrap:wrap + align-items:start. Lands layout-display-inline-block.html in L0.exact (65 fixtures, all 100.00%). Unlocks WPT refs that rely on inline-block for horizontal layout (37% of css-flexbox, 30% of css-position refs). --- crates/grida-canvas/src/htmlcss/collect.rs | 12 ++++- crates/grida-canvas/src/htmlcss/layout.rs | 33 ++++++++++++ .../L0/layout-display-inline-block.html | 51 +++++++++++++++++++ fixtures/test-html/suites/L0.exact.json | 3 +- 4 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 fixtures/test-html/L0/layout-display-inline-block.html diff --git a/crates/grida-canvas/src/htmlcss/collect.rs b/crates/grida-canvas/src/htmlcss/collect.rs index cb29cea2a..723e1d3c5 100644 --- a/crates/grida-canvas/src/htmlcss/collect.rs +++ b/crates/grida-canvas/src/htmlcss/collect.rs @@ -432,8 +432,16 @@ fn collect_element_with_counter( // Widgets with intrinsic sizes need their own Taffy node // for sizing to work — don't flatten them into inline groups. - let is_inline = child.display == types::Display::Inline - || child.display == types::Display::InlineBlock; + // Same for inline-block with explicit sizing: its width/height + // would be lost inside the inline path. Layout later emulates + // inline-block flow by mapping the parent to Taffy flex-wrap + // when all of its element children are inline-block. + let inline_block_with_size = child.display == types::Display::InlineBlock + && (child.width != types::CssLength::Auto + || child.height != types::CssLength::Auto); + let is_inline = (child.display == types::Display::Inline + || child.display == types::Display::InlineBlock) + && !inline_block_with_size; if is_inline && !child.widget.is_widget() && child.replaced.is_none() { collect_inline_items(&child, &mut pending_inline); } else { diff --git a/crates/grida-canvas/src/htmlcss/layout.rs b/crates/grida-canvas/src/htmlcss/layout.rs index 7c03e12a3..c99967c9a 100644 --- a/crates/grida-canvas/src/htmlcss/layout.rs +++ b/crates/grida-canvas/src/htmlcss/layout.rs @@ -126,6 +126,19 @@ fn build_taffy_node( apply_replaced_intrinsic_size(&mut style, replaced, images); } + // Emulate inline-block sibling flow via Taffy flex-wrap. Taffy has no + // inline formatting context, so a block container holding only + // inline-block siblings would otherwise stack them vertically. Only + // apply when ≥2 inline-block element children exist and no text or + // non-inline-block elements would be misrouted through flex. + if style.display == taffy::Display::Block && should_emulate_inline_block_container(el) { + style.display = taffy::Display::Flex; + style.flex_wrap = taffy::FlexWrap::Wrap; + // Override default `stretch` so inline-blocks keep their + // own block-size instead of filling the container's line height. + style.align_items = Some(taffy::AlignItems::Start); + } + // Build child nodes let mut child_ids: Vec = Vec::new(); @@ -176,6 +189,26 @@ fn build_taffy_node( taffy.new_with_children(style, &child_ids).unwrap() } +/// Returns true when `el` should lay out its children as a horizontal +/// flex-wrap row to emulate inline-block flow. Only safe when the +/// container holds ≥2 inline-block element siblings and no text or +/// non-inline-block element children (those would require a real inline +/// formatting context to mix with inline-blocks correctly). +fn should_emulate_inline_block_container(el: &StyledElement) -> bool { + let mut inline_block_count = 0usize; + for child in &el.children { + match child { + StyledNode::Element(child_el) => match child_el.display { + types::Display::InlineBlock => inline_block_count += 1, + types::Display::None => {} + _ => return false, + }, + StyledNode::Text(_) | StyledNode::InlineGroup(_) => return false, + } + } + inline_block_count >= 2 +} + /// Taffy context for text/inline leaf nodes. Stores inline items so the /// measure function can build a Skia Paragraph with placeholders at any /// available width. diff --git a/fixtures/test-html/L0/layout-display-inline-block.html b/fixtures/test-html/L0/layout-display-inline-block.html new file mode 100644 index 000000000..7bb679ed5 --- /dev/null +++ b/fixtures/test-html/L0/layout-display-inline-block.html @@ -0,0 +1,51 @@ + + + + + Layout: display: inline-block + + + +
+
+
+
+
+
+ + diff --git a/fixtures/test-html/suites/L0.exact.json b/fixtures/test-html/suites/L0.exact.json index 2e21f5434..5716626e4 100644 --- a/fixtures/test-html/suites/L0.exact.json +++ b/fixtures/test-html/suites/L0.exact.json @@ -85,6 +85,7 @@ { "path": "../L0/paint-border-style-dashed.html" }, { "path": "../L0/paint-filter-drop-shadow.html" }, { "path": "../L0/paint-transform-matrix.html" }, - { "path": "../L0/paint-filter-blur.html" } + { "path": "../L0/paint-filter-blur.html" }, + { "path": "../L0/layout-display-inline-block.html" } ] }