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    font_scale: f32,
64) -> TableLayout {
65    let cols = params.columns.max(1);
66    let rows = params.rows.max(1);
67    let border = params.border_width;
68    let padding = params.cell_padding;
69    let spacing = params.cell_spacing;
70
71    // Total overhead: borders + spacing + padding for all columns
72    let total_overhead =
73        border * 2.0 + spacing * (cols as f32 - 1.0).max(0.0) + padding * 2.0 * cols as f32;
74    let content_area = (available_width - total_overhead).max(0.0);
75
76    // Compute column widths
77    let column_content_widths =
78        compute_column_widths(&params.column_widths, cols, content_area, padding);
79
80    // Compute column x positions
81    let mut column_xs = Vec::with_capacity(cols);
82    let mut x = border;
83    for (c, &col_w) in column_content_widths.iter().enumerate() {
84        column_xs.push(x + padding);
85        x += padding * 2.0 + col_w;
86        if c < cols - 1 {
87            x += spacing;
88        }
89    }
90    let total_width = x + border;
91
92    // Lay out each cell's blocks
93    let mut cell_layouts = Vec::new();
94    // Track row heights (max cell content height per row)
95    let mut row_heights = vec![0.0f32; rows];
96
97    for cell_params in &params.cells {
98        let r = cell_params.row;
99        let c = cell_params.column;
100        if c >= cols || r >= rows {
101            continue;
102        }
103
104        let cell_width = column_content_widths[c];
105        let mut cell_blocks = Vec::new();
106        let mut cell_height = 0.0f32;
107
108        for block_params in &cell_params.blocks {
109            let block = layout_block(registry, block_params, cell_width, scale_factor, font_scale);
110            cell_height += block.height;
111            cell_blocks.push(block);
112        }
113
114        // Position blocks within the cell vertically
115        let mut block_y = 0.0f32;
116        for block in &mut cell_blocks {
117            block.y = block_y;
118            block_y += block.height;
119        }
120
121        row_heights[r] = row_heights[r].max(cell_height);
122
123        cell_layouts.push(CellLayout {
124            row: r,
125            column: c,
126            blocks: cell_blocks,
127            background_color: cell_params.background_color,
128        });
129    }
130
131    // Compute row y positions
132    let mut row_ys = Vec::with_capacity(rows);
133    let mut y = border;
134    for (r, &row_h) in row_heights.iter().enumerate() {
135        row_ys.push(y + padding);
136        y += padding * 2.0 + row_h;
137        if r < rows - 1 {
138            y += spacing;
139        }
140    }
141    let total_height = y + border;
142
143    TableLayout {
144        table_id: params.table_id,
145        y: 0.0, // set by flow
146        total_height,
147        total_width,
148        column_xs,
149        column_content_widths,
150        row_ys,
151        row_heights,
152        cell_layouts,
153        border_width: border,
154        cell_padding: padding,
155    }
156}
157
158fn compute_column_widths(
159    specified: &[f32],
160    cols: usize,
161    content_area: f32,
162    _padding: f32,
163) -> Vec<f32> {
164    // When no explicit widths, distribute content_area evenly.
165    // Clamp to a reasonable range: at least 1px (zero-width guard) and
166    // at most a sensible default when the viewport is unbounded.
167    let default_col_width = if content_area.is_finite() {
168        (content_area / cols as f32).max(1.0)
169    } else {
170        200.0
171    };
172
173    if specified.is_empty() || specified.len() != cols {
174        return vec![default_col_width; cols];
175    }
176
177    // Use specified proportions
178    let total: f32 = specified.iter().sum();
179    if total <= 0.0 {
180        return vec![default_col_width; cols];
181    }
182
183    specified
184        .iter()
185        .map(|&s| (s / total) * content_area)
186        .collect()
187}
188
189/// Generate border and background decoration rects for a top-level table.
190pub fn generate_table_decorations(table: &TableLayout, scroll_offset: f32) -> Vec<DecorationRect> {
191    generate_table_decorations_at(table, 0.0, 0.0, scroll_offset)
192}
193
194/// Generate border and background decoration rects for a table whose
195/// origin is shifted by `(offset_x, offset_y)` — a table nested inside a
196/// frame (e.g. a blockquote), where the offsets are the frame's content
197/// origin.
198pub fn generate_table_decorations_at(
199    table: &TableLayout,
200    offset_x: f32,
201    offset_y: f32,
202    scroll_offset: f32,
203) -> Vec<DecorationRect> {
204    let mut decorations = Vec::new();
205    let table_y = offset_y + table.y - scroll_offset;
206
207    // Outer table border
208    if table.border_width > 0.0 {
209        let bw = table.border_width;
210        let color = [0.6, 0.6, 0.6, 1.0]; // gray border
211        // Top
212        decorations.push(DecorationRect {
213            rect: [offset_x, table_y, table.total_width, bw],
214            color,
215            kind: DecorationKind::TableBorder,
216        });
217        // Bottom
218        decorations.push(DecorationRect {
219            rect: [
220                offset_x,
221                table_y + table.total_height - bw,
222                table.total_width,
223                bw,
224            ],
225            color,
226            kind: DecorationKind::TableBorder,
227        });
228        // Left
229        decorations.push(DecorationRect {
230            rect: [offset_x, table_y, bw, table.total_height],
231            color,
232            kind: DecorationKind::TableBorder,
233        });
234        // Right
235        decorations.push(DecorationRect {
236            rect: [
237                offset_x + table.total_width - bw,
238                table_y,
239                bw,
240                table.total_height,
241            ],
242            color,
243            kind: DecorationKind::TableBorder,
244        });
245
246        // Row borders
247        for r in 1..table.row_ys.len() {
248            let row_y = table.row_ys[r] - table.cell_padding;
249            decorations.push(DecorationRect {
250                rect: [offset_x, table_y + row_y - bw / 2.0, table.total_width, bw],
251                color,
252                kind: DecorationKind::TableBorder,
253            });
254        }
255
256        // Column borders
257        for c in 1..table.column_xs.len() {
258            let col_x = table.column_xs[c] - table.cell_padding;
259            decorations.push(DecorationRect {
260                rect: [offset_x + col_x - bw / 2.0, table_y, bw, table.total_height],
261                color,
262                kind: DecorationKind::TableBorder,
263            });
264        }
265    }
266
267    // Cell backgrounds
268    for cell in &table.cell_layouts {
269        if let Some(bg_color) = cell.background_color
270            && cell.row < table.row_ys.len()
271            && cell.column < table.column_xs.len()
272        {
273            let cx = table.column_xs[cell.column] - table.cell_padding;
274            let cy = table.row_ys[cell.row] - table.cell_padding;
275            let cw = table.column_content_widths[cell.column] + table.cell_padding * 2.0;
276            let ch = table.row_heights[cell.row] + table.cell_padding * 2.0;
277            decorations.push(DecorationRect {
278                rect: [offset_x + cx, table_y + cy, cw, ch],
279                color: bg_color,
280                kind: DecorationKind::TableCellBackground,
281            });
282        }
283    }
284
285    decorations
286}