Skip to main content

text_typeset/layout/
table.rs

1use crate::font::registry::FontRegistry;
2use crate::layout::block::{BlockLayout, BlockLayoutParams, layout_block};
3use crate::types::{DecorationKind, DecorationRect};
4
5/// Parameters for a table, extracted from text-document's TableSnapshot.
6pub struct TableLayoutParams {
7    pub table_id: usize,
8    pub rows: usize,
9    pub columns: usize,
10    /// Relative column widths (0.0-1.0). If empty, columns are distributed evenly.
11    pub column_widths: Vec<f32>,
12    pub border_width: f32,
13    pub cell_spacing: f32,
14    pub cell_padding: f32,
15    /// One entry per cell, in row-major order (row 0 col 0, row 0 col 1, ...).
16    pub cells: Vec<CellLayoutParams>,
17}
18
19/// Parameters for a single table cell.
20pub struct CellLayoutParams {
21    pub row: usize,
22    pub column: usize,
23    pub blocks: Vec<BlockLayoutParams>,
24    pub background_color: Option<[f32; 4]>,
25}
26
27/// Computed layout for an entire table.
28pub struct TableLayout {
29    pub table_id: usize,
30    /// Y position relative to document start (set by flow).
31    pub y: f32,
32    /// Total height of the table including borders.
33    pub total_height: f32,
34    /// Total width of the table.
35    pub total_width: f32,
36    /// Computed column x positions (left edge of each column's content area).
37    pub column_xs: Vec<f32>,
38    /// Computed column widths (content area, excluding padding).
39    pub column_content_widths: Vec<f32>,
40    /// Computed row y positions (top edge of each row's content area, relative to table top).
41    pub row_ys: Vec<f32>,
42    /// Computed row heights (content area).
43    pub row_heights: Vec<f32>,
44    /// Laid out cells. Each cell contains its block layouts.
45    pub cell_layouts: Vec<CellLayout>,
46    pub border_width: f32,
47    pub cell_padding: f32,
48}
49
50pub struct CellLayout {
51    pub row: usize,
52    pub column: usize,
53    pub blocks: Vec<BlockLayout>,
54    pub background_color: Option<[f32; 4]>,
55}
56
57/// Lay out a table: distribute column widths, lay out cell contents, compute row heights.
58pub fn layout_table(
59    registry: &FontRegistry,
60    params: &TableLayoutParams,
61    available_width: f32,
62    scale_factor: f32,
63) -> TableLayout {
64    let cols = params.columns.max(1);
65    let rows = params.rows.max(1);
66    let border = params.border_width;
67    let padding = params.cell_padding;
68    let spacing = params.cell_spacing;
69
70    // Total overhead: borders + spacing + padding for all columns
71    let total_overhead =
72        border * 2.0 + spacing * (cols as f32 - 1.0).max(0.0) + padding * 2.0 * cols as f32;
73    let content_area = (available_width - total_overhead).max(0.0);
74
75    // Compute column widths
76    let column_content_widths =
77        compute_column_widths(&params.column_widths, cols, content_area, padding);
78
79    // Compute column x positions
80    let mut column_xs = Vec::with_capacity(cols);
81    let mut x = border;
82    for (c, &col_w) in column_content_widths.iter().enumerate() {
83        column_xs.push(x + padding);
84        x += padding * 2.0 + col_w;
85        if c < cols - 1 {
86            x += spacing;
87        }
88    }
89    let total_width = x + border;
90
91    // Lay out each cell's blocks
92    let mut cell_layouts = Vec::new();
93    // Track row heights (max cell content height per row)
94    let mut row_heights = vec![0.0f32; rows];
95
96    for cell_params in &params.cells {
97        let r = cell_params.row;
98        let c = cell_params.column;
99        if c >= cols || r >= rows {
100            continue;
101        }
102
103        let cell_width = column_content_widths[c];
104        let mut cell_blocks = Vec::new();
105        let mut cell_height = 0.0f32;
106
107        for block_params in &cell_params.blocks {
108            let block = layout_block(registry, block_params, cell_width, scale_factor);
109            cell_height += block.height;
110            cell_blocks.push(block);
111        }
112
113        // Position blocks within the cell vertically
114        let mut block_y = 0.0f32;
115        for block in &mut cell_blocks {
116            block.y = block_y;
117            block_y += block.height;
118        }
119
120        row_heights[r] = row_heights[r].max(cell_height);
121
122        cell_layouts.push(CellLayout {
123            row: r,
124            column: c,
125            blocks: cell_blocks,
126            background_color: cell_params.background_color,
127        });
128    }
129
130    // Compute row y positions
131    let mut row_ys = Vec::with_capacity(rows);
132    let mut y = border;
133    for (r, &row_h) in row_heights.iter().enumerate() {
134        row_ys.push(y + padding);
135        y += padding * 2.0 + row_h;
136        if r < rows - 1 {
137            y += spacing;
138        }
139    }
140    let total_height = y + border;
141
142    TableLayout {
143        table_id: params.table_id,
144        y: 0.0, // set by flow
145        total_height,
146        total_width,
147        column_xs,
148        column_content_widths,
149        row_ys,
150        row_heights,
151        cell_layouts,
152        border_width: border,
153        cell_padding: padding,
154    }
155}
156
157fn compute_column_widths(
158    specified: &[f32],
159    cols: usize,
160    content_area: f32,
161    _padding: f32,
162) -> Vec<f32> {
163    // When no explicit widths, distribute content_area evenly.
164    // Clamp to a reasonable range: at least 1px (zero-width guard) and
165    // at most a sensible default when the viewport is unbounded.
166    let default_col_width = if content_area.is_finite() {
167        (content_area / cols as f32).max(1.0)
168    } else {
169        200.0
170    };
171
172    if specified.is_empty() || specified.len() != cols {
173        return vec![default_col_width; cols];
174    }
175
176    // Use specified proportions
177    let total: f32 = specified.iter().sum();
178    if total <= 0.0 {
179        return vec![default_col_width; cols];
180    }
181
182    specified
183        .iter()
184        .map(|&s| (s / total) * content_area)
185        .collect()
186}
187
188/// Generate border and background decoration rects for a table.
189pub fn generate_table_decorations(table: &TableLayout, scroll_offset: f32) -> Vec<DecorationRect> {
190    let mut decorations = Vec::new();
191    let table_y = table.y - scroll_offset;
192
193    // Outer table border
194    if table.border_width > 0.0 {
195        let bw = table.border_width;
196        let color = [0.6, 0.6, 0.6, 1.0]; // gray border
197        // Top
198        decorations.push(DecorationRect {
199            rect: [0.0, table_y, table.total_width, bw],
200            color,
201            kind: DecorationKind::TableBorder,
202        });
203        // Bottom
204        decorations.push(DecorationRect {
205            rect: [
206                0.0,
207                table_y + table.total_height - bw,
208                table.total_width,
209                bw,
210            ],
211            color,
212            kind: DecorationKind::TableBorder,
213        });
214        // Left
215        decorations.push(DecorationRect {
216            rect: [0.0, table_y, bw, table.total_height],
217            color,
218            kind: DecorationKind::TableBorder,
219        });
220        // Right
221        decorations.push(DecorationRect {
222            rect: [table.total_width - bw, table_y, bw, table.total_height],
223            color,
224            kind: DecorationKind::TableBorder,
225        });
226
227        // Row borders
228        for r in 1..table.row_ys.len() {
229            let row_y = table.row_ys[r] - table.cell_padding;
230            decorations.push(DecorationRect {
231                rect: [0.0, table_y + row_y - bw / 2.0, table.total_width, bw],
232                color,
233                kind: DecorationKind::TableBorder,
234            });
235        }
236
237        // Column borders
238        for c in 1..table.column_xs.len() {
239            let col_x = table.column_xs[c] - table.cell_padding;
240            decorations.push(DecorationRect {
241                rect: [col_x - bw / 2.0, table_y, bw, table.total_height],
242                color,
243                kind: DecorationKind::TableBorder,
244            });
245        }
246    }
247
248    // Cell backgrounds
249    for cell in &table.cell_layouts {
250        if let Some(bg_color) = cell.background_color
251            && cell.row < table.row_ys.len()
252            && cell.column < table.column_xs.len()
253        {
254            let cx = table.column_xs[cell.column] - table.cell_padding;
255            let cy = table.row_ys[cell.row] - table.cell_padding;
256            let cw = table.column_content_widths[cell.column] + table.cell_padding * 2.0;
257            let ch = table.row_heights[cell.row] + table.cell_padding * 2.0;
258            decorations.push(DecorationRect {
259                rect: [cx, table_y + cy, cw, ch],
260                color: bg_color,
261                kind: DecorationKind::TableCellBackground,
262            });
263        }
264    }
265
266    decorations
267}