Preserve collapsed margins in intrinsic sizing

This commit is contained in:
SGavrl committed 2026-07-30 21:33:49 +00:00
1 parent 90c64b2c31
commit 839f1c2338
3 files changed
+160 -24

No files matched your search

@@ -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#"<html><head><style>
html,body{margin:0}
#outer{display:grid;width:300px;grid-template-rows:auto auto;row-gap:20px}
#item{position:relative}
#lead{height:20px;margin-top:16px}
#deep{height:100px;margin-top:64px}
#after{height:10px}
</style></head><body>
<div id="outer">
<div id="item">
<div id="lead"></div>
<section><div id="deep"></div></section>
</div>
<div id="after"></div>
</div>
</body></html>"#,
);
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#"<html><head><style>
html,body{margin:0}
#outer{display:grid;width:300px;grid-template-rows:auto auto;row-gap:20px}
#item{position:relative}
#lead{height:20px;margin-top:16px}
#deep{height:100px;margin-top:-24px}
#after{height:10px}
</style></head><body>
<div id="outer">
<div id="item">
<div id="lead"></div>
<section><div id="deep"></div></section>
</div>
<div id="after"></div>
</div>
</body></html>"#,
);
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] #[test]
fn inline_block_flex_items_keep_block_inner_flow() { fn inline_block_flex_items_keep_block_inner_flow() {
let tree = parse_html(include_str!("../../../render-repros/inline-block-flex-items.html")); let tree = parse_html(include_str!("../../../render-repros/inline-block-flex-items.html"));
+29 -20
View File
@@ -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 // Short-circuit if computing size
if run_mode == RunMode::ComputeSize { 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. // 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))] #[cfg_attr(not(feature = "content_size"), allow(unused_variables))]
let content_size = inflow_content_size.f32_max(absolute_content_size); let content_size = inflow_content_size.f32_max(absolute_content_size);
@@ -591,19 +611,8 @@ fn compute_inner(
#[cfg(feature = "content_size")] #[cfg(feature = "content_size")]
content_size, content_size,
first_baselines: Point::NONE, first_baselines: Point::NONE,
top_margin: if own_margins_collapse_with_children.start { top_margin,
first_child_top_margin_set bottom_margin,
} 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)
},
margins_can_collapse_through: can_be_collapsed_through, margins_can_collapse_through: can_be_collapsed_through,
} }
} }
+47 -4
View File
@@ -4,7 +4,7 @@
use crate::geometry::Size; use crate::geometry::Size;
use crate::style::AvailableSpace; use crate::style::AvailableSpace;
use crate::tree::{LayoutInput, LayoutOutput, RunMode}; use crate::tree::{CollapsibleMarginSet, LayoutInput, LayoutOutput, RunMode};
use crate::RequestedAxis; use crate::RequestedAxis;
/// The number of cache entries for each node in the tree /// The number of cache entries for each node in the tree
@@ -81,6 +81,12 @@ fn size_mixed_cache_key(kd: Size<Option<f32>>, avs: Size<AvailableSpace>) -> u64
(mixed_cache_key(kd.width, avs.width) as u64) << 32 | mixed_cache_key(kd.height, avs.height) as 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 /// Space-optimised cache key that packs bits into as small a size as possible
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize))] #[cfg_attr(feature = "serde", derive(Serialize))]
@@ -132,6 +138,40 @@ pub(crate) struct CacheEntry<T> {
content: T, 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<f32>,
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 /// A cache for caching the results of a sizing a Grid Item or Flexbox Item
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize))] #[cfg_attr(feature = "serde", derive(Serialize))]
@@ -139,7 +179,7 @@ pub struct Cache {
/// The cache entry for the node's final layout /// The cache entry for the node's final layout
final_layout_entry: Option<CacheEntry<LayoutOutput>>, final_layout_entry: Option<CacheEntry<LayoutOutput>>,
/// The cache entries for the node's preliminary size measurements /// The cache entries for the node's preliminary size measurements
measure_entries: [Option<CacheEntry<Size<f32>>>; CACHE_SIZE], measure_entries: [Option<CacheEntry<MeasureOutput>>; CACHE_SIZE],
/// Tracks if all cache entries are empty /// Tracks if all cache entries are empty
is_empty: bool, is_empty: bool,
} }
@@ -230,8 +270,10 @@ impl Cache {
for entry in self.measure_entries.iter().flatten() { for entry in self.measure_entries.iter().flatten() {
if entry.key.kd_available_space == key.kd_available_space if entry.key.kd_available_space == key.kd_available_space
&& (entry.key.x_axis_parent_size() == key.x_axis_parent_size()) && (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 => { RunMode::ComputeSize => {
self.is_empty = false; self.is_empty = false;
let cache_slot = Self::compute_cache_slot(input.known_dimensions, input.available_space); 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 => {} RunMode::PerformHiddenLayout => {}
} }