Preserve collapsed margins in intrinsic sizing
This commit is contained in:
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]
|
||||
fn inline_block_flex_items_keep_block_inner_flow() {
|
||||
let tree = parse_html(include_str!("../../../render-repros/inline-block-flex-items.html"));
|
||||
|
||||
Vendored
+29
-20
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+47
-4
@@ -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<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
|
||||
}
|
||||
|
||||
#[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<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
|
||||
#[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<CacheEntry<LayoutOutput>>,
|
||||
/// 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
|
||||
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 => {}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user