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