Skip to main content

lutra_bin/table/
layout.rs

1//! Layout computation for table rendering (pass 1).
2
3use bytes::Buf;
4
5use crate::ArrayReader;
6use crate::ir;
7use crate::tabular::TableCell;
8
9use super::format::{format_ty_name, format_value, truncate};
10use super::{Config, Table};
11
12/// Computed layout for table rendering.
13#[derive(Debug, Clone)]
14pub struct Layout {
15    /// Height of the names section of the header (column names)
16    pub names_height: usize,
17    /// Hierarchical column groups (for multi-row headers).
18    pub column_groups: Vec<ColumnGroup>,
19    /// Flat list of leaf columns.
20    pub columns: Vec<Column>,
21
22    /// Width for the index column.
23    pub col_index_width: usize,
24    /// Width for each leaf column.
25    pub col_widths: Vec<usize>,
26    /// Visual height for each logical row (due to array expansion).
27    pub row_heights: Vec<usize>,
28    /// Number of table rows
29    pub total_rows: usize,
30}
31
32/// Leaf column with type info (for formatting/alignment).
33#[derive(Debug, Clone)]
34pub struct Column {
35    /// Column name.
36    pub name: String,
37    /// Type display string for header.
38    pub ty_name: String,
39    /// Alignment for data in this column.
40    pub align: Align,
41}
42
43/// Cell alignment.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum Align {
46    Left,
47    Right,
48}
49
50/// Hierarchical column group (for multi-row headers with nested columns).
51#[derive(Debug, Clone)]
52pub struct ColumnGroup {
53    /// Column group name (field name or positional index).
54    pub name: String,
55    /// Nested column groups (empty for leaf columns).
56    pub children: Vec<ColumnGroup>,
57}
58
59impl ColumnGroup {
60    /// Count of leaf columns under this column group.
61    pub fn leaf_count(&self) -> usize {
62        if self.children.is_empty() {
63            1
64        } else {
65            self.children.iter().map(|c| c.leaf_count()).sum()
66        }
67    }
68
69    /// Maximum depth of the column hierarchy.
70    pub fn depth(&self) -> usize {
71        if self.children.is_empty() {
72            1
73        } else {
74            1 + self.children.iter().map(|c| c.depth()).max().unwrap_or(0)
75        }
76    }
77}
78
79impl<'d, 't> Table<'d, 't> {
80    /// Compute layout by sampling rows from the tabular iterator.
81    pub fn compute_layout(mut self, config: &Config) -> Layout {
82        let column_groups = self.build_column_groups(self.row_ty());
83        let columns = self.flatten_columns(&column_groups, self.row_ty());
84
85        let names_height = max_column_depth(&column_groups);
86
87        // Compute row index width based on total rows
88        let total_rows = self.remaining();
89        let col_index_width = total_rows.saturating_sub(1).to_string().len();
90
91        // Initialize widths from headers
92        let mut col_widths: Vec<usize> = columns
93            .iter()
94            .map(|c| {
95                let name_width = c.name.chars().count();
96                let ty_width = c.ty_name.chars().count();
97                name_width.max(ty_width)
98            })
99            .collect();
100
101        let mut row_heights = Vec::new();
102        let mut rows_scanned = 0;
103
104        while let Some(row) = self.next() {
105            let mut row_height = 1usize;
106            for (i, cell) in row.iter().enumerate() {
107                let (width, height) = self.measure_cell(cell, config);
108                if i < col_widths.len() {
109                    col_widths[i] = col_widths[i].max(width);
110                }
111                row_height = row_height.max(height);
112            }
113            row_heights.push(row_height);
114
115            rows_scanned += 1;
116            if let Some(l) = config.sample_rows
117                && rows_scanned >= l
118            {
119                break;
120            }
121        }
122
123        Layout {
124            names_height,
125            column_groups,
126            columns,
127            col_widths,
128            col_index_width,
129            row_heights,
130            total_rows,
131        }
132    }
133
134    fn build_column_groups(&self, ty: &'t ir::Ty) -> Vec<ColumnGroup> {
135        let ty = self.get_ty_mat(ty);
136        match &ty.kind {
137            ir::TyKind::Tuple(fields) => fields
138                .iter()
139                .enumerate()
140                .map(|(i, f)| {
141                    let name = f.name.clone().unwrap_or_else(|| i.to_string());
142                    let children = self.build_column_groups(&f.ty);
143                    ColumnGroup { name, children }
144                })
145                .collect(),
146            _ => vec![],
147        }
148    }
149
150    fn flatten_columns(&self, groups: &[ColumnGroup], ty: &'t ir::Ty) -> Vec<Column> {
151        let ty = self.get_ty_mat(ty);
152        match &ty.kind {
153            ir::TyKind::Tuple(fields) => {
154                let mut leaves = Vec::new();
155                for (group, field) in groups.iter().zip(fields.iter()) {
156                    if group.children.is_empty() {
157                        leaves.push(Column {
158                            name: group.name.clone(),
159                            ty_name: format_ty_name(&field.ty, self),
160                            align: self.infer_align(&field.ty),
161                        });
162                    } else {
163                        leaves.extend(self.flatten_columns(&group.children, &field.ty));
164                    }
165                }
166                leaves
167            }
168            _ => {
169                // Single column for non-tuple root (primitive, array, enum)
170                vec![Column {
171                    name: "value".into(),
172                    ty_name: format_ty_name(ty, self),
173                    align: self.infer_align(ty),
174                }]
175            }
176        }
177    }
178
179    /// Infer alignment from type.
180    fn infer_align(&self, ty: &ir::Ty) -> Align {
181        let ty = self.get_ty_mat(ty);
182        match &ty.kind {
183            ir::TyKind::Primitive(p) => match p {
184                ir::TyPrimitive::bool | ir::TyPrimitive::text => Align::Left,
185                _ => Align::Right, // All numeric types
186            },
187            ir::TyKind::Enum(_) => Align::Left,
188            ir::TyKind::Array(_) => Align::Left,
189            ir::TyKind::Tuple(_) => Align::Left,
190            ir::TyKind::Function(_) => Align::Left,
191            ir::TyKind::Ident(_) => unreachable!("should be resolved"),
192        }
193    }
194
195    /// Measure a cell's width and height.
196    fn measure_cell(&self, cell: &TableCell, config: &Config) -> (usize, usize) {
197        let ty = self.get_ty_mat(cell.ty());
198
199        match &ty.kind {
200            ir::TyKind::Array(item_ty) if self.is_flat(item_ty) => {
201                // Expandable array: measure items
202                let reader = ArrayReader::new_for_ty(cell.data(), ty);
203                let count = reader.remaining();
204
205                let height = if count == 0 {
206                    1
207                } else if count <= config.max_array_items {
208                    count
209                } else {
210                    config.max_array_items // (max-1) items + "… X more" row
211                };
212
213                // Measure width of items (sample first few)
214                let mut max_width = 0usize;
215                let items_to_measure = count.min(config.max_array_items);
216                for item_data in reader.take(items_to_measure) {
217                    if let Ok((text, _)) = format_value(item_data.chunk(), item_ty, self) {
218                        let text = truncate(&text, config.max_col_width);
219                        max_width = max_width.max(text.chars().count());
220                    }
221                }
222
223                // Also consider the "… N more" text width
224                if count > config.max_array_items {
225                    let ellipsis_text = format!("… {} more", count - (config.max_array_items - 1));
226                    max_width = max_width.max(ellipsis_text.chars().count());
227                }
228
229                (max_width.min(config.max_col_width), height)
230            }
231            ir::TyKind::Array(_) => {
232                // Non-flat array: "[…]"
233                (3, 1)
234            }
235            _ => {
236                // Primitive or enum: format and measure
237                if let Ok((text, _)) = format_value(cell.data(), cell.ty(), self) {
238                    let text = truncate(&text, config.max_col_width);
239                    (text.chars().count(), 1)
240                } else {
241                    (3, 1) // fallback for errors
242                }
243            }
244        }
245    }
246}
247
248fn max_column_depth(column_groups: &[ColumnGroup]) -> usize {
249    column_groups.iter().map(|c| c.depth()).max().unwrap_or(1)
250}