diff --git a/crates/obscura-render/tests/layout_test.rs b/crates/obscura-render/tests/layout_test.rs index 266f96d..0dc70f5 100644 --- a/crates/obscura-render/tests/layout_test.rs +++ b/crates/obscura-render/tests/layout_test.rs @@ -777,6 +777,90 @@ fn ratio_only_inline_svg_sizes_inside_an_auto_grid_row() { ); } +/// Chromium 150 includes collapsed descendant margins in a grid item's +/// intrinsic block-size. The nested 64px margin reaches through `section`, +/// making the first item 200px tall; the second row therefore begins after +/// that item and the 20px row gap, rather than overlapping the descendant. +#[test] +fn grid_item_intrinsic_height_includes_collapsed_descendant_margins() { + let tree = parse_html( + r#" +
+
+
+
+
+
+
+ "#, + ); + let layout = layout_dom(&tree, (800.0, 600.0)); + let rect = |id| layout.rects[&tree.get_element_by_id(id).unwrap()]; + let outer = rect("outer"); + let item = rect("item"); + let deep = rect("deep"); + let after = rect("after"); + + assert!( + (item.height - 200.0).abs() < 0.01 && (deep.y + deep.height - 200.0).abs() < 0.01, + "the grid item must contain the collapsed nested margin: item={item:?} deep={deep:?}" + ); + assert!( + (after.y - 220.0).abs() < 0.01 && (outer.height - 230.0).abs() < 0.01, + "later grid rows must not overlap overflowing intrinsic content: outer={outer:?} after={after:?}" + ); +} + +/// Collapsible margin sets retain their largest positive and most-negative +/// members separately. A negative nested margin therefore shortens the grid +/// item's intrinsic contribution and overlaps the preceding block in exactly +/// the same way during measurement and final layout. +#[test] +fn grid_item_intrinsic_height_keeps_negative_collapsed_margin() { + let tree = parse_html( + r#" +
+
+
+
+
+
+
+ "#, + ); + let layout = layout_dom(&tree, (800.0, 600.0)); + let rect = |id| layout.rects[&tree.get_element_by_id(id).unwrap()]; + let outer = rect("outer"); + let item = rect("item"); + let deep = rect("deep"); + let after = rect("after"); + + assert!( + (item.height - 112.0).abs() < 0.01 + && (deep.y - 12.0).abs() < 0.01 + && (deep.height - 100.0).abs() < 0.01, + "negative collapsed margin geometry must survive measurement: item={item:?} deep={deep:?}" + ); + assert!( + (after.y - 132.0).abs() < 0.01 && (outer.height - 142.0).abs() < 0.01, + "the following grid row must use the shortened contribution: outer={outer:?} after={after:?}" + ); +} + #[test] fn inline_block_flex_items_keep_block_inner_flow() { let tree = parse_html(include_str!("../../../render-repros/inline-block-flex-items.html")); diff --git a/vendor/taffy/src/compute/block.rs b/vendor/taffy/src/compute/block.rs index 17fee8d..fcd9b0f 100644 --- a/vendor/taffy/src/compute/block.rs +++ b/vendor/taffy/src/compute/block.rs @@ -534,9 +534,35 @@ fn compute_inner( } } + // Margin-collapsing metadata is part of a block's intrinsic contribution, + // not only its final child placement. Grid and flex sizing can measure a + // block here and use its exposed start/end margin sets to size an ancestor + // item. Dropping them from ComputeSize makes the ancestor track shorter + // than the block's eventual descendants. + let all_in_flow_children_can_be_collapsed_through = + items.iter().all(|item| item.position == Position::Absolute || item.can_be_collapsed_through); + let can_be_collapsed_through = + !has_styles_preventing_being_collapsed_through && all_in_flow_children_can_be_collapsed_through; + let top_margin = if own_margins_collapse_with_children.start { + first_child_top_margin_set + } else { + let margin_top = raw_margin.top.resolve_or_zero(parent_size.width, |val, basis| tree.calc(val, basis)); + CollapsibleMarginSet::from_margin(margin_top) + }; + let bottom_margin = if own_margins_collapse_with_children.end { + last_child_bottom_margin_set + } else { + let margin_bottom = raw_margin.bottom.resolve_or_zero(parent_size.width, |val, basis| tree.calc(val, basis)); + CollapsibleMarginSet::from_margin(margin_bottom) + }; + // Short-circuit if computing size if run_mode == RunMode::ComputeSize { - return LayoutOutput::from_outer_size(final_outer_size); + let mut output = LayoutOutput::from_outer_size(final_outer_size); + output.top_margin = top_margin; + output.bottom_margin = bottom_margin; + output.margins_can_collapse_through = can_be_collapsed_through; + return output; } // Commit deferred in-flow layouts to the tree. Floated items already wrote their own layouts. @@ -577,12 +603,6 @@ fn compute_inner( } } - // 7. Determine whether this node can be collapsed through - let all_in_flow_children_can_be_collapsed_through = - items.iter().all(|item| item.position == Position::Absolute || item.can_be_collapsed_through); - let can_be_collapsed_through = - !has_styles_preventing_being_collapsed_through && all_in_flow_children_can_be_collapsed_through; - #[cfg_attr(not(feature = "content_size"), allow(unused_variables))] let content_size = inflow_content_size.f32_max(absolute_content_size); @@ -591,19 +611,8 @@ fn compute_inner( #[cfg(feature = "content_size")] content_size, first_baselines: Point::NONE, - top_margin: if own_margins_collapse_with_children.start { - first_child_top_margin_set - } else { - let margin_top = raw_margin.top.resolve_or_zero(parent_size.width, |val, basis| tree.calc(val, basis)); - CollapsibleMarginSet::from_margin(margin_top) - }, - bottom_margin: if own_margins_collapse_with_children.end { - last_child_bottom_margin_set - } else { - let margin_bottom = - raw_margin.bottom.resolve_or_zero(parent_size.width, |val, basis| tree.calc(val, basis)); - CollapsibleMarginSet::from_margin(margin_bottom) - }, + top_margin, + bottom_margin, margins_can_collapse_through: can_be_collapsed_through, } } diff --git a/vendor/taffy/src/tree/cache.rs b/vendor/taffy/src/tree/cache.rs index 5187419..03f2010 100644 --- a/vendor/taffy/src/tree/cache.rs +++ b/vendor/taffy/src/tree/cache.rs @@ -4,7 +4,7 @@ use crate::geometry::Size; use crate::style::AvailableSpace; -use crate::tree::{LayoutInput, LayoutOutput, RunMode}; +use crate::tree::{CollapsibleMarginSet, LayoutInput, LayoutOutput, RunMode}; use crate::RequestedAxis; /// The number of cache entries for each node in the tree @@ -81,6 +81,12 @@ fn size_mixed_cache_key(kd: Size>, avs: Size) -> u64 (mixed_cache_key(kd.width, avs.width) as u64) << 32 | mixed_cache_key(kd.height, avs.height) as u64 } +#[inline(always)] +fn vertical_margin_context_key(input: &LayoutInput) -> u8 { + u8::from(input.vertical_margins_are_collapsible.start) + | (u8::from(input.vertical_margins_are_collapsible.end) << 1) +} + /// Space-optimised cache key that packs bits into as small a size as possible #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(Serialize))] @@ -132,6 +138,40 @@ pub(crate) struct CacheEntry { content: T, } +/// The block-layout metadata required by a preliminary size contribution. +/// +/// A compact entry avoids retaining full baselines/content-size while keeping +/// the collapsible margins that an ancestor grid or flex item must consume. +#[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize))] +struct MeasureOutput { + size: Size, + top_margin: CollapsibleMarginSet, + bottom_margin: CollapsibleMarginSet, + margins_can_collapse_through: bool, + vertical_margins_are_collapsible: u8, +} + +impl MeasureOutput { + fn new(input: &LayoutInput, output: LayoutOutput) -> Self { + Self { + size: output.size, + top_margin: output.top_margin, + bottom_margin: output.bottom_margin, + margins_can_collapse_through: output.margins_can_collapse_through, + vertical_margins_are_collapsible: vertical_margin_context_key(input), + } + } + + fn into_layout_output(self) -> LayoutOutput { + let mut output = LayoutOutput::from_outer_size(self.size); + output.top_margin = self.top_margin; + output.bottom_margin = self.bottom_margin; + output.margins_can_collapse_through = self.margins_can_collapse_through; + output + } +} + /// A cache for caching the results of a sizing a Grid Item or Flexbox Item #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize))] @@ -139,7 +179,7 @@ pub struct Cache { /// The cache entry for the node's final layout final_layout_entry: Option>, /// The cache entries for the node's preliminary size measurements - measure_entries: [Option>>; CACHE_SIZE], + measure_entries: [Option>; CACHE_SIZE], /// Tracks if all cache entries are empty is_empty: bool, } @@ -230,8 +270,10 @@ impl Cache { for entry in self.measure_entries.iter().flatten() { if entry.key.kd_available_space == key.kd_available_space && (entry.key.x_axis_parent_size() == key.x_axis_parent_size()) + && entry.content.vertical_margins_are_collapsible + == vertical_margin_context_key(input) { - return Some(LayoutOutput::from_outer_size(entry.content)); + return Some(entry.content.into_layout_output()); } } @@ -252,7 +294,8 @@ impl Cache { RunMode::ComputeSize => { self.is_empty = false; let cache_slot = Self::compute_cache_slot(input.known_dimensions, input.available_space); - self.measure_entries[cache_slot] = Some(CacheEntry { key, content: layout_output.size }); + self.measure_entries[cache_slot] = + Some(CacheEntry { key, content: MeasureOutput::new(input, layout_output) }); } RunMode::PerformHiddenLayout => {} }