From d3485ba91ec84ed029fc59bd341595bd25109733 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Fri, 9 Oct 2026 20:07:22 +0000 Subject: [PATCH 1/5] Widen an inline root's containing block claims to those of its inline spans --- packages/blitz-dom/src/layout/construct.rs | 36 ++++++++++++++++++++++ packages/blitz-dom/src/node/node.rs | 3 ++ packages/blitz-dom/src/node/text.rs | 3 ++ packages/stylo_taffy/src/convert.rs | 23 ++++++++++++++ packages/stylo_taffy/src/wrapper.rs | 11 ++++++- 5 files changed, 75 insertions(+), 1 deletion(-) diff --git a/packages/blitz-dom/src/layout/construct.rs b/packages/blitz-dom/src/layout/construct.rs index 63a1657c5f..735f62fac4 100644 --- a/packages/blitz-dom/src/layout/construct.rs +++ b/packages/blitz-dom/src/layout/construct.rs @@ -30,6 +30,7 @@ use crate::{ qual_name, stylo_to_parley, traversal::{iter_children, iter_children_and_pseudos}, }; +use stylo_taffy::StyleFlags; use super::{ damage::ALL_DAMAGE, @@ -1098,6 +1099,8 @@ pub(crate) fn build_inline_layout_into( let mut text_transformer = TextTransformer::default(); text_transformer.word_break(&builder); + let mut span_cb_flags = StyleFlags::empty(); + if let Some(before_id) = root_node.before() { build_inline_layout_recursive( &mut builder, @@ -1105,6 +1108,8 @@ pub(crate) fn build_inline_layout_into( nodes, before_id, &text_transform, + StyleFlags::empty(), + &mut span_cb_flags, ); } for child_id in root_node.children.iter().copied() { @@ -1114,6 +1119,8 @@ pub(crate) fn build_inline_layout_into( nodes, child_id, &text_transform, + StyleFlags::empty(), + &mut span_cb_flags, ); } if let Some(after_id) = root_node.after() { @@ -1123,10 +1130,13 @@ pub(crate) fn build_inline_layout_into( nodes, after_id, &text_transform, + StyleFlags::empty(), + &mut span_cb_flags, ); } text_layout.text = builder.build_into(&mut text_layout.layout); + text_layout.span_cb_flags = span_cb_flags; return; fn build_inline_layout_recursive( @@ -1135,6 +1145,10 @@ pub(crate) fn build_inline_layout_into( nodes: &crate::NodeTree, node_id: NodeId, parent_text_transform: &CaseTransform, + // The containing block flags of the inline spans between the node and the inline root + span_cb_flags: StyleFlags, + // Accumulates `span_cb_flags` of every inline box + root_cb_flags: &mut StyleFlags, ) { let node = &nodes[node_id]; @@ -1197,6 +1211,8 @@ pub(crate) fn build_inline_layout_into( nodes, child_id, &text_transform, + span_cb_flags, + root_cb_flags, ); } builder.pop_style_span(); @@ -1208,6 +1224,7 @@ pub(crate) fn build_inline_layout_into( if box_kind == InlineBoxKind::InFlow { text_transformer.word_break(builder); } + *root_cb_flags |= span_cb_flags; builder.push_inline_box(InlineBox { id: node_id.as_u64(), kind: box_kind, @@ -1245,6 +1262,18 @@ pub(crate) fn build_inline_layout_into( builder.push_style_span(style); + let mut span_cb_flags = span_cb_flags; + if let Some(style) = node.primary_styles() { + let claims = + stylo_taffy::convert::inline_containing_block_claims(&style); + if claims.absolute { + span_cb_flags |= StyleFlags::SPAN_ABSOLUTE_CB; + } + if claims.fixed { + span_cb_flags |= StyleFlags::SPAN_FIXED_CB; + } + } + if let Some(before_id) = node.before() { build_inline_layout_recursive( builder, @@ -1252,6 +1281,8 @@ pub(crate) fn build_inline_layout_into( nodes, before_id, &text_transform, + span_cb_flags, + root_cb_flags, ); } @@ -1262,6 +1293,8 @@ pub(crate) fn build_inline_layout_into( nodes, child_id, &text_transform, + span_cb_flags, + root_cb_flags, ); } if let Some(after_id) = node.after() { @@ -1271,6 +1304,8 @@ pub(crate) fn build_inline_layout_into( nodes, after_id, &text_transform, + span_cb_flags, + root_cb_flags, ); } @@ -1282,6 +1317,7 @@ pub(crate) fn build_inline_layout_into( if box_kind == InlineBoxKind::InFlow { text_transformer.word_break(builder); } + *root_cb_flags |= span_cb_flags; builder.push_inline_box(InlineBox { id: node_id.as_u64(), kind: box_kind, diff --git a/packages/blitz-dom/src/node/node.rs b/packages/blitz-dom/src/node/node.rs index 4c57b35969..b088084ebd 100644 --- a/packages/blitz-dom/src/node/node.rs +++ b/packages/blitz-dom/src/node/node.rs @@ -1120,6 +1120,9 @@ impl Node { if crate::layout::replaced::is_replaced_element(&el.name.local) { flags |= stylo_taffy::StyleFlags::IS_REPLACED; } + if let Some(inline_layout) = el.inline_layout_data.as_deref() { + flags |= inline_layout.span_cb_flags; + } } #[cfg(feature = "writing-mode")] diff --git a/packages/blitz-dom/src/node/text.rs b/packages/blitz-dom/src/node/text.rs index d920b821e3..cc4127edb8 100644 --- a/packages/blitz-dom/src/node/text.rs +++ b/packages/blitz-dom/src/node/text.rs @@ -29,6 +29,9 @@ pub struct TextLayout { /// Block-axis offset (in CSS px) of the line boxes from the top of the container's content /// box, as applied by `align-content`. pub block_offset: f32, + /// The out-of-flow positions for which some inline span with an inline box below it is a + /// containing block (as `SPAN_*_CB` flags). The inline root claims those boxes in its place. + pub span_cb_flags: stylo_taffy::StyleFlags, } impl TextLayout { diff --git a/packages/stylo_taffy/src/convert.rs b/packages/stylo_taffy/src/convert.rs index c8e24e3960..0e6ddfe622 100644 --- a/packages/stylo_taffy/src/convert.rs +++ b/packages/stylo_taffy/src/convert.rs @@ -370,6 +370,29 @@ pub fn containing_block_claims(style: &stylo::ComputedValues) -> taffy::Containi } } +/// [`containing_block_claims`] for a non-atomic inline box, to which transforms and containment +/// do not apply. +pub fn inline_containing_block_claims( + style: &stylo::ComputedValues, +) -> taffy::ContainingBlockClaims { + use style::values::specified::box_::WillChangeBits; + + let box_style = style.get_box(); + let effects = style.get_effects(); + let fixed = !effects.filter.0.is_empty() + || !effects.backdrop_filter.0.is_empty() + || box_style + .will_change + .bits + .intersects(WillChangeBits::FIXPOS_CB_NON_SVG); + taffy::ContainingBlockClaims { + absolute: box_style.position != stylo::Position::Static + || fixed + || establishes_absolute_containing_block(style), + fixed, + } +} + #[inline] pub fn overflow(input: stylo::Overflow) -> taffy::Overflow { match input { diff --git a/packages/stylo_taffy/src/wrapper.rs b/packages/stylo_taffy/src/wrapper.rs index 5926baeb09..fe93789a62 100644 --- a/packages/stylo_taffy/src/wrapper.rs +++ b/packages/stylo_taffy/src/wrapper.rs @@ -23,6 +23,12 @@ bitflags! { pub struct StyleFlags: u8 { /// Whether the node is a replaced element (e.g. an image or form control) const IS_REPLACED = 1 << 0; + /// Whether an inline span laid out by the node (an inline root) is a containing block + /// for `position: absolute` boxes + const SPAN_ABSOLUTE_CB = 1 << 1; + /// Whether an inline span laid out by the node (an inline root) is a containing block + /// for `position: fixed` boxes + const SPAN_FIXED_CB = 1 << 2; } } @@ -212,7 +218,10 @@ impl> taffy::CoreStyle for TaffyStyloStyle #[inline] fn is_containing_block(&self) -> taffy::ContainingBlockClaims { - convert::containing_block_claims(&self.style) + let mut claims = convert::containing_block_claims(&self.style); + claims.absolute |= self.flags.contains(StyleFlags::SPAN_ABSOLUTE_CB); + claims.fixed |= self.flags.contains(StyleFlags::SPAN_FIXED_CB); + claims } #[inline] From ca298d71c290d028f44aacf755941e9edd909c89 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Fri, 9 Oct 2026 20:08:11 +0000 Subject: [PATCH 2/5] Add tests for abspos boxes with an inline span containing block --- .../abspos_inline_span_containing_block.rs | 176 ++++++++++++++++++ tests/blitz-tests/tests/all.rs | 1 + 2 files changed, 177 insertions(+) create mode 100644 tests/blitz-tests/tests/abspos_inline_span_containing_block.rs diff --git a/tests/blitz-tests/tests/abspos_inline_span_containing_block.rs b/tests/blitz-tests/tests/abspos_inline_span_containing_block.rs new file mode 100644 index 0000000000..4927d1681d --- /dev/null +++ b/tests/blitz-tests/tests/abspos_inline_span_containing_block.rs @@ -0,0 +1,176 @@ +//! Out-of-flow boxes whose containing block is an inline span (which has no layout node) are +//! positioned against the padding box of the span's inline root, rather than against a further +//! ancestor. The span's own geometry is not resolved. + +use blitz_test_harness::{Harness, Rect}; + +const STYLE: &str = r#" + body { margin: 0; font-size: 16px; line-height: 20px; } + #root { margin: 30px 0 0 40px; width: 400px; height: 60px; padding: 10px; border: 5px solid; } + .rel { position: relative; } + .fill { top: 0; left: 0; right: 0; bottom: 0; } +"#; + +/// The padding box of `#root` +const ROOT_PADDING_BOX: Rect = Rect { + x: 45.0, + y: 35.0, + width: 420.0, + height: 80.0, +}; + +fn layout_rect(body: &str, selector: &str) -> Rect { + let html = format!("
{body}
"); + Harness::from_html(&html).layout_rect(selector) +} + +#[track_caller] +fn assert_rect_eq(actual: Rect, expected: Rect) { + let close = |a: f32, b: f32| (a - b).abs() < 1.0; + assert!( + close(actual.x, expected.x) + && close(actual.y, expected.y) + && close(actual.width, expected.width) + && close(actual.height, expected.height), + "expected {expected:?}, got {actual:?}" + ); +} + +#[test] +fn child_of_span() { + let rect = layout_rect( + "before text
", + "#abs", + ); + assert_rect_eq(rect, ROOT_PADDING_BOX); +} + +#[test] +fn child_of_nested_span() { + let rect = layout_rect( + "before text
", + "#abs", + ); + assert_rect_eq(rect, ROOT_PADDING_BOX); +} + +#[test] +fn descendant_of_inline_box_in_span() { + let rect = layout_rect( + "before

