fix(layout): resolve table row and section block sizes before Grid

This commit is contained in:
ldm0
2026-09-18 16:19:13 +08:00
committed by Donough Liu
parent d05c1f7fee
commit 1a75321d36
4 changed files with 1106 additions and 86 deletions
+297 -85
View File
@@ -25,8 +25,10 @@ use crate::{
style::{LayoutInlineAlignment, resolve_stylo_calc_value},
};
mod block;
mod collapsed_borders;
mod columns;
mod rows;
pub(crate) use collapsed_borders::CollapsedTableBorders;
use collapsed_borders::{prepare_collapsed_table_borders, set_collapsed_border_geometry};
@@ -45,6 +47,7 @@ struct TableCell {
column: usize,
row_span: usize,
column_span: usize,
block_layout: Option<block::CellBlockLayout>,
}
#[derive(Clone, Copy)]
@@ -52,7 +55,7 @@ struct TableRow {
id: LayoutBoxId,
group: Option<LayoutBoxId>,
index: usize,
track: taffy::TrackSizingFunction,
grid_index: usize,
}
#[derive(Clone, Copy)]
@@ -133,8 +136,13 @@ struct TableContext {
collapsed_borders: bool,
column_count: usize,
column_constraints: Vec<TableColumnConstraint>,
column_sizes: Vec<f32>,
sections: Vec<rows::SectionConstraint>,
section_boxes: Vec<LayoutBoxId>,
section_tracks: Vec<std::ops::Range<usize>>,
layout_mode: TableLayoutMode,
inline_border_spacing: f32,
block_border_spacing: f32,
writing_mode: WritingMode,
}
@@ -274,7 +282,31 @@ where
{
let mut context = build_table_context(world, root);
context.collect_cell_inline_constraints(world);
let grid_inputs = context.resolve_column_tracks(inputs);
let mut grid_inputs = context.resolve_column_tracks(inputs);
let allocated_wrapper = world.boxes[root.index()]
.layout_parent
.is_some_and(|parent| {
let display = world.boxes[parent.index()].style.display();
display.is_flex_container() || display.is_grid_container()
});
if allocated_wrapper && !context.captions.is_empty() {
let mut space = grid_inputs.constraint_space(context.writing_mode);
if let Some(block_size) = space.known_size.block_size {
// Flex/Grid allocate the complete wrapper. Caption space must not
// be allocated to the table grid a second time.
let captions = layout_captions(
world,
&context.captions,
space.known_size.inline_size.unwrap_or(0.0),
0.0,
context.writing_mode,
RunMode::ComputeSize,
);
space.known_size.block_size = Some((block_size - captions).max(0.0));
grid_inputs = space.into_layout_input();
}
}
let grid_inputs = context.resolve_row_tracks(world, grid_inputs);
let mut output = {
let mut wrapper = TableTreeWrapper {
world,
@@ -283,7 +315,7 @@ where
compute_grid_layout(&mut wrapper, NodeId::from(0usize), grid_inputs)
};
if inputs.run_mode == RunMode::PerformLayout {
{
let caption_parent_writing_mode = world.boxes[root.index()].style.writing_mode();
let top_captions = context
.captions
@@ -303,16 +335,21 @@ where
output.size.width,
0.0,
caption_parent_writing_mode,
inputs.run_mode,
);
shift_grid_children(world, &context.cells, top_height);
let bottom_height = layout_captions(
world,
&bottom_captions,
output.size.width,
top_height + output.size.height,
caption_parent_writing_mode,
inputs.run_mode,
);
apply_structural_layout(world, root, &context, top_height, output.size);
if inputs.run_mode == RunMode::PerformLayout {
context.align_row_baselines(world, &mut output);
shift_grid_children(world, &context.cells, top_height);
apply_structural_layout(world, root, &context, top_height, output.size);
}
if let Some(first_baseline) = &mut output.first_baselines.y {
*first_baseline += top_height;
}
@@ -351,6 +388,8 @@ where
let mut columns = Vec::new();
let mut max_columns = 0usize;
let mut column_tracks = Vec::new();
let mut sections = Vec::new();
let mut section_boxes = Vec::new();
let layout_mode = if root_style.uses_fixed_table_layout() {
TableLayoutMode::Fixed
} else {
@@ -361,7 +400,31 @@ where
collect_columns(world, column, None, &mut columns, &mut column_tracks);
}
for section in grouped_children.sections() {
let start = rows.len();
collect_rows(world, section, None, &mut rows, &mut cells);
{
let dimension = writing_mode
.to_logical(world.boxes[section.index()].style.taffy.size)
.block_size;
let is_group = matches!(
world.boxes[section.index()].kind,
LayoutBoxKind::TableRowGroup
| LayoutBoxKind::TableHeaderGroup
| LayoutBoxKind::TableFooterGroup
| LayoutBoxKind::AnonymousTableRowGroup
);
sections.push(rows::SectionConstraint {
rows: start..rows.len(),
fixed: (is_group && dimension.tag() == taffy::CompactLength::LENGTH_TAG)
.then(|| dimension.value().max(0.0)),
percent: (is_group && dimension.tag() == taffy::CompactLength::PERCENT_TAG)
.then(|| dimension.value().max(0.0)),
is_body: Some(section) != grouped_children.header
&& Some(section) != grouped_children.footer,
size: 0.0,
});
section_boxes.push(section);
}
}
place_table_cells(&mut cells, &rows, &mut max_columns);
max_columns = max_columns.max(column_tracks.len()).max(1);
@@ -376,7 +439,7 @@ where
end: style_helpers::span(cell.row_span as u16),
};
clear_table_cell_inline_sizing(&mut cell.style, writing_mode);
normalize_table_cell_block_sizing(&mut cell.style, writing_mode);
block::clear_cell_block_sizing(&mut cell.style, writing_mode);
}
let placeholder_track: taffy::TrackSizingFunction = style_helpers::auto();
style.grid_template_columns =
@@ -384,7 +447,7 @@ where
style.grid_template_rows = if rows.is_empty() {
vec![style_helpers::auto()]
} else {
rows.iter().map(|row| row.track.into()).collect()
vec![style_helpers::auto(); rows.len()]
};
style.gap = Size {
width: style_helpers::length(spacing.width),
@@ -411,8 +474,13 @@ where
collapsed_borders: collapsed,
column_count: max_columns,
column_constraints: column_tracks,
column_sizes: Vec::new(),
sections,
section_boxes,
section_tracks: Vec::new(),
layout_mode,
inline_border_spacing: spacing.width,
block_border_spacing: spacing.height,
writing_mode,
}
}
@@ -518,12 +586,13 @@ impl TableContext {
)
};
self.style.grid_template_columns = column_sizes
.into_iter()
.map(|size| {
.iter()
.map(|&size| {
let track: taffy::TrackSizingFunction = style_helpers::length(size);
track.into()
})
.collect();
self.column_sizes = column_sizes;
let numeric_inline_size = if self.style.box_sizing == taffy::BoxSizing::ContentBox {
(used_inline_size - inline_insets).max(0.0)
@@ -718,9 +787,7 @@ fn collect_rows<N>(
id: current,
group,
index: row_index,
track: minimum_dimension_track(
world.boxes[current.index()].style.taffy.size.height,
),
grid_index: row_index,
});
for cell in world.boxes[current.index()].children.iter().copied() {
if !matches!(
@@ -752,6 +819,7 @@ fn collect_rows<N>(
column: 0,
row_span,
column_span,
block_layout: None,
});
}
}
@@ -848,20 +916,6 @@ fn dimension_track(dimension: Dimension) -> TableColumnConstraint {
}
}
fn minimum_dimension_track(dimension: Dimension) -> taffy::TrackSizingFunction {
match dimension.tag() {
taffy::CompactLength::LENGTH_TAG => style_helpers::minmax(
style_helpers::length(dimension.value()),
style_helpers::auto(),
),
taffy::CompactLength::PERCENT_TAG => style_helpers::minmax(
style_helpers::percent(dimension.value()),
style_helpers::auto(),
),
_ => style_helpers::auto(),
}
}
fn authored_table_cell_inline_constraint(
style: &Style<Atom>,
table_writing_mode: WritingMode,
@@ -1047,22 +1101,6 @@ fn clear_table_cell_inline_sizing(style: &mut Style<Atom>, writing_mode: Writing
set_physical_inline_dimension(writing_mode, &mut style.max_size, Dimension::auto());
}
fn normalize_table_cell_block_sizing(style: &mut Style<Atom>, writing_mode: WritingMode) {
let size = writing_mode.to_logical(style.size).block_size;
let min_size = writing_mode.to_logical(style.min_size).block_size;
if writing_mode.is_horizontal() {
if min_size.is_auto() {
style.min_size.height = size;
}
style.size.height = Dimension::auto();
} else {
if min_size.is_auto() {
style.min_size.width = size;
}
style.size.width = Dimension::auto();
}
}
fn set_physical_inline_dimension(
writing_mode: WritingMode,
size: &mut Size<Dimension>,
@@ -1081,6 +1119,7 @@ fn layout_captions<N>(
width: f32,
mut y: f32,
parent_writing_mode: WritingMode,
run_mode: RunMode,
) -> f32
where
N: Copy + Debug + Eq + Hash,
@@ -1112,20 +1151,22 @@ where
},
sizing_mode: SizingMode::InherentSize,
sizing_purpose: SizingPurpose::Layout,
run_mode: RunMode::PerformLayout,
run_mode,
axis: taffy::RequestedAxis::Both,
block_auto_behavior: AutoSizeBehavior::FitContent,
vertical_margins_are_collapsible: Line::FALSE,
};
let output = world.compute_child_layout(caption.to_taffy(), inputs);
set_box_layout(
world,
caption,
Point { x: margin.left, y },
output,
order,
Some(width),
);
if run_mode == RunMode::PerformLayout {
set_box_layout(
world,
caption,
Point { x: margin.left, y },
output,
order,
Some(width),
);
}
y += output.size.height + margin.bottom;
}
y - start
@@ -1169,9 +1210,50 @@ fn apply_structural_layout<N>(
let row_starts = track_starts(origin.y, &detailed.rows.sizes, &detailed.rows.gutters);
let column_starts = track_starts(origin.x, &detailed.columns.sizes, &detailed.columns.gutters);
let content_width = track_extent(&detailed.columns.sizes, &detailed.columns.gutters);
let content_height = track_extent(&detailed.rows.sizes, &detailed.rows.gutters);
// Empty tables retain spacing in their intrinsic inline contribution,
// while their row/group rectangles span the content box when no real
// columns exist. Keep that distinction out of column sizing.
let empty_inline_spacing = if context.cells.is_empty() && context.columns.is_empty() {
context.inline_border_spacing
} else {
0.0
};
let part_x = origin.x - empty_inline_spacing;
let part_width = content_width + 2.0 * empty_inline_spacing;
let occupied_sections = || {
context
.sections
.iter()
.zip(&context.section_tracks)
.filter(|(section, _)| !section.rows.is_empty() || section.size > 0.0)
};
let first_section_track = occupied_sections()
.next()
.map_or(0, |(_, range)| range.start);
let last_section_track = occupied_sections()
.next_back()
.map_or(detailed.rows.sizes.len(), |(_, range)| range.end);
let content_top = row_starts
.get(first_section_track)
.copied()
.unwrap_or(origin.y);
let content_height = track_range_extent(
&detailed.rows.sizes,
&detailed.rows.gutters,
first_section_track,
last_section_track,
);
if context.collapsed_borders {
let mut row_lines = row_starts.clone();
// Border conflicts are indexed by actual rows. Empty-section tracks
// occupy space but must not change the conflict grid's row count.
let mut row_lines: Vec<_> = context
.rows
.iter()
.map(|row| row_starts[row.grid_index])
.collect();
if let Some(first) = row_lines.first_mut() {
*first = origin.y;
}
row_lines.push(origin.y + content_height);
let mut column_lines = column_starts.clone();
column_lines.push(origin.x + content_width);
@@ -1179,31 +1261,33 @@ fn apply_structural_layout<N>(
}
for row in &context.rows {
let y = row_starts.get(row.index).copied().unwrap_or(origin.y);
let height = detailed.rows.sizes.get(row.index).copied().unwrap_or(0.0);
set_table_part_layout(world, row.id, origin.x, y, content_width, height);
let y = row_starts.get(row.grid_index).copied().unwrap_or(origin.y);
let height = detailed
.rows
.sizes
.get(row.grid_index)
.copied()
.unwrap_or(0.0);
set_table_part_layout(world, row.id, part_x, y, part_width, height);
}
let mut groups = context
.rows
.iter()
.filter_map(|row| row.group)
.collect::<Vec<_>>();
groups.sort_by_key(|id| id.index());
groups.dedup();
for group in groups {
let group_rows = context.rows.iter().filter(|row| row.group == Some(group));
let mut start = usize::MAX;
let mut end = 0usize;
for row in group_rows {
start = start.min(row.index);
end = end.max(row.index + 1);
}
if start != usize::MAX {
let y = row_starts.get(start).copied().unwrap_or(origin.y);
let height =
track_range_extent(&detailed.rows.sizes, &detailed.rows.gutters, start, end);
set_table_part_layout(world, group, origin.x, y, content_width, height);
for (&group, tracks) in context.section_boxes.iter().zip(&context.section_tracks) {
if matches!(
world.boxes[group.index()].kind,
LayoutBoxKind::TableRow | LayoutBoxKind::AnonymousTableRow
) {
continue;
}
let y = row_starts
.get(tracks.start)
.copied()
.unwrap_or(origin.y + content_height);
let height = track_range_extent(
&detailed.rows.sizes,
&detailed.rows.gutters,
tracks.start,
tracks.end,
);
set_table_part_layout(world, group, part_x, y, part_width, height);
}
for column in &context.columns {
let x = column_starts.get(column.start).copied().unwrap_or(origin.x);
@@ -1213,7 +1297,7 @@ fn apply_structural_layout<N>(
column.start,
column.start.saturating_add(column.span),
);
set_table_part_layout(world, column.id, x, origin.y, width, content_height);
set_table_part_layout(world, column.id, x, content_top, width, content_height);
}
let mut column_groups = context
.columns
@@ -1241,7 +1325,7 @@ fn apply_structural_layout<N>(
start,
end,
);
set_table_part_layout(world, group, x, origin.y, width, content_height);
set_table_part_layout(world, group, x, content_top, width, content_height);
}
}
@@ -1451,19 +1535,26 @@ where
fn compute_child_layout(&mut self, node_id: NodeId, inputs: LayoutInput) -> LayoutOutput {
let cell_index = usize::from(node_id);
let layout = self.context.cells[cell_index].block_layout;
let mode = self.context.writing_mode;
// The virtual table grid owns the used grid-item style: margins are
// zero, column sizing has consumed every applicable inline constraint,
// and cell block size is a minimum contribution.
self.with_grid_cell_style(cell_index, |world, cell| {
world.compute_child_layout(cell.to_taffy(), inputs)
})
let output = self.with_grid_cell_style(cell_index, |world, cell| {
block::layout_cell(world, cell, inputs, mode, layout)
});
if inputs.run_mode == RunMode::PerformLayout
&& let Some(layout) = &mut self.context.cells[cell_index].block_layout
&& layout.baseline.is_some()
{
layout.baseline = output.first_baselines.y;
}
output
}
fn compute_child_size(&mut self, node_id: NodeId, inputs: LayoutInput) -> IntrinsicSizeResult {
let cell_index = usize::from(node_id);
self.with_grid_cell_style(cell_index, |world, cell| {
world.compute_child_size(cell.to_taffy(), inputs)
})
self.compute_child_layout(node_id, inputs)
.into_intrinsic_size_result()
}
}
@@ -1496,6 +1587,127 @@ where
mod tests {
use super::*;
#[test]
fn row_group_and_caption_sizes_agree_in_cold_measurement_and_warm_layout() {
use crate::{LayoutDisplay, PaintColor, ResolvedLayoutStyle};
let make_box = |kind, display, height: Option<f32>| {
LayoutWorld::<usize>::new_box(
None,
None,
None,
"table-height-test".into(),
None,
None,
None,
kind,
ResolvedLayoutStyle::synthetic(
display,
Style {
size: Size {
width: Dimension::length(200.0),
height: height.map_or(Dimension::auto(), Dimension::length),
},
..Style::default()
},
PaintColor::TRANSPARENT,
),
None,
)
};
let mut world = LayoutWorld::new(
make_box(LayoutBoxKind::TableWrapper, LayoutDisplay::Table, None),
false,
);
let root = world.root();
let caption = world.allocate(make_box(
LayoutBoxKind::TableCaption,
LayoutDisplay::TableCaption,
Some(20.0),
));
let group = world.allocate(make_box(
LayoutBoxKind::TableRowGroup,
LayoutDisplay::TableRowGroup,
Some(80.0),
));
world.boxes[root.index()].children = vec![caption, group];
let mut cells = Vec::new();
for height in [10.0, 30.0] {
let row = world.allocate(make_box(
LayoutBoxKind::TableRow,
LayoutDisplay::TableRow,
None,
));
let cell = world.allocate(make_box(
LayoutBoxKind::TableCell,
LayoutDisplay::TableCell,
Some(height),
));
world.boxes[group.index()].children.push(row);
world.boxes[row.index()].children.push(cell);
cells.push(cell);
}
prepare_table_layout_trees(&mut world);
let inputs = LayoutInput {
known_dimensions: Size {
width: Some(200.0),
height: None,
},
definite_dimensions: Size {
width: Some(200.0),
height: None,
},
parent_size: Size {
width: Some(200.0),
height: None,
},
parent_writing_mode: WritingMode::HorizontalTb,
available_space: Size {
width: AvailableSpace::Definite(200.0),
height: AvailableSpace::MaxContent,
},
run_mode: RunMode::ComputeSize,
sizing_mode: SizingMode::InherentSize,
sizing_purpose: SizingPurpose::Layout,
axis: RequestedAxis::Both,
block_auto_behavior: AutoSizeBehavior::FitContent,
vertical_margins_are_collapsible: Line::FALSE,
};
let cold = world.compute_child_layout(root.to_taffy(), inputs);
assert_eq!(
cold.size,
Size {
width: 200.0,
height: 100.0
}
);
for id in cells.iter().chain([&caption, &group]) {
assert_eq!(world.boxes[id.index()].unrounded_layout.size, Size::ZERO);
}
let final_layout = world.compute_child_layout(
root.to_taffy(),
LayoutInput {
run_mode: RunMode::PerformLayout,
..inputs
},
);
let warm = world.compute_child_layout(root.to_taffy(), inputs);
assert_eq!(cold.size, final_layout.size);
assert_eq!(cold.size, warm.size);
assert_eq!(
world.boxes[group.index()].unrounded_layout.size.height,
80.0
);
assert_eq!(
world.boxes[cells[0].index()].unrounded_layout.size.height,
20.0
);
assert_eq!(
world.boxes[cells[1].index()].unrounded_layout.size.height,
60.0
);
}
#[test]
fn percentage_dependent_fixed_table_has_unbounded_parent_max_content_size() {
let grid = columns::TableGridInlineMinMax { min: 4.0, max: 4.0 };
+536
View File
@@ -0,0 +1,536 @@
//! Measure cells at resolved column widths and pass solved rows to Grid.
use super::*;
use rows::{RowConstraint, RowspanConstraint};
#[derive(Clone, Copy)]
pub(super) struct CellBlockLayout {
pub size: f32,
pub natural_size: f32,
pub definite: bool,
pub baseline: Option<f32>,
}
pub(super) fn set_block<T>(mode: WritingMode, size: &mut Size<T>, value: T) {
if mode.is_horizontal() {
size.height = value;
} else {
size.width = value;
}
}
pub(super) fn clear_cell_block_sizing(style: &mut Style<Atom>, mode: WritingMode) {
set_block(mode, &mut style.size, Dimension::auto());
set_block(mode, &mut style.min_size, Dimension::auto());
set_block(mode, &mut style.max_size, Dimension::auto());
}
fn fixed(dimension: Dimension) -> Option<f32> {
(dimension.tag() == taffy::CompactLength::LENGTH_TAG).then(|| dimension.value().max(0.0))
}
fn percent(dimension: Dimension) -> Option<f32> {
(dimension.tag() == taffy::CompactLength::PERCENT_TAG).then(|| dimension.value().max(0.0))
}
fn block_sum(mode: WritingMode, rect: Rect<f32>) -> f32 {
if mode.is_horizontal() {
rect.top + rect.bottom
} else {
rect.left + rect.right
}
}
impl TableContext {
pub(super) fn align_row_baselines<N>(
&self,
world: &mut LayoutWorld<N>,
output: &mut LayoutOutput,
) where
N: Copy + Debug + Eq + Hash,
{
let mut baselines: Vec<Option<f32>> = vec![None; self.rows.len()];
for cell in &self.cells {
if let Some(baseline) = cell.block_layout.and_then(|layout| layout.baseline) {
baselines[cell.row] = Some(baselines[cell.row].unwrap_or(0.0).max(baseline));
}
}
for cell in &self.cells {
if let Some(baseline) = cell.block_layout.and_then(|layout| layout.baseline) {
let offset = baselines[cell.row].unwrap_or(baseline) - baseline;
if offset > 0.0 {
shift_cell_contents(world, cell.id, offset);
}
}
}
if let Some(detailed) = &self.detailed {
let padding = self
.style
.padding
.resolve_or_zero(Some(output.size.width), resolve_stylo_calc_value);
let border = self
.style
.border
.resolve_or_zero(Some(output.size.width), resolve_stylo_calc_value);
let starts = track_starts(
padding.top + border.top,
&detailed.rows.sizes,
&detailed.rows.gutters,
);
if let Some(Some(baseline)) = baselines.first() {
output.first_baselines.y = Some(starts[self.rows[0].grid_index] + baseline);
}
if let Some(Some(baseline)) = baselines.last() {
output.last_baselines.y =
Some(starts[self.rows.last().unwrap().grid_index] + baseline);
}
}
}
pub(super) fn resolve_row_tracks<N>(
&mut self,
world: &mut LayoutWorld<N>,
inputs: LayoutInput,
) -> LayoutInput
where
N: Copy + Debug + Eq + Hash,
{
let mode = self.writing_mode;
// Inline-only intrinsic probes do not need a second cell measurement.
if inputs.run_mode == RunMode::ComputeSize
&& inputs.axis == RequestedAxis::from(mode.inline_axis())
{
return inputs;
}
let mut space = inputs.constraint_space(mode);
let percentage_basis = space.margin_padding_percentage_basis();
let padding = self
.style
.padding
.resolve_or_zero(percentage_basis, resolve_stylo_calc_value);
let border = self
.style
.border
.resolve_or_zero(percentage_basis, resolve_stylo_calc_value);
let insets = block_sum(mode, padding) + block_sum(mode, border);
let preferred = mode.to_logical(self.style.size).block_size;
let adjustment = if self.style.box_sizing == taffy::BoxSizing::ContentBox {
// Outer cell spacing is projected as Grid padding, but belongs
// inside the CSS table's content box.
insets - 2.0 * self.block_border_spacing
} else {
0.0
};
let resolve = |d: Dimension| {
d.maybe_resolve(
space.percentage_resolution_size.block_size,
resolve_stylo_calc_value,
)
.map(|v| v + adjustment)
};
let authored = inputs.sizing_mode == SizingMode::InherentSize;
let target = space
.known_size
.block_size
.or_else(|| authored.then(|| resolve(preferred)).flatten());
let min = authored
.then(|| resolve(mode.to_logical(self.style.min_size).block_size))
.flatten();
let max = authored
.then(|| resolve(mode.to_logical(self.style.max_size).block_size))
.flatten();
let target = target
.map(|v| v.min(max.unwrap_or(f32::INFINITY)))
.unwrap_or(0.0)
.max(min.unwrap_or(0.0));
let mut rows: Vec<_> = self
.rows
.iter()
.map(|row| {
let dimension = mode
.to_logical(world.boxes[row.id.index()].style.taffy.size)
.block_size;
RowConstraint {
size: fixed(dimension).unwrap_or(0.0),
percent: percent(dimension),
constrained: fixed(dimension).is_some() || percent(dimension).is_some(),
..Default::default()
}
})
.collect();
let mut spans = Vec::new();
for index in 0..self.cells.len() {
let cell = &self.cells[index];
let authored_style = &world.boxes[cell.id.index()].style.taffy;
let dimension = mode.to_logical(authored_style.size).block_size;
let cell_padding = authored_style
.padding
.resolve_or_zero(space.known_size.inline_size, resolve_stylo_calc_value);
let cell_border = authored_style
.border
.resolve_or_zero(space.known_size.inline_size, resolve_stylo_calc_value);
let cell_insets = block_sum(mode, cell_padding) + block_sum(mode, cell_border);
let css_size = outer_fixed_size(dimension, cell_insets, authored_style.box_sizing);
let cell_percent = percent(dimension);
let baseline_aligned = matches!(
world.boxes[cell.id.index()].style.vertical_align().kind,
LayoutInlineAlignment::Baseline
) && authored_style.align_content.is_none();
let inline = self.column_sizes[cell.column..cell.column + cell.column_span]
.iter()
.sum::<f32>()
+ self.inline_border_spacing * cell.column_span.saturating_sub(1) as f32;
let measure_inputs = LayoutInput {
known_dimensions: mode.to_physical(LogicalSize {
inline_size: Some(inline),
block_size: None,
}),
definite_dimensions: mode.to_physical(LogicalSize {
inline_size: Some(inline),
block_size: None,
}),
parent_size: mode.to_physical(LogicalSize {
inline_size: space.known_size.inline_size,
block_size: None,
}),
parent_writing_mode: mode,
available_space: mode.to_physical(LogicalSize {
inline_size: AvailableSpace::Definite(inline),
block_size: AvailableSpace::MaxContent,
}),
run_mode: RunMode::ComputeSize,
sizing_mode: SizingMode::InherentSize,
sizing_purpose: SizingPurpose::Layout,
axis: RequestedAxis::Both,
block_auto_behavior: AutoSizeBehavior::FitContent,
vertical_margins_are_collapsible: Line::FALSE,
};
// Percentage padding is relative to the table, not the cell's
// Grid area. Freeze that basis across both measurement and layout.
self.cells[index].style.padding = cell_padding.map(style_helpers::length);
let restricted = !preferred.is_auto() || fixed(dimension).is_some();
let cell_id = self.cells[index].id;
let restored =
restrict_scrollable_percentage_children(world, cell_id, mode, restricted);
let mut wrapper = TableTreeWrapper {
world,
context: self,
};
let output = wrapper.with_grid_cell_style(index, |world, cell| {
world.compute_child_layout(cell.to_taffy(), measure_inputs)
});
for (id, style) in restored {
world.boxes[id.index()].style.taffy = style;
world.cache_clear(id.to_taffy());
}
let natural = mode.to_logical(output.size).block_size;
let cell = &mut self.cells[index];
let baseline = if mode.is_horizontal() {
output.first_baselines.y
} else {
output.first_baselines.x
};
let end_inset = if mode.is_horizontal() {
cell_padding.bottom + cell_border.bottom
} else {
cell_padding.left + cell_border.left
};
let baseline = (baseline_aligned && !world.boxes[cell.id.index()].children.is_empty())
.then(|| baseline.unwrap_or((natural - end_inset).max(0.0)));
cell.block_layout = Some(CellBlockLayout {
size: 0.0,
natural_size: natural,
definite: fixed(dimension).is_some(),
baseline,
});
let row = &mut rows[cell.row];
if baseline_aligned && !world.boxes[cell.id.index()].children.is_empty() {
row.ascent = Some(row.ascent.unwrap_or(0.0).max(baseline.unwrap_or(0.0)));
if cell.row_span == 1 {
row.descent = row.descent.max(natural - baseline.unwrap_or(0.0));
}
}
let minimum = natural.max(css_size.unwrap_or(0.0));
if cell.row_span == 1 {
row.size = row.size.max(minimum);
row.constrained |= css_size.is_some() || cell_percent.is_some();
if let Some(p) = cell_percent {
row.percent = Some(row.percent.unwrap_or(0.0).max(p));
}
} else {
row.has_rowspan_start = true;
spans.push(RowspanConstraint {
rows: cell.row..cell.row + cell.row_span,
size: minimum,
});
}
}
rows::resolve_minimums(
&mut rows,
&mut self.sections,
&mut spans,
self.block_border_spacing,
);
let nonempty_sections = self
.sections
.iter()
.filter(|section| !section.rows.is_empty())
.count();
let minimum = self
.sections
.iter()
.map(|section| section.size)
.sum::<f32>()
+ nonempty_sections.saturating_sub(1) as f32 * self.block_border_spacing
+ insets
- if nonempty_sections == 0 {
2.0 * self.block_border_spacing
} else {
0.0
};
let used = minimum.max(target);
rows::distribute_table(
&mut rows,
&mut self.sections,
(used - insets).max(0.0),
self.block_border_spacing,
);
for cell in &mut self.cells {
let layout = cell.block_layout.as_mut().unwrap();
layout.size = rows[cell.row..cell.row + cell.row_span]
.iter()
.map(|row| row.size)
.sum::<f32>()
+ cell.row_span.saturating_sub(1) as f32 * self.block_border_spacing;
layout.definite |= !preferred.is_auto() && layout.size > layout.natural_size;
}
let mut tracks = Vec::new();
// Empty sections take up height, but do not introduce cell spacing.
// With such sections, explicit spacer tracks express the gaps before
// real rows and after the table that a uniform Grid gap cannot model.
let explicit_spacing = (self.rows.is_empty()
|| self.sections.iter().any(|section| section.rows.is_empty()))
&& self.block_border_spacing > 0.0;
if explicit_spacing {
self.style.gap.height = style_helpers::length(0.0);
self.style.padding.top =
style_helpers::length((padding.top - self.block_border_spacing).max(0.0));
self.style.padding.bottom =
style_helpers::length((padding.bottom - self.block_border_spacing).max(0.0));
}
for section in &self.sections {
let mut start = tracks.len();
if section.rows.is_empty() {
// A section can have height even without a DOM row. Reserve a
// numeric track without creating an anonymous CSS row box.
tracks.push(section.size);
} else {
for index in section.rows.clone() {
if explicit_spacing {
tracks.push(self.block_border_spacing);
if index == section.rows.start {
start += 1;
}
}
self.rows[index].grid_index = tracks.len();
tracks.push(rows[index].size);
}
}
self.section_tracks.push(start..tracks.len());
}
if explicit_spacing && !self.rows.is_empty() {
tracks.push(self.block_border_spacing);
}
for cell in &mut self.cells {
cell.style.grid_row.start = style_helpers::line(
(self.rows[cell.row].grid_index + 1).min(i16::MAX as usize) as i16,
);
cell.style.grid_row.end = style_helpers::span(
(self.rows[cell.row + cell.row_span - 1].grid_index
- self.rows[cell.row].grid_index
+ 1)
.min(u16::MAX as usize) as u16,
);
}
if tracks.is_empty() {
tracks.push(
(used - insets
+ if explicit_spacing {
2.0 * self.block_border_spacing
} else {
0.0
})
.max(0.0),
);
}
self.style.grid_template_rows = tracks
.into_iter()
.map(|size| {
let track: taffy::TrackSizingFunction = style_helpers::length(size);
track.into()
})
.collect();
self.style.align_content = Some(taffy::AlignContent::START);
set_block(
mode,
&mut self.style.size,
style_helpers::length(if self.style.box_sizing == taffy::BoxSizing::ContentBox {
(used - insets
+ if explicit_spacing {
2.0 * self.block_border_spacing
} else {
0.0
})
.max(0.0)
} else {
used
}),
);
set_block(mode, &mut self.style.min_size, Dimension::auto());
set_block(mode, &mut self.style.max_size, Dimension::auto());
space.known_size.block_size = Some(used);
// Used geometry alone is not a new percentage-resolution guarantee.
space.into_layout_input()
}
}
pub(super) fn layout_cell<N>(
world: &mut LayoutWorld<N>,
cell: LayoutBoxId,
mut inputs: LayoutInput,
mode: WritingMode,
layout: Option<CellBlockLayout>,
) -> LayoutOutput
where
N: Copy + Debug + Eq + Hash,
{
let Some(layout) = layout else {
return world.compute_child_layout(cell.to_taffy(), inputs);
};
set_block(mode, &mut inputs.parent_size, None);
set_block(
mode,
&mut inputs.known_dimensions,
layout.definite.then_some(layout.size),
);
set_block(
mode,
&mut inputs.definite_dimensions,
layout.definite.then_some(layout.size),
);
if !layout.definite {
set_block(
mode,
&mut inputs.available_space,
AvailableSpace::MaxContent,
);
}
let mut output = world.compute_child_layout(cell.to_taffy(), inputs);
let free = (layout.size - mode.to_logical(output.size).block_size).max(0.0);
let alignment = world.boxes[cell.index()].style.taffy.align_content;
let offset = if layout.baseline.is_some() {
0.0
} else {
crate::taffy_tree::single_subject_block_alignment_offset(alignment, free)
};
if inputs.run_mode == RunMode::PerformLayout && offset != 0.0 {
shift_cell_contents(world, cell, offset);
}
if let Some(baseline) = &mut output.first_baselines.y {
*baseline += offset;
}
if let Some(baseline) = &mut output.last_baselines.y {
*baseline += offset;
}
if layout.baseline.is_some() && output.first_baselines.y.is_none() {
// Cells with no line baseline use the content's block-end edge. This
// must be sampled again after percentage descendants have laid out.
let padding = world.boxes[cell.index()]
.style
.taffy
.padding
.resolve_or_zero(inputs.parent_size.width, resolve_stylo_calc_value);
output.first_baselines.y = Some(if inputs.run_mode == RunMode::PerformLayout {
(output.content_size.height - padding.bottom).max(0.0)
} else {
layout.baseline.unwrap_or(0.0)
});
}
output.content_size.height += offset;
set_block(mode, &mut output.size, layout.size);
output
}
/// Blink's restricted-cell first pass sizes direct scrollable percentage
/// children to their initial minimum. The normal second pass restores the
/// authored style and resolves the percentage against the final cell height.
fn restrict_scrollable_percentage_children<N>(
world: &mut LayoutWorld<N>,
cell: LayoutBoxId,
mode: WritingMode,
restricted: bool,
) -> Vec<(LayoutBoxId, Style<Atom>)>
where
N: Copy + Debug + Eq + Hash,
{
if !restricted {
return Vec::new();
}
let mut restored = Vec::new();
for id in world.boxes[cell.index()].layout_children.clone() {
let child = &world.boxes[id.index()];
let style = &child.style.taffy;
if !child.is_replaced()
&& mode
.to_logical(style.size)
.block_size
.may_have_percentage_dependence()
&& matches!(
child.style.overflow_modes()[usize::from(mode.is_horizontal())],
crate::style::LayoutOverflowMode::Auto | crate::style::LayoutOverflowMode::Scroll
)
{
let minimum = mode
.to_logical(style.min_size)
.block_size
.maybe_resolve(None, resolve_stylo_calc_value)
.unwrap_or(0.0);
restored.push((id, style.clone()));
set_block(
mode,
&mut world.boxes[id.index()].style.taffy.size,
Dimension::length(minimum),
);
world.cache_clear(id.to_taffy());
}
}
restored
}
fn shift_cell_contents<N>(world: &mut LayoutWorld<N>, cell: LayoutBoxId, offset: f32)
where
N: Copy + Debug + Eq + Hash,
{
for child in world.boxes[cell.index()].layout_children.clone() {
if world.boxes[child.index()].style.taffy.position != taffy::Position::Absolute {
world.boxes[child.index()].unrounded_layout.location.y += offset;
}
}
if let Some(context) = &mut world.boxes[cell.index()].inline_layout {
for line in &mut context.line_placements {
line.translate_block_axis(offset);
}
for line in &mut context.fragments.lines {
line.rect.y += offset;
line.baseline += offset;
if let crate::inline::InlinePaintBounds::Bounded(rect) = &mut line.paint_bounds {
rect.y += offset;
}
}
for text in &mut context.fragments.text {
text.rect.y += offset;
}
for fragment in &mut context.fragments.boxes {
fragment.rect.y += offset;
}
}
}
+269
View File
@@ -0,0 +1,269 @@
//! CSS table block-size constraints, independent of the Grid backend.
//!
//! Distribution follows Blink's table_layout_utils.cc: rowspan deficits,
//! fixed section sizes, then table -> sections -> rows. Content minima are
//! never shrunk. Percentages retain their section-specific resolution basis.
use std::{cmp::Ordering, ops::Range};
#[derive(Clone, Copy, Debug, Default)]
pub(super) struct RowConstraint {
pub size: f32,
pub percent: Option<f32>,
pub constrained: bool,
pub has_rowspan_start: bool,
pub ascent: Option<f32>,
pub descent: f32,
}
#[derive(Clone, Debug)]
pub(super) struct SectionConstraint {
pub rows: Range<usize>,
pub fixed: Option<f32>,
pub percent: Option<f32>,
pub is_body: bool,
pub size: f32,
}
#[derive(Clone, Debug)]
pub(super) struct RowspanConstraint {
pub rows: Range<usize>,
pub size: f32,
}
fn extent(rows: &[RowConstraint], spacing: f32) -> f32 {
rows.iter().map(|row| row.size).sum::<f32>() + rows.len().saturating_sub(1) as f32 * spacing
}
// Blink uses LayoutUnit (1/64 CSS px), giving the rounding remainder to the
// last eligible track. Keep that deterministic behavior for fractional shares.
fn shares(extra: f32, weights: &[f32]) -> Vec<f32> {
let sum = weights.iter().sum::<f32>();
let mut remaining = extra;
weights
.iter()
.enumerate()
.map(|(index, weight)| {
let value = if index + 1 == weights.len() {
remaining
} else {
let ratio = if sum > 0.0 {
*weight / sum
} else {
1.0 / weights.len() as f32
};
(extra * ratio * 64.0).floor() / 64.0
};
remaining -= value;
value
})
.collect()
}
fn grow_rows(rows: &mut [RowConstraint], indices: &[usize], extra: f32, proportional: bool) {
let weights: Vec<_> = indices
.iter()
.map(|&i| if proportional { rows[i].size } else { 1.0 })
.collect();
for (&i, delta) in indices.iter().zip(shares(extra, &weights)) {
rows[i].size += delta;
}
}
fn distribute_rows(
rows: &mut [RowConstraint],
target: f32,
spacing: f32,
basis: Option<f32>,
rowspan: bool,
) {
let mut extra = target - extent(rows, spacing);
if extra <= 0.0 || rows.is_empty() {
return;
}
let deficits: Vec<_> = rows
.iter()
.map(|row| {
row.percent.zip(basis).map_or(0.0, |(percent, basis)| {
(percent * basis - row.size).max(0.0)
})
})
.collect();
let deficit = deficits.iter().sum::<f32>();
if deficit > 0.0 {
let amount = extra.min(deficit);
let eligible: Vec<_> = deficits
.iter()
.enumerate()
.filter_map(|(i, &d)| (d > 0.0).then_some(i))
.collect();
let weights: Vec<_> = eligible.iter().map(|&i| deficits[i]).collect();
for (&i, delta) in eligible.iter().zip(shares(amount, &weights)) {
rows[i].size += delta;
}
extra -= amount;
}
if extra <= 0.0 {
return;
}
if rowspan {
let origins: Vec<_> = (1..rows.len())
.filter(|&i| rows[i].has_rowspan_start)
.collect();
if !origins.is_empty() {
grow_rows(rows, &origins, extra, false);
return;
}
}
let constrained =
|row: &RowConstraint| row.constrained && (row.percent.is_none() || basis.is_some());
let auto_nonempty: Vec<_> = (0..rows.len())
.filter(|&i| rows[i].size > 0.0 && !constrained(&rows[i]))
.collect();
if !auto_nonempty.is_empty() {
grow_rows(rows, &auto_nonempty, extra, true);
return;
}
let empty: Vec<_> = (0..rows.len()).filter(|&i| rows[i].size == 0.0).collect();
if !empty.is_empty() {
if rowspan && empty.len() == rows.len() {
rows[*empty.last().unwrap()].size += extra;
return;
}
if !rowspan {
let auto_empty: Vec<_> = empty
.iter()
.copied()
.filter(|&i| !constrained(&rows[i]))
.collect();
grow_rows(
rows,
if auto_empty.is_empty() {
&empty
} else {
&auto_empty
},
extra,
false,
);
return;
}
}
let nonempty: Vec<_> = (0..rows.len()).filter(|&i| rows[i].size > 0.0).collect();
grow_rows(rows, &nonempty, extra, true);
}
/// Establish the content minimum before resolving the table's own height.
pub(super) fn resolve_minimums(
rows: &mut [RowConstraint],
sections: &mut [SectionConstraint],
spans: &mut [RowspanConstraint],
spacing: f32,
) {
for row in rows.iter_mut() {
row.size = row.size.max(row.ascent.unwrap_or(0.0) + row.descent);
}
for section in sections.iter() {
let mut total = 0.0_f32;
for row in &mut rows[section.rows.clone()] {
if let Some(percent) = &mut row.percent {
*percent = percent.min((1.0 - total).max(0.0));
total += *percent;
}
}
}
spans.sort_by(|a, b| {
if a.rows == b.rows {
return b.size.total_cmp(&a.size);
}
if a.rows.start >= b.rows.start && a.rows.end <= b.rows.end {
return Ordering::Less;
}
if b.rows.start >= a.rows.start && b.rows.end <= a.rows.end {
return Ordering::Greater;
}
a.rows.start.cmp(&b.rows.start)
});
for span in spans {
distribute_rows(&mut rows[span.rows.clone()], span.size, spacing, None, true);
}
for section in sections {
let group_rows = &mut rows[section.rows.clone()];
let minimum = extent(group_rows, spacing);
if let Some(fixed) = section.fixed.filter(|&fixed| fixed > minimum) {
distribute_rows(group_rows, fixed, spacing, Some(fixed), false);
}
section.size = extent(group_rows, spacing).max(section.fixed.unwrap_or(0.0));
}
}
/// `target` excludes outer decorations and outer cell spacing, but includes
/// the gaps between nonempty sections. Empty sections have no grid tracks.
pub(super) fn distribute_table(
rows: &mut [RowConstraint],
sections: &mut [SectionConstraint],
target: f32,
spacing: f32,
) {
if sections.is_empty() {
return;
}
let target = (target - sections.len().saturating_sub(1) as f32 * spacing).max(0.0);
let mut extra = target - sections.iter().map(|s| s.size).sum::<f32>();
if extra <= 0.0 {
return;
}
let original: Vec<_> = sections.iter().map(|s| s.size).collect();
let deficits: Vec<_> = sections
.iter()
.map(|s| s.percent.map_or(0.0, |p| (p * target - s.size).max(0.0)))
.collect();
let deficit = deficits.iter().sum::<f32>();
if deficit > 0.0 {
let amount = extra.min(deficit);
let eligible: Vec<_> = (0..sections.len()).filter(|&i| deficits[i] > 0.0).collect();
let weights: Vec<_> = eligible.iter().map(|&i| deficits[i]).collect();
for (&i, delta) in eligible.iter().zip(shares(amount, &weights)) {
sections[i].size += delta;
}
extra -= amount;
}
if extra > 0.0 {
let has_body = sections.iter().any(|s| s.is_body);
let priority = |s: &SectionConstraint| {
if s.percent.is_some() {
2
} else if s.fixed.is_some() {
1
} else {
0
}
};
let rank = sections
.iter()
.filter(|s| !has_body || s.is_body)
.map(priority)
.min()
.unwrap();
let eligible: Vec<_> = (0..sections.len())
.filter(|&i| (!has_body || sections[i].is_body) && priority(&sections[i]) == rank)
.collect();
let weights: Vec<_> = eligible.iter().map(|&i| sections[i].size).collect();
for (&i, delta) in eligible.iter().zip(shares(extra, &weights)) {
sections[i].size += delta;
}
}
for (section, original) in sections.iter().zip(original) {
if section.size > original {
distribute_rows(
&mut rows[section.rows.clone()],
section.size,
spacing,
Some(section.size),
false,
);
}
}
}
+4 -1
View File
@@ -3115,7 +3115,10 @@ impl InlineMeasurement {
/// this adapter boundary instead. This is the leaf equivalent of Chromium's
/// `AlignBlockContent` plus `BoxFragmentBuilder::MoveChildrenInDirection`, not
/// a post-layout paint translation.
fn single_subject_block_alignment_offset(alignment: Option<AlignContent>, free_space: f32) -> f32 {
pub(crate) fn single_subject_block_alignment_offset(
alignment: Option<AlignContent>,
free_space: f32,
) -> f32 {
let Some(alignment) = alignment else {
return 0.0;
};