aboutsummaryrefslogtreecommitdiffstats
path: root/components/layout_2020/table/layout.rs
diff options
context:
space:
mode:
authorMartin Robinson <mrobinson@igalia.com>2025-01-21 14:29:55 +0100
committerGitHub <noreply@github.com>2025-01-21 13:29:55 +0000
commitc17668bb0e6427da2aa31dbbd4801d976f03fa53 (patch)
treedd78fd551ec870d1b71930a8df0aa96c39b31dc5 /components/layout_2020/table/layout.rs
parentd00d76c1e8f1ef435fa7559b19b88f8934474395 (diff)
downloadservo-c17668bb0e6427da2aa31dbbd4801d976f03fa53.tar.gz
servo-c17668bb0e6427da2aa31dbbd4801d976f03fa53.zip
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>
Diffstat (limited to 'components/layout_2020/table/layout.rs')
-rw-r--r--components/layout_2020/table/layout.rs603
1 files changed, 276 insertions, 327 deletions
diff --git a/components/layout_2020/table/layout.rs b/components/layout_2020/table/layout.rs
index 49521baaa05..b0b0036922d 100644
--- a/components/layout_2020/table/layout.rs
+++ b/components/layout_2020/table/layout.rs
@@ -6,7 +6,7 @@ use core::cmp::Ordering;
use std::mem;
use std::ops::Range;
-use app_units::{Au, MAX_AU};
+use app_units::Au;
use log::warn;
use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator};
use servo_arc::Arc;
@@ -99,7 +99,24 @@ struct ColumnLayout {
constrained: bool,
has_originating_cells: bool,
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 {
@@ -200,19 +217,19 @@ pub(crate) struct TableLayout<'a> {
#[derive(Clone, Debug)]
struct CellOrTrackMeasure {
content_sizes: ContentSizes,
- percentage: Percentage,
+ percentage: Option<Percentage>,
}
impl Zero for CellOrTrackMeasure {
fn zero() -> Self {
Self {
content_sizes: ContentSizes::zero(),
- percentage: Percentage(0.),
+ percentage: None,
}
}
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(),
};
- let (size, min_size, max_size, inline_size_is_auto, percentage_contribution) =
- get_outer_sizes_for_measurement(
- &cell.base.style,
- writing_mode,
- &padding_border_sums,
- );
+ let CellOrColumnOuterSizes {
+ preferred: preferred_size,
+ min: min_size,
+ max: max_size,
+ 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>
// > 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.
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 like allowing the content-box width to be negative.
Au::zero()
} else {
- size.inline.min(max_size.inline).max(min_size.inline)
+ preferred_size
+ .inline
+ .clamp_between_extremums(min_size.inline, max_size.inline)
}
} else {
inline_content_sizes
.min_content
- .min(max_size.inline)
- .max(min_size.inline)
+ .clamp_between_extremums(min_size.inline, max_size.inline)
};
let outer_max_content_width = if self.columns[column_index].constrained {
inline_content_sizes
.min_content
- .max(size.inline)
- .min(max_size.inline)
- .max(min_size.inline)
+ .max(preferred_size.inline)
+ .clamp_between_extremums(min_size.inline, max_size.inline)
} else {
inline_content_sizes
.max_content
- .max(size.inline)
- .min(max_size.inline)
- .max(min_size.inline)
+ .max(preferred_size.inline)
+ .clamp_between_extremums(min_size.inline, max_size.inline)
};
assert!(outer_min_content_width <= outer_max_content_width);
@@ -338,7 +359,7 @@ impl<'a> TableLayout<'a> {
min_content: outer_min_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
// of the layout is known.
let block_measure = CellOrTrackMeasure {
- content_sizes: size.block.into(),
- percentage: percentage_contribution.block,
+ content_sizes: preferred_size.block.into(),
+ percentage: percentage_size.block,
};
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 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 {
let column = &mut self.columns[column_index];
@@ -477,31 +498,34 @@ impl<'a> TableLayout<'a> {
for row_index in 0..self.table.size.height {
let coords = TableSlotCoordinates::new(column_index, row_index);
- match self.table.resolve_first_cell(coords) {
- Some(cell) if cell.colspan == 1 => cell,
- Some(cell) => {
- next_span_n = next_span_n.min(cell.colspan);
- continue;
- },
+ let cell_measure = &self.cell_measures[row_index][column_index].inline;
+
+ let cell = match self.table.get_slot(coords) {
+ Some(TableSlot::Cell(cell)) => cell,
_ => 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
// intrinsic percentage width as described above.
- let cell_measure = &self.cell_measures[row_index][column_index].inline;
- column.content_sizes.max_assign(cell_measure.content_sizes);
- column.percentage =
- Percentage(column_measure.percentage.0.max(cell_measure.percentage.0));
+ column.incorporate_cell_measure(cell_measure);
}
}
- // Now we have the base computation complete, so iteratively take into account cells
- // with higher colspan. Using `next_span_n` we can skip over span counts that don't
- // correspond to any cells.
- while next_span_n < usize::MAX {
- (next_span_n, self.columns) =
- self.compute_content_sizes_for_columns_with_span_up_to_n(next_span_n);
- }
+ // Sort the colspanned cell constraints by their span and starting column.
+ colspan_cell_constraints.sort_by(ColspanToDistribute::comparison_for_sort);
+
+ // Distribute constraints from cells with colspan != 1 to their component columns.
+ self.distribute_colspanned_cells_to_columns(colspan_cell_constraints);
// > intrinsic percentage width of a column:
// > the smaller of:
@@ -511,216 +535,98 @@ impl<'a> TableLayout<'a> {
// > the table (further left when direction is "ltr" (right for "rtl"))
let mut total_intrinsic_percentage_width = 0.;
for column in self.columns.iter_mut() {
- let final_intrinsic_percentage_width = column
- .percentage
- .0
- .min(1. - total_intrinsic_percentage_width);
- total_intrinsic_percentage_width += final_intrinsic_percentage_width;
- column.percentage = Percentage(final_intrinsic_percentage_width);
+ if let Some(ref mut percentage) = column.percentage {
+ let final_intrinsic_percentage_width =
+ percentage.0.min(1. - total_intrinsic_percentage_width);
+ total_intrinsic_percentage_width += final_intrinsic_percentage_width;
+ *percentage = Percentage(final_intrinsic_percentage_width);
+ }
}
}
- fn compute_content_sizes_for_columns_with_span_up_to_n(
- &self,
- n: usize,
- ) -> (usize, Vec<ColumnLayout>) {
- let mut next_span_n = usize::MAX;
- let mut new_columns = Vec::new();
- let border_spacing = self.table.border_spacing();
+ fn distribute_colspanned_cells_to_columns(
+ &mut self,
+ colspan_cell_constraints: Vec<ColspanToDistribute>,
+ ) {
+ for colspan_cell_constraints in colspan_cell_constraints {
+ self.distribute_colspanned_cell_to_columns(colspan_cell_constraints);
+ }
+ }
- for column_index in 0..self.table.size.width {
- let old_column = &self.columns[column_index];
- let mut new_column_content_sizes = old_column.content_sizes;
- let mut new_column_intrinsic_percentage_width = old_column.percentage;
+ /// Distribute the inline size from a cell with colspan != 1 to the columns that it spans.
+ /// This is heavily inspired by the approach that Chromium takes in redistributing colspan
+ /// cells' inline size to columns (`DistributeColspanCellToColumnsAuto` in
+ /// `blink/renderer/core/layout/table/table_layout_utils.cc`).
+ fn distribute_colspanned_cell_to_columns(
+ &mut self,
+ colspan_cell_constraints: ColspanToDistribute,
+ ) {
+ let border_spacing = self.table.border_spacing().inline;
+ let column_range = colspan_cell_constraints.range();
+ let column_count = column_range.len();
+ let total_border_spacing =
+ border_spacing.scale_by((colspan_cell_constraints.span - 1) as f32);
+
+ let mut percent_columns_count = 0;
+ let mut columns_percent_sum = 0.;
+ let mut columns_non_percent_max_inline_size_sum = Au::zero();
+ for column in self.columns[column_range.clone()].iter() {
+ if let Some(percentage) = column.percentage {
+ percent_columns_count += 1;
+ columns_percent_sum += percentage.0;
+ } else {
+ columns_non_percent_max_inline_size_sum += column.content_sizes.max_content;
+ }
+ }
- for row_index in 0..self.table.size.height {
- let coords = TableSlotCoordinates::new(column_index, row_index);
- let resolved_coords = match self.table.resolve_first_cell_coords(coords) {
- Some(resolved_coords) => resolved_coords,
- None => continue,
- };
+ let colspan_percentage = colspan_cell_constraints.percentage.unwrap_or_default();
+ let surplus_percent = colspan_percentage.0 - columns_percent_sum;
+ 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 cell = match self.table.resolve_first_cell(resolved_coords) {
- Some(cell) if cell.colspan <= n => cell,
- Some(cell) => {
- next_span_n = next_span_n.min(cell.colspan);
- continue;
- },
- _ => continue,
+ let ratio = if columns_non_percent_max_inline_size_sum.is_zero() {
+ 1. / ((column_count - percent_columns_count) as f32)
+ } else {
+ column.content_sizes.max_content.to_f32_px() /
+ columns_non_percent_max_inline_size_sum.to_f32_px()
};
+ column.percentage = Some(Percentage(surplus_percent * ratio));
+ }
+ }
- 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;
- let baseline_content_sizes: ContentSizes = columns_spanned.clone().fold(
- ContentSizes::zero(),
- |total: ContentSizes, spanned_column_index| {
- total + self.columns[spanned_column_index].content_sizes
- },
- );
-
- 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)**
- // >
- // > the largest of the min-content width of the column 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 min-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.
- //
- // Note: This definition is likely a typo, so we use the sum of the min-content
- // widths here instead.
- let baseline_min_content_width = baseline_content_sizes.min_content;
- let baseline_max_content_width = baseline_content_sizes.max_content;
-
- // > 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.
- let baseline_border_spacing = border_spacing.inline * (n as i32 - 1);
-
- // > 3. The contribution of the cell is the sum of:
- // > a. the min-content width of the column based on cells of span up to N-1
- let a = old_column_content_size.min_content;
-
- // > b. the product of:
- // > - the ratio of:
- // > - the max-content width of the column based on cells of span up
- // > to N-1 of the column minus the min-content width of the
- // > column based on cells of span up to N-1 of the column, to
- // > - the baseline max-content width minus the baseline min-content
- // > width
- // > 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
- // > at least 0 and at most the difference between the baseline
- // > max-content width and the baseline min-content width
- let old_content_size_difference =
- old_column_content_size.max_content - old_column_content_size.min_content;
- let baseline_difference = baseline_min_content_width - baseline_max_content_width;
-
- 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,
- });
+ let colspan_cell_min_size = (colspan_cell_constraints.content_sizes.min_content -
+ total_border_spacing)
+ .max(Au::zero());
+ let distributed_minimum = Self::distribute_width_to_columns(
+ colspan_cell_min_size,
+ &self.columns[column_range.clone()],
+ );
+ {
+ let column_span = &mut self.columns[colspan_cell_constraints.range()];
+ for (column, minimum_size) in column_span.iter_mut().zip(distributed_minimum) {
+ column.content_sizes.min_content.max_assign(minimum_size);
+ }
+ }
- // > 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 column’s 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 colspan_cell_max_size = (colspan_cell_constraints.content_sizes.max_content -
+ total_border_spacing)
+ .max(Au::zero());
+ let distributed_maximum = Self::distribute_width_to_columns(
+ colspan_cell_max_size,
+ &self.columns[colspan_cell_constraints.range()],
+ );
+ {
+ let column_span = &mut self.columns[colspan_cell_constraints.range()];
+ for (column, maximum_size) in column_span.iter_mut().zip(distributed_maximum) {
+ column
+ .content_sizes
+ .max_content
+ .max_assign(maximum_size.max(column.content_sizes.min_content));
}
- 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.
@@ -819,14 +725,10 @@ impl<'a> TableLayout<'a> {
/// Distribute width to columns, performing step 2.4 of table layout from
/// <https://drafts.csswg.org/css-tables/#table-layout-algorithm>.
- fn distribute_width_to_columns(
- &self,
- target_inline_size: Au,
- columns: &[ColumnLayout],
- ) -> Vec<Au> {
+ fn distribute_width_to_columns(target_inline_size: Au, columns: &[ColumnLayout]) -> Vec<Au> {
// No need to do anything if there is no column.
// Note that tables without rows may still have columns.
- if self.table.size.width.is_zero() {
+ if columns.is_empty() {
return Vec::new();
}
@@ -872,8 +774,8 @@ impl<'a> TableLayout<'a> {
min_content_percentage_sizing_guess,
min_content_specified_sizing_guess,
max_content_sizing_guess,
- ) = if !column.percentage.is_zero() {
- let resolved = target_inline_size.scale_by(column.percentage.0);
+ ) = if let Some(percentage) = column.percentage {
+ let resolved = target_inline_size.scale_by(percentage.0);
let percent_guess = min_content_width.max(resolved);
(percent_guess, percent_guess, percent_guess)
} else if constrained {
@@ -901,9 +803,11 @@ impl<'a> TableLayout<'a> {
let max_content_sizing_sum = sum(&max_content_sizing_guesses);
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,
max_content_sizing_sum,
+ target_inline_size,
);
return max_content_sizing_guesses;
}
@@ -999,26 +903,29 @@ impl<'a> TableLayout<'a> {
/// This is an implementation of *Distributing excess width to columns* from
/// <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) {
- let all_columns = 0..self.table.size.width;
- let extra_inline_size = self.assignable_width - column_sizes_sum;
+ fn distribute_extra_width_to_columns(
+ columns: &[ColumnLayout],
+ 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 =
- |column_index: &usize| self.columns[*column_index].has_originating_cells;
- let is_constrained = |column_index: &usize| self.columns[*column_index].constrained;
+ |column_index: &usize| columns[*column_index].has_originating_cells;
+ let is_constrained = |column_index: &usize| columns[*column_index].constrained;
let is_unconstrained = |column_index: &usize| !is_constrained(column_index);
- let has_percent_greater_than_zero =
- |column_index: &usize| self.columns[*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.columns[*column_index]
- .content_sizes
- .max_content
- .is_zero()
+ let has_percent_greater_than_zero = |column_index: &usize| {
+ columns[*column_index]
+ .percentage
+ .is_some_and(|percentage| percentage.0 > 0.)
};
+ 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 =
- |column_index: usize| self.columns[column_index].content_sizes.max_content;
+ let max_content_sum = |column_index: usize| columns[column_index].content_sizes.max_content;
// > 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
@@ -1038,10 +945,7 @@ impl<'a> TableLayout<'a> {
if total_max_content_width != Au::zero() {
for column_index in unconstrained_max_content_columns {
column_sizes[column_index] += extra_inline_size.scale_by(
- self.columns[column_index]
- .content_sizes
- .max_content
- .to_f32_px() /
+ columns[column_index].content_sizes.max_content.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() {
for column_index in constrained_max_content_columns {
column_sizes[column_index] += extra_inline_size.scale_by(
- self.columns[column_index]
- .content_sizes
- .max_content
- .to_f32_px() /
+ columns[column_index].content_sizes.max_content.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 total_percent = columns_with_percentage
.clone()
- .map(|column_index| self.columns[column_index].percentage.0)
+ .map(|column_index| columns[column_index].percentage.unwrap_or_default().0)
.sum::<f32>();
if total_percent > 0. {
for column_index in columns_with_percentage {
- column_sizes[column_index] += extra_inline_size
- .scale_by(self.columns[column_index].percentage.0 / total_percent);
+ let column_percentage = columns[column_index].percentage.unwrap_or_default();
+ column_sizes[column_index] +=
+ extra_inline_size.scale_by(column_percentage.0 / total_percent);
}
return;
}
@@ -1130,8 +1032,7 @@ impl<'a> TableLayout<'a> {
// > Otherwise, the distributed widths of all columns are increased by equal amounts so the
// total increase adds to the excess width.
- let extra_space_for_all_columns =
- extra_inline_size.scale_by(1.0 / self.table.size.width as f32);
+ let extra_space_for_all_columns = extra_inline_size.scale_by(1.0 / columns.len() as f32);
for guess in column_sizes.iter_mut() {
*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);
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 {
let cell_percentage = self.cell_measures[row_index][column_index]
.block
.percentage
+ .unwrap_or_default()
.0;
percentage = percentage.max(cell_percentage);
@@ -1821,7 +1723,7 @@ impl<'a> TableLayout<'a> {
containing_block_for_children: &ContainingBlock,
) -> BoxFragment {
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(
layout_context,
containing_block_for_children,
@@ -2650,8 +2552,13 @@ impl Table {
None => return CellOrTrackMeasure::zero(),
};
- let (size, min_size, max_size, _, percentage_contribution) =
- get_outer_sizes_for_measurement(&column.style, writing_mode, &LogicalVec2::zero());
+ let CellOrColumnOuterSizes {
+ preferred: preferred_size,
+ min: min_size,
+ max: max_size,
+ percentage: percentage_size,
+ ..
+ } = CellOrColumnOuterSizes::new(&column.style, writing_mode, &Default::default());
CellOrTrackMeasure {
content_sizes: ContentSizes {
@@ -2663,9 +2570,11 @@ impl Table {
// > The outer max-content width of a table-column or table-column-group is
// > max(min-width, min(max-width, width)).
// 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(
size: &LogicalVec2<Size<ComputedLengthPercentage>>,
max_size: &LogicalVec2<Size<ComputedLengthPercentage>>,
-) -> LogicalVec2<Percentage> {
+) -> LogicalVec2<Option<Percentage>> {
// From <https://drafts.csswg.org/css-tables/#percentage-contribution>
// > 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
@@ -2922,13 +2831,11 @@ fn get_size_percentage_contribution(
|size: &Size<ComputedLengthPercentage>, max_size: &Size<ComputedLengthPercentage>| {
let size_percentage = size
.to_numeric()
- .and_then(|length_percentage| length_percentage.to_percentage())
- .unwrap_or(Percentage(0.));
+ .and_then(|length_percentage| length_percentage.to_percentage());
let max_size_percentage = max_size
.to_numeric()
- .and_then(|length_percentage| length_percentage.to_percentage())
- .unwrap_or(Percentage(f32::INFINITY));
- Percentage(size_percentage.0.min(max_size_percentage.0))
+ .and_then(|length_percentage| length_percentage.to_percentage());
+ max_two_optional_percentages(size_percentage, max_size_percentage)
};
LogicalVec2 {
@@ -2937,43 +2844,60 @@ fn get_size_percentage_contribution(
}
}
-fn get_outer_sizes_for_measurement(
- style: &Arc<ComputedValues>,
- writing_mode: WritingMode,
- padding_border_sums: &LogicalVec2<Au>,
-) -> (
- LogicalVec2<Au>,
- 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);
- let min_size = style.min_box_size(writing_mode);
- let max_size = style.max_box_size(writing_mode);
- (
- outer_size(size.map(|v| get_size_for_axis(v).unwrap_or_else(Au::zero))),
- outer_size(min_size.map(|v| get_size_for_axis(v).unwrap_or_else(Au::zero))),
- outer_size(max_size.map(|v| get_size_for_axis(v).unwrap_or(MAX_AU))),
- !size.inline.is_numeric(),
- get_size_percentage_contribution(&size, &max_size),
- )
+struct CellOrColumnOuterSizes {
+ min: LogicalVec2<Au>,
+ preferred: LogicalVec2<Au>,
+ max: LogicalVec2<Option<Au>>,
+ percentage: LogicalVec2<Option<Percentage>>,
+ inline_preferred_size_is_auto: bool,
+}
+
+impl CellOrColumnOuterSizes {
+ fn new(
+ style: &Arc<ComputedValues>,
+ writing_mode: WritingMode,
+ padding_border_sums: &LogicalVec2<Au>,
+ ) -> Self {
+ 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 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> {
@@ -2991,3 +2915,28 @@ impl RowspanToDistribute<'_> {
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
+ }
+}