layout: Improve distribution colspan cell inline size (#35095)

We previously tried to implement the [table specification algorithm] for
distributing the inline size of cells with `rowspan` > 1. This algorithm
isn't great though, so this change starts switching Servo to using an
algorithm like the one used in LayoutNG from blink. This leads to
improvements in test results.

Limitations:
 - Currently, non-fixed layout mode is handled, but a followup change will
   very likely addressed fixed mode tables.
 - Column merging is not handled at all.

Fixes #6578.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Co-authored-by: Oriol Brufau <obrufau@igalia.com>
This commit is contained in:
Martin Robinson 2025-01-21 14:29:55 +01:00 committed by GitHub
parent d00d76c1e8
commit c17668bb0e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 282 additions and 417 deletions

View file

@ -6,7 +6,7 @@ use core::cmp::Ordering;
use std::mem; use std::mem;
use std::ops::Range; use std::ops::Range;
use app_units::{Au, MAX_AU}; use app_units::Au;
use log::warn; use log::warn;
use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator}; use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator};
use servo_arc::Arc; use servo_arc::Arc;
@ -99,7 +99,24 @@ struct ColumnLayout {
constrained: bool, constrained: bool,
has_originating_cells: bool, has_originating_cells: bool,
content_sizes: ContentSizes, content_sizes: ContentSizes,
percentage: Percentage, percentage: Option<Percentage>,
}
fn max_two_optional_percentages(
a: Option<Percentage>,
b: Option<Percentage>,
) -> Option<Percentage> {
match (a, b) {
(Some(a), Some(b)) => Some(Percentage(a.0.max(b.0))),
_ => a.or(b),
}
}
impl ColumnLayout {
fn incorporate_cell_measure(&mut self, cell_measure: &CellOrTrackMeasure) {
self.content_sizes.max_assign(cell_measure.content_sizes);
self.percentage = max_two_optional_percentages(self.percentage, cell_measure.percentage);
}
} }
impl CollapsedBorder { impl CollapsedBorder {
@ -200,19 +217,19 @@ pub(crate) struct TableLayout<'a> {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
struct CellOrTrackMeasure { struct CellOrTrackMeasure {
content_sizes: ContentSizes, content_sizes: ContentSizes,
percentage: Percentage, percentage: Option<Percentage>,
} }
impl Zero for CellOrTrackMeasure { impl Zero for CellOrTrackMeasure {
fn zero() -> Self { fn zero() -> Self {
Self { Self {
content_sizes: ContentSizes::zero(), content_sizes: ContentSizes::zero(),
percentage: Percentage(0.), percentage: None,
} }
} }
fn is_zero(&self) -> bool { fn is_zero(&self) -> bool {
self.content_sizes.is_zero() && self.percentage.is_zero() self.content_sizes.is_zero() && self.percentage.is_none()
} }
} }
@ -280,12 +297,17 @@ impl<'a> TableLayout<'a> {
block: padding.block_sum() + border.block_sum(), block: padding.block_sum() + border.block_sum(),
}; };
let (size, min_size, max_size, inline_size_is_auto, percentage_contribution) = let CellOrColumnOuterSizes {
get_outer_sizes_for_measurement( preferred: preferred_size,
&cell.base.style, min: min_size,
writing_mode, max: max_size,
&padding_border_sums, inline_preferred_size_is_auto,
); percentage: percentage_size,
} = CellOrColumnOuterSizes::new(
&cell.base.style,
writing_mode,
&padding_border_sums,
);
// <https://drafts.csswg.org/css-tables/#in-fixed-mode> // <https://drafts.csswg.org/css-tables/#in-fixed-mode>
// > When a table-root is laid out in fixed mode, the content of its table-cells is ignored // > When a table-root is laid out in fixed mode, the content of its table-cells is ignored
@ -305,31 +327,30 @@ impl<'a> TableLayout<'a> {
// These formulas differ from the spec, but seem to match Gecko and Blink. // These formulas differ from the spec, but seem to match Gecko and Blink.
let outer_min_content_width = if is_in_fixed_mode { let outer_min_content_width = if is_in_fixed_mode {
if inline_size_is_auto { if inline_preferred_size_is_auto {
// This is an outer size, but we deliberately ignore borders and padding. // This is an outer size, but we deliberately ignore borders and padding.
// This is like allowing the content-box width to be negative. // This is like allowing the content-box width to be negative.
Au::zero() Au::zero()
} else { } else {
size.inline.min(max_size.inline).max(min_size.inline) preferred_size
.inline
.clamp_between_extremums(min_size.inline, max_size.inline)
} }
} else { } else {
inline_content_sizes inline_content_sizes
.min_content .min_content
.min(max_size.inline) .clamp_between_extremums(min_size.inline, max_size.inline)
.max(min_size.inline)
}; };
let outer_max_content_width = if self.columns[column_index].constrained { let outer_max_content_width = if self.columns[column_index].constrained {
inline_content_sizes inline_content_sizes
.min_content .min_content
.max(size.inline) .max(preferred_size.inline)
.min(max_size.inline) .clamp_between_extremums(min_size.inline, max_size.inline)
.max(min_size.inline)
} else { } else {
inline_content_sizes inline_content_sizes
.max_content .max_content
.max(size.inline) .max(preferred_size.inline)
.min(max_size.inline) .clamp_between_extremums(min_size.inline, max_size.inline)
.max(min_size.inline)
}; };
assert!(outer_min_content_width <= outer_max_content_width); assert!(outer_min_content_width <= outer_max_content_width);
@ -338,7 +359,7 @@ impl<'a> TableLayout<'a> {
min_content: outer_min_content_width, min_content: outer_min_content_width,
max_content: outer_max_content_width, max_content: outer_max_content_width,
}, },
percentage: percentage_contribution.inline, percentage: percentage_size.inline,
} }
}; };
@ -346,8 +367,8 @@ impl<'a> TableLayout<'a> {
// These sizes are incorporated after the first row layout pass, when the block size // These sizes are incorporated after the first row layout pass, when the block size
// of the layout is known. // of the layout is known.
let block_measure = CellOrTrackMeasure { let block_measure = CellOrTrackMeasure {
content_sizes: size.block.into(), content_sizes: preferred_size.block.into(),
percentage: percentage_contribution.block, percentage: percentage_size.block,
}; };
self.cell_measures[row_index][column_index] = LogicalVec2 { self.cell_measures[row_index][column_index] = LogicalVec2 {
@ -465,7 +486,7 @@ impl<'a> TableLayout<'a> {
// //
// TODO: Take into account `table-column` and `table-column-group` lengths. // TODO: Take into account `table-column` and `table-column-group` lengths.
// TODO: Take into account changes to this computation for fixed table layout. // TODO: Take into account changes to this computation for fixed table layout.
let mut next_span_n = usize::MAX; let mut colspan_cell_constraints = Vec::new();
for column_index in 0..self.table.size.width { for column_index in 0..self.table.size.width {
let column = &mut self.columns[column_index]; let column = &mut self.columns[column_index];
@ -477,31 +498,34 @@ impl<'a> TableLayout<'a> {
for row_index in 0..self.table.size.height { for row_index in 0..self.table.size.height {
let coords = TableSlotCoordinates::new(column_index, row_index); let coords = TableSlotCoordinates::new(column_index, row_index);
match self.table.resolve_first_cell(coords) { let cell_measure = &self.cell_measures[row_index][column_index].inline;
Some(cell) if cell.colspan == 1 => cell,
Some(cell) => { let cell = match self.table.get_slot(coords) {
next_span_n = next_span_n.min(cell.colspan); Some(TableSlot::Cell(cell)) => cell,
continue;
},
_ => continue, _ => continue,
}; };
if cell.colspan != 1 {
colspan_cell_constraints.push(ColspanToDistribute {
starting_column: column_index,
span: cell.colspan,
content_sizes: cell_measure.content_sizes,
percentage: cell_measure.percentage,
});
continue;
}
// This takes the max of `min_content`, `max_content`, and // This takes the max of `min_content`, `max_content`, and
// intrinsic percentage width as described above. // intrinsic percentage width as described above.
let cell_measure = &self.cell_measures[row_index][column_index].inline; column.incorporate_cell_measure(cell_measure);
column.content_sizes.max_assign(cell_measure.content_sizes);
column.percentage =
Percentage(column_measure.percentage.0.max(cell_measure.percentage.0));
} }
} }
// Now we have the base computation complete, so iteratively take into account cells // Sort the colspanned cell constraints by their span and starting column.
// with higher colspan. Using `next_span_n` we can skip over span counts that don't colspan_cell_constraints.sort_by(ColspanToDistribute::comparison_for_sort);
// correspond to any cells.
while next_span_n < usize::MAX { // Distribute constraints from cells with colspan != 1 to their component columns.
(next_span_n, self.columns) = self.distribute_colspanned_cells_to_columns(colspan_cell_constraints);
self.compute_content_sizes_for_columns_with_span_up_to_n(next_span_n);
}
// > intrinsic percentage width of a column: // > intrinsic percentage width of a column:
// > the smaller of: // > the smaller of:
@ -511,216 +535,98 @@ impl<'a> TableLayout<'a> {
// > the table (further left when direction is "ltr" (right for "rtl")) // > the table (further left when direction is "ltr" (right for "rtl"))
let mut total_intrinsic_percentage_width = 0.; let mut total_intrinsic_percentage_width = 0.;
for column in self.columns.iter_mut() { for column in self.columns.iter_mut() {
let final_intrinsic_percentage_width = column if let Some(ref mut percentage) = column.percentage {
.percentage let final_intrinsic_percentage_width =
.0 percentage.0.min(1. - total_intrinsic_percentage_width);
.min(1. - total_intrinsic_percentage_width); total_intrinsic_percentage_width += final_intrinsic_percentage_width;
total_intrinsic_percentage_width += final_intrinsic_percentage_width; *percentage = Percentage(final_intrinsic_percentage_width);
column.percentage = Percentage(final_intrinsic_percentage_width); }
} }
} }
fn compute_content_sizes_for_columns_with_span_up_to_n( fn distribute_colspanned_cells_to_columns(
&self, &mut self,
n: usize, colspan_cell_constraints: Vec<ColspanToDistribute>,
) -> (usize, Vec<ColumnLayout>) { ) {
let mut next_span_n = usize::MAX; for colspan_cell_constraints in colspan_cell_constraints {
let mut new_columns = Vec::new(); self.distribute_colspanned_cell_to_columns(colspan_cell_constraints);
let border_spacing = self.table.border_spacing(); }
}
for column_index in 0..self.table.size.width {
let old_column = &self.columns[column_index]; /// Distribute the inline size from a cell with colspan != 1 to the columns that it spans.
let mut new_column_content_sizes = old_column.content_sizes; /// This is heavily inspired by the approach that Chromium takes in redistributing colspan
let mut new_column_intrinsic_percentage_width = old_column.percentage; /// cells' inline size to columns (`DistributeColspanCellToColumnsAuto` in
/// `blink/renderer/core/layout/table/table_layout_utils.cc`).
for row_index in 0..self.table.size.height { fn distribute_colspanned_cell_to_columns(
let coords = TableSlotCoordinates::new(column_index, row_index); &mut self,
let resolved_coords = match self.table.resolve_first_cell_coords(coords) { colspan_cell_constraints: ColspanToDistribute,
Some(resolved_coords) => resolved_coords, ) {
None => continue, let border_spacing = self.table.border_spacing().inline;
}; let column_range = colspan_cell_constraints.range();
let column_count = column_range.len();
let cell = match self.table.resolve_first_cell(resolved_coords) { let total_border_spacing =
Some(cell) if cell.colspan <= n => cell, border_spacing.scale_by((colspan_cell_constraints.span - 1) as f32);
Some(cell) => {
next_span_n = next_span_n.min(cell.colspan); let mut percent_columns_count = 0;
continue; let mut columns_percent_sum = 0.;
}, let mut columns_non_percent_max_inline_size_sum = Au::zero();
_ => continue, for column in self.columns[column_range.clone()].iter() {
}; if let Some(percentage) = column.percentage {
percent_columns_count += 1;
let cell_measures = columns_percent_sum += percentage.0;
&self.cell_measures[resolved_coords.y][resolved_coords.x].inline; } else {
let cell_inline_content_sizes = cell_measures.content_sizes; columns_non_percent_max_inline_size_sum += column.content_sizes.max_content;
}
let columns_spanned = resolved_coords.x..resolved_coords.x + cell.colspan; }
let baseline_content_sizes: ContentSizes = columns_spanned.clone().fold(
ContentSizes::zero(), let colspan_percentage = colspan_cell_constraints.percentage.unwrap_or_default();
|total: ContentSizes, spanned_column_index| { let surplus_percent = colspan_percentage.0 - columns_percent_sum;
total + self.columns[spanned_column_index].content_sizes if surplus_percent > 0. && column_count > percent_columns_count {
}, for column in self.columns[column_range.clone()].iter_mut() {
); if column.percentage.is_some() {
continue;
let old_column_content_size = old_column.content_sizes; }
// > **min-content width of a column based on cells of span up to N (N > 1)** let ratio = if columns_non_percent_max_inline_size_sum.is_zero() {
// > 1. / ((column_count - percent_columns_count) as f32)
// > the largest of the min-content width of the column based on cells of span up to } else {
// > N-1 and the contributions of the cells in the column whose colSpan is N, where column.content_sizes.max_content.to_f32_px() /
// > the contribution of a cell is the result of taking the following steps: columns_non_percent_max_inline_size_sum.to_f32_px()
// > };
// > 1. Define the baseline min-content width as the sum of the max-content column.percentage = Some(Percentage(surplus_percent * ratio));
// > widths based on cells of span up to N-1 of all columns that the cell spans. }
// }
// Note: This definition is likely a typo, so we use the sum of the min-content
// widths here instead. let colspan_cell_min_size = (colspan_cell_constraints.content_sizes.min_content -
let baseline_min_content_width = baseline_content_sizes.min_content; total_border_spacing)
let baseline_max_content_width = baseline_content_sizes.max_content; .max(Au::zero());
let distributed_minimum = Self::distribute_width_to_columns(
// > 2. Define the baseline border spacing as the sum of the horizontal colspan_cell_min_size,
// > border-spacing for any columns spanned by the cell, other than the one in &self.columns[column_range.clone()],
// > which the cell originates. );
let baseline_border_spacing = border_spacing.inline * (n as i32 - 1); {
let column_span = &mut self.columns[colspan_cell_constraints.range()];
// > 3. The contribution of the cell is the sum of: for (column, minimum_size) in column_span.iter_mut().zip(distributed_minimum) {
// > a. the min-content width of the column based on cells of span up to N-1 column.content_sizes.min_content.max_assign(minimum_size);
let a = old_column_content_size.min_content; }
}
// > b. the product of:
// > - the ratio of: let colspan_cell_max_size = (colspan_cell_constraints.content_sizes.max_content -
// > - the max-content width of the column based on cells of span up total_border_spacing)
// > to N-1 of the column minus the min-content width of the .max(Au::zero());
// > column based on cells of span up to N-1 of the column, to let distributed_maximum = Self::distribute_width_to_columns(
// > - the baseline max-content width minus the baseline min-content colspan_cell_max_size,
// > width &self.columns[colspan_cell_constraints.range()],
// > or zero if this ratio is undefined, and );
// > - the outer min-content width of the cell minus the baseline {
// > min-content width and the baseline border spacing, clamped to be let column_span = &mut self.columns[colspan_cell_constraints.range()];
// > at least 0 and at most the difference between the baseline for (column, maximum_size) in column_span.iter_mut().zip(distributed_maximum) {
// > max-content width and the baseline min-content width column
let old_content_size_difference = .content_sizes
old_column_content_size.max_content - old_column_content_size.min_content; .max_content
let baseline_difference = baseline_min_content_width - baseline_max_content_width; .max_assign(maximum_size.max(column.content_sizes.min_content));
}
let mut b =
old_content_size_difference.to_f32_px() / baseline_difference.to_f32_px();
if !b.is_finite() {
b = 0.0;
}
let b = (cell_inline_content_sizes.min_content -
baseline_content_sizes.min_content -
baseline_border_spacing)
.clamp_between_extremums(Au::zero(), Some(baseline_difference))
.scale_by(b);
// > c. the product of:
// > - the ratio of the max-content width based on cells of span up to
// > N-1 of the column to the baseline max-content width
// > - the outer min-content width of the cell minus the baseline
// > max-content width and baseline border spacing, or 0 if this is
// > negative
let c = (cell_inline_content_sizes.min_content -
baseline_content_sizes.max_content -
baseline_border_spacing)
.min(Au::zero())
.scale_by(
old_column_content_size.max_content.to_f32_px() /
baseline_content_sizes.max_content.to_f32_px(),
);
let new_column_min_content_width = a + b + c;
// > **max-content width of a column based on cells of span up to N (N > 1)**
// >
// > The largest of the max-content width based on cells of span up to N-1 and 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:
// > 1. Define the baseline max-content width as the sum of the max-content
// > widths based on cells of span up to N-1 of all columns that the cell spans.
//
// This is calculated above for the min-content width.
// > 2. Define the baseline border spacing as the sum of the horizontal
// > border-spacing for any columns spanned by the cell, other than the one in
// > which the cell originates.
//
// This is calculated above for min-content width.
// > 3. The contribution of the cell is the sum of:
// > a. the max-content width of the column based on cells of span up to N-1
let a = old_column_content_size.max_content;
// > b. the product of:
// > 1. the ratio of the max-content width based on cells of span up to
// > N-1 of the column to the baseline max-content width
let b_1 = old_column_content_size.max_content.to_f32_px() /
baseline_content_sizes.max_content.to_f32_px();
// > 2. the outer max-content width of the cell minus the baseline
// > max-content width and the baseline border spacing, or 0 if this
// > is negative
let b_2 = (cell_inline_content_sizes.max_content -
baseline_content_sizes.max_content -
baseline_border_spacing)
.min(Au::zero());
let b = b_2.scale_by(b_1);
let new_column_max_content_width = a + b + c;
// The computed values for the column are always the largest of any processed cell
// in that column.
new_column_content_sizes.max_assign(ContentSizes {
min_content: new_column_min_content_width,
max_content: new_column_max_content_width,
});
// > If the intrinsic percentage width of a column based on cells of span up to N-1 is
// > greater than 0%, then the intrinsic percentage width of the column based on cells
// > of span up to N is the same as the intrinsic percentage width of the column based
// > on cells of span up to N-1.
// > 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.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
// > negative result, change it to 0%.
let mut spanned_columns_with_zero = 0;
let other_column_percentages_sum =
(columns_spanned).fold(0., |sum, spanned_column_index| {
let spanned_column_percentage =
self.columns[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 -
Percentage(other_column_percentages_sum))
.clamp_to_non_negative();
// > Multiply by the ratio of:
// > 1. the columns non-spanning max-content width to
// > 2. the sum of the non-spanning max-content widths of all columns
// > spanned by the cell that have an intrinsic percentage width of the column
// > based on cells of span up to N-1 equal to 0%.
// > However, if this ratio is undefined because the denominator is zero,
// > instead use the 1 divided by the number of columns spanned by the cell
// > that have an intrinsic percentage width of the column based on cells of
// > span up to N-1 equal to zero.
let step_3 = step_2.0 * (1.0 / spanned_columns_with_zero as f32);
new_column_intrinsic_percentage_width =
Percentage(new_column_intrinsic_percentage_width.0.max(step_3));
}
}
let mut new_column = old_column.clone();
new_column.content_sizes = new_column_content_sizes;
new_column.percentage = new_column_intrinsic_percentage_width;
new_columns.push(new_column);
} }
(next_span_n, new_columns)
} }
/// Compute the GRIDMIN and GRIDMAX. /// Compute the GRIDMIN and GRIDMAX.
@ -819,14 +725,10 @@ impl<'a> TableLayout<'a> {
/// Distribute width to columns, performing step 2.4 of table layout from /// Distribute width to columns, performing step 2.4 of table layout from
/// <https://drafts.csswg.org/css-tables/#table-layout-algorithm>. /// <https://drafts.csswg.org/css-tables/#table-layout-algorithm>.
fn distribute_width_to_columns( fn distribute_width_to_columns(target_inline_size: Au, columns: &[ColumnLayout]) -> Vec<Au> {
&self,
target_inline_size: Au,
columns: &[ColumnLayout],
) -> Vec<Au> {
// No need to do anything if there is no column. // No need to do anything if there is no column.
// Note that tables without rows may still have columns. // Note that tables without rows may still have columns.
if self.table.size.width.is_zero() { if columns.is_empty() {
return Vec::new(); return Vec::new();
} }
@ -872,8 +774,8 @@ impl<'a> TableLayout<'a> {
min_content_percentage_sizing_guess, min_content_percentage_sizing_guess,
min_content_specified_sizing_guess, min_content_specified_sizing_guess,
max_content_sizing_guess, max_content_sizing_guess,
) = if !column.percentage.is_zero() { ) = if let Some(percentage) = column.percentage {
let resolved = target_inline_size.scale_by(column.percentage.0); let resolved = target_inline_size.scale_by(percentage.0);
let percent_guess = min_content_width.max(resolved); let percent_guess = min_content_width.max(resolved);
(percent_guess, percent_guess, percent_guess) (percent_guess, percent_guess, percent_guess)
} else if constrained { } else if constrained {
@ -901,9 +803,11 @@ impl<'a> TableLayout<'a> {
let max_content_sizing_sum = sum(&max_content_sizing_guesses); let max_content_sizing_sum = sum(&max_content_sizing_guesses);
if target_inline_size >= max_content_sizing_sum { if target_inline_size >= max_content_sizing_sum {
self.distribute_extra_width_to_columns( Self::distribute_extra_width_to_columns(
columns,
&mut max_content_sizing_guesses, &mut max_content_sizing_guesses,
max_content_sizing_sum, max_content_sizing_sum,
target_inline_size,
); );
return max_content_sizing_guesses; return max_content_sizing_guesses;
} }
@ -999,26 +903,29 @@ impl<'a> TableLayout<'a> {
/// This is an implementation of *Distributing excess width to columns* from /// This is an implementation of *Distributing excess width to columns* from
/// <https://drafts.csswg.org/css-tables/#distributing-width-to-columns>. /// <https://drafts.csswg.org/css-tables/#distributing-width-to-columns>.
fn distribute_extra_width_to_columns(&self, column_sizes: &mut [Au], column_sizes_sum: Au) { fn distribute_extra_width_to_columns(
let all_columns = 0..self.table.size.width; columns: &[ColumnLayout],
let extra_inline_size = self.assignable_width - column_sizes_sum; column_sizes: &mut [Au],
column_sizes_sum: Au,
assignable_width: Au,
) {
let all_columns = 0..columns.len();
let extra_inline_size = assignable_width - column_sizes_sum;
let has_originating_cells = let has_originating_cells =
|column_index: &usize| self.columns[*column_index].has_originating_cells; |column_index: &usize| columns[*column_index].has_originating_cells;
let is_constrained = |column_index: &usize| self.columns[*column_index].constrained; let is_constrained = |column_index: &usize| columns[*column_index].constrained;
let is_unconstrained = |column_index: &usize| !is_constrained(column_index); let is_unconstrained = |column_index: &usize| !is_constrained(column_index);
let has_percent_greater_than_zero = let has_percent_greater_than_zero = |column_index: &usize| {
|column_index: &usize| self.columns[*column_index].percentage.0 > 0.; columns[*column_index]
let has_percent_zero = |column_index: &usize| !has_percent_greater_than_zero(column_index); .percentage
let has_max_content = |column_index: &usize| { .is_some_and(|percentage| percentage.0 > 0.)
!self.columns[*column_index]
.content_sizes
.max_content
.is_zero()
}; };
let has_percent_zero = |column_index: &usize| !has_percent_greater_than_zero(column_index);
let has_max_content =
|column_index: &usize| !columns[*column_index].content_sizes.max_content.is_zero();
let max_content_sum = let max_content_sum = |column_index: usize| columns[column_index].content_sizes.max_content;
|column_index: usize| self.columns[column_index].content_sizes.max_content;
// > If there are non-constrained columns that have originating cells with intrinsic // > If there are non-constrained columns that have originating cells with intrinsic
// > percentage width of 0% and with nonzero max-content width (aka the columns allowed to // > percentage width of 0% and with nonzero max-content width (aka the columns allowed to
@ -1038,10 +945,7 @@ impl<'a> TableLayout<'a> {
if total_max_content_width != Au::zero() { if total_max_content_width != Au::zero() {
for column_index in unconstrained_max_content_columns { for column_index in unconstrained_max_content_columns {
column_sizes[column_index] += extra_inline_size.scale_by( column_sizes[column_index] += extra_inline_size.scale_by(
self.columns[column_index] columns[column_index].content_sizes.max_content.to_f32_px() /
.content_sizes
.max_content
.to_f32_px() /
total_max_content_width.to_f32_px(), total_max_content_width.to_f32_px(),
); );
} }
@ -1086,10 +990,7 @@ impl<'a> TableLayout<'a> {
if total_max_content_width != Au::zero() { if total_max_content_width != Au::zero() {
for column_index in constrained_max_content_columns { for column_index in constrained_max_content_columns {
column_sizes[column_index] += extra_inline_size.scale_by( column_sizes[column_index] += extra_inline_size.scale_by(
self.columns[column_index] columns[column_index].content_sizes.max_content.to_f32_px() /
.content_sizes
.max_content
.to_f32_px() /
total_max_content_width.to_f32_px(), total_max_content_width.to_f32_px(),
); );
} }
@ -1104,12 +1005,13 @@ impl<'a> TableLayout<'a> {
let columns_with_percentage = all_columns.clone().filter(has_percent_greater_than_zero); let columns_with_percentage = all_columns.clone().filter(has_percent_greater_than_zero);
let total_percent = columns_with_percentage let total_percent = columns_with_percentage
.clone() .clone()
.map(|column_index| self.columns[column_index].percentage.0) .map(|column_index| columns[column_index].percentage.unwrap_or_default().0)
.sum::<f32>(); .sum::<f32>();
if total_percent > 0. { if total_percent > 0. {
for column_index in columns_with_percentage { for column_index in columns_with_percentage {
column_sizes[column_index] += extra_inline_size let column_percentage = columns[column_index].percentage.unwrap_or_default();
.scale_by(self.columns[column_index].percentage.0 / total_percent); column_sizes[column_index] +=
extra_inline_size.scale_by(column_percentage.0 / total_percent);
} }
return; return;
} }
@ -1130,8 +1032,7 @@ impl<'a> TableLayout<'a> {
// > Otherwise, the distributed widths of all columns are increased by equal amounts so the // > Otherwise, the distributed widths of all columns are increased by equal amounts so the
// total increase adds to the excess width. // total increase adds to the excess width.
let extra_space_for_all_columns = let extra_space_for_all_columns = extra_inline_size.scale_by(1.0 / columns.len() as f32);
extra_inline_size.scale_by(1.0 / self.table.size.width as f32);
for guess in column_sizes.iter_mut() { for guess in column_sizes.iter_mut() {
*guess += extra_space_for_all_columns; *guess += extra_space_for_all_columns;
} }
@ -1320,11 +1221,12 @@ impl<'a> TableLayout<'a> {
.get_row_measure_for_row_at_index(writing_mode, row_index); .get_row_measure_for_row_at_index(writing_mode, row_index);
row_sizes[row_index].max_assign(row_measure.content_sizes.min_content); row_sizes[row_index].max_assign(row_measure.content_sizes.min_content);
let mut percentage = row_measure.percentage.0; let mut percentage = row_measure.percentage.unwrap_or_default().0;
for column_index in 0..self.table.size.width { for column_index in 0..self.table.size.width {
let cell_percentage = self.cell_measures[row_index][column_index] let cell_percentage = self.cell_measures[row_index][column_index]
.block .block
.percentage .percentage
.unwrap_or_default()
.0; .0;
percentage = percentage.max(cell_percentage); percentage = percentage.max(cell_percentage);
@ -1821,7 +1723,7 @@ impl<'a> TableLayout<'a> {
containing_block_for_children: &ContainingBlock, containing_block_for_children: &ContainingBlock,
) -> BoxFragment { ) -> BoxFragment {
self.distributed_column_widths = self.distributed_column_widths =
self.distribute_width_to_columns(self.assignable_width, &self.columns); Self::distribute_width_to_columns(self.assignable_width, &self.columns);
self.layout_cells_in_row( self.layout_cells_in_row(
layout_context, layout_context,
containing_block_for_children, containing_block_for_children,
@ -2650,8 +2552,13 @@ impl Table {
None => return CellOrTrackMeasure::zero(), None => return CellOrTrackMeasure::zero(),
}; };
let (size, min_size, max_size, _, percentage_contribution) = let CellOrColumnOuterSizes {
get_outer_sizes_for_measurement(&column.style, writing_mode, &LogicalVec2::zero()); preferred: preferred_size,
min: min_size,
max: max_size,
percentage: percentage_size,
..
} = CellOrColumnOuterSizes::new(&column.style, writing_mode, &Default::default());
CellOrTrackMeasure { CellOrTrackMeasure {
content_sizes: ContentSizes { content_sizes: ContentSizes {
@ -2663,9 +2570,11 @@ impl Table {
// > The outer max-content width of a table-column or table-column-group is // > The outer max-content width of a table-column or table-column-group is
// > max(min-width, min(max-width, width)). // > max(min-width, min(max-width, width)).
// This matches Gecko, but Blink and WebKit ignore max_size. // This matches Gecko, but Blink and WebKit ignore max_size.
max_content: min_size.inline.max(max_size.inline.min(size.inline)), max_content: preferred_size
.inline
.clamp_between_extremums(min_size.inline, max_size.inline),
}, },
percentage: percentage_contribution.inline, percentage: percentage_size.inline,
} }
} }
@ -2910,7 +2819,7 @@ impl TableSlotCell {
fn get_size_percentage_contribution( fn get_size_percentage_contribution(
size: &LogicalVec2<Size<ComputedLengthPercentage>>, size: &LogicalVec2<Size<ComputedLengthPercentage>>,
max_size: &LogicalVec2<Size<ComputedLengthPercentage>>, max_size: &LogicalVec2<Size<ComputedLengthPercentage>>,
) -> LogicalVec2<Percentage> { ) -> LogicalVec2<Option<Percentage>> {
// From <https://drafts.csswg.org/css-tables/#percentage-contribution> // From <https://drafts.csswg.org/css-tables/#percentage-contribution>
// > The percentage contribution of a table cell, column, or column group is defined // > The percentage contribution of a table cell, column, or column group is defined
// > in terms of the computed values of width and max-width that have computed values // > in terms of the computed values of width and max-width that have computed values
@ -2922,13 +2831,11 @@ fn get_size_percentage_contribution(
|size: &Size<ComputedLengthPercentage>, max_size: &Size<ComputedLengthPercentage>| { |size: &Size<ComputedLengthPercentage>, max_size: &Size<ComputedLengthPercentage>| {
let size_percentage = size let size_percentage = size
.to_numeric() .to_numeric()
.and_then(|length_percentage| length_percentage.to_percentage()) .and_then(|length_percentage| length_percentage.to_percentage());
.unwrap_or(Percentage(0.));
let max_size_percentage = max_size let max_size_percentage = max_size
.to_numeric() .to_numeric()
.and_then(|length_percentage| length_percentage.to_percentage()) .and_then(|length_percentage| length_percentage.to_percentage());
.unwrap_or(Percentage(f32::INFINITY)); max_two_optional_percentages(size_percentage, max_size_percentage)
Percentage(size_percentage.0.min(max_size_percentage.0))
}; };
LogicalVec2 { LogicalVec2 {
@ -2937,43 +2844,60 @@ fn get_size_percentage_contribution(
} }
} }
fn get_outer_sizes_for_measurement( struct CellOrColumnOuterSizes {
style: &Arc<ComputedValues>, min: LogicalVec2<Au>,
writing_mode: WritingMode, preferred: LogicalVec2<Au>,
padding_border_sums: &LogicalVec2<Au>, max: LogicalVec2<Option<Au>>,
) -> ( percentage: LogicalVec2<Option<Percentage>>,
LogicalVec2<Au>, inline_preferred_size_is_auto: bool,
LogicalVec2<Au>, }
LogicalVec2<Au>,
bool,
LogicalVec2<Percentage>,
) {
let box_sizing = style.get_position().box_sizing;
let outer_size = |size: LogicalVec2<Au>| match box_sizing {
BoxSizing::ContentBox => size + *padding_border_sums,
BoxSizing::BorderBox => LogicalVec2 {
inline: size.inline.max(padding_border_sums.inline),
block: size.block.max(padding_border_sums.block),
},
};
let get_size_for_axis = |size: &Size<ComputedLengthPercentage>| {
// Note that measures treat all size values other than <length>
// as the initial value of the property.
size.to_numeric()
.and_then(|length_percentage| length_percentage.to_length())
.map(Au::from)
};
let size = style.box_size(writing_mode); impl CellOrColumnOuterSizes {
let min_size = style.min_box_size(writing_mode); fn new(
let max_size = style.max_box_size(writing_mode); style: &Arc<ComputedValues>,
( writing_mode: WritingMode,
outer_size(size.map(|v| get_size_for_axis(v).unwrap_or_else(Au::zero))), padding_border_sums: &LogicalVec2<Au>,
outer_size(min_size.map(|v| get_size_for_axis(v).unwrap_or_else(Au::zero))), ) -> Self {
outer_size(max_size.map(|v| get_size_for_axis(v).unwrap_or(MAX_AU))), let box_sizing = style.get_position().box_sizing;
!size.inline.is_numeric(), let outer_size = |size: LogicalVec2<Au>| match box_sizing {
get_size_percentage_contribution(&size, &max_size), BoxSizing::ContentBox => size + *padding_border_sums,
) BoxSizing::BorderBox => LogicalVec2 {
inline: size.inline.max(padding_border_sums.inline),
block: size.block.max(padding_border_sums.block),
},
};
let outer_size_for_max = |size: LogicalVec2<Option<Au>>| match box_sizing {
BoxSizing::ContentBox => size.map_inline_and_block_axes(
|inline| inline.map(|inline| inline + padding_border_sums.inline),
|block| block.map(|block| block + padding_border_sums.block),
),
BoxSizing::BorderBox => size.map_inline_and_block_axes(
|inline| inline.map(|inline| inline.max(padding_border_sums.inline)),
|block| block.map(|block| block.max(padding_border_sums.block)),
),
};
let get_size_for_axis = |size: &Size<ComputedLengthPercentage>| {
// Note that measures treat all size values other than <length>
// as the initial value of the property.
size.to_numeric()
.and_then(|length_percentage| length_percentage.to_length())
.map(Au::from)
};
let size = style.box_size(writing_mode);
let min_size = style.min_box_size(writing_mode);
let max_size = style.max_box_size(writing_mode);
Self {
min: outer_size(min_size.map(|v| get_size_for_axis(v).unwrap_or_default())),
preferred: outer_size(size.map(|v| get_size_for_axis(v).unwrap_or_default())),
max: outer_size_for_max(max_size.map(get_size_for_axis)),
inline_preferred_size_is_auto: !size.inline.is_numeric(),
percentage: get_size_percentage_contribution(&size, &max_size),
}
}
} }
struct RowspanToDistribute<'a> { struct RowspanToDistribute<'a> {
@ -2991,3 +2915,28 @@ impl RowspanToDistribute<'_> {
other.coordinates.y > self.coordinates.y && other.range().end < self.range().end other.coordinates.y > self.coordinates.y && other.range().end < self.range().end
} }
} }
/// The inline size constraints provided by a cell that span multiple columns (`colspan` > 1).
/// These constraints are distributed to the individual columns that make up this cell's span.
#[derive(Debug)]
struct ColspanToDistribute {
starting_column: usize,
span: usize,
content_sizes: ContentSizes,
percentage: Option<Percentage>,
}
impl ColspanToDistribute {
/// A comparison function to sort the colspan cell constraints primarily by their span
/// width and secondarily by their starting column. This is not an implementation of
/// `PartialOrd` because we want to return [`Ordering::Equal`] even if `self != other`.
fn comparison_for_sort(a: &Self, b: &Self) -> Ordering {
a.span
.cmp(&b.span)
.then_with(|| b.starting_column.cmp(&b.starting_column))
}
fn range(&self) -> Range<usize> {
self.starting_column..self.starting_column + self.span
}
}

View file

@ -1,9 +0,0 @@
[colspan-001.html]
[td 1]
expected: FAIL
[td 2]
expected: FAIL
[td 4]
expected: FAIL

View file

@ -1,9 +0,0 @@
[colspan-002.html]
[td 1]
expected: FAIL
[td 2]
expected: FAIL
[td 4]
expected: FAIL

View file

@ -1,9 +0,0 @@
[colspan-003.html]
[td 1]
expected: FAIL
[td 2]
expected: FAIL
[td 4]
expected: FAIL

View file

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

View file

@ -1,7 +1,4 @@
[html5-table-formatting-3.html] [html5-table-formatting-3.html]
[Anonymous consecutive columns spanned by the same set of cells are merged]
expected: FAIL
[Explicitely-defined consecutive columns spanned by the same set of cells are not merged, and cells span across border-spacing] [Explicitely-defined consecutive columns spanned by the same set of cells are not merged, and cells span across border-spacing]
expected: FAIL expected: FAIL

View file

@ -8,27 +8,9 @@
[table 3] [table 3]
expected: FAIL expected: FAIL
[table 4]
expected: FAIL
[table 6] [table 6]
expected: FAIL expected: FAIL
[table 9]
expected: FAIL
[table 10]
expected: FAIL
[table 11]
expected: FAIL
[table 12]
expected: FAIL
[table 13]
expected: FAIL
[table 14] [table 14]
expected: FAIL expected: FAIL
@ -41,24 +23,12 @@
[table 17] [table 17]
expected: FAIL expected: FAIL
[table 18]
expected: FAIL
[table 19]
expected: FAIL
[table 20] [table 20]
expected: FAIL expected: FAIL
[table 22] [table 22]
expected: FAIL expected: FAIL
[table 23]
expected: FAIL
[table 24]
expected: FAIL
[table 26] [table 26]
expected: FAIL expected: FAIL
@ -68,14 +38,5 @@
[table 28] [table 28]
expected: FAIL expected: FAIL
[table 29]
expected: FAIL
[table 30]
expected: FAIL
[table 31]
expected: FAIL
[table 8] [table 8]
expected: FAIL expected: FAIL

View file

@ -13,6 +13,3 @@
[table 15] [table 15]
expected: FAIL expected: FAIL
[table 7]
expected: FAIL

View file

@ -4,6 +4,3 @@
[table 19] [table 19]
expected: FAIL expected: FAIL
[table 12]
expected: FAIL

View file

@ -1,6 +0,0 @@
[col-span-limits.html]
[col span of 1000 must work]
expected: FAIL
[col span of 1001 must be treated as 1000]
expected: FAIL