Skip to main content

orinium_browser/engine/layouter/
table_layout.rs

1//! Post-layout column alignment for HTML tables.
2//!
3//! Table elements currently use flex formatting in `ui_layout`. Flex rows
4//! size their cells independently, while HTML tables share column widths
5//! across every row group. This pass preserves the layout/info tree shape and
6//! expands each column to the widest cell found anywhere in the table.
7
8use ui_layout::LayoutNode;
9
10use super::shift::{grow_box_width, move_box_x};
11use super::types::{ContainerRole, InfoNode, NodeKind};
12
13pub fn align_table_columns(layout: &mut LayoutNode, info: &InfoNode) {
14    for (child_layout, child_info) in layout.children.iter_mut().zip(&info.children) {
15        if let Some(child_layout) = child_layout.node_mut() {
16            align_table_columns(child_layout, child_info);
17        }
18    }
19
20    if container_role(info) == Some(&ContainerRole::Table) {
21        align_one_table(layout, info);
22    }
23}
24
25fn align_one_table(layout: &mut LayoutNode, info: &InfoNode) {
26    let mut widths: Vec<f32> = Vec::new();
27    visit_rows(layout, info, &mut |row, row_info| {
28        let mut column = 0;
29        for (cell, cell_info) in row.children.iter().zip(&row_info.children) {
30            if container_role(cell_info) != Some(&ContainerRole::TableCell) {
31                continue;
32            }
33            let Some(cell) = cell.node() else {
34                continue;
35            };
36            if widths.len() <= column {
37                widths.resize(column + 1, 0.0);
38            }
39            widths[column] = widths[column].max(cell.layout_box.width_box());
40            column += 1;
41        }
42    });
43
44    if widths.is_empty() {
45        return;
46    }
47
48    let table_width = widths.iter().sum();
49    visit_rows_mut(layout, info, table_width, &widths);
50    grow_box_width(&mut layout.layout_box, table_width);
51}
52
53fn visit_rows(
54    layout: &LayoutNode,
55    info: &InfoNode,
56    visit: &mut impl FnMut(&LayoutNode, &InfoNode),
57) {
58    for (child, child_info) in layout.children.iter().zip(&info.children) {
59        let Some(child) = child.node() else {
60            continue;
61        };
62        match container_role(child_info) {
63            Some(ContainerRole::TableRow) => visit(child, child_info),
64            Some(ContainerRole::TableRowGroup) => visit_rows(child, child_info, visit),
65            _ => {}
66        }
67    }
68}
69
70fn visit_rows_mut(layout: &mut LayoutNode, info: &InfoNode, table_width: f32, widths: &[f32]) {
71    for (child, child_info) in layout.children.iter_mut().zip(&info.children) {
72        let Some(child) = child.node_mut() else {
73            continue;
74        };
75        match container_role(child_info) {
76            Some(ContainerRole::TableRow) => align_row(child, child_info, widths),
77            Some(ContainerRole::TableRowGroup) => {
78                visit_rows_mut(child, child_info, table_width, widths);
79                grow_box_width(&mut child.layout_box, table_width);
80            }
81            _ => {}
82        }
83    }
84}
85
86fn align_row(row: &mut LayoutNode, info: &InfoNode, widths: &[f32]) {
87    let start_x = row
88        .children
89        .iter()
90        .zip(&info.children)
91        .find_map(|(cell, cell_info)| {
92            if container_role(cell_info) != Some(&ContainerRole::TableCell) {
93                return None;
94            }
95            Some(cell.node()?.layout_box.iter().next()?.border_box.x)
96        })
97        .unwrap_or(0.0);
98
99    let mut column = 0;
100    let mut x = start_x;
101    for (cell, cell_info) in row.children.iter_mut().zip(&info.children) {
102        if container_role(cell_info) != Some(&ContainerRole::TableCell) {
103            continue;
104        }
105        let Some(cell) = cell.node_mut() else {
106            continue;
107        };
108        let Some(width) = widths.get(column).copied() else {
109            break;
110        };
111        move_box_x(&mut cell.layout_box, x);
112        grow_box_width(&mut cell.layout_box, width);
113        x += width;
114        column += 1;
115    }
116    grow_box_width(&mut row.layout_box, x - start_x);
117}
118
119fn container_role(info: &InfoNode) -> Option<&ContainerRole> {
120    match &info.kind {
121        NodeKind::Container { role, .. } => Some(role),
122        _ => None,
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use ui_layout::{LayoutBox, LayoutNode, Rect, Style};
129
130    use super::*;
131    use crate::engine::layouter::types::ContainerStyle;
132
133    fn info(role: ContainerRole, children: Vec<InfoNode>) -> InfoNode {
134        InfoNode {
135            kind: NodeKind::Container {
136                scroll_x: false,
137                scroll_y: false,
138                scroll_offset_x: 0.0,
139                scroll_offset_y: 0.0,
140                style: ContainerStyle::default(),
141                role,
142            },
143            children,
144            dom_id: None,
145        }
146    }
147
148    fn box_node(x: f32, width: f32, children: Vec<LayoutNode>) -> LayoutNode {
149        let mut node = LayoutNode::with_children(Style::default(), children);
150        node.layout_box = LayoutBox::BlockBox(
151            Rect {
152                x,
153                width,
154                height: 20.0,
155                ..Default::default()
156            }
157            .into(),
158        );
159        node
160    }
161
162    #[test]
163    fn shares_column_widths_across_row_groups() {
164        let header = box_node(
165            0.0,
166            140.0,
167            vec![box_node(0.0, 70.0, vec![]), box_node(70.0, 70.0, vec![])],
168        );
169        let body = box_node(
170            0.0,
171            100.0,
172            vec![box_node(0.0, 60.0, vec![]), box_node(60.0, 40.0, vec![])],
173        );
174        let mut table = box_node(
175            0.0,
176            140.0,
177            vec![
178                box_node(0.0, 140.0, vec![header]),
179                box_node(0.0, 100.0, vec![body]),
180            ],
181        );
182        let row_info = || {
183            info(
184                ContainerRole::TableRow,
185                vec![
186                    info(ContainerRole::TableCell, vec![]),
187                    info(ContainerRole::TableCell, vec![]),
188                ],
189            )
190        };
191        let table_info = info(
192            ContainerRole::Table,
193            vec![
194                info(ContainerRole::TableRowGroup, vec![row_info()]),
195                info(ContainerRole::TableRowGroup, vec![row_info()]),
196            ],
197        );
198
199        align_table_columns(&mut table, &table_info);
200
201        let body_group = table.children[1].node().unwrap();
202        let body_row = body_group.children[0].node().unwrap();
203        let first = body_row.children[0].node().unwrap();
204        let second = body_row.children[1].node().unwrap();
205        assert_eq!(first.layout_box.width_box(), 70.0);
206        assert_eq!(second.layout_box.width_box(), 70.0);
207        assert_eq!(second.layout_box.iter().next().unwrap().border_box.x, 70.0);
208        assert_eq!(body_row.layout_box.width_box(), 140.0);
209        assert_eq!(body_group.layout_box.width_box(), 140.0);
210    }
211}