Skip to main content

layout/table/
mod.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4#![allow(rustdoc::private_intra_doc_links)]
5
6//! # HTML Tables (╯°□°)╯︵ ┻━┻
7//!
8//! This implementation is based on the [table section of the HTML 5 Specification][1],
9//! the draft [CSS Table Module Level! 3][2] and the [LayoutNG implementation of tables][3] in Blink.
10//! In general, the draft specification differs greatly from what other browsers do, so we
11//! generally follow LayoutNG when in question.
12//!
13//! [1]: https://html.spec.whatwg.org/multipage/#tables
14//! [2]: https://drafts.csswg.org/css-tables
15//! [3]: https://source.chromium.org/chromium/chromium/src/third_party/+/main:blink/renderer/core/layout/table
16//!
17//! Table layout is divided into two phases:
18//!
19//! 1. Box Tree Construction
20//! 2. Fragment Tree Construction
21//!
22//! ## Box Tree Construction
23//!
24//! During box tree construction, table layout (`construct.rs`) will traverse the DOM and construct
25//! the basic box tree representation of a table, using the structs defined in this file ([`Table`],
26//! [`TableTrackGroup`], [`TableTrack`], etc). When processing the DOM, elements are handled
27//! differently depending on their `display` value. For instance, an element with `display:
28//! table-cell` is treated as a table cell. HTML table elements like `<table>` and `<td>` are
29//! assigned the corresponding display value from the user agent stylesheet.
30//!
31//! Every [`Table`] holds an array of [`TableSlot`]. A [`TableSlot`] represents either a cell, a cell
32//! location occupied by a cell originating from another place in the table, or is empty. In
33//! addition, when there are table model errors, a slot may spanned by more than one cell.
34//!
35//! During processing, the box tree construction agorithm will also fix up table structure, for
36//! instance, creating anonymous rows for lone table cells and putting non-table content into
37//! anonymous cells. In addition, flow layout will collect table elements outside of tables and create
38//! anonymous tables for them.
39//!
40//! After processing, box tree construction does a fix up pass on tables, converting rowspan=0 into
41//! definite rowspan values and making sure that rowspan and celspan values are not larger than the
42//! table itself. Finally, row groups may be reordered to enforce the fact that the first `<thead>`
43//! comes before `<tbody>` which comes before the first `<tfoot>`.
44//!
45//! ## Fragment Tree Construction
46//!
47//! Fragment tree construction involves calculating the size and positioning of all table elements,
48//! given their style, content, and cell and row spans. This happens both during intrinsic inline
49//! size computation as well as layout into Fragments. In both of these cases, measurement and
50//! layout is done by [`layout::TableLayout`], though for intrinsic size computation only a partial
51//! layout is done.
52//!
53//! In general, we follow the following steps when laying out table content:
54//!
55//! 1. Compute track constrainedness and has originating cells
56//! 2. Compute cell measures
57//! 3. Compute column measures
58//! 4. Compute intrinsic inline sizes for columns and the table
59//! 5. Compute the final table inline size
60//! 6. Distribute size to columns
61//! 7. Do first pass cell layout
62//! 8. Do row layout
63//! 9. Compute table height and final row sizes
64//! 10. Create fragments for table elements (columns, column groups, rows, row groups, cells)
65//!
66//! For intrinsic size computation this process stops at step 4.
67
68mod construct;
69mod layout;
70
71use std::ops::Range;
72
73use app_units::Au;
74use atomic_refcell::AtomicRef;
75pub(crate) use construct::AnonymousTableContent;
76pub use construct::TableBuilder;
77use euclid::{Point2D, Size2D, UnknownUnit, Vector2D};
78use malloc_size_of_derive::MallocSizeOf;
79use script::layout_dom::{ServoDangerousStyleElement, ServoLayoutNode};
80use servo_arc::Arc;
81use style::context::SharedStyleContext;
82use style::properties::ComputedValues;
83use style::properties::style_structs::Font;
84use style::selector_parser::PseudoElement;
85
86use super::flow::BlockFormattingContext;
87use crate::cell::{ArcRefCell, WeakRefCell};
88use crate::dom::WeakLayoutBox;
89use crate::flow::BlockContainer;
90use crate::formatting_contexts::{
91    IndependentFormattingContext, IndependentFormattingContextContents,
92};
93use crate::fragment_tree::BaseFragmentInfo;
94use crate::geom::PhysicalVec;
95use crate::layout_box_base::LayoutBoxBase;
96use crate::style_ext::BorderStyleColor;
97use crate::table::layout::TableLayout;
98use crate::{PropagatedBoxTreeData, SharedStyle};
99
100pub type TableSize = Size2D<usize, UnknownUnit>;
101
102#[derive(Debug, MallocSizeOf)]
103pub struct Table {
104    /// The style of this table. These are the properties that apply to the "wrapper" ie the element
105    /// that contains both the grid and the captions. Not all properties are actually used on the
106    /// wrapper though, such as background and borders, which apply to the grid.
107    style: Arc<ComputedValues>,
108
109    /// The style of this table's grid. This is an anonymous style based on the table's style, but
110    /// eliminating all the properties handled by the "wrapper."
111    grid_style: Arc<ComputedValues>,
112
113    /// The [`BaseFragmentInfo`] for this table's grid. This is necessary so that when the
114    /// grid has a background image, it can be associated with the table's node.
115    grid_base_fragment_info: BaseFragmentInfo,
116
117    /// The captions for this table.
118    pub captions: Vec<ArcRefCell<TableCaption>>,
119
120    /// The column groups for this table.
121    pub column_groups: Vec<ArcRefCell<TableTrackGroup>>,
122
123    /// The columns of this table defined by `<colgroup> | display: table-column-group`
124    /// and `<col> | display: table-column` elements as well as `display: table-column`.
125    pub columns: Vec<ArcRefCell<TableTrack>>,
126
127    /// The rows groups for this table defined by `<tbody>`, `<thead>`, and `<tfoot>`.
128    pub row_groups: Vec<ArcRefCell<TableTrackGroup>>,
129
130    /// The rows of this table defined by `<tr>` or `display: table-row` elements.
131    pub rows: Vec<ArcRefCell<TableTrack>>,
132
133    /// The content of the slots of this table.
134    pub slots: Vec<Vec<TableSlot>>,
135
136    /// The size of this table.
137    pub size: TableSize,
138
139    /// Whether or not this Table is anonymous.
140    anonymous: bool,
141
142    /// Whether percentage columns are taken into account during inline content sizes calculation.
143    percentage_columns_allowed_for_inline_content_sizes: bool,
144}
145
146impl Table {
147    pub(crate) fn new(
148        style: Arc<ComputedValues>,
149        grid_style: Arc<ComputedValues>,
150        base_fragment_info: BaseFragmentInfo,
151        percentage_columns_allowed_for_inline_content_sizes: bool,
152    ) -> Self {
153        Self {
154            style,
155            grid_style,
156            grid_base_fragment_info: base_fragment_info,
157            captions: Vec::new(),
158            column_groups: Vec::new(),
159            columns: Vec::new(),
160            row_groups: Vec::new(),
161            rows: Vec::new(),
162            slots: Vec::new(),
163            size: TableSize::zero(),
164            anonymous: false,
165            percentage_columns_allowed_for_inline_content_sizes,
166        }
167    }
168
169    /// Return the slot at the given coordinates, if it exists in the table, otherwise
170    /// return None.
171    fn get_slot(&self, coords: TableSlotCoordinates) -> Option<&TableSlot> {
172        self.slots.get(coords.y)?.get(coords.x)
173    }
174
175    fn resolve_first_cell_coords(
176        &self,
177        coords: TableSlotCoordinates,
178    ) -> Option<TableSlotCoordinates> {
179        match self.get_slot(coords) {
180            Some(&TableSlot::Cell(_)) => Some(coords),
181            Some(TableSlot::Spanned(offsets)) => Some(coords - offsets[0]),
182            _ => None,
183        }
184    }
185
186    fn resolve_first_cell(
187        &self,
188        coords: TableSlotCoordinates,
189    ) -> Option<AtomicRef<'_, TableSlotCell>> {
190        let resolved_coords = self.resolve_first_cell_coords(coords)?;
191        let slot = self.get_slot(resolved_coords);
192        match slot {
193            Some(TableSlot::Cell(cell)) => Some(cell.borrow()),
194            _ => unreachable!(
195                "Spanned slot should not point to an empty cell or another spanned slot."
196            ),
197        }
198    }
199
200    pub(crate) fn repair_style(
201        &mut self,
202        context: &SharedStyleContext,
203        new_style: &Arc<ComputedValues>,
204    ) {
205        self.style = new_style.clone();
206        self.grid_style = context
207            .stylist
208            .style_for_anonymous::<ServoDangerousStyleElement>(
209                &context.guards,
210                &PseudoElement::ServoTableGrid,
211                new_style,
212            );
213    }
214
215    pub(crate) fn subtree_size(&self) -> usize {
216        self.slots
217            .iter()
218            .flat_map(|row| row.iter())
219            .map(TableSlot::subtree_size)
220            .sum()
221    }
222}
223
224type TableSlotCoordinates = Point2D<usize, UnknownUnit>;
225pub type TableSlotOffset = Vector2D<usize, UnknownUnit>;
226
227#[derive(Debug, MallocSizeOf)]
228pub struct TableSlotCell {
229    /// The independent formatting context that the cell establishes for its contents.
230    /// Currently, this should always be a block formatting context.
231    pub(crate) context: IndependentFormattingContext,
232
233    /// Number of columns that the cell is to span. Must be greater than zero.
234    colspan: usize,
235
236    /// Number of rows that the cell is to span. Zero means that the cell is to span all
237    /// the remaining rows in the row group.
238    rowspan: usize,
239}
240
241impl TableSlotCell {
242    pub fn mock_for_testing(id: usize, colspan: usize, rowspan: usize) -> Self {
243        let base = LayoutBoxBase::new(
244            BaseFragmentInfo::new_for_testing(id),
245            ComputedValues::initial_values_with_font_override(Font::initial_values()).to_arc(),
246        );
247        let contents = IndependentFormattingContextContents::Flow(BlockFormattingContext {
248            contents: BlockContainer::BlockLevelBoxes(Vec::new()),
249            contains_floats: false,
250        });
251        let propagated_data =
252            PropagatedBoxTreeData::default().disallowing_percentage_table_columns();
253        Self {
254            context: IndependentFormattingContext::new(base, contents, propagated_data),
255            colspan,
256            rowspan,
257        }
258    }
259
260    /// Get the node id of this cell's [`BaseFragmentInfo`]. This is used for unit tests.
261    pub fn node_id(&self) -> usize {
262        self.context
263            .base
264            .base_fragment_info
265            .tag
266            .map_or(0, |tag| tag.node.0)
267    }
268}
269
270/// A single table slot. It may be an actual cell, or a reference
271/// to a previous cell that is spanned here
272///
273/// In case of table model errors, it may be multiple references
274#[derive(MallocSizeOf)]
275pub enum TableSlot {
276    /// A table cell, with a colspan and a rowspan.
277    Cell(ArcRefCell<TableSlotCell>),
278
279    /// This slot is spanned by one or more multiple cells earlier in the table, which are
280    /// found at the given negative coordinate offsets. The vector is in the order of most
281    /// recent to earliest cell.
282    ///
283    /// If there is more than one cell that spans a slot, this is a table model error, but
284    /// we still keep track of it. See
285    /// <https://html.spec.whatwg.org/multipage/#table-model-error>
286    Spanned(Vec<TableSlotOffset>),
287
288    /// An empty spot in the table. This can happen when there is a gap in columns between
289    /// cells that are defined and one which should exist because of cell with a rowspan
290    /// from a previous row.
291    Empty,
292}
293
294impl std::fmt::Debug for TableSlot {
295    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
296        match self {
297            Self::Cell(_) => f.debug_tuple("Cell").finish(),
298            Self::Spanned(spanned) => f.debug_tuple("Spanned").field(spanned).finish(),
299            Self::Empty => write!(f, "Empty"),
300        }
301    }
302}
303
304impl TableSlot {
305    fn new_spanned(offset: TableSlotOffset) -> Self {
306        Self::Spanned(vec![offset])
307    }
308
309    pub(crate) fn subtree_size(&self) -> usize {
310        match self {
311            TableSlot::Cell(cell) => cell.borrow().context.subtree_size(),
312            TableSlot::Spanned(..) | TableSlot::Empty => 0,
313        }
314    }
315}
316
317/// A row or column of a table.
318#[derive(Debug, MallocSizeOf)]
319pub struct TableTrack {
320    /// The [`LayoutBoxBase`] of this [`TableTrack`].
321    base: LayoutBoxBase,
322
323    /// The index of the table row or column group parent in the table's list of row or column
324    /// groups.
325    group_index: Option<usize>,
326
327    /// Whether or not this [`TableTrack`] was anonymous, for instance created due to
328    /// a `span` attribute set on a parent `<colgroup>`.
329    is_anonymous: bool,
330
331    /// A shared container for this track's style, used to share the style for the purposes
332    /// of drawing backgrounds in individual cells. This allows updating the style in a
333    /// single place and having it affect all cell `Fragment`s.
334    shared_background_style: SharedStyle,
335}
336
337impl TableTrack {
338    fn repair_style(&mut self, new_style: &Arc<ComputedValues>) {
339        self.base.repair_style(new_style);
340        self.shared_background_style = SharedStyle::new(new_style.clone());
341    }
342}
343
344#[derive(Debug, MallocSizeOf, PartialEq)]
345pub enum TableTrackGroupType {
346    HeaderGroup,
347    FooterGroup,
348    RowGroup,
349    ColumnGroup,
350}
351
352#[derive(Debug, MallocSizeOf)]
353pub struct TableTrackGroup {
354    /// The [`LayoutBoxBase`] of this [`TableTrackGroup`].
355    base: LayoutBoxBase,
356
357    /// The type of this [`TableTrackGroup`].
358    group_type: TableTrackGroupType,
359
360    /// The range of tracks in this [`TableTrackGroup`].
361    track_range: Range<usize>,
362
363    /// A shared container for this track's style, used to share the style for the purposes
364    /// of drawing backgrounds in individual cells. This allows updating the style in a
365    /// single place and having it affect all cell `Fragment`s.
366    shared_background_style: SharedStyle,
367}
368
369impl TableTrackGroup {
370    pub(super) fn is_empty(&self) -> bool {
371        self.track_range.is_empty()
372    }
373
374    fn repair_style(&mut self, new_style: &Arc<ComputedValues>) {
375        self.base.repair_style(new_style);
376        self.shared_background_style = SharedStyle::new(new_style.clone());
377    }
378}
379
380#[derive(Debug, MallocSizeOf)]
381pub struct TableCaption {
382    /// The contents of this cell, with its own layout.
383    pub(crate) context: IndependentFormattingContext,
384}
385
386/// A calculated collapsed border.
387#[derive(Clone, Debug, Default, MallocSizeOf, PartialEq)]
388pub(crate) struct CollapsedBorder {
389    pub style_color: BorderStyleColor,
390    pub width: Au,
391}
392
393/// Represents a piecewise sequence of collapsed borders along a line.
394pub(crate) type CollapsedBorderLine = Vec<CollapsedBorder>;
395
396#[derive(Clone, Debug, MallocSizeOf)]
397pub(crate) struct SpecificTableGridInfo {
398    pub collapsed_borders: PhysicalVec<Vec<CollapsedBorderLine>>,
399    pub track_sizes: PhysicalVec<Vec<Au>>,
400}
401
402pub(crate) struct TableLayoutStyle<'a> {
403    table: &'a Table,
404    layout: Option<&'a TableLayout<'a>>,
405}
406
407/// Table parts that are stored in the DOM. This is used in order to map from
408/// the DOM to the box tree and will eventually be important for incremental
409/// layout.
410#[derive(Debug, MallocSizeOf)]
411pub(crate) enum TableLevelBox {
412    Caption(ArcRefCell<TableCaption>),
413    Cell(ArcRefCell<TableSlotCell>),
414    TrackGroup(ArcRefCell<TableTrackGroup>),
415    Track(ArcRefCell<TableTrack>),
416}
417
418impl TableLevelBox {
419    pub(crate) fn with_base<T>(&self, callback: impl FnOnce(&LayoutBoxBase) -> T) -> T {
420        match self {
421            TableLevelBox::Caption(caption) => callback(&caption.borrow().context.base),
422            TableLevelBox::Cell(cell) => callback(&cell.borrow().context.base),
423            TableLevelBox::TrackGroup(track_group) => callback(&track_group.borrow().base),
424            TableLevelBox::Track(track) => callback(&track.borrow().base),
425        }
426    }
427
428    pub(crate) fn with_base_mut<T>(&mut self, callback: impl FnOnce(&mut LayoutBoxBase) -> T) -> T {
429        match self {
430            TableLevelBox::Caption(caption) => callback(&mut caption.borrow_mut().context.base),
431            TableLevelBox::Cell(cell) => callback(&mut cell.borrow_mut().context.base),
432            TableLevelBox::TrackGroup(track_group) => callback(&mut track_group.borrow_mut().base),
433            TableLevelBox::Track(track) => callback(&mut track.borrow_mut().base),
434        }
435    }
436
437    pub(crate) fn repair_style(
438        &self,
439        context: &SharedStyleContext<'_>,
440        node: &ServoLayoutNode,
441        new_style: &Arc<ComputedValues>,
442    ) {
443        match self {
444            TableLevelBox::Caption(caption) => caption
445                .borrow_mut()
446                .context
447                .repair_style(context, node, new_style),
448            TableLevelBox::Cell(cell) => cell
449                .borrow_mut()
450                .context
451                .repair_style(context, node, new_style),
452            TableLevelBox::TrackGroup(track_group) => {
453                track_group.borrow_mut().repair_style(new_style);
454            },
455            TableLevelBox::Track(track) => track.borrow_mut().repair_style(new_style),
456        }
457    }
458
459    pub(crate) fn attached_to_tree(&self, layout_box: WeakLayoutBox) {
460        match self {
461            Self::Caption(caption) => caption.borrow().context.attached_to_tree(layout_box),
462            Self::Cell(cell) => cell.borrow().context.attached_to_tree(layout_box),
463            Self::TrackGroup(_) | Self::Track(_) => {
464                // The parentage of tracks within a track group, and cells within a row, is handled
465                // when the entire table is attached to the tree.
466            },
467        }
468    }
469
470    pub(crate) fn downgrade(&self) -> WeakTableLevelBox {
471        match self {
472            Self::Caption(caption) => WeakTableLevelBox::Caption(caption.downgrade()),
473            Self::Cell(cell) => WeakTableLevelBox::Cell(cell.downgrade()),
474            Self::TrackGroup(track_group) => WeakTableLevelBox::TrackGroup(track_group.downgrade()),
475            Self::Track(track) => WeakTableLevelBox::Track(track.downgrade()),
476        }
477    }
478}
479
480#[derive(Clone, Debug, MallocSizeOf)]
481pub(crate) enum WeakTableLevelBox {
482    Caption(WeakRefCell<TableCaption>),
483    Cell(WeakRefCell<TableSlotCell>),
484    TrackGroup(WeakRefCell<TableTrackGroup>),
485    Track(WeakRefCell<TableTrack>),
486}
487
488impl WeakTableLevelBox {
489    pub(crate) fn upgrade(&self) -> Option<TableLevelBox> {
490        Some(match self {
491            Self::Caption(caption) => TableLevelBox::Caption(caption.upgrade()?),
492            Self::Cell(cell) => TableLevelBox::Cell(cell.upgrade()?),
493            Self::TrackGroup(track_group) => TableLevelBox::TrackGroup(track_group.upgrade()?),
494            Self::Track(track) => TableLevelBox::Track(track.upgrade()?),
495        })
496    }
497}