layout: Add initial support for row height distribution (#31421)

This change adds a version of row height distribution that follows the
distribtuion algorithm used for tables in Blink's LayoutNG. This is just
an intermediate step toward implementing a distribution algorithm for
both rows and columns more similar to Layout NG.

The CSS Table 3 specification is often wrong with regard to web
compatability, which is why we have abandoned it in favor of the Layout
NG algorithm for row height distribution.  this work.

Co-authored-by: Oriol Brufau <obrufau@igalia.com>
This commit is contained in:
Martin Robinson 2024-02-29 13:12:54 +01:00 committed by GitHub
parent 31cfaf290d
commit 127aa657c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
49 changed files with 591 additions and 302 deletions

View file

@ -24,13 +24,6 @@ pub(crate) struct ContentSizes {
/// <https://drafts.csswg.org/css-sizing/#intrinsic-sizes>
impl ContentSizes {
pub fn zero() -> Self {
Self {
min_content: Au::zero(),
max_content: Au::zero(),
}
}
pub fn map(&self, f: impl Fn(Au) -> Au) -> Self {
Self {
min_content: f(self.min_content),
@ -57,6 +50,19 @@ impl ContentSizes {
}
}
impl Zero for ContentSizes {
fn zero() -> Self {
Self {
min_content: Au::zero(),
max_content: Au::zero(),
}
}
fn is_zero(&self) -> bool {
self.min_content.is_zero() && self.max_content.is_zero()
}
}
impl Add for ContentSizes {
type Output = Self;

View file

@ -2,6 +2,8 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::ops::Range;
use app_units::{Au, MAX_AU};
use log::warn;
use servo_arc::Arc;
@ -52,17 +54,32 @@ impl CellLayout {
}
}
/// Information stored during the layout of rows.
#[derive(Clone, Debug, Default)]
struct RowLayout {
constrained: bool,
has_cell_with_span_greater_than_one: bool,
percent: Percentage,
}
/// Information stored during the layout of columns.
#[derive(Clone, Debug, Default)]
struct ColumnLayout {
constrained: bool,
has_originating_cells: bool,
}
/// A helper struct that performs the layout of the box tree version
/// of a table into the fragment tree version. This implements
/// <https://drafts.csswg.org/css-tables/#table-layout-algorithm>
struct TableLayout<'a> {
table: &'a Table,
pbm: PaddingBorderMargin,
column_constrainedness: Vec<bool>,
column_has_originating_cell: Vec<bool>,
cell_measures: Vec<Vec<CellOrColumnMeasure>>,
rows: Vec<RowLayout>,
columns: Vec<ColumnLayout>,
cell_measures: Vec<Vec<LogicalVec2<CellOrTrackMeasure>>>,
assignable_width: Au,
column_measures: Vec<CellOrColumnMeasure>,
column_measures: Vec<CellOrTrackMeasure>,
distributed_column_widths: Vec<Au>,
row_sizes: Vec<Au>,
row_baselines: Vec<Au>,
@ -71,18 +88,22 @@ struct TableLayout<'a> {
}
#[derive(Clone, Debug)]
struct CellOrColumnMeasure {
struct CellOrTrackMeasure {
content_sizes: ContentSizes,
percentage_width: Percentage,
percentage: Percentage,
}
impl CellOrColumnMeasure {
impl Zero for CellOrTrackMeasure {
fn zero() -> Self {
Self {
content_sizes: ContentSizes::zero(),
percentage_width: Percentage(0.),
percentage: Percentage(0.),
}
}
fn is_zero(&self) -> bool {
self.content_sizes.is_zero() && self.percentage.is_zero()
}
}
impl<'a> TableLayout<'a> {
@ -90,8 +111,8 @@ impl<'a> TableLayout<'a> {
Self {
table,
pbm: PaddingBorderMargin::zero(),
column_constrainedness: Vec::new(),
column_has_originating_cell: Vec::new(),
rows: Vec::new(),
columns: Vec::new(),
cell_measures: Vec::new(),
assignable_width: Au::zero(),
column_measures: Vec::new(),
@ -112,13 +133,15 @@ impl<'a> TableLayout<'a> {
containing_block: &ContainingBlock,
) {
let writing_mode = containing_block.style.writing_mode;
self.compute_column_constrainedness_and_has_originating_cells(writing_mode);
self.compute_track_constrainedness_and_has_originating_cells(writing_mode);
self.compute_cell_measures(layout_context, containing_block);
self.compute_column_measures(containing_block.style.writing_mode);
self.compute_table_width(containing_block);
self.distributed_column_widths = self.distribute_width_to_columns();
self.do_row_layout_first_pass(layout_context, containing_block, positioning_context);
self.distribute_height_to_rows();
self.layout_cells_in_row(layout_context, containing_block, positioning_context);
let first_layout_row_heights = self.do_first_row_layout(writing_mode);
self.compute_table_height_and_final_row_heights(first_layout_row_heights, containing_block);
}
/// This is an implementation of *Computing Cell Measures* from
@ -128,10 +151,11 @@ impl<'a> TableLayout<'a> {
layout_context: &LayoutContext,
containing_block: &ContainingBlock,
) {
let row_measures = vec![LogicalVec2::zero(); self.table.size.width];
self.cell_measures = vec![row_measures; self.table.size.height];
let writing_mode = containing_block.style.writing_mode;
for row_index in 0..self.table.size.height {
let mut row_measures = vec![CellOrColumnMeasure::zero(); self.table.size.width];
for column_index in 0..self.table.size.width {
let cell = match self.table.slots[row_index][column_index] {
TableSlot::Cell(ref cell) => cell,
@ -139,7 +163,7 @@ impl<'a> TableLayout<'a> {
};
let (size, min_size, max_size) = get_sizes_from_style(&cell.style, writing_mode);
let content_sizes = cell
let inline_content_sizes = cell
.contents
.contents
.inline_content_sizes(layout_context, writing_mode);
@ -148,16 +172,17 @@ impl<'a> TableLayout<'a> {
// > The outer min-content width of a table-cell is max(min-width, min-content width)
// > adjusted by the cell intrinsic offsets.
let mut outer_min_content_width = content_sizes.min_content.max(min_size.inline);
let mut outer_max_content_width = if !self.column_constrainedness[column_index] {
let mut outer_min_content_width =
inline_content_sizes.min_content.max(min_size.inline);
let mut outer_max_content_width = if !self.columns[column_index].constrained {
// > The outer max-content width of a table-cell in a non-constrained column is
// > max(min-width, width, min-content width, min(max-width, max-content width))
// > adjusted by the cell intrinsic offsets.
min_size
.inline
.max(size.inline)
.max(content_sizes.min_content)
.max(max_size.inline.min(content_sizes.max_content))
.max(inline_content_sizes.min_content)
.max(max_size.inline.min(inline_content_sizes.max_content))
} else {
// > The outer max-content width of a table-cell in a constrained column is
// > max(min-width, width, min-content width, min(max-width, width)) adjusted by the
@ -165,30 +190,63 @@ impl<'a> TableLayout<'a> {
min_size
.inline
.max(size.inline)
.max(content_sizes.min_content)
.max(inline_content_sizes.min_content)
.max(max_size.inline.min(size.inline))
};
let inline_padding_border_sum = Au::from(
cell.style
.padding(writing_mode)
.percentages_relative_to(Length::zero())
.inline_sum() +
cell.style.border_width(writing_mode).inline_sum(),
);
let padding = cell
.style
.padding(writing_mode)
.percentages_relative_to(Length::zero());
let border = cell.style.border_width(writing_mode);
let inline_padding_border_sum =
Au::from(padding.inline_sum() + border.inline_sum());
outer_min_content_width += inline_padding_border_sum;
outer_max_content_width += inline_padding_border_sum;
row_measures[column_index] = CellOrColumnMeasure {
let inline_measure = CellOrTrackMeasure {
content_sizes: ContentSizes {
min_content: outer_min_content_width,
max_content: outer_max_content_width,
},
percentage_width: percentage_contribution.inline,
percentage: percentage_contribution.inline,
};
// These calculations do not take into account the `min-content` and `max-content`
// sizes. These sizes are incorporated after the first row layout pass, when the
// block size of the layout is known.
//
// TODO: Is it correct to use the block size as the minimum of the `outer min
// content height` here? The specification doesn't mention this, but it does cause
// a test to pass.
let mut outer_min_content_height = min_size.block.max(size.block);
let mut outer_max_content_height = if !self.rows[row_index].constrained {
min_size.block.max(size.block)
} else {
min_size
.block
.max(size.block)
.max(max_size.block.min(size.block))
};
let block_padding_border_sum = Au::from(padding.block_sum() + border.block_sum());
outer_min_content_height += block_padding_border_sum;
outer_max_content_height += block_padding_border_sum;
let block_measure = CellOrTrackMeasure {
content_sizes: ContentSizes {
min_content: outer_min_content_height,
max_content: outer_max_content_height,
},
percentage: percentage_contribution.block,
};
self.cell_measures[row_index][column_index] = LogicalVec2 {
inline: inline_measure,
block: block_measure,
};
}
self.cell_measures.push(row_measures);
}
}
@ -197,14 +255,48 @@ impl<'a> TableLayout<'a> {
/// > A column is constrained if its corresponding table-column-group (if any), its
/// > corresponding table-column (if any), or any of the cells spanning only that
/// > column has a computed width that is not "auto", and is not a percentage.
fn compute_column_constrainedness_and_has_originating_cells(
fn compute_track_constrainedness_and_has_originating_cells(
&mut self,
writing_mode: WritingMode,
) {
for column_index in 0..self.table.size.width {
let mut column_constrained = false;
let mut column_has_originating_cell = false;
self.rows = vec![RowLayout::default(); self.table.size.height];
self.columns = vec![ColumnLayout::default(); self.table.size.width];
for column_index in 0..self.table.size.width {
if let Some(column) = self.table.columns.get(column_index) {
if !column.style.box_size(writing_mode).inline.is_auto() {
self.columns[column_index].constrained = true;
continue;
}
if let Some(column_group_index) = column.group_index {
let column_group = &self.table.column_groups[column_group_index];
if !column_group.style.box_size(writing_mode).inline.is_auto() {
self.columns[column_index].constrained = true;
continue;
}
}
self.columns[column_index].constrained = false;
}
}
for row_index in 0..self.table.size.height {
if let Some(row) = self.table.rows.get(row_index) {
if !row.style.box_size(writing_mode).block.is_auto() {
self.rows[row_index].constrained = true;
continue;
}
if let Some(row_group_index) = row.group_index {
let row_group = &self.table.row_groups[row_group_index];
if !row_group.style.box_size(writing_mode).block.is_auto() {
self.rows[row_index].constrained = true;
continue;
}
}
}
self.rows[row_index].constrained = false;
}
for column_index in 0..self.table.size.width {
for row_index in 0..self.table.size.height {
let coords = TableSlotCoordinates::new(column_index, row_index);
let cell_constrained = match self.table.resolve_first_cell(coords) {
@ -217,13 +309,20 @@ impl<'a> TableLayout<'a> {
.unwrap_or(false),
_ => false,
};
column_has_originating_cell = column_has_originating_cell ||
let rowspan_greater_than_1 = match self.table.slots[row_index][column_index] {
TableSlot::Cell(ref cell) => cell.rowspan > 1,
_ => false,
};
self.rows[row_index].has_cell_with_span_greater_than_one |= rowspan_greater_than_1;
self.rows[row_index].constrained |= cell_constrained;
let has_originating_cell =
matches!(self.table.get_slot(coords), Some(TableSlot::Cell(_)));
column_constrained = column_constrained || cell_constrained;
self.columns[column_index].has_originating_cells |= has_originating_cell;
self.columns[column_index].constrained |= cell_constrained;
}
self.column_constrainedness.push(column_constrained);
self.column_has_originating_cell
.push(column_has_originating_cell);
}
}
@ -281,16 +380,12 @@ impl<'a> TableLayout<'a> {
// This takes the max of `min_content`, `max_content`, and
// intrinsic percentage width as described above.
let cell_measure = &self.cell_measures[row_index][column_index];
let cell_measure = &self.cell_measures[row_index][column_index].inline;
column_measure
.content_sizes
.max_assign(cell_measure.content_sizes);
column_measure.percentage_width = Percentage(
column_measure
.percentage_width
.0
.max(cell_measure.percentage_width.0),
);
column_measure.percentage =
Percentage(column_measure.percentage.0.max(cell_measure.percentage.0));
}
column_measures.push(column_measure);
@ -314,11 +409,11 @@ impl<'a> TableLayout<'a> {
for column_index in 0..self.table.size.width {
let column_measure = &mut column_measures[column_index];
let final_intrinsic_percentage_width = column_measure
.percentage_width
.percentage
.0
.min(100. - total_intrinsic_percentage_width);
total_intrinsic_percentage_width += final_intrinsic_percentage_width;
column_measure.percentage_width = Percentage(final_intrinsic_percentage_width);
column_measure.percentage = Percentage(final_intrinsic_percentage_width);
}
self.column_measures = column_measures;
@ -327,8 +422,8 @@ impl<'a> TableLayout<'a> {
fn compute_content_sizes_for_columns_with_span_up_to_n(
&self,
n: usize,
old_column_measures: &[CellOrColumnMeasure],
) -> (usize, Vec<CellOrColumnMeasure>) {
old_column_measures: &[CellOrTrackMeasure],
) -> (usize, Vec<CellOrTrackMeasure>) {
let mut next_span_n = usize::MAX;
let mut new_content_sizes_for_columns = Vec::new();
let border_spacing = self.table.border_spacing();
@ -354,7 +449,8 @@ impl<'a> TableLayout<'a> {
_ => continue,
};
let cell_measures = &self.cell_measures[resolved_coords.y][resolved_coords.x];
let cell_measures =
&self.cell_measures[resolved_coords.y][resolved_coords.x].inline;
let cell_inline_content_sizes = cell_measures.content_sizes;
let columns_spanned = resolved_coords.x..resolved_coords.x + cell.colspan;
@ -485,9 +581,7 @@ impl<'a> TableLayout<'a> {
// > Otherwise, it is the largest of the contributions of the cells in the column
// > whose colSpan is N, where the contribution of a cell is the result of taking
// > the following steps:
if old_column_measure.percentage_width.0 <= 0. &&
cell_measures.percentage_width.0 != 0.
{
if old_column_measure.percentage.0 <= 0. && cell_measures.percentage.0 != 0. {
// > 1. Start with the percentage contribution of the cell.
// > 2. Subtract the intrinsic percentage width of the column based on cells
// > of span up to N-1 of all columns that the cell spans. If this gives a
@ -496,13 +590,13 @@ impl<'a> TableLayout<'a> {
let other_column_percentages_sum =
(columns_spanned).fold(0., |sum, spanned_column_index| {
let spanned_column_percentage =
old_column_measures[spanned_column_index].percentage_width;
old_column_measures[spanned_column_index].percentage;
if spanned_column_percentage.0 == 0. {
spanned_columns_with_zero += 1;
}
sum + spanned_column_percentage.0
});
let step_2 = (cell_measures.percentage_width -
let step_2 = (cell_measures.percentage -
Percentage(other_column_percentages_sum))
.clamp_to_non_negative();
@ -521,9 +615,9 @@ impl<'a> TableLayout<'a> {
Percentage(new_column_intrinsic_percentage_width.0.max(step_3));
}
}
new_content_sizes_for_columns.push(CellOrColumnMeasure {
new_content_sizes_for_columns.push(CellOrTrackMeasure {
content_sizes: new_column_content_sizes,
percentage_width: new_column_intrinsic_percentage_width,
percentage: new_column_intrinsic_percentage_width,
});
}
(next_span_n, new_content_sizes_for_columns)
@ -636,16 +730,14 @@ impl<'a> TableLayout<'a> {
let column_measure = &self.column_measures[column_idx];
let min_content_width = column_measure.content_sizes.min_content;
let max_content_width = column_measure.content_sizes.max_content;
let constrained = self.column_constrainedness[column_idx];
let constrained = self.columns[column_idx].constrained;
let (
min_content_percentage_sizing_guess,
min_content_specified_sizing_guess,
max_content_sizing_guess,
) = if !column_measure.percentage_width.is_zero() {
let resolved = self
.assignable_width
.scale_by(column_measure.percentage_width.0);
) = if !column_measure.percentage.is_zero() {
let resolved = self.assignable_width.scale_by(column_measure.percentage.0);
let percent_guess = min_content_width.max(resolved);
(percent_guess, percent_guess, percent_guess)
} else if constrained {
@ -746,11 +838,11 @@ impl<'a> TableLayout<'a> {
let extra_inline_size = self.assignable_width - column_sizes_sum;
let has_originating_cells =
|column_index: &usize| self.column_has_originating_cell[*column_index];
let is_constrained = |column_index: &usize| self.column_constrainedness[*column_index];
|column_index: &usize| self.columns[*column_index].has_originating_cells;
let is_constrained = |column_index: &usize| self.columns[*column_index].constrained;
let is_unconstrained = |column_index: &usize| !is_constrained(column_index);
let has_percent_greater_than_zero =
|column_index: &usize| self.column_measures[*column_index].percentage_width.0 > 0.;
|column_index: &usize| self.column_measures[*column_index].percentage.0 > 0.;
let has_percent_zero = |column_index: &usize| !has_percent_greater_than_zero(column_index);
let has_max_content = |column_index: &usize| {
self.column_measures[*column_index]
@ -846,13 +938,12 @@ impl<'a> TableLayout<'a> {
let columns_with_percentage = all_columns.clone().filter(has_percent_greater_than_zero);
let total_percent = columns_with_percentage
.clone()
.map(|column_index| self.column_measures[column_index].percentage_width.0)
.map(|column_index| self.column_measures[column_index].percentage.0)
.sum::<f32>();
if total_percent > 0. {
for column_index in columns_with_percentage {
column_sizes[column_index] += extra_inline_size.scale_by(
self.column_measures[column_index].percentage_width.0 / total_percent,
);
column_sizes[column_index] += extra_inline_size
.scale_by(self.column_measures[column_index].percentage.0 / total_percent);
}
return;
}
@ -882,7 +973,7 @@ impl<'a> TableLayout<'a> {
/// This is an implementation of *Row layout (first pass)* from
/// <https://drafts.csswg.org/css-tables/#row-layout>.
fn do_row_layout_first_pass(
fn layout_cells_in_row(
&mut self,
layout_context: &LayoutContext,
containing_block: &ContainingBlock,
@ -930,86 +1021,349 @@ impl<'a> TableLayout<'a> {
&mut positioning_context,
&containing_block_for_children,
);
let content_size_from_layout = ContentSizes {
min_content: layout.content_block_size,
max_content: layout.content_block_size,
};
self.cell_measures[row_index][column_index]
.block
.content_sizes
.max_assign(content_size_from_layout);
cells_laid_out_row.push(Some(CellLayout {
layout,
padding,
border,
positioning_context,
}))
}));
}
self.cells_laid_out.push(cells_laid_out_row);
}
}
fn distribute_height_to_rows(&mut self) {
/// Do the first layout of a table row, after laying out the cells themselves. This is
/// more or less and implementation of <https://drafts.csswg.org/css-tables/#row-layout>.
fn do_first_row_layout(&mut self, writing_mode: WritingMode) -> Vec<Au> {
let mut row_sizes = (0..self.table.size.height)
.map(|row_index| {
let (mut max_ascent, mut max_descent, mut max_row_height) =
(Au::zero(), Au::zero(), Au::zero());
for column_index in 0..self.table.size.width {
let cell = match self.table.slots[row_index][column_index] {
TableSlot::Cell(ref cell) => cell,
_ => continue,
};
let layout = match self.cells_laid_out[row_index][column_index] {
Some(ref layout) => layout,
None => {
warn!(
"Did not find a layout at a slot index with an originating cell."
);
continue;
},
};
let outer_block_size = layout.outer_block_size();
if cell.rowspan == 1 {
max_row_height = max_row_height.max(outer_block_size);
}
if cell.effective_vertical_align() == VerticalAlignKeyword::Baseline {
let ascent = layout.ascent();
let border_padding_start =
layout.border.block_start + layout.padding.block_start;
let border_padding_end = layout.border.block_end + layout.padding.block_end;
max_ascent = max_ascent.max(ascent + border_padding_start.into());
// Only take into account the descent of this cell if doesn't span
// rows. The descent portion of the cell in cells that do span rows
// may extend into other rows.
if cell.rowspan == 1 {
max_descent = max_descent.max(
layout.layout.content_block_size - ascent +
border_padding_end.into(),
);
}
}
}
self.row_baselines.push(max_ascent);
max_row_height.max(max_ascent + max_descent)
})
.collect();
self.calculate_row_sizes_after_first_layout(&mut row_sizes, writing_mode);
row_sizes
}
/// After doing layout of table rows, calculate final row size and distribute space across
/// rowspanned cells. This follows the implementation of LayoutNG and the priority
/// agorithm described at <https://github.com/w3c/csswg-drafts/issues/4418>.
fn calculate_row_sizes_after_first_layout(
&mut self,
row_sizes: &mut Vec<Au>,
writing_mode: WritingMode,
) {
let mut cells_to_distribute = Vec::new();
let mut total_percentage = 0.;
for row_index in 0..self.table.size.height {
let (mut max_ascent, mut max_descent, mut max_row_height) =
(Au::zero(), Au::zero(), Au::zero());
let row_measure = self
.table
.get_row_measure_for_row_at_index(writing_mode, row_index);
row_sizes[row_index] = row_sizes[row_index].max(row_measure.content_sizes.min_content);
let mut percentage = match self.table.rows.get(row_index) {
Some(row) => {
get_size_percentage_contribution_from_style(&row.style, writing_mode)
.block
.0
},
None => 0.,
};
for column_index in 0..self.table.size.width {
let coords = TableSlotCoordinates::new(column_index, row_index);
let cell_percentage = self.cell_measures[row_index][column_index]
.block
.percentage
.0;
percentage = percentage.max(cell_percentage);
let cell_measure = &self.cell_measures[row_index][column_index].block;
let cell = match self.table.slots[row_index][column_index] {
TableSlot::Cell(ref cell) => cell,
TableSlot::Spanned(ref spanned_cells) if spanned_cells[0].y != 0 => {
let offset = spanned_cells[0];
let origin = coords - offset;
// We only allocate the remaining space for the last row of the rowspanned cell.
if let Some(TableSlot::Cell(origin_cell)) = self.table.get_slot(origin) {
if origin_cell.rowspan != offset.y + 1 {
continue;
}
}
// This is all of the rows that are spanned except this one.
let used_block_size = (origin.y..coords.y)
.map(|row_index| self.row_sizes[row_index])
.fold(Au::zero(), |sum, size| sum + size);
if let Some(layout) = &self.cells_laid_out[origin.y][origin.x] {
max_row_height =
max_row_height.max(layout.outer_block_size() - used_block_size);
}
TableSlot::Cell(ref cell) if cell.rowspan > 1 => cell,
TableSlot::Cell(_) => {
// If this is an originating cell, that isn't spanning, then we make sure the row is
// at least big enough to hold the cell.
row_sizes[row_index] =
row_sizes[row_index].max(cell_measure.content_sizes.max_content);
continue;
},
_ => continue,
};
let layout = match self.cells_laid_out[row_index][column_index] {
Some(ref layout) => layout,
None => {
warn!("Did not find a layout at a slot index with an originating cell.");
continue;
},
};
cells_to_distribute.push(RowspanToDistribute {
coordinates: TableSlotCoordinates::new(column_index, row_index),
cell,
measure: cell_measure,
});
}
let outer_block_size = layout.outer_block_size();
if cell.rowspan == 1 {
max_row_height = max_row_height.max(outer_block_size);
}
self.rows[row_index].percent = Percentage(percentage.min(100. - total_percentage));
total_percentage += self.rows[row_index].percent.0;
}
if cell.effective_vertical_align() == VerticalAlignKeyword::Baseline {
let ascent = layout.ascent();
let border_padding_start =
layout.border.block_start + layout.padding.block_start;
let border_padding_end = layout.border.block_end + layout.padding.block_end;
max_ascent = max_ascent.max(ascent + border_padding_start.into());
cells_to_distribute.sort_by(|a, b| {
if a.range() == b.range() {
return a
.measure
.content_sizes
.min_content
.cmp(&b.measure.content_sizes.min_content);
}
if a.fully_encloses(b) {
return std::cmp::Ordering::Greater;
}
if b.fully_encloses(a) {
return std::cmp::Ordering::Less;
}
a.coordinates.y.cmp(&b.coordinates.y)
});
// Only take into account the descent of this cell if doesn't span
// rows. The descent portion of the cell in cells that do span rows
// will be allocated to the other rows that it spans.
if cell.rowspan == 1 {
max_descent = max_descent.max(
layout.layout.content_block_size - ascent + border_padding_end.into(),
);
for rowspan_to_distribute in cells_to_distribute {
let rows_spanned = rowspan_to_distribute.range();
let current_rows_size: Au = rows_spanned.clone().map(|index| row_sizes[index]).sum();
let border_spacing_spanned =
self.table.border_spacing().block * (rows_spanned.len() - 1) as i32;
let excess_size = (rowspan_to_distribute.measure.content_sizes.min_content -
current_rows_size -
border_spacing_spanned)
.max(Au::zero());
self.distribute_extra_size_to_rows(
excess_size,
rows_spanned,
row_sizes,
None,
true, /* rowspan_distribution */
);
}
}
/// An implementation of the same extra block size distribution algorithm used in
/// LayoutNG and described at <https://github.com/w3c/csswg-drafts/issues/4418>.
fn distribute_extra_size_to_rows(
&self,
mut excess_size: Au,
track_range: Range<usize>,
track_sizes: &mut Vec<Au>,
percentage_resolution_size: Option<Au>,
rowspan_distribution: bool,
) {
if excess_size.is_zero() {
return;
}
let is_constrained = |track_index: &usize| self.rows[*track_index].constrained;
let is_unconstrained = |track_index: &usize| !is_constrained(track_index);
let is_empty: Vec<bool> = track_sizes.iter().map(|size| size.is_zero()).collect();
let is_not_empty = |track_index: &usize| !is_empty[*track_index];
let other_row_that_starts_a_rowspan = |track_index: &usize| {
*track_index != track_range.start &&
self.rows[*track_index].has_cell_with_span_greater_than_one
};
// If we have a table height (not during rowspan distribution), first distribute to rows
// that have percentage sizes proportionally to the size missing to reach the percentage
// of table height required.
if let Some(percentage_resolution_size) = percentage_resolution_size {
let get_percent_block_size_deficit = |row_index: usize, track_size: Au| {
let size_needed_for_percent =
percentage_resolution_size.scale_by(self.rows[row_index].percent.0 / 100.);
(size_needed_for_percent - track_size).max(Au::zero())
};
let percent_block_size_deficit: Au = track_range
.clone()
.map(|index| get_percent_block_size_deficit(index, track_sizes[index]))
.sum();
let percent_distributable_block_size = percent_block_size_deficit.min(excess_size);
if percent_distributable_block_size > Au::zero() {
for track_index in track_range.clone() {
let row_deficit =
get_percent_block_size_deficit(track_index, track_sizes[track_index]);
if row_deficit > Au::zero() {
let ratio =
row_deficit.to_f32_px() / percent_block_size_deficit.to_f32_px();
let size = percent_distributable_block_size.scale_by(ratio);
track_sizes[track_index] += size;
excess_size -= size;
}
}
}
self.row_baselines.push(max_ascent);
self.row_sizes
.push(max_row_height.max(max_ascent + max_descent));
}
// If this is rowspan distribution and there are rows other than the first row that have a
// cell with rowspan > 1, distribute the extra space equally to those rows.
if rowspan_distribution {
let rows_that_start_rowspan: Vec<usize> = track_range
.clone()
.filter(other_row_that_starts_a_rowspan)
.collect();
if !rows_that_start_rowspan.is_empty() {
let scale = 1.0 / rows_that_start_rowspan.len() as f32;
for track_index in rows_that_start_rowspan.iter() {
track_sizes[*track_index] += excess_size.scale_by(scale);
}
return;
}
}
// If there are unconstrained non-empty rows, grow them all proportionally to their current size.
let unconstrained_non_empty_rows: Vec<usize> = track_range
.clone()
.filter(is_unconstrained)
.filter(is_not_empty)
.collect();
if !unconstrained_non_empty_rows.is_empty() {
let total_size: Au = unconstrained_non_empty_rows
.iter()
.map(|index| track_sizes[*index])
.sum();
for track_index in unconstrained_non_empty_rows.iter() {
let scale = track_sizes[*track_index].to_f32_px() / total_size.to_f32_px();
track_sizes[*track_index] += excess_size.scale_by(scale);
}
return;
}
let (non_empty_rows, empty_rows): (Vec<usize>, Vec<usize>) =
track_range.clone().partition(is_not_empty);
let only_have_empty_rows = empty_rows.len() == track_range.len();
if !empty_rows.is_empty() {
// If this is rowspan distribution and there are only empty rows, just grow the
// last one.
if rowspan_distribution && only_have_empty_rows {
track_sizes[*empty_rows.last().unwrap()] += excess_size;
return;
}
// Otherwise, if we only have empty rows or if all the non-empty rows are constrained,
// then grow the empty rows.
let non_empty_rows_all_constrained = !non_empty_rows.iter().any(is_unconstrained);
if only_have_empty_rows || non_empty_rows_all_constrained {
// If there are both unconstrained and constrained empty rows, only increase the
// size of the unconstrained ones, otherwise increase the size of all empty rows.
let mut rows_to_grow = &empty_rows;
let unconstrained_empty_rows: Vec<usize> = rows_to_grow
.iter()
.copied()
.filter(is_unconstrained)
.collect();
if !unconstrained_empty_rows.is_empty() {
rows_to_grow = &unconstrained_empty_rows;
}
// All empty rows that will grow equally.
let scale = 1.0 / rows_to_grow.len() as f32;
for track_index in rows_to_grow.iter() {
track_sizes[*track_index] += excess_size.scale_by(scale);
}
return;
}
}
// If there are non-empty rows, they all grow in proportion to their current size,
// whether or not they are constrained.
if !non_empty_rows.is_empty() {
let total_size: Au = non_empty_rows.iter().map(|index| track_sizes[*index]).sum();
for track_index in non_empty_rows.iter() {
let scale = track_sizes[*track_index].to_f32_px() / total_size.to_f32_px();
track_sizes[*track_index] += excess_size.scale_by(scale);
}
}
}
/// Given computed row sizes, compute the final block size of the table and distribute extra
/// block size to table rows.
fn compute_table_height_and_final_row_heights(
&mut self,
mut row_sizes: Vec<Au>,
containing_block: &ContainingBlock,
) {
// The table content height is the maximum of the computed table height from style and the
// sum of computed row heights from row layout plus size from borders and spacing.
let table_height_from_style = self
.table
.style
.content_box_size(containing_block, &self.pbm)
.block
.auto_is(Length::zero)
.into();
let border_spacing = self.table.border_spacing();
let border_and_spacing = self.pbm.border.block_sum() +
border_spacing.block * (self.table.size.height as i32 + 1);
let table_height_from_rows = row_sizes.iter().sum::<Au>() + border_and_spacing;
let final_table_height = table_height_from_rows.max(table_height_from_style);
// If the table height is defined by the rows sizes, there is no extra space to distribute
// to rows.
if final_table_height == table_height_from_rows {
self.row_sizes = row_sizes;
return;
}
// There was extra block size added to the table from the table style, so distribute this
// extra space to rows using the same distribution algorithm used for distributing rowspan
// space.
// TODO: This should first distribute space to row groups and then to rows.
self.distribute_extra_size_to_rows(
final_table_height - table_height_from_rows,
0..self.table.size.height,
&mut row_sizes,
Some(final_table_height),
false, /* rowspan_distribution */
);
self.row_sizes = row_sizes;
}
/// Lay out the table of this [`TableLayout`] into fragments. This should only be be called
@ -1372,17 +1726,17 @@ impl Table {
&self,
writing_mode: WritingMode,
column_index: usize,
) -> CellOrColumnMeasure {
) -> CellOrTrackMeasure {
let column = match self.columns.get(column_index) {
Some(column) => column,
None => return CellOrColumnMeasure::zero(),
None => return CellOrTrackMeasure::zero(),
};
let (size, min_size, max_size) = get_sizes_from_style(&column.style, writing_mode);
let percentage_contribution =
get_size_percentage_contribution_from_style(&column.style, writing_mode);
CellOrColumnMeasure {
CellOrTrackMeasure {
content_sizes: ContentSizes {
// > The outer min-content width of a table-column or table-column-group is
// > max(min-width, width).
@ -1391,7 +1745,34 @@ impl Table {
// > max(min-width, min(max-width, width)).
max_content: min_size.inline.max(max_size.inline.min(size.inline)),
},
percentage_width: percentage_contribution.inline,
percentage: percentage_contribution.inline,
}
}
fn get_row_measure_for_row_at_index(
&self,
writing_mode: WritingMode,
row_index: usize,
) -> CellOrTrackMeasure {
let row = match self.rows.get(row_index) {
Some(row) => row,
None => return CellOrTrackMeasure::zero(),
};
let (size, min_size, max_size) = get_sizes_from_style(&row.style, writing_mode);
let percentage_contribution =
get_size_percentage_contribution_from_style(&row.style, writing_mode);
CellOrTrackMeasure {
content_sizes: ContentSizes {
// > The outer min-content width of a table-column or table-column-group is
// > max(min-width, width).
min_content: min_size.inline.max(size.block),
// > The outer max-content width of a table-column or table-column-group is
// > max(min-width, min(max-width, width)).
max_content: min_size.inline.max(max_size.block.min(size.block)),
},
percentage: percentage_contribution.block,
}
}
@ -1579,3 +1960,19 @@ fn get_sizes_from_style(
(size, min_size, max_size)
}
struct RowspanToDistribute<'a> {
coordinates: TableSlotCoordinates,
cell: &'a TableSlotCell,
measure: &'a CellOrTrackMeasure,
}
impl<'a> RowspanToDistribute<'a> {
fn range(&self) -> Range<usize> {
self.coordinates.y..self.coordinates.y + self.cell.rowspan
}
fn fully_encloses(&self, other: &RowspanToDistribute) -> bool {
other.coordinates.y > self.coordinates.y && other.range().end < self.range().end
}
}

View file

@ -1,2 +0,0 @@
[clear-applies-to-001.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[clear-applies-to-002.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[clear-applies-to-003.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[clear-applies-to-004.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[clear-applies-to-005.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[clear-applies-to-006.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[clear-applies-to-007.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[clear-applies-to-013.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[clear-applies-to-014.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[float-applies-to-013.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[float-applies-to-014.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[height-table-cell-001.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[padding-applies-to-013a.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[row-visibility-002.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[separated-border-model-004e.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[table-cell-001.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[table-height-algorithm-008a.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[table-height-algorithm-008b.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[table-height-algorithm-008c.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[table-visual-layout-026a.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[table-visual-layout-026b.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[table-visual-layout-026c.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[table-visual-layout-026d.xht]
expected: FAIL

View file

@ -1,12 +0,0 @@
[border-spacing-included-in-sizes-001.html]
[tbody 1]
expected: FAIL
[tbody 2]
expected: FAIL
[tbody 3]
expected: FAIL
[tfoot tr 4]
expected: FAIL

View file

@ -1,15 +0,0 @@
[bounding-box-computation-1.html]
[Table-cell is 100px tall]
expected: FAIL
[Table-row is 100px tall]
expected: FAIL
[Table-row-group is 100px tall]
expected: FAIL
[Table-column is 100px tall]
expected: FAIL
[Table-column-group is 100px tall]
expected: FAIL

View file

@ -1,3 +0,0 @@
[chrome-rowspan-bug.html]
[table tracks correct use of rowspan]
expected: FAIL

View file

@ -2,9 +2,6 @@
[main table 1]
expected: FAIL
[main table 2]
expected: FAIL
[main table 4]
expected: FAIL
@ -31,3 +28,6 @@
[main table 3]
expected: FAIL
[main table 2]
expected: FAIL

View file

@ -1,2 +0,0 @@
[fixup-dynamic-anonymous-table-001.html]
expected: FAIL

View file

@ -1,10 +1,4 @@
[computing-row-measure-0.html]
[Checking intermediate min-content height for span 1 (1)]
expected: FAIL
[Checking intermediate min-content height for span 1 (3)]
expected: FAIL
[Checking intermediate min-content height for span 1 (2)]
expected: FAIL

View file

@ -1,9 +1,9 @@
[computing-row-measure-1.html]
[Checking intermediate min-content width for span 2 (4)]
expected: FAIL
[Checking intermediate min-content width for span 2 (1)]
expected: FAIL
[Checking intermediate min-content width for span 2 (2)]
expected: FAIL
[Checking intermediate min-content width for span 2 (4)]
expected: FAIL

View file

@ -11,26 +11,14 @@
[Border-spacing is added between any two unmerged columns (2)]
expected: FAIL
[Border-spacing is added between any two unmerged rows (2)]
expected: FAIL
[Border-spacing is added between any two unmerged columns (3)]
expected: FAIL
[Border-spacing is added between any two unmerged rows (3)]
expected: FAIL
[Border-spacing is added between any two unmerged columns (4)]
expected: FAIL
[Border-spacing is added between any two unmerged rows (4)]
expected: FAIL
[Border-spacing is added between any two unmerged columns (5)]
expected: FAIL
[Border-spacing is added between any two unmerged rows (5)]
expected: FAIL
[Explicitely defined rows are not merged]
expected: FAIL

View file

@ -1,2 +0,0 @@
[col-change-span-bg-invalidation-002.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[subpixel-table-cell-height-001.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[subpixel-table-cell-width-002.html]
expected: FAIL

View file

@ -1,3 +0,0 @@
[table-cell-scroll-height.html]
[scrollHeight on scrollable table cell]
expected: FAIL

View file

@ -2,9 +2,6 @@
[2.1. An anonymous table-row box must be generated around each sequence of consecutive children of a table-root box which are not proper table child boxes. (1/2)]
expected: FAIL
[2.1. An anonymous table-row box must be generated around each sequence of consecutive children of a table-root box which are not proper table child boxes. (2/2)]
expected: FAIL
[2.2. An anonymous table-row box must be generated around each sequence of consecutive children of a table-row-grouping box which are not table-row boxes. (1/3)]
expected: FAIL

View file

@ -2,14 +2,5 @@
[table, .display-table 1]
expected: FAIL
[table, .display-table 2]
expected: FAIL
[table, .display-table 3]
expected: FAIL
[table, .display-table 4]
expected: FAIL
[table, .display-table 5]
expected: FAIL

View file

@ -14,9 +14,6 @@
[table 6]
expected: FAIL
[table 7]
expected: FAIL
[table 9]
expected: FAIL
@ -85,3 +82,6 @@
[table 8]
expected: FAIL
[table 7]
expected: FAIL

View file

@ -1,4 +1,16 @@
[rowspan-height-redistribution.html]
[table 7]
expected: FAIL
[table 8]
expected: FAIL
[table 22]
expected: FAIL
[table 23]
expected: FAIL
[table 2]
expected: FAIL
@ -14,38 +26,41 @@
[table 6]
expected: FAIL
[table 7]
expected: FAIL
[table 8]
expected: FAIL
[table 9]
expected: FAIL
[table 10]
expected: FAIL
[table 11]
expected: FAIL
[table 12]
expected: FAIL
[table 13]
expected: FAIL
[table 14]
expected: FAIL
[table 15]
expected: FAIL
[table 16]
expected: FAIL
[table 17]
expected: FAIL
[table 18]
expected: FAIL
[table 19]
[table 20]
expected: FAIL
[table 21]
expected: FAIL
[table 22]
expected: FAIL
[table 23]
expected: FAIL
[table 20]
[table 24]
expected: FAIL

View file

@ -56,9 +56,6 @@
[table 23]
expected: FAIL
[table 24]
expected: FAIL
[table 25]
expected: FAIL

View file

@ -14,9 +14,6 @@
[table 6]
expected: FAIL
[table 7]
expected: FAIL
[table 9]
expected: FAIL
@ -37,3 +34,6 @@
[table 15]
expected: FAIL
[table 7]
expected: FAIL

View file

@ -26,9 +26,6 @@
[table 11]
expected: FAIL
[table 12]
expected: FAIL
[table 14]
expected: FAIL
@ -58,3 +55,6 @@
[table 26]
expected: FAIL
[table 12]
expected: FAIL

View file

@ -14,9 +14,6 @@
[table 5]
expected: FAIL
[table 6]
expected: FAIL
[table 7]
expected: FAIL

View file

@ -11,9 +11,6 @@
[table 6]
expected: FAIL
[table 7]
expected: FAIL
[table 8]
expected: FAIL

View file

@ -5,9 +5,6 @@
[table 9]
expected: FAIL
[table 11]
expected: FAIL
[table 5]
expected: FAIL

View file

@ -0,0 +1,6 @@
[visibility-collapse-row-005.html]
[collapsed row should not contribute to overflow]
expected: FAIL
[collapsed section should not contribute to overflow]
expected: FAIL

View file

@ -1,12 +1,6 @@
[computing-column-measure-1.html]
[Checking intermediate min-content height for span 2 (1)]
[Checking intermediate min-content height for span 2 (4)]
expected: FAIL
[Checking intermediate min-content height for span 2 (2)]
expected: FAIL
[Checking intermediate min-content height for span 2 (3)]
expected: FAIL
[Checking intermediate min-content height for span 2 (4)]
expected: FAIL