//! 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); } }