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,
|
&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(
|
if apply_table_cell_block_alignment(
|
||||||
tree,
|
tree,
|
||||||
&mut taffy_tree,
|
&mut taffy_tree,
|
||||||
@@ -2421,6 +2445,25 @@ pub(crate) fn layout_dom_with_web_fonts(
|
|||||||
) {
|
) {
|
||||||
let _ = taffy_tree.compute_layout(taffy_root, available);
|
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(
|
if apply_table_cell_block_alignment(
|
||||||
tree,
|
tree,
|
||||||
&mut taffy_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
|
/// 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
|
/// 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
|
/// `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.
|
// resolved to line placements on children in a later pass.
|
||||||
pub grid_template_columns: Vec<taffy::GridTemplateComponent<String>>,
|
pub grid_template_columns: Vec<taffy::GridTemplateComponent<String>>,
|
||||||
pub grid_template_rows: 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_auto_flow: Option<taffy::GridAutoFlow>,
|
||||||
pub grid_areas: Option<Vec<Vec<String>>>,
|
pub grid_areas: Option<Vec<Vec<String>>>,
|
||||||
pub grid_area_name: Option<String>,
|
pub grid_area_name: Option<String>,
|
||||||
|
|||||||
@@ -993,6 +993,7 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
|||||||
}
|
}
|
||||||
"grid-template-columns" => {
|
"grid-template-columns" => {
|
||||||
let (tracks, names) = parse_track_list_named(value);
|
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_template_columns = tracks;
|
||||||
style.grid_col_line_names = (!names.is_empty()).then(|| build_line_map(names));
|
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<taffy::GridTemplateComponent<String>>,
|
||||||
Vec<(String, i16)>,
|
Vec<(String, i16)>,
|
||||||
) {
|
) {
|
||||||
|
let tokens = tokenize_tracks(value);
|
||||||
let mut tracks = Vec::new();
|
let mut tracks = Vec::new();
|
||||||
let mut names = Vec::new();
|
let mut names = Vec::new();
|
||||||
let mut line: i16 = 1;
|
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);
|
expand_track_token(&tok, &mut tracks, &mut names, &mut line);
|
||||||
}
|
}
|
||||||
(tracks, names)
|
(tracks, names)
|
||||||
@@ -1945,6 +1953,7 @@ fn parse_grid_template(style: &mut LayoutStyle, value: &str) {
|
|||||||
}
|
}
|
||||||
if let Some(cols) = cols_part {
|
if let Some(cols) = cols_part {
|
||||||
let (tracks, names) = parse_track_list_named(cols);
|
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_template_columns = tracks;
|
||||||
style.grid_col_line_names = (!names.is_empty()).then(|| build_line_map(names));
|
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") {
|
if rows.to_ascii_lowercase().contains("auto-flow") {
|
||||||
style.grid_template_rows.clear();
|
style.grid_template_rows.clear();
|
||||||
let (tracks, names) = parse_track_list_named(columns);
|
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_template_columns = tracks;
|
||||||
style.grid_col_line_names = (!names.is_empty()).then(|| build_line_map(names));
|
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") {
|
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") {
|
} else if columns.to_ascii_lowercase().contains("auto-flow") {
|
||||||
style.grid_template_columns.clear();
|
style.grid_template_columns.clear();
|
||||||
|
style.grid_template_columns_subgrid = false;
|
||||||
let (tracks, names) = parse_track_list_named(rows);
|
let (tracks, names) = parse_track_list_named(rows);
|
||||||
style.grid_template_rows = tracks;
|
style.grid_template_rows = tracks;
|
||||||
style.grid_row_line_names = (!names.is_empty()).then(|| build_line_map(names));
|
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> {
|
fn parse_grid_auto_flow(value: &str) -> Option<taffy::GridAutoFlow> {
|
||||||
let lower = value.trim().to_ascii_lowercase();
|
let lower = value.trim().to_ascii_lowercase();
|
||||||
let dense = lower.split_whitespace().any(|token| token == "dense");
|
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]
|
#[test]
|
||||||
fn empty_block_before_participates_in_normal_flow_geometry() {
|
fn empty_block_before_participates_in_normal_flow_geometry() {
|
||||||
let tree = parse_html(
|
let tree = parse_html(
|
||||||
|
|||||||
Reference in new issue
Block a user