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::{BoxModel, LayoutBox, LayoutNode};
9
10use super::types::{ContainerRole, InfoNode, NodeKind};
11
12pub fn align_table_columns(layout: &mut LayoutNode, info: &InfoNode) {
13    for (child_layout, child_info) in layout.children.iter_mut().zip(&info.children) {
14        if let Some(child_layout) = child_layout.node_mut() {
15            align_table_columns(child_layout, child_info);
16        }
17    }
18
19    if container_role(info) == Some(&ContainerRole::Table) {
20        align_one_table(layout, info);
21    }
22}
23
24fn align_one_table(layout: &mut LayoutNode, info: &InfoNode) {
25    let mut widths: Vec<f32> = Vec::new();
26    visit_rows(layout, info, &mut |row, row_info| {
27        let mut column = 0;
28        for (cell, cell_info) in row.children.iter().zip(&row_info.children) {
29            if container_role(cell_info) != Some(&ContainerRole::TableCell) {
30                continue;
31            }
32            let Some(cell) = cell.node() else {
33                continue;
34            };
35            if widths.len() <= column {
36                widths.resize(column + 1, 0.0);
37            }
38            widths[column] = widths[column].max(cell.layout_box.width_box());
39            column += 1;
40        }
41    });
42
43    if widths.is_empty() {
44        return;
45    }
46
47    let table_width = widths.iter().sum();
48    visit_rows_mut(layout, info, table_width, &widths);
49    grow_box_width(&mut layout.layout_box, table_width);
50}
51
52fn visit_rows(
53    layout: &LayoutNode,
54    info: &InfoNode,
55    visit: &mut impl FnMut(&LayoutNode, &InfoNode),
56) {
57    for (child, child_info) in layout.children.iter().zip(&info.children) {
58        let Some(child) = child.node() else {
59            continue;
60        };
61        match container_role(child_info) {
62            Some(ContainerRole::TableRow) => visit(child, child_info),
63            Some(ContainerRole::TableRowGroup) => visit_rows(child, child_info, visit),
64            _ => {}
65        }
66    }
67}
68
69fn visit_rows_mut(layout: &mut LayoutNode, info: &InfoNode, table_width: f32, widths: &[f32]) {
70    for (child, child_info) in layout.children.iter_mut().zip(&info.children) {
71        let Some(child) = child.node_mut() else {
72            continue;
73        };
74        match container_role(child_info) {
75            Some(ContainerRole::TableRow) => align_row(child, child_info, widths),
76            Some(ContainerRole::TableRowGroup) => {
77                visit_rows_mut(child, child_info, table_width, widths);
78                grow_box_width(&mut child.layout_box, table_width);
79            }
80            _ => {}
81        }
82    }
83}
84
85fn align_row(row: &mut LayoutNode, info: &InfoNode, widths: &[f32]) {
86    let start_x = row
87        .children
88        .iter()
89        .zip(&info.children)
90        .find_map(|(cell, cell_info)| {
91            if container_role(cell_info) != Some(&ContainerRole::TableCell) {
92                return None;
93            }
94            Some(cell.node()?.layout_box.iter().next()?.border_box.x)
95        })
96        .unwrap_or(0.0);
97
98    let mut column = 0;
99    let mut x = start_x;
100    for (cell, cell_info) in row.children.iter_mut().zip(&info.children) {
101        if container_role(cell_info) != Some(&ContainerRole::TableCell) {
102            continue;
103        }
104        let Some(cell) = cell.node_mut() else {
105            continue;
106        };
107        let Some(width) = widths.get(column).copied() else {
108            break;
109        };
110        move_box_x(&mut cell.layout_box, x);
111        grow_box_width(&mut cell.layout_box, width);
112        x += width;
113        column += 1;
114    }
115    grow_box_width(&mut row.layout_box, x - start_x);
116}
117
118fn container_role(info: &InfoNode) -> Option<&ContainerRole> {
119    match &info.kind {
120        NodeKind::Container { role, .. } => Some(role),
121        _ => None,
122    }
123}
124
125fn move_box_x(layout_box: &mut LayoutBox, target_x: f32) {
126    match layout_box {
127        LayoutBox::None => {}
128        LayoutBox::BlockBox(model) => translate_box_x(model, target_x - model.border_box.x),
129        LayoutBox::InlineBox(inline) => {
130            let dx = target_x - inline.box_model.border_box.x;
131            translate_box_x(&mut inline.box_model, dx);
132            inline
133                .line_spans
134                .iter_mut()
135                .for_each(|l| l.line_pos.0 += dx);
136        }
137    }
138}
139
140fn translate_box_x(model: &mut BoxModel, dx: f32) {
141    model.border_box.x += dx;
142    model.padding_box.x += dx;
143    model.content_box.x += dx;
144    model.children_box.x += dx;
145}
146
147fn grow_box_width(layout_box: &mut LayoutBox, target_border_width: f32) {
148    let model = match layout_box {
149        LayoutBox::None => return,
150        LayoutBox::BlockBox(model) => model,
151        LayoutBox::InlineBox(inline) => &mut inline.box_model,
152    };
153    let delta = (target_border_width - model.border_box.width).max(0.0);
154    model.border_box.width += delta;
155    model.padding_box.width += delta;
156    model.content_box.width += delta;
157    model.children_box.width += delta;
158}
159
160#[cfg(test)]
161mod tests {
162    use ui_layout::{LayoutNode, Rect, Style};
163
164    use super::*;
165    use crate::engine::layouter::types::ContainerStyle;
166
167    fn info(role: ContainerRole, children: Vec<InfoNode>) -> InfoNode {
168        InfoNode {
169            kind: NodeKind::Container {
170                scroll_x: false,
171                scroll_y: false,
172                scroll_offset_x: 0.0,
173                scroll_offset_y: 0.0,
174                style: ContainerStyle::default(),
175                role,
176            },
177            children,
178            dom_id: None,
179        }
180    }
181
182    fn box_node(x: f32, width: f32, children: Vec<LayoutNode>) -> LayoutNode {
183        let mut node = LayoutNode::with_children(Style::default(), children);
184        node.layout_box = LayoutBox::BlockBox(
185            Rect {
186                x,
187                width,
188                height: 20.0,
189                ..Default::default()
190            }
191            .into(),
192        );
193        node
194    }
195
196    #[test]
197    fn shares_column_widths_across_row_groups() {
198        let header = box_node(
199            0.0,
200            140.0,
201            vec![box_node(0.0, 70.0, vec![]), box_node(70.0, 70.0, vec![])],
202        );
203        let body = box_node(
204            0.0,
205            100.0,
206            vec![box_node(0.0, 60.0, vec![]), box_node(60.0, 40.0, vec![])],
207        );
208        let mut table = box_node(
209            0.0,
210            140.0,
211            vec![
212                box_node(0.0, 140.0, vec![header]),
213                box_node(0.0, 100.0, vec![body]),
214            ],
215        );
216        let row_info = || {
217            info(
218                ContainerRole::TableRow,
219                vec![
220                    info(ContainerRole::TableCell, vec![]),
221                    info(ContainerRole::TableCell, vec![]),
222                ],
223            )
224        };
225        let table_info = info(
226            ContainerRole::Table,
227            vec![
228                info(ContainerRole::TableRowGroup, vec![row_info()]),
229                info(ContainerRole::TableRowGroup, vec![row_info()]),
230            ],
231        );
232
233        align_table_columns(&mut table, &table_info);
234
235        let body_group = table.children[1].node().unwrap();
236        let body_row = body_group.children[0].node().unwrap();
237        let first = body_row.children[0].node().unwrap();
238        let second = body_row.children[1].node().unwrap();
239        assert_eq!(first.layout_box.width_box(), 70.0);
240        assert_eq!(second.layout_box.width_box(), 70.0);
241        assert_eq!(second.layout_box.iter().next().unwrap().border_box.x, 70.0);
242        assert_eq!(body_row.layout_box.width_box(), 140.0);
243        assert_eq!(body_group.layout_box.width_box(), 140.0);
244    }
245}