Skip to main content

blitz_dom/layout/
table.rs

1use blitz_traits::node_id::NodeId;
2use std::{ops::Range, sync::Arc};
3
4use atomic_refcell::AtomicRefCell;
5use markup5ever::local_name;
6use style::properties::style_structs::Border;
7use style::servo_arc::Arc as ServoArc;
8use style::values::specified::box_::{DisplayInside, DisplayOutside};
9use style::{
10    Atom, computed_values::border_collapse::T as BorderCollapse,
11    computed_values::table_layout::T as TableLayout,
12};
13use taffy::{
14    DetailedGridInfo, LayoutPartialTree as _, ResolveOrZero, TrackSizingFunction, style_helpers,
15};
16
17use crate::BaseDocument;
18
19use super::damage::{CONSTRUCT_BOX, CONSTRUCT_DESCENDENT, CONSTRUCT_FC};
20use super::resolve_calc_value;
21
22pub struct TableTreeWrapper<'doc> {
23    pub(crate) doc: &'doc mut BaseDocument,
24    pub(crate) ctx: Arc<TableContext>,
25}
26
27#[derive(Debug, Clone)]
28pub struct TableContext {
29    pub style: taffy::Style<Atom>,
30    pub cells: Vec<TableCell>,
31    pub rows: Vec<TableRow>,
32    pub row_groups: Vec<TableRowGroup>,
33    pub computed_grid_info: AtomicRefCell<Option<DetailedGridInfo>>,
34    pub border_style: Option<ServoArc<Border>>,
35    pub border_collapse: BorderCollapse,
36}
37
38impl TableContext {
39    pub(crate) fn references_removed_nodes<S: std::hash::BuildHasher>(
40        &self,
41        removed: &std::collections::HashSet<NodeId, S>,
42    ) -> bool {
43        self.cells
44            .iter()
45            .any(|cell| removed.contains(&cell.node_id))
46            || self.rows.iter().any(|row| removed.contains(&row.node_id))
47            || self
48                .row_groups
49                .iter()
50                .any(|group| removed.contains(&group.node_id))
51    }
52}
53
54// #[derive(Debug, Clone, Eq, PartialEq)]
55// pub enum TableItemKind {
56//     Row,
57//     Cell,
58// }
59
60#[derive(Debug, Clone)]
61pub struct TableCell {
62    // kind: TableItemKind,
63    node_id: NodeId,
64    style: taffy::Style<Atom>,
65}
66
67#[derive(Debug, Clone)]
68pub struct TableRow {
69    // kind: TableItemKind,
70    pub node_id: NodeId,
71    pub height: f32,
72    /// Where this row's cells sit in [`TableContext::cells`].
73    ///
74    /// Rows are flattened away into the grid, so nothing else records which
75    /// cells belong to which row, and without that a row cannot be given the
76    /// box its cells occupy. It reported 0x0 and "not displayed", and anything
77    /// walking a table by row had nothing to walk.
78    pub cells: Range<usize>,
79}
80
81/// A `<thead>`, `<tbody>` or `<tfoot>`, and the rows it holds.
82#[derive(Debug, Clone)]
83pub struct TableRowGroup {
84    pub node_id: NodeId,
85    /// Where this group's rows sit in [`TableContext::rows`].
86    pub rows: Range<usize>,
87}
88
89pub(crate) fn build_table_context(
90    doc: &mut BaseDocument,
91    table_root_node_id: NodeId,
92) -> (TableContext, Vec<NodeId>) {
93    let mut cells: Vec<TableCell> = Vec::new();
94    let mut rows: Vec<TableRow> = Vec::new();
95    let mut row_groups: Vec<TableRowGroup> = Vec::new();
96    let mut row = 0u16;
97    let mut col = 0u16;
98
99    let root_node = &mut doc.nodes[table_root_node_id];
100
101    let children = std::mem::take(&mut root_node.children);
102
103    let Some(stylo_styles) = root_node.primary_styles() else {
104        panic!("Ignoring table because it has no styles");
105    };
106
107    let mut style = stylo_taffy::to_taffy_style(&stylo_styles);
108    style.item_is_table = true;
109    // Use `dense` row-flow so that each cell scans the row from its
110    // leftmost column for the first free track. Without `dense`,
111    // Taffy's placement cursor advances globally across rows, which means
112    // cells in later rows do not backfill columns freed up by rowspan cells.
113    style.grid_auto_flow = taffy::GridAutoFlow::RowDense;
114    style.grid_auto_columns = Vec::new();
115    style.grid_auto_rows = Vec::new();
116
117    let is_fixed = match stylo_styles.clone_table_layout() {
118        TableLayout::Fixed => true,
119        TableLayout::Auto => false,
120    };
121
122    let border_collapse = stylo_styles.clone_border_collapse();
123    let border_spacing = stylo_styles.clone_border_spacing().0;
124
125    drop(stylo_styles);
126
127    let mut column_sizes: Vec<taffy::TrackSizingFunction> = Vec::new();
128    let mut first_cell_border: Option<ServoArc<Border>> = None;
129    for child_id in children.iter().copied() {
130        collect_table_cells(
131            doc,
132            child_id,
133            is_fixed,
134            border_collapse,
135            &mut row,
136            &mut col,
137            &mut cells,
138            &mut rows,
139            &mut row_groups,
140            &mut column_sizes,
141            &mut first_cell_border,
142        );
143    }
144    column_sizes.resize(col as usize, style_helpers::auto());
145
146    style.grid_template_columns = column_sizes.into_iter().map(|dim| dim.into()).collect();
147    style.grid_template_rows = vec![style_helpers::auto(); row as usize];
148
149    style.gap = match border_collapse {
150        BorderCollapse::Separate => {
151            // In the separated borders model, `border-spacing` also applies between
152            // the table border and the outermost cells, in addition to between cells.
153            let spacing_x = border_spacing.width.px();
154            let spacing_y = border_spacing.height.px();
155            let padding = style.padding.resolve_or_zero(None, resolve_calc_value);
156            style.padding = taffy::Rect {
157                left: style_helpers::length(padding.left + spacing_x),
158                right: style_helpers::length(padding.right + spacing_x),
159                top: style_helpers::length(padding.top + spacing_y),
160                bottom: style_helpers::length(padding.bottom + spacing_y),
161            };
162            taffy::Size {
163                width: style_helpers::length(spacing_x),
164                height: style_helpers::length(spacing_y),
165            }
166        }
167        BorderCollapse::Collapse => first_cell_border
168            .as_ref()
169            .map(|border| {
170                let x = border
171                    .border_left_width
172                    .0
173                    .max(border.border_right_width.0)
174                    .to_f32_px();
175                let y = border
176                    .border_top_width
177                    .0
178                    .max(border.border_bottom_width.0)
179                    .to_f32_px();
180                taffy::Size {
181                    width: style_helpers::length(x),
182                    height: style_helpers::length(y),
183                }
184            })
185            .unwrap_or(taffy::Size::ZERO.map(style_helpers::length)),
186    };
187
188    if border_collapse == BorderCollapse::Collapse {
189        style.border = taffy::Rect {
190            left: style.gap.width,
191            right: style.gap.width,
192            top: style.gap.height,
193            bottom: style.gap.height,
194        };
195    }
196
197    let layout_children = cells.iter().map(|cell| cell.node_id).collect();
198    let root_node = &mut doc.nodes[table_root_node_id];
199    root_node.children = children;
200
201    (
202        TableContext {
203            style,
204            cells,
205            rows,
206            row_groups,
207            computed_grid_info: AtomicRefCell::new(None),
208            border_collapse,
209            border_style: first_cell_border,
210        },
211        layout_children,
212    )
213}
214
215#[allow(clippy::too_many_arguments)]
216pub(crate) fn collect_table_cells(
217    doc: &mut BaseDocument,
218    node_id: NodeId,
219    is_fixed: bool,
220    border_collapse: BorderCollapse,
221    row: &mut u16,
222    col: &mut u16,
223    cells: &mut Vec<TableCell>,
224    rows: &mut Vec<TableRow>,
225    row_groups: &mut Vec<TableRowGroup>,
226    columns: &mut Vec<TrackSizingFunction>,
227    first_cell_border: &mut Option<ServoArc<Border>>,
228) {
229    let node = &mut doc.nodes[node_id];
230
231    if !node.is_element() {
232        return;
233    }
234
235    let Some(display) = node.primary_styles().map(|s| s.clone_display()) else {
236        #[cfg(feature = "tracing")]
237        tracing::info!("Ignoring table descendent because it has no styles");
238        return;
239    };
240
241    if display.outside() == DisplayOutside::None {
242        node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
243        return;
244    }
245
246    match display.inside() {
247        DisplayInside::TableRowGroup
248        | DisplayInside::TableHeaderGroup
249        | DisplayInside::TableFooterGroup
250        | DisplayInside::Contents => {
251            let is_row_group = !matches!(display.inside(), DisplayInside::Contents);
252            let first_row = rows.len();
253            let children = std::mem::take(&mut doc.nodes[node_id].children);
254            for child_id in children.iter().copied() {
255                doc.nodes[child_id]
256                    .remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
257                collect_table_cells(
258                    doc,
259                    child_id,
260                    is_fixed,
261                    border_collapse,
262                    row,
263                    col,
264                    cells,
265                    rows,
266                    row_groups,
267                    columns,
268                    first_cell_border,
269                );
270            }
271            doc.nodes[node_id].children = children;
272            if is_row_group {
273                row_groups.push(TableRowGroup {
274                    node_id,
275                    rows: first_row..rows.len(),
276                });
277            }
278        }
279        DisplayInside::TableRow => {
280            node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
281            *row += 1;
282            *col = 0;
283
284            let row_index = rows.len();
285            let first_cell = cells.len();
286            rows.push(TableRow {
287                node_id,
288                height: 0.0,
289                cells: first_cell..first_cell,
290            });
291
292            let children = std::mem::take(&mut doc.nodes[node_id].children);
293            for child_id in children.iter().copied() {
294                collect_table_cells(
295                    doc,
296                    child_id,
297                    is_fixed,
298                    border_collapse,
299                    row,
300                    col,
301                    cells,
302                    rows,
303                    row_groups,
304                    columns,
305                    first_cell_border,
306                );
307            }
308            doc.nodes[node_id].children = children;
309            rows[row_index].cells = first_cell..cells.len();
310        }
311        DisplayInside::TableCell => {
312            // node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
313            let stylo_style = &node.primary_styles().unwrap();
314            let colspan: u16 = node
315                .attr(local_name!("colspan"))
316                .and_then(|val| val.parse().ok())
317                .unwrap_or(1);
318            let rowspan: u16 = node
319                .attr(local_name!("rowspan"))
320                .and_then(|val| val.parse::<u16>().ok())
321                .map(|v| v.clamp(1, 65534))
322                .unwrap_or(1);
323            let mut style = stylo_taffy::to_taffy_style(stylo_style);
324
325            if first_cell_border.is_none() {
326                *first_cell_border = Some(stylo_style.clone_border());
327            }
328
329            if *row == 1 {
330                let column = match style.size.width.tag() {
331                    taffy::CompactLength::LENGTH_TAG => {
332                        let len = style.size.width.value();
333                        let padding = style.padding.resolve_or_zero(None, resolve_calc_value);
334                        let border = style.border.resolve_or_zero(None, resolve_calc_value);
335                        match style.box_sizing {
336                            taffy::BoxSizing::ContentBox => style_helpers::length(
337                                len + padding.left + padding.right + border.left + border.right,
338                            ),
339                            taffy::BoxSizing::BorderBox => style_helpers::length(len),
340                        }
341                    }
342                    taffy::CompactLength::PERCENT_TAG => {
343                        if is_fixed {
344                            style_helpers::percent(style.size.width.value())
345                        } else {
346                            style_helpers::auto()
347                        }
348                    }
349                    taffy::CompactLength::AUTO_TAG => style_helpers::auto(),
350                    // Dimension values are always length, percentage, auto or calc(),
351                    // so any other tag is a calc() value. Pass it through so that
352                    // Taffy resolves it against the table's inner width.
353                    _ => style.size.width.into(),
354                };
355                columns.push(column);
356            }
357
358            // Zero-out cell borders is BorderCollapse is Collapse
359            // Borders are handled at the table level in this mode
360            if border_collapse == BorderCollapse::Collapse {
361                style.border = taffy::Rect::ZERO.map(style_helpers::length);
362            }
363
364            // The margin properties do not apply to table-internal elements
365            style.margin = taffy::Rect::ZERO.map(style_helpers::length);
366
367            // Let Taffy auto-place the column. Combined with
368            // `grid_auto_flow: RowDense` set on the table root, each cell
369            // scans from the first track in its row for a free position,
370            // which makes cells automatically skip columns occupied by
371            // rowspan cells from earlier rows.
372            style.grid_column = taffy::Line {
373                start: style_helpers::auto(),
374                end: style_helpers::span(colspan),
375            };
376            style.grid_row = taffy::Line {
377                start: style_helpers::line(*row as i16),
378                end: style_helpers::span(rowspan),
379            };
380            style.size.width = style_helpers::auto();
381            cells.push(TableCell { node_id, style });
382
383            *col += colspan;
384        }
385        DisplayInside::Flow
386        | DisplayInside::FlowRoot
387        | DisplayInside::Flex
388        | DisplayInside::Grid => {
389            node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
390            // Probably a table caption: ignore
391            // println!(
392            //     "Warning: ignoring non-table typed descendent of table ({:?})",
393            //     display.inside()
394            // );
395        }
396        DisplayInside::TableColumnGroup | DisplayInside::TableColumn | DisplayInside::Table => {
397            node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
398            //Ignore
399        }
400        DisplayInside::None => {
401            node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
402            // Ignore
403        }
404    }
405}
406
407/// Give every `<tr>` and every row group the box its cells occupy.
408///
409/// Table layout flattens the rows into a grid of cells, so the row and
410/// row-group nodes never reach Taffy and nothing ever wrote a layout for them:
411/// each one reported 0x0 and, to anything asking whether an element is
412/// displayed, "no". A row is not laid out, it is described, so this runs after
413/// the grid is computed and derives each box from the cells that are.
414///
415/// Rounded and unrounded are both written. The rounding pass walks the box
416/// tree, and these nodes are not in it, so a value left only in the unrounded
417/// slot would never reach `final_layout`, which is what every geometry query
418/// reads.
419impl BaseDocument {
420    pub(crate) fn assign_table_row_layouts(&mut self) {
421        let contexts: Vec<Arc<TableContext>> = self
422            .nodes
423            .iter()
424            .filter(|(_, node)| node.flags.is_table_root())
425            .filter_map(
426                |(_, node)| match &node.data.downcast_element()?.special_data {
427                    crate::node::SpecialElementData::TableRoot(context) => {
428                        Some(Arc::clone(context))
429                    }
430                    _ => None,
431                },
432            )
433            .collect();
434        for context in contexts {
435            assign_row_layouts(self, &context);
436        }
437    }
438}
439
440pub(crate) fn assign_row_layouts(doc: &mut BaseDocument, ctx: &TableContext) {
441    if ctx.rows.is_empty() {
442        return;
443    }
444
445    // The grid's own extent, so that a row spans the table rather than only the
446    // cells that happen to be in it. A row with a colspan short of the full
447    // width is still as wide as the table.
448    let mut left = f32::MAX;
449    let mut right = f32::MIN;
450    for cell in &ctx.cells {
451        let Some(node) = doc.nodes.get(cell.node_id) else {
452            continue;
453        };
454        let layout = node.final_layout();
455        left = left.min(layout.location.x);
456        right = right.max(layout.location.x + layout.size.width);
457    }
458    if left > right {
459        return;
460    }
461
462    let mut row_extents: Vec<Option<(f32, f32)>> = Vec::with_capacity(ctx.rows.len());
463    for row in &ctx.rows {
464        let mut top = f32::MAX;
465        let mut bottom = f32::MIN;
466        for cell in &ctx.cells[row.cells.clone()] {
467            let Some(node) = doc.nodes.get(cell.node_id) else {
468                continue;
469            };
470            let layout = node.final_layout();
471            top = top.min(layout.location.y);
472            bottom = bottom.max(layout.location.y + layout.size.height);
473        }
474        if top > bottom {
475            row_extents.push(None);
476            continue;
477        }
478        row_extents.push(Some((top, bottom)));
479        write_box(doc, row.node_id, left, top, right - left, bottom - top);
480    }
481
482    for group in &ctx.row_groups {
483        let mut top = f32::MAX;
484        let mut bottom = f32::MIN;
485        for extent in row_extents[group.rows.clone()].iter().flatten() {
486            top = top.min(extent.0);
487            bottom = bottom.max(extent.1);
488        }
489        if top > bottom {
490            continue;
491        }
492        write_box(doc, group.node_id, left, top, right - left, bottom - top);
493    }
494}
495
496fn write_box(doc: &mut BaseDocument, node_id: NodeId, x: f32, y: f32, width: f32, height: f32) {
497    let Some(node) = doc.nodes.get_mut(node_id) else {
498        return;
499    };
500    let mut layout = taffy::Layout::with_order(node.final_layout().order);
501    layout.location = taffy::Point { x, y };
502    layout.size = taffy::Size { width, height };
503    layout.content_size = layout.size;
504    *node.unrounded_layout_mut() = layout;
505    *node.final_layout_mut() = layout;
506}
507
508pub struct RangeIter(Range<usize>);
509
510impl Iterator for RangeIter {
511    type Item = taffy::NodeId;
512
513    fn next(&mut self) -> Option<Self::Item> {
514        self.0.next().map(taffy::NodeId::from)
515    }
516}
517
518impl taffy::TraversePartialTree for TableTreeWrapper<'_> {
519    type ChildIter<'a>
520        = RangeIter
521    where
522        Self: 'a;
523
524    #[inline(always)]
525    fn child_ids(&self, _node_id: taffy::NodeId) -> Self::ChildIter<'_> {
526        RangeIter(0..self.ctx.cells.len())
527    }
528
529    #[inline(always)]
530    fn child_count(&self, _node_id: taffy::NodeId) -> usize {
531        self.ctx.cells.len()
532    }
533
534    #[inline(always)]
535    fn get_child_id(&self, _node_id: taffy::NodeId, index: usize) -> taffy::NodeId {
536        index.into()
537    }
538}
539impl taffy::TraverseTree for TableTreeWrapper<'_> {}
540
541impl taffy::LayoutPartialTree for TableTreeWrapper<'_> {
542    type CoreContainerStyle<'a>
543        = &'a taffy::Style<Atom>
544    where
545        Self: 'a;
546
547    type CustomIdent = Atom;
548
549    fn get_core_container_style(&self, _node_id: taffy::NodeId) -> &taffy::Style<Atom> {
550        &self.ctx.style
551    }
552
553    fn resolve_calc_value(&self, calc_ptr: *const (), parent_size: f32) -> f32 {
554        resolve_calc_value(calc_ptr, parent_size)
555    }
556
557    fn set_unrounded_layout(&mut self, node_id: taffy::NodeId, layout: &taffy::Layout) {
558        let node_id = crate::taffy_node_id(self.ctx.cells[usize::from(node_id)].node_id);
559        self.doc.set_unrounded_layout(node_id, layout)
560    }
561
562    fn compute_child_layout(
563        &mut self,
564        node_id: taffy::NodeId,
565        inputs: taffy::tree::LayoutInput,
566    ) -> taffy::LayoutOutput {
567        let cell = &self.ctx.cells[usize::from(node_id)];
568        let node_id = crate::taffy_node_id(cell.node_id);
569        self.doc.compute_child_layout(node_id, inputs)
570    }
571}
572
573impl taffy::LayoutGridContainer for TableTreeWrapper<'_> {
574    type GridContainerStyle<'a>
575        = &'a taffy::Style<Atom>
576    where
577        Self: 'a;
578
579    type GridItemStyle<'a>
580        = &'a taffy::Style<Atom>
581    where
582        Self: 'a;
583
584    fn get_grid_container_style(&self, node_id: taffy::NodeId) -> Self::GridContainerStyle<'_> {
585        self.get_core_container_style(node_id)
586    }
587
588    fn get_grid_child_style(&self, child_node_id: taffy::NodeId) -> Self::GridItemStyle<'_> {
589        &self.ctx.cells[usize::from(child_node_id)].style
590    }
591
592    fn set_detailed_grid_info(
593        &mut self,
594        _node_id: taffy::NodeId,
595        detailed_grid_info: DetailedGridInfo,
596    ) {
597        *self.ctx.computed_grid_info.borrow_mut() = Some(detailed_grid_info);
598    }
599}