text

\ +
", + "#abs", + ); + assert_rect_eq(rect, ROOT_PADDING_BOX); +} + +#[test] +fn auto_insets_use_static_position() { + let in_span = layout_rect( + "before textabs", + "#abs", + ); + let in_root = layout_rect( + "before textabs", + "#abs", + ); + assert!( + in_root.x > 55.0, + "not at the start of the line: {in_root:?}" + ); + assert_rect_eq(in_span, in_root); +} + +#[test] +fn span_which_is_not_a_containing_block() { + let rect = layout_rect( + "before text
", + "#abs", + ); + assert_eq!((rect.x, rect.y), (0.0, 0.0)); + assert!(rect.width > ROOT_PADDING_BOX.width); +} + +#[test] +fn fixed_child_of_relative_span() { + let rect = layout_rect( + "before text
", + "#fixed", + ); + assert_eq!((rect.x, rect.y), (0.0, 0.0)); + assert!(rect.width > ROOT_PADDING_BOX.width); +} + +#[test] +fn fixed_child_of_filtered_span() { + let rect = layout_rect( + "before text
", + "#fixed", + ); + assert_rect_eq(rect, ROOT_PADDING_BOX); +} + +#[test] +fn fixed_descendant_of_absolute_child_of_relative_span() { + let rect = layout_rect( + "before text
\ +
", + "#fixed", + ); + assert_eq!((rect.x, rect.y), (0.0, 0.0)); + assert!(rect.width > ROOT_PADDING_BOX.width); +} + +#[test] +fn fixed_child_of_transformed_span() { + // Transforms and containment do not apply to non-atomic inline boxes + let rect = layout_rect( + "before text\ +
", + "#fixed", + ); + assert_eq!((rect.x, rect.y), (0.0, 0.0)); + assert!(rect.width > ROOT_PADDING_BOX.width); +} + +#[test] +fn fixed_descendant_unclaimed_by_span_is_claimed_by_root() { + let html = format!( + "
before \ + text
\ +
" + ); + let rect = Harness::from_html(&html).layout_rect("#fixed"); + assert_rect_eq(rect, ROOT_PADDING_BOX); +} + +/// The root's claims are the union of its spans', so a box beside a containing-block span is +/// claimed by the root too (imprecise: its containing block is really a further ancestor) +#[test] +fn sibling_of_span_with_inline_box_is_claimed_by_root() { + let rect = layout_rect( + "box\ +
", + "#abs", + ); + assert_rect_eq(rect, ROOT_PADDING_BOX); +} + +/// A containing-block span with no inline box below it does not widen the root's claims +#[test] +fn sibling_of_text_only_span_is_not_claimed_by_root() { + let rect = layout_rect( + "text\ +
", + "#abs", + ); + assert_rect_eq( + rect, + Rect { + x: 0.0, + y: 0.0, + width: 800.0, + height: 600.0, + }, + ); +} diff --git a/tests/blitz-tests/tests/all.rs b/tests/blitz-tests/tests/all.rs index 079f69eb50..a3c2207095 100644 --- a/tests/blitz-tests/tests/all.rs +++ b/tests/blitz-tests/tests/all.rs @@ -1,6 +1,7 @@ //! All integration tests are compiled into this single test binary because linking //! one binary per file dominates build time. New test files must be added here. +mod abspos_inline_span_containing_block; mod accessibility_hidden; mod accessibility_roles; mod animations; From e5d5eef4a9403c7880043d5396ee8160324b7331 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Fri, 9 Oct 2026 20:10:05 +0000 Subject: [PATCH 3/5] Rebuild the box tree when an inline span's containing block claims change --- packages/blitz-dom/src/layout/damage.rs | 9 ++ .../abspos_inline_span_containing_block.rs | 85 +++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/packages/blitz-dom/src/layout/damage.rs b/packages/blitz-dom/src/layout/damage.rs index b1ae9f0b7d..0ab66e3130 100644 --- a/packages/blitz-dom/src/layout/damage.rs +++ b/packages/blitz-dom/src/layout/damage.rs @@ -284,6 +284,15 @@ pub(crate) fn compute_layout_damage(old: &ComputedValues, new: &ComputedValues) return true; } + // An inline root caches the containing block claims of its inline spans + if new_box.display.outside() == DisplayOutside::Inline + && new_box.display.inside() == DisplayInside::Flow + && stylo_taffy::convert::inline_containing_block_claims(old) + != stylo_taffy::convert::inline_containing_block_claims(new) + { + return true; + } + if new_box.display.outside() == DisplayOutside::Block && new_box.display.inside() == DisplayInside::Flow { diff --git a/tests/blitz-tests/tests/abspos_inline_span_containing_block.rs b/tests/blitz-tests/tests/abspos_inline_span_containing_block.rs index 4927d1681d..ca18962939 100644 --- a/tests/blitz-tests/tests/abspos_inline_span_containing_block.rs +++ b/tests/blitz-tests/tests/abspos_inline_span_containing_block.rs @@ -174,3 +174,88 @@ fn sibling_of_text_only_span_is_not_claimed_by_root() { }, ); } + +/// Adding or removing a containing-block-establishing property on a span updates the root's +/// claims +fn assert_span_cb_toggle(cb_prop: &str, position: &str) { + use markup5ever::{QualName, local_name, ns}; + + let html = format!( + "
before text\ +
" + ); + let mut harness = Harness::from_html(&html); + let span = harness.node("#span"); + let viewport = harness.layout_rect("#abs"); + assert!( + viewport.width > ROOT_PADDING_BOX.width, + "initially unclaimed" + ); + + let mut set_style = |harness: &mut Harness, style: &str| { + let name = QualName::new(None, ns!(), local_name!("style")); + harness.base_mut().mutate().set_attribute(span, name, style); + harness.pump(); + }; + + set_style(&mut harness, cb_prop); + assert_rect_eq(harness.layout_rect("#abs"), ROOT_PADDING_BOX); + + set_style(&mut harness, ""); + assert_rect_eq(harness.layout_rect("#abs"), viewport); +} + +#[test] +fn dynamic_position_on_span() { + assert_span_cb_toggle("position: relative", "absolute"); +} + +#[test] +fn dynamic_filter_on_span() { + assert_span_cb_toggle("filter: blur(1px)", "fixed"); +} + +#[test] +fn dynamic_will_change_on_span() { + assert_span_cb_toggle("will-change: filter", "fixed"); + assert_span_cb_toggle("will-change: position", "absolute"); +} + +/// As `assert_span_cb_toggle`, but through `:hover`, which damages the span through its style +/// change alone +fn assert_span_cb_hover_toggle(cb_prop: &str, position: &str) { + let html = format!( + "
text\ +
" + ); + let mut harness = Harness::from_html(&html); + let viewport = harness.layout_rect("#abs"); + assert!( + viewport.width > ROOT_PADDING_BOX.width, + "initially unclaimed" + ); + + harness.base_mut().set_hover_to(60.0, 50.0); + harness.pump(); + assert_rect_eq(harness.layout_rect("#abs"), ROOT_PADDING_BOX); + + harness.base_mut().set_hover_to(700.0, 500.0); + harness.pump(); + assert_rect_eq(harness.layout_rect("#abs"), viewport); +} + +#[test] +fn hover_position_on_span() { + assert_span_cb_hover_toggle("position: relative", "absolute"); +} + +#[test] +fn hover_filter_on_span() { + assert_span_cb_hover_toggle("filter: blur(1px)", "fixed"); +} + +#[test] +fn hover_will_change_on_span() { + assert_span_cb_hover_toggle("will-change: filter", "fixed"); + assert_span_cb_hover_toggle("will-change: position", "absolute"); +} From cd975186329e0783f172c6b0126be889e3ad5038 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Fri, 9 Oct 2026 20:23:54 +0000 Subject: [PATCH 4/5] Compute inline span containing block claims at layout time --- packages/blitz-dom/src/layout/construct.rs | 36 ------------------ packages/blitz-dom/src/layout/damage.rs | 9 ----- packages/blitz-dom/src/layout/inline.rs | 44 ++++++++++++++++++++++ packages/blitz-dom/src/node/text.rs | 1 + 4 files changed, 45 insertions(+), 45 deletions(-) diff --git a/packages/blitz-dom/src/layout/construct.rs b/packages/blitz-dom/src/layout/construct.rs index 735f62fac4..63a1657c5f 100644 --- a/packages/blitz-dom/src/layout/construct.rs +++ b/packages/blitz-dom/src/layout/construct.rs @@ -30,7 +30,6 @@ use crate::{ qual_name, stylo_to_parley, traversal::{iter_children, iter_children_and_pseudos}, }; -use stylo_taffy::StyleFlags; use super::{ damage::ALL_DAMAGE, @@ -1099,8 +1098,6 @@ pub(crate) fn build_inline_layout_into( let mut text_transformer = TextTransformer::default(); text_transformer.word_break(&builder); - let mut span_cb_flags = StyleFlags::empty(); - if let Some(before_id) = root_node.before() { build_inline_layout_recursive( &mut builder, @@ -1108,8 +1105,6 @@ pub(crate) fn build_inline_layout_into( nodes, before_id, &text_transform, - StyleFlags::empty(), - &mut span_cb_flags, ); } for child_id in root_node.children.iter().copied() { @@ -1119,8 +1114,6 @@ pub(crate) fn build_inline_layout_into( nodes, child_id, &text_transform, - StyleFlags::empty(), - &mut span_cb_flags, ); } if let Some(after_id) = root_node.after() { @@ -1130,13 +1123,10 @@ pub(crate) fn build_inline_layout_into( nodes, after_id, &text_transform, - StyleFlags::empty(), - &mut span_cb_flags, ); } text_layout.text = builder.build_into(&mut text_layout.layout); - text_layout.span_cb_flags = span_cb_flags; return; fn build_inline_layout_recursive( @@ -1145,10 +1135,6 @@ pub(crate) fn build_inline_layout_into( nodes: &crate::NodeTree, node_id: NodeId, parent_text_transform: &CaseTransform, - // The containing block flags of the inline spans between the node and the inline root - span_cb_flags: StyleFlags, - // Accumulates `span_cb_flags` of every inline box - root_cb_flags: &mut StyleFlags, ) { let node = &nodes[node_id]; @@ -1211,8 +1197,6 @@ pub(crate) fn build_inline_layout_into( nodes, child_id, &text_transform, - span_cb_flags, - root_cb_flags, ); } builder.pop_style_span(); @@ -1224,7 +1208,6 @@ pub(crate) fn build_inline_layout_into( if box_kind == InlineBoxKind::InFlow { text_transformer.word_break(builder); } - *root_cb_flags |= span_cb_flags; builder.push_inline_box(InlineBox { id: node_id.as_u64(), kind: box_kind, @@ -1262,18 +1245,6 @@ pub(crate) fn build_inline_layout_into( builder.push_style_span(style); - let mut span_cb_flags = span_cb_flags; - if let Some(style) = node.primary_styles() { - let claims = - stylo_taffy::convert::inline_containing_block_claims(&style); - if claims.absolute { - span_cb_flags |= StyleFlags::SPAN_ABSOLUTE_CB; - } - if claims.fixed { - span_cb_flags |= StyleFlags::SPAN_FIXED_CB; - } - } - if let Some(before_id) = node.before() { build_inline_layout_recursive( builder, @@ -1281,8 +1252,6 @@ pub(crate) fn build_inline_layout_into( nodes, before_id, &text_transform, - span_cb_flags, - root_cb_flags, ); } @@ -1293,8 +1262,6 @@ pub(crate) fn build_inline_layout_into( nodes, child_id, &text_transform, - span_cb_flags, - root_cb_flags, ); } if let Some(after_id) = node.after() { @@ -1304,8 +1271,6 @@ pub(crate) fn build_inline_layout_into( nodes, after_id, &text_transform, - span_cb_flags, - root_cb_flags, ); } @@ -1317,7 +1282,6 @@ pub(crate) fn build_inline_layout_into( if box_kind == InlineBoxKind::InFlow { text_transformer.word_break(builder); } - *root_cb_flags |= span_cb_flags; builder.push_inline_box(InlineBox { id: node_id.as_u64(), kind: box_kind, diff --git a/packages/blitz-dom/src/layout/damage.rs b/packages/blitz-dom/src/layout/damage.rs index 0ab66e3130..b1ae9f0b7d 100644 --- a/packages/blitz-dom/src/layout/damage.rs +++ b/packages/blitz-dom/src/layout/damage.rs @@ -284,15 +284,6 @@ pub(crate) fn compute_layout_damage(old: &ComputedValues, new: &ComputedValues) return true; } - // An inline root caches the containing block claims of its inline spans - if new_box.display.outside() == DisplayOutside::Inline - && new_box.display.inside() == DisplayInside::Flow - && stylo_taffy::convert::inline_containing_block_claims(old) - != stylo_taffy::convert::inline_containing_block_claims(new) - { - return true; - } - if new_box.display.outside() == DisplayOutside::Block && new_box.display.inside() == DisplayInside::Flow { diff --git a/packages/blitz-dom/src/layout/inline.rs b/packages/blitz-dom/src/layout/inline.rs index e5d0118ad5..0ffeafb033 100644 --- a/packages/blitz-dom/src/layout/inline.rs +++ b/packages/blitz-dom/src/layout/inline.rs @@ -18,6 +18,8 @@ use parley::YieldData; #[cfg(feature = "floats")] use taffy::{BlockItemStyle as _, Clear, Float, prelude::TaffyMaxContent}; +use stylo_taffy::StyleFlags; + use super::resolve_calc_value; use crate::layout::LayoutPassState; use crate::stylo_to_parley; @@ -192,6 +194,39 @@ impl LayoutPassState<'_> { } } + /// The out-of-flow positions for which an inline span between the inline box `box_id` and its + /// inline root `root_id` is a containing block (as `SPAN_*_CB` flags) + fn inline_span_cb_flags(&self, root_id: NodeId, box_id: NodeId) -> StyleFlags { + let root = &self.nodes[root_id]; + // The spans of an anonymous inline root are descendants of its parent + let root_parent = root.is_anonymous().then_some(root.parent).flatten(); + + let mut flags = StyleFlags::empty(); + let mut current = self.nodes[box_id].parent; + while let Some(id) = current { + if id == root_id || Some(id) == root_parent { + return flags; + } + let ancestor = &self.nodes[id]; + if let Some(style) = ancestor.primary_styles() { + let display = style.clone_display(); + if display.outside() == DisplayOutside::Inline + && display.inside() == DisplayInside::Flow + { + let claims = stylo_taffy::convert::inline_containing_block_claims(&style); + if claims.absolute { + flags |= StyleFlags::SPAN_ABSOLUTE_CB; + } + if claims.fixed { + flags |= StyleFlags::SPAN_FIXED_CB; + } + } + } + current = ancestor.parent; + } + StyleFlags::empty() + } + fn compute_inline_layout_inner( &mut self, node_id: NodeId, @@ -870,11 +905,16 @@ impl LayoutPassState<'_> { // Store sizes and positions of inline boxes let mut ibox_order: u32 = 0; + let mut span_cb_flags = StyleFlags::empty(); for line in inline_layout.layout.lines() { for item in line.items() { if let parley::layout::PositionedLayoutItem::InlineBox(ibox) = item { let order = ibox_order; ibox_order += 1; + if inputs.run_mode == RunMode::PerformLayout { + span_cb_flags |= + self.inline_span_cb_flags(node_id, NodeId::from_u64(ibox.id)); + } let node = &self.nodes[NodeId::from_u64(ibox.id)]; let style = self.child_layout_style(node); let padding = style @@ -1059,6 +1099,10 @@ impl LayoutPassState<'_> { .flatten() .map(line_baseline); + if inputs.run_mode == RunMode::PerformLayout { + inline_layout.span_cb_flags = span_cb_flags; + } + // Put layout back self.nodes[node_id] .data diff --git a/packages/blitz-dom/src/node/text.rs b/packages/blitz-dom/src/node/text.rs index cc4127edb8..61906af145 100644 --- a/packages/blitz-dom/src/node/text.rs +++ b/packages/blitz-dom/src/node/text.rs @@ -31,6 +31,7 @@ pub struct TextLayout { pub block_offset: f32, /// The out-of-flow positions for which some inline span with an inline box below it is a /// containing block (as `SPAN_*_CB` flags). The inline root claims those boxes in its place. + /// Set by each `RunMode::PerformLayout` run of inline layout. pub span_cb_flags: stylo_taffy::StyleFlags, } From 7aa91a36043339dc3bbe2c59e18fd7aa63ee5985 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Fri, 9 Oct 2026 21:10:03 +0000 Subject: [PATCH 5/5] Remove tests of the approximate inline span containing block behaviour --- .../abspos_inline_span_containing_block.rs | 261 ------------------ tests/blitz-tests/tests/all.rs | 1 - 2 files changed, 262 deletions(-) delete mode 100644 tests/blitz-tests/tests/abspos_inline_span_containing_block.rs diff --git a/tests/blitz-tests/tests/abspos_inline_span_containing_block.rs b/tests/blitz-tests/tests/abspos_inline_span_containing_block.rs deleted file mode 100644 index ca18962939..0000000000 --- a/tests/blitz-tests/tests/abspos_inline_span_containing_block.rs +++ /dev/null @@ -1,261 +0,0 @@ -//! Out-of-flow boxes whose containing block is an inline span (which has no layout node) are -//! positioned against the padding box of the span's inline root, rather than against a further -//! ancestor. The span's own geometry is not resolved. - -use blitz_test_harness::{Harness, Rect}; - -const STYLE: &str = r#" - body { margin: 0; font-size: 16px; line-height: 20px; } - #root { margin: 30px 0 0 40px; width: 400px; height: 60px; padding: 10px; border: 5px solid; } - .rel { position: relative; } - .fill { top: 0; left: 0; right: 0; bottom: 0; } -"#; - -/// The padding box of `#root` -const ROOT_PADDING_BOX: Rect = Rect { - x: 45.0, - y: 35.0, - width: 420.0, - height: 80.0, -}; - -fn layout_rect(body: &str, selector: &str) -> Rect { - let html = format!("
{body}
"); - Harness::from_html(&html).layout_rect(selector) -} - -#[track_caller] -fn assert_rect_eq(actual: Rect, expected: Rect) { - let close = |a: f32, b: f32| (a - b).abs() < 1.0; - assert!( - close(actual.x, expected.x) - && close(actual.y, expected.y) - && close(actual.width, expected.width) - && close(actual.height, expected.height), - "expected {expected:?}, got {actual:?}" - ); -} - -#[test] -fn child_of_span() { - let rect = layout_rect( - "before text
", - "#abs", - ); - assert_rect_eq(rect, ROOT_PADDING_BOX); -} - -#[test] -fn child_of_nested_span() { - let rect = layout_rect( - "before text
", - "#abs", - ); - assert_rect_eq(rect, ROOT_PADDING_BOX); -} - -#[test] -fn descendant_of_inline_box_in_span() { - let rect = layout_rect( - "before

