Support safe column subgrid propagation
This commit is contained in:
1 parent
661212e6aa
commit
90c64b2c31
4 files changed
+566
-1
No files matched your search
@@ -2378,6 +2378,30 @@ pub(crate) fn layout_dom_with_web_fonts(
|
||||
&mut measure,
|
||||
);
|
||||
}
|
||||
if apply_full_span_column_subgrids(
|
||||
tree,
|
||||
&mut taffy_tree,
|
||||
&id_map,
|
||||
&styles,
|
||||
|tree, node| {
|
||||
tree.compute_layout_with_measure(
|
||||
node,
|
||||
taffy::Size {
|
||||
width: taffy::AvailableSpace::MaxContent,
|
||||
height: taffy::AvailableSpace::MaxContent,
|
||||
},
|
||||
&mut measure,
|
||||
)
|
||||
.ok()?;
|
||||
tree.layout(node).ok().map(|layout| layout.size.width)
|
||||
},
|
||||
) {
|
||||
let _ = taffy_tree.compute_layout_with_measure(
|
||||
taffy_root,
|
||||
available,
|
||||
&mut measure,
|
||||
);
|
||||
}
|
||||
if apply_table_cell_block_alignment(
|
||||
tree,
|
||||
&mut taffy_tree,
|
||||
@@ -2421,6 +2445,25 @@ pub(crate) fn layout_dom_with_web_fonts(
|
||||
) {
|
||||
let _ = taffy_tree.compute_layout(taffy_root, available);
|
||||
}
|
||||
if apply_full_span_column_subgrids(
|
||||
tree,
|
||||
&mut taffy_tree,
|
||||
&id_map,
|
||||
&styles,
|
||||
|tree, node| {
|
||||
tree.compute_layout(
|
||||
node,
|
||||
taffy::Size {
|
||||
width: taffy::AvailableSpace::MaxContent,
|
||||
height: taffy::AvailableSpace::MaxContent,
|
||||
},
|
||||
)
|
||||
.ok()?;
|
||||
tree.layout(node).ok().map(|layout| layout.size.width)
|
||||
},
|
||||
) {
|
||||
let _ = taffy_tree.compute_layout(taffy_root, available);
|
||||
}
|
||||
if apply_table_cell_block_alignment(
|
||||
tree,
|
||||
&mut taffy_tree,
|
||||
@@ -2715,6 +2758,359 @@ fn resolve_grid_areas(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct ColumnSubgridWrapper {
|
||||
node: taffy::NodeId,
|
||||
gap: f32,
|
||||
start_mbp: f32,
|
||||
end_mbp: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct ColumnSubgridLeaf {
|
||||
node: taffy::NodeId,
|
||||
dom: NodeId,
|
||||
column: usize,
|
||||
gap: f32,
|
||||
start_mbp: f32,
|
||||
end_mbp: f32,
|
||||
}
|
||||
|
||||
struct ColumnSubgridPlan {
|
||||
parent: taffy::NodeId,
|
||||
track_count: usize,
|
||||
parent_gap: f32,
|
||||
wrappers: Vec<ColumnSubgridWrapper>,
|
||||
leaves: Vec<ColumnSubgridLeaf>,
|
||||
}
|
||||
|
||||
fn is_full_span_column_subgrid(style: &crate::LayoutStyle) -> bool {
|
||||
if style.display != crate::Display::Grid
|
||||
|| !style.grid_template_columns_subgrid
|
||||
|| style.overflow_hidden
|
||||
|| style.position == Some(taffy::Position::Absolute)
|
||||
|| style.width != crate::Dimension::Auto
|
||||
|| style.justify_self.is_some_and(|value| value != taffy::AlignSelf::STRETCH)
|
||||
|| style.margin_auto[1]
|
||||
|| style.margin_auto[3]
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let Some(line) = &style.grid_column else {
|
||||
return false;
|
||||
};
|
||||
matches!(
|
||||
(&line.start, &line.end),
|
||||
(taffy::GridPlacement::Line(start), taffy::GridPlacement::Line(end))
|
||||
if start.as_i16() == 1 && end.as_i16() == -1
|
||||
)
|
||||
}
|
||||
|
||||
/// Collect the deliberately bounded Grid Level 2 subset used by broad
|
||||
/// "aligned rows" components: a full-span column subgrid, optionally nested
|
||||
/// through more full-span column subgrids, whose final children auto-place one
|
||||
/// per inherited column. Partial spans, explicit leaf placement, independent
|
||||
/// formatting contexts, and authored inline sizing are left on the existing
|
||||
/// fallback rather than being represented inaccurately.
|
||||
fn collect_column_subgrid_descendants(
|
||||
tree: &DomTree,
|
||||
dom: NodeId,
|
||||
track_count: usize,
|
||||
taffy_by_dom: &HashMap<NodeId, taffy::NodeId>,
|
||||
styles: &HashMap<NodeId, crate::LayoutStyle>,
|
||||
start_mbp: f32,
|
||||
end_mbp: f32,
|
||||
depth: usize,
|
||||
wrappers: &mut Vec<ColumnSubgridWrapper>,
|
||||
leaves: &mut Vec<ColumnSubgridLeaf>,
|
||||
) -> bool {
|
||||
if depth > 8 {
|
||||
return false;
|
||||
}
|
||||
let Some(style) = styles.get(&dom) else {
|
||||
return false;
|
||||
};
|
||||
let Some(&node) = taffy_by_dom.get(&dom) else {
|
||||
return false;
|
||||
};
|
||||
let start_mbp = start_mbp + style.margin.left + style.border.left + style.padding.left;
|
||||
let end_mbp = end_mbp + style.margin.right + style.border.right + style.padding.right;
|
||||
let gap = style.column_gap.unwrap_or(0.0);
|
||||
wrappers.push(ColumnSubgridWrapper { node, gap, start_mbp, end_mbp });
|
||||
|
||||
let children: Vec<NodeId> = tree
|
||||
.children(dom)
|
||||
.into_iter()
|
||||
.filter(|child| {
|
||||
styles
|
||||
.get(child)
|
||||
.map(|style| style.display != crate::Display::None)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.collect();
|
||||
if children.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let nested = children
|
||||
.iter()
|
||||
.filter(|child| styles.get(child).is_some_and(is_full_span_column_subgrid))
|
||||
.count();
|
||||
if nested > 0 {
|
||||
if nested != children.len() {
|
||||
return false;
|
||||
}
|
||||
return children.into_iter().all(|child| {
|
||||
collect_column_subgrid_descendants(
|
||||
tree,
|
||||
child,
|
||||
track_count,
|
||||
taffy_by_dom,
|
||||
styles,
|
||||
start_mbp,
|
||||
end_mbp,
|
||||
depth + 1,
|
||||
wrappers,
|
||||
leaves,
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
let flow = style.grid_auto_flow.unwrap_or(taffy::GridAutoFlow::Row);
|
||||
if !matches!(flow, taffy::GridAutoFlow::Row | taffy::GridAutoFlow::RowDense) {
|
||||
return false;
|
||||
}
|
||||
for (index, child) in children.into_iter().enumerate() {
|
||||
let Some(child_style) = styles.get(&child) else {
|
||||
return false;
|
||||
};
|
||||
// This first subset intentionally excludes spanning and explicitly
|
||||
// placed items. It is the common data-row/card-row pattern and keeps
|
||||
// each descendant contribution attributable to one ancestor track.
|
||||
if child_style.grid_column.is_some()
|
||||
|| child_style.position == Some(taffy::Position::Absolute)
|
||||
|| child_style.margin_auto[1]
|
||||
|| child_style.margin_auto[3]
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let Some(&node) = taffy_by_dom.get(&child) else {
|
||||
return false;
|
||||
};
|
||||
leaves.push(ColumnSubgridLeaf {
|
||||
node,
|
||||
dom: child,
|
||||
column: index % track_count,
|
||||
gap,
|
||||
start_mbp,
|
||||
end_mbp,
|
||||
});
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn fixed_grid_tracks(widths: &[f32]) -> Vec<taffy::GridTemplateComponent<String>> {
|
||||
widths
|
||||
.iter()
|
||||
.map(|width| {
|
||||
taffy::GridTemplateComponent::Single(taffy::MinMax {
|
||||
min: taffy::MinTrackSizingFunction::length((*width).max(0.0)),
|
||||
max: taffy::MaxTrackSizingFunction::length((*width).max(0.0)),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Resolve a safe full-span column-subgrid subset in two passes.
|
||||
///
|
||||
/// Gecko first collects subgrid descendants into the nearest non-subgridded
|
||||
/// ancestor's track sizing, then copies that ancestor's *used* track sizes
|
||||
/// down the chain. Its descendant contributions add accumulated edge
|
||||
/// margin/border/padding and center a custom subgrid gap over the ancestor
|
||||
/// gap. Taffy has no subgrid primitive, so we reproduce those same operations
|
||||
/// only for definite, all-auto, single-span rows. Once every max-content
|
||||
/// growth limit fits, `justify-content:normal` stretches all auto tracks by an
|
||||
/// equal share; narrower/cyclic cases decline this fast path.
|
||||
///
|
||||
/// The current style model does not expose orthogonal writing modes and emits
|
||||
/// Taffy's default LTR direction for every grid; this physical-column reduction
|
||||
/// therefore cannot accidentally enter an RTL/orthogonal layout path that the
|
||||
/// surrounding renderer does not implement yet. Percentage-width parents are
|
||||
/// accepted only after the preliminary layout produced finite, explicit used
|
||||
/// track sizes; that resolved track sum is the definite basis for pass two.
|
||||
fn apply_full_span_column_subgrids<F>(
|
||||
tree: &DomTree,
|
||||
taffy_tree: &mut TaffyTree<usize>,
|
||||
id_map: &HashMap<taffy::NodeId, NodeId>,
|
||||
styles: &HashMap<NodeId, crate::LayoutStyle>,
|
||||
mut measure_max_content: F,
|
||||
) -> bool
|
||||
where
|
||||
F: FnMut(&mut TaffyTree<usize>, taffy::NodeId) -> Option<f32>,
|
||||
{
|
||||
let taffy_by_dom: HashMap<NodeId, taffy::NodeId> =
|
||||
id_map.iter().map(|(taffy, dom)| (*dom, *taffy)).collect();
|
||||
let mut plans = Vec::new();
|
||||
|
||||
for (&dom, style) in styles {
|
||||
if style.display != crate::Display::Grid
|
||||
|| style.grid_template_columns_subgrid
|
||||
|| !matches!(style.width, crate::Dimension::Px(_) | crate::Dimension::Percent(_))
|
||||
|| style.justify_content.is_some_and(|value| value != taffy::JustifyContent::STRETCH)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let all_auto = !style.grid_template_columns.is_empty()
|
||||
&& style.grid_template_columns.iter().all(|track| {
|
||||
matches!(
|
||||
track,
|
||||
taffy::GridTemplateComponent::Single(size)
|
||||
if size.min.is_auto() && size.max.is_auto()
|
||||
)
|
||||
});
|
||||
if !all_auto || style.grid_template_columns.len() > 32 {
|
||||
continue;
|
||||
}
|
||||
let track_count = style.grid_template_columns.len();
|
||||
let direct: Vec<NodeId> = tree
|
||||
.children(dom)
|
||||
.into_iter()
|
||||
.filter(|child| styles.get(child).is_some_and(|style| style.display != crate::Display::None))
|
||||
.collect();
|
||||
if direct.is_empty()
|
||||
|| !direct
|
||||
.iter()
|
||||
.all(|child| styles.get(child).is_some_and(is_full_span_column_subgrid))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let Some(&parent) = taffy_by_dom.get(&dom) else {
|
||||
continue;
|
||||
};
|
||||
let mut wrappers = Vec::new();
|
||||
let mut leaves = Vec::new();
|
||||
if !direct.into_iter().all(|child| {
|
||||
collect_column_subgrid_descendants(
|
||||
tree,
|
||||
child,
|
||||
track_count,
|
||||
&taffy_by_dom,
|
||||
styles,
|
||||
0.0,
|
||||
0.0,
|
||||
0,
|
||||
&mut wrappers,
|
||||
&mut leaves,
|
||||
)
|
||||
}) || leaves.is_empty()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
plans.push(ColumnSubgridPlan {
|
||||
parent,
|
||||
track_count,
|
||||
parent_gap: style.column_gap.unwrap_or(0.0),
|
||||
wrappers,
|
||||
leaves,
|
||||
});
|
||||
}
|
||||
|
||||
let mut changed = false;
|
||||
for plan in plans {
|
||||
let target_track_sum = match taffy_tree.detailed_layout_info(plan.parent) {
|
||||
taffy::tree::DetailedLayoutInfo::Grid(info)
|
||||
if info.columns.negative_implicit_tracks == 0
|
||||
&& info.columns.positive_implicit_tracks == 0
|
||||
&& info.columns.explicit_tracks as usize == plan.track_count =>
|
||||
{
|
||||
info.columns.sizes.iter().sum::<f32>()
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
if !target_track_sum.is_finite() || target_track_sum <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
let mut max_content = vec![0.0f32; plan.track_count];
|
||||
let mut valid = true;
|
||||
for leaf in &plan.leaves {
|
||||
let Some(mut contribution) = measure_max_content(taffy_tree, leaf.node) else {
|
||||
valid = false;
|
||||
break;
|
||||
};
|
||||
let Some(leaf_style) = styles.get(&leaf.dom) else {
|
||||
valid = false;
|
||||
break;
|
||||
};
|
||||
contribution += leaf_style.margin.left + leaf_style.margin.right;
|
||||
if plan.track_count > 1 {
|
||||
let gap_delta = leaf.gap - plan.parent_gap;
|
||||
contribution += if leaf.column == 0 || leaf.column + 1 == plan.track_count {
|
||||
gap_delta / 2.0
|
||||
} else {
|
||||
gap_delta
|
||||
};
|
||||
}
|
||||
if leaf.column == 0 {
|
||||
contribution += leaf.start_mbp;
|
||||
}
|
||||
if leaf.column + 1 == plan.track_count {
|
||||
contribution += leaf.end_mbp;
|
||||
}
|
||||
max_content[leaf.column] = max_content[leaf.column].max(contribution.max(0.0));
|
||||
}
|
||||
let max_sum: f32 = max_content.iter().sum();
|
||||
if !valid || max_sum > target_track_sum + 0.01 {
|
||||
continue;
|
||||
}
|
||||
let stretch = (target_track_sum - max_sum) / plan.track_count as f32;
|
||||
let used: Vec<f32> = max_content.iter().map(|width| width + stretch).collect();
|
||||
if used.iter().any(|width| !width.is_finite() || *width < 0.0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate the entire copied chain before mutating anything. A large
|
||||
// edge MBP or gap delta can exhaust an outer track; declining the plan
|
||||
// atomically is safer than leaving only the ancestor frozen.
|
||||
let mut copied_wrappers = Vec::with_capacity(plan.wrappers.len());
|
||||
for wrapper in &plan.wrappers {
|
||||
let mut copied = used.clone();
|
||||
if plan.track_count > 1 {
|
||||
let root_half = plan.parent_gap / 2.0;
|
||||
let child_half = wrapper.gap / 2.0;
|
||||
for (index, width) in copied.iter_mut().enumerate() {
|
||||
*width += if index == 0 || index + 1 == plan.track_count {
|
||||
root_half - child_half
|
||||
} else {
|
||||
plan.parent_gap - wrapper.gap
|
||||
};
|
||||
}
|
||||
}
|
||||
copied[0] -= wrapper.start_mbp;
|
||||
copied[plan.track_count - 1] -= wrapper.end_mbp;
|
||||
if copied.iter().any(|width| !width.is_finite() || *width < 0.0)
|
||||
|| taffy_tree.style(wrapper.node).is_err()
|
||||
{
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
copied_wrappers.push((wrapper.node, copied));
|
||||
}
|
||||
if !valid || taffy_tree.style(plan.parent).is_err() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut parent_style = taffy_tree.style(plan.parent).unwrap().clone();
|
||||
parent_style.grid_template_columns = fixed_grid_tracks(&used);
|
||||
let _ = taffy_tree.set_style(plan.parent, parent_style);
|
||||
for (node, copied) in copied_wrappers {
|
||||
let mut style = taffy_tree.style(node).unwrap().clone();
|
||||
style.grid_template_columns = fixed_grid_tracks(&copied);
|
||||
let _ = taffy_tree.set_style(node, style);
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
changed
|
||||
}
|
||||
|
||||
/// Resolve a raw `grid-column`/`grid-row` value that names grid lines into a
|
||||
/// numeric `taffy::Line`, using `map` (line-name -> 1-based line number). Handles
|
||||
/// `a / b` (each side a name, integer, or `span n`) and the single-ident
|
||||
|
||||
@@ -365,6 +365,10 @@ pub struct LayoutStyle {
|
||||
// resolved to line placements on children in a later pass.
|
||||
pub grid_template_columns: Vec<taffy::GridTemplateComponent<String>>,
|
||||
pub grid_template_rows: Vec<taffy::GridTemplateComponent<String>>,
|
||||
/// `grid-template-columns: subgrid`. Taffy has no native subgrid track
|
||||
/// component, so `dom` resolves the safe full-span column subset after
|
||||
/// measuring the non-subgridded ancestor's intrinsic track contributions.
|
||||
pub grid_template_columns_subgrid: bool,
|
||||
pub grid_auto_flow: Option<taffy::GridAutoFlow>,
|
||||
pub grid_areas: Option<Vec<Vec<String>>>,
|
||||
pub grid_area_name: Option<String>,
|
||||
|
||||
@@ -993,6 +993,7 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
||||
}
|
||||
"grid-template-columns" => {
|
||||
let (tracks, names) = parse_track_list_named(value);
|
||||
style.grid_template_columns_subgrid = is_subgrid_track_list(value);
|
||||
style.grid_template_columns = tracks;
|
||||
style.grid_col_line_names = (!names.is_empty()).then(|| build_line_map(names));
|
||||
}
|
||||
@@ -1749,10 +1750,17 @@ pub(crate) fn parse_track_list_named(
|
||||
Vec<taffy::GridTemplateComponent<String>>,
|
||||
Vec<(String, i16)>,
|
||||
) {
|
||||
let tokens = tokenize_tracks(value);
|
||||
let mut tracks = Vec::new();
|
||||
let mut names = Vec::new();
|
||||
let mut line: i16 = 1;
|
||||
for tok in tokenize_tracks(value) {
|
||||
// A subgridded axis owns line names but no sizing functions. Do not turn
|
||||
// the keyword into the generic unknown-token => auto-track fallback.
|
||||
let is_subgrid = tokens
|
||||
.first()
|
||||
.map(|token| token.eq_ignore_ascii_case("subgrid"))
|
||||
.unwrap_or(false);
|
||||
for tok in tokens.into_iter().skip(usize::from(is_subgrid)) {
|
||||
expand_track_token(&tok, &mut tracks, &mut names, &mut line);
|
||||
}
|
||||
(tracks, names)
|
||||
@@ -1945,6 +1953,7 @@ fn parse_grid_template(style: &mut LayoutStyle, value: &str) {
|
||||
}
|
||||
if let Some(cols) = cols_part {
|
||||
let (tracks, names) = parse_track_list_named(cols);
|
||||
style.grid_template_columns_subgrid = is_subgrid_track_list(cols);
|
||||
style.grid_template_columns = tracks;
|
||||
style.grid_col_line_names = (!names.is_empty()).then(|| build_line_map(names));
|
||||
}
|
||||
@@ -1963,6 +1972,7 @@ fn parse_grid_shorthand(style: &mut LayoutStyle, value: &str) {
|
||||
if rows.to_ascii_lowercase().contains("auto-flow") {
|
||||
style.grid_template_rows.clear();
|
||||
let (tracks, names) = parse_track_list_named(columns);
|
||||
style.grid_template_columns_subgrid = is_subgrid_track_list(columns);
|
||||
style.grid_template_columns = tracks;
|
||||
style.grid_col_line_names = (!names.is_empty()).then(|| build_line_map(names));
|
||||
style.grid_auto_flow = Some(if rows.to_ascii_lowercase().contains("dense") {
|
||||
@@ -1972,6 +1982,7 @@ fn parse_grid_shorthand(style: &mut LayoutStyle, value: &str) {
|
||||
});
|
||||
} else if columns.to_ascii_lowercase().contains("auto-flow") {
|
||||
style.grid_template_columns.clear();
|
||||
style.grid_template_columns_subgrid = false;
|
||||
let (tracks, names) = parse_track_list_named(rows);
|
||||
style.grid_template_rows = tracks;
|
||||
style.grid_row_line_names = (!names.is_empty()).then(|| build_line_map(names));
|
||||
@@ -1985,6 +1996,13 @@ fn parse_grid_shorthand(style: &mut LayoutStyle, value: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_subgrid_track_list(value: &str) -> bool {
|
||||
tokenize_tracks(value)
|
||||
.first()
|
||||
.map(|token| token.eq_ignore_ascii_case("subgrid"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn parse_grid_auto_flow(value: &str) -> Option<taffy::GridAutoFlow> {
|
||||
let lower = value.trim().to_ascii_lowercase();
|
||||
let dense = lower.split_whitespace().any(|token| token == "dense");
|
||||
|
||||
@@ -1512,6 +1512,153 @@ fn auto_grid_track_freezes_at_growth_limit_during_distribution() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Chromium 150 oracle for the full-span nested column-subgrid pattern used by
|
||||
/// Mozilla's springboard/data rows. The 20px child gap is centered on the
|
||||
/// ancestor's zero-width grid lines, so descendant contributions inflate the
|
||||
/// ancestor tracks by [10, 20, 10]. The remaining 240px then stretches the
|
||||
/// three auto tracks equally: 130px, 180px, 190px.
|
||||
#[test]
|
||||
fn nested_full_span_column_subgrid_shares_ancestor_tracks() {
|
||||
let tree = parse_html(
|
||||
r#"
|
||||
<style>
|
||||
* { box-sizing:border-box }
|
||||
html, body { margin:0 }
|
||||
#parent {
|
||||
display:grid;
|
||||
width:500px;
|
||||
grid-template-columns:repeat(3, auto);
|
||||
}
|
||||
.subgrid, .inner {
|
||||
display:grid;
|
||||
grid-column:1 / -1;
|
||||
grid-template-columns:subgrid;
|
||||
}
|
||||
.inner { column-gap:20px }
|
||||
.leaf { height:20px }
|
||||
.a { width:40px }
|
||||
.b { width:80px }
|
||||
.c { width:100px }
|
||||
</style>
|
||||
<div id="parent">
|
||||
<div id="row-one" class="subgrid">
|
||||
<div id="inner-one" class="inner">
|
||||
<div id="a" class="leaf a"></div>
|
||||
<div id="b" class="leaf b"></div>
|
||||
<div id="c" class="leaf c"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="subgrid">
|
||||
<div class="inner">
|
||||
<div class="leaf a"></div>
|
||||
<div class="leaf b"></div>
|
||||
<div class="leaf c"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"#,
|
||||
);
|
||||
let layout = layout_dom(&tree, (800.0, 600.0));
|
||||
let rect = |id| layout.rects[&tree.get_element_by_id(id).unwrap()];
|
||||
let parent = rect("parent");
|
||||
let row = rect("row-one");
|
||||
let inner = rect("inner-one");
|
||||
let a = rect("a");
|
||||
let b = rect("b");
|
||||
let c = rect("c");
|
||||
|
||||
assert!(
|
||||
(parent.width - 500.0).abs() < 0.01
|
||||
&& (parent.height - 40.0).abs() < 0.01
|
||||
&& (row.width - 500.0).abs() < 0.01
|
||||
&& (row.height - 20.0).abs() < 0.01
|
||||
&& (inner.width - 500.0).abs() < 0.01
|
||||
&& (inner.height - 20.0).abs() < 0.01,
|
||||
"nested subgrids must share one row of ancestor tracks: \
|
||||
parent={parent:?} row={row:?} inner={inner:?}"
|
||||
);
|
||||
assert!(
|
||||
(a.x - 0.0).abs() < 0.01
|
||||
&& (a.width - 40.0).abs() < 0.01
|
||||
&& (b.x - 140.0).abs() < 0.01
|
||||
&& (b.width - 80.0).abs() < 0.01
|
||||
&& (c.x - 320.0).abs() < 0.01
|
||||
&& (c.width - 100.0).abs() < 0.01,
|
||||
"Chrome column positions: a={a:?} b={b:?} c={c:?}"
|
||||
);
|
||||
|
||||
let repeated = layout_dom(&tree, (800.0, 600.0));
|
||||
for id in ["parent", "row-one", "inner-one", "a", "b", "c"] {
|
||||
let node = tree.get_element_by_id(id).unwrap();
|
||||
assert_eq!(
|
||||
layout.rects[&node], repeated.rects[&node],
|
||||
"the two-pass reduction must be stable across repeated layouts: {id}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Cases outside the bounded reduction must retain the existing fallback,
|
||||
/// rather than freezing an invented set of ancestor tracks. The narrow parent
|
||||
/// cannot fit every max-content growth limit, and the second row only spans a
|
||||
/// subset of its parent columns.
|
||||
#[test]
|
||||
fn unsafe_column_subgrid_cases_decline_the_fast_path() {
|
||||
let tree = parse_html(
|
||||
r#"
|
||||
<style>
|
||||
* { box-sizing:border-box }
|
||||
html, body { margin:0 }
|
||||
.parent {
|
||||
display:grid;
|
||||
grid-template-columns:repeat(3, auto);
|
||||
}
|
||||
#narrow { width:200px }
|
||||
#partial { width:500px }
|
||||
.subgrid, .inner {
|
||||
display:grid;
|
||||
grid-column:1 / -1;
|
||||
grid-template-columns:subgrid;
|
||||
}
|
||||
#partial-row { grid-column:1 / 3 }
|
||||
.inner { column-gap:20px }
|
||||
.leaf { height:20px }
|
||||
.a { width:40px }
|
||||
.b { width:80px }
|
||||
.c { width:100px }
|
||||
</style>
|
||||
<div id="narrow" class="parent">
|
||||
<div class="subgrid"><div class="inner">
|
||||
<div class="leaf a"></div>
|
||||
<div id="narrow-b" class="leaf b"></div>
|
||||
<div class="leaf c"></div>
|
||||
</div></div>
|
||||
</div>
|
||||
<div id="partial" class="parent">
|
||||
<div id="partial-row" class="subgrid"><div class="inner">
|
||||
<div class="leaf a"></div>
|
||||
<div id="partial-b" class="leaf b"></div>
|
||||
<div class="leaf c"></div>
|
||||
</div></div>
|
||||
</div>
|
||||
"#,
|
||||
);
|
||||
let layout = layout_dom(&tree, (800.0, 600.0));
|
||||
let rect = |id| layout.rects[&tree.get_element_by_id(id).unwrap()];
|
||||
let narrow = rect("narrow");
|
||||
let narrow_b = rect("narrow-b");
|
||||
let partial = rect("partial");
|
||||
let partial_b = rect("partial-b");
|
||||
|
||||
assert!(
|
||||
(narrow.height - 60.0).abs() < 0.01 && (narrow_b.x - narrow.x).abs() < 0.01,
|
||||
"max-content overflow must retain the stacked fallback: {narrow:?} {narrow_b:?}"
|
||||
);
|
||||
assert!(
|
||||
(partial.height - 60.0).abs() < 0.01 && (partial_b.x - partial.x).abs() < 0.01,
|
||||
"partial-span subgrid must retain the stacked fallback: {partial:?} {partial_b:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_block_before_participates_in_normal_flow_geometry() {
|
||||
let tree = parse_html(
|
||||
|
||||
Reference in new issue
Block a user