mirror of
https://github.com/lexmount/moli.git
synced 2026-09-25 00:01:28 +00:00
fix(layout): resolve table row and section block sizes before Grid
This commit is contained in:
+297
-85
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(§ions[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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user