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#![expect(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 use construct::TableBuilder;
76pub(crate) use construct::{AnonymousTableContent, AnonymousTableContentElement};
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, Fragment};
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        base: &LayoutBoxBase,
205    ) {
206        self.style = new_style.clone();
207        let new_grid_style = context
208            .stylist
209            .style_for_anonymous::<ServoDangerousStyleElement>(
210                &context.guards,
211                &PseudoElement::ServoTableGrid,
212                new_style,
213            );
214
215        // Update the style of the fragments of the table grid. This is typically handled in
216        // `LayoutBoxBase::repair_style()`, but the table grid doesn't have a `LayoutBoxBase`.
217        // The fragments of the table grid are children of the fragments of the table wrapper,
218        // but there can also be fragments of table captions, so we need to check.
219        for fragment in base.fragments.borrow().iter() {
220            if let Some(children) = fragment.children() {
221                for child_fragment in &*children {
222                    if let Fragment::Box(box_fragment) = child_fragment &&
223                        Arc::ptr_eq(&self.grid_style, &box_fragment.style.borrow())
224                    {
225                        child_fragment.repair_style(&new_grid_style);
226                    }
227                }
228            }
229            fragment.repair_style(new_style);
230        }
231        self.grid_style = new_grid_style;
232    }
233
234    pub(crate) fn subtree_size(&self) -> usize {
235        self.slots
236            .iter()
237            .flat_map(|row| row.iter())
238            .map(TableSlot::subtree_size)
239            .sum()
240    }
241}
242
243type TableSlotCoordinates = Point2D<usize, UnknownUnit>;
244pub type TableSlotOffset = Vector2D<usize, UnknownUnit>;
245
246#[derive(Debug, MallocSizeOf)]
247pub struct TableSlotCell {
248    /// The independent formatting context that the cell establishes for its contents.
249    /// Currently, this should always be a block formatting context.
250    pub(crate) context: IndependentFormattingContext,
251
252    /// Number of columns that the cell is to span. Must be greater than zero.
253    colspan: usize,
254
255    /// Number of rows that the cell is to span. Zero means that the cell is to span all
256    /// the remaining rows in the row group.
257    rowspan: usize,
258}
259
260impl TableSlotCell {
261    pub fn mock_for_testing(id: usize, colspan: usize, rowspan: usize) -> Self {
262        let base = LayoutBoxBase::new(
263            BaseFragmentInfo::new_for_testing(id),
264            ComputedValues::initial_values_with_font_override(Font::initial_values()).to_arc(),
265        );
266        let contents = IndependentFormattingContextContents::Flow(BlockFormattingContext {
267            contents: BlockContainer::BlockLevelBoxes(Vec::new()),
268            contains_floats: false,
269        });
270        let propagated_data =
271            PropagatedBoxTreeData::default().disallowing_percentage_table_columns();
272        Self {
273            context: IndependentFormattingContext::new(base, contents, propagated_data),
274            colspan,
275            rowspan,
276        }
277    }
278
279    /// Get the node id of this cell's [`BaseFragmentInfo`]. This is used for unit tests.
280    pub fn node_id(&self) -> usize {
281        self.context
282            .base
283            .base_fragment_info
284            .tag
285            .map_or(0, |tag| tag.node.0)
286    }
287}
288
289/// A single table slot. It may be an actual cell, or a reference
290/// to a previous cell that is spanned here
291///
292/// In case of table model errors, it may be multiple references
293#[derive(MallocSizeOf)]
294pub enum TableSlot {
295    /// A table cell, with a colspan and a rowspan.
296    Cell(ArcRefCell<TableSlotCell>),
297
298    /// This slot is spanned by one or more multiple cells earlier in the table, which are
299    /// found at the given negative coordinate offsets. The vector is in the order of most
300    /// recent to earliest cell.
301    ///
302    /// If there is more than one cell that spans a slot, this is a table model error, but
303    /// we still keep track of it. See
304    /// <https://html.spec.whatwg.org/multipage/#table-model-error>
305    Spanned(Vec<TableSlotOffset>),
306
307    /// An empty spot in the table. This can happen when there is a gap in columns between
308    /// cells that are defined and one which should exist because of cell with a rowspan
309    /// from a previous row.
310    Empty,
311}
312
313impl std::fmt::Debug for TableSlot {
314    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
315        match self {
316            Self::Cell(_) => f.debug_tuple("Cell").finish(),
317            Self::Spanned(spanned) => f.debug_tuple("Spanned").field(spanned).finish(),
318            Self::Empty => write!(f, "Empty"),
319        }
320    }
321}
322
323impl TableSlot {
324    fn new_spanned(offset: TableSlotOffset) -> Self {
325        Self::Spanned(vec![offset])
326    }
327
328    pub(crate) fn subtree_size(&self) -> usize {
329        match self {
330            TableSlot::Cell(cell) => cell.borrow().context.subtree_size(),
331            TableSlot::Spanned(..) | TableSlot::Empty => 0,
332        }
333    }
334}
335
336/// A row or column of a table.
337#[derive(Debug, MallocSizeOf)]
338pub struct TableTrack {
339    /// The [`LayoutBoxBase`] of this [`TableTrack`].
340    base: LayoutBoxBase,
341
342    /// The index of the table row or column group parent in the table's list of row or column
343    /// groups.
344    group_index: Option<usize>,
345
346    /// Whether or not this [`TableTrack`] was anonymous, for instance created due to
347    /// a `span` attribute set on a parent `<colgroup>`.
348    is_anonymous: bool,
349
350    /// A shared container for this track's style, used to share the style for the purposes
351    /// of drawing backgrounds in individual cells. This allows updating the style in a
352    /// single place and having it affect all cell `Fragment`s.
353    shared_background_style: SharedStyle,
354}
355
356impl TableTrack {
357    fn repair_style(&mut self, new_style: &Arc<ComputedValues>) {
358        self.base.repair_style(new_style);
359        self.shared_background_style = SharedStyle::new(new_style.clone());
360    }
361}
362
363#[derive(Debug, MallocSizeOf, PartialEq)]
364pub enum TableTrackGroupType {
365    HeaderGroup,
366    FooterGroup,
367    RowGroup,
368    ColumnGroup,
369}
370
371#[derive(Debug, MallocSizeOf)]
372pub struct TableTrackGroup {
373    /// The [`LayoutBoxBase`] of this [`TableTrackGroup`].
374    base: LayoutBoxBase,
375
376    /// The type of this [`TableTrackGroup`].
377    group_type: TableTrackGroupType,
378
379    /// The range of tracks in this [`TableTrackGroup`].
380    track_range: Range<usize>,
381
382    /// A shared container for this track's style, used to share the style for the purposes
383    /// of drawing backgrounds in individual cells. This allows updating the style in a
384    /// single place and having it affect all cell `Fragment`s.
385    shared_background_style: SharedStyle,
386}
387
388impl TableTrackGroup {
389    pub(super) fn is_empty(&self) -> bool {
390        self.track_range.is_empty()
391    }
392
393    fn repair_style(&mut self, new_style: &Arc<ComputedValues>) {
394        self.base.repair_style(new_style);
395        self.shared_background_style = SharedStyle::new(new_style.clone());
396    }
397}
398
399#[derive(Debug, MallocSizeOf)]
400pub struct TableCaption {
401    /// The contents of this cell, with its own layout.
402    pub(crate) context: IndependentFormattingContext,
403}
404
405/// A calculated collapsed border.
406#[derive(Clone, Debug, Default, MallocSizeOf, PartialEq)]
407pub(crate) struct CollapsedBorder {
408    pub style_color: BorderStyleColor,
409    pub width: Au,
410}
411
412/// Represents a piecewise sequence of collapsed borders along a line.
413pub(crate) type CollapsedBorderLine = Vec<CollapsedBorder>;
414
415#[derive(Clone, Debug, MallocSizeOf)]
416pub(crate) struct SpecificTableGridInfo {
417    pub collapsed_borders: PhysicalVec<Vec<CollapsedBorderLine>>,
418    pub track_sizes: PhysicalVec<Vec<Au>>,
419}
420
421pub(crate) struct TableLayoutStyle<'a> {
422    table: &'a Table,
423    layout: Option<&'a TableLayout<'a>>,
424}
425
426/// Table parts that are stored in the DOM. This is used in order to map from
427/// the DOM to the box tree and will eventually be important for incremental
428/// layout.
429#[derive(Debug, MallocSizeOf)]
430pub(crate) enum TableLevelBox {
431    Caption(ArcRefCell<TableCaption>),
432    Cell(ArcRefCell<TableSlotCell>),
433    TrackGroup(ArcRefCell<TableTrackGroup>),
434    Track(ArcRefCell<TableTrack>),
435}
436
437impl TableLevelBox {
438    pub(crate) fn with_base<T>(&self, callback: impl FnOnce(&LayoutBoxBase) -> T) -> T {
439        match self {
440            TableLevelBox::Caption(caption) => callback(&caption.borrow().context.base),
441            TableLevelBox::Cell(cell) => callback(&cell.borrow().context.base),
442            TableLevelBox::TrackGroup(track_group) => callback(&track_group.borrow().base),
443            TableLevelBox::Track(track) => callback(&track.borrow().base),
444        }
445    }
446
447    pub(crate) fn with_base_mut<T>(&mut self, callback: impl FnOnce(&mut LayoutBoxBase) -> T) -> T {
448        match self {
449            TableLevelBox::Caption(caption) => callback(&mut caption.borrow_mut().context.base),
450            TableLevelBox::Cell(cell) => callback(&mut cell.borrow_mut().context.base),
451            TableLevelBox::TrackGroup(track_group) => callback(&mut track_group.borrow_mut().base),
452            TableLevelBox::Track(track) => callback(&mut track.borrow_mut().base),
453        }
454    }
455
456    pub(crate) fn repair_style(
457        &self,
458        context: &SharedStyleContext<'_>,
459        node: &ServoLayoutNode,
460        new_style: &Arc<ComputedValues>,
461    ) {
462        match self {
463            TableLevelBox::Caption(caption) => caption
464                .borrow_mut()
465                .context
466                .repair_style(context, node, new_style),
467            TableLevelBox::Cell(cell) => cell
468                .borrow_mut()
469                .context
470                .repair_style(context, node, new_style),
471            TableLevelBox::TrackGroup(track_group) => {
472                track_group.borrow_mut().repair_style(new_style);
473            },
474            TableLevelBox::Track(track) => track.borrow_mut().repair_style(new_style),
475        }
476    }
477
478    pub(crate) fn attached_to_tree(&self, layout_box: WeakLayoutBox) {
479        match self {
480            Self::Caption(caption) => caption.borrow().context.attached_to_tree(layout_box),
481            Self::Cell(cell) => cell.borrow().context.attached_to_tree(layout_box),
482            Self::TrackGroup(_) | Self::Track(_) => {
483                // The parentage of tracks within a track group, and cells within a row, is handled
484                // when the entire table is attached to the tree.
485            },
486        }
487    }
488
489    pub(crate) fn downgrade(&self) -> WeakTableLevelBox {
490        match self {
491            Self::Caption(caption) => WeakTableLevelBox::Caption(caption.downgrade()),
492            Self::Cell(cell) => WeakTableLevelBox::Cell(cell.downgrade()),
493            Self::TrackGroup(track_group) => WeakTableLevelBox::TrackGroup(track_group.downgrade()),
494            Self::Track(track) => WeakTableLevelBox::Track(track.downgrade()),
495        }
496    }
497}
498
499#[derive(Clone, Debug, MallocSizeOf)]
500pub(crate) enum WeakTableLevelBox {
501    Caption(WeakRefCell<TableCaption>),
502    Cell(WeakRefCell<TableSlotCell>),
503    TrackGroup(WeakRefCell<TableTrackGroup>),
504    Track(WeakRefCell<TableTrack>),
505}
506
507impl WeakTableLevelBox {
508    pub(crate) fn upgrade(&self) -> Option<TableLevelBox> {
509        Some(match self {
510            Self::Caption(caption) => TableLevelBox::Caption(caption.upgrade()?),
511            Self::Cell(cell) => TableLevelBox::Cell(cell.upgrade()?),
512            Self::TrackGroup(track_group) => TableLevelBox::TrackGroup(track_group.upgrade()?),
513            Self::Track(track) => TableLevelBox::Track(track.upgrade()?),
514        })
515    }
516}