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::console::RenderContext;
7use crate::panel::BorderStyle;
8use crate::renderable::{Renderable, Segment};
9use crate::style::Style;
10use crate::text::{Span, Text};
11use crate::box_drawing::Line;
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(
358        &self,
359        widths: &[usize],
360        line: &Line,
361    ) -> Segment {
362        let mut spans = vec![Span::styled(line.left.to_string(), self.style)];
363
364        for (i, &width) in widths.iter().enumerate() {
365            let cell_width = width + self.padding * 2;
366            spans.push(Span::styled(
367                line.mid.to_string().repeat(cell_width),
368                self.style,
369            ));
370            if i < widths.len() - 1 {
371                spans.push(Span::styled(line.cross.to_string(), self.style));
372            }
373        }
374
375        spans.push(Span::styled(line.right.to_string(), self.style));
376        Segment::line(spans)
377    }
378
379    fn render_row(
380        &self,
381        cells: &[Text],
382        widths: &[usize],
383        line: &Line,
384        cell_styles: &[Style],
385    ) -> Vec<Segment> {
386        // For simplicity, render single-line rows
387        // A full implementation would handle wrapping
388        let mut spans = Vec::new();
389
390        if self.show_border {
391            spans.push(Span::styled(line.left.to_string(), self.style));
392        }
393
394        for (i, width) in widths.iter().enumerate() {
395            let cell = cells.get(i);
396            let content = cell.map(|c| c.plain_text()).unwrap_or_default();
397            let _content_width = UnicodeWidthStr::width(content.as_str());
398            let cell_style = cell_styles.get(i).copied().unwrap_or_default();
399
400            let align = self.columns.get(i).map(|c| c.align).unwrap_or_default();
401            let padded = pad_string(&content, *width, align);
402
403            // Add padding
404            spans.push(Span::raw(" ".repeat(self.padding)));
405            spans.push(Span::styled(padded, cell_style));
406            spans.push(Span::raw(" ".repeat(self.padding)));
407
408            if i < widths.len() - 1 {
409                spans.push(Span::styled(line.cross.to_string(), self.style));
410            } else if self.show_border {
411                spans.push(Span::styled(line.right.to_string(), self.style));
412            }
413        }
414
415        vec![Segment::line(spans)]
416    }
417}
418
419fn pad_string(s: &str, width: usize, align: ColumnAlign) -> String {
420    let content_width = UnicodeWidthStr::width(s);
421    if content_width >= width {
422        return truncate_string(s, width);
423    }
424
425    let padding = width - content_width;
426    match align {
427        ColumnAlign::Left => format!("{}{}", s, " ".repeat(padding)),
428        ColumnAlign::Right => format!("{}{}", " ".repeat(padding), s),
429        ColumnAlign::Center => {
430            let left = padding / 2;
431            let right = padding - left;
432            format!("{}{}{}", " ".repeat(left), s, " ".repeat(right))
433        }
434    }
435}
436
437fn truncate_string(s: &str, width: usize) -> String {
438    use unicode_segmentation::UnicodeSegmentation;
439
440    let mut result = String::new();
441    let mut current_width = 0;
442
443    for grapheme in s.graphemes(true) {
444        let grapheme_width = UnicodeWidthStr::width(grapheme);
445        if current_width + grapheme_width > width {
446            if width > 1 && current_width < width {
447                result.push('…');
448            }
449            break;
450        }
451        result.push_str(grapheme);
452        current_width += grapheme_width;
453    }
454
455    // Pad if shorter
456    while current_width < width {
457        result.push(' ');
458        current_width += 1;
459    }
460
461    result
462}
463
464impl From<&str> for Column {
465    fn from(s: &str) -> Self {
466        Column::new(s)
467    }
468}
469
470impl From<String> for Column {
471    fn from(s: String) -> Self {
472        Column::new(&s)
473    }
474}
475
476impl Renderable for Table {
477    fn render(&self, context: &RenderContext) -> Vec<Segment> {
478        if self.columns.is_empty() {
479            return vec![];
480        }
481
482        let box_chars = self.border_style.to_box();
483        let widths = self.calculate_widths(context.width);
484        let mut segments = Vec::new();
485
486        // Calculate total table width for title centering
487        let content_width: usize = widths.iter().map(|w| w + self.padding * 2).sum();
488        let border_overhead = if self.show_border {
489            widths.len() + 1
490        } else {
491            widths.len() - 1
492        };
493        let table_width = content_width + border_overhead;
494
495        // Title
496        if let Some(title) = &self.title {
497            let title_width = UnicodeWidthStr::width(title.as_str());
498            if title_width <= table_width {
499                let padding = table_width - title_width;
500                let left_pad = padding / 2;
501                let right_pad = padding - left_pad;
502
503                let mut spans = Vec::new();
504                if left_pad > 0 {
505                    spans.push(Span::raw(" ".repeat(left_pad)));
506                }
507                spans.push(Span::styled(title.clone(), Style::new().bold()));
508                if right_pad > 0 {
509                    spans.push(Span::raw(" ".repeat(right_pad)));
510                }
511                segments.push(Segment::line(spans));
512            } else {
513                // Truncate or just print? Just print for now.
514                segments.push(Segment::line(vec![Span::styled(
515                    title.clone(),
516                    Style::new().bold(),
517                )]));
518            }
519        }
520
521        // Top border
522        if self.show_border {
523            segments.push(self.render_horizontal_line(
524                &widths,
525                &box_chars.top,
526            ));
527        }
528
529        // Header row
530        if self.show_header {
531            let header_cells: Vec<Text> = self
532                .columns
533                .iter()
534                .map(|c| Text::styled(c.header.clone(), c.header_style))
535                .collect();
536            let header_styles: Vec<Style> = self.columns.iter().map(|c| c.header_style).collect();
537            // Use header box line for vertical separators in header
538            segments.extend(self.render_row(&header_cells, &widths, &box_chars.header, &header_styles));
539
540            // Header separator
541            if self.show_border || self.show_row_lines {
542                segments.push(self.render_horizontal_line(
543                    &widths,
544                    &box_chars.head,
545                ));
546            }
547        }
548
549        // Data rows
550        for (row_idx, row) in self.rows.iter().enumerate() {
551            let cell_styles: Vec<Style> = self.columns.iter().map(|c| c.style).collect();
552            // Use cell box line for vertical separators in body
553            segments.extend(self.render_row(&row.cells, &widths, &box_chars.cell, &cell_styles));
554
555            // Row separator
556            if self.show_row_lines && row_idx < self.rows.len() - 1 {
557                segments.push(self.render_horizontal_line(
558                    &widths,
559                    &box_chars.mid,
560                ));
561            }
562        }
563
564        // Bottom border
565        if self.show_border {
566            segments.push(self.render_horizontal_line(
567                &widths,
568                &box_chars.bottom,
569            ));
570        }
571
572        segments
573    }
574}
575
576#[cfg(test)]
577mod tests {
578    use super::*;
579
580    #[test]
581    fn test_table_basic() {
582        let mut table = Table::new();
583        table.add_column("Name");
584        table.add_column("Age");
585        table.add_row_strs(&["Alice", "30"]);
586        table.add_row_strs(&["Bob", "25"]);
587
588        let context = RenderContext { width: 40, height: None };
589        let segments = table.render(&context);
590
591        assert!(!segments.is_empty());
592
593        // Check that output contains our data
594        let text: String = segments.iter().map(|s| s.plain_text()).collect();
595        assert!(text.contains("Name"));
596        assert!(text.contains("Alice"));
597        assert!(text.contains("Bob"));
598    }
599
600    #[test]
601    fn test_table_builder() {
602        let table = Table::new()
603            .columns(["A", "B", "C"])
604            .border_style(BorderStyle::Square);
605
606        assert_eq!(table.columns.len(), 3);
607    }
608
609    #[test]
610    fn test_pad_string() {
611        assert_eq!(pad_string("hi", 5, ColumnAlign::Left), "hi   ");
612        assert_eq!(pad_string("hi", 5, ColumnAlign::Right), "   hi");
613        assert_eq!(pad_string("hi", 5, ColumnAlign::Center), " hi  ");
614    }
615}