fast_rich/
table.rs

1//! Tables for displaying structured data.
2//!
3//! Tables support headers, multiple columns with alignment and width control,
4//! and various border styles.
5
6use crate::box_drawing::Line;
7use crate::console::RenderContext;
8use crate::panel::BorderStyle;
9use crate::renderable::{Renderable, Segment};
10use crate::style::Style;
11use crate::text::{Span, Text};
12use unicode_width::UnicodeWidthStr;
13
14/// Column alignment.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub enum ColumnAlign {
17    #[default]
18    Left,
19    Center,
20    Right,
21}
22
23/// Column width specification.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
25pub enum ColumnWidth {
26    /// Automatic width based on content
27    #[default]
28    Auto,
29    /// Fixed width
30    Fixed(usize),
31    /// Min width
32    Min(usize),
33    /// Max width
34    Max(usize),
35}
36
37/// A table column definition.
38#[derive(Debug, Clone)]
39pub struct Column {
40    /// Column header
41    pub header: String,
42    /// Column alignment
43    pub align: ColumnAlign,
44    /// Column width
45    pub width: ColumnWidth,
46    /// Header style
47    pub header_style: Style,
48    /// Cell style
49    pub style: Style,
50    /// Whether to wrap content
51    pub wrap: bool,
52    /// Minimum width (computed, reserved for future use)
53    #[allow(dead_code)]
54    min_width: usize,
55    /// Maximum width (computed, reserved for future use)
56    #[allow(dead_code)]
57    max_width: usize,
58}
59
60impl Column {
61    /// Create a new column with a header.
62    pub fn new(header: &str) -> Self {
63        let header_width = UnicodeWidthStr::width(header);
64        Column {
65            header: header.to_string(),
66            align: ColumnAlign::Left,
67            width: ColumnWidth::Auto,
68            header_style: Style::new().bold(),
69            style: Style::new(),
70            wrap: true,
71            min_width: header_width,
72            max_width: header_width,
73        }
74    }
75
76    /// Set the column alignment.
77    pub fn align(mut self, align: ColumnAlign) -> Self {
78        self.align = align;
79        self
80    }
81
82    /// Set the column width.
83    pub fn width(mut self, width: ColumnWidth) -> Self {
84        self.width = width;
85        self
86    }
87
88    /// Set the header style.
89    pub fn header_style(mut self, style: Style) -> Self {
90        self.header_style = style;
91        self
92    }
93
94    /// Set the cell style.
95    pub fn style(mut self, style: Style) -> Self {
96        self.style = style;
97        self
98    }
99
100    /// Set whether to wrap content.
101    pub fn wrap(mut self, wrap: bool) -> Self {
102        self.wrap = wrap;
103        self
104    }
105
106    /// Center align shorthand.
107    pub fn center(self) -> Self {
108        self.align(ColumnAlign::Center)
109    }
110
111    /// Right align shorthand.
112    pub fn right(self) -> Self {
113        self.align(ColumnAlign::Right)
114    }
115}
116
117/// A row of table cells.
118#[derive(Debug, Clone)]
119pub struct Row {
120    cells: Vec<Text>,
121    style: Option<Style>,
122}
123
124impl Row {
125    /// Create a new row with cells.
126    pub fn new<I, T>(cells: I) -> Self
127    where
128        I: IntoIterator<Item = T>,
129        T: Into<Text>,
130    {
131        Row {
132            cells: cells.into_iter().map(Into::into).collect(),
133            style: None,
134        }
135    }
136
137    /// Set a style for the entire row.
138    pub fn style(mut self, style: Style) -> Self {
139        self.style = Some(style);
140        self
141    }
142}
143
144// Removed TableBorderChars struct and implementation
145
146/// A table for displaying structured data.
147#[derive(Debug, Clone)]
148pub struct Table {
149    /// Column definitions
150    columns: Vec<Column>,
151    /// Data rows
152    rows: Vec<Row>,
153    /// Border style
154    border_style: BorderStyle,
155    /// Border style (colors etc)
156    style: Style,
157    /// Show header row
158    show_header: bool,
159    /// Show border
160    show_border: bool,
161    /// Show row separators
162    show_row_lines: bool,
163    /// Padding in cells
164    padding: usize,
165    /// Title
166    title: Option<String>,
167    /// Expand to full width
168    expand: bool,
169}
170
171impl Default for Table {
172    fn default() -> Self {
173        Self::new()
174    }
175}
176
177impl Table {
178    /// Create a new empty table.
179    pub fn new() -> Self {
180        Table {
181            columns: Vec::new(),
182            rows: Vec::new(),
183            border_style: BorderStyle::Rounded,
184            style: Style::new(),
185            show_header: true,
186            show_border: true,
187            show_row_lines: false,
188            padding: 1,
189            title: None,
190            expand: false,
191        }
192    }
193
194    /// Add a column to the table.
195    pub fn add_column<C: Into<Column>>(&mut self, column: C) -> &mut Self {
196        self.columns.push(column.into());
197        self
198    }
199
200    /// Add a column by header name.
201    pub fn column(mut self, header: &str) -> Self {
202        self.columns.push(Column::new(header));
203        self
204    }
205
206    /// Add multiple columns by header names.
207    pub fn columns<I, S>(mut self, headers: I) -> Self
208    where
209        I: IntoIterator<Item = S>,
210        S: AsRef<str>,
211    {
212        for header in headers {
213            self.columns.push(Column::new(header.as_ref()));
214        }
215        self
216    }
217
218    /// Add a row to the table.
219    pub fn add_row<I, T>(&mut self, cells: I) -> &mut Self
220    where
221        I: IntoIterator<Item = T>,
222        T: Into<Text>,
223    {
224        self.rows.push(Row::new(cells));
225        self
226    }
227
228    /// Add a row from string slices (convenience method).
229    pub fn add_row_strs(&mut self, cells: &[&str]) -> &mut Self {
230        let text_cells: Vec<Text> = cells.iter().map(|s| Text::plain(s.to_string())).collect();
231        self.rows.push(Row {
232            cells: text_cells,
233            style: None,
234        });
235        self
236    }
237
238    /// Add a Row object to the table.
239    pub fn add_row_obj(&mut self, row: Row) -> &mut Self {
240        self.rows.push(row);
241        self
242    }
243
244    /// Set the border style.
245    pub fn border_style(mut self, style: BorderStyle) -> Self {
246        self.border_style = style;
247        self
248    }
249
250    /// Set the table style (border colors).
251    pub fn style(mut self, style: Style) -> Self {
252        self.style = style;
253        self
254    }
255
256    /// Set the table title.
257    pub fn set_title(mut self, title: &str) -> Self {
258        self.title = Some(title.to_string());
259        self
260    }
261
262    /// Set whether to show the header.
263    pub fn show_header(mut self, show: bool) -> Self {
264        self.show_header = show;
265        self
266    }
267
268    /// Set whether to show the border.
269    pub fn show_border(mut self, show: bool) -> Self {
270        self.show_border = show;
271        self
272    }
273
274    /// Set whether to show row separator lines.
275    pub fn show_row_lines(mut self, show: bool) -> Self {
276        self.show_row_lines = show;
277        self
278    }
279
280    /// Set cell padding.
281    pub fn padding(mut self, padding: usize) -> Self {
282        self.padding = padding;
283        self
284    }
285
286    /// Set the table title.
287    pub fn title(mut self, title: &str) -> Self {
288        self.title = Some(title.to_string());
289        self
290    }
291
292    /// Set whether to expand to full width.
293    pub fn expand(mut self, expand: bool) -> Self {
294        self.expand = expand;
295        self
296    }
297
298    /// Calculate column widths based on content.
299    fn calculate_widths(&self, available_width: usize) -> Vec<usize> {
300        let num_cols = self.columns.len();
301        if num_cols == 0 {
302            return vec![];
303        }
304
305        // Calculate content widths
306        let mut max_widths: Vec<usize> = self
307            .columns
308            .iter()
309            .map(|c| UnicodeWidthStr::width(c.header.as_str()))
310            .collect();
311
312        for row in &self.rows {
313            for (i, cell) in row.cells.iter().enumerate() {
314                if i < max_widths.len() {
315                    max_widths[i] = max_widths[i].max(cell.width());
316                }
317            }
318        }
319
320        // Calculate overhead (borders, padding)
321        let overhead = if self.show_border {
322            1 + num_cols + 1 + (self.padding * 2 * num_cols)
323        } else {
324            (num_cols - 1) + (self.padding * 2 * num_cols)
325        };
326
327        let content_width = available_width.saturating_sub(overhead);
328
329        // Simple proportional distribution
330        let total_content: usize = max_widths.iter().sum();
331        if total_content == 0 {
332            return vec![content_width / num_cols.max(1); num_cols];
333        }
334
335        if total_content <= content_width {
336            // Everything fits
337            if self.expand {
338                // Distribute extra space
339                let extra = content_width - total_content;
340                let per_col = extra / num_cols;
341                max_widths.iter().map(|w| w + per_col).collect()
342            } else {
343                max_widths
344            }
345        } else {
346            // Need to shrink - proportional distribution
347            max_widths
348                .iter()
349                .map(|w| {
350                    let ratio = *w as f64 / total_content as f64;
351                    ((content_width as f64 * ratio) as usize).max(1)
352                })
353                .collect()
354        }
355    }
356
357    fn render_horizontal_line(&self, widths: &[usize], line: &Line) -> Segment {
358        let mut spans = vec![Span::styled(line.left.to_string(), self.style)];
359
360        for (i, &width) in widths.iter().enumerate() {
361            let cell_width = width + self.padding * 2;
362            spans.push(Span::styled(
363                line.mid.to_string().repeat(cell_width),
364                self.style,
365            ));
366            if i < widths.len() - 1 {
367                spans.push(Span::styled(line.cross.to_string(), self.style));
368            }
369        }
370
371        spans.push(Span::styled(line.right.to_string(), self.style));
372        Segment::line(spans)
373    }
374
375    fn render_row(
376        &self,
377        cells: &[Text],
378        widths: &[usize],
379        line: &Line,
380        cell_styles: &[Style],
381    ) -> Vec<Segment> {
382        // For simplicity, render single-line rows
383        // A full implementation would handle wrapping
384        let mut spans = Vec::new();
385
386        if self.show_border {
387            spans.push(Span::styled(line.left.to_string(), self.style));
388        }
389
390        for (i, width) in widths.iter().enumerate() {
391            let cell = cells.get(i);
392            let content = cell.map(|c| c.plain_text()).unwrap_or_default();
393            let _content_width = UnicodeWidthStr::width(content.as_str());
394            let cell_style = cell_styles.get(i).copied().unwrap_or_default();
395
396            let align = self.columns.get(i).map(|c| c.align).unwrap_or_default();
397            let padded = pad_string(&content, *width, align);
398
399            // Add padding
400            spans.push(Span::raw(" ".repeat(self.padding)));
401            spans.push(Span::styled(padded, cell_style));
402            spans.push(Span::raw(" ".repeat(self.padding)));
403
404            if i < widths.len() - 1 {
405                spans.push(Span::styled(line.cross.to_string(), self.style));
406            } else if self.show_border {
407                spans.push(Span::styled(line.right.to_string(), self.style));
408            }
409        }
410
411        vec![Segment::line(spans)]
412    }
413}
414
415fn pad_string(s: &str, width: usize, align: ColumnAlign) -> String {
416    let content_width = UnicodeWidthStr::width(s);
417    if content_width >= width {
418        return truncate_string(s, width);
419    }
420
421    let padding = width - content_width;
422    match align {
423        ColumnAlign::Left => format!("{}{}", s, " ".repeat(padding)),
424        ColumnAlign::Right => format!("{}{}", " ".repeat(padding), s),
425        ColumnAlign::Center => {
426            let left = padding / 2;
427            let right = padding - left;
428            format!("{}{}{}", " ".repeat(left), s, " ".repeat(right))
429        }
430    }
431}
432
433fn truncate_string(s: &str, width: usize) -> String {
434    use unicode_segmentation::UnicodeSegmentation;
435
436    let mut result = String::new();
437    let mut current_width = 0;
438
439    for grapheme in s.graphemes(true) {
440        let grapheme_width = UnicodeWidthStr::width(grapheme);
441        if current_width + grapheme_width > width {
442            if width > 1 && current_width < width {
443                result.push('…');
444            }
445            break;
446        }
447        result.push_str(grapheme);
448        current_width += grapheme_width;
449    }
450
451    // Pad if shorter
452    while current_width < width {
453        result.push(' ');
454        current_width += 1;
455    }
456
457    result
458}
459
460impl From<&str> for Column {
461    fn from(s: &str) -> Self {
462        Column::new(s)
463    }
464}
465
466impl From<String> for Column {
467    fn from(s: String) -> Self {
468        Column::new(&s)
469    }
470}
471
472impl Renderable for Table {
473    fn render(&self, context: &RenderContext) -> Vec<Segment> {
474        if self.columns.is_empty() {
475            return vec![];
476        }
477
478        let box_chars = self.border_style.to_box();
479        let widths = self.calculate_widths(context.width);
480        let mut segments = Vec::new();
481
482        // Calculate total table width for title centering
483        let content_width: usize = widths.iter().map(|w| w + self.padding * 2).sum();
484        let border_overhead = if self.show_border {
485            widths.len() + 1
486        } else {
487            widths.len() - 1
488        };
489        let table_width = content_width + border_overhead;
490
491        // Title
492        if let Some(title) = &self.title {
493            let title_width = UnicodeWidthStr::width(title.as_str());
494            if title_width <= table_width {
495                let padding = table_width - title_width;
496                let left_pad = padding / 2;
497                let right_pad = padding - left_pad;
498
499                let mut spans = Vec::new();
500                if left_pad > 0 {
501                    spans.push(Span::raw(" ".repeat(left_pad)));
502                }
503                spans.push(Span::styled(title.clone(), Style::new().bold()));
504                if right_pad > 0 {
505                    spans.push(Span::raw(" ".repeat(right_pad)));
506                }
507                segments.push(Segment::line(spans));
508            } else {
509                // Truncate or just print? Just print for now.
510                segments.push(Segment::line(vec![Span::styled(
511                    title.clone(),
512                    Style::new().bold(),
513                )]));
514            }
515        }
516
517        // Top border
518        if self.show_border {
519            segments.push(self.render_horizontal_line(&widths, &box_chars.top));
520        }
521
522        // Header row
523        if self.show_header {
524            let header_cells: Vec<Text> = self
525                .columns
526                .iter()
527                .map(|c| Text::styled(c.header.clone(), c.header_style))
528                .collect();
529            let header_styles: Vec<Style> = self.columns.iter().map(|c| c.header_style).collect();
530            // Use header box line for vertical separators in header
531            segments.extend(self.render_row(
532                &header_cells,
533                &widths,
534                &box_chars.header,
535                &header_styles,
536            ));
537
538            // Header separator
539            if self.show_border || self.show_row_lines {
540                segments.push(self.render_horizontal_line(&widths, &box_chars.head));
541            }
542        }
543
544        // Data rows
545        for (row_idx, row) in self.rows.iter().enumerate() {
546            let cell_styles: Vec<Style> = self.columns.iter().map(|c| c.style).collect();
547            // Use cell box line for vertical separators in body
548            segments.extend(self.render_row(&row.cells, &widths, &box_chars.cell, &cell_styles));
549
550            // Row separator
551            if self.show_row_lines && row_idx < self.rows.len() - 1 {
552                segments.push(self.render_horizontal_line(&widths, &box_chars.mid));
553            }
554        }
555
556        // Bottom border
557        if self.show_border {
558            segments.push(self.render_horizontal_line(&widths, &box_chars.bottom));
559        }
560
561        segments
562    }
563}
564
565#[cfg(test)]
566mod tests {
567    use super::*;
568
569    #[test]
570    fn test_table_basic() {
571        let mut table = Table::new();
572        table.add_column("Name");
573        table.add_column("Age");
574        table.add_row_strs(&["Alice", "30"]);
575        table.add_row_strs(&["Bob", "25"]);
576
577        let context = RenderContext {
578            width: 40,
579            height: None,
580        };
581        let segments = table.render(&context);
582
583        assert!(!segments.is_empty());
584
585        // Check that output contains our data
586        let text: String = segments.iter().map(|s| s.plain_text()).collect();
587        assert!(text.contains("Name"));
588        assert!(text.contains("Alice"));
589        assert!(text.contains("Bob"));
590    }
591
592    #[test]
593    fn test_table_builder() {
594        let table = Table::new()
595            .columns(["A", "B", "C"])
596            .border_style(BorderStyle::Square);
597
598        assert_eq!(table.columns.len(), 3);
599    }
600
601    #[test]
602    fn test_pad_string() {
603        assert_eq!(pad_string("hi", 5, ColumnAlign::Left), "hi   ");
604        assert_eq!(pad_string("hi", 5, ColumnAlign::Right), "   hi");
605        assert_eq!(pad_string("hi", 5, ColumnAlign::Center), " hi  ");
606    }
607}