Skip to main content

rich/
table.rs

1//! Tables.
2//!
3//! Port of upstream `rich/table.py` (core subset). A [`Table`] lays out columns
4//! and rows inside a box, sizing each column to its widest cell.
5//!
6//! Scope: headers, rows, box choice (with legacy/ASCII substitution), per-cell
7//! padding, **`pad_edge`** + **`show_edge`** + **`collapse_padding`**, header
8//! styling (incl. a per-column header-content span and a per-column header-cell
9//! fill), a **table-level style** + **border style**,
10//! multi-line/wrapped cells (with **ellipsis overflow**), **shrink-to-fit** +
11//! **expand** column widths, per-column justify, **explicit width**, per-column
12//! **`ratio`/`min_width`/`max_width`**, **per-column style**, **`no_wrap`**,
13//! title, caption, and `show_lines`. Headers and cells may be styled [`Text`]
14//! (`add_column_text`, `add_row_text`), as upstream accepts renderables; plain
15//! strings (`add_column`, `add_row`) are console markup, as upstream's `str`
16//! cells are (see [`Cell::Markup`]).
17//! Deferred (tracked in the Table issue): the rare width-0 column padding edge.
18
19use std::sync::Arc;
20
21use crate::console::{Console, ConsoleOptions, Justify, Overflow};
22use crate::measure::Measurement;
23use crate::protocol::{LineRenderable, Renderable};
24use crate::r#box::{Box as BoxSet, RowLevel, HEAVY_HEAD};
25use crate::segment::Segment;
26use crate::style::Style;
27use crate::text::{Text, DEFAULT_TAB_SIZE};
28
29/// A single column definition. Mirrors the used subset of `rich.table.Column`.
30struct Column {
31    header: Cell,
32    /// Highlight `str` cells (port of `Column.highlight`); `None` takes the
33    /// table's `highlight`, as `add_column(highlight=None)` does.
34    highlight: Option<bool>,
35    justify: Justify,
36    /// An explicit content width; when set, the column doesn't shrink to fit.
37    width: Option<usize>,
38    /// A style applied to this column's body cells.
39    style: Style,
40    /// An extra style span applied to the header *content* only (over the base
41    /// `header_style`), leaving the header padding as `header_style`. Mirrors
42    /// upstream stylizing the heading `Text` (e.g. `markdown.table.header`).
43    header_content_style: Option<Style>,
44    /// A per-column header *cell* style — combined over the table-level
45    /// `header_style` to fill the whole header cell (content + padding). Port of
46    /// `Column.header_style` (as used by e.g. rich-cli's numeric columns).
47    header_fill: Option<Style>,
48    /// When set, the column flexes to this share of the free width when the table
49    /// is `expand`ed (port of `Column.ratio`; makes the column "flexible").
50    ratio: Option<usize>,
51    /// A floor on the column's content width (port of `Column.min_width`).
52    min_width: Option<usize>,
53    /// A cap on the column's content width — wider cells wrap (port of
54    /// `Column.max_width`).
55    max_width: Option<usize>,
56    /// When set, cells are never wrapped — they crop to one line (with ellipsis).
57    no_wrap: bool,
58    /// How over-long cell text is handled (upstream `Column.overflow`,
59    /// default `"ellipsis"`). A cell `Text`'s own overflow wins.
60    overflow: Overflow,
61}
62
63/// A table cell: a markup string, styled text, or any renderable (upstream
64/// accepts all three).
65#[derive(Clone)]
66pub enum Cell {
67    /// A text cell, rendered literally (a `Text` is never re-parsed); its own
68    /// `justify`, `overflow` and `no_wrap` override the column's.
69    Text(Text),
70    /// A plain string, which upstream renders through `Console.render_str`:
71    /// console markup and emoji codes are applied, and the column's
72    /// `highlight` decides whether it is highlighted. Pass [`Cell::Text`] for
73    /// data that must stay literal.
74    Markup(String),
75    /// A renderable cell, measured with `__rich_measure__` and rendered at the
76    /// column width, as upstream's `Padding(renderable)` cell is.
77    Renderable(Arc<dyn Renderable + Send + Sync>),
78}
79
80impl From<Text> for Cell {
81    fn from(text: Text) -> Self {
82        Cell::Text(text)
83    }
84}
85
86/// A string cell is console markup, as upstream's `str` renderables are.
87impl From<&str> for Cell {
88    fn from(text: &str) -> Self {
89        Cell::Markup(text.to_string())
90    }
91}
92
93impl From<String> for Cell {
94    fn from(text: String) -> Self {
95        Cell::Markup(text)
96    }
97}
98
99impl From<&String> for Cell {
100    fn from(text: &String) -> Self {
101        Cell::Markup(text.clone())
102    }
103}
104
105impl Cell {
106    /// The cell as `Text`, or `None` for a renderable. A markup string goes
107    /// through [`Console::render_str`] with `highlight`.
108    pub(crate) fn to_text(&self, console: &Console, highlight: Option<bool>) -> Option<Text> {
109        match self {
110            Cell::Text(text) => Some(text.clone()),
111            Cell::Markup(markup) => Some(console.render_str(markup, highlight)),
112            Cell::Renderable(_) => None,
113        }
114    }
115
116    /// `Measurement.get(console, options, cell)`. A string is measured as
117    /// upstream measures a `str`: `render_str(..., highlight=False)`, then the
118    /// resulting `Text`'s `__rich_measure__`.
119    pub(crate) fn measure_cell(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
120        match self {
121            Cell::Text(text) => Measurement::get(console, options, text),
122            // A string with no `[` or `:` is its own plain text (markup and
123            // emoji leave it alone); measure it without building a `Text`.
124            Cell::Markup(markup) if !markup.contains(['[', ':']) => {
125                if options.max_width < 1 {
126                    return Measurement::new(0, 0);
127                }
128                let (minimum, maximum) = crate::text::measure_plain(markup);
129                let width = Measurement::new(minimum, maximum)
130                    .normalize()
131                    .with_maximum(options.max_width);
132                if width.maximum < 1 {
133                    Measurement::new(0, 0)
134                } else {
135                    width.normalize()
136                }
137            }
138            Cell::Markup(markup) => {
139                Measurement::get(console, options, &console.render_str(markup, Some(false)))
140            }
141            Cell::Renderable(renderable) => Measurement::get(console, options, renderable.as_ref()),
142        }
143    }
144}
145
146impl Default for Cell {
147    fn default() -> Self {
148        Cell::Text(Text::default())
149    }
150}
151
152/// Options for a column, as upstream's `Column(...)` takes them. Pass to
153/// [`Table::add_column_with`].
154#[derive(Clone, Debug)]
155pub struct ColumnOptions {
156    /// Content justification (`justify`, default left).
157    pub justify: Justify,
158    /// A fixed content width (`width`).
159    pub width: Option<usize>,
160    /// A floor on the content width (`min_width`).
161    pub min_width: Option<usize>,
162    /// A cap on the content width (`max_width`).
163    pub max_width: Option<usize>,
164    /// A share of the free width when the table expands (`ratio`).
165    pub ratio: Option<usize>,
166    /// Never wrap cells (`no_wrap`).
167    pub no_wrap: bool,
168    /// How over-long text is handled (`overflow`, default ellipsis).
169    pub overflow: Overflow,
170    /// The style of the column's body cells (`style`).
171    pub style: Style,
172}
173
174impl Default for ColumnOptions {
175    fn default() -> Self {
176        ColumnOptions {
177            justify: Justify::Left,
178            width: None,
179            min_width: None,
180            max_width: None,
181            ratio: None,
182            no_wrap: false,
183            overflow: Overflow::Ellipsis,
184            style: Style::new(),
185        }
186    }
187}
188
189/// A grid of cells rendered inside a box. Mirrors `rich.table.Table`.
190pub struct Table {
191    columns: Vec<Column>,
192    rows: Vec<Vec<Cell>>,
193    box_set: BoxSet,
194    /// `box=None`: no borders and no column dividers (see [`Table::grid`]).
195    no_box: bool,
196    show_header: bool,
197    show_lines: bool,
198    show_edge: bool,
199    pad_edge: bool,
200    collapse_padding: bool,
201    expand: bool,
202    title: Option<String>,
203    caption: Option<String>,
204    padding: (usize, usize, usize, usize),
205    header_style: Style,
206    border_style: Style,
207    style: Style,
208    /// Highlight `str` cells (port of `Table.highlight`, default `False`).
209    highlight: bool,
210}
211
212impl Default for Table {
213    fn default() -> Self {
214        Table {
215            columns: Vec::new(),
216            rows: Vec::new(),
217            box_set: HEAVY_HEAD,
218            no_box: false,
219            show_header: true,
220            show_lines: false,
221            show_edge: true,
222            pad_edge: true,
223            collapse_padding: false,
224            expand: false,
225            title: None,
226            caption: None,
227            padding: (0, 1, 0, 1),
228            header_style: Style::parse("bold").expect("valid built-in style"),
229            border_style: Style::new(),
230            style: Style::new(),
231            highlight: false,
232        }
233    }
234}
235
236impl Table {
237    pub fn new() -> Self {
238        Table::default()
239    }
240
241    /// A table with no borders, for laying out columns. Port of `Table.grid`:
242    /// `box=None`, no header or edge, `padding=0`, `collapse_padding=True` and
243    /// `pad_edge=False`.
244    pub fn grid() -> Self {
245        Table {
246            no_box: true,
247            show_header: false,
248            show_edge: false,
249            pad_edge: false,
250            collapse_padding: true,
251            padding: (0, 0, 0, 0),
252            ..Table::default()
253        }
254    }
255
256    /// Draw no borders and no column dividers (upstream `box=None`).
257    pub fn without_box(mut self) -> Self {
258        self.no_box = true;
259        self
260    }
261
262    /// Cell padding as `(top, right, bottom, left)` (upstream `padding`).
263    pub fn padding(mut self, top: usize, right: usize, bottom: usize, left: usize) -> Self {
264        self.padding = (top, right, bottom, left);
265        self
266    }
267
268    /// Choose the box-drawing set.
269    pub fn box_set(mut self, box_set: BoxSet) -> Self {
270        self.box_set = box_set;
271        self
272    }
273
274    /// Style the box border (edges + dividers). Composed over the table-level
275    /// style: `border = style + border_style`. Port of `Table(border_style=…)`.
276    pub fn border_style(mut self, style: Style) -> Self {
277        self.border_style = style;
278        self
279    }
280
281    /// Whether to render the header row.
282    pub fn show_header(mut self, show: bool) -> Self {
283        self.show_header = show;
284        self
285    }
286
287    /// Expand the table to fill the available width.
288    pub fn expand(mut self, expand: bool) -> Self {
289        self.expand = expand;
290        self
291    }
292
293    /// Draw a separator line between each body row.
294    pub fn show_lines(mut self, show: bool) -> Self {
295        self.show_lines = show;
296        self
297    }
298
299    /// Draw the outer box edges (top/bottom borders + left/right glyphs). When
300    /// off, only the internal dividers and content remain. Port of `show_edge`.
301    pub fn show_edge(mut self, show: bool) -> Self {
302        self.show_edge = show;
303        self
304    }
305
306    /// Pad the outer cell edges. When off, the first column drops its left pad
307    /// and the last column its right pad. Port of `pad_edge`.
308    pub fn pad_edge(mut self, pad: bool) -> Self {
309        self.pad_edge = pad;
310        self
311    }
312
313    /// Merge adjacent cell padding: an interior column's left pad is reduced by
314    /// the previous column's right pad. Port of `collapse_padding`.
315    pub fn collapse_padding(mut self, collapse: bool) -> Self {
316        self.collapse_padding = collapse;
317        self
318    }
319
320    /// Default style for the whole table. Upstream applies it as the base of the
321    /// border style (`border_style = style + border_style`); cell content keeps
322    /// its own styles. Port of `Table(style=…)`.
323    pub fn style(mut self, style: Style) -> Self {
324        self.style = style;
325        self
326    }
327
328    /// Highlight string cells with the console's highlighter (upstream
329    /// `Table(highlight=…)`, default off). Columns added without their own
330    /// setting use it.
331    pub fn highlight(mut self, highlight: bool) -> Self {
332        self.highlight = highlight;
333        self
334    }
335
336    /// The `(left, right)` padding for column `index` of `ncols`. Port of
337    /// `_get_padding_width` (collapse) combined with the `pad_edge` edge drops.
338    fn cell_padding(&self, index: usize, ncols: usize) -> (usize, usize) {
339        let (_, pr, _, pl) = self.padding;
340        // collapse_padding: interior columns shed the overlap with the previous
341        // column's right pad.
342        let mut left = if self.collapse_padding && index > 0 {
343            pl.saturating_sub(pr)
344        } else {
345            pl
346        };
347        let mut right = pr;
348        // pad_edge: the outer edges lose their padding.
349        if !self.pad_edge && index == 0 {
350            left = 0;
351        }
352        if !self.pad_edge && index + 1 == ncols {
353            right = 0;
354        }
355        (left, right)
356    }
357
358    /// A centered title rendered above the table.
359    pub fn title(mut self, title: impl Into<String>) -> Self {
360        self.title = Some(title.into());
361        self
362    }
363
364    /// A centered caption rendered below the table.
365    pub fn caption(mut self, caption: impl Into<String>) -> Self {
366        self.caption = Some(caption.into());
367        self
368    }
369
370    /// Add a left-justified column with the given header, which is console
371    /// markup as upstream's `add_column("[b]Name")` is.
372    pub fn add_column(&mut self, header: impl Into<String>) -> &mut Self {
373        self.add_column_justify(header, Justify::Left)
374    }
375
376    /// Add a column with an explicit justification. The header is console
377    /// markup; use [`add_column_text`](Self::add_column_text) for a literal one.
378    pub fn add_column_justify(&mut self, header: impl Into<String>, justify: Justify) -> &mut Self {
379        self.add_column_text(Text::default(), justify);
380        if let Some(column) = self.columns.last_mut() {
381            column.header = Cell::Markup(header.into());
382        }
383        self
384    }
385
386    /// Add a column whose header is a styled [`Text`], as upstream's
387    /// `add_column(header=Text(...))` does. The text's spans survive into the
388    /// header cell; its own `justify`, `overflow` and `no_wrap` override the
389    /// column's, as `Text.__rich_console__` prefers them over the options.
390    pub fn add_column_text(&mut self, header: Text, justify: Justify) -> &mut Self {
391        self.columns.push(Column {
392            header: Cell::Text(header),
393            highlight: None,
394            justify,
395            width: None,
396            style: Style::new(),
397            header_content_style: None,
398            header_fill: None,
399            ratio: None,
400            min_width: None,
401            max_width: None,
402            no_wrap: false,
403            overflow: Overflow::Ellipsis,
404        });
405        self
406    }
407
408    /// Add a column with every [`ColumnOptions`] set, as upstream's
409    /// `add_column(header, justify=…, width=…, ratio=…, …)` does.
410    pub fn add_column_with(&mut self, header: Text, options: ColumnOptions) -> &mut Self {
411        self.columns.push(Column {
412            header: Cell::Text(header),
413            highlight: None,
414            justify: options.justify,
415            width: options.width,
416            style: options.style,
417            header_content_style: None,
418            header_fill: None,
419            ratio: options.ratio,
420            min_width: options.min_width,
421            max_width: options.max_width,
422            no_wrap: options.no_wrap,
423            overflow: options.overflow,
424        });
425        self
426    }
427
428    /// Pin the most-recently-added column to an explicit content width. Content
429    /// wider than this wraps (with ellipsis overflow) instead of shrinking the
430    /// column. Chain after `add_column`.
431    pub fn column_width(&mut self, width: usize) -> &mut Self {
432        if let Some(column) = self.columns.last_mut() {
433            column.width = Some(width);
434        }
435        self
436    }
437
438    /// Give the most-recently-added column a flex `ratio`: when the table is
439    /// `expand`ed, ratio columns share the free width in proportion. Chain after
440    /// `add_column`. Port of `Column.ratio`.
441    pub fn column_ratio(&mut self, ratio: usize) -> &mut Self {
442        if let Some(column) = self.columns.last_mut() {
443            column.ratio = Some(ratio);
444        }
445        self
446    }
447
448    /// Set a minimum content width on the most-recently-added column. Chain after
449    /// `add_column`. Port of `Column.min_width`.
450    pub fn column_min_width(&mut self, min_width: usize) -> &mut Self {
451        if let Some(column) = self.columns.last_mut() {
452            column.min_width = Some(min_width);
453        }
454        self
455    }
456
457    /// Set a maximum content width on the most-recently-added column — wider
458    /// cells wrap. Chain after `add_column`. Port of `Column.max_width`.
459    pub fn column_max_width(&mut self, max_width: usize) -> &mut Self {
460        if let Some(column) = self.columns.last_mut() {
461            column.max_width = Some(max_width);
462        }
463        self
464    }
465
466    /// Apply a style to the most-recently-added column's body cells. Chain after
467    /// `add_column`.
468    pub fn column_style(&mut self, style: Style) -> &mut Self {
469        if let Some(column) = self.columns.last_mut() {
470            column.style = style;
471        }
472        self
473    }
474
475    /// Style the most-recently-added column's header *content* (the visible
476    /// characters), leaving its padding as the base `header_style`. Chain after
477    /// `add_column`. Mirrors upstream stylizing the heading `Text`.
478    pub fn column_header_style(&mut self, style: Style) -> &mut Self {
479        if let Some(column) = self.columns.last_mut() {
480            column.header_content_style = Some(style);
481        }
482        self
483    }
484
485    /// Style the most-recently-added column's whole header *cell* (content +
486    /// padding), combined over the table-level `header_style`. Chain after
487    /// `add_column`. Port of `Column.header_style`.
488    pub fn column_header_fill(&mut self, style: Style) -> &mut Self {
489        if let Some(column) = self.columns.last_mut() {
490            column.header_fill = Some(style);
491        }
492        self
493    }
494
495    /// Set how the most-recently-added column handles over-long text (upstream
496    /// `Column.overflow`, default ellipsis). Chain after `add_column`.
497    pub fn column_overflow(&mut self, overflow: Overflow) -> &mut Self {
498        if let Some(column) = self.columns.last_mut() {
499            column.overflow = overflow;
500        }
501        self
502    }
503
504    /// Set whether the most-recently-added column highlights its string cells
505    /// (upstream `Column.highlight`). Chain after `add_column`.
506    pub fn column_highlight(&mut self, highlight: bool) -> &mut Self {
507        if let Some(column) = self.columns.last_mut() {
508            column.highlight = Some(highlight);
509        }
510        self
511    }
512
513    /// Mark the most-recently-added column `no_wrap`: its cells crop to a single
514    /// line (with ellipsis) instead of wrapping. Chain after `add_column`.
515    pub fn column_no_wrap(&mut self) -> &mut Self {
516        if let Some(column) = self.columns.last_mut() {
517            column.no_wrap = true;
518        }
519        self
520    }
521
522    /// Add a row of string cells (extra cells are ignored; missing cells render
523    /// empty). Each string is console markup, as upstream's `add_row("[b]x")`
524    /// is; use [`add_row_text`](Self::add_row_text) for literal data.
525    pub fn add_row(&mut self, cells: &[&str]) -> &mut Self {
526        self.rows.push(
527            cells
528                .iter()
529                .map(|s| Cell::Markup((*s).to_string()))
530                .collect(),
531        );
532        self
533    }
534
535    /// Add a row of styled [`Text`] cells, as upstream's `add_row(Text(...))`.
536    /// Each cell keeps its spans, and its own `justify`, `overflow` and
537    /// `no_wrap` override the column's.
538    pub fn add_row_text(&mut self, cells: Vec<Text>) -> &mut Self {
539        self.rows.push(cells.into_iter().map(Cell::Text).collect());
540        self
541    }
542
543    /// Add a row of [`Cell`]s, which may be any renderable.
544    pub fn add_row_cells(&mut self, cells: Vec<Cell>) -> &mut Self {
545        self.rows.push(cells);
546        self
547    }
548
549    /// The width of the borders: `ncols - 1` dividers, plus the two outer
550    /// edges when shown; no box, no border. Port of `_extra_width`.
551    fn extra_width(&self) -> usize {
552        if self.no_box {
553            0
554        } else {
555            (if self.show_edge { 2 } else { 0 }) + self.columns.len().saturating_sub(1)
556        }
557    }
558
559    /// The column's padding width. Port of `_get_padding_width`, which (unlike
560    /// the per-cell padding of `_get_cells`) drops the left pad entirely under
561    /// `collapse_padding`.
562    fn padding_width(&self, index: usize) -> usize {
563        let (_, mut pad_right, _, mut pad_left) = self.padding;
564        if self.collapse_padding {
565            pad_left = 0;
566        }
567        if !self.pad_edge {
568            if index == 0 {
569                pad_left = 0;
570            }
571            if index + 1 == self.columns.len() {
572                pad_right = 0;
573            }
574        }
575        pad_left + pad_right
576    }
577
578    /// `Measurement.get` of one of `_get_cells`' cells: the cell wrapped in
579    /// `Padding(renderable, (0, right, 0, left))` when the table has any
580    /// padding. Port of `Padding.__rich_measure__` over the cell.
581    fn measure_padded_cell(
582        &self,
583        console: &Console,
584        options: &ConsoleOptions,
585        cell: &Cell,
586        (left, right): (usize, usize),
587    ) -> Measurement {
588        let max_width = options.max_width;
589        if max_width < 1 {
590            return Measurement::new(0, 0);
591        }
592        let (top, pr, bottom, pl) = self.padding;
593        if top == 0 && pr == 0 && bottom == 0 && pl == 0 {
594            return cell.measure_cell(console, options);
595        }
596        let extra_width = left + right;
597        let width = if max_width < extra_width + 1 {
598            Measurement::new(max_width, max_width)
599        } else {
600            let inner = cell.measure_cell(console, options);
601            Measurement::new(inner.minimum + extra_width, inner.maximum + extra_width)
602                .with_maximum(max_width)
603        };
604        // `Measurement.get` around the `Padding`.
605        let width = width.normalize().with_maximum(max_width);
606        if width.maximum < 1 {
607            Measurement::new(0, 0)
608        } else {
609            width.normalize()
610        }
611    }
612
613    /// The minimum and maximum width of column `index` (content + padding).
614    /// Port of `Table._measure_column`: every cell, header included, is
615    /// measured with `Measurement.get`, so a nested renderable (a `Table`,
616    /// `Panel`, …) sizes its column by its own `__rich_measure__`.
617    fn measure_column(
618        &self,
619        console: &Console,
620        options: &ConsoleOptions,
621        index: usize,
622    ) -> Measurement {
623        let max_width = options.max_width;
624        if max_width < 1 {
625            return Measurement::new(0, 0);
626        }
627        let column = &self.columns[index];
628        let padding_width = self.padding_width(index);
629        if let Some(width) = column.width {
630            // Fixed width column.
631            return Measurement::new(width + padding_width, width + padding_width)
632                .with_maximum(max_width);
633        }
634        // Every cell of a column shares its left/right padding; only the
635        // vertical padding depends on the row.
636        let padding = self.cell_padding(index, self.columns.len());
637        let empty = Cell::Markup(String::new());
638        let header = self.show_header.then_some(&column.header);
639        let body = self.rows.iter().map(|row| row.get(index).unwrap_or(&empty));
640        let mut measured = false;
641        let (mut minimum, mut maximum) = (0, 0);
642        for cell in header.into_iter().chain(body) {
643            let width = self.measure_padded_cell(console, options, cell, padding);
644            minimum = minimum.max(width.minimum);
645            maximum = maximum.max(width.maximum);
646            measured = true;
647        }
648        let measurement = if measured {
649            Measurement::new(minimum, maximum)
650        } else {
651            Measurement::new(1, max_width)
652        }
653        .with_maximum(max_width);
654        measurement.clamp(
655            column.min_width.map(|width| width + padding_width),
656            column.max_width.map(|width| width + padding_width),
657        )
658    }
659
660    /// The rendered width (content + padding) of each column, shrinking the
661    /// widest columns to fit `available` when necessary. Port of the non-flexible
662    /// path of `Table._calculate_column_widths` + `_collapse_widths`.
663    fn column_widths(
664        &self,
665        console: &Console,
666        options: &ConsoleOptions,
667        available: usize,
668    ) -> Vec<usize> {
669        let options = &options.update_width(available);
670        // A fixed-width column uses its declared width; others measure content,
671        // clamped to the column's [min_width, max_width]. Port of `_measure_column`.
672        let maximums: Vec<i64> = (0..self.columns.len())
673            .map(|index| self.measure_column(console, options, index).maximum as i64)
674            .collect();
675        let mut widths: Vec<i64> = maximums.iter().map(|&width| width.max(1)).collect();
676
677        // Expand with explicit ratios: flexible (ratio) columns share the free
678        // width in proportion, fixed columns keep their measured width. Port of
679        // the `if self.expand: … if any(ratios)` block of `_calculate_column_widths`.
680        if self.expand {
681            let ratios: Vec<i64> = self
682                .columns
683                .iter()
684                .filter(|c| c.ratio.is_some())
685                .map(|c| c.ratio.unwrap() as i64)
686                .collect();
687            if ratios.iter().any(|&r| r > 0) {
688                let fixed_widths: Vec<i64> = maximums
689                    .iter()
690                    .zip(&self.columns)
691                    .map(|(&w, c)| if c.ratio.is_some() { 0 } else { w })
692                    .collect();
693                let flex_minimum: Vec<i64> = self
694                    .columns
695                    .iter()
696                    .enumerate()
697                    .filter(|(_, c)| c.ratio.is_some())
698                    .map(|(index, c)| (c.width.unwrap_or(1) + self.padding_width(index)) as i64)
699                    .collect();
700                let flexible_width = available as i64 - fixed_widths.iter().sum::<i64>();
701                let flex_widths = ratio_distribute(flexible_width, &ratios, Some(&flex_minimum));
702                let mut iter_flex = flex_widths.into_iter();
703                for (index, column) in self.columns.iter().enumerate() {
704                    if column.ratio.is_some() {
705                        widths[index] = fixed_widths[index] + iter_flex.next().unwrap_or(0);
706                    }
707                }
708            }
709        }
710
711        let table_width: i64 = widths.iter().sum();
712        let collapsed = table_width > available as i64;
713        if collapsed {
714            // Only auto-width, wrapping columns may shrink; fixed and no_wrap
715            // columns hold their width (no_wrap only yields via the last resort).
716            let wrapable: Vec<bool> = self
717                .columns
718                .iter()
719                .map(|c| c.width.is_none() && !c.no_wrap)
720                .collect();
721            widths = collapse_widths(widths, &wrapable, available as i64);
722            // Last resort: if fixed columns still overflow, reduce every column
723            // evenly. Port of `_calculate_column_widths`'s final `ratio_reduce`.
724            let table_width: i64 = widths.iter().sum();
725            if table_width > available as i64 {
726                let excess = table_width - available as i64;
727                let ratios = vec![1i64; widths.len()];
728                widths = ratio_reduce(excess, &ratios, &widths, &widths);
729            }
730            // Upstream measures every column again at its reduced width, so a
731            // `min_width` re-inflates its column and the table overflows (the
732            // console crop then cuts it).
733            widths = widths
734                .iter()
735                .enumerate()
736                .map(|(index, &width)| {
737                    self.measure_column(
738                        console,
739                        &options.update_width(width.max(0) as usize),
740                        index,
741                    )
742                    .maximum as i64
743                })
744                .collect();
745        }
746
747        // Expand: distribute the leftover width proportionally. Port of the
748        // `elif … and self.expand` tail of `_calculate_column_widths` (via
749        // `ratio_distribute`), which a table that had to collapse never reaches.
750        let table_width: i64 = widths.iter().sum();
751        if !collapsed && self.expand && table_width < available as i64 && table_width > 0 {
752            let pad = ratio_distribute(available as i64 - table_width, &widths, None);
753            for (width, extra) in widths.iter_mut().zip(pad) {
754                *width += extra;
755            }
756        }
757        widths.into_iter().map(|w| w.max(0) as usize).collect()
758    }
759
760    /// `cell_padding` shrunk so that padding alone can never exceed the width
761    /// the column was actually allotted.
762    ///
763    /// When many columns compete for a narrow terminal a column can be squeezed
764    /// below its own padding. The cell then still emitted a full left and right
765    /// pad, so every such column spent two cells where its border spent one and
766    /// the content row grew wider than the table — at 29 columns in an 80-cell
767    /// terminal the row overflowed by 15 cells and was cropped, taking the
768    /// right-hand border with it while the border rows kept theirs.
769    fn cell_padding_fitted(&self, index: usize, ncols: usize, rendered: usize) -> (usize, usize) {
770        let (mut pl, mut pr) = self.cell_padding(index, ncols);
771        while pl + pr > rendered {
772            if pr > pl {
773                pr -= 1;
774            } else if pl > 0 {
775                pl -= 1;
776            } else {
777                break;
778            }
779        }
780        (pl, pr)
781    }
782
783    /// The effective style for a cell in column `index`: the header style for a
784    /// header row, else that column's own style.
785    fn cell_style(&self, index: usize, is_header: bool) -> Style {
786        if is_header {
787            // A per-column header cell style is combined over the table-level one.
788            match self.columns.get(index).and_then(|c| c.header_fill.as_ref()) {
789                Some(fill) => self.header_style.combine(fill),
790                None => self.header_style.clone(),
791            }
792        } else {
793            self.columns
794                .get(index)
795                .map(|c| c.style.clone())
796                .unwrap_or_default()
797        }
798    }
799
800    /// Pad a cell's rendered lines to its width, with the vertical padding
801    /// above and below: upstream's `Padding` around the cell. Blank rows are
802    /// one run across the whole cell, as `Padding`'s blank lines are.
803    /// The `(top, bottom)` padding of a cell in the first and/or last of the
804    /// rendered rows (header included). Port of `_get_cells`' `get_padding`:
805    /// with `collapse_padding` every row but the last keeps only
806    /// `max(0, top - bottom)` below it, and without `pad_edge` the first row
807    /// loses its top and the last row its bottom.
808    fn vertical_padding(&self, first_row: bool, last_row: bool) -> (usize, usize) {
809        let (mut top, _, mut bottom, _) = self.padding;
810        if self.collapse_padding && !last_row {
811            bottom = top.saturating_sub(bottom);
812        }
813        if !self.pad_edge {
814            if first_row {
815                top = 0;
816            }
817            if last_row {
818                bottom = 0;
819            }
820        }
821        (top, bottom)
822    }
823
824    fn pad_cell_lines(
825        &self,
826        lines: Vec<Vec<Segment>>,
827        width: usize,
828        (cpl, cpr): (usize, usize),
829        (pt, pb): (usize, usize),
830        style: &Style,
831    ) -> Vec<Vec<Segment>> {
832        let cell_fill = Some(style.clone());
833        let cell_width = cpl + width + cpr;
834        // The cell's `Padding` renders at the whole cell width, and
835        // `Console.render` yields nothing at all below width 1: no content and
836        // no vertical padding either (the row keeps its minimum height of 1).
837        if cell_width == 0 {
838            return Vec::new();
839        }
840        let blank = || vec![Segment::new(" ".repeat(cell_width), cell_fill.clone())];
841        let mut padded_lines: Vec<Vec<Segment>> = Vec::new();
842        for _ in 0..pt {
843            padded_lines.push(blank());
844        }
845        for line in &lines {
846            let mut row = Vec::new();
847            if cpl > 0 {
848                row.push(Segment::new(" ".repeat(cpl), cell_fill.clone()));
849            }
850            // The cell's segments pass through, as `Padding` yields them.
851            row.extend(Segment::adjust_line_length(line, width, cell_fill.clone()));
852            if cpr > 0 {
853                row.push(Segment::new(" ".repeat(cpr), cell_fill.clone()));
854            }
855            padded_lines.push(row);
856        }
857        for _ in 0..pb {
858            padded_lines.push(blank());
859        }
860        padded_lines
861    }
862
863    /// Render one table row (a list of cell strings) into visual lines.
864    #[allow(clippy::too_many_arguments)]
865    fn render_row(
866        &self,
867        console: &Console,
868        options: &ConsoleOptions,
869        cells: &[Cell],
870        rendered_widths: &[usize],
871        is_header: bool,
872        (first_row, last_row): (bool, bool),
873        edges: Option<(char, char, char)>,
874    ) -> Vec<Vec<Segment>> {
875        // Horizontal padding is per-column (see `cell_padding`); vertical
876        // padding depends on the row's place (see `vertical_padding`).
877        let vertical = self.vertical_padding(first_row, last_row);
878        let border = Some(self.style.combine(&self.border_style));
879        let ncols = self.columns.len();
880        // Derived here rather than by the caller so the padding used to lay the
881        // row out is the same padding the content width was reduced by.
882        let paddings: Vec<(usize, usize)> = (0..ncols)
883            .map(|index| {
884                let rendered = rendered_widths.get(index).copied().unwrap_or(0);
885                self.cell_padding_fitted(index, ncols, rendered)
886            })
887            .collect();
888        let content_widths: Vec<usize> = rendered_widths
889            .iter()
890            .zip(&paddings)
891            .map(|(w, (pl, pr))| w.saturating_sub(pl + pr))
892            .collect();
893
894        // Render each cell into padded, simplified visual lines.
895        let mut cell_lines: Vec<Vec<Vec<Segment>>> = Vec::with_capacity(ncols);
896        let mut height = 1;
897        for (index, width) in content_widths.iter().enumerate() {
898            let style = self.cell_style(index, is_header);
899            let column = self.columns.get(index);
900            let mut text = match cells.get(index) {
901                Some(Cell::Text(text)) => text.clone(),
902                // `render_options.update(highlight=column.highlight)`, then
903                // `Console.render` of a `str` calls `render_str`.
904                Some(Cell::Markup(markup)) => console.render_str(
905                    markup,
906                    Some(column.and_then(|c| c.highlight).unwrap_or(self.highlight)),
907                ),
908                None => Text::default(),
909                Some(Cell::Renderable(renderable)) => {
910                    // `console.render_lines(renderable, render_options, style)`
911                    // at the content width, with the column's justify,
912                    // no_wrap and overflow as options.
913                    let mut cell_options = options.update_width(*width);
914                    cell_options.justify = column.map_or(Justify::Left, |c| c.justify);
915                    cell_options.no_wrap = Some(column.is_some_and(|c| c.no_wrap));
916                    cell_options.overflow = Some(column.map_or(Overflow::Ellipsis, |c| c.overflow));
917                    let lines = if *width == 0 {
918                        Vec::new()
919                    } else {
920                        console.render_lines_styled(
921                            renderable.as_ref(),
922                            &cell_options,
923                            Some(&style),
924                            true,
925                        )
926                    };
927                    cell_lines.push(self.pad_cell_lines(
928                        lines,
929                        *width,
930                        paddings[index],
931                        vertical,
932                        &style,
933                    ));
934                    height = height.max(cell_lines.last().map_or(0, Vec::len));
935                    continue;
936                }
937            };
938            // Upstream renders the cell `Text` with the column's `justify`,
939            // `no_wrap` and `overflow="ellipsis"` as options, which the text's own
940            // settings override: wrap, then justify (which strips a right- or
941            // center-justified line before measuring it), then truncate.
942            let justify = match text.get_justify() {
943                Justify::Default => column.map(|c| c.justify).unwrap_or(Justify::Left),
944                own => own,
945            };
946            let overflow = text
947                .get_overflow()
948                .unwrap_or_else(|| column.map_or(Overflow::Ellipsis, |c| c.overflow));
949            let no_wrap = text
950                .get_no_wrap()
951                .unwrap_or_else(|| column.map(|c| c.no_wrap).unwrap_or(false));
952            // Header content carries its own style span over `header_style`; the
953            // justify/edge padding stays `header_style` (matches upstream).
954            if is_header {
955                if let Some(span) = column.and_then(|c| c.header_content_style.clone()) {
956                    let len = text.plain().len();
957                    text.stylize(span, 0, len);
958                }
959            }
960            // Upstream renders the cell as `Padding(renderable, …)` through
961            // `render_lines`: a zero-width content area renders no lines
962            // (`Console.render` returns nothing below width 1), while empty
963            // text still renders one blank line.
964            //
965            // The text renders on its own and the cell style is applied to the
966            // result (`render_lines(..., style=...)`), so a span keeps its own
967            // segment even where it matches the cell style: `[b]Name` under a
968            // bold header is `Name` + padding, as upstream prints it. Only
969            // equal *unstyled-cell* runs merge, which rejoins the justify
970            // padding that `Text.pad_right` would have appended to the plain.
971            let mut lines: Vec<Vec<Segment>> = if *width == 0 {
972                Vec::new()
973            } else {
974                text.render_lines_wrapped(
975                    console.theme(),
976                    &Style::new(),
977                    Some(*width),
978                    justify,
979                    overflow,
980                    no_wrap,
981                )
982                .iter()
983                .map(|line| Segment::apply_style(&Segment::simplify(line), &style))
984                .collect()
985            };
986            if lines.is_empty() && *width > 0 {
987                lines.push(Vec::new());
988            }
989            let padded_lines =
990                self.pad_cell_lines(lines, *width, paddings[index], vertical, &style);
991            height = height.max(padded_lines.len());
992            cell_lines.push(padded_lines);
993        }
994
995        // Shape every cell to the row height (#445). Upstream aligns each cell
996        // to `row_height` (the tallest cell, possibly 0) with the cell style:
997        // header cells to the bottom, body cells (vertical "top") to the top.
998        // `Segment.set_shape` then pads to `max_height` (at least 1) with an
999        // unstyled blank.
1000        let row_height = cell_lines.iter().map(Vec::len).max().unwrap_or(0);
1001        for (index, lines) in cell_lines.iter_mut().enumerate() {
1002            let (cpl, cpr) = paddings[index];
1003            let blank = " ".repeat(cpl + content_widths[index] + cpr);
1004            let filler = vec![Segment::new(
1005                blank.clone(),
1006                Some(self.cell_style(index, is_header)),
1007            )];
1008            let missing = row_height.saturating_sub(lines.len());
1009            if is_header {
1010                lines.splice(0..0, std::iter::repeat_n(filler, missing));
1011            } else {
1012                lines.extend(std::iter::repeat_n(filler, missing));
1013            }
1014            while lines.len() < height {
1015                lines.push(vec![Segment::new(blank.clone(), None)]);
1016            }
1017        }
1018
1019        let last = ncols.saturating_sub(1);
1020        let mut rows_out: Vec<Vec<Segment>> = Vec::with_capacity(height);
1021        // `r` indexes into each column's per-line vector, so a range loop is the
1022        // natural shape here (the columns are iterated with `enumerate`).
1023        #[allow(clippy::needless_range_loop)]
1024        for r in 0..height {
1025            let mut row = Vec::new();
1026            if let (Some((edge_left, _, _)), true) = (edges, self.show_edge) {
1027                row.push(Segment::new(edge_left.to_string(), border.clone()));
1028            }
1029            for (c, column_lines) in cell_lines.iter().enumerate() {
1030                row.extend(column_lines[r].clone());
1031                let Some((_, edge_vertical, edge_right)) = edges else {
1032                    continue;
1033                };
1034                if c != last {
1035                    row.push(Segment::new(edge_vertical.to_string(), border.clone()));
1036                } else if self.show_edge {
1037                    row.push(Segment::new(edge_right.to_string(), border.clone()));
1038                }
1039            }
1040            rows_out.push(row);
1041        }
1042        rows_out
1043    }
1044}
1045
1046impl LineRenderable for Table {
1047    /// Render visual lines in order without retaining the full rendered table.
1048    ///
1049    /// Like upstream's `Table.__rich_console__` / `_render` generators, this
1050    /// measures all columns first, then renders only one row block at a time.
1051    /// Lines contain styled segments without a trailing newline. The callback
1052    /// may write each line immediately; its first error stops rendering.
1053    /// The table still owns its source rows for column-width measurement.
1054    fn try_for_each_line<E>(
1055        &self,
1056        console: &Console,
1057        options: &ConsoleOptions,
1058        mut emit: impl FnMut(Vec<Segment>) -> Result<(), E>,
1059    ) -> Result<(), E> {
1060        if self.columns.is_empty() {
1061            return emit(vec![Segment::new("", None)]);
1062        }
1063        // Fall back to a terminal-safe box on legacy Windows / non-UTF-8.
1064        let box_set = self.box_set.substitute(
1065            console.legacy_windows(),
1066            console.safe_box(),
1067            console.ascii_only(),
1068        );
1069        let extra_width = self.extra_width();
1070        let available = options.max_width.saturating_sub(extra_width);
1071
1072        let rendered_widths = self.column_widths(console, options, available);
1073        let border = Some(self.style.combine(&self.border_style));
1074
1075        // Full table width (for centering title/caption): columns + borders.
1076        let table_width: usize = rendered_widths.iter().sum::<usize>() + extra_width;
1077
1078        // Title, centered above the table.
1079        if let Some(title) = self.title.as_ref().filter(|title| !title.is_empty()) {
1080            for line in render_annotation(console, options, title, "table.title", table_width) {
1081                emit(line)?;
1082            }
1083        }
1084
1085        let edge = self.show_edge;
1086        let boxed = !self.no_box;
1087        if boxed && edge {
1088            emit(vec![Segment::new(
1089                box_set.get_top(&rendered_widths, edge),
1090                border.clone(),
1091            )])?;
1092        }
1093
1094        let head_edges =
1095            boxed.then_some((box_set.head_left, box_set.head_vertical, box_set.head_right));
1096        let body_edges =
1097            boxed.then_some((box_set.mid_left, box_set.mid_vertical, box_set.mid_right));
1098
1099        if self.show_header {
1100            let headers: Vec<Cell> = self.columns.iter().map(|c| c.header.clone()).collect();
1101            for line in self.render_row(
1102                console,
1103                options,
1104                &headers,
1105                &rendered_widths,
1106                true,
1107                (true, self.rows.is_empty()),
1108                head_edges,
1109            ) {
1110                emit(line)?;
1111            }
1112            if boxed {
1113                emit(vec![Segment::new(
1114                    box_set.get_row(&rendered_widths, RowLevel::Head, edge),
1115                    border.clone(),
1116                )])?;
1117            }
1118        }
1119
1120        let row_last = self.rows.len().saturating_sub(1);
1121        for (index, row) in self.rows.iter().enumerate() {
1122            let place = (!self.show_header && index == 0, index == row_last);
1123            for line in self.render_row(
1124                console,
1125                options,
1126                row,
1127                &rendered_widths,
1128                false,
1129                place,
1130                body_edges,
1131            ) {
1132                emit(line)?;
1133            }
1134            if boxed && self.show_lines && index != row_last {
1135                emit(vec![Segment::new(
1136                    box_set.get_row(&rendered_widths, RowLevel::Row, edge),
1137                    border.clone(),
1138                )])?;
1139            }
1140        }
1141
1142        if boxed && edge {
1143            emit(vec![Segment::new(
1144                box_set.get_bottom(&rendered_widths, edge),
1145                border.clone(),
1146            )])?;
1147        }
1148
1149        // Caption, centered below the table.
1150        if let Some(caption) = self.caption.as_ref().filter(|caption| !caption.is_empty()) {
1151            for line in render_annotation(console, options, caption, "table.caption", table_width) {
1152                emit(line)?;
1153            }
1154        }
1155
1156        Ok(())
1157    }
1158}
1159
1160impl crate::protocol::OwnedTableRows for Table {
1161    fn extend_owned_rows(&mut self, rows: Vec<Vec<String>>) -> &mut Self {
1162        self.rows.extend(
1163            rows.into_iter()
1164                .map(|row| row.into_iter().map(Cell::Markup).collect::<Vec<_>>()),
1165        );
1166        self
1167    }
1168}
1169
1170impl Renderable for Table {
1171    /// Port of `Table.__rich_measure__`: the column widths the table would
1172    /// render at, then each column measured within their total.
1173    fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
1174        if self.columns.is_empty() {
1175            // `_extra_width` counts `len(columns) - 1` dividers, so an empty
1176            // boxed table measures `2 - 1` with edges and `-1` (normalized to
1177            // 0) without.
1178            let width = usize::from(!self.no_box && self.show_edge);
1179            return Measurement::new(width, width);
1180        }
1181        let extra_width = self.extra_width();
1182        let max_width: usize = self
1183            .column_widths(
1184                console,
1185                options,
1186                options.max_width.saturating_sub(extra_width),
1187            )
1188            .iter()
1189            .sum();
1190        let options = options.update_width(max_width);
1191        let (minimum, maximum) = (0..self.columns.len())
1192            .map(|index| self.measure_column(console, &options, index))
1193            .fold((0, 0), |(minimum, maximum), width| {
1194                (minimum + width.minimum, maximum + width.maximum)
1195            });
1196        Measurement::new(minimum + extra_width, maximum + extra_width)
1197    }
1198
1199    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
1200        let mut segments = Vec::new();
1201        let mut first = true;
1202        let result: Result<(), std::convert::Infallible> =
1203            self.try_for_each_line(console, options, |line| {
1204                if !first {
1205                    segments.push(Segment::line());
1206                }
1207                first = false;
1208                segments.extend(line);
1209                Ok(())
1210            });
1211        match result {
1212            Ok(()) => segments,
1213            Err(never) => match never {},
1214        }
1215    }
1216}
1217
1218/// Port of `Table.__rich_console__.render_annotation`: markup and emoji are
1219/// enabled, automatic highlighting is disabled, and long annotations wrap.
1220fn render_annotation(
1221    console: &Console,
1222    options: &ConsoleOptions,
1223    annotation: &str,
1224    style: &str,
1225    width: usize,
1226) -> Vec<Vec<Segment>> {
1227    let expanded = console.expand_emoji(annotation);
1228    let mut text = Text::from_markup(&expanded).unwrap_or_else(|_| Text::new(expanded));
1229    text.set_base_style(style);
1230    let overflow = options.overflow.unwrap_or(Overflow::Fold);
1231    let no_wrap = options.no_wrap.unwrap_or(false) || overflow == Overflow::Ignore;
1232    let mut lines = Vec::new();
1233    for mut hard_line in text.split("\n", false, true) {
1234        hard_line.expand_tabs(DEFAULT_TAB_SIZE);
1235        let wrapped = if no_wrap {
1236            vec![hard_line]
1237        } else {
1238            let char_offsets: Vec<usize> = hard_line
1239                .plain()
1240                .char_indices()
1241                .map(|(i, _)| i)
1242                .chain(std::iter::once(hard_line.plain().len()))
1243                .collect();
1244            let breaks: Vec<usize> =
1245                crate::wrap::divide_line(hard_line.plain(), width, overflow == Overflow::Fold)
1246                    .into_iter()
1247                    .map(|i| char_offsets[i])
1248                    .collect();
1249            hard_line.divide(&breaks)
1250        };
1251        for mut line in wrapped {
1252            if overflow != Overflow::Ignore {
1253                // Upstream justifies the Text before rendering its segments.
1254                // This preserves annotation span boundaries while merging the
1255                // base-styled padding with an unstyled title's single run.
1256                line.rstrip();
1257                line.truncate(width, Some(overflow), false);
1258                line.pad_left(width.saturating_sub(line.cell_len()) / 2, ' ');
1259                line.pad_right(width.saturating_sub(line.cell_len()), ' ');
1260                line.truncate(width, Some(overflow), false);
1261            }
1262            lines.push(line.render(console.theme(), console.base_style()));
1263        }
1264    }
1265    lines
1266}
1267
1268/// Round half to even (banker's rounding), matching Python's `round`.
1269fn round_half_even(value: f64) -> i64 {
1270    let floor = value.floor();
1271    let diff = value - floor;
1272    if (diff - 0.5).abs() < 1e-9 {
1273        let f = floor as i64;
1274        if f % 2 == 0 {
1275            f
1276        } else {
1277            f + 1
1278        }
1279    } else {
1280        value.round() as i64
1281    }
1282}
1283
1284/// Reduce `values` by `total`, distributed across slots by `ratios` (capped by
1285/// `maximums`). Direct port of `rich._ratio.ratio_reduce`.
1286fn ratio_reduce(total: i64, ratios: &[i64], maximums: &[i64], values: &[i64]) -> Vec<i64> {
1287    let ratios: Vec<i64> = ratios
1288        .iter()
1289        .zip(maximums)
1290        .map(|(&r, &m)| if m != 0 { r } else { 0 })
1291        .collect();
1292    let mut total_ratio: i64 = ratios.iter().sum();
1293    if total_ratio == 0 {
1294        return values.to_vec();
1295    }
1296    let mut total_remaining = total;
1297    let mut result = Vec::with_capacity(values.len());
1298    for ((&ratio, &maximum), &value) in ratios.iter().zip(maximums).zip(values) {
1299        if ratio != 0 && total_ratio > 0 {
1300            let distributed = maximum.min(round_half_even(
1301                ratio as f64 * total_remaining as f64 / total_ratio as f64,
1302            ));
1303            result.push(value - distributed);
1304            total_remaining -= distributed;
1305            total_ratio -= ratio;
1306        } else {
1307            result.push(value);
1308        }
1309    }
1310    result
1311}
1312
1313/// Divide `total` across slots proportionally to `ratios` (ceil each share),
1314/// each share floored at the matching `minimums` entry when given. Port of
1315/// `rich._ratio.ratio_distribute`.
1316fn ratio_distribute(total: i64, ratios: &[i64], minimums: Option<&[i64]>) -> Vec<i64> {
1317    // Upstream zeroes the ratio of any slot whose minimum is 0 (falsy).
1318    let ratios: Vec<i64> = match minimums {
1319        Some(mins) => ratios
1320            .iter()
1321            .zip(mins)
1322            .map(|(&r, &m)| if m != 0 { r } else { 0 })
1323            .collect(),
1324        None => ratios.to_vec(),
1325    };
1326    let mut total_ratio: i64 = ratios.iter().sum();
1327    let mut total_remaining = total;
1328    let mut result = Vec::with_capacity(ratios.len());
1329    for (index, &ratio) in ratios.iter().enumerate() {
1330        let minimum = minimums.map_or(0, |m| m[index]);
1331        let distributed = if total_ratio > 0 {
1332            // ceil(ratio * total_remaining / total_ratio) for positive values,
1333            // then floored at `minimum`.
1334            let numerator = ratio * total_remaining;
1335            let ceil_div = (numerator + total_ratio - 1) / total_ratio;
1336            minimum.max(ceil_div)
1337        } else {
1338            total_remaining
1339        };
1340        result.push(distributed);
1341        total_ratio -= ratio;
1342        total_remaining -= distributed;
1343    }
1344    result
1345}
1346
1347/// Reduce `widths` so their total is under `max_width`, shrinking the widest
1348/// wrapable columns first. Direct port of `Table._collapse_widths`.
1349fn collapse_widths(mut widths: Vec<i64>, wrapable: &[bool], max_width: i64) -> Vec<i64> {
1350    let mut total_width: i64 = widths.iter().sum();
1351    let mut excess_width = total_width - max_width;
1352    if wrapable.iter().any(|&w| w) {
1353        while total_width != 0 && excess_width > 0 {
1354            let max_column = widths
1355                .iter()
1356                .zip(wrapable)
1357                .filter(|(_, &w)| w)
1358                .map(|(&x, _)| x)
1359                .max()
1360                .unwrap_or(0);
1361            let second_max_column = widths
1362                .iter()
1363                .zip(wrapable)
1364                .map(|(&x, &w)| if w && x != max_column { x } else { 0 })
1365                .max()
1366                .unwrap_or(0);
1367            let column_difference = max_column - second_max_column;
1368            let ratios: Vec<i64> = widths
1369                .iter()
1370                .zip(wrapable)
1371                .map(|(&x, &w)| i64::from(x == max_column && w))
1372                .collect();
1373            if !ratios.iter().any(|&r| r != 0) || column_difference == 0 {
1374                break;
1375            }
1376            let max_reduce = vec![excess_width.min(column_difference); widths.len()];
1377            widths = ratio_reduce(excess_width, &ratios, &max_reduce, &widths);
1378            total_width = widths.iter().sum();
1379            excess_width = total_width - max_width;
1380        }
1381    }
1382    widths
1383}
1384
1385#[cfg(test)]
1386mod tests {
1387    use super::*;
1388    use crate::color::ColorSystem;
1389    use crate::r#box::SQUARE;
1390
1391    fn console() -> Console {
1392        Console::builder()
1393            .force_terminal(true)
1394            .color_system(Some(ColorSystem::Truecolor))
1395            .width(40)
1396            .no_color(false)
1397            .build()
1398    }
1399
1400    #[test]
1401    fn owned_rows_preserve_measurement_styles_and_missing_cells() {
1402        use crate::protocol::OwnedTableRows;
1403        for width in [1, 12, 40, 80] {
1404            let console = Console::builder().width(width).force_terminal(true).build();
1405            let build = || {
1406                let mut table = Table::new()
1407                    .title("Rows")
1408                    .caption("owned or borrowed")
1409                    .show_lines(true);
1410                table.add_column("Name");
1411                table.add_column_justify("Value", Justify::Right);
1412                table
1413            };
1414            let mut borrowed = build();
1415            let mut owned = build();
1416            for row in [
1417                vec!["漢字\n🙂", "123"],
1418                vec!["short"],
1419                vec!["extra", "4", "ignored"],
1420            ] {
1421                borrowed.add_row(&row);
1422                owned.extend_owned_rows(vec![row.into_iter().map(str::to_owned).collect()]);
1423            }
1424            assert_eq!(
1425                console.render_to_string(&borrowed),
1426                console.render_to_string(&owned)
1427            );
1428        }
1429    }
1430
1431    #[test]
1432    fn simple_square_table() {
1433        let mut table = Table::new().box_set(SQUARE);
1434        table.add_column("Name");
1435        table.add_column("Age");
1436        table.add_row(&["Alice", "30"]);
1437        table.add_row(&["Bob", "7"]);
1438        let out = console().render_export(&table);
1439        let expected = concat!(
1440            "┌───────┬─────┐\n",
1441            "│\x1b[1m \x1b[0m\x1b[1mName \x1b[0m\x1b[1m \x1b[0m│\x1b[1m \x1b[0m\x1b[1mAge\x1b[0m\x1b[1m \x1b[0m│\n",
1442            "├───────┼─────┤\n",
1443            "│ Alice │ 30  │\n",
1444            "│ Bob   │ 7   │\n",
1445            "└───────┴─────┘\n",
1446        );
1447        assert_eq!(out, expected);
1448    }
1449
1450    #[test]
1451    fn streamed_lines_match_styled_table_output() {
1452        let mut table = Table::new().box_set(SQUARE);
1453        table.add_column("Name");
1454        table.add_column("Age");
1455        table.add_row(&["Alice", "30"]);
1456        table.add_row(&["Bob", "7"]);
1457        let console = console();
1458        let mut streamed = String::new();
1459        table
1460            .try_for_each_line(&console, &console.options(), |line| {
1461                assert!(line.iter().all(|segment| !segment.text.contains('\n')));
1462                streamed.push_str(&console.segments_to_string(&line));
1463                streamed.push('\n');
1464                Ok::<_, std::convert::Infallible>(())
1465            })
1466            .unwrap();
1467        // `simple_square_table` above fixes these bytes independently of the
1468        // collection path, including distinct header-style segments.
1469        assert_eq!(streamed, console.render_export(&table));
1470        assert_eq!(streamed.lines().count(), 6);
1471    }
1472
1473    #[test]
1474    fn streamed_lines_stop_at_the_first_writer_error() {
1475        let mut table = Table::new()
1476            .box_set(SQUARE)
1477            .title("People")
1478            .caption("End")
1479            .show_lines(true);
1480        table.add_column("Name");
1481        table.add_row(&["Alice\nBob"]);
1482        table.add_row(&["Carol"]);
1483        let console = console();
1484        let mut visits = 0;
1485        let result = table.try_for_each_line(&console, &console.options(), |_| {
1486            visits += 1;
1487            if visits == 5 {
1488                Err("writer failed")
1489            } else {
1490                Ok(())
1491            }
1492        });
1493        assert_eq!(result, Err("writer failed"));
1494        assert_eq!(visits, 5);
1495    }
1496
1497    /// A column squeezed below its own padding still emitted a full left and
1498    /// right pad, so each such column spent two cells where its border spent
1499    /// one. The content row then overflowed the table and was cropped, losing
1500    /// its right-hand border while the border rows kept theirs.
1501    #[test]
1502    fn a_column_narrower_than_its_padding_stays_inside_the_border() {
1503        for ncols in [20usize, 29, 40] {
1504            let mut table = Table::new().box_set(SQUARE);
1505            for i in 0..ncols {
1506                table.add_column(format!("c{i}"));
1507            }
1508            let row: Vec<String> = (0..ncols).map(|i| i.to_string()).collect();
1509            table.add_row(&row.iter().map(String::as_str).collect::<Vec<_>>());
1510            let console = Console::builder().width(80).color_system(None).build();
1511            let out = console.render_to_string(&table);
1512            let rows: Vec<&str> = out.lines().filter(|l| !l.trim().is_empty()).collect();
1513            let widths: Vec<usize> = rows.iter().map(|r| r.chars().count()).collect();
1514            assert!(
1515                widths.iter().all(|w| *w == widths[0]),
1516                "{ncols} columns produced ragged rows: {widths:?}"
1517            );
1518            for (index, row) in rows.iter().enumerate() {
1519                let last = row.chars().last().expect("non-empty row");
1520                assert!(
1521                    !last.is_whitespace(),
1522                    "{ncols} columns: row {index} lost its right border: {row:?}"
1523                );
1524            }
1525        }
1526    }
1527
1528    /// A cell spanning several lines occupies its WIDEST line. Measuring the raw
1529    /// string made it as wide as all its lines summed — `\n` measures zero, so
1530    /// nothing capped it — and a quoted CSV cell holding two sentences blew its
1531    /// column out to 31 cells where upstream gives 23.
1532    #[test]
1533    fn a_multi_line_cell_is_measured_by_its_widest_line() {
1534        let mut table = Table::new().box_set(SQUARE);
1535        table.add_column("name");
1536        table.add_column("bio");
1537        table.add_row(&["Alice", "line one\nline two is much longer"]);
1538        table.add_row(&["Bob", "short"]);
1539        let console = Console::builder().width(60).color_system(None).build();
1540        let out = console.render_to_string(&table);
1541        let top = out.lines().next().expect("a top border");
1542        let width = top.chars().count();
1543        // "line two is much longer" is 23 cells; summing both lines would be 31.
1544        assert!(
1545            width < 40,
1546            "the multi-line cell was measured as the sum of its lines: {width} wide"
1547        );
1548        assert!(
1549            out.contains("line two is much longer"),
1550            "content lost: {out:?}"
1551        );
1552    }
1553}