text

\ -
", - "#abs", - ); - assert_rect_eq(rect, ROOT_PADDING_BOX); -} - -#[test] -fn auto_insets_use_static_position() { - let in_span = layout_rect( - "before textabs", - "#abs", - ); - let in_root = layout_rect( - "before textabs", - "#abs", - ); - assert!( - in_root.x > 55.0, - "not at the start of the line: {in_root:?}" - ); - assert_rect_eq(in_span, in_root); -} - -#[test] -fn span_which_is_not_a_containing_block() { - let rect = layout_rect( - "before text
", - "#abs", - ); - assert_eq!((rect.x, rect.y), (0.0, 0.0)); - assert!(rect.width > ROOT_PADDING_BOX.width); -} - -#[test] -fn fixed_child_of_relative_span() { - let rect = layout_rect( - "before text
", - "#fixed", - ); - assert_eq!((rect.x, rect.y), (0.0, 0.0)); - assert!(rect.width > ROOT_PADDING_BOX.width); -} - -#[test] -fn fixed_child_of_filtered_span() { - let rect = layout_rect( - "before text
", - "#fixed", - ); - assert_rect_eq(rect, ROOT_PADDING_BOX); -} - -#[test] -fn fixed_descendant_of_absolute_child_of_relative_span() { - let rect = layout_rect( - "before text
\ -
", - "#fixed", - ); - assert_eq!((rect.x, rect.y), (0.0, 0.0)); - assert!(rect.width > ROOT_PADDING_BOX.width); -} - -#[test] -fn fixed_child_of_transformed_span() { - // Transforms and containment do not apply to non-atomic inline boxes - let rect = layout_rect( - "before text\ -
", - "#fixed", - ); - assert_eq!((rect.x, rect.y), (0.0, 0.0)); - assert!(rect.width > ROOT_PADDING_BOX.width); -} - -#[test] -fn fixed_descendant_unclaimed_by_span_is_claimed_by_root() { - let html = format!( - "
before \ - text
\ -
" - ); - let rect = Harness::from_html(&html).layout_rect("#fixed"); - assert_rect_eq(rect, ROOT_PADDING_BOX); -} - -/// The root's claims are the union of its spans', so a box beside a containing-block span is -/// claimed by the root too (imprecise: its containing block is really a further ancestor) -#[test] -fn sibling_of_span_with_inline_box_is_claimed_by_root() { - let rect = layout_rect( - "box\ -
", - "#abs", - ); - assert_rect_eq(rect, ROOT_PADDING_BOX); -} - -/// A containing-block span with no inline box below it does not widen the root's claims -#[test] -fn sibling_of_text_only_span_is_not_claimed_by_root() { - let rect = layout_rect( - "text\ -
", - "#abs", - ); - assert_rect_eq( - rect, - Rect { - x: 0.0, - y: 0.0, - width: 800.0, - height: 600.0, - }, - ); -} - -/// Adding or removing a containing-block-establishing property on a span updates the root's -/// claims -fn assert_span_cb_toggle(cb_prop: &str, position: &str) { - use markup5ever::{QualName, local_name, ns}; - - let html = format!( - "
before text\ -
" - ); - let mut harness = Harness::from_html(&html); - let span = harness.node("#span"); - let viewport = harness.layout_rect("#abs"); - assert!( - viewport.width > ROOT_PADDING_BOX.width, - "initially unclaimed" - ); - - let mut set_style = |harness: &mut Harness, style: &str| { - let name = QualName::new(None, ns!(), local_name!("style")); - harness.base_mut().mutate().set_attribute(span, name, style); - harness.pump(); - }; - - set_style(&mut harness, cb_prop); - assert_rect_eq(harness.layout_rect("#abs"), ROOT_PADDING_BOX); - - set_style(&mut harness, ""); - assert_rect_eq(harness.layout_rect("#abs"), viewport); -} - -#[test] -fn dynamic_position_on_span() { - assert_span_cb_toggle("position: relative", "absolute"); -} - -#[test] -fn dynamic_filter_on_span() { - assert_span_cb_toggle("filter: blur(1px)", "fixed"); -} - -#[test] -fn dynamic_will_change_on_span() { - assert_span_cb_toggle("will-change: filter", "fixed"); - assert_span_cb_toggle("will-change: position", "absolute"); -} - -/// As `assert_span_cb_toggle`, but through `:hover`, which damages the span through its style -/// change alone -fn assert_span_cb_hover_toggle(cb_prop: &str, position: &str) { - let html = format!( - "
text\ -
" - ); - let mut harness = Harness::from_html(&html); - let viewport = harness.layout_rect("#abs"); - assert!( - viewport.width > ROOT_PADDING_BOX.width, - "initially unclaimed" - ); - - harness.base_mut().set_hover_to(60.0, 50.0); - harness.pump(); - assert_rect_eq(harness.layout_rect("#abs"), ROOT_PADDING_BOX); - - harness.base_mut().set_hover_to(700.0, 500.0); - harness.pump(); - assert_rect_eq(harness.layout_rect("#abs"), viewport); -} - -#[test] -fn hover_position_on_span() { - assert_span_cb_hover_toggle("position: relative", "absolute"); -} - -#[test] -fn hover_filter_on_span() { - assert_span_cb_hover_toggle("filter: blur(1px)", "fixed"); -} - -#[test] -fn hover_will_change_on_span() { - assert_span_cb_hover_toggle("will-change: filter", "fixed"); - assert_span_cb_hover_toggle("will-change: position", "absolute"); -} diff --git a/tests/blitz-tests/tests/all.rs b/tests/blitz-tests/tests/all.rs index a3c2207095..079f69eb50 100644 --- a/tests/blitz-tests/tests/all.rs +++ b/tests/blitz-tests/tests/all.rs @@ -1,7 +1,6 @@ //! All integration tests are compiled into this single test binary because linking //! one binary per file dominates build time. New test files must be added here. -mod abspos_inline_span_containing_block; mod accessibility_hidden; mod accessibility_roles; mod animations;