blob: 2ef5202bc0c262f11fc95e58bea9d491a3e8a755 [file] [log] [blame] [edit]
/* This Source Code Form is subject to the terms of the Mozilla Public
* 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 core::cmp::Ordering;
use std::mem;
use std::ops::Range;
use app_units::Au;
use atomic_refcell::AtomicRef;
use log::warn;
use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator};
use servo_arc::Arc;
use style::Zero;
use style::computed_values::border_collapse::T as BorderCollapse;
use style::computed_values::box_sizing::T as BoxSizing;
use style::computed_values::caption_side::T as CaptionSide;
use style::computed_values::empty_cells::T as EmptyCells;
use style::computed_values::position::T as Position;
use style::computed_values::table_layout::T as TableLayoutMode;
use style::computed_values::visibility::T as Visibility;
use style::properties::ComputedValues;
use style::values::computed::{
BorderStyle, LengthPercentage as ComputedLengthPercentage, Percentage,
};
use style::values::generics::box_::{GenericVerticalAlign as VerticalAlign, VerticalAlignKeyword};
use super::{
ArcRefCell, CollapsedBorder, CollapsedBorderLine, SpecificTableGridInfo, Table, TableCaption,
TableLayoutStyle, TableSlot, TableSlotCell, TableSlotCoordinates, TableTrack, TableTrackGroup,
};
use crate::context::LayoutContext;
use crate::formatting_contexts::Baselines;
use crate::fragment_tree::{
BoxFragment, CollapsedBlockMargins, ExtraBackground, Fragment, FragmentFlags,
PositioningFragment, SpecificLayoutInfo,
};
use crate::geom::{
LogicalRect, LogicalSides, LogicalSides1D, LogicalVec2, PhysicalPoint, PhysicalRect,
PhysicalSides, PhysicalVec, ToLogical, ToLogicalWithContainingBlock,
};
use crate::layout_box_base::CacheableLayoutResult;
use crate::positioned::{PositioningContext, PositioningContextLength, relative_adjustement};
use crate::sizing::{
ComputeInlineContentSizes, ContentSizes, InlineContentSizesResult, Size, SizeConstraint,
};
use crate::style_ext::{
BorderStyleColor, Clamp, ComputedValuesExt, LayoutStyle, PaddingBorderMargin,
};
use crate::{
ConstraintSpace, ContainingBlock, ContainingBlockSize, IndefiniteContainingBlock, WritingMode,
};
/// A result of a final or speculative layout of a single cell in
/// the table. Note that this is only done for slots that are not
/// covered by spans or empty.
struct CellLayout {
layout: CacheableLayoutResult,
padding: LogicalSides<Au>,
border: LogicalSides<Au>,
positioning_context: PositioningContext,
}
impl CellLayout {
fn ascent(&self) -> Au {
self.layout
.baselines
.first
.unwrap_or(self.layout.content_block_size)
}
/// The block size of this laid out cell including its border and padding.
fn outer_block_size(&self) -> Au {
self.layout.content_block_size + self.border.block_sum() + self.padding.block_sum()
}
/// Whether the cell has no in-flow or out-of-flow contents, other than collapsed whitespace.
/// Note this logic differs from 'empty-cells', which counts abspos contents as empty.
fn is_empty(&self) -> bool {
self.layout.fragments.is_empty()
}
/// Whether the cell is considered empty for the purpose of the 'empty-cells' property.
fn is_empty_for_empty_cells(&self) -> bool {
self.layout
.fragments
.iter()
.all(|fragment| matches!(fragment, Fragment::AbsoluteOrFixedPositioned(_)))
}
}
/// 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,
content_sizes: ContentSizes,
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 {
fn new(style_color: BorderStyleColor, width: Au) -> Self {
Self { style_color, width }
}
fn from_layout_style(
layout_style: &LayoutStyle,
writing_mode: WritingMode,
) -> LogicalSides<Self> {
let border_style_color = layout_style.style().border_style_color(writing_mode);
let border_width = layout_style.border_width(writing_mode);
LogicalSides {
inline_start: Self::new(border_style_color.inline_start, border_width.inline_start),
inline_end: Self::new(border_style_color.inline_end, border_width.inline_end),
block_start: Self::new(border_style_color.block_start, border_width.block_start),
block_end: Self::new(border_style_color.block_end, border_width.block_end),
}
}
fn max_assign(&mut self, other: &Self) {
if *self < *other {
*self = other.clone();
}
}
fn max_assign_to_slice(&self, slice: &mut [CollapsedBorder]) {
for collapsed_border in slice {
collapsed_border.max_assign(self)
}
}
fn hide(&mut self) {
self.style_color = BorderStyleColor::hidden();
self.width = Au::zero();
}
}
/// <https://drafts.csswg.org/css-tables/#border-specificity>
/// > Given two borders styles, the border style having the most specificity is the border style which…
/// > 1. … has the value "hidden" as border-style, if only one does
/// > 2. … has the biggest border-width, once converted into css pixels
/// > 3. … has the border-style which comes first in the following list:
/// > double, solid, dashed, dotted, ridge, outset, groove, inset, none
impl PartialOrd for CollapsedBorder {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
let is_hidden = |border: &Self| border.style_color.style == BorderStyle::Hidden;
let style_specificity = |border: &Self| match border.style_color.style {
BorderStyle::None => 0,
BorderStyle::Inset => 1,
BorderStyle::Groove => 2,
BorderStyle::Outset => 3,
BorderStyle::Ridge => 4,
BorderStyle::Dotted => 5,
BorderStyle::Dashed => 6,
BorderStyle::Solid => 7,
BorderStyle::Double => 8,
BorderStyle::Hidden => 9,
};
let candidate = (is_hidden(self).cmp(&is_hidden(other)))
.then_with(|| self.width.cmp(&other.width))
.then_with(|| style_specificity(self).cmp(&style_specificity(other)));
if !candidate.is_eq() || self.style_color.color == other.style_color.color {
Some(candidate)
} else {
None
}
}
}
impl Eq for CollapsedBorder {}
type CollapsedBorders = LogicalVec2<Vec<CollapsedBorderLine>>;
/// 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>
pub(crate) struct TableLayout<'a> {
table: &'a Table,
pbm: PaddingBorderMargin,
rows: Vec<RowLayout>,
columns: Vec<ColumnLayout>,
cell_measures: Vec<Vec<LogicalVec2<CellOrTrackMeasure>>>,
/// The calculated width of the table, including space for the grid and also for any
/// captions.
table_width: Au,
/// The table width minus the total horizontal border spacing (if any). This is the
/// width that we will be able to allocate to the columns.
assignable_width: Au,
final_table_height: Au,
distributed_column_widths: Vec<Au>,
row_sizes: Vec<Au>,
/// The accumulated baseline of each row, relative to the top of the row.
row_baselines: Vec<Au>,
cells_laid_out: Vec<Vec<Option<CellLayout>>>,
basis_for_cell_padding_percentage: Au,
/// Information about collapsed borders.
collapsed_borders: Option<CollapsedBorders>,
is_in_fixed_mode: bool,
}
#[derive(Clone, Debug)]
struct CellOrTrackMeasure {
content_sizes: ContentSizes,
percentage: Option<Percentage>,
}
impl Zero for CellOrTrackMeasure {
fn zero() -> Self {
Self {
content_sizes: ContentSizes::zero(),
percentage: None,
}
}
fn is_zero(&self) -> bool {
self.content_sizes.is_zero() && self.percentage.is_none()
}
}
impl<'a> TableLayout<'a> {
fn new(table: &'a Table) -> TableLayout<'a> {
// The CSSWG resolved that only `inline-size: auto` can prevent fixed table mode.
// <https://github.com/w3c/csswg-drafts/issues/10937#issuecomment-2669150397>
let style = &table.style;
let is_in_fixed_mode = style.get_table().table_layout == TableLayoutMode::Fixed &&
!style.box_size(style.writing_mode).inline.is_initial();
Self {
table,
pbm: PaddingBorderMargin::zero(),
rows: Vec::new(),
columns: Vec::new(),
cell_measures: Vec::new(),
table_width: Au::zero(),
assignable_width: Au::zero(),
final_table_height: Au::zero(),
distributed_column_widths: Vec::new(),
row_sizes: Vec::new(),
row_baselines: Vec::new(),
cells_laid_out: Vec::new(),
basis_for_cell_padding_percentage: Au::zero(),
collapsed_borders: None,
is_in_fixed_mode,
}
}
/// This is an implementation of *Computing Cell Measures* from
/// <https://drafts.csswg.org/css-tables/#computing-cell-measures>.
pub(crate) fn compute_cell_measures(
&mut self,
layout_context: &LayoutContext,
writing_mode: WritingMode,
) {
let row_measures = vec![LogicalVec2::zero(); self.table.size.width];
self.cell_measures = vec![row_measures; self.table.size.height];
for row_index in 0..self.table.size.height {
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,
}
.borrow();
let layout_style = cell.layout_style();
let padding = layout_style
.padding(writing_mode)
.percentages_relative_to(Au::zero());
let border = self
.get_collapsed_border_widths_for_area(LogicalSides {
inline_start: column_index,
inline_end: column_index + cell.colspan,
block_start: row_index,
block_end: row_index + cell.rowspan,
})
.unwrap_or_else(|| layout_style.border_width(writing_mode));
let padding_border_sums = LogicalVec2 {
inline: padding.inline_sum() + border.inline_sum(),
block: padding.block_sum() + border.block_sum(),
};
let CellOrColumnOuterSizes {
preferred: preferred_size,
min: min_size,
max: max_size,
percentage: percentage_size,
} = CellOrColumnOuterSizes::new(
&cell.base.style,
writing_mode,
&padding_border_sums,
self.is_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
// > for the purpose of width computation, the aggregation algorithm for column sizing considers
// > only table-cells belonging to the first row track
let inline_measure = if self.is_in_fixed_mode {
if row_index > 0 {
CellOrTrackMeasure::zero()
} else {
CellOrTrackMeasure {
content_sizes: preferred_size.inline.into(),
percentage: percentage_size.inline,
}
}
} else {
let inline_content_sizes = cell.inline_content_sizes(layout_context) +
padding_border_sums.inline.into();
assert!(
inline_content_sizes.max_content >= inline_content_sizes.min_content,
"the max-content size should never be smaller than the min-content size"
);
// These formulas differ from the spec, but seem to match Gecko and Blink.
let outer_min_content_width = inline_content_sizes
.min_content
.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(preferred_size.inline)
.clamp_between_extremums(min_size.inline, max_size.inline)
} else {
inline_content_sizes
.max_content
.max(preferred_size.inline)
.clamp_between_extremums(min_size.inline, max_size.inline)
};
assert!(outer_min_content_width <= outer_max_content_width);
CellOrTrackMeasure {
content_sizes: ContentSizes {
min_content: outer_min_content_width,
max_content: outer_max_content_width,
},
percentage: percentage_size.inline,
}
};
// This measure doesn't 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.
let block_measure = CellOrTrackMeasure {
content_sizes: preferred_size.block.into(),
percentage: percentage_size.block,
};
self.cell_measures[row_index][column_index] = LogicalVec2 {
inline: inline_measure,
block: block_measure,
};
}
}
}
/// Compute the constrainedness of every column in the table.
///
/// > 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_track_constrainedness_and_has_originating_cells(
&mut self,
writing_mode: WritingMode,
) {
self.rows = vec![RowLayout::default(); self.table.size.height];
self.columns = vec![ColumnLayout::default(); self.table.size.width];
let is_length = |size: &Size<ComputedLengthPercentage>| {
size.to_numeric().is_some_and(|size| !size.has_percentage())
};
for column_index in 0..self.table.size.width {
if let Some(column) = self.table.columns.get(column_index) {
let column = column.borrow();
if is_length(&column.base.style.box_size(writing_mode).inline) {
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].borrow();
if is_length(&column_group.base.style.box_size(writing_mode).inline) {
self.columns[column_index].constrained = true;
continue;
}
}
}
}
for row_index in 0..self.table.size.height {
if let Some(row) = self.table.rows.get(row_index) {
let row = row.borrow();
if is_length(&row.base.style.box_size(writing_mode).block) {
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].borrow();
if is_length(&row_group.base.style.box_size(writing_mode).block) {
self.rows[row_index].constrained = true;
continue;
}
}
}
}
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) {
Some(cell) if cell.colspan == 1 => {
cell.base.style.box_size(writing_mode).map(is_length)
},
_ => LogicalVec2::default(),
};
let rowspan_greater_than_1 = match self.table.slots[row_index][column_index] {
TableSlot::Cell(ref cell) => cell.borrow().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.block;
let has_originating_cell =
matches!(self.table.get_slot(coords), Some(TableSlot::Cell(_)));
self.columns[column_index].has_originating_cells |= has_originating_cell;
self.columns[column_index].constrained |= cell_constrained.inline;
}
}
}
/// This is an implementation of *Computing Column Measures* from
/// <https://drafts.csswg.org/css-tables/#computing-column-measures>.
fn compute_column_measures(&mut self, writing_mode: WritingMode) {
// Compute the column measures only taking into account cells with colspan == 1.
// This is the base case that will be used to iteratively account for cells with
// larger colspans afterward.
//
// > min-content width of a column based on cells of span up to 1
// > The largest of:
// > - the width specified for the column:
// > - the outer min-content width of its corresponding table-column,
// > if any (and not auto)
// > - the outer min-content width of its corresponding table-column-group, if any
// > - or 0, if there is none
// > - the outer min-content width of each cell that spans the column whose colSpan
// > is 1 (or just the one in the first row in fixed mode) or 0 if there is none
// >
// > max-content width of a column based on cells of span up to 1
// > The largest of:
// > - the outer max-content width of its corresponding
// > table-column-group, if any
// > - the outer max-content width of its corresponding table-column, if any
// > - the outer max-content width of each cell that spans the column
// > whose colSpan is 1 (or just the one in the first row if in fixed mode) or 0
// > if there is no such cell
// >
// > intrinsic percentage width of a column based on cells of span up to 1
// > The largest of the percentage contributions of each cell that spans the column whose colSpan is
// > 1, of its corresponding table-column (if any), and of its corresponding table-column-group (if
// > any)
//
// 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 colspan_cell_constraints = Vec::new();
for column_index in 0..self.table.size.width {
let column = &mut self.columns[column_index];
let column_measure = self.table.get_column_measure_for_column_at_index(
writing_mode,
column_index,
self.is_in_fixed_mode,
);
column.content_sizes = column_measure.content_sizes;
column.percentage = column_measure.percentage;
for row_index in 0..self.table.size.height {
let coords = TableSlotCoordinates::new(column_index, row_index);
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,
}
.borrow();
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.
column.incorporate_cell_measure(cell_measure);
}
}
// 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:
// > * the intrinsic percentage width of the column based on cells of span up to N,
// > where N is the number of columns in the table
// > * 100% minus the sum of the intrinsic percentage width of all prior columns in
// > 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() {
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 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);
}
}
/// 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;
}
}
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 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 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);
}
}
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));
}
}
}
fn compute_measures(&mut self, layout_context: &LayoutContext, writing_mode: WritingMode) {
self.compute_track_constrainedness_and_has_originating_cells(writing_mode);
self.compute_cell_measures(layout_context, writing_mode);
self.compute_column_measures(writing_mode);
}
/// Compute the GRIDMIN and GRIDMAX.
fn compute_grid_min_max(&self) -> ContentSizes {
// https://drafts.csswg.org/css-tables/#gridmin:
// > The row/column-grid width minimum (GRIDMIN) width is the sum of the min-content width of
// > all the columns plus cell spacing or borders.
// https://drafts.csswg.org/css-tables/#gridmax:
// > The row/column-grid width maximum (GRIDMAX) width is the sum of the max-content width of
// > all the columns plus cell spacing or borders.
//
// The specification doesn't say what to do with columns with percentages, so we follow the
// approach that LayoutNG takes here. We try to figure out the size contribution
// of the percentage columns, by working backward to find the calculated
// percentage of non-percent columns and using that to calculate the size of the
// percent columns.
let mut largest_percentage_column_max_size = Au::zero();
let mut percent_sum = 0.;
let mut non_percent_columns_max_sum = Au::zero();
let mut grid_min_max = ContentSizes::zero();
for column in self.columns.iter() {
match column.percentage {
Some(percentage) if !percentage.is_zero() => {
largest_percentage_column_max_size.max_assign(
column
.content_sizes
.max_content
.scale_by(1.0 / percentage.0),
);
percent_sum += percentage.0;
},
_ => {
non_percent_columns_max_sum += column.content_sizes.max_content;
},
}
grid_min_max += column.content_sizes;
}
grid_min_max
.max_content
.max_assign(largest_percentage_column_max_size);
// Do not take into account percentage of columns when this table is a descendant
// of a flex, grid, or table container. These modes with percentage columns can
// cause inline width to become infinitely wide.
if !percent_sum.is_zero() &&
self.table
.percentage_columns_allowed_for_inline_content_sizes
{
let total_inline_size =
non_percent_columns_max_sum.scale_by(1.0 / (1.0 - percent_sum.min(1.0)));
grid_min_max.max_content.max_assign(total_inline_size);
}
assert!(
grid_min_max.min_content <= grid_min_max.max_content,
"GRIDMAX should never be smaller than GRIDMIN {:?}",
grid_min_max
);
let inline_border_spacing = self.table.total_border_spacing().inline;
grid_min_max.min_content += inline_border_spacing;
grid_min_max.max_content += inline_border_spacing;
grid_min_max
}
/// Compute CAPMIN: <https://drafts.csswg.org/css-tables/#capmin>
fn compute_caption_minimum_inline_size(&self, layout_context: &LayoutContext) -> Au {
let containing_block = IndefiniteContainingBlock {
size: LogicalVec2::default(),
writing_mode: self.table.style.writing_mode,
};
self.table
.captions
.iter()
.map(|caption| {
caption
.borrow()
.context
.outer_inline_content_sizes(
layout_context,
&containing_block,
&LogicalVec2::zero(),
false, /* auto_block_size_stretches_to_containing_block */
)
.sizes
.min_content
})
.max()
.unwrap_or_default()
}
fn compute_table_width(&mut self, containing_block_for_children: &ContainingBlock) {
// This assumes that the parent formatting context computed the correct inline size
// of the table, by enforcing its min-content size as a minimum.
// This should be roughly equivalent to what the spec calls "used width of a table".
// https://drafts.csswg.org/css-tables/#used-width-of-table
self.table_width = containing_block_for_children.size.inline;
// > The assignable table width is the used width of the table minus the total horizontal
// > border spacing (if any). This is the width that we will be able to allocate to the
// > columns.
self.assignable_width = self.table_width - self.table.total_border_spacing().inline;
// This is the amount that we will use to resolve percentages in the padding of cells.
// It matches what Gecko and Blink do, though they disagree when there is a big caption.
self.basis_for_cell_padding_percentage =
self.table_width - self.table.border_spacing().inline * 2;
}
/// 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(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 columns.is_empty() {
return Vec::new();
}
// > First, each column of the table is assigned a sizing type:
// > * percent-column: a column whose any constraint is defined to use a percentage only
// > (with a value different from 0%)
// > * pixel-column: column whose any constraint is defined to use a defined length only
// > (and is not a percent-column)
// > * auto-column: any other column
// >
// > Then, valid sizing methods are to be assigned to the columns by sizing type, yielding
// > the following sizing-guesses:
// >
// > * The min-content sizing-guess is the set of column width assignments where
// > each column is assigned its min-content width.
// > * The min-content-percentage sizing-guess is the set of column width assignments where:
// > * each percent-column is assigned the larger of:
// > * its intrinsic percentage width times the assignable width and
// > * its min-content width.
// > * all other columns are assigned their min-content width.
// > * The min-content-specified sizing-guess is the set of column width assignments where:
// > * each percent-column is assigned the larger of:
// > * its intrinsic percentage width times the assignable width and
// > * its min-content width
// > * any other column that is constrained is assigned its max-content width
// > * all other columns are assigned their min-content width.
// > * The max-content sizing-guess is the set of column width assignments where:
// > * each percent-column is assigned the larger of:
// > * its intrinsic percentage width times the assignable width and
// > * its min-content width
// > * all other columns are assigned their max-content width.
let mut min_content_sizing_guesses = Vec::new();
let mut min_content_percentage_sizing_guesses = Vec::new();
let mut min_content_specified_sizing_guesses = Vec::new();
let mut max_content_sizing_guesses = Vec::new();
for column in columns {
let min_content_width = column.content_sizes.min_content;
let max_content_width = column.content_sizes.max_content;
let constrained = column.constrained;
let (
min_content_percentage_sizing_guess,
min_content_specified_sizing_guess,
max_content_sizing_guess,
) = 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 {
(min_content_width, max_content_width, max_content_width)
} else {
(min_content_width, min_content_width, max_content_width)
};
min_content_sizing_guesses.push(min_content_width);
min_content_percentage_sizing_guesses.push(min_content_percentage_sizing_guess);
min_content_specified_sizing_guesses.push(min_content_specified_sizing_guess);
max_content_sizing_guesses.push(max_content_sizing_guess);
}
// > If the assignable table width is less than or equal to the max-content sizing-guess, the
// > used widths of the columns must be the linear combination (with weights adding to 1) of
// > the two consecutive sizing-guesses whose width sums bound the available width.
//
// > Otherwise, the used widths of the columns are the result of starting from the max-content
// > sizing-guess and distributing the excess width to the columns of the table according to
// > the rules for distributing excess width to columns (for used width).
fn sum(guesses: &[Au]) -> Au {
guesses.iter().fold(Au::zero(), |sum, guess| sum + *guess)
}
let max_content_sizing_sum = sum(&max_content_sizing_guesses);
if target_inline_size >= max_content_sizing_sum {
Self::distribute_extra_width_to_columns(
columns,
&mut max_content_sizing_guesses,
max_content_sizing_sum,
target_inline_size,
);
return max_content_sizing_guesses;
}
let min_content_specified_sizing_sum = sum(&min_content_specified_sizing_guesses);
if target_inline_size == min_content_specified_sizing_sum {
return min_content_specified_sizing_guesses;
}
let min_content_percentage_sizing_sum = sum(&min_content_percentage_sizing_guesses);
if target_inline_size == min_content_percentage_sizing_sum {
return min_content_percentage_sizing_guesses;
}
let min_content_sizes_sum = sum(&min_content_sizing_guesses);
if target_inline_size <= min_content_sizes_sum {
return min_content_sizing_guesses;
}
let bounds = |sum_a, sum_b| target_inline_size > sum_a && target_inline_size < sum_b;
let blend = |a: &[Au], sum_a: Au, b: &[Au], sum_b: Au| {
// First convert the Au units to f32 in order to do floating point division.
let weight_a = (target_inline_size - sum_b).to_f32_px() / (sum_a - sum_b).to_f32_px();
let weight_b = 1.0 - weight_a;
let mut remaining_assignable_width = target_inline_size;
let mut widths: Vec<Au> = a
.iter()
.zip(b.iter())
.map(|(guess_a, guess_b)| {
let column_width = guess_a.scale_by(weight_a) + guess_b.scale_by(weight_b);
// Clamp to avoid exceeding the assignable width. This could otherwise
// happen when dealing with huge values whose sum is clamped to MAX_AU.
let column_width = column_width.min(remaining_assignable_width);
remaining_assignable_width -= column_width;
column_width
})
.collect();
if !remaining_assignable_width.is_zero() {
// The computations above can introduce floating-point imprecisions.
// Since these errors are very small (1Au), it's fine to simply adjust
// the first column such that the total width matches the assignable width
debug_assert!(
remaining_assignable_width >= Au::zero(),
"Sum of columns shouldn't exceed the assignable table width"
);
debug_assert!(
remaining_assignable_width <= Au::new(widths.len() as i32),
"A deviation of more than one Au per column is unlikely to be caused by float imprecision"
);
// We checked if the table had columns at the top of the function, so there
// always is a first column
widths[0] += remaining_assignable_width;
}
debug_assert!(widths.iter().sum::<Au>() == target_inline_size);
widths
};
if bounds(min_content_sizes_sum, min_content_percentage_sizing_sum) {
return blend(
&min_content_sizing_guesses,
min_content_sizes_sum,
&min_content_percentage_sizing_guesses,
min_content_percentage_sizing_sum,
);
}
if bounds(
min_content_percentage_sizing_sum,
min_content_specified_sizing_sum,
) {
return blend(
&min_content_percentage_sizing_guesses,
min_content_percentage_sizing_sum,
&min_content_specified_sizing_guesses,
min_content_specified_sizing_sum,
);
}
assert!(bounds(
min_content_specified_sizing_sum,
max_content_sizing_sum
));
blend(
&min_content_specified_sizing_guesses,
min_content_specified_sizing_sum,
&max_content_sizing_guesses,
max_content_sizing_sum,
)
}
/// 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(
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| 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| {
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| 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
// > grow by this rule), the distributed widths of the columns allowed to grow by this rule
// > are increased in proportion to max-content width so the total increase adds to the
// > excess width.
let unconstrained_max_content_columns = all_columns
.clone()
.filter(is_unconstrained)
.filter(has_originating_cells)
.filter(has_percent_zero)
.filter(has_max_content);
let total_max_content_width = unconstrained_max_content_columns
.clone()
.map(max_content_sum)
.fold(Au::zero(), |a, b| a + b);
if !total_max_content_width.is_zero() {
for column_index in unconstrained_max_content_columns {
column_sizes[column_index] += extra_inline_size.scale_by(
columns[column_index].content_sizes.max_content.to_f32_px() /
total_max_content_width.to_f32_px(),
);
}
return;
}
// > Otherwise, if there are non-constrained columns that have originating cells with intrinsic
// > percentage width of 0% (aka the columns allowed to grow by this rule, which thanks to the
// > previous rule must have zero max-content width), the distributed widths of the columns
// > allowed to grow by this rule are increased by equal amounts so the total increase adds to
// > the excess width.V
let unconstrained_no_percent_columns = all_columns
.clone()
.filter(is_unconstrained)
.filter(has_originating_cells)
.filter(has_percent_zero);
let total_unconstrained_no_percent = unconstrained_no_percent_columns.clone().count();
if total_unconstrained_no_percent > 0 {
let extra_space_per_column =
extra_inline_size.scale_by(1.0 / total_unconstrained_no_percent as f32);
for column_index in unconstrained_no_percent_columns {
column_sizes[column_index] += extra_space_per_column;
}
return;
}
// > Otherwise, if there are constrained columns with intrinsic percentage width of 0% and
// > with nonzero max-content width (aka the columns allowed to grow by this rule, which, due
// > to other rules, must have originating cells), the distributed widths of the columns
// > allowed to grow by this rule are increased in proportion to max-content width so the
// > total increase adds to the excess width.
let constrained_max_content_columns = all_columns
.clone()
.filter(is_constrained)
.filter(has_originating_cells)
.filter(has_percent_zero)
.filter(has_max_content);
let total_max_content_width = constrained_max_content_columns
.clone()
.map(max_content_sum)
.fold(Au::zero(), |a, b| a + b);
if !total_max_content_width.is_zero() {
for column_index in constrained_max_content_columns {
column_sizes[column_index] += extra_inline_size.scale_by(
columns[column_index].content_sizes.max_content.to_f32_px() /
total_max_content_width.to_f32_px(),
);
}
return;
}
// > Otherwise, if there are columns with intrinsic percentage width greater than 0% (aka the
// > columns allowed to grow by this rule, which, due to other rules, must have originating
// > cells), the distributed widths of the columns allowed to grow by this rule are increased
// > in proportion to intrinsic percentage width so the total increase adds to the excess
// > width.
let columns_with_percentage = all_columns.clone().filter(has_percent_greater_than_zero);
let total_percent = columns_with_percentage
.clone()
.map(|column_index| columns[column_index].percentage.unwrap_or_default().0)
.sum::<f32>();
if total_percent > 0. {
for column_index in columns_with_percentage {
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;
}
// > Otherwise, if there is any such column, the distributed widths of all columns that have
// > originating cells are increased by equal amounts so the total increase adds to the excess
// > width.
let has_originating_cells_columns = all_columns.clone().filter(has_originating_cells);
let total_has_originating_cells = has_originating_cells_columns.clone().count();
if total_has_originating_cells > 0 {
let extra_space_per_column =
extra_inline_size.scale_by(1.0 / total_has_originating_cells as f32);
for column_index in has_originating_cells_columns {
column_sizes[column_index] += extra_space_per_column;
}
return;
}
// > 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 / columns.len() as f32);
for guess in column_sizes.iter_mut() {
*guess += extra_space_for_all_columns;
}
}
/// This is an implementation of *Row layout (first pass)* from
/// <https://drafts.csswg.org/css-tables/#row-layout>.
fn layout_cells_in_row(
&mut self,
layout_context: &LayoutContext,
containing_block_for_table: &ContainingBlock,
) {
let layout_table_slot = |coordinate: TableSlotCoordinates, slot: &TableSlot| {
let TableSlot::Cell(cell) = slot else {
return None;
};
let cell = cell.borrow();
let area = LogicalSides {
inline_start: coordinate.x,
inline_end: coordinate.x + cell.colspan,
block_start: coordinate.y,
block_end: coordinate.y + cell.rowspan,
};
let layout_style = cell.layout_style();
let border = self
.get_collapsed_border_widths_for_area(area)
.unwrap_or_else(|| {
layout_style.border_width(containing_block_for_table.style.writing_mode)
});
let padding: LogicalSides<Au> = layout_style
.padding(containing_block_for_table.style.writing_mode)
.percentages_relative_to(self.basis_for_cell_padding_percentage);
let inline_border_padding_sum = border.inline_sum() + padding.inline_sum();
let border_spacing_spanned =
self.table.border_spacing().inline * (cell.colspan - 1) as i32;
let mut total_cell_width: Au = (coordinate.x..coordinate.x + cell.colspan)
.map(|column_index| self.distributed_column_widths[column_index])
.sum::<Au>() -
inline_border_padding_sum +
border_spacing_spanned;
total_cell_width = total_cell_width.max(Au::zero());
let containing_block_for_children = ContainingBlock {
size: ContainingBlockSize {
inline: total_cell_width,
block: SizeConstraint::default(),
},
style: &cell.base.style,
};
let mut positioning_context = PositioningContext::default();
let layout = cell.contents.layout(
layout_context,
&mut positioning_context,
&containing_block_for_children,
);
Some(CellLayout {
layout,
padding,
border,
positioning_context,
})
};
self.cells_laid_out = if layout_context.use_rayon {
self.table
.slots
.par_iter()
.enumerate()
.map(|(row_index, row_slots)| {
row_slots
.par_iter()
.enumerate()
.map(|(column_index, slot)| {
layout_table_slot(
TableSlotCoordinates::new(column_index, row_index),
slot,
)
})
.collect()
})
.collect()
} else {
self.table
.slots
.iter()
.enumerate()
.map(|(row_index, row_slots)| {
row_slots
.iter()
.enumerate()
.map(|(column_index, slot)| {
layout_table_slot(
TableSlotCoordinates::new(column_index, row_index),
slot,
)
})
.collect()
})
.collect()
};
// Now go through all cells laid out and update the cell measure based on the size
// determined during layout.
for row_index in 0..self.table.size.height {
for column_index in 0..self.table.size.width {
let Some(layout) = &self.cells_laid_out[row_index][column_index] else {
continue;
};
self.cell_measures[row_index][column_index]
.block
.content_sizes
.max_assign(layout.outer_block_size().into());
}
}
}
/// 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 cell = cell.borrow();
let outer_block_size = layout.outer_block_size();
if cell.rowspan == 1 {
max_row_height.max_assign(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_assign(ascent + border_padding_start);
// 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_assign(
layout.layout.content_block_size - ascent + border_padding_end,
);
}
}
}
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
}
#[allow(clippy::ptr_arg)] // Needs to be a vec because of the function above
/// 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.;
#[allow(clippy::needless_range_loop)] // It makes sense to use it here
for row_index in 0..self.table.size.height {
let row_measure = self
.table
.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.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);
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) if cell.borrow().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].max_assign(cell_measure.content_sizes.max_content);
continue;
},
_ => continue,
};
cells_to_distribute.push(RowspanToDistribute {
coordinates: TableSlotCoordinates::new(column_index, row_index),
cell: cell.borrow(),
measure: cell_measure,
});
}
self.rows[row_index].percent = Percentage(percentage.min(1. - total_percentage));
total_percentage += self.rows[row_index].percent.0;
}
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)
});
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 [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);
(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;
}
}
}
}
// 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_for_children: &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.
// TODO: for `height: stretch`, the block size of the containing block is the available
// space for the entire table wrapper, but here we are using that amount for the table grid.
// Therefore, if there is a caption, this will cause overflow. Gecko and WebKit have the
// same problem, but not Blink.
let table_height_from_style = match containing_block_for_children.size.block {
SizeConstraint::Definite(size) => size,
SizeConstraint::MinMax(min, _) => min,
};
let block_border_spacing = self.table.total_border_spacing().block;
let table_height_from_rows = row_sizes.iter().sum::<Au>() + block_border_spacing;
self.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 self.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(
self.final_table_height - table_height_from_rows,
0..self.table.size.height,
&mut row_sizes,
Some(self.final_table_height),
false, /* rowspan_distribution */
);
self.row_sizes = row_sizes;
}
fn layout_caption(
&self,
caption: &TableCaption,
layout_context: &LayoutContext,
parent_positioning_context: &mut PositioningContext,
) -> BoxFragment {
let containing_block = &ContainingBlock {
size: ContainingBlockSize {
inline: self.table_width + self.pbm.padding_border_sums.inline,
block: SizeConstraint::default(),
},
style: &self.table.style,
};
// The parent of a caption is the table wrapper, which establishes an independent
// formatting context. Therefore, we don't ignore block margins when resolving a
// stretch block size. https://drafts.csswg.org/css-sizing-4/#stretch-fit-sizing
let ignore_block_margins_for_stretch = LogicalSides1D::new(false, false);
let mut positioning_context =
PositioningContext::new_for_layout_box_base(&caption.context.base);
let mut box_fragment = caption.context.layout_in_flow_block_level(
layout_context,
positioning_context
.as_mut()
.unwrap_or(parent_positioning_context),
containing_block,
None, /* sequential_layout_state */
ignore_block_margins_for_stretch,
);
if let Some(mut positioning_context) = positioning_context.take() {
positioning_context.layout_collected_children(layout_context, &mut box_fragment);
parent_positioning_context.append(positioning_context);
}
box_fragment
}
/// Lay out the table (grid and captions) of this [`TableLayout`] into fragments. This should
/// only be be called after calling [`TableLayout.compute_measures`].
#[servo_tracing::instrument(name = "Table::layout", skip_all)]
fn layout(
mut self,
layout_context: &LayoutContext,
positioning_context: &mut PositioningContext,
containing_block_for_children: &ContainingBlock,
containing_block_for_table: &ContainingBlock,
) -> CacheableLayoutResult {
let table_writing_mode = containing_block_for_children.style.writing_mode;
self.compute_border_collapse(table_writing_mode);
let layout_style = self.table.layout_style(Some(&self));
self.pbm = layout_style
.padding_border_margin_with_writing_mode_and_containing_block_inline_size(
table_writing_mode,
containing_block_for_table.size.inline,
);
self.compute_measures(layout_context, table_writing_mode);
self.compute_table_width(containing_block_for_children);
// The table wrapper is the one that has the CSS properties for the grid's border and padding. This
// weirdness is difficult to express in Servo's layout system. We have the wrapper size itself as if
// those properties applied to it and then just account for the discrepency in sizing here. In reality,
// the wrapper does not draw borders / backgrounds and all of its content (grid and captions) are
// placed with a negative offset in the table wrapper's content box so that they overlap the undrawn
// border / padding area.
//
// TODO: This is a pretty large hack. It would be nicer to actually have the grid sized properly,
// but it works for now.
//
// Get the padding, border, and margin of the table using the inline size of the table's containing
// block but in the writing of the table itself.
// TODO: This is broken for orthoganol flows, because the inline size of the parent isn't necessarily
// the inline size of the table.
let containing_block_for_logical_conversion = ContainingBlock {
size: ContainingBlockSize {
inline: self.table_width,
block: containing_block_for_table.size.block,
},
style: containing_block_for_children.style,
};
let offset_from_wrapper = -self.pbm.padding - self.pbm.border;
let mut current_block_offset = offset_from_wrapper.block_start;
let mut table_layout = CacheableLayoutResult {
fragments: Vec::new(),
content_block_size: Zero::zero(),
content_inline_size_for_table: None,
baselines: Baselines::default(),
depends_on_block_constraints: true,
specific_layout_info: Some(SpecificLayoutInfo::TableWrapper),
collapsible_margins_in_children: CollapsedBlockMargins::zero(),
};
table_layout
.fragments
.extend(self.table.captions.iter().filter_map(|caption| {
let caption = caption.borrow();
if caption.context.style().clone_caption_side() != CaptionSide::Top {
return None;
}
let original_positioning_context_length = positioning_context.len();
let mut caption_fragment =
self.layout_caption(&caption, layout_context, positioning_context);
// The caption is not placed yet. Construct a rectangle for it in the adjusted containing block
// for the table children and only then convert the result to physical geometry.
let caption_pbm = caption_fragment
.padding_border_margin()
.to_logical(table_writing_mode);
let caption_relative_offset = match caption_fragment.style.clone_position() {
Position::Relative => {
relative_adjustement(&caption_fragment.style, containing_block_for_children)
},
_ => LogicalVec2::zero(),
};
caption_fragment.content_rect = LogicalRect {
start_corner: LogicalVec2 {
inline: offset_from_wrapper.inline_start + caption_pbm.inline_start,
block: current_block_offset + caption_pbm.block_start,
} + caption_relative_offset,
size: caption_fragment
.content_rect
.size
.to_logical(table_writing_mode),
}
.as_physical(Some(&containing_block_for_logical_conversion));
current_block_offset += caption_fragment
.margin_rect()
.size
.to_logical(table_writing_mode)
.block;
let caption_fragment = Fragment::Box(ArcRefCell::new(caption_fragment));
positioning_context.adjust_static_position_of_hoisted_fragments(
&caption_fragment,
original_positioning_context_length,
);
caption.context.base.set_fragment(caption_fragment.clone());
Some(caption_fragment)
}));
let original_positioning_context_length = positioning_context.len();
let mut grid_fragment = self.layout_grid(
layout_context,
positioning_context,
&containing_block_for_logical_conversion,
containing_block_for_children,
);
// Take the baseline of the grid fragment, after adjusting it to be in the coordinate system
// of the table wrapper.
let logical_grid_content_rect = grid_fragment
.content_rect
.to_logical(&containing_block_for_logical_conversion);
let grid_pbm = grid_fragment
.padding_border_margin()
.to_logical(table_writing_mode);
table_layout.baselines = grid_fragment.baselines(table_writing_mode).offset(
current_block_offset +
logical_grid_content_rect.start_corner.block +
grid_pbm.block_start,
);
grid_fragment.content_rect = LogicalRect {
start_corner: LogicalVec2 {
inline: offset_from_wrapper.inline_start + grid_pbm.inline_start,
block: current_block_offset + grid_pbm.block_start,
},
size: grid_fragment
.content_rect
.size
.to_logical(table_writing_mode),
}
.as_physical(Some(&containing_block_for_logical_conversion));
current_block_offset += grid_fragment
.border_rect()
.size
.to_logical(table_writing_mode)
.block;
if logical_grid_content_rect.size.inline < self.table_width {
// This can happen when collapsing columns
table_layout.content_inline_size_for_table =
Some(logical_grid_content_rect.size.inline);
}
let grid_fragment = Fragment::Box(ArcRefCell::new(grid_fragment));
positioning_context.adjust_static_position_of_hoisted_fragments(
&grid_fragment,
original_positioning_context_length,
);
table_layout.fragments.push(grid_fragment);
table_layout
.fragments
.extend(self.table.captions.iter().filter_map(|caption| {
let caption = caption.borrow();
if caption.context.style().clone_caption_side() != CaptionSide::Bottom {
return None;
}
let original_positioning_context_length = positioning_context.len();
let mut caption_fragment =
self.layout_caption(&caption, layout_context, positioning_context);
// The caption is not placed yet. Construct a rectangle for it in the adjusted containing block
// for the table children and only then convert the result to physical geometry.
let caption_pbm = caption_fragment
.padding_border_margin()
.to_logical(table_writing_mode);
caption_fragment.content_rect = LogicalRect {
start_corner: LogicalVec2 {
inline: offset_from_wrapper.inline_start + caption_pbm.inline_start,
block: current_block_offset + caption_pbm.block_start,
},
size: caption_fragment
.content_rect
.size
.to_logical(table_writing_mode),
}
.as_physical(Some(&containing_block_for_logical_conversion));
current_block_offset += caption_fragment
.margin_rect()
.size
.to_logical(table_writing_mode)
.block;
let caption_fragment = Fragment::Box(ArcRefCell::new(caption_fragment));
positioning_context.adjust_static_position_of_hoisted_fragments(
&caption_fragment,
original_positioning_context_length,
);
caption.context.base.set_fragment(caption_fragment.clone());
Some(caption_fragment)
}));
table_layout.content_block_size = current_block_offset + offset_from_wrapper.block_end;
table_layout
}
/// Lay out the grid portion of this [`TableLayout`] into fragments. This should only be be
/// called after calling [`TableLayout.compute_measures`].
fn layout_grid(
&mut self,
layout_context: &LayoutContext,
positioning_context: &mut PositioningContext,
containing_block_for_logical_conversion: &ContainingBlock,
containing_block_for_children: &ContainingBlock,
) -> BoxFragment {
self.distributed_column_widths =
Self::distribute_width_to_columns(self.assignable_width, &self.columns);
self.layout_cells_in_row(layout_context, containing_block_for_children);
let table_writing_mode = containing_block_for_children.style.writing_mode;
let first_layout_row_heights = self.do_first_row_layout(table_writing_mode);
self.compute_table_height_and_final_row_heights(
first_layout_row_heights,
containing_block_for_children,
);
assert_eq!(self.table.size.height, self.row_sizes.len());
assert_eq!(self.table.size.width, self.distributed_column_widths.len());
if self.table.size.width == 0 && self.table.size.height == 0 {
let content_rect = LogicalRect {
start_corner: LogicalVec2::zero(),
size: LogicalVec2 {
inline: self.table_width,
block: self.final_table_height,
},
}
.as_physical(Some(containing_block_for_logical_conversion));
return BoxFragment::new(
self.table.grid_base_fragment_info,
self.table.grid_style.clone(),
Vec::new(),
content_rect,
self.pbm.padding.to_physical(table_writing_mode),
self.pbm.border.to_physical(table_writing_mode),
PhysicalSides::zero(),
self.specific_layout_info_for_grid(),
);
}
let mut table_fragments = Vec::new();
let table_and_track_dimensions = TableAndTrackDimensions::new(self);
self.make_fragments_for_columns_and_column_groups(
&table_and_track_dimensions,
&mut table_fragments,
);
let mut baselines = Baselines::default();
let mut row_group_fragment_layout = None;
for row_index in 0..self.table.size.height {
// From <https://drafts.csswg.org/css-align-3/#baseline-export>
// > If any cells in the row participate in first baseline/last baseline alignment along
// > the inline axis, the first/last baseline set of the row is generated from their
// > shared alignment baseline and the row’s first available font, after alignment has
// > been performed. Otherwise, the first/last baseline set of the row is synthesized from
// > the lowest and highest content edges of the cells in the row. [CSS2]
//
// If any cell below has baseline alignment, these values will be overwritten,
// but they are initialized to the content edge of the first row.
if row_index == 0 {
let row_end = table_and_track_dimensions
.get_row_rect(0)
.max_block_position();
baselines.first = Some(row_end);
baselines.last = Some(row_end);
}
let row_is_collapsed = self.is_row_collapsed(row_index);
let table_row = self.table.rows[row_index].borrow();
let mut row_fragment_layout = RowFragmentLayout::new(
&table_row,
row_index,
&table_and_track_dimensions,
&self.table.style,
);
let old_row_group_index = row_group_fragment_layout
.as_ref()
.map(|layout: &RowGroupFragmentLayout| layout.index);
if table_row.group_index != old_row_group_index {
// First create the Fragment for any existing RowGroupFragmentLayout.
if let Some(old_row_group_layout) = row_group_fragment_layout.take() {
table_fragments.push(old_row_group_layout.finish(
layout_context,
positioning_context,
containing_block_for_logical_conversion,
containing_block_for_children,
));
}
// Then, create a new RowGroupFragmentLayout for the current and potentially subsequent rows.
if let Some(new_group_index) = table_row.group_index {
row_group_fragment_layout = Some(RowGroupFragmentLayout::new(
self.table.row_groups[new_group_index].clone(),
new_group_index,
&table_and_track_dimensions,
));
}
}
let column_indices = 0..self.table.size.width;
row_fragment_layout.fragments.reserve(self.table.size.width);
for column_index in column_indices {
self.do_final_cell_layout(
row_index,
column_index,
&table_and_track_dimensions,
&mut baselines,
&mut row_fragment_layout,
row_group_fragment_layout.as_mut(),
positioning_context,
self.is_column_collapsed(column_index) || row_is_collapsed,
);
}
let row_fragment = row_fragment_layout.finish(
layout_context,
positioning_context,
containing_block_for_logical_conversion,
containing_block_for_children,
&mut row_group_fragment_layout,
);
match row_group_fragment_layout.as_mut() {
Some(layout) => layout.fragments.push(row_fragment),
None => table_fragments.push(row_fragment),
}
}
if let Some(row_group_layout) = row_group_fragment_layout.take() {
table_fragments.push(row_group_layout.finish(
layout_context,
positioning_context,
containing_block_for_logical_conversion,
containing_block_for_children,
));
}
let content_rect = LogicalRect {
start_corner: LogicalVec2::zero(),
size: LogicalVec2 {
inline: table_and_track_dimensions.table_rect.max_inline_position(),
block: table_and_track_dimensions.table_rect.max_block_position(),
},
}
.as_physical(Some(containing_block_for_logical_conversion));
BoxFragment::new(
self.table.grid_base_fragment_info,
self.table.grid_style.clone(),
table_fragments,
content_rect,
self.pbm.padding.to_physical(table_writing_mode),
self.pbm.border.to_physical(table_writing_mode),
PhysicalSides::zero(),
self.specific_layout_info_for_grid(),
)
.with_baselines(baselines)
}
fn specific_layout_info_for_grid(&mut self) -> Option<SpecificLayoutInfo> {
mem::take(&mut self.collapsed_borders).map(|mut collapsed_borders| {
// TODO: It would probably be better to use `TableAndTrackDimensions`, since that
// has already taken care of collapsed tracks and knows the final track positions.
let mut track_sizes = LogicalVec2 {
inline: mem::take(&mut self.distributed_column_widths),
block: mem::take(&mut self.row_sizes),
};
for (column_index, column_size) in track_sizes.inline.iter_mut().enumerate() {
if self.is_column_collapsed(column_index) {
mem::take(column_size);
}
}
for (row_index, row_size) in track_sizes.block.iter_mut().enumerate() {
if self.is_row_collapsed(row_index) {
mem::take(row_size);
}
}
let writing_mode = self.table.style.writing_mode;
if !writing_mode.is_bidi_ltr() {
track_sizes.inline.reverse();
collapsed_borders.inline.reverse();
for border_line in &mut collapsed_borders.block {
border_line.reverse();
}
}
SpecificLayoutInfo::TableGridWithCollapsedBorders(Box::new(SpecificTableGridInfo {
collapsed_borders: if writing_mode.is_horizontal() {
PhysicalVec::new(collapsed_borders.inline, collapsed_borders.block)
} else {
PhysicalVec::new(collapsed_borders.block, collapsed_borders.inline)
},
track_sizes: if writing_mode.is_horizontal() {
PhysicalVec::new(track_sizes.inline, track_sizes.block)
} else {
PhysicalVec::new(track_sizes.block, track_sizes.inline)
},
}))
})
}
fn is_row_collapsed(&self, row_index: usize) -> bool {
let Some(row) = &self.table.rows.get(row_index) else {
return false;
};
let row = row.borrow();
if row.base.style.get_inherited_box().visibility == Visibility::Collapse {
return true;
}
let row_group = match row.group_index {
Some(group_index) => self.table.row_groups[group_index].borrow(),
None => return false,
};
row_group.base.style.get_inherited_box().visibility == Visibility::Collapse
}
fn is_column_collapsed(&self, column_index: usize) -> bool {
let Some(column) = &self.table.columns.get(column_index) else {
return false;
};
let column = column.borrow();
if column.base.style.get_inherited_box().visibility == Visibility::Collapse {
return true;
}
let col_group = match column.group_index {
Some(group_index) => self.table.column_groups[group_index].borrow(),
None => return false,
};
col_group.base.style.get_inherited_box().visibility == Visibility::Collapse
}
#[allow(clippy::too_many_arguments)]
fn do_final_cell_layout(
&mut self,
row_index: usize,
column_index: usize,
dimensions: &TableAndTrackDimensions,
baselines: &mut Baselines,
row_fragment_layout: &mut RowFragmentLayout,
row_group_fragment_layout: Option<&mut RowGroupFragmentLayout>,
positioning_context_for_table: &mut PositioningContext,
is_collapsed: bool,
) {
// The PositioningContext for cells is, in order or preference, the PositioningContext of the row,
// the PositioningContext of the row group, or the PositioningContext of the table.
let row_group_positioning_context =
row_group_fragment_layout.and_then(|layout| layout.positioning_context.as_mut());
let positioning_context = row_fragment_layout
.positioning_context
.as_mut()
.or(row_group_positioning_context)
.unwrap_or(positioning_context_for_table);
let layout = match self.cells_laid_out[row_index][column_index].take() {
Some(layout) => layout,
None => {
return;
},
};
let cell = match self.table.slots[row_index][column_index] {
TableSlot::Cell(ref cell) => cell,
_ => {
warn!("Did not find a non-spanned cell at index with layout.");
return;
},
}
.borrow();
// If this cell has baseline alignment, it can adjust the table's overall baseline.
let row_block_offset = row_fragment_layout.rect.start_corner.block;
let row_baseline = self.row_baselines[row_index];
if cell.effective_vertical_align() == VerticalAlignKeyword::Baseline && !layout.is_empty() {
let baseline = row_block_offset + row_baseline;
if row_index == 0 {
baselines.first = Some(baseline);
}
baselines.last = Some(baseline);
}
let mut row_relative_cell_rect = dimensions.get_cell_rect(
TableSlotCoordinates::new(column_index, row_index),
cell.rowspan,
cell.colspan,
);
row_relative_cell_rect.start_corner -= row_fragment_layout.rect.start_corner;
let mut fragment = cell.create_fragment(
layout,
row_relative_cell_rect,
row_baseline,
positioning_context,
&self.table.style,
&row_fragment_layout.containing_block,
is_collapsed,
);
// Make a table part rectangle relative to the row fragment for the purposes of
// drawing extra backgrounds.
//
// This rectangle is an offset between the row fragment and the other table
// part rectangle (row group, column, column group). Everything between them
// is laid out in a left-to-right fashion, but respecting the verticality of
// the writing mode. This is why below, only the axes are flipped, but the
// rectangle is not flipped for RTL.
let make_relative_to_row_start = |mut rect: LogicalRect<Au>| {
rect.start_corner -= row_fragment_layout.rect.start_corner;
let writing_mode = self.table.style.writing_mode;
PhysicalRect::new(
if writing_mode.is_horizontal() {
PhysicalPoint::new(rect.start_corner.inline, rect.start_corner.block)
} else {
PhysicalPoint::new(rect.start_corner.block, rect.start_corner.inline)
},
rect.size.to_physical_size(writing_mode),
)
};
let column = self.table.columns.get(column_index);
let column_group = column
.and_then(|column| column.borrow().group_index)
.and_then(|index| self.table.column_groups.get(index));
if let Some(column_group) = column_group {
let column_group = column_group.borrow();
let rect = make_relative_to_row_start(dimensions.get_column_group_rect(&column_group));
fragment.add_extra_background(ExtraBackground {
style: column_group.shared_background_style.clone(),
rect,
})
}
if let Some(column) = column {
let column = column.borrow();
if !column.is_anonymous {
let rect = make_relative_to_row_start(dimensions.get_column_rect(column_index));
fragment.add_extra_background(ExtraBackground {
style: column.shared_background_style.clone(),
rect,
})
}
}
let row = self.table.rows.get(row_index);
let row_group = row
.and_then(|row| row.borrow().group_index)
.and_then(|index| self.table.row_groups.get(index));
if let Some(row_group) = row_group {
let rect =
make_relative_to_row_start(dimensions.get_row_group_rect(&row_group.borrow()));
fragment.add_extra_background(ExtraBackground {
style: row_group.borrow().shared_background_style.clone(),
rect,
})
}
if let Some(row) = row {
let row = row.borrow();
let rect = make_relative_to_row_start(row_fragment_layout.rect);
fragment.add_extra_background(ExtraBackground {
style: row.shared_background_style.clone(),
rect,
})
}
let fragment = Fragment::Box(ArcRefCell::new(fragment));
cell.base.set_fragment(fragment.clone());
row_fragment_layout.fragments.push(fragment);
}
fn make_fragments_for_columns_and_column_groups(
&self,
dimensions: &TableAndTrackDimensions,
fragments: &mut Vec<Fragment>,
) {
for column_group in self.table.column_groups.iter() {
let column_group = column_group.borrow();
if !column_group.is_empty() {
let fragment = Fragment::Positioning(PositioningFragment::new_empty(
column_group.base.base_fragment_info,
dimensions
.get_column_group_rect(&column_group)
.as_physical(None),
column_group.base.style.clone(),
));
column_group.base.set_fragment(fragment.clone());
fragments.push(fragment);
}
}
for (column_index, column) in self.table.columns.iter().enumerate() {
let column = column.borrow();
let fragment = Fragment::Positioning(PositioningFragment::new_empty(
column.base.base_fragment_info,
dimensions.get_column_rect(column_index).as_physical(None),
column.base.style.clone(),
));
column.base.set_fragment(fragment.clone());
fragments.push(fragment);
}
}
fn compute_border_collapse(&mut self, writing_mode: WritingMode) {
if self.table.style.get_inherited_table().border_collapse != BorderCollapse::Collapse {
self.collapsed_borders = None;
return;
}
let mut collapsed_borders = LogicalVec2 {
block: vec![
vec![Default::default(); self.table.size.width];
self.table.size.height + 1
],
inline: vec![
vec![Default::default(); self.table.size.height];
self.table.size.width + 1
],
};
let apply_border = |collapsed_borders: &mut CollapsedBorders,
layout_style: &LayoutStyle,
block: &Range<usize>,
inline: &Range<usize>| {
let border = CollapsedBorder::from_layout_style(layout_style, writing_mode);
border
.block_start
.max_assign_to_slice(&mut collapsed_borders.block[block.start][inline.clone()]);
border
.block_end
.max_assign_to_slice(&mut collapsed_borders.block[block.end][inline.clone()]);
border
.inline_start
.max_assign_to_slice(&mut collapsed_borders.inline[inline.start][block.clone()]);
border
.inline_end
.max_assign_to_slice(&mut collapsed_borders.inline[inline.end][block.clone()]);
};
let hide_inner_borders = |collapsed_borders: &mut CollapsedBorders,
block: &Range<usize>,
inline: &Range<usize>| {
for x in inline.clone() {
for y in block.clone() {
if x != inline.start {
collapsed_borders.inline[x][y].hide();
}
if y != block.start {
collapsed_borders.block[y][x].hide();
}
}
}
};
let all_rows = 0..self.table.size.height;
let all_columns = 0..self.table.size.width;
for row_index in all_rows.clone() {
for column_index in all_columns.clone() {
let cell = match self.table.slots[row_index][column_index] {
TableSlot::Cell(ref cell) => cell,
_ => continue,
}
.borrow();
let block_range = row_index..row_index + cell.rowspan;
let inline_range = column_index..column_index + cell.colspan;
hide_inner_borders(&mut collapsed_borders, &block_range, &inline_range);
apply_border(
&mut collapsed_borders,
&cell.layout_style(),
&block_range,
&inline_range,
);
}
}
for (row_index, row) in self.table.rows.iter().enumerate() {
let row = row.borrow();
apply_border(
&mut collapsed_borders,
&row.layout_style(),
&(row_index..row_index + 1),
&all_columns,
);
}
for row_group in &self.table.row_groups {
let row_group = row_group.borrow();
apply_border(
&mut collapsed_borders,
&row_group.layout_style(),
&row_group.track_range,
&all_columns,
);
}
for (column_index, column) in self.table.columns.iter().enumerate() {
let column = column.borrow();
apply_border(
&mut collapsed_borders,
&column.layout_style(),
&all_rows,
&(column_index..column_index + 1),
);
}
for column_group in &self.table.column_groups {
let column_group = column_group.borrow();
apply_border(
&mut collapsed_borders,
&column_group.layout_style(),
&all_rows,
&column_group.track_range,
);
}
apply_border(
&mut collapsed_borders,
&self.table.layout_style_for_grid(),
&all_rows,
&all_columns,
);
self.collapsed_borders = Some(collapsed_borders);
}
fn get_collapsed_border_widths_for_area(
&self,
area: LogicalSides<usize>,
) -> Option<LogicalSides<Au>> {
let collapsed_borders = self.collapsed_borders.as_ref()?;
let columns = || area.inline_start..area.inline_end;
let rows = || area.block_start..area.block_end;
let max_width = |slice: &[CollapsedBorder]| {
let slice_widths = slice.iter().map(|collapsed_border| collapsed_border.width);
slice_widths.max().unwrap_or_default()
};
Some(area.map_inline_and_block_axes(
|column| max_width(&collapsed_borders.inline[*column][rows()]) / 2,
|row| max_width(&collapsed_borders.block[*row][columns()]) / 2,
))
}
}
struct RowFragmentLayout<'a> {
row: &'a TableTrack,
rect: LogicalRect<Au>,
containing_block: ContainingBlock<'a>,
positioning_context: Option<PositioningContext>,
fragments: Vec<Fragment>,
}
impl<'a> RowFragmentLayout<'a> {
fn new(
table_row: &'a TableTrack,
index: usize,
dimensions: &TableAndTrackDimensions,
table_style: &'a ComputedValues,
) -> Self {
let rect = dimensions.get_row_rect(index);
let containing_block = ContainingBlock {
size: ContainingBlockSize {
inline: rect.size.inline,
block: SizeConstraint::Definite(rect.size.block),
},
style: table_style,
};
Self {
row: table_row,
rect,
positioning_context: PositioningContext::new_for_layout_box_base(&table_row.base),
containing_block,
fragments: Vec::new(),
}
}
fn finish(
mut self,
layout_context: &LayoutContext,
table_positioning_context: &mut PositioningContext,
containing_block_for_logical_conversion: &ContainingBlock,
containing_block_for_children: &ContainingBlock,
row_group_fragment_layout: &mut Option<RowGroupFragmentLayout>,
) -> Fragment {
if self.positioning_context.is_some() {
self.rect.start_corner +=
relative_adjustement(&self.row.base.style, containing_block_for_children);
}
let (inline_size, block_size) = if let Some(row_group_layout) = row_group_fragment_layout {
self.rect.start_corner -= row_group_layout.rect.start_corner;
(
row_group_layout.rect.size.inline,
SizeConstraint::Definite(row_group_layout.rect.size.block),
)
} else {
(
containing_block_for_logical_conversion.size.inline,
containing_block_for_logical_conversion.size.block,
)
};
let row_group_containing_block = ContainingBlock {
size: ContainingBlockSize {
inline: inline_size,
block: block_size,
},
style: containing_block_for_logical_conversion.style,
};
let mut row_fragment = BoxFragment::new(
self.row.base.base_fragment_info,
self.row.base.style.clone(),
self.fragments,
self.rect.as_physical(Some(&row_group_containing_block)),
PhysicalSides::zero(), /* padding */
PhysicalSides::zero(), /* border */
PhysicalSides::zero(), /* margin */
None, /* specific_layout_info */
);
row_fragment.set_does_not_paint_background();
if let Some(mut row_positioning_context) = self.positioning_context.take() {
row_positioning_context.layout_collected_children(layout_context, &mut row_fragment);
let parent_positioning_context = row_group_fragment_layout
.as_mut()
.and_then(|layout| layout.positioning_context.as_mut())
.unwrap_or(table_positioning_context);
parent_positioning_context.append(row_positioning_context);
}
let fragment = Fragment::Box(ArcRefCell::new(row_fragment));
self.row.base.set_fragment(fragment.clone());
fragment
}
}
struct RowGroupFragmentLayout {
row_group: ArcRefCell<TableTrackGroup>,
rect: LogicalRect<Au>,
positioning_context: Option<PositioningContext>,
index: usize,
fragments: Vec<Fragment>,
}
impl RowGroupFragmentLayout {
fn new(
row_group: ArcRefCell<TableTrackGroup>,
index: usize,
dimensions: &TableAndTrackDimensions,
) -> Self {
let (rect, positioning_context) = {
let row_group = row_group.borrow();
(
dimensions.get_row_group_rect(&row_group),
PositioningContext::new_for_layout_box_base(&row_group.base),
)
};
Self {
row_group,
rect,
positioning_context,
index,
fragments: Vec::new(),
}
}
fn finish(
mut self,
layout_context: &LayoutContext,
table_positioning_context: &mut PositioningContext,
containing_block_for_logical_conversion: &ContainingBlock,
containing_block_for_children: &ContainingBlock,
) -> Fragment {
let row_group = self.row_group.borrow();
if self.positioning_context.is_some() {
self.rect.start_corner +=
relative_adjustement(&row_group.base.style, containing_block_for_children);
}
let mut row_group_fragment = BoxFragment::new(
row_group.base.base_fragment_info,
row_group.base.style.clone(),
self.fragments,
self.rect
.as_physical(Some(containing_block_for_logical_conversion)),
PhysicalSides::zero(), /* padding */
PhysicalSides::zero(), /* border */
PhysicalSides::zero(), /* margin */
None, /* specific_layout_info */
);
row_group_fragment.set_does_not_paint_background();
if let Some(mut row_positioning_context) = self.positioning_context.take() {
row_positioning_context
.layout_collected_children(layout_context, &mut row_group_fragment);
table_positioning_context.append(row_positioning_context);
}
let fragment = Fragment::Box(ArcRefCell::new(row_group_fragment));
row_group.base.set_fragment(fragment.clone());
fragment
}
}
struct TableAndTrackDimensions {
/// The rect of the full table, not counting for borders, padding, and margin.
table_rect: LogicalRect<Au>,
/// The rect of the full table, not counting for borders, padding, and margin
/// and offset by any border spacing and caption.
table_cells_rect: LogicalRect<Au>,
/// The min and max block offsets of each table row.
row_dimensions: Vec<(Au, Au)>,
/// The min and max inline offsets of each table column
column_dimensions: Vec<(Au, Au)>,
}
impl TableAndTrackDimensions {
fn new(table_layout: &TableLayout) -> Self {
let border_spacing = table_layout.table.border_spacing();
// The sizes used for a dimension when that dimension has no table tracks.
let fallback_inline_size = table_layout.assignable_width;
let fallback_block_size = table_layout.final_table_height;
let mut column_dimensions = Vec::new();
let mut column_offset = Au::zero();
for column_index in 0..table_layout.table.size.width {
if table_layout.is_column_collapsed(column_index) {
column_dimensions.push((column_offset, column_offset));
continue;
}
let start_offset = column_offset + border_spacing.inline;
let end_offset = start_offset + table_layout.distributed_column_widths[column_index];
column_dimensions.push((start_offset, end_offset));
column_offset = end_offset;
}
column_offset += if table_layout.table.size.width == 0 {
fallback_inline_size
} else {
border_spacing.inline
};
let mut row_dimensions = Vec::new();
let mut row_offset = Au::zero();
for row_index in 0..table_layout.table.size.height {
if table_layout.is_row_collapsed(row_index) {
row_dimensions.push((row_offset, row_offset));
continue;
}
let start_offset = row_offset + border_spacing.block;
let end_offset = start_offset + table_layout.row_sizes[row_index];
row_dimensions.push((start_offset, end_offset));
row_offset = end_offset;
}
row_offset += if table_layout.table.size.height == 0 {
fallback_block_size
} else {
border_spacing.block
};
let table_start_corner = LogicalVec2 {
inline: column_dimensions.first().map_or_else(Au::zero, |v| v.0),
block: row_dimensions.first().map_or_else(Au::zero, |v| v.0),
};
let table_size = LogicalVec2 {
inline: column_dimensions
.last()
.map_or(fallback_inline_size, |v| v.1),
block: row_dimensions.last().map_or(fallback_block_size, |v| v.1),
} - table_start_corner;
let table_cells_rect = LogicalRect {
start_corner: table_start_corner,
size: table_size,
};
let table_rect = LogicalRect {
start_corner: LogicalVec2::zero(),
size: LogicalVec2 {
inline: column_offset,
block: row_offset,
},
};
Self {
table_rect,
table_cells_rect,
row_dimensions,
column_dimensions,
}
}
fn get_row_rect(&self, row_index: usize) -> LogicalRect<Au> {
let mut row_rect = self.table_cells_rect;
let row_dimensions = self.row_dimensions[row_index];
row_rect.start_corner.block = row_dimensions.0;
row_rect.size.block = row_dimensions.1 - row_dimensions.0;
row_rect
}
fn get_column_rect(&self, column_index: usize) -> LogicalRect<Au> {
let mut row_rect = self.table_cells_rect;
let column_dimensions = self.column_dimensions[column_index];
row_rect.start_corner.inline = column_dimensions.0;
row_rect.size.inline = column_dimensions.1 - column_dimensions.0;
row_rect
}
fn get_row_group_rect(&self, row_group: &TableTrackGroup) -> LogicalRect<Au> {
if row_group.is_empty() {
return LogicalRect::zero();
}
let mut row_group_rect = self.table_cells_rect;
let block_start = self.row_dimensions[row_group.track_range.start].0;
let block_end = self.row_dimensions[row_group.track_range.end - 1].1;
row_group_rect.start_corner.block = block_start;
row_group_rect.size.block = block_end - block_start;
row_group_rect
}
fn get_column_group_rect(&self, column_group: &TableTrackGroup) -> LogicalRect<Au> {
if column_group.is_empty() {
return LogicalRect::zero();
}
let mut column_group_rect = self.table_cells_rect;
let inline_start = self.column_dimensions[column_group.track_range.start].0;
let inline_end = self.column_dimensions[column_group.track_range.end - 1].1;
column_group_rect.start_corner.inline = inline_start;
column_group_rect.size.inline = inline_end - inline_start;
column_group_rect
}
fn get_cell_rect(
&self,
coordinates: TableSlotCoordinates,
rowspan: usize,
colspan: usize,
) -> LogicalRect<Au> {
let start_corner = LogicalVec2 {
inline: self.column_dimensions[coordinates.x].0,
block: self.row_dimensions[coordinates.y].0,
};
let size = LogicalVec2 {
inline: self.column_dimensions[coordinates.x + colspan - 1].1,
block: self.row_dimensions[coordinates.y + rowspan - 1].1,
} - start_corner;
LogicalRect { start_corner, size }
}
}
impl Table {
fn border_spacing(&self) -> LogicalVec2<Au> {
if self.style.clone_border_collapse() == BorderCollapse::Collapse {
LogicalVec2::zero()
} else {
let border_spacing = self.style.clone_border_spacing();
LogicalVec2 {
inline: border_spacing.horizontal(),
block: border_spacing.vertical(),
}
}
}
fn total_border_spacing(&self) -> LogicalVec2<Au> {
let border_spacing = self.border_spacing();
LogicalVec2 {
inline: if self.size.width > 0 {
border_spacing.inline * (self.size.width as i32 + 1)
} else {
Au::zero()
},
block: if self.size.height > 0 {
border_spacing.block * (self.size.height as i32 + 1)
} else {
Au::zero()
},
}
}
fn get_column_measure_for_column_at_index(
&self,
writing_mode: WritingMode,
column_index: usize,
is_in_fixed_mode: bool,
) -> CellOrTrackMeasure {
let column = match self.columns.get(column_index) {
Some(column) => column,
None => return CellOrTrackMeasure::zero(),
}
.borrow();
let CellOrColumnOuterSizes {
preferred: preferred_size,
min: min_size,
max: max_size,
percentage: percentage_size,
} = CellOrColumnOuterSizes::new(
&column.base.style,
writing_mode,
&Default::default(),
is_in_fixed_mode,
);
CellOrTrackMeasure {
content_sizes: ContentSizes {
// > The outer min-content width of a table-column or table-column-group is
// > max(min-width, width).
// But that's clearly wrong, since it would be equal to or greater than
// the outer max-content width. So we match other browsers instead.
min_content: min_size.inline,
// > 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: preferred_size
.inline
.clamp_between_extremums(min_size.inline, max_size.inline),
},
percentage: percentage_size.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(),
};
// In the block axis, the min-content and max-content sizes are the same
// (except for new layout boxes like grid and flex containers). Note that
// other browsers don't seem to use the min and max sizing properties here.
let row = row.borrow();
let size = row.base.style.box_size(writing_mode);
let max_size = row.base.style.max_box_size(writing_mode);
let percentage_contribution = get_size_percentage_contribution(&size, &max_size);
CellOrTrackMeasure {
content_sizes: size
.block
.to_numeric()
.and_then(|size| size.to_length())
.map_or_else(Au::zero, Au::from)
.into(),
percentage: percentage_contribution.block,
}
}
pub(crate) fn layout(
&self,
layout_context: &LayoutContext,
positioning_context: &mut PositioningContext,
containing_block_for_children: &ContainingBlock,
containing_block_for_table: &ContainingBlock,
) -> CacheableLayoutResult {
TableLayout::new(self).layout(
layout_context,
positioning_context,
containing_block_for_children,
containing_block_for_table,
)
}
}
impl ComputeInlineContentSizes for Table {
#[servo_tracing::instrument(name = "Table::compute_inline_content_sizes", skip_all)]
fn compute_inline_content_sizes(
&self,
layout_context: &LayoutContext,
constraint_space: &ConstraintSpace,
) -> InlineContentSizesResult {
let writing_mode = constraint_space.writing_mode;
let mut layout = TableLayout::new(self);
layout.compute_border_collapse(writing_mode);
layout.pbm = self
.layout_style(Some(&layout))
.padding_border_margin_with_writing_mode_and_containing_block_inline_size(
writing_mode,
Au::zero(),
);
layout.compute_measures(layout_context, writing_mode);
let grid_content_sizes = layout.compute_grid_min_max();
// Padding and border should apply to the table grid, but they will be taken into
// account when computing the inline content sizes of the table wrapper (our parent), so
// this code removes their contribution from the inline content size of the caption.
let caption_content_sizes = ContentSizes::from(
layout.compute_caption_minimum_inline_size(layout_context) -
layout.pbm.padding_border_sums.inline,
);
InlineContentSizesResult {
sizes: grid_content_sizes.max(caption_content_sizes),
depends_on_block_constraints: false,
}
}
}
impl Table {
#[inline]
pub(crate) fn layout_style<'a>(
&'a self,
layout: Option<&'a TableLayout<'a>>,
) -> LayoutStyle<'a> {
LayoutStyle::Table(TableLayoutStyle {
table: self,
layout,
})
}
#[inline]
pub(crate) fn layout_style_for_grid(&self) -> LayoutStyle<'_> {
LayoutStyle::Default(&self.grid_style)
}
}
impl TableTrack {
#[inline]
pub(crate) fn layout_style(&self) -> LayoutStyle<'_> {
LayoutStyle::Default(&self.base.style)
}
}
impl TableTrackGroup {
#[inline]
pub(crate) fn layout_style(&self) -> LayoutStyle<'_> {
LayoutStyle::Default(&self.base.style)
}
}
impl TableLayoutStyle<'_> {
#[inline]
pub(crate) fn style(&self) -> &ComputedValues {
&self.table.style
}
#[inline]
pub(crate) fn collapses_borders(&self) -> bool {
self.style().get_inherited_table().border_collapse == BorderCollapse::Collapse
}
pub(crate) fn halved_collapsed_border_widths(&self) -> LogicalSides<Au> {
debug_assert!(self.collapses_borders());
let area = LogicalSides {
inline_start: 0,
inline_end: self.table.size.width,
block_start: 0,
block_end: self.table.size.height,
};
if let Some(layout) = self.layout {
layout.get_collapsed_border_widths_for_area(area)
} else {
// TODO: this should be cached.
let mut layout = TableLayout::new(self.table);
layout.compute_border_collapse(self.style().writing_mode);
layout.get_collapsed_border_widths_for_area(area)
}
.expect("Collapsed borders should be computed")
}
}
impl TableSlotCell {
#[inline]
fn layout_style(&self) -> LayoutStyle<'_> {
self.contents.layout_style(&self.base)
}
fn effective_vertical_align(&self) -> VerticalAlignKeyword {
match self.base.style.clone_vertical_align() {
VerticalAlign::Keyword(VerticalAlignKeyword::Top) => VerticalAlignKeyword::Top,
VerticalAlign::Keyword(VerticalAlignKeyword::Bottom) => VerticalAlignKeyword::Bottom,
VerticalAlign::Keyword(VerticalAlignKeyword::Middle) => VerticalAlignKeyword::Middle,
_ => VerticalAlignKeyword::Baseline,
}
}
fn inline_content_sizes(&self, layout_context: &LayoutContext) -> ContentSizes {
let constraint_space = ConstraintSpace::new_for_style_and_ratio(
&self.base.style,
None, /* TODO: support preferred aspect ratios on non-replaced boxes */
);
self.base
.inline_content_sizes(layout_context, &constraint_space, &self.contents.contents)
.sizes
}
#[allow(clippy::too_many_arguments)]
fn create_fragment(
&self,
mut layout: CellLayout,
cell_rect: LogicalRect<Au>,
cell_baseline: Au,
positioning_context: &mut PositioningContext,
table_style: &ComputedValues,
containing_block: &ContainingBlock,
is_collapsed: bool,
) -> BoxFragment {
// This must be scoped to this function because it conflicts with euclid's Zero.
use style::Zero as StyleZero;
let cell_content_rect = cell_rect.deflate(&(layout.padding + layout.border));
let content_block_size = layout.layout.content_block_size;
let vertical_align_offset = match self.effective_vertical_align() {
VerticalAlignKeyword::Top => Au::zero(),
VerticalAlignKeyword::Bottom => cell_content_rect.size.block - content_block_size,
VerticalAlignKeyword::Middle => {
(cell_content_rect.size.block - content_block_size).scale_by(0.5)
},
_ => {
cell_baseline -
(layout.padding.block_start + layout.border.block_start) -
layout.ascent()
},
};
let mut base_fragment_info = self.base.base_fragment_info;
if self.base.style.get_inherited_table().empty_cells == EmptyCells::Hide &&
table_style.get_inherited_table().border_collapse != BorderCollapse::Collapse &&
layout.is_empty_for_empty_cells()
{
base_fragment_info.flags.insert(FragmentFlags::DO_NOT_PAINT);
}
if is_collapsed {
base_fragment_info.flags.insert(FragmentFlags::IS_COLLAPSED);
}
// Create an `AnonymousFragment` to move the cell contents to the cell baseline.
let mut vertical_align_fragment_rect = cell_content_rect;
vertical_align_fragment_rect.start_corner = LogicalVec2 {
inline: Au::zero(),
block: vertical_align_offset,
};
let vertical_align_fragment = PositioningFragment::new_anonymous(
self.base.style.clone(),
vertical_align_fragment_rect.as_physical(None),
layout.layout.fragments,
);
// Adjust the static position of all absolute children based on the
// final content rect of this fragment. Note that we are not shifting by the position of the
// Anonymous fragment we use to shift content to the baseline.
//
// TODO(mrobinson): This is correct for absolutes that are direct children of the table
// cell, but wrong for absolute fragments that are more deeply nested in the hierarchy of
// fragments.
let physical_cell_rect = cell_content_rect.as_physical(Some(containing_block));
layout
.positioning_context
.adjust_static_position_of_hoisted_fragments_with_offset(
&physical_cell_rect.origin.to_vector(),
PositioningContextLength::zero(),
);
positioning_context.append(layout.positioning_context);
let specific_layout_info = (table_style.get_inherited_table().border_collapse ==
BorderCollapse::Collapse)
.then_some(SpecificLayoutInfo::TableCellWithCollapsedBorders);
BoxFragment::new(
base_fragment_info,
self.base.style.clone(),
vec![Fragment::Positioning(vertical_align_fragment)],
physical_cell_rect,
layout.padding.to_physical(table_style.writing_mode),
layout.border.to_physical(table_style.writing_mode),
PhysicalSides::zero(), /* margin */
specific_layout_info,
)
.with_baselines(layout.layout.baselines)
}
}
fn get_size_percentage_contribution(
size: &LogicalVec2<Size<ComputedLengthPercentage>>,
max_size: &LogicalVec2<Size<ComputedLengthPercentage>>,
) -> 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
// > that are percentages:
// > min(percentage width, percentage max-width).
// > If the computed values are not percentages, then 0% is used for width, and an
// > infinite percentage is used for max-width.
LogicalVec2 {
inline: max_two_optional_percentages(
size.inline.to_percentage(),
max_size.inline.to_percentage(),
),
block: max_two_optional_percentages(
size.block.to_percentage(),
max_size.block.to_percentage(),
),
}
}
struct CellOrColumnOuterSizes {
min: LogicalVec2<Au>,
preferred: LogicalVec2<Au>,
max: LogicalVec2<Option<Au>>,
percentage: LogicalVec2<Option<Percentage>>,
}
impl CellOrColumnOuterSizes {
fn new(
style: &Arc<ComputedValues>,
writing_mode: WritingMode,
padding_border_sums: &LogicalVec2<Au>,
is_in_fixed_mode: bool,
) -> 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_option_size = |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);
if is_in_fixed_mode {
return Self {
percentage: size.map(|v| v.to_percentage()),
preferred: outer_option_size(size.map(get_size_for_axis))
.map(|v| v.unwrap_or_default()),
min: LogicalVec2::default(),
max: LogicalVec2::default(),
};
}
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_option_size(max_size.map(get_size_for_axis)),
percentage: get_size_percentage_contribution(&size, &max_size),
}
}
}
struct RowspanToDistribute<'a> {
coordinates: TableSlotCoordinates,
cell: AtomicRef<'a, TableSlotCell>,
measure: &'a CellOrTrackMeasure,
}
impl RowspanToDistribute<'_> {
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
}
}
/// 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
}
}