//! CSS 2.1's fixed table layout in the separated-borders model -- §17.5.2.1 (the fixed
//! algorithm's column widths and the table's own used width), §17.5.3 (row heights),
//! §17.6.1 (`border-spacing`, and borders belonging to cells alone) and §17.5's rule that
//! a cell's margins are ignored -- isolated behind two functions with plain-data inputs
//! and outputs (`roadmap.md` -> "Phase 1 — Engine bootstrap" step 4). A generated
//! candidate implements both against the types in this module alone, the way the
//! margin-collapsing function is split into `classify` and `combine`:
//!
//! - [`columns`-shaped](TableColumnsResolver): given the table's own border-box width,
//! its horizontal padding and border, the horizontal `border-spacing`, and one entry
//! per cell of the first row carrying that cell's `width`, horizontal padding and
//! border and `box-sizing`, returns each column's width and x offset and the table's
//! used border-box width.
//! - [`rows`-shaped](TableRowsResolver): given the vertical `border-spacing`, the table's
//! vertical padding and border, and one entry per row carrying the row's own `height`
//! and every one of its cells' border-box heights, returns each row's height and y
//! offset and the table's used border-box height.
//!
//! The two are one algorithm split on the axis CSS 2.1 splits it on: §17.5.2.1's whole
//! point is that the horizontal layout of a fixed table does not depend on cell content,
//! so the columns are resolved first, cells are laid out inside them, and only then are
//! the heights their content produced handed to the second function. `fragment.rs` owns
//! that order and the layout in between; neither function ever lays anything out itself.
//!
//! **What `fragment.rs` has already done**, so that a candidate does not repeat it. The
//! table's own `width` arrives as one border-box length in px, its percentage resolved
//! against the table's containing block and its `box-sizing` folded in -- the same
//! already-resolved discipline `crate::absolute::AbsoluteInputs` keeps. A cell's `width`
//! does *not*: it arrives as written, because a percentage there is of the table and so
//! is the candidate's to resolve, and `box-sizing` there decides whether the value
//! already includes the cell's own padding and border, which is a step of §17.5.2.1
//! itself. Cell margins never arrive at all (§17.5: margins do not apply to a table cell),
//! and a cell's border-box height is measured for it, since only `fragment.rs` can see
//! inside the box.
//!
//! **Geometry conventions.** A column's [`x`](TableColumn::x) and a row's
//! [`y`](TableRow::y) are offsets from the table's *content-box* origin, and a column's
//! width and a row's height are the cell border box that sits in them -- so in the
//! separated model the first column starts one horizontal `border-spacing` in from the
//! content-box origin, the last ends one in from its far edge, and adjacent columns are
//! one apart (§17.6.1). Both functions' `used_*` answers are the table's own *border-box*
//! size, padding and border included, which is why each takes the table's own edges:
//! §17.5.2.1 makes the table's used width the greater of its own `width` and what its
//! columns need, so the answer is not always the width that went in.
//!
//! Scope. In: `table-layout: fixed` on a `display: table` box with an explicit `width`;
//! column widths from the first row's cells as a length, a percentage or `auto`, with
//! `auto` columns sharing what is left equally and a table wider than its columns
//! spreading the difference over them; one- and two-value `border-spacing`; padding and
//! borders on the table and on its cells under either `box-sizing`; a cell's margins
//! ignored; a row's height as the tallest of its own `height` and its cells' border
//! boxes, a cell's own content being block children of explicit height; and
//! `vertical-align: top` on every cell.
//!
//! Out, and not modelled anywhere in this module: `table-layout: auto` and every table
//! whose used layout is auto -- a `width: auto` table, and the anonymous table §17.2.1
//! wraps around a stray row or cell, whose `table-layout` is the initial `auto`; column
//! and column-group elements (`display: table-column`, whose geometry the engines are
//! unlikely to report alike); row groups; captions; `colspan` and `rowspan`, which are
//! HTML attributes a `
` cell cannot carry; the collapsing-borders model;
//! `vertical-align` other than `top`, which needs cell baselines and so waits on the
//! inline-atomic function's line boxes being in use by cells; a table `height` larger
//! than its rows; `inline-table`; and text.
//!
//! [`columns_baseline`] and [`rows_baseline`] are this function's deliberately wrong
//! starting candidates: every column is the full width of the table and every cell is
//! stacked under the one before it, which is exactly what the engine did before this
//! module existed -- `boxtree.rs` gave a `display: table-cell` element one block-level
//! box and `fragment.rs` stacked it in normal flow like any other, filling its containing
//! block's width.
use dom::computed::LengthPercentageOrAuto;
/// One cell of the first row, as far as §17.5.2.1 step 2 reads it. Every field is that
/// one cell's own; the column it sets is the one at the same index.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TableColumnInput {
/// The cell's computed `width`, as written: a length, a percentage of the table, or
/// `auto` (this module's own doc comment explains why this one is not pre-resolved).
/// `Unresolved` -- a `calc()` mixing a length and a percentage -- is not produced by
/// this function's corpus and is the candidate's to treat as `auto`.
pub width: LengthPercentageOrAuto,
pub padding_left: f64,
pub padding_right: f64,
pub border_left: f64,
pub border_right: f64,
/// Whether this cell's `box-sizing` is `border-box`, in which case its `width`
/// already includes the padding and border above and the column it sets is that width
/// alone; under `content-box` the column is that width plus both edges.
pub border_box_sizing: bool,
}
/// Plain-data inputs to one table's column widths, narrowed to what §17.5.2.1 reads --
/// which is deliberately nothing about any cell's content, and nothing about any row but
/// the first.
#[derive(Debug, Clone, PartialEq)]
pub struct TableColumnsInputs {
/// The table's own `width` as a border-box length in CSS pixels, percentage resolved
/// and `box-sizing` folded in (this module's own doc comment). §17.5.2.1's "the value
/// of the `width` property for the table element", before the columns have their say.
pub border_box_width: f64,
pub padding_left: f64,
pub padding_right: f64,
pub border_left: f64,
pub border_right: f64,
/// The horizontal component of the table's `border-spacing` (§17.6.1): the gap
/// between two adjacent columns, and between the table's content edge and the column
/// beside it. A table of `n` columns has `n + 1` of these gaps inside its content
/// box.
pub border_spacing: f64,
/// One entry per cell of the table's first row, in document order. Empty for a table
/// with no row and no cell at all, which has no column either.
pub columns: Vec
,
}
/// Where one column sits and how wide it is, both measured as the cell border box that
/// occupies it (this module's own doc comment on geometry conventions).
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TableColumn {
pub width: f64,
/// The column's left edge, as a distance right from the table's content-box origin.
pub x: f64,
}
/// The columns §17.5.2.1 resolves, and the table's own used width once they have had
/// their say.
#[derive(Debug, Clone, PartialEq)]
pub struct TableColumnsOutputs {
/// One column per entry of [`TableColumnsInputs::columns`], in the same order.
pub columns: Vec,
/// The table's used border-box width: §17.5.2.1's "greater of the value of the
/// `width` property for the table element and the sum of the column widths (plus cell
/// spacing or borders)", with the table's own padding and border added back on.
pub used_width: f64,
}
/// One row, as far as §17.5.3 reads it.
#[derive(Debug, Clone, PartialEq)]
pub struct TableRowInput {
/// The row's own computed `height`. A percentage is of the table's height, which in
/// this function's scope is always its rows' own and so has no definite basis; it
/// reaches a candidate as `Percentage` and is its to treat as `auto`, the way
/// `Unresolved` is.
pub height: LengthPercentageOrAuto,
/// Every cell of this row, in document order, as the border-box height its own
/// content gave it -- what §17.5.3's "the minimum height required by the content"
/// comes to once `fragment.rs` has laid that content out.
pub cell_heights: Vec,
}
/// Where one row sits and how tall it is. Its cells' border boxes all take this same
/// height (§17.5.3: a cell's used height is its row's), which is what makes
/// `vertical-align` matter at all and why anything but `top` is out of scope.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TableRow {
pub height: f64,
/// The row's top edge, as a distance down from the table's content-box origin.
pub y: f64,
}
/// The rows §17.5.3 resolves, and the table's own used height.
#[derive(Debug, Clone, PartialEq)]
pub struct TableRowsOutputs {
/// One row per entry of [`TableRowsInputs::rows`], in the same order.
pub rows: Vec,
/// The table's used border-box height: its rows plus every vertical `border-spacing`
/// gap between and around them, with its own padding and border added back on.
pub used_height: f64,
}
/// Plain-data inputs to one table's row heights, narrowed to what §17.5.3 reads.
#[derive(Debug, Clone, PartialEq)]
pub struct TableRowsInputs {
/// The vertical component of the table's `border-spacing` (§17.6.1), the counterpart
/// of [`TableColumnsInputs::border_spacing`] on the block axis: a table of `n` rows
/// has `n + 1` of these gaps inside its content box.
pub border_spacing: f64,
pub padding_top: f64,
pub padding_bottom: f64,
pub border_top: f64,
pub border_bottom: f64,
/// Every row of the table, in document order, anonymous rows (§17.2.1) included --
/// a generated row is a row like any other here, the box tree having already decided
/// which cells belong to it.
pub rows: Vec,
}
/// One candidate's column half: plain data in, plain data out. Every candidate module the
/// generation loop compiles exports a function of this type named `columns`.
pub type TableColumnsResolver = fn(&TableColumnsInputs) -> TableColumnsOutputs;
/// One candidate's row half, exported as `rows` beside `columns` -- the same two-function
/// shape `crate::margin`'s `MarginClassifyResolver`/`MarginCombineResolver` pair takes.
pub type TableRowsResolver = fn(&TableRowsInputs) -> TableRowsOutputs;
/// This function's deliberately wrong starting candidate for the column half (this
/// module's own doc comment): every column is as wide as the table's whole content box
/// and starts at its left content edge, `border-spacing` and every cell's own `width` are
/// ignored outright, and the table keeps whatever width it was given -- the width a
/// `display: table-cell` box got when it was stacked as an ordinary block filling its
/// containing block.
pub fn columns_baseline(inputs: &TableColumnsInputs) -> TableColumnsOutputs {
let content_width = (inputs.border_box_width
- inputs.padding_left
- inputs.padding_right
- inputs.border_left
- inputs.border_right)
.max(0.0);
TableColumnsOutputs {
columns: inputs.columns.iter().map(|_| TableColumn { width: content_width, x: 0.0 }).collect(),
used_width: inputs.border_box_width,
}
}
/// This function's deliberately wrong starting candidate for the row half: a row is as
/// tall as its cells stacked one under the other rather than as its tallest cell, rows
/// follow each other with no `border-spacing` between them, and a row's own `height` is
/// ignored -- normal-flow block stacking, which is what the engine did with these boxes
/// before this module existed.
pub fn rows_baseline(inputs: &TableRowsInputs) -> TableRowsOutputs {
let mut rows = Vec::with_capacity(inputs.rows.len());
let mut y = 0.0;
for row in &inputs.rows {
let height: f64 = row.cell_heights.iter().sum();
rows.push(TableRow { height, y });
y += height;
}
TableRowsOutputs {
rows,
used_height: y + inputs.padding_top + inputs.padding_bottom + inputs.border_top + inputs.border_bottom,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cell(width: LengthPercentageOrAuto) -> TableColumnInput {
TableColumnInput {
width,
padding_left: 0.0,
padding_right: 0.0,
border_left: 0.0,
border_right: 0.0,
border_box_sizing: false,
}
}
fn columns_inputs(columns: Vec) -> TableColumnsInputs {
TableColumnsInputs {
border_box_width: 400.0,
padding_left: 0.0,
padding_right: 0.0,
border_left: 0.0,
border_right: 0.0,
border_spacing: 10.0,
columns,
}
}
fn rows_inputs(rows: Vec) -> TableRowsInputs {
TableRowsInputs {
border_spacing: 10.0,
padding_top: 0.0,
padding_bottom: 0.0,
border_top: 0.0,
border_bottom: 0.0,
rows,
}
}
#[test]
fn columns_baseline_gives_every_column_the_whole_table_width() {
let out = columns_baseline(&columns_inputs(vec![
cell(LengthPercentageOrAuto::Length(50.0)),
cell(LengthPercentageOrAuto::Auto),
]));
assert_eq!(out.columns, vec![
TableColumn { width: 400.0, x: 0.0 },
TableColumn { width: 400.0, x: 0.0 },
]);
assert_eq!(out.used_width, 400.0, "the width that went in, never widened by the columns");
}
/// The wrongness that matters most on the horizontal axis: a cell's own `width` and
/// the spacing around it change nothing.
#[test]
fn columns_baseline_ignores_the_cell_widths_and_the_spacing() {
let mut inputs = columns_inputs(vec![cell(LengthPercentageOrAuto::Length(120.0))]);
inputs.border_spacing = 0.0;
let zero_spacing = columns_baseline(&inputs);
inputs.border_spacing = 25.0;
assert_eq!(columns_baseline(&inputs).columns, zero_spacing.columns);
assert_eq!(zero_spacing.columns[0].width, 400.0, "not the 120px the cell asked for");
}
#[test]
fn columns_baseline_folds_the_tables_own_edges_out_of_the_content_width() {
let mut inputs = columns_inputs(vec![cell(LengthPercentageOrAuto::Auto)]);
inputs.padding_left = 12.0;
inputs.padding_right = 8.0;
inputs.border_left = 2.0;
inputs.border_right = 3.0;
assert_eq!(columns_baseline(&inputs).columns[0].width, 375.0, "400 - 20 padding - 5 border");
}
#[test]
fn rows_baseline_stacks_the_cells_of_one_row_instead_of_overlaying_them() {
let out = rows_baseline(&rows_inputs(vec![TableRowInput {
height: LengthPercentageOrAuto::Auto,
cell_heights: vec![20.0, 35.0],
}]));
assert_eq!(out.rows, vec![TableRow { height: 55.0, y: 0.0 }], "20 + 35, not max(20, 35)");
assert_eq!(out.used_height, 55.0);
}
#[test]
fn rows_baseline_leaves_no_spacing_between_rows() {
let out = rows_baseline(&rows_inputs(vec![
TableRowInput { height: LengthPercentageOrAuto::Auto, cell_heights: vec![20.0] },
TableRowInput { height: LengthPercentageOrAuto::Auto, cell_heights: vec![30.0] },
]));
assert_eq!(out.rows[1].y, 20.0, "flush against the first row, ignoring the 10px border-spacing");
assert_eq!(out.used_height, 50.0, "and no spacing above the first row or below the last");
}
#[test]
fn rows_baseline_ignores_a_rows_own_height() {
let out = rows_baseline(&rows_inputs(vec![TableRowInput {
height: LengthPercentageOrAuto::Length(90.0),
cell_heights: vec![20.0],
}]));
assert_eq!(out.rows[0].height, 20.0, "not the 90px the row asked for");
}
#[test]
fn rows_baseline_adds_the_tables_own_edges_back_onto_the_used_height() {
let mut inputs =
rows_inputs(vec![TableRowInput { height: LengthPercentageOrAuto::Auto, cell_heights: vec![40.0] }]);
inputs.padding_top = 5.0;
inputs.padding_bottom = 6.0;
inputs.border_top = 1.0;
inputs.border_bottom = 2.0;
assert_eq!(rows_baseline(&inputs).used_height, 54.0, "40 + 11 padding + 3 border");
}
#[test]
fn no_rows_and_no_columns_is_an_empty_answer() {
assert!(columns_baseline(&columns_inputs(Vec::new())).columns.is_empty());
let rows = rows_baseline(&rows_inputs(Vec::new()));
assert!(rows.rows.is_empty());
assert_eq!(rows.used_height, 0.0);
}
}