Skip to main content

rich_rs/
table.rs

1//! Table: a renderable table with columns and rows.
2//!
3//! Table displays data in a grid with optional headers, footers, and borders.
4//!
5//! # Example
6//!
7//! ```
8//! use rich_rs::Table;
9//!
10//! let mut table = Table::new();
11//! table.add_column_str("Name");
12//! table.add_column_str("Age");
13//! table.add_row_strs(&["Alice", "30"]);
14//! table.add_row_strs(&["Bob", "25"]);
15//! ```
16
17use std::io::Stdout;
18
19use crate::align::VerticalAlignMethod;
20use crate::r#box::{Box as RichBox, HEAVY_HEAD, RowLevel};
21use crate::console::{ConsoleOptions, JustifyMethod, OverflowMethod};
22use crate::measure::Measurement;
23use crate::padding::PaddingDimensions;
24use crate::rule::AlignMethod;
25use crate::segment::{Segment, Segments};
26use crate::style::Style;
27use crate::text::Text;
28use crate::{Console, Renderable};
29
30// ============================================================================
31// Column
32// ============================================================================
33
34/// A column definition within a table.
35///
36/// Columns define how data in a particular column should be displayed,
37/// including headers, footers, styling, and width constraints.
38///
39/// # Note
40///
41/// The `justify`, `vertical`, `overflow`, and `no_wrap` fields are defined for API
42/// compatibility but are not yet fully implemented. They will be applied in a future
43/// version. Currently, cell content is rendered with default justification and wrapping.
44pub struct Column {
45    /// Column header content.
46    pub header: Option<Box<dyn Renderable + Send + Sync>>,
47    /// Column footer content.
48    pub footer: Option<Box<dyn Renderable + Send + Sync>>,
49    /// Style for the header.
50    pub header_style: Style,
51    /// Style for the footer.
52    pub footer_style: Style,
53    /// Style for column cells.
54    pub style: Style,
55    /// Horizontal alignment for cell content.
56    pub justify: JustifyMethod,
57    /// Vertical alignment for cell content.
58    pub vertical: VerticalAlignMethod,
59    /// Overflow handling method.
60    pub overflow: OverflowMethod,
61    /// Fixed width (if set, overrides auto-width).
62    pub width: Option<usize>,
63    /// Minimum width constraint.
64    pub min_width: Option<usize>,
65    /// Maximum width constraint.
66    pub max_width: Option<usize>,
67    /// Flexible width ratio (for distributing extra space).
68    pub ratio: Option<usize>,
69    /// Prevent text wrapping in this column.
70    pub no_wrap: bool,
71    /// Internal index (set when added to table).
72    _index: usize,
73}
74
75impl std::fmt::Debug for Column {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        f.debug_struct("Column")
78            .field("header_style", &self.header_style)
79            .field("footer_style", &self.footer_style)
80            .field("style", &self.style)
81            .field("justify", &self.justify)
82            .field("vertical", &self.vertical)
83            .field("overflow", &self.overflow)
84            .field("width", &self.width)
85            .field("min_width", &self.min_width)
86            .field("max_width", &self.max_width)
87            .field("ratio", &self.ratio)
88            .field("no_wrap", &self.no_wrap)
89            .finish_non_exhaustive()
90    }
91}
92
93impl Default for Column {
94    fn default() -> Self {
95        Column {
96            header: None,
97            footer: None,
98            header_style: Style::default(),
99            footer_style: Style::default(),
100            style: Style::default(),
101            justify: JustifyMethod::Left,
102            vertical: VerticalAlignMethod::Top,
103            overflow: OverflowMethod::Ellipsis,
104            width: None,
105            min_width: None,
106            max_width: None,
107            ratio: None,
108            no_wrap: false,
109            _index: 0,
110        }
111    }
112}
113
114impl Column {
115    /// Create a new column with default settings.
116    pub fn new() -> Self {
117        Self::default()
118    }
119
120    /// Create a column with a string header.
121    pub fn with_header_str(header: &str) -> Self {
122        Column {
123            header: Some(Box::new(Text::plain(header))),
124            ..Default::default()
125        }
126    }
127
128    /// Create a column with a renderable header.
129    pub fn with_header(header: Box<dyn Renderable + Send + Sync>) -> Self {
130        Column {
131            header: Some(header),
132            ..Default::default()
133        }
134    }
135
136    /// Set the header style.
137    pub fn header_style(mut self, style: Style) -> Self {
138        self.header_style = style;
139        self
140    }
141
142    /// Set the footer style.
143    pub fn footer_style(mut self, style: Style) -> Self {
144        self.footer_style = style;
145        self
146    }
147
148    /// Set the cell style.
149    pub fn style(mut self, style: Style) -> Self {
150        self.style = style;
151        self
152    }
153
154    /// Set the horizontal alignment.
155    pub fn justify(mut self, justify: JustifyMethod) -> Self {
156        self.justify = justify;
157        self
158    }
159
160    /// Set the vertical alignment.
161    pub fn vertical(mut self, vertical: VerticalAlignMethod) -> Self {
162        self.vertical = vertical;
163        self
164    }
165
166    /// Set a fixed width.
167    pub fn width(mut self, width: usize) -> Self {
168        self.width = Some(width);
169        self
170    }
171
172    /// Set the minimum width.
173    pub fn min_width(mut self, width: usize) -> Self {
174        self.min_width = Some(width);
175        self
176    }
177
178    /// Set the maximum width.
179    pub fn max_width(mut self, width: usize) -> Self {
180        self.max_width = Some(width);
181        self
182    }
183
184    /// Set the ratio for flexible width distribution.
185    pub fn ratio(mut self, ratio: usize) -> Self {
186        self.ratio = Some(ratio);
187        self
188    }
189
190    /// Set no_wrap mode.
191    pub fn no_wrap(mut self, no_wrap: bool) -> Self {
192        self.no_wrap = no_wrap;
193        self
194    }
195
196    /// Set the footer content (builder pattern).
197    pub fn with_footer(mut self, footer: Box<dyn Renderable + Send + Sync>) -> Self {
198        self.footer = Some(footer);
199        self
200    }
201
202    /// Check if this column is flexible (has a ratio set).
203    pub fn flexible(&self) -> bool {
204        self.ratio.is_some()
205    }
206
207    // Mutation methods for use with Live displays
208
209    /// Set the column justification.
210    pub fn set_justify(&mut self, justify: JustifyMethod) {
211        self.justify = justify;
212    }
213
214    /// Set the column style.
215    pub fn set_style(&mut self, style: Style) {
216        self.style = style;
217    }
218
219    /// Set the header style.
220    pub fn set_header_style(&mut self, style: Style) {
221        self.header_style = style;
222    }
223
224    /// Set the footer style.
225    pub fn set_footer_style(&mut self, style: Style) {
226        self.footer_style = style;
227    }
228}
229
230// ============================================================================
231// Row
232// ============================================================================
233
234/// A row within a table.
235///
236/// Rows contain cells and optional style overrides.
237pub struct Row {
238    /// Cells in this row (one per column).
239    pub cells: Vec<Box<dyn Renderable + Send + Sync>>,
240    /// Optional style override for the entire row.
241    pub style: Option<Style>,
242    /// Draw a section separator after this row.
243    pub end_section: bool,
244}
245
246impl std::fmt::Debug for Row {
247    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
248        f.debug_struct("Row")
249            .field("cells_count", &self.cells.len())
250            .field("style", &self.style)
251            .field("end_section", &self.end_section)
252            .finish()
253    }
254}
255
256impl Row {
257    /// Create a new row with the given cells.
258    pub fn new(cells: Vec<Box<dyn Renderable + Send + Sync>>) -> Self {
259        Row {
260            cells,
261            style: None,
262            end_section: false,
263        }
264    }
265
266    /// Create an empty row.
267    pub fn empty() -> Self {
268        Row {
269            cells: Vec::new(),
270            style: None,
271            end_section: false,
272        }
273    }
274
275    /// Set the row style.
276    pub fn with_style(mut self, style: Style) -> Self {
277        self.style = Some(style);
278        self
279    }
280
281    /// Set end_section to draw a separator after this row.
282    pub fn with_end_section(mut self, end_section: bool) -> Self {
283        self.end_section = end_section;
284        self
285    }
286}
287
288// ============================================================================
289// Table
290// ============================================================================
291
292/// A console-renderable table with rows and columns.
293///
294/// Table supports headers, footers, various border styles, and flexible
295/// column width calculation.
296///
297/// # Example
298///
299/// ```
300/// use rich_rs::Table;
301///
302/// let mut table = Table::new();
303/// table.add_column_str("Name");
304/// table.add_column_str("Score");
305/// table.add_row_strs(&["Alice", "100"]);
306/// table.add_row_strs(&["Bob", "95"]);
307/// ```
308pub struct Table {
309    /// Column definitions.
310    columns: Vec<Column>,
311    /// Data rows (cells stored in columns for measurement efficiency).
312    rows: Vec<Row>,
313    /// Border style (None = no borders).
314    box_type: Option<RichBox>,
315    /// Use ASCII-safe box characters (None = use console default).
316    safe_box: Option<bool>,
317    /// Cell padding (top, right, bottom, left) — CSS order.
318    padding: (usize, usize, usize, usize),
319    /// Collapse padding between adjacent cells.
320    collapse_padding: bool,
321    /// Pad the edge cells.
322    pad_edge: bool,
323    /// Expand table to fill available width.
324    expand: bool,
325    /// Show header row.
326    show_header: bool,
327    /// Show footer row.
328    show_footer: bool,
329    /// Show outer edge (border).
330    show_edge: bool,
331    /// Show lines between all rows.
332    show_lines: bool,
333    /// Number of blank lines between rows (alternative to show_lines).
334    leading: usize,
335    /// Base style for the table.
336    style: Style,
337    /// Alternating row styles.
338    row_styles: Vec<Style>,
339    /// Style for the header row.
340    header_style: Style,
341    /// Style for the footer row.
342    footer_style: Style,
343    /// Style for borders.
344    border_style: Style,
345    /// Optional title above the table.
346    title: Option<Text>,
347    /// Optional caption below the table.
348    caption: Option<Text>,
349    /// Style for the title.
350    title_style: Option<Style>,
351    /// Style for the caption.
352    caption_style: Option<Style>,
353    /// Title alignment.
354    title_align: AlignMethod,
355    /// Caption alignment.
356    caption_align: AlignMethod,
357    /// Fixed width (None = auto).
358    width: Option<usize>,
359    /// Minimum width.
360    min_width: Option<usize>,
361    /// Enable highlighting of cell contents.
362    highlight: bool,
363}
364
365impl std::fmt::Debug for Table {
366    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
367        f.debug_struct("Table")
368            .field("columns", &self.columns.len())
369            .field("rows", &self.rows.len())
370            .field("box_type", &self.box_type)
371            .field("show_header", &self.show_header)
372            .field("show_footer", &self.show_footer)
373            .field("show_edge", &self.show_edge)
374            .field("expand", &self.expand)
375            .field("width", &self.width)
376            .finish_non_exhaustive()
377    }
378}
379
380impl Default for Table {
381    fn default() -> Self {
382        Table::new()
383    }
384}
385
386impl Table {
387    /// Create a new empty table with default settings.
388    ///
389    /// The default table uses HEAVY_HEAD border style, shows headers,
390    /// and has standard padding.
391    pub fn new() -> Self {
392        Table {
393            columns: Vec::new(),
394            rows: Vec::new(),
395            box_type: Some(HEAVY_HEAD),
396            safe_box: None,
397            padding: (0, 1, 0, 1),
398            collapse_padding: false,
399            pad_edge: true,
400            expand: false,
401            show_header: true,
402            show_footer: false,
403            show_edge: true,
404            show_lines: false,
405            leading: 0,
406            style: Style::default(),
407            row_styles: Vec::new(),
408            header_style: Style::new().with_bold(true),
409            footer_style: Style::default(),
410            border_style: Style::default(),
411            title: None,
412            caption: None,
413            title_style: None,
414            caption_style: None,
415            title_align: AlignMethod::Center,
416            caption_align: AlignMethod::Center,
417            width: None,
418            min_width: None,
419            highlight: false,
420        }
421    }
422
423    /// Create a grid (table with no borders or header).
424    ///
425    /// A grid is useful for simple layouts without table decoration.
426    pub fn grid() -> Self {
427        Table {
428            box_type: None,
429            padding: (0, 0, 0, 0),
430            collapse_padding: true,
431            pad_edge: false,
432            show_header: false,
433            show_footer: false,
434            show_edge: false,
435            ..Table::new()
436        }
437    }
438
439    // ========================================================================
440    // Builder methods
441    // ========================================================================
442
443    /// Set the border style.
444    pub fn with_box(mut self, box_type: Option<RichBox>) -> Self {
445        self.box_type = box_type;
446        self
447    }
448
449    /// Set whether to use ASCII-safe box characters.
450    pub fn with_safe_box(mut self, safe: bool) -> Self {
451        self.safe_box = Some(safe);
452        self
453    }
454
455    /// Set cell padding (left and right), keeping top/bottom at 0.
456    ///
457    /// For full 4-way padding, use `with_padding_dims`.
458    pub fn with_padding(mut self, left: usize, right: usize) -> Self {
459        self.padding = (0, right, 0, left);
460        self
461    }
462
463    /// Set cell padding using CSS-order dimensions.
464    ///
465    /// Accepts `PaddingDimensions` (or anything that converts to it):
466    /// - `usize` — all four sides
467    /// - `(vert, horiz)` — top/bottom and left/right
468    /// - `(top, right, bottom, left)` — CSS order
469    pub fn with_padding_dims(mut self, pad: impl Into<PaddingDimensions>) -> Self {
470        self.padding = pad.into().unpack();
471        self
472    }
473
474    /// Set the title style.
475    pub fn with_title_style(mut self, style: Style) -> Self {
476        self.title_style = Some(style);
477        self
478    }
479
480    /// Set the caption style.
481    pub fn with_caption_style(mut self, style: Style) -> Self {
482        self.caption_style = Some(style);
483        self
484    }
485
486    /// Set whether to collapse padding between cells.
487    pub fn with_collapse_padding(mut self, collapse: bool) -> Self {
488        self.collapse_padding = collapse;
489        self
490    }
491
492    /// Set whether to pad edge cells.
493    pub fn with_pad_edge(mut self, pad: bool) -> Self {
494        self.pad_edge = pad;
495        self
496    }
497
498    /// Set whether to expand to fill available width.
499    pub fn with_expand(mut self, expand: bool) -> Self {
500        self.expand = expand;
501        self
502    }
503
504    /// Set whether to show the header row.
505    pub fn with_show_header(mut self, show: bool) -> Self {
506        self.show_header = show;
507        self
508    }
509
510    /// Set whether to show the footer row.
511    pub fn with_show_footer(mut self, show: bool) -> Self {
512        self.show_footer = show;
513        self
514    }
515
516    /// Set whether to show the outer edge.
517    pub fn with_show_edge(mut self, show: bool) -> Self {
518        self.show_edge = show;
519        self
520    }
521
522    /// Set whether to show lines between all rows.
523    pub fn with_show_lines(mut self, show: bool) -> Self {
524        self.show_lines = show;
525        self
526    }
527
528    /// Set number of blank lines between rows.
529    pub fn with_leading(mut self, leading: usize) -> Self {
530        self.leading = leading;
531        self
532    }
533
534    /// Set the base table style.
535    pub fn with_style(mut self, style: Style) -> Self {
536        self.style = style;
537        self
538    }
539
540    /// Set alternating row styles.
541    pub fn with_row_styles(mut self, styles: Vec<Style>) -> Self {
542        self.row_styles = styles;
543        self
544    }
545
546    /// Set the header style.
547    pub fn with_header_style(mut self, style: Style) -> Self {
548        self.header_style = style;
549        self
550    }
551
552    /// Set the footer style.
553    pub fn with_footer_style(mut self, style: Style) -> Self {
554        self.footer_style = style;
555        self
556    }
557
558    /// Set the border style.
559    pub fn with_border_style(mut self, style: Style) -> Self {
560        self.border_style = style;
561        self
562    }
563
564    /// Set the title above the table.
565    pub fn with_title(mut self, title: &str) -> Self {
566        self.title = Some(Text::plain(title));
567        self
568    }
569
570    /// Set the title with a Text object.
571    pub fn with_title_text(mut self, title: Text) -> Self {
572        self.title = Some(title);
573        self
574    }
575
576    /// Set the caption below the table.
577    pub fn with_caption(mut self, caption: &str) -> Self {
578        self.caption = Some(Text::plain(caption));
579        self
580    }
581
582    /// Set the caption with a Text object.
583    pub fn with_caption_text(mut self, caption: Text) -> Self {
584        self.caption = Some(caption);
585        self
586    }
587
588    /// Set title alignment.
589    pub fn with_title_align(mut self, align: AlignMethod) -> Self {
590        self.title_align = align;
591        self
592    }
593
594    /// Set caption alignment.
595    pub fn with_caption_align(mut self, align: AlignMethod) -> Self {
596        self.caption_align = align;
597        self
598    }
599
600    /// Set a fixed width for the table.
601    pub fn with_width(mut self, width: usize) -> Self {
602        self.width = Some(width);
603        self
604    }
605
606    /// Set the minimum width.
607    pub fn with_min_width(mut self, width: usize) -> Self {
608        self.min_width = Some(width);
609        self
610    }
611
612    /// Set whether to highlight cell contents.
613    pub fn with_highlight(mut self, highlight: bool) -> Self {
614        self.highlight = highlight;
615        self
616    }
617
618    // ========================================================================
619    // Mutation methods
620    // ========================================================================
621
622    /// Add a column to the table.
623    pub fn add_column(&mut self, mut column: Column) {
624        column._index = self.columns.len();
625        self.columns.push(column);
626    }
627
628    /// Add a column with a string header.
629    pub fn add_column_str(&mut self, header: &str) {
630        self.add_column(Column::with_header_str(header));
631    }
632
633    /// Add a column with a renderable header.
634    pub fn add_column_renderable(&mut self, header: Box<dyn Renderable + Send + Sync>) {
635        self.add_column(Column::with_header(header));
636    }
637
638    /// Add a row of cells to the table.
639    pub fn add_row(&mut self, row: Row) {
640        // Auto-create columns if needed
641        while self.columns.len() < row.cells.len() {
642            let mut col = Column::default();
643            col._index = self.columns.len();
644            self.columns.push(col);
645        }
646        self.rows.push(row);
647    }
648
649    /// Add a row of string cells.
650    pub fn add_row_strs(&mut self, cells: &[&str]) {
651        let cell_boxes: Vec<Box<dyn Renderable + Send + Sync>> = cells
652            .iter()
653            .map(|s| Box::new(Text::plain(*s)) as Box<dyn Renderable + Send + Sync>)
654            .collect();
655        self.add_row(Row::new(cell_boxes));
656    }
657
658    /// Add a row of renderable cells.
659    pub fn add_row_renderables(&mut self, cells: Vec<Box<dyn Renderable + Send + Sync>>) {
660        self.add_row(Row::new(cells));
661    }
662
663    /// Mark the last row as an end-of-section (draws separator after).
664    pub fn add_section(&mut self) {
665        if let Some(row) = self.rows.last_mut() {
666            row.end_section = true;
667        }
668    }
669
670    /// Get the number of rows (excluding header/footer).
671    pub fn row_count(&self) -> usize {
672        self.rows.len()
673    }
674
675    /// Get the number of columns.
676    pub fn column_count(&self) -> usize {
677        self.columns.len()
678    }
679
680    // ========================================================================
681    // Mutation methods (for use with Live displays)
682    // ========================================================================
683
684    /// Set the table title.
685    pub fn set_title(&mut self, title: Option<Text>) {
686        self.title = title;
687    }
688
689    /// Set the table caption.
690    pub fn set_caption(&mut self, caption: Option<Text>) {
691        self.caption = caption;
692    }
693
694    /// Set whether to show the footer row.
695    pub fn set_show_footer(&mut self, show: bool) {
696        self.show_footer = show;
697    }
698
699    /// Set the border style.
700    pub fn set_border_style(&mut self, style: Style) {
701        self.border_style = style;
702    }
703
704    /// Set the box drawing style.
705    pub fn set_box(&mut self, box_type: Option<RichBox>) {
706        self.box_type = box_type;
707    }
708
709    /// Set alternating row styles.
710    pub fn set_row_styles(&mut self, styles: Vec<Style>) {
711        self.row_styles = styles;
712    }
713
714    /// Set whether to pad the edge cells.
715    pub fn set_pad_edge(&mut self, pad: bool) {
716        self.pad_edge = pad;
717    }
718
719    /// Set the fixed width (None for auto).
720    pub fn set_width(&mut self, width: Option<usize>) {
721        self.width = width;
722    }
723
724    /// Get a mutable reference to a column by index.
725    pub fn column_mut(&mut self, idx: usize) -> Option<&mut Column> {
726        self.columns.get_mut(idx)
727    }
728
729    // ========================================================================
730    // Internal helpers
731    // ========================================================================
732
733    /// Calculate extra width from borders and dividers.
734    fn extra_width(&self) -> usize {
735        let mut width = 0;
736        if self.box_type.is_some() && self.show_edge {
737            width += 2; // Left and right edges
738        }
739        if self.box_type.is_some() && self.columns.len() > 1 {
740            width += self.columns.len() - 1; // Dividers between columns
741        }
742        width
743    }
744
745    /// Get the padding width used for measuring / sizing columns.
746    ///
747    /// Mirrors Python Rich's `_get_padding_width`: collapse padding affects the
748    /// *computed* padding width, but `pad_edge` does not.
749    ///
750    /// Rich uses this value when interpreting column width constraints
751    /// (e.g. fixed-width columns, min/max bounds), while the actual padding applied
752    /// to cells may vary with `pad_edge` (handled in `get_padding_for_column`).
753    fn get_measure_padding_width(&self, column_index: usize) -> usize {
754        let (_top, pad_right, _bottom, pad_left) = self.padding;
755        let mut left = pad_left;
756        let right = pad_right;
757
758        if self.collapse_padding && column_index > 0 {
759            left = left.saturating_sub(right);
760        }
761
762        left + right
763    }
764
765    /// Get the actual (left, right) padding for a specific column,
766    /// accounting for collapse_padding and pad_edge settings.
767    fn get_padding_for_column(&self, column_index: usize) -> (usize, usize) {
768        let (_top, pad_right, _bottom, pad_left) = self.padding;
769        let num_columns = self.columns.len();
770        let is_first = column_index == 0;
771        let is_last = column_index == num_columns.saturating_sub(1);
772
773        let mut left = pad_left;
774        let mut right = pad_right;
775
776        // Collapse padding between columns (avoid double padding)
777        if self.collapse_padding && !is_first {
778            // Mirror Python Rich behavior: subtract the right padding rather than forcing to 0.
779            // This matters when left != right.
780            left = left.saturating_sub(pad_right);
781        }
782
783        // Don't pad edges if pad_edge is false
784        if !self.pad_edge {
785            if is_first {
786                left = 0;
787            }
788            if is_last {
789                right = 0;
790            }
791        }
792
793        (left, right)
794    }
795
796    /// Get the row style for a given row index.
797    fn get_row_style(&self, index: usize) -> Style {
798        let mut style = Style::default();
799        if !self.row_styles.is_empty() {
800            style = style.combine(&self.row_styles[index % self.row_styles.len()]);
801        }
802        if let Some(row_style) = self.rows.get(index).and_then(|r| r.style) {
803            style = style.combine(&row_style);
804        }
805        style
806    }
807
808    /// Measure a column to determine its min/max width.
809    fn measure_column(
810        &self,
811        console: &Console<Stdout>,
812        options: &ConsoleOptions,
813        column: &Column,
814    ) -> Measurement {
815        let max_width = options.max_width;
816        if max_width < 1 {
817            return Measurement::new(0, 0);
818        }
819
820        // Python Rich measures padded cell renderables, which means `pad_edge` affects
821        // the measured width of flexible columns. For fixed-width columns / clamp
822        // bounds Rich uses `_get_padding_width` (which ignores `pad_edge`), so we
823        // keep both concepts here:
824        // - `content_padding_width`: actual padding applied at render time
825        // - `bounds_padding_width`: width used when interpreting column width constraints
826        let (pad_left, pad_right) = self.get_padding_for_column(column._index);
827        let content_padding_width = pad_left + pad_right;
828        let bounds_padding_width = self.get_measure_padding_width(column._index);
829
830        // Fixed width column
831        if let Some(w) = column.width {
832            return Measurement::new(w + bounds_padding_width, w + bounds_padding_width)
833                .with_maximum(max_width);
834        }
835
836        // Measure all cells in this column
837        let mut min_widths: Vec<usize> = Vec::new();
838        let mut max_widths: Vec<usize> = Vec::new();
839
840        // Measure header
841        if self.show_header {
842            if let Some(ref header) = column.header {
843                let m = header.measure(console, options);
844                min_widths.push(m.minimum);
845                max_widths.push(m.maximum);
846            }
847        }
848
849        // Measure data cells
850        for row in &self.rows {
851            if let Some(cell) = row.cells.get(column._index) {
852                let m = cell.measure(console, options);
853                min_widths.push(m.minimum);
854                max_widths.push(m.maximum);
855            }
856        }
857
858        // Measure footer
859        if self.show_footer {
860            if let Some(ref footer) = column.footer {
861                let m = footer.measure(console, options);
862                min_widths.push(m.minimum);
863                max_widths.push(m.maximum);
864            }
865        }
866
867        let min_w = min_widths.iter().max().copied().unwrap_or(1) + content_padding_width;
868        let max_w = max_widths.iter().max().copied().unwrap_or(max_width) + content_padding_width;
869
870        Measurement::new(min_w, max_w)
871            .with_maximum(max_width)
872            .clamp_bounds(
873                column.min_width.map(|w| w + bounds_padding_width),
874                column.max_width.map(|w| w + bounds_padding_width),
875            )
876    }
877
878    /// Calculate column widths based on content and constraints.
879    fn calculate_column_widths(
880        &self,
881        console: &Console<Stdout>,
882        options: &ConsoleOptions,
883    ) -> Vec<usize> {
884        if self.columns.is_empty() {
885            return Vec::new();
886        }
887
888        // Important: `options.max_width` here is the available width for *columns* only
889        // (i.e. it excludes any border / divider extra width). This mirrors Python Rich,
890        // which calls `_calculate_column_widths` with `options.update_width(max_width - extra_width)`.
891        let max_width = options.max_width;
892        let extra_width = self.extra_width();
893        let effective_expand = self.expand || self.width.is_some();
894
895        // Measure each column
896        let measurements: Vec<Measurement> = self
897            .columns
898            .iter()
899            .map(|col| self.measure_column(console, options, col))
900            .collect();
901
902        let mut widths: Vec<usize> = measurements.iter().map(|m| m.maximum.max(1)).collect();
903
904        // Handle flexible columns with ratios
905        if effective_expand {
906            let ratios: Vec<usize> = self
907                .columns
908                .iter()
909                .filter(|c| c.flexible())
910                .map(|c| c.ratio.unwrap_or(0))
911                .collect();
912
913            if ratios.iter().any(|&r| r > 0) {
914                let fixed_widths: Vec<usize> = self
915                    .columns
916                    .iter()
917                    .zip(measurements.iter())
918                    .map(|(column, measurement)| {
919                        if column.flexible() {
920                            0
921                        } else {
922                            measurement.maximum
923                        }
924                    })
925                    .collect();
926
927                let flex_minimums: Vec<usize> = self
928                    .columns
929                    .iter()
930                    .filter(|column| column.flexible())
931                    .map(|column| {
932                        (column.width.unwrap_or(1)) + self.get_measure_padding_width(column._index)
933                    })
934                    .collect();
935
936                let fixed_total: usize = fixed_widths.iter().sum();
937                let flexible_width = max_width.saturating_sub(fixed_total);
938                let flex_widths = ratio_distribute(flexible_width, &ratios, Some(&flex_minimums));
939                let mut flex_iter = flex_widths.into_iter();
940                for (index, column) in self.columns.iter().enumerate() {
941                    if column.flexible() {
942                        widths[index] = fixed_widths[index] + flex_iter.next().unwrap_or(0);
943                    }
944                }
945            }
946        }
947
948        let mut table_width: usize = widths.iter().sum();
949
950        if table_width > max_width {
951            widths = collapse_widths(
952                widths,
953                self.columns
954                    .iter()
955                    .map(|column| column.width.is_none() && !column.no_wrap)
956                    .collect(),
957                max_width,
958            );
959
960            table_width = widths.iter().sum();
961
962            // Last resort: reduce columns evenly.
963            if table_width > max_width {
964                let excess_width = table_width - max_width;
965                let ratios = vec![1; widths.len()];
966                widths = ratio_reduce(excess_width, &ratios, &widths, &widths);
967                table_width = widths.iter().sum();
968            }
969
970            // Re-measure with constrained widths (critical for parity with Python Rich).
971            let constrained: Vec<Measurement> = widths
972                .iter()
973                .zip(self.columns.iter())
974                .map(|(width, column)| {
975                    self.measure_column(console, &options.update_width(*width), column)
976                })
977                .collect();
978            widths = constrained.iter().map(|m| m.maximum).collect();
979        }
980
981        // If expanding and table is too narrow, distribute extra space.
982        // Mirrors Python Rich's logic: distribute proportionally using current widths.
983        if (table_width < max_width && effective_expand)
984            || (self.min_width.is_some()
985                && table_width < self.min_width.unwrap_or(0).saturating_sub(extra_width))
986        {
987            let min_target = self.min_width.unwrap_or(0).saturating_sub(extra_width);
988            let target = if self.min_width.is_some() {
989                min_target.min(max_width)
990            } else {
991                max_width
992            };
993
994            if table_width < target {
995                let pad_widths = ratio_distribute(target - table_width, &widths, None);
996                widths = widths
997                    .into_iter()
998                    .zip(pad_widths.into_iter())
999                    .map(|(w, pad)| w + pad)
1000                    .collect();
1001            }
1002        }
1003
1004        widths
1005    }
1006
1007    /// Render a cell with proper styling and padding.
1008    fn render_cell(
1009        &self,
1010        console: &Console<Stdout>,
1011        options: &ConsoleOptions,
1012        cell: &dyn Renderable,
1013        column: &Column,
1014        width: usize,
1015        style: Style,
1016        _is_header: bool,
1017        _is_footer: bool,
1018    ) -> Vec<Vec<Segment>> {
1019        // Get actual padding for this column (respects collapse_padding and pad_edge)
1020        let (pad_left, pad_right) = self.get_padding_for_column(column._index);
1021        let padding = pad_left + pad_right;
1022        let content_width = width.saturating_sub(padding);
1023
1024        // Create options for cell rendering
1025        let mut cell_options = options.update_width(content_width);
1026        if column.justify != JustifyMethod::Left {
1027            cell_options.justify = Some(column.justify);
1028        }
1029        if column.overflow != OverflowMethod::Ellipsis {
1030            cell_options.overflow = Some(column.overflow);
1031        }
1032        if column.no_wrap {
1033            cell_options.no_wrap = true;
1034        }
1035
1036        // Render cell content
1037        let cell_lines = console.render_lines(cell, Some(&cell_options), Some(style), true, false);
1038
1039        // Apply padding to each line
1040        let left_pad = Segment::styled(" ".repeat(pad_left), style);
1041        let right_pad = Segment::styled(" ".repeat(pad_right), style);
1042
1043        let mut result: Vec<Vec<Segment>> = Vec::new();
1044        let (pad_top, _, pad_bottom, _) = self.padding;
1045
1046        // Add top padding blank lines
1047        if pad_top > 0 {
1048            let blank = Segment::adjust_line_length(&[], width, Some(style), true);
1049            for _ in 0..pad_top {
1050                result.push(blank.clone());
1051            }
1052        }
1053
1054        // Add content lines with left/right padding
1055        for line in cell_lines {
1056            let mut padded = Vec::new();
1057            if pad_left > 0 {
1058                padded.push(left_pad.clone());
1059            }
1060            for seg in line {
1061                padded.push(seg);
1062            }
1063            if pad_right > 0 {
1064                padded.push(right_pad.clone());
1065            }
1066            result.push(Segment::adjust_line_length(
1067                &padded,
1068                width,
1069                Some(style),
1070                true,
1071            ));
1072        }
1073
1074        // Add bottom padding blank lines
1075        if pad_bottom > 0 {
1076            let blank = Segment::adjust_line_length(&[], width, Some(style), true);
1077            for _ in 0..pad_bottom {
1078                result.push(blank.clone());
1079            }
1080        }
1081
1082        result
1083    }
1084}
1085
1086impl Renderable for Table {
1087    fn render(&self, console: &Console<Stdout>, options: &ConsoleOptions) -> Segments {
1088        let mut result = Segments::new();
1089
1090        // Empty table
1091        if self.columns.is_empty() {
1092            result.push(Segment::line());
1093            return result;
1094        }
1095
1096        // Determine box characters
1097        let safe_box = self.safe_box.unwrap_or(options.legacy_windows);
1098        let box_chars = self
1099            .box_type
1100            .map(|b| b.substitute(safe_box, options.ascii_only()));
1101
1102        // Calculate column widths
1103        let max_width = self.width.unwrap_or(options.max_width);
1104        let extra = self.extra_width();
1105        // Match Python Rich: calculate widths against the available *column* width,
1106        // which excludes the table's extra border / divider width.
1107        let col_options = options.update_width(max_width.saturating_sub(extra));
1108        let widths = self.calculate_column_widths(console, &col_options);
1109
1110        if widths.is_empty() {
1111            result.push(Segment::line());
1112            return result;
1113        }
1114
1115        let table_width: usize = widths.iter().sum::<usize>() + extra;
1116        let border_style = self.style.combine(&self.border_style);
1117        let new_line = Segment::line();
1118
1119        // Render title
1120        if let Some(ref title) = self.title {
1121            let title_lines =
1122                console.render_lines(title, Some(options), self.title_style, false, false);
1123            for line in title_lines {
1124                let line_width = Segment::get_line_length(&line);
1125                let padding = table_width.saturating_sub(line_width);
1126
1127                let (left_pad, right_pad) = match self.title_align {
1128                    AlignMethod::Left => (0, padding),
1129                    AlignMethod::Center => {
1130                        let left = padding / 2;
1131                        (left, padding - left)
1132                    }
1133                    AlignMethod::Right => (padding, 0),
1134                };
1135
1136                if left_pad > 0 {
1137                    result.push(Segment::new(" ".repeat(left_pad)));
1138                }
1139                for seg in line {
1140                    result.push(seg);
1141                }
1142                if right_pad > 0 {
1143                    result.push(Segment::new(" ".repeat(right_pad)));
1144                }
1145                result.push(new_line.clone());
1146            }
1147
1148            // Match Python Rich's grid title spacing: insert a blank line after the title
1149            // for borderless tables (e.g. `Table::grid()`).
1150            if !self.show_edge {
1151                result.push(new_line.clone());
1152            }
1153        }
1154
1155        // Render top border
1156        if let Some(ref bx) = box_chars {
1157            if self.show_edge {
1158                let top = bx.get_top(&widths);
1159                result.push(Segment::styled(top, border_style));
1160                result.push(new_line.clone());
1161            }
1162        }
1163
1164        // Render header row
1165        if self.show_header {
1166            let header_row_style = self.header_style;
1167
1168            // Render each header cell
1169            let mut header_cells: Vec<Vec<Vec<Segment>>> = Vec::new();
1170            let mut max_height = 1;
1171
1172            let empty_text = Text::plain("");
1173            for (i, column) in self.columns.iter().enumerate() {
1174                let cell: &dyn Renderable = column
1175                    .header
1176                    .as_ref()
1177                    .map(|b| b.as_ref())
1178                    .unwrap_or(&empty_text as &dyn Renderable);
1179
1180                let cell_style = header_row_style.combine(&column.header_style);
1181                let cell_lines = self.render_cell(
1182                    console, options, cell, column, widths[i], cell_style, true, false,
1183                );
1184                max_height = max_height.max(cell_lines.len());
1185                header_cells.push(cell_lines);
1186            }
1187
1188            // Normalize heights - use column header style for padding
1189            for (i, cells) in header_cells.iter_mut().enumerate() {
1190                let col_style = header_row_style.combine(&self.columns[i].header_style);
1191                let hint_style = cells.last().and_then(|line| Segment::get_last_style(line));
1192                while cells.len() < max_height {
1193                    // When padding vertically, preserve only the **background** of the last rendered
1194                    // line to avoid visible hairlines in block backgrounds, but don't let decoration
1195                    // attributes (underline/dim/etc.) bleed into the padding.
1196                    let pad_style = hint_style
1197                        .and_then(|hint| hint.bgcolor.map(|bg| Style::new().with_bgcolor(bg)))
1198                        .map(|bg_style| col_style.combine(&bg_style))
1199                        .unwrap_or(col_style);
1200                    let blank = Segment::adjust_line_length(&[], widths[i], Some(pad_style), true);
1201                    cells.push(blank);
1202                }
1203            }
1204
1205            // Render header lines
1206            for line_idx in 0..max_height {
1207                if let Some(ref bx) = box_chars {
1208                    if self.show_edge {
1209                        result.push(Segment::styled(bx.head_left.to_string(), border_style));
1210                    }
1211                }
1212
1213                for (col_idx, cells) in header_cells.iter().enumerate() {
1214                    for seg in &cells[line_idx] {
1215                        result.push(seg.clone());
1216                    }
1217                    if col_idx < header_cells.len() - 1 {
1218                        if let Some(ref bx) = box_chars {
1219                            result
1220                                .push(Segment::styled(bx.head_vertical.to_string(), border_style));
1221                        }
1222                    }
1223                }
1224
1225                if let Some(ref bx) = box_chars {
1226                    if self.show_edge {
1227                        result.push(Segment::styled(bx.head_right.to_string(), border_style));
1228                    }
1229                }
1230                result.push(new_line.clone());
1231            }
1232
1233            // Render header separator
1234            if let Some(ref bx) = box_chars {
1235                let row_line = bx.get_row(&widths, RowLevel::Head, self.show_edge);
1236                result.push(Segment::styled(row_line, border_style));
1237                result.push(new_line.clone());
1238            }
1239        }
1240
1241        // Render data rows
1242        let empty_cell = Text::plain("");
1243        for (row_idx, row) in self.rows.iter().enumerate() {
1244            let row_style = self.get_row_style(row_idx);
1245
1246            // Render each cell
1247            let mut row_cells: Vec<Vec<Vec<Segment>>> = Vec::new();
1248            let mut max_height = 1;
1249
1250            for (col_idx, column) in self.columns.iter().enumerate() {
1251                let cell: &dyn Renderable = row
1252                    .cells
1253                    .get(col_idx)
1254                    .map(|b| b.as_ref())
1255                    .unwrap_or(&empty_cell as &dyn Renderable);
1256
1257                let cell_style = self.style.combine(&column.style).combine(&row_style);
1258                let cell_lines = self.render_cell(
1259                    console,
1260                    options,
1261                    cell,
1262                    column,
1263                    widths[col_idx],
1264                    cell_style,
1265                    false,
1266                    false,
1267                );
1268                max_height = max_height.max(cell_lines.len());
1269                row_cells.push(cell_lines);
1270            }
1271
1272            // Normalize heights - use column style for padding and respect vertical alignment
1273            for (i, cells) in row_cells.iter_mut().enumerate() {
1274                let col_style = self
1275                    .style
1276                    .combine(&self.columns[i].style)
1277                    .combine(&row_style);
1278                let hint_style = cells.last().and_then(|line| Segment::get_last_style(line));
1279                // Preserve only background from the last rendered line (see header comment).
1280                let pad_style = hint_style
1281                    .and_then(|hint| hint.bgcolor.map(|bg| Style::new().with_bgcolor(bg)))
1282                    .map(|bg_style| col_style.combine(&bg_style))
1283                    .unwrap_or(col_style);
1284                let blank = Segment::adjust_line_length(&[], widths[i], Some(pad_style), true);
1285
1286                let lines_needed = max_height.saturating_sub(cells.len());
1287                if lines_needed > 0 {
1288                    match self.columns[i].vertical {
1289                        VerticalAlignMethod::Top => {
1290                            // Add blanks at end
1291                            for _ in 0..lines_needed {
1292                                cells.push(blank.clone());
1293                            }
1294                        }
1295                        VerticalAlignMethod::Middle => {
1296                            // Add blanks at start and end
1297                            let top_pad = lines_needed / 2;
1298                            let bottom_pad = lines_needed - top_pad;
1299                            let mut new_cells = Vec::with_capacity(max_height);
1300                            for _ in 0..top_pad {
1301                                new_cells.push(blank.clone());
1302                            }
1303                            new_cells.append(cells);
1304                            for _ in 0..bottom_pad {
1305                                new_cells.push(blank.clone());
1306                            }
1307                            *cells = new_cells;
1308                        }
1309                        VerticalAlignMethod::Bottom => {
1310                            // Add blanks at start
1311                            let mut new_cells = Vec::with_capacity(max_height);
1312                            for _ in 0..lines_needed {
1313                                new_cells.push(blank.clone());
1314                            }
1315                            new_cells.append(cells);
1316                            *cells = new_cells;
1317                        }
1318                    }
1319                }
1320            }
1321
1322            // Render row lines
1323            for line_idx in 0..max_height {
1324                if let Some(ref bx) = box_chars {
1325                    if self.show_edge {
1326                        result.push(Segment::styled(bx.mid_left.to_string(), border_style));
1327                    }
1328                }
1329
1330                for (col_idx, cells) in row_cells.iter().enumerate() {
1331                    for seg in &cells[line_idx] {
1332                        result.push(seg.clone());
1333                    }
1334                    if col_idx < row_cells.len() - 1 {
1335                        if let Some(ref bx) = box_chars {
1336                            result.push(Segment::styled(bx.mid_vertical.to_string(), border_style));
1337                        }
1338                    }
1339                }
1340
1341                if let Some(ref bx) = box_chars {
1342                    if self.show_edge {
1343                        result.push(Segment::styled(bx.mid_right.to_string(), border_style));
1344                    }
1345                }
1346                result.push(new_line.clone());
1347            }
1348
1349            // Render row separator if needed
1350            let is_last_row = row_idx == self.rows.len() - 1;
1351            let needs_separator = !is_last_row && (self.show_lines || row.end_section);
1352
1353            if let Some(ref bx) = box_chars {
1354                if needs_separator {
1355                    let row_line = bx.get_row(&widths, RowLevel::Row, self.show_edge);
1356                    result.push(Segment::styled(row_line, border_style));
1357                    result.push(new_line.clone());
1358                } else if self.leading > 0 && !is_last_row {
1359                    // Add blank lines for leading
1360                    for _ in 0..self.leading {
1361                        let row_line = bx.get_row(&widths, RowLevel::Mid, self.show_edge);
1362                        result.push(Segment::styled(row_line, border_style));
1363                        result.push(new_line.clone());
1364                    }
1365                }
1366            }
1367        }
1368
1369        // Render footer
1370        if self.show_footer {
1371            // Footer separator
1372            if let Some(ref bx) = box_chars {
1373                let row_line = bx.get_row(&widths, RowLevel::Foot, self.show_edge);
1374                result.push(Segment::styled(row_line, border_style));
1375                result.push(new_line.clone());
1376            }
1377
1378            let footer_row_style = self.footer_style;
1379
1380            let mut footer_cells: Vec<Vec<Vec<Segment>>> = Vec::new();
1381            let mut max_height = 1;
1382
1383            let empty_footer = Text::plain("");
1384            for (i, column) in self.columns.iter().enumerate() {
1385                let cell: &dyn Renderable = column
1386                    .footer
1387                    .as_ref()
1388                    .map(|b| b.as_ref())
1389                    .unwrap_or(&empty_footer as &dyn Renderable);
1390
1391                let cell_style = footer_row_style.combine(&column.footer_style);
1392                let cell_lines = self.render_cell(
1393                    console, options, cell, column, widths[i], cell_style, false, true,
1394                );
1395                max_height = max_height.max(cell_lines.len());
1396                footer_cells.push(cell_lines);
1397            }
1398
1399            // Normalize heights - use column footer style for padding
1400            for (i, cells) in footer_cells.iter_mut().enumerate() {
1401                let col_style = footer_row_style.combine(&self.columns[i].footer_style);
1402                let hint_style = cells.last().and_then(|line| Segment::get_last_style(line));
1403                while cells.len() < max_height {
1404                    // Preserve only background from the last rendered line (see header comment).
1405                    let pad_style = hint_style
1406                        .and_then(|hint| hint.bgcolor.map(|bg| Style::new().with_bgcolor(bg)))
1407                        .map(|bg_style| col_style.combine(&bg_style))
1408                        .unwrap_or(col_style);
1409                    let blank = Segment::adjust_line_length(&[], widths[i], Some(pad_style), true);
1410                    cells.push(blank);
1411                }
1412            }
1413
1414            // Render footer lines
1415            for line_idx in 0..max_height {
1416                if let Some(ref bx) = box_chars {
1417                    if self.show_edge {
1418                        result.push(Segment::styled(bx.foot_left.to_string(), border_style));
1419                    }
1420                }
1421
1422                for (col_idx, cells) in footer_cells.iter().enumerate() {
1423                    for seg in &cells[line_idx] {
1424                        result.push(seg.clone());
1425                    }
1426                    if col_idx < footer_cells.len() - 1 {
1427                        if let Some(ref bx) = box_chars {
1428                            result
1429                                .push(Segment::styled(bx.foot_vertical.to_string(), border_style));
1430                        }
1431                    }
1432                }
1433
1434                if let Some(ref bx) = box_chars {
1435                    if self.show_edge {
1436                        result.push(Segment::styled(bx.foot_right.to_string(), border_style));
1437                    }
1438                }
1439                result.push(new_line.clone());
1440            }
1441        }
1442
1443        // Render bottom border
1444        if let Some(ref bx) = box_chars {
1445            if self.show_edge {
1446                let bottom = bx.get_bottom(&widths);
1447                result.push(Segment::styled(bottom, border_style));
1448                result.push(new_line.clone());
1449            }
1450        }
1451
1452        // Render caption
1453        if let Some(ref caption) = self.caption {
1454            let mut caption_options = options.clone();
1455            caption_options.max_width = table_width;
1456            let caption_lines = console.render_lines(
1457                caption,
1458                Some(&caption_options),
1459                self.caption_style,
1460                false,
1461                false,
1462            );
1463            for line in caption_lines {
1464                let line_width = Segment::get_line_length(&line);
1465                let padding = table_width.saturating_sub(line_width);
1466
1467                let (left_pad, right_pad) = match self.caption_align {
1468                    AlignMethod::Left => (0, padding),
1469                    AlignMethod::Center => {
1470                        let left = padding / 2;
1471                        (left, padding - left)
1472                    }
1473                    AlignMethod::Right => (padding, 0),
1474                };
1475
1476                if left_pad > 0 {
1477                    result.push(Segment::new(" ".repeat(left_pad)));
1478                }
1479                for seg in line {
1480                    result.push(seg);
1481                }
1482                if right_pad > 0 {
1483                    result.push(Segment::new(" ".repeat(right_pad)));
1484                }
1485                result.push(new_line.clone());
1486            }
1487        }
1488
1489        result
1490    }
1491
1492    fn measure(&self, console: &Console<Stdout>, options: &ConsoleOptions) -> Measurement {
1493        if self.columns.is_empty() {
1494            return Measurement::new(0, 0);
1495        }
1496
1497        let max_width = self.width.unwrap_or(options.max_width);
1498        if max_width == 0 {
1499            return Measurement::new(0, 0);
1500        }
1501
1502        // Mirror Python Rich: measure columns against available column width
1503        // (excluding border / dividers), then re-measure columns under the computed
1504        // total column width to get final min/max ranges.
1505        let extra = self.extra_width();
1506        let col_options = options.update_width(max_width.saturating_sub(extra));
1507        let col_widths = self.calculate_column_widths(console, &col_options);
1508        let content_max_width: usize = col_widths.iter().sum();
1509
1510        let measurements: Vec<Measurement> = self
1511            .columns
1512            .iter()
1513            .map(|col| self.measure_column(console, &options.update_width(content_max_width), col))
1514            .collect();
1515
1516        let min_width: usize = measurements.iter().map(|m| m.minimum).sum::<usize>() + extra;
1517        let max_calc: usize = if self.width.is_some() {
1518            self.width.unwrap()
1519        } else {
1520            measurements.iter().map(|m| m.maximum).sum::<usize>() + extra
1521        };
1522
1523        Measurement::new(min_width, max_calc).clamp_bounds(self.min_width, Some(max_width))
1524    }
1525}
1526
1527// ============================================================================
1528// Width distribution helpers (ported from Python Rich)
1529// ============================================================================
1530
1531fn div_ceil(numer: u128, denom: u128) -> usize {
1532    if denom == 0 {
1533        return 0;
1534    }
1535    ((numer + denom - 1) / denom) as usize
1536}
1537
1538/// Banker's rounding (ties to even) for positive rationals.
1539///
1540/// Python's `round()` uses bankers rounding, which is important for Rich parity.
1541fn round_div_bankers(numer: u128, denom: u128) -> usize {
1542    if denom == 0 {
1543        return 0;
1544    }
1545    let q = numer / denom;
1546    let r = numer % denom;
1547    let twice_r = r.saturating_mul(2);
1548
1549    if twice_r < denom {
1550        q as usize
1551    } else if twice_r > denom {
1552        (q + 1) as usize
1553    } else {
1554        // Exactly half-way: round to even.
1555        if q % 2 == 0 {
1556            q as usize
1557        } else {
1558            (q + 1) as usize
1559        }
1560    }
1561}
1562
1563/// Equivalent of `rich._ratio.ratio_reduce`.
1564///
1565/// `total` is the number of cells to remove from `values`, proportionally to `ratios`,
1566/// with per-slot caps in `maximums`.
1567fn ratio_reduce(
1568    total: usize,
1569    ratios: &[usize],
1570    maximums: &[usize],
1571    values: &[usize],
1572) -> Vec<usize> {
1573    let mut adjusted_ratios: Vec<usize> = Vec::with_capacity(ratios.len());
1574    for (&ratio, &max) in ratios.iter().zip(maximums.iter()) {
1575        adjusted_ratios.push(if max > 0 { ratio } else { 0 });
1576    }
1577
1578    let mut total_ratio: usize = adjusted_ratios.iter().sum();
1579    if total_ratio == 0 {
1580        return values.to_vec();
1581    }
1582
1583    let mut total_remaining = total;
1584    let mut result: Vec<usize> = Vec::with_capacity(values.len());
1585
1586    for ((&ratio, &maximum), &value) in adjusted_ratios
1587        .iter()
1588        .zip(maximums.iter())
1589        .zip(values.iter())
1590    {
1591        if ratio > 0 && total_ratio > 0 {
1592            let numer = (ratio as u128) * (total_remaining as u128);
1593            let rounded = round_div_bankers(numer, total_ratio as u128);
1594            let distributed = rounded.min(maximum);
1595            result.push(value.saturating_sub(distributed));
1596            total_remaining = total_remaining.saturating_sub(distributed);
1597            total_ratio = total_ratio.saturating_sub(ratio);
1598        } else {
1599            result.push(value);
1600        }
1601    }
1602
1603    result
1604}
1605
1606/// Equivalent of `rich._ratio.ratio_distribute` (with optional minimums).
1607///
1608/// Distributes `total` into parts based on `ratios`. With `minimums`, each slot
1609/// will get at least its minimum (and slots with minimum 0 are treated as ratio 0).
1610fn ratio_distribute(total: usize, ratios: &[usize], minimums: Option<&[usize]>) -> Vec<usize> {
1611    let mut adjusted_ratios: Vec<usize> = Vec::with_capacity(ratios.len());
1612    if let Some(minimums) = minimums {
1613        for (&ratio, &min) in ratios.iter().zip(minimums.iter()) {
1614            adjusted_ratios.push(if min > 0 { ratio } else { 0 });
1615        }
1616    } else {
1617        adjusted_ratios.extend_from_slice(ratios);
1618    }
1619
1620    let mut total_ratio: usize = adjusted_ratios.iter().sum();
1621    if total_ratio == 0 {
1622        // Mirrors Rich's expectation: ratio_distribute requires sum(ratios) > 0,
1623        // but in practice we can just return zeros for robustness.
1624        return vec![0; ratios.len()];
1625    }
1626
1627    let mut total_remaining = total;
1628    let mut distributed_total: Vec<usize> = Vec::with_capacity(adjusted_ratios.len());
1629
1630    let mins: Vec<usize> = if let Some(minimums) = minimums {
1631        minimums.to_vec()
1632    } else {
1633        vec![0; adjusted_ratios.len()]
1634    };
1635
1636    for (&ratio, &minimum) in adjusted_ratios.iter().zip(mins.iter()) {
1637        let distributed = if total_ratio > 0 {
1638            let numer = (ratio as u128) * (total_remaining as u128);
1639            div_ceil(numer, total_ratio as u128).max(minimum)
1640        } else {
1641            total_remaining
1642        };
1643        distributed_total.push(distributed);
1644        total_ratio = total_ratio.saturating_sub(ratio);
1645        total_remaining = total_remaining.saturating_sub(distributed);
1646    }
1647
1648    distributed_total
1649}
1650
1651/// Equivalent of `Table._collapse_widths` from Python Rich.
1652fn collapse_widths(mut widths: Vec<usize>, wrapable: Vec<bool>, max_width: usize) -> Vec<usize> {
1653    let mut total_width: usize = widths.iter().sum();
1654    let mut excess_width = total_width.saturating_sub(max_width);
1655
1656    if wrapable.iter().any(|&w| w) {
1657        while total_width > 0 && excess_width > 0 {
1658            let max_column = widths
1659                .iter()
1660                .zip(wrapable.iter())
1661                .filter_map(|(width, allow_wrap)| allow_wrap.then_some(*width))
1662                .max()
1663                .unwrap_or(0);
1664
1665            let second_max_column = widths
1666                .iter()
1667                .zip(wrapable.iter())
1668                .filter_map(|(width, allow_wrap)| {
1669                    if *allow_wrap && *width != max_column {
1670                        Some(*width)
1671                    } else {
1672                        None
1673                    }
1674                })
1675                .max()
1676                .unwrap_or(0);
1677
1678            let column_difference = max_column.saturating_sub(second_max_column);
1679            let ratios: Vec<usize> = widths
1680                .iter()
1681                .zip(wrapable.iter())
1682                .map(|(width, allow_wrap)| {
1683                    if *allow_wrap && *width == max_column {
1684                        1
1685                    } else {
1686                        0
1687                    }
1688                })
1689                .collect();
1690
1691            if ratios.iter().all(|&r| r == 0) || column_difference == 0 {
1692                break;
1693            }
1694
1695            let max_reduce: Vec<usize> = vec![excess_width.min(column_difference); widths.len()];
1696            widths = ratio_reduce(excess_width, &ratios, &max_reduce, &widths);
1697
1698            total_width = widths.iter().sum();
1699            excess_width = total_width.saturating_sub(max_width);
1700        }
1701    }
1702
1703    widths
1704}
1705
1706// ============================================================================
1707// Tests
1708// ============================================================================
1709
1710#[cfg(test)]
1711mod tests {
1712    use super::*;
1713    use crate::r#box::{ASCII, DOUBLE, ROUNDED, SQUARE};
1714    use crate::cells::cell_len;
1715
1716    // ==================== Column tests ====================
1717
1718    #[test]
1719    fn test_column_default() {
1720        let col = Column::new();
1721        assert!(col.header.is_none());
1722        assert_eq!(col.justify, JustifyMethod::Left);
1723        assert!(!col.flexible());
1724    }
1725
1726    #[test]
1727    fn test_column_with_header_str() {
1728        let col = Column::with_header_str("Name");
1729        assert!(col.header.is_some());
1730    }
1731
1732    #[test]
1733    fn test_column_flexible() {
1734        let col = Column::new().ratio(1);
1735        assert!(col.flexible());
1736    }
1737
1738    #[test]
1739    fn test_column_builder() {
1740        let col = Column::new()
1741            .justify(JustifyMethod::Right)
1742            .width(10)
1743            .min_width(5)
1744            .max_width(20)
1745            .no_wrap(true);
1746
1747        assert_eq!(col.justify, JustifyMethod::Right);
1748        assert_eq!(col.width, Some(10));
1749        assert_eq!(col.min_width, Some(5));
1750        assert_eq!(col.max_width, Some(20));
1751        assert!(col.no_wrap);
1752    }
1753
1754    // ==================== Row tests ====================
1755
1756    #[test]
1757    fn test_row_empty() {
1758        let row = Row::empty();
1759        assert!(row.cells.is_empty());
1760        assert!(row.style.is_none());
1761        assert!(!row.end_section);
1762    }
1763
1764    #[test]
1765    fn test_row_with_style() {
1766        let style = Style::new().with_bold(true);
1767        let row = Row::empty().with_style(style);
1768        assert_eq!(row.style, Some(style));
1769    }
1770
1771    #[test]
1772    fn test_row_with_end_section() {
1773        let row = Row::empty().with_end_section(true);
1774        assert!(row.end_section);
1775    }
1776
1777    // ==================== Table construction tests ====================
1778
1779    #[test]
1780    fn test_table_new() {
1781        let table = Table::new();
1782        assert_eq!(table.column_count(), 0);
1783        assert_eq!(table.row_count(), 0);
1784        assert!(table.box_type.is_some());
1785        assert!(table.show_header);
1786    }
1787
1788    #[test]
1789    fn test_table_grid() {
1790        let table = Table::grid();
1791        assert!(table.box_type.is_none());
1792        assert!(!table.show_header);
1793        assert!(!table.show_edge);
1794    }
1795
1796    #[test]
1797    fn test_table_add_column() {
1798        let mut table = Table::new();
1799        table.add_column_str("Name");
1800        table.add_column_str("Age");
1801        assert_eq!(table.column_count(), 2);
1802    }
1803
1804    #[test]
1805    fn test_table_add_row() {
1806        let mut table = Table::new();
1807        table.add_column_str("Name");
1808        table.add_column_str("Age");
1809        table.add_row_strs(&["Alice", "30"]);
1810        table.add_row_strs(&["Bob", "25"]);
1811        assert_eq!(table.row_count(), 2);
1812    }
1813
1814    #[test]
1815    fn test_table_auto_add_columns() {
1816        let mut table = Table::new();
1817        table.add_row_strs(&["A", "B", "C"]);
1818        assert_eq!(table.column_count(), 3);
1819    }
1820
1821    #[test]
1822    fn test_table_add_section() {
1823        let mut table = Table::new();
1824        table.add_row_strs(&["A", "B"]);
1825        table.add_section();
1826        assert!(table.rows[0].end_section);
1827    }
1828
1829    // ==================== Table builder tests ====================
1830
1831    #[test]
1832    fn test_table_builder() {
1833        let table = Table::new()
1834            .with_box(Some(DOUBLE))
1835            .with_expand(true)
1836            .with_show_header(false)
1837            .with_width(50);
1838
1839        assert_eq!(table.box_type, Some(DOUBLE));
1840        assert!(table.expand);
1841        assert!(!table.show_header);
1842        assert_eq!(table.width, Some(50));
1843    }
1844
1845    #[test]
1846    fn test_table_with_title() {
1847        let table = Table::new().with_title("My Table");
1848        assert!(table.title.is_some());
1849    }
1850
1851    #[test]
1852    fn test_table_with_caption() {
1853        let table = Table::new().with_caption("Data from 2023");
1854        assert!(table.caption.is_some());
1855    }
1856
1857    #[test]
1858    fn test_table_with_styles() {
1859        let style = Style::new().with_bold(true);
1860        let table = Table::new()
1861            .with_style(style)
1862            .with_header_style(style)
1863            .with_border_style(style)
1864            .with_row_styles(vec![style]);
1865
1866        assert_eq!(table.style, style);
1867        assert_eq!(table.header_style, style);
1868        assert_eq!(table.border_style, style);
1869        assert_eq!(table.row_styles.len(), 1);
1870    }
1871
1872    // ==================== Table render tests ====================
1873
1874    #[test]
1875    fn test_table_render_empty() {
1876        let table = Table::new();
1877        let console = Console::with_options(ConsoleOptions::default());
1878        let options = console.options().clone();
1879
1880        let segments = table.render(&console, &options);
1881        let output: String = segments.iter().map(|s| s.text.to_string()).collect();
1882
1883        assert!(output.contains('\n'));
1884    }
1885
1886    #[test]
1887    fn test_table_render_basic() {
1888        let mut table = Table::new();
1889        table.add_column_str("Name");
1890        table.add_column_str("Age");
1891        table.add_row_strs(&["Alice", "30"]);
1892
1893        let console = Console::with_options(ConsoleOptions {
1894            max_width: 40,
1895            ..Default::default()
1896        });
1897        let options = console.options().clone();
1898
1899        let segments = table.render(&console, &options);
1900        let output: String = segments.iter().map(|s| s.text.to_string()).collect();
1901
1902        assert!(output.contains("Name"));
1903        assert!(output.contains("Age"));
1904        assert!(output.contains("Alice"));
1905        assert!(output.contains("30"));
1906    }
1907
1908    #[test]
1909    fn test_table_render_grid() {
1910        let mut table = Table::grid();
1911        table.add_row_strs(&["A", "B", "C"]);
1912
1913        let console = Console::with_options(ConsoleOptions {
1914            max_width: 20,
1915            ..Default::default()
1916        });
1917        let options = console.options().clone();
1918
1919        let segments = table.render(&console, &options);
1920        let output: String = segments.iter().map(|s| s.text.to_string()).collect();
1921
1922        assert!(output.contains('A'));
1923        assert!(output.contains('B'));
1924        assert!(output.contains('C'));
1925        // Grid has no borders
1926        assert!(!output.contains('┏'));
1927    }
1928
1929    #[test]
1930    fn test_table_render_with_box_styles() {
1931        let boxes = [ROUNDED, SQUARE, DOUBLE, ASCII];
1932
1933        for box_style in boxes {
1934            let mut table = Table::new().with_box(Some(box_style));
1935            table.add_column_str("X");
1936            table.add_row_strs(&["Y"]);
1937
1938            let console = Console::with_options(ConsoleOptions {
1939                max_width: 20,
1940                ..Default::default()
1941            });
1942            let options = console.options().clone();
1943
1944            let segments = table.render(&console, &options);
1945            assert!(!segments.is_empty());
1946        }
1947    }
1948
1949    #[test]
1950    fn test_table_render_no_edge() {
1951        let mut table = Table::new().with_show_edge(false);
1952        table.add_column_str("Name");
1953        table.add_row_strs(&["Alice"]);
1954
1955        let console = Console::with_options(ConsoleOptions {
1956            max_width: 30,
1957            ..Default::default()
1958        });
1959        let options = console.options().clone();
1960
1961        let segments = table.render(&console, &options);
1962        let output: String = segments.iter().map(|s| s.text.to_string()).collect();
1963
1964        // No left/right borders
1965        assert!(!output.contains("┃Name"));
1966    }
1967
1968    #[test]
1969    fn test_table_render_show_lines() {
1970        let mut table = Table::new().with_show_lines(true);
1971        table.add_column_str("Name");
1972        table.add_row_strs(&["Alice"]);
1973        table.add_row_strs(&["Bob"]);
1974
1975        let console = Console::with_options(ConsoleOptions {
1976            max_width: 30,
1977            ..Default::default()
1978        });
1979        let options = console.options().clone();
1980
1981        let segments = table.render(&console, &options);
1982        let output: String = segments.iter().map(|s| s.text.to_string()).collect();
1983        let lines: Vec<&str> = output.lines().collect();
1984
1985        // Should have separators between rows
1986        assert!(lines.len() > 4); // top + header + sep + alice + sep + bob + bottom
1987    }
1988
1989    #[test]
1990    fn test_table_render_expand() {
1991        let mut table = Table::new().with_expand(true);
1992        table.add_column_str("X");
1993        table.add_row_strs(&["Y"]);
1994
1995        let console = Console::with_options(ConsoleOptions {
1996            max_width: 40,
1997            ..Default::default()
1998        });
1999        let options = console.options().clone();
2000
2001        let segments = table.render(&console, &options);
2002        let output: String = segments.iter().map(|s| s.text.to_string()).collect();
2003        let first_line = output.lines().next().unwrap();
2004
2005        // Without expand, a single column with "X" header would be ~5 chars
2006        // With expand, it should be significantly wider (at least 30+)
2007        let width = cell_len(first_line);
2008        assert!(
2009            width >= 30,
2010            "Expanded table width {} should be >= 30",
2011            width
2012        );
2013    }
2014
2015    // ==================== Measure tests ====================
2016
2017    #[test]
2018    fn test_table_measure_empty() {
2019        let table = Table::new();
2020        let console = Console::new();
2021        let options = ConsoleOptions::default();
2022
2023        let m = table.measure(&console, &options);
2024        assert_eq!(m.minimum, 0);
2025        assert_eq!(m.maximum, 0);
2026    }
2027
2028    #[test]
2029    fn test_table_measure_basic() {
2030        let mut table = Table::new();
2031        table.add_column_str("Name");
2032        table.add_row_strs(&["Alice"]);
2033
2034        let console = Console::new();
2035        let options = ConsoleOptions::default();
2036
2037        let m = table.measure(&console, &options);
2038        assert!(m.minimum > 0);
2039        assert!(m.maximum >= m.minimum);
2040    }
2041
2042    #[test]
2043    fn test_table_measure_fixed_width() {
2044        let mut table = Table::new().with_width(50);
2045        table.add_column_str("Name");
2046        table.add_row_strs(&["Alice"]);
2047
2048        let console = Console::new();
2049        let options = ConsoleOptions::default();
2050
2051        let m = table.measure(&console, &options);
2052        assert_eq!(m.maximum, 50);
2053    }
2054
2055    // ==================== Title/caption style tests ====================
2056
2057    #[test]
2058    fn test_table_with_title_style() {
2059        let style = Style::new().with_bold(true);
2060        let table = Table::new().with_title("My Title").with_title_style(style);
2061        assert_eq!(table.title_style, Some(style));
2062    }
2063
2064    #[test]
2065    fn test_table_with_caption_style() {
2066        let style = Style::new().with_italic(true);
2067        let table = Table::new()
2068            .with_caption("My Caption")
2069            .with_caption_style(style);
2070        assert_eq!(table.caption_style, Some(style));
2071    }
2072
2073    #[test]
2074    fn test_table_four_way_padding() {
2075        use crate::padding::PaddingDimensions;
2076        let table = Table::new().with_padding_dims(PaddingDimensions::FourWay(1, 2, 1, 2));
2077        assert_eq!(table.padding, (1, 2, 1, 2));
2078    }
2079
2080    #[test]
2081    fn test_table_padding_from_usize() {
2082        let table = Table::new().with_padding_dims(3usize);
2083        assert_eq!(table.padding, (3, 3, 3, 3));
2084    }
2085
2086    #[test]
2087    fn test_table_padding_compat() {
2088        // Old 2-arg with_padding sets left/right with 0 top/bottom
2089        let table = Table::new().with_padding(2, 3);
2090        assert_eq!(table.padding, (0, 3, 0, 2));
2091    }
2092
2093    // ==================== Send + Sync tests ====================
2094
2095    #[test]
2096    fn test_table_is_send_sync() {
2097        fn assert_send<T: Send>() {}
2098        fn assert_sync<T: Sync>() {}
2099        assert_send::<Table>();
2100        assert_sync::<Table>();
2101    }
2102
2103    #[test]
2104    fn test_column_is_send_sync() {
2105        fn assert_send<T: Send>() {}
2106        fn assert_sync<T: Sync>() {}
2107        assert_send::<Column>();
2108        assert_sync::<Column>();
2109    }
2110
2111    #[test]
2112    fn test_row_is_send_sync() {
2113        fn assert_send<T: Send>() {}
2114        fn assert_sync<T: Sync>() {}
2115        assert_send::<Row>();
2116        assert_sync::<Row>();
2117    }
2118
2119    // ==================== Debug tests ====================
2120
2121    #[test]
2122    fn test_table_debug() {
2123        let mut table = Table::new().with_title("Test");
2124        table.add_column_str("A");
2125        table.add_row_strs(&["B"]);
2126
2127        let debug_str = format!("{:?}", table);
2128        assert!(debug_str.contains("Table"));
2129        assert!(debug_str.contains("columns"));
2130        assert!(debug_str.contains("rows"));
2131    }
2132
2133    #[test]
2134    fn test_column_debug() {
2135        let col = Column::with_header_str("Name");
2136        let debug_str = format!("{:?}", col);
2137        assert!(debug_str.contains("Column"));
2138    }
2139
2140    #[test]
2141    fn test_row_debug() {
2142        let row = Row::empty();
2143        let debug_str = format!("{:?}", row);
2144        assert!(debug_str.contains("Row"));
2145    }
2146}