Skip to main content

cssbox_core/
table.rs

1//! CSS Table Layout algorithm.
2//!
3//! Implements CSS 2.1 §17 — Tables.
4//! Supports both `table-layout: fixed` and `table-layout: auto`.
5
6use crate::box_model::BoxModel;
7use crate::fragment::{Fragment, FragmentKind};
8use crate::geometry::{Point, Size};
9use crate::layout::{self, LayoutContext};
10use crate::style::*;
11use crate::tree::NodeId;
12
13/// Layout a table element and its contents.
14pub fn layout_table(
15    ctx: &LayoutContext,
16    node: NodeId,
17    containing_block_width: f32,
18    containing_block_height: f32,
19) -> Fragment {
20    let style = ctx.tree.style(node);
21    let mut fragment = Fragment::new(node, FragmentKind::Box);
22
23    // Resolve table box model
24    let border = BoxModel::resolve_border(style);
25    let padding = BoxModel::resolve_padding(style, containing_block_width);
26    let margin = BoxModel::resolve_margin(style, containing_block_width);
27
28    fragment.border = border;
29    fragment.padding = padding;
30    fragment.margin = margin;
31
32    let content_width = match style.width.resolve(containing_block_width) {
33        Some(mut w) => {
34            if style.box_sizing == BoxSizing::BorderBox {
35                w = (w - border.horizontal() - padding.horizontal()).max(0.0);
36            }
37            w
38        }
39        None => (containing_block_width
40            - border.horizontal()
41            - padding.horizontal()
42            - margin.horizontal())
43        .max(0.0),
44    };
45
46    // Collect table structure
47    let mut table = TableStructure::new();
48    collect_table_structure(ctx, node, &mut table);
49
50    let is_fixed = style.table_layout == TableLayout::Fixed;
51    let is_collapse = style.border_collapse == BorderCollapse::Collapse;
52    let border_spacing = if is_collapse {
53        0.0
54    } else {
55        style.border_spacing
56    };
57
58    // Determine column widths
59    let col_widths = if is_fixed {
60        fixed_table_layout(&table, content_width, border_spacing, ctx)
61    } else {
62        auto_table_layout(&table, content_width, border_spacing, ctx)
63    };
64
65    let num_cols = col_widths.len();
66    let total_spacing = border_spacing * (num_cols + 1) as f32;
67    let table_content_width = col_widths.iter().sum::<f32>() + total_spacing;
68
69    // Layout captions (top)
70    let mut cursor_y = 0.0f32;
71    if style.caption_side == CaptionSide::Top {
72        for &caption_node in &table.captions {
73            let mut cap_frag =
74                layout::layout_node(ctx, caption_node, content_width, containing_block_height);
75            cap_frag.position = Point::new(cap_frag.margin.left, cursor_y);
76            cursor_y += cap_frag.border_box().height + cap_frag.margin.vertical();
77            fragment.children.push(cap_frag);
78        }
79    }
80
81    // Layout rows
82    for row in table.rows.iter() {
83        let mut row_height: f32 = 0.0;
84        let mut cell_fragments: Vec<(usize, Fragment)> = Vec::new();
85
86        for cell in &row.cells {
87            let col_idx = cell.col_start;
88            let col_span = cell.col_span;
89
90            // Calculate cell width
91            let mut cell_width = 0.0f32;
92            for c in col_idx..(col_idx + col_span).min(num_cols) {
93                cell_width += col_widths[c];
94                if c > col_idx {
95                    cell_width += border_spacing;
96                }
97            }
98
99            // Layout cell content
100            let cell_frag =
101                layout::layout_node(ctx, cell.node, cell_width, containing_block_height);
102
103            let cell_total_height = cell_frag.border_box().height;
104            row_height = row_height.max(cell_total_height);
105
106            cell_fragments.push((col_idx, cell_frag));
107        }
108
109        // Resolve specified row height
110        if let Some(row_node) = row.node {
111            let row_style = ctx.tree.style(row_node);
112            if let Some(h) = row_style.height.resolve(containing_block_height) {
113                row_height = row_height.max(h);
114            }
115        }
116
117        // Position cells
118        for (col_idx, mut cell_frag) in cell_fragments {
119            let x = col_position(&col_widths, col_idx, border_spacing);
120            cell_frag.position = Point::new(x, cursor_y);
121
122            // Vertical alignment in cell — default to top
123            // TODO: implement vertical-align for table cells
124            fragment.children.push(cell_frag);
125        }
126
127        cursor_y += row_height + border_spacing;
128    }
129
130    // Layout captions (bottom)
131    if style.caption_side == CaptionSide::Bottom {
132        for &caption_node in &table.captions {
133            let mut cap_frag =
134                layout::layout_node(ctx, caption_node, content_width, containing_block_height);
135            cap_frag.position = Point::new(cap_frag.margin.left, cursor_y);
136            cursor_y += cap_frag.border_box().height + cap_frag.margin.vertical();
137            fragment.children.push(cap_frag);
138        }
139    }
140
141    let final_height = style
142        .height
143        .resolve(containing_block_height)
144        .unwrap_or(cursor_y);
145    let min_h = style.min_height.resolve(containing_block_height);
146    let max_h = style
147        .max_height
148        .resolve(containing_block_height)
149        .unwrap_or(f32::INFINITY);
150
151    fragment.size = Size::new(
152        table_content_width.max(content_width),
153        final_height.max(min_h).min(max_h),
154    );
155
156    fragment
157}
158
159// --- Table structure collection ---
160
161struct TableStructure {
162    rows: Vec<TableRow>,
163    captions: Vec<NodeId>,
164    column_specs: Vec<ColumnSpec>,
165}
166
167struct TableRow {
168    node: Option<NodeId>,
169    cells: Vec<TableCell>,
170}
171
172struct TableCell {
173    node: NodeId,
174    col_start: usize,
175    col_span: usize,
176}
177
178struct ColumnSpec {
179    width: Option<f32>,
180}
181
182impl TableStructure {
183    fn new() -> Self {
184        Self {
185            rows: Vec::new(),
186            captions: Vec::new(),
187            column_specs: Vec::new(),
188        }
189    }
190
191    fn num_columns(&self) -> usize {
192        let from_cells = self
193            .rows
194            .iter()
195            .flat_map(|r| &r.cells)
196            .map(|c| c.col_start + c.col_span)
197            .max()
198            .unwrap_or(0);
199        from_cells.max(self.column_specs.len())
200    }
201}
202
203fn collect_table_structure(ctx: &LayoutContext, table_node: NodeId, table: &mut TableStructure) {
204    let children = ctx.tree.children(table_node);
205
206    for &child_id in children {
207        let child_style = ctx.tree.style(child_id);
208
209        match child_style.display.inner {
210            DisplayInner::TableCaption => {
211                table.captions.push(child_id);
212            }
213            DisplayInner::TableRow => {
214                collect_row(ctx, child_id, table);
215            }
216            DisplayInner::TableRowGroup
217            | DisplayInner::TableHeaderGroup
218            | DisplayInner::TableFooterGroup => {
219                // Recurse into row groups
220                let group_children = ctx.tree.children(child_id);
221                for &gc in group_children {
222                    let gc_style = ctx.tree.style(gc);
223                    if gc_style.display.inner == DisplayInner::TableRow {
224                        collect_row(ctx, gc, table);
225                    }
226                }
227            }
228            DisplayInner::TableColumn | DisplayInner::TableColumnGroup => {
229                // Column definitions (for width hints)
230                let col_style = ctx.tree.style(child_id);
231                if let Some(w) = col_style.width.resolve(0.0) {
232                    table.column_specs.push(ColumnSpec { width: Some(w) });
233                }
234            }
235            DisplayInner::TableCell => {
236                // Direct cell child (implicit row)
237                let row = TableRow {
238                    node: None,
239                    cells: vec![TableCell {
240                        node: child_id,
241                        col_start: 0,
242                        col_span: 1,
243                    }],
244                };
245                table.rows.push(row);
246            }
247            _ => {
248                // Treat as anonymous table cell
249            }
250        }
251    }
252}
253
254fn collect_row(ctx: &LayoutContext, row_node: NodeId, table: &mut TableStructure) {
255    let children = ctx.tree.children(row_node);
256    let mut cells = Vec::new();
257    let mut col = 0;
258
259    for &child_id in children {
260        let child_style = ctx.tree.style(child_id);
261        if child_style.display.is_none() {
262            continue;
263        }
264
265        cells.push(TableCell {
266            node: child_id,
267            col_start: col,
268            col_span: 1, // TODO: colspan attribute support
269        });
270        col += 1;
271    }
272
273    table.rows.push(TableRow {
274        node: Some(row_node),
275        cells,
276    });
277}
278
279// --- Fixed table layout (CSS 2.1 §17.5.2.1) ---
280
281fn fixed_table_layout(
282    table: &TableStructure,
283    table_width: f32,
284    border_spacing: f32,
285    ctx: &LayoutContext,
286) -> Vec<f32> {
287    let num_cols = table.num_columns().max(1);
288    let total_spacing = border_spacing * (num_cols + 1) as f32;
289    let available = (table_width - total_spacing).max(0.0);
290
291    let mut widths = vec![0.0f32; num_cols];
292    let mut assigned = vec![false; num_cols];
293
294    // First: use column specs
295    for (i, spec) in table.column_specs.iter().enumerate() {
296        if i < num_cols {
297            if let Some(w) = spec.width {
298                widths[i] = w;
299                assigned[i] = true;
300            }
301        }
302    }
303
304    // Second: use first row cell widths
305    if let Some(first_row) = table.rows.first() {
306        for cell in &first_row.cells {
307            if cell.col_start < num_cols && !assigned[cell.col_start] {
308                let cell_style = ctx.tree.style(cell.node);
309                if let Some(w) = cell_style.width.resolve(available) {
310                    widths[cell.col_start] = w;
311                    assigned[cell.col_start] = true;
312                }
313            }
314        }
315    }
316
317    // Third: distribute remaining space equally to unassigned columns
318    let assigned_total: f32 = widths.iter().sum();
319    let remaining = (available - assigned_total).max(0.0);
320    let unassigned_count = assigned.iter().filter(|&&a| !a).count();
321
322    if unassigned_count > 0 {
323        let per_col = remaining / unassigned_count as f32;
324        for i in 0..num_cols {
325            if !assigned[i] {
326                widths[i] = per_col;
327            }
328        }
329    }
330
331    widths
332}
333
334// --- Auto table layout (CSS 2.1 §17.5.2.2) ---
335
336fn auto_table_layout(
337    table: &TableStructure,
338    table_width: f32,
339    border_spacing: f32,
340    ctx: &LayoutContext,
341) -> Vec<f32> {
342    let num_cols = table.num_columns().max(1);
343    let total_spacing = border_spacing * (num_cols + 1) as f32;
344    let available = (table_width - total_spacing).max(0.0);
345
346    // Simplified auto layout: measure minimum and preferred widths
347    let mut min_widths = vec![0.0f32; num_cols];
348    let mut pref_widths = vec![0.0f32; num_cols];
349
350    for row in &table.rows {
351        for cell in &row.cells {
352            if cell.col_span == 1 && cell.col_start < num_cols {
353                let cell_style = ctx.tree.style(cell.node);
354
355                // If cell has explicit width, use that
356                if let Some(w) = cell_style.width.resolve(available) {
357                    pref_widths[cell.col_start] = pref_widths[cell.col_start].max(w);
358                    min_widths[cell.col_start] = min_widths[cell.col_start].max(w);
359                } else {
360                    // Content-based: use min/max content widths
361                    let min_w = 1.0; // minimum content width placeholder
362                    let pref_w = available / num_cols as f32;
363                    min_widths[cell.col_start] = min_widths[cell.col_start].max(min_w);
364                    pref_widths[cell.col_start] = pref_widths[cell.col_start].max(pref_w);
365                }
366            }
367        }
368    }
369
370    // Distribute available width
371    let total_pref: f32 = pref_widths.iter().sum();
372
373    if total_pref <= available {
374        // All preferred widths fit — distribute extra proportionally
375        let extra = available - total_pref;
376        let per_col = extra / num_cols as f32;
377        let mut result = pref_widths.clone();
378        for w in &mut result {
379            *w += per_col;
380        }
381        result
382    } else {
383        // Need to shrink — use minimum widths as floor
384        let total_min: f32 = min_widths.iter().sum();
385        if total_min >= available {
386            // Even minimums don't fit — use minimums
387            min_widths
388        } else {
389            // Interpolate between min and preferred
390            let flex_range = total_pref - total_min;
391            let available_range = available - total_min;
392            let factor = if flex_range > 0.0 {
393                available_range / flex_range
394            } else {
395                0.0
396            };
397
398            let mut result = Vec::with_capacity(num_cols);
399            for i in 0..num_cols {
400                let w = min_widths[i] + (pref_widths[i] - min_widths[i]) * factor;
401                result.push(w);
402            }
403            result
404        }
405    }
406}
407
408fn col_position(col_widths: &[f32], col_idx: usize, border_spacing: f32) -> f32 {
409    let mut x = border_spacing;
410    for i in 0..col_idx {
411        x += col_widths[i] + border_spacing;
412    }
413    x
414}
415
416#[cfg(test)]
417mod tests {
418    use super::*;
419    use crate::layout::{compute_layout, FixedWidthTextMeasure};
420    use crate::style::ComputedStyle;
421    use crate::tree::BoxTreeBuilder;
422    use crate::values::LengthPercentageAuto;
423
424    fn make_table_style() -> ComputedStyle {
425        ComputedStyle {
426            display: Display::TABLE,
427            ..ComputedStyle::block()
428        }
429    }
430
431    fn make_row_style() -> ComputedStyle {
432        ComputedStyle {
433            display: Display::TABLE_ROW,
434            ..ComputedStyle::block()
435        }
436    }
437
438    fn make_cell_style() -> ComputedStyle {
439        let mut s = ComputedStyle {
440            display: Display::TABLE_CELL,
441            ..ComputedStyle::block()
442        };
443        s.height = LengthPercentageAuto::px(30.0);
444        s
445    }
446
447    #[test]
448    fn test_simple_table_2x2() {
449        let mut builder = BoxTreeBuilder::new();
450        let root = builder.root(make_table_style());
451
452        let row1 = builder.element(root, make_row_style());
453        builder.element(row1, make_cell_style());
454        builder.element(row1, make_cell_style());
455
456        let row2 = builder.element(root, make_row_style());
457        builder.element(row2, make_cell_style());
458        builder.element(row2, make_cell_style());
459
460        let tree = builder.build();
461        let result = compute_layout(&tree, &FixedWidthTextMeasure, Size::new(800.0, 600.0));
462
463        let root_layout = result.bounding_rect(tree.root()).unwrap();
464        assert!(root_layout.width >= 800.0);
465        assert!(root_layout.height > 0.0);
466    }
467
468    #[test]
469    fn test_col_position() {
470        let widths = vec![100.0, 200.0, 150.0];
471        assert_eq!(col_position(&widths, 0, 5.0), 5.0);
472        assert_eq!(col_position(&widths, 1, 5.0), 110.0); // 5 + 100 + 5
473        assert_eq!(col_position(&widths, 2, 5.0), 315.0); // 5 + 100 + 5 + 200 + 5
474    }
475}