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`; the table-level `width` / `min_width`,
14//! footers (`show_footer`, column `footer` / `footer_style`), `leading`,
15//! alternating `row_styles`, per-row `style` and sections (`add_section`,
16//! `end_section`), `header_style` / `footer_style` / `title_style` /
17//! `caption_style`, `title_justify` / `caption_justify` and `safe_box`.
18//! A row with more cells than columns adds columns, as upstream's `add_row`
19//! does. Headers and cells may be styled [`Text`]
20//! (`add_column_text`, `add_row_text`), as upstream accepts renderables; plain
21//! strings (`add_column`, `add_row`) are console markup, as upstream's `str`
22//! cells are (see [`Cell::Markup`]).
23//! Deferred (tracked in the Table issue): the rare width-0 column padding edge.
24
25use std::sync::Arc;
26
27use crate::console::{Console, ConsoleOptions, Justify, Overflow};
28use crate::measure::Measurement;
29use crate::protocol::{LineRenderable, Renderable};
30use crate::r#box::{Box as BoxSet, RowLevel, HEAVY_HEAD};
31use crate::segment::Segment;
32use crate::style::{Style, StyleType};
33use crate::text::Text;
34
35/// A single column definition. Mirrors the used subset of `rich.table.Column`.
36struct Column {
37    header: Cell,
38    /// The footer cell (port of `Column.footer`), shown with `show_footer`.
39    footer: Cell,
40    /// A per-column footer *cell* style, combined over the table-level
41    /// `footer_style` (port of `Column.footer_style`).
42    footer_fill: Option<Style>,
43    /// Vertical alignment of body cells (port of `Column.vertical`).
44    vertical: crate::align::VerticalAlign,
45    /// Highlight `str` cells (port of `Column.highlight`); `None` takes the
46    /// table's `highlight`, as `add_column(highlight=None)` does.
47    highlight: Option<bool>,
48    justify: Justify,
49    /// An explicit content width; when set, the column doesn't shrink to fit.
50    width: Option<usize>,
51    /// A style applied to this column's body cells.
52    style: Style,
53    /// An extra style span applied to the header *content* only (over the base
54    /// `header_style`), leaving the header padding as `header_style`. Mirrors
55    /// upstream stylizing the heading `Text` (e.g. `markdown.table.header`).
56    header_content_style: Option<Style>,
57    /// A per-column header *cell* style — combined over the table-level
58    /// `header_style` to fill the whole header cell (content + padding). Port of
59    /// `Column.header_style` (as used by e.g. rich-cli's numeric columns).
60    header_fill: Option<Style>,
61    /// When set, the column flexes to this share of the free width when the table
62    /// is `expand`ed (port of `Column.ratio`; makes the column "flexible").
63    ratio: Option<usize>,
64    /// A floor on the column's content width (port of `Column.min_width`).
65    min_width: Option<usize>,
66    /// A cap on the column's content width — wider cells wrap (port of
67    /// `Column.max_width`).
68    max_width: Option<usize>,
69    /// When set, cells are never wrapped — they crop to one line (with ellipsis).
70    no_wrap: bool,
71    /// How over-long cell text is handled (upstream `Column.overflow`,
72    /// default `"ellipsis"`). A cell `Text`'s own overflow wins.
73    overflow: Overflow,
74}
75
76impl Column {
77    /// A column with every upstream default and the given header / justify.
78    fn new(header: Cell, justify: Justify) -> Self {
79        Column {
80            header,
81            footer: Cell::Markup(String::new()),
82            footer_fill: None,
83            highlight: None,
84            vertical: crate::align::VerticalAlign::Top,
85            justify,
86            width: None,
87            style: Style::new(),
88            header_content_style: None,
89            header_fill: None,
90            ratio: None,
91            min_width: None,
92            max_width: None,
93            no_wrap: false,
94            overflow: Overflow::Ellipsis,
95        }
96    }
97}
98
99/// A row's cells and the row's own options. Port of `rich.table.Row` (which
100/// upstream keeps apart from the cells, stored per column).
101struct Row {
102    cells: Vec<Cell>,
103    /// A style for the whole row (`add_row(..., style=…)`).
104    style: Option<StyleType>,
105    /// Draw a line beneath the row (`end_section`).
106    end_section: bool,
107}
108
109/// Which kind of row is being rendered: the header, a body row (with its
110/// resolved row style) or the footer.
111#[derive(Clone)]
112enum RowKind {
113    Header,
114    Body(Style),
115    Footer,
116}
117
118/// A table cell: a markup string, styled text, or any renderable (upstream
119/// accepts all three).
120#[derive(Clone)]
121pub enum Cell {
122    /// A text cell, rendered literally (a `Text` is never re-parsed); its own
123    /// `justify`, `overflow` and `no_wrap` override the column's.
124    Text(Text),
125    /// A plain string, which upstream renders through `Console.render_str`:
126    /// console markup and emoji codes are applied, and the column's
127    /// `highlight` decides whether it is highlighted. Pass [`Cell::Text`] for
128    /// data that must stay literal.
129    Markup(String),
130    /// A renderable cell, measured with `__rich_measure__` and rendered at the
131    /// column width, as upstream's `Padding(renderable)` cell is.
132    Renderable(Arc<dyn Renderable + Send + Sync>),
133}
134
135impl From<Text> for Cell {
136    fn from(text: Text) -> Self {
137        Cell::Text(text)
138    }
139}
140
141/// A string cell is console markup, as upstream's `str` renderables are.
142impl From<&str> for Cell {
143    fn from(text: &str) -> Self {
144        Cell::Markup(text.to_string())
145    }
146}
147
148impl From<String> for Cell {
149    fn from(text: String) -> Self {
150        Cell::Markup(text)
151    }
152}
153
154impl From<&String> for Cell {
155    fn from(text: &String) -> Self {
156        Cell::Markup(text.clone())
157    }
158}
159
160impl Cell {
161    /// The cell as `Text`, or `None` for a renderable. A markup string goes
162    /// through [`Console::render_str`] with `highlight`.
163    pub(crate) fn to_text(&self, console: &Console, highlight: Option<bool>) -> Option<Text> {
164        match self {
165            Cell::Text(text) => Some(text.clone()),
166            Cell::Markup(markup) => Some(console.render_str(markup, highlight)),
167            Cell::Renderable(_) => None,
168        }
169    }
170
171    /// `Measurement.get(console, options, cell)`. A string is measured as
172    /// upstream measures a `str`: `render_str(..., highlight=False)`, then the
173    /// resulting `Text`'s `__rich_measure__`.
174    pub(crate) fn measure_cell(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
175        match self {
176            Cell::Text(text) => Measurement::get(console, options, text),
177            // A string with no `[` or `:` is its own plain text (markup and
178            // emoji leave it alone); measure it without building a `Text`.
179            Cell::Markup(markup) if !markup.contains(['[', ':']) => {
180                if options.max_width < 1 {
181                    return Measurement::new(0, 0);
182                }
183                let (minimum, maximum) = crate::text::measure_plain(markup);
184                let width = Measurement::new(minimum, maximum)
185                    .normalize()
186                    .with_maximum(options.max_width);
187                if width.maximum < 1 {
188                    Measurement::new(0, 0)
189                } else {
190                    width.normalize()
191                }
192            }
193            Cell::Markup(markup) => {
194                Measurement::get(console, options, &console.render_str(markup, Some(false)))
195            }
196            Cell::Renderable(renderable) => Measurement::get(console, options, renderable.as_ref()),
197        }
198    }
199}
200
201impl Default for Cell {
202    fn default() -> Self {
203        Cell::Text(Text::default())
204    }
205}
206
207/// Options for a column, as upstream's `Column(...)` takes them. Pass to
208/// [`Table::add_column_with`].
209#[derive(Clone, Debug)]
210pub struct ColumnOptions {
211    /// Content justification (`justify`, default left).
212    pub justify: Justify,
213    /// A fixed content width (`width`).
214    pub width: Option<usize>,
215    /// A floor on the content width (`min_width`).
216    pub min_width: Option<usize>,
217    /// A cap on the content width (`max_width`).
218    pub max_width: Option<usize>,
219    /// A share of the free width when the table expands (`ratio`).
220    pub ratio: Option<usize>,
221    /// Never wrap cells (`no_wrap`).
222    pub no_wrap: bool,
223    /// How over-long text is handled (`overflow`, default ellipsis).
224    pub overflow: Overflow,
225    /// The style of the column's body cells (`style`).
226    pub style: Style,
227}
228
229impl Default for ColumnOptions {
230    fn default() -> Self {
231        ColumnOptions {
232            justify: Justify::Left,
233            width: None,
234            min_width: None,
235            max_width: None,
236            ratio: None,
237            no_wrap: false,
238            overflow: Overflow::Ellipsis,
239            style: Style::new(),
240        }
241    }
242}
243
244/// A grid of cells rendered inside a box. Mirrors `rich.table.Table`.
245pub struct Table {
246    columns: Vec<Column>,
247    rows: Vec<Row>,
248    box_set: BoxSet,
249    /// `box=None`: no borders and no column dividers (see [`Table::grid`]).
250    no_box: bool,
251    show_header: bool,
252    show_lines: bool,
253    show_edge: bool,
254    pad_edge: bool,
255    collapse_padding: bool,
256    expand: bool,
257    /// The title: a markup string or a [`Text`] (upstream `TextType`).
258    title: Option<Cell>,
259    caption: Option<Cell>,
260    padding: (usize, usize, usize, usize),
261    /// The header row's style (upstream default `"table.header"`).
262    header_style: StyleType,
263    /// The footer row's style (upstream default `"table.footer"`).
264    footer_style: StyleType,
265    border_style: Style,
266    style: Style,
267    /// Highlight `str` cells (port of `Table.highlight`, default `False`).
268    highlight: bool,
269    /// The table's width, borders included; setting it implies `expand`.
270    width: Option<usize>,
271    /// The table's minimum width, borders included.
272    min_width: Option<usize>,
273    show_footer: bool,
274    /// Blank lines between rows (drawn with the box's `mid` row).
275    leading: usize,
276    /// Styles that body rows cycle through.
277    row_styles: Vec<StyleType>,
278    /// `None` (the default) takes `table.title` / `table.caption`.
279    title_style: Option<StyleType>,
280    caption_style: Option<StyleType>,
281    title_justify: Justify,
282    caption_justify: Justify,
283    /// Substitute boxes a legacy Windows console cannot draw; `None` takes
284    /// the console's `safe_box`.
285    safe_box: Option<bool>,
286}
287
288impl Default for Table {
289    fn default() -> Self {
290        Table {
291            columns: Vec::new(),
292            rows: Vec::new(),
293            box_set: HEAVY_HEAD,
294            no_box: false,
295            show_header: true,
296            show_lines: false,
297            show_edge: true,
298            pad_edge: true,
299            collapse_padding: false,
300            expand: false,
301            title: None,
302            caption: None,
303            padding: (0, 1, 0, 1),
304            header_style: StyleType::Name("table.header".to_string()),
305            footer_style: StyleType::Name("table.footer".to_string()),
306            border_style: Style::new(),
307            style: Style::new(),
308            highlight: false,
309            width: None,
310            min_width: None,
311            show_footer: false,
312            leading: 0,
313            row_styles: Vec::new(),
314            title_style: None,
315            caption_style: None,
316            title_justify: Justify::Center,
317            caption_justify: Justify::Center,
318            safe_box: None,
319        }
320    }
321}
322
323impl Table {
324    pub fn new() -> Self {
325        Table::default()
326    }
327
328    /// A table with no borders, for laying out columns. Port of `Table.grid`:
329    /// `box=None`, no header or edge, `padding=0`, `collapse_padding=True` and
330    /// `pad_edge=False`.
331    pub fn grid() -> Self {
332        Table {
333            no_box: true,
334            show_header: false,
335            show_edge: false,
336            pad_edge: false,
337            collapse_padding: true,
338            padding: (0, 0, 0, 0),
339            ..Table::default()
340        }
341    }
342
343    /// Draw no borders and no column dividers (upstream `box=None`).
344    pub fn without_box(mut self) -> Self {
345        self.no_box = true;
346        self
347    }
348
349    /// Cell padding as `(top, right, bottom, left)` (upstream `padding`).
350    pub fn padding(mut self, top: usize, right: usize, bottom: usize, left: usize) -> Self {
351        self.padding = (top, right, bottom, left);
352        self
353    }
354
355    /// Choose the box-drawing set.
356    pub fn box_set(mut self, box_set: BoxSet) -> Self {
357        self.box_set = box_set;
358        self
359    }
360
361    /// Style the box border (edges + dividers). Composed over the table-level
362    /// style: `border = style + border_style`. Port of `Table(border_style=…)`.
363    pub fn border_style(mut self, style: Style) -> Self {
364        self.border_style = style;
365        self
366    }
367
368    /// Whether to render the header row.
369    pub fn show_header(mut self, show: bool) -> Self {
370        self.show_header = show;
371        self
372    }
373
374    /// Expand the table to fill the available width.
375    pub fn expand(mut self, expand: bool) -> Self {
376        self.expand = expand;
377        self
378    }
379
380    /// Draw a separator line between each body row.
381    pub fn show_lines(mut self, show: bool) -> Self {
382        self.show_lines = show;
383        self
384    }
385
386    /// Draw the outer box edges (top/bottom borders + left/right glyphs). When
387    /// off, only the internal dividers and content remain. Port of `show_edge`.
388    pub fn show_edge(mut self, show: bool) -> Self {
389        self.show_edge = show;
390        self
391    }
392
393    /// Pad the outer cell edges. When off, the first column drops its left pad
394    /// and the last column its right pad. Port of `pad_edge`.
395    pub fn pad_edge(mut self, pad: bool) -> Self {
396        self.pad_edge = pad;
397        self
398    }
399
400    /// Merge adjacent cell padding: an interior column's left pad is reduced by
401    /// the previous column's right pad. Port of `collapse_padding`.
402    pub fn collapse_padding(mut self, collapse: bool) -> Self {
403        self.collapse_padding = collapse;
404        self
405    }
406
407    /// Default style for the whole table. Upstream applies it as the base of the
408    /// border style (`border_style = style + border_style`); cell content keeps
409    /// its own styles. Port of `Table(style=…)`.
410    pub fn style(mut self, style: Style) -> Self {
411        self.style = style;
412        self
413    }
414
415    /// Highlight string cells with the console's highlighter (upstream
416    /// `Table(highlight=…)`, default off). Columns added without their own
417    /// setting use it.
418    pub fn highlight(mut self, highlight: bool) -> Self {
419        self.highlight = highlight;
420        self
421    }
422
423    /// The `(left, right)` padding for column `index` of `ncols`. Port of
424    /// `_get_padding_width` (collapse) combined with the `pad_edge` edge drops.
425    fn cell_padding(&self, index: usize, ncols: usize) -> (usize, usize) {
426        let (_, pr, _, pl) = self.padding;
427        // collapse_padding: interior columns shed the overlap with the previous
428        // column's right pad.
429        let mut left = if self.collapse_padding && index > 0 {
430            pl.saturating_sub(pr)
431        } else {
432            pl
433        };
434        let mut right = pr;
435        // pad_edge: the outer edges lose their padding.
436        if !self.pad_edge && index == 0 {
437            left = 0;
438        }
439        if !self.pad_edge && index + 1 == ncols {
440            right = 0;
441        }
442        (left, right)
443    }
444
445    /// A title rendered above the table (console markup, centered by
446    /// default).
447    pub fn title(mut self, title: impl Into<String>) -> Self {
448        self.title = Some(Cell::Markup(title.into()));
449        self
450    }
451
452    /// A caption rendered below the table (console markup).
453    pub fn caption(mut self, caption: impl Into<String>) -> Self {
454        self.caption = Some(Cell::Markup(caption.into()));
455        self
456    }
457
458    /// A [`Text`] title (upstream `title=Text(...)`): rendered as it is, in
459    /// its own style and justify, with no `title_style`.
460    pub fn title_text(mut self, title: Text) -> Self {
461        self.title = Some(Cell::Text(title));
462        self
463    }
464
465    /// A [`Text`] caption (upstream `caption=Text(...)`).
466    pub fn caption_text(mut self, caption: Text) -> Self {
467        self.caption = Some(Cell::Text(caption));
468        self
469    }
470
471    /// The style of a markup title (upstream `title_style`; default
472    /// `table.title`).
473    pub fn title_style(mut self, style: impl Into<StyleType>) -> Self {
474        self.title_style = Some(style.into());
475        self
476    }
477
478    /// The style of a markup caption (upstream `caption_style`; default
479    /// `table.caption`).
480    pub fn caption_style(mut self, style: impl Into<StyleType>) -> Self {
481        self.caption_style = Some(style.into());
482        self
483    }
484
485    /// How the title is justified (upstream `title_justify`, default center).
486    pub fn title_justify(mut self, justify: Justify) -> Self {
487        self.title_justify = justify;
488        self
489    }
490
491    /// How the caption is justified (upstream `caption_justify`).
492    pub fn caption_justify(mut self, justify: Justify) -> Self {
493        self.caption_justify = justify;
494        self
495    }
496
497    /// The header row's style (upstream `header_style`, default
498    /// `table.header`). A column's own header style combines over it.
499    pub fn header_style(mut self, style: impl Into<StyleType>) -> Self {
500        self.header_style = style.into();
501        self
502    }
503
504    /// The footer row's style (upstream `footer_style`, default
505    /// `table.footer`).
506    pub fn footer_style(mut self, style: impl Into<StyleType>) -> Self {
507        self.footer_style = style.into();
508        self
509    }
510
511    /// The table's width, borders included (upstream `width`). Setting it
512    /// expands the table to exactly that width.
513    pub fn width(mut self, width: Option<usize>) -> Self {
514        self.width = width;
515        self
516    }
517
518    /// The table's minimum width, borders included (upstream `min_width`).
519    pub fn min_width(mut self, min_width: Option<usize>) -> Self {
520        self.min_width = min_width;
521        self
522    }
523
524    /// Render a footer row from each column's `footer` (upstream
525    /// `show_footer`).
526    pub fn show_footer(mut self, show: bool) -> Self {
527        self.show_footer = show;
528        self
529    }
530
531    /// Blank lines between rows (upstream `leading`); takes precedence over
532    /// `show_lines`.
533    pub fn leading(mut self, leading: usize) -> Self {
534        self.leading = leading;
535        self
536    }
537
538    /// Styles body rows cycle through (upstream `row_styles`).
539    pub fn row_styles(mut self, styles: Vec<StyleType>) -> Self {
540        self.row_styles = styles;
541        self
542    }
543
544    /// Whether to substitute boxes a legacy Windows console cannot draw
545    /// (upstream `safe_box`; `None` takes the console's).
546    pub fn safe_box(mut self, safe_box: Option<bool>) -> Self {
547        self.safe_box = safe_box;
548        self
549    }
550
551    /// Whether the table expands: `expand`, or any explicit `width`. Port
552    /// of the `Table.expand` property.
553    pub fn is_expand(&self) -> bool {
554        self.expand || self.width.is_some()
555    }
556
557    /// The number of rows (upstream `row_count`).
558    pub fn row_count(&self) -> usize {
559        self.rows.len()
560    }
561
562    /// The number of columns.
563    pub fn column_count(&self) -> usize {
564        self.columns.len()
565    }
566
567    /// The style of body row `index`: its place in `row_styles`, then its
568    /// own style. Port of `Table.get_row_style`.
569    pub fn get_row_style(&self, console: &Console, index: usize) -> Style {
570        let theme = console.theme();
571        let mut style = Style::new();
572        if !self.row_styles.is_empty() {
573            style = style
574                .combine(&theme.get_style_or_null(&self.row_styles[index % self.row_styles.len()]));
575        }
576        if let Some(row_style) = self.rows.get(index).and_then(|row| row.style.as_ref()) {
577            style = style.combine(&theme.get_style_or_null(row_style));
578        }
579        style
580    }
581
582    /// End the current section: draw a line beneath the last row. Port of
583    /// `Table.add_section`.
584    pub fn add_section(&mut self) -> &mut Self {
585        if let Some(row) = self.rows.last_mut() {
586            row.end_section = true;
587        }
588        self
589    }
590
591    /// Add a left-justified column with the given header, which is console
592    /// markup as upstream's `add_column("[b]Name")` is.
593    pub fn add_column(&mut self, header: impl Into<String>) -> &mut Self {
594        self.add_column_justify(header, Justify::Left)
595    }
596
597    /// Add a column with an explicit justification. The header is console
598    /// markup; use [`add_column_text`](Self::add_column_text) for a literal one.
599    pub fn add_column_justify(&mut self, header: impl Into<String>, justify: Justify) -> &mut Self {
600        self.add_column_text(Text::default(), justify);
601        if let Some(column) = self.columns.last_mut() {
602            column.header = Cell::Markup(header.into());
603        }
604        self
605    }
606
607    /// Add a column whose header is a styled [`Text`], as upstream's
608    /// `add_column(header=Text(...))` does. The text's spans survive into the
609    /// header cell; its own `justify`, `overflow` and `no_wrap` override the
610    /// column's, as `Text.__rich_console__` prefers them over the options.
611    pub fn add_column_text(&mut self, header: Text, justify: Justify) -> &mut Self {
612        self.columns.push(Column::new(Cell::Text(header), justify));
613        self
614    }
615
616    /// Add a column whose header is any [`Cell`] (upstream's `header` is any
617    /// renderable).
618    pub fn add_column_cell(&mut self, header: Cell, justify: Justify) -> &mut Self {
619        self.columns.push(Column::new(header, justify));
620        self
621    }
622
623    /// Set the most-recently-added column's footer (upstream
624    /// `Column.footer`), shown with [`show_footer`](Self::show_footer).
625    pub fn column_footer(&mut self, footer: impl Into<Cell>) -> &mut Self {
626        if let Some(column) = self.columns.last_mut() {
627            column.footer = footer.into();
628        }
629        self
630    }
631
632    /// Style the most-recently-added column's whole footer cell, combined
633    /// over the table-level `footer_style` (upstream `Column.footer_style`).
634    pub fn column_footer_fill(&mut self, style: Style) -> &mut Self {
635        if let Some(column) = self.columns.last_mut() {
636            column.footer_fill = Some(style);
637        }
638        self
639    }
640
641    /// Add a column with every [`ColumnOptions`] set, as upstream's
642    /// `add_column(header, justify=…, width=…, ratio=…, …)` does.
643    pub fn add_column_with(&mut self, header: Text, options: ColumnOptions) -> &mut Self {
644        self.columns.push(Column {
645            width: options.width,
646            style: options.style,
647            ratio: options.ratio,
648            min_width: options.min_width,
649            max_width: options.max_width,
650            no_wrap: options.no_wrap,
651            overflow: options.overflow,
652            ..Column::new(Cell::Text(header), options.justify)
653        });
654        self
655    }
656
657    /// Set the vertical alignment of the most-recently-added column's body
658    /// cells (upstream `Column.vertical`, default top). A cell whose
659    /// renderable has its own [`vertical`](crate::protocol::Renderable::vertical)
660    /// uses that instead. Chain after `add_column`.
661    pub fn column_vertical(&mut self, vertical: crate::align::VerticalAlign) -> &mut Self {
662        if let Some(column) = self.columns.last_mut() {
663            column.vertical = vertical;
664        }
665        self
666    }
667
668    /// Pin the most-recently-added column to an explicit content width. Content
669    /// wider than this wraps (with ellipsis overflow) instead of shrinking the
670    /// column. Chain after `add_column`.
671    pub fn column_width(&mut self, width: usize) -> &mut Self {
672        if let Some(column) = self.columns.last_mut() {
673            column.width = Some(width);
674        }
675        self
676    }
677
678    /// Give the most-recently-added column a flex `ratio`: when the table is
679    /// `expand`ed, ratio columns share the free width in proportion. Chain after
680    /// `add_column`. Port of `Column.ratio`.
681    pub fn column_ratio(&mut self, ratio: usize) -> &mut Self {
682        if let Some(column) = self.columns.last_mut() {
683            column.ratio = Some(ratio);
684        }
685        self
686    }
687
688    /// Set a minimum content width on the most-recently-added column. Chain after
689    /// `add_column`. Port of `Column.min_width`.
690    pub fn column_min_width(&mut self, min_width: usize) -> &mut Self {
691        if let Some(column) = self.columns.last_mut() {
692            column.min_width = Some(min_width);
693        }
694        self
695    }
696
697    /// Set a maximum content width on the most-recently-added column — wider
698    /// cells wrap. Chain after `add_column`. Port of `Column.max_width`.
699    pub fn column_max_width(&mut self, max_width: usize) -> &mut Self {
700        if let Some(column) = self.columns.last_mut() {
701            column.max_width = Some(max_width);
702        }
703        self
704    }
705
706    /// Apply a style to the most-recently-added column's body cells. Chain after
707    /// `add_column`.
708    pub fn column_style(&mut self, style: Style) -> &mut Self {
709        if let Some(column) = self.columns.last_mut() {
710            column.style = style;
711        }
712        self
713    }
714
715    /// Style the most-recently-added column's header *content* (the visible
716    /// characters), leaving its padding as the base `header_style`. Chain after
717    /// `add_column`. Mirrors upstream stylizing the heading `Text`.
718    pub fn column_header_style(&mut self, style: Style) -> &mut Self {
719        if let Some(column) = self.columns.last_mut() {
720            column.header_content_style = Some(style);
721        }
722        self
723    }
724
725    /// Style the most-recently-added column's whole header *cell* (content +
726    /// padding), combined over the table-level `header_style`. Chain after
727    /// `add_column`. Port of `Column.header_style`.
728    pub fn column_header_fill(&mut self, style: Style) -> &mut Self {
729        if let Some(column) = self.columns.last_mut() {
730            column.header_fill = Some(style);
731        }
732        self
733    }
734
735    /// Set how the most-recently-added column handles over-long text (upstream
736    /// `Column.overflow`, default ellipsis). Chain after `add_column`.
737    pub fn column_overflow(&mut self, overflow: Overflow) -> &mut Self {
738        if let Some(column) = self.columns.last_mut() {
739            column.overflow = overflow;
740        }
741        self
742    }
743
744    /// Set whether the most-recently-added column highlights its string cells
745    /// (upstream `Column.highlight`). Chain after `add_column`.
746    pub fn column_highlight(&mut self, highlight: bool) -> &mut Self {
747        if let Some(column) = self.columns.last_mut() {
748            column.highlight = Some(highlight);
749        }
750        self
751    }
752
753    /// Mark the most-recently-added column `no_wrap`: its cells crop to a single
754    /// line (with ellipsis) instead of wrapping. Chain after `add_column`.
755    pub fn column_no_wrap(&mut self) -> &mut Self {
756        if let Some(column) = self.columns.last_mut() {
757            column.no_wrap = true;
758        }
759        self
760    }
761
762    /// Add a row of string cells (extra cells are ignored; missing cells render
763    /// empty). Each string is console markup, as upstream's `add_row("[b]x")`
764    /// is; use [`add_row_text`](Self::add_row_text) for literal data.
765    pub fn add_row(&mut self, cells: &[&str]) -> &mut Self {
766        self.push_row(
767            cells
768                .iter()
769                .map(|s| Cell::Markup((*s).to_string()))
770                .collect(),
771            None,
772            false,
773        )
774    }
775
776    /// Add a row with upstream's keyword options: a `style` for the whole
777    /// row and `end_section` to draw a line beneath it. Port of
778    /// `add_row(*renderables, style=…, end_section=…)`.
779    pub fn add_row_with(
780        &mut self,
781        cells: Vec<Cell>,
782        style: Option<StyleType>,
783        end_section: bool,
784    ) -> &mut Self {
785        self.push_row(cells, style, end_section)
786    }
787
788    /// Port of `add_row`'s body: a row longer than the table adds a column
789    /// (with every default and the table's `highlight`) for each extra cell.
790    fn push_row(
791        &mut self,
792        cells: Vec<Cell>,
793        style: Option<StyleType>,
794        end_section: bool,
795    ) -> &mut Self {
796        while self.columns.len() < cells.len() {
797            let mut column = Column::new(Cell::Markup(String::new()), Justify::Left);
798            column.highlight = Some(self.highlight);
799            self.columns.push(column);
800        }
801        self.rows.push(Row {
802            cells,
803            style,
804            end_section,
805        });
806        self
807    }
808
809    /// Add a row of styled [`Text`] cells, as upstream's `add_row(Text(...))`.
810    /// Each cell keeps its spans, and its own `justify`, `overflow` and
811    /// `no_wrap` override the column's.
812    pub fn add_row_text(&mut self, cells: Vec<Text>) -> &mut Self {
813        self.push_row(cells.into_iter().map(Cell::Text).collect(), None, false)
814    }
815
816    /// Add a row of [`Cell`]s, which may be any renderable.
817    pub fn add_row_cells(&mut self, cells: Vec<Cell>) -> &mut Self {
818        self.push_row(cells, None, false)
819    }
820
821    /// The width of the borders: `ncols - 1` dividers, plus the two outer
822    /// edges when shown; no box, no border. Port of `_extra_width`.
823    fn extra_width(&self) -> usize {
824        if self.no_box {
825            0
826        } else {
827            (if self.show_edge { 2 } else { 0 }) + self.columns.len().saturating_sub(1)
828        }
829    }
830
831    /// The column's padding width. Port of `_get_padding_width`, which (unlike
832    /// the per-cell padding of `_get_cells`) drops the left pad entirely under
833    /// `collapse_padding`.
834    fn padding_width(&self, index: usize) -> usize {
835        let (_, mut pad_right, _, mut pad_left) = self.padding;
836        if self.collapse_padding {
837            pad_left = 0;
838        }
839        if !self.pad_edge {
840            if index == 0 {
841                pad_left = 0;
842            }
843            if index + 1 == self.columns.len() {
844                pad_right = 0;
845            }
846        }
847        pad_left + pad_right
848    }
849
850    /// `Measurement.get` of one of `_get_cells`' cells: the cell wrapped in
851    /// `Padding(renderable, (0, right, 0, left))` when the table has any
852    /// padding. Port of `Padding.__rich_measure__` over the cell.
853    fn measure_padded_cell(
854        &self,
855        console: &Console,
856        options: &ConsoleOptions,
857        cell: &Cell,
858        (left, right): (usize, usize),
859    ) -> Measurement {
860        let max_width = options.max_width;
861        if max_width < 1 {
862            return Measurement::new(0, 0);
863        }
864        let (top, pr, bottom, pl) = self.padding;
865        if top == 0 && pr == 0 && bottom == 0 && pl == 0 {
866            return cell.measure_cell(console, options);
867        }
868        let extra_width = left + right;
869        let width = if max_width < extra_width + 1 {
870            Measurement::new(max_width, max_width)
871        } else {
872            let inner = cell.measure_cell(console, options);
873            Measurement::new(inner.minimum + extra_width, inner.maximum + extra_width)
874                .with_maximum(max_width)
875        };
876        // `Measurement.get` around the `Padding`.
877        let width = width.normalize().with_maximum(max_width);
878        if width.maximum < 1 {
879            Measurement::new(0, 0)
880        } else {
881            width.normalize()
882        }
883    }
884
885    /// The minimum and maximum width of column `index` (content + padding).
886    /// Port of `Table._measure_column`: every cell, header included, is
887    /// measured with `Measurement.get`, so a nested renderable (a `Table`,
888    /// `Panel`, …) sizes its column by its own `__rich_measure__`.
889    fn measure_column(
890        &self,
891        console: &Console,
892        options: &ConsoleOptions,
893        index: usize,
894    ) -> Measurement {
895        let max_width = options.max_width;
896        if max_width < 1 {
897            return Measurement::new(0, 0);
898        }
899        let column = &self.columns[index];
900        let padding_width = self.padding_width(index);
901        if let Some(width) = column.width {
902            // Fixed width column.
903            return Measurement::new(width + padding_width, width + padding_width)
904                .with_maximum(max_width);
905        }
906        // Every cell of a column shares its left/right padding; only the
907        // vertical padding depends on the row.
908        let padding = self.cell_padding(index, self.columns.len());
909        let empty = Cell::Markup(String::new());
910        let header = self.show_header.then_some(&column.header);
911        let body = self
912            .rows
913            .iter()
914            .map(|row| row.cells.get(index).unwrap_or(&empty));
915        let footer = self.show_footer.then_some(&column.footer);
916        let mut measured = false;
917        let (mut minimum, mut maximum) = (0, 0);
918        for cell in header.into_iter().chain(body).chain(footer) {
919            let width = self.measure_padded_cell(console, options, cell, padding);
920            minimum = minimum.max(width.minimum);
921            maximum = maximum.max(width.maximum);
922            measured = true;
923        }
924        let measurement = if measured {
925            Measurement::new(minimum, maximum)
926        } else {
927            Measurement::new(1, max_width)
928        }
929        .with_maximum(max_width);
930        measurement.clamp(
931            column.min_width.map(|width| width + padding_width),
932            column.max_width.map(|width| width + padding_width),
933        )
934    }
935
936    /// The rendered width (content + padding) of each column, shrinking the
937    /// widest columns to fit `available` when necessary. Port of
938    /// `Table._calculate_column_widths` + `_collapse_widths`.
939    fn column_widths(
940        &self,
941        console: &Console,
942        options: &ConsoleOptions,
943        available: usize,
944    ) -> Vec<usize> {
945        let options = &options.update_width(available);
946        let max_width = available as i64;
947        let extra_width = self.extra_width() as i64;
948        // A fixed-width column uses its declared width; others measure content,
949        // clamped to the column's [min_width, max_width]. Port of `_measure_column`.
950        let maximums: Vec<i64> = (0..self.columns.len())
951            .map(|index| self.measure_column(console, options, index).maximum as i64)
952            .collect();
953        let mut widths: Vec<i64> = maximums.iter().map(|&width| width.max(1)).collect();
954
955        // Expand with explicit ratios: flexible (ratio) columns share the free
956        // width in proportion, fixed columns keep their measured width. Port of
957        // the `if self.expand: … if any(ratios)` block of `_calculate_column_widths`.
958        if self.is_expand() {
959            let ratios: Vec<i64> = self
960                .columns
961                .iter()
962                .filter(|c| c.ratio.is_some())
963                .map(|c| i64::try_from(c.ratio.unwrap()).unwrap_or(i64::MAX))
964                .collect();
965            if ratios.iter().any(|&r| r > 0) {
966                let fixed_widths: Vec<i64> = maximums
967                    .iter()
968                    .zip(&self.columns)
969                    .map(|(&w, c)| if c.ratio.is_some() { 0 } else { w })
970                    .collect();
971                let flex_minimum: Vec<i64> = self
972                    .columns
973                    .iter()
974                    .enumerate()
975                    .filter(|(_, c)| c.ratio.is_some())
976                    .map(|(index, c)| (c.width.unwrap_or(1) + self.padding_width(index)) as i64)
977                    .collect();
978                let flexible_width = max_width - fixed_widths.iter().sum::<i64>();
979                let flex_widths = ratio_distribute(flexible_width, &ratios, Some(&flex_minimum));
980                let mut iter_flex = flex_widths.into_iter();
981                for (index, column) in self.columns.iter().enumerate() {
982                    if column.ratio.is_some() {
983                        widths[index] = fixed_widths[index] + iter_flex.next().unwrap_or(0);
984                    }
985                }
986            }
987        }
988
989        let mut table_width: i64 = widths.iter().sum();
990        if table_width > max_width {
991            // Only auto-width, wrapping columns may shrink; fixed and no_wrap
992            // columns hold their width (no_wrap only yields via the last resort).
993            let wrapable: Vec<bool> = self
994                .columns
995                .iter()
996                .map(|c| c.width.is_none() && !c.no_wrap)
997                .collect();
998            widths = collapse_widths(widths, &wrapable, max_width);
999            table_width = widths.iter().sum();
1000            // Last resort: if fixed columns still overflow, reduce every column
1001            // evenly. Port of `_calculate_column_widths`'s final `ratio_reduce`.
1002            if table_width > max_width {
1003                let excess = table_width - max_width;
1004                let ratios = vec![1i64; widths.len()];
1005                widths = ratio_reduce(excess, &ratios, &widths, &widths);
1006                table_width = widths.iter().sum();
1007            }
1008            // Upstream measures every column again at its reduced width, so a
1009            // `min_width` re-inflates its column and the table overflows (the
1010            // console crop then cuts it). `table_width` keeps the reduced sum,
1011            // as upstream's does.
1012            widths = widths
1013                .iter()
1014                .enumerate()
1015                .map(|(index, &width)| {
1016                    self.measure_column(
1017                        console,
1018                        &options.update_width(width.max(0) as usize),
1019                        index,
1020                    )
1021                    .maximum as i64
1022                })
1023                .collect();
1024        }
1025
1026        // Expand, or grow to the table's `min_width`: distribute the leftover
1027        // width proportionally (`ratio_distribute`, which asserts a positive
1028        // total ratio — a table of zero-width columns is left as it is).
1029        let min_width = self.min_width.map(|width| width as i64);
1030        if (table_width < max_width && self.is_expand())
1031            || min_width.is_some_and(|min_width| table_width < min_width - extra_width)
1032        {
1033            let target = match min_width {
1034                None => max_width,
1035                Some(min_width) => (min_width - extra_width).min(max_width),
1036            };
1037            if widths.iter().sum::<i64>() > 0 {
1038                let pad = ratio_distribute(target - table_width, &widths, None);
1039                for (width, extra) in widths.iter_mut().zip(pad) {
1040                    *width += extra;
1041                }
1042            }
1043        }
1044        widths.into_iter().map(|w| w.max(0) as usize).collect()
1045    }
1046
1047    /// `cell_padding` shrunk so that padding alone can never exceed the width
1048    /// the column was actually allotted.
1049    ///
1050    /// When many columns compete for a narrow terminal a column can be squeezed
1051    /// below its own padding. The cell then still emitted a full left and right
1052    /// pad, so every such column spent two cells where its border spent one and
1053    /// the content row grew wider than the table — at 29 columns in an 80-cell
1054    /// terminal the row overflowed by 15 cells and was cropped, taking the
1055    /// right-hand border with it while the border rows kept theirs.
1056    fn cell_padding_fitted(&self, index: usize, ncols: usize, rendered: usize) -> (usize, usize) {
1057        let (mut pl, mut pr) = self.cell_padding(index, ncols);
1058        while pl + pr > rendered {
1059            if pr > pl {
1060                pr -= 1;
1061            } else if pl > 0 {
1062                pl -= 1;
1063            } else {
1064                break;
1065            }
1066        }
1067        (pl, pr)
1068    }
1069
1070    /// The effective style for a cell in column `index`: `_get_cells`' cell
1071    /// style plus the row style. The header row takes `header_style` and the
1072    /// column's own header style, the footer likewise, and a body cell the
1073    /// column's style and then its row's.
1074    fn cell_style(&self, console: &Console, index: usize, kind: &RowKind) -> Style {
1075        let column = self.columns.get(index);
1076        match kind {
1077            RowKind::Header => {
1078                let base = console.theme().get_style_or_null(&self.header_style);
1079                match column.and_then(|c| c.header_fill.as_ref()) {
1080                    Some(fill) => base.combine(fill),
1081                    None => base,
1082                }
1083            }
1084            RowKind::Footer => {
1085                let base = console.theme().get_style_or_null(&self.footer_style);
1086                match column.and_then(|c| c.footer_fill.as_ref()) {
1087                    Some(fill) => base.combine(fill),
1088                    None => base,
1089                }
1090            }
1091            RowKind::Body(row_style) => column
1092                .map(|c| c.style.clone())
1093                .unwrap_or_default()
1094                .combine(row_style),
1095        }
1096    }
1097
1098    /// Pad a cell's rendered lines to its width, with the vertical padding
1099    /// above and below: upstream's `Padding` around the cell. Blank rows are
1100    /// one run across the whole cell, as `Padding`'s blank lines are.
1101    /// The `(top, bottom)` padding of a cell in the first and/or last of the
1102    /// rendered rows (header included). Port of `_get_cells`' `get_padding`:
1103    /// with `collapse_padding` every row but the last keeps only
1104    /// `max(0, top - bottom)` below it, and without `pad_edge` the first row
1105    /// loses its top and the last row its bottom.
1106    fn vertical_padding(&self, first_row: bool, last_row: bool) -> (usize, usize) {
1107        let (mut top, _, mut bottom, _) = self.padding;
1108        if self.collapse_padding && !last_row {
1109            bottom = top.saturating_sub(bottom);
1110        }
1111        if !self.pad_edge {
1112            if first_row {
1113                top = 0;
1114            }
1115            if last_row {
1116                bottom = 0;
1117            }
1118        }
1119        (top, bottom)
1120    }
1121
1122    fn pad_cell_lines(
1123        &self,
1124        lines: Vec<Vec<Segment>>,
1125        width: usize,
1126        (cpl, cpr): (usize, usize),
1127        (pt, pb): (usize, usize),
1128        style: &Style,
1129    ) -> Vec<Vec<Segment>> {
1130        let cell_fill = Some(style.clone());
1131        let cell_width = cpl + width + cpr;
1132        // The cell's `Padding` renders at the whole cell width, and
1133        // `Console.render` yields nothing at all below width 1: no content and
1134        // no vertical padding either (the row keeps its minimum height of 1).
1135        if cell_width == 0 {
1136            return Vec::new();
1137        }
1138        let blank = || vec![Segment::new(" ".repeat(cell_width), cell_fill.clone())];
1139        let mut padded_lines: Vec<Vec<Segment>> = Vec::new();
1140        for _ in 0..pt {
1141            padded_lines.push(blank());
1142        }
1143        for line in &lines {
1144            let mut row = Vec::new();
1145            if cpl > 0 {
1146                row.push(Segment::new(" ".repeat(cpl), cell_fill.clone()));
1147            }
1148            // The cell's segments pass through, as `Padding` yields them.
1149            row.extend(Segment::adjust_line_length(line, width, cell_fill.clone()));
1150            if cpr > 0 {
1151                row.push(Segment::new(" ".repeat(cpr), cell_fill.clone()));
1152            }
1153            padded_lines.push(row);
1154        }
1155        for _ in 0..pb {
1156            padded_lines.push(blank());
1157        }
1158        padded_lines
1159    }
1160
1161    /// Render one table row (a list of cell strings) into visual lines.
1162    #[allow(clippy::too_many_arguments)]
1163    fn render_row(
1164        &self,
1165        console: &Console,
1166        options: &ConsoleOptions,
1167        cells: &[Cell],
1168        rendered_widths: &[usize],
1169        kind: &RowKind,
1170        (first_row, last_row): (bool, bool),
1171        edges: Option<(Segment, Segment, Segment)>,
1172    ) -> Vec<Vec<Segment>> {
1173        let is_header = matches!(kind, RowKind::Header);
1174        // Horizontal padding is per-column (see `cell_padding`); vertical
1175        // padding depends on the row's place (see `vertical_padding`).
1176        let vertical = self.vertical_padding(first_row, last_row);
1177        let ncols = self.columns.len();
1178        // Derived here rather than by the caller so the padding used to lay the
1179        // row out is the same padding the content width was reduced by.
1180        let paddings: Vec<(usize, usize)> = (0..ncols)
1181            .map(|index| {
1182                let rendered = rendered_widths.get(index).copied().unwrap_or(0);
1183                self.cell_padding_fitted(index, ncols, rendered)
1184            })
1185            .collect();
1186        let content_widths: Vec<usize> = rendered_widths
1187            .iter()
1188            .zip(&paddings)
1189            .map(|(w, (pl, pr))| w.saturating_sub(pl + pr))
1190            .collect();
1191
1192        // Render each cell into padded, simplified visual lines.
1193        let mut cell_lines: Vec<Vec<Vec<Segment>>> = Vec::with_capacity(ncols);
1194        let mut height = 1;
1195        for (index, width) in content_widths.iter().enumerate() {
1196            let style = self.cell_style(console, index, kind);
1197            let column = self.columns.get(index);
1198            let mut text = match cells.get(index) {
1199                Some(Cell::Text(text)) => text.clone(),
1200                // `render_options.update(highlight=column.highlight)`, then
1201                // `Console.render` of a `str` calls `render_str`.
1202                Some(Cell::Markup(markup)) => console.render_str(
1203                    markup,
1204                    Some(column.and_then(|c| c.highlight).unwrap_or(self.highlight)),
1205                ),
1206                None => Text::default(),
1207                Some(Cell::Renderable(renderable)) => {
1208                    // `console.render_lines(renderable, render_options, style)`
1209                    // at the content width, with the column's justify,
1210                    // no_wrap and overflow as options.
1211                    let mut cell_options = options.update_width(*width);
1212                    cell_options.highlight =
1213                        Some(column.and_then(|c| c.highlight).unwrap_or(self.highlight));
1214                    cell_options.justify = column.map_or(Justify::Left, |c| c.justify);
1215                    cell_options.no_wrap = Some(column.is_some_and(|c| c.no_wrap));
1216                    cell_options.overflow = Some(column.map_or(Overflow::Ellipsis, |c| c.overflow));
1217                    let lines = if *width == 0 {
1218                        Vec::new()
1219                    } else {
1220                        console.render_lines_styled(
1221                            renderable.as_ref(),
1222                            &cell_options,
1223                            Some(&style),
1224                            true,
1225                        )
1226                    };
1227                    cell_lines.push(self.pad_cell_lines(
1228                        lines,
1229                        *width,
1230                        paddings[index],
1231                        vertical,
1232                        &style,
1233                    ));
1234                    height = height.max(cell_lines.last().map_or(0, Vec::len));
1235                    continue;
1236                }
1237            };
1238            // Upstream renders the cell `Text` with the column's `justify`,
1239            // `no_wrap` and `overflow="ellipsis"` as options, which the text's own
1240            // settings override: wrap, then justify (which strips a right- or
1241            // center-justified line before measuring it), then truncate.
1242            let justify = text
1243                .get_justify_option()
1244                .unwrap_or_else(|| column.map(|c| c.justify).unwrap_or(Justify::Left));
1245            let overflow = text
1246                .get_overflow()
1247                .unwrap_or_else(|| column.map_or(Overflow::Ellipsis, |c| c.overflow));
1248            let no_wrap = text
1249                .get_no_wrap()
1250                .unwrap_or_else(|| column.map(|c| c.no_wrap).unwrap_or(false));
1251            // Header content carries its own style span over `header_style`; the
1252            // justify/edge padding stays `header_style` (matches upstream).
1253            if is_header {
1254                if let Some(span) = column.and_then(|c| c.header_content_style.clone()) {
1255                    let len = text.plain().len();
1256                    text.stylize(span, 0, len);
1257                }
1258            }
1259            // Upstream renders the cell as `Padding(renderable, …)` through
1260            // `render_lines`: a zero-width content area renders no lines
1261            // (`Console.render` returns nothing below width 1), while empty
1262            // text still renders one blank line.
1263            //
1264            // The text renders on its own and the cell style is applied to the
1265            // result (`render_lines(..., style=...)`), so a span keeps its own
1266            // segment even where it matches the cell style: `[b]Name` under a
1267            // bold header is `Name` + padding, as upstream prints it. The
1268            // justify padding joins the text's last run only where no span
1269            // ends there, as `Text.pad_right` on the plain string does.
1270            let tab_size = text.console_tab_size(console);
1271            let mut lines: Vec<Vec<Segment>> = if *width == 0 {
1272                Vec::new()
1273            } else {
1274                text.render_lines_wrapped_tabs(
1275                    console.theme(),
1276                    &Style::new(),
1277                    Some(*width),
1278                    justify,
1279                    overflow,
1280                    no_wrap,
1281                    tab_size,
1282                )
1283                .iter()
1284                .map(|line| Segment::apply_style(line, &style))
1285                .collect()
1286            };
1287            if lines.is_empty() && *width > 0 {
1288                lines.push(Vec::new());
1289            }
1290            let padded_lines =
1291                self.pad_cell_lines(lines, *width, paddings[index], vertical, &style);
1292            height = height.max(padded_lines.len());
1293            cell_lines.push(padded_lines);
1294        }
1295
1296        // Shape every cell to the row height (#445). Upstream aligns each cell
1297        // to `row_height` (the tallest cell, possibly 0) with the cell style:
1298        // header cells to the bottom, body cells (vertical "top") to the top.
1299        // `Segment.set_shape` then pads to `max_height` (at least 1) with an
1300        // unstyled blank.
1301        let row_height = cell_lines.iter().map(Vec::len).max().unwrap_or(0);
1302        for (index, lines) in cell_lines.iter_mut().enumerate() {
1303            // `getattr(renderable, "vertical", None) or column.vertical`; a
1304            // header row aligns to the bottom and a footer to the top,
1305            // whatever the cell says.
1306            let vertical = if is_header {
1307                crate::align::VerticalAlign::Bottom
1308            } else if matches!(kind, RowKind::Footer) {
1309                crate::align::VerticalAlign::Top
1310            } else {
1311                match cells.get(index) {
1312                    Some(Cell::Renderable(renderable)) => renderable.vertical(),
1313                    _ => None,
1314                }
1315                .unwrap_or_else(|| {
1316                    self.columns
1317                        .get(index)
1318                        .map_or(crate::align::VerticalAlign::Top, |c| c.vertical)
1319                })
1320            };
1321            let (cpl, cpr) = paddings[index];
1322            let blank = " ".repeat(cpl + content_widths[index] + cpr);
1323            let filler = vec![Segment::new(
1324                blank.clone(),
1325                Some(self.cell_style(console, index, kind)),
1326            )];
1327            let missing = row_height.saturating_sub(lines.len());
1328            let top = match vertical {
1329                crate::align::VerticalAlign::Top => 0,
1330                crate::align::VerticalAlign::Middle => missing / 2,
1331                crate::align::VerticalAlign::Bottom => missing,
1332            };
1333            lines.splice(0..0, std::iter::repeat_n(filler.clone(), top));
1334            lines.extend(std::iter::repeat_n(filler, missing - top));
1335            while lines.len() < height {
1336                lines.push(vec![Segment::new(blank.clone(), None)]);
1337            }
1338        }
1339
1340        let last = ncols.saturating_sub(1);
1341        let mut rows_out: Vec<Vec<Segment>> = Vec::with_capacity(height);
1342        // `r` indexes into each column's per-line vector, so a range loop is the
1343        // natural shape here (the columns are iterated with `enumerate`).
1344        #[allow(clippy::needless_range_loop)]
1345        for r in 0..height {
1346            let mut row = Vec::new();
1347            if let (Some((edge_left, _, _)), true) = (&edges, self.show_edge) {
1348                row.push(edge_left.clone());
1349            }
1350            for (c, column_lines) in cell_lines.iter().enumerate() {
1351                row.extend(column_lines[r].clone());
1352                let Some((_, divider, edge_right)) = &edges else {
1353                    continue;
1354                };
1355                if c != last {
1356                    row.push(divider.clone());
1357                } else if self.show_edge {
1358                    row.push(edge_right.clone());
1359                }
1360            }
1361            rows_out.push(row);
1362        }
1363        rows_out
1364    }
1365}
1366
1367impl LineRenderable for Table {
1368    /// Render visual lines in order without retaining the full rendered table.
1369    ///
1370    /// Like upstream's `Table.__rich_console__` / `_render` generators, this
1371    /// measures all columns first, then renders only one row block at a time.
1372    /// Lines contain styled segments without a trailing newline. The callback
1373    /// may write each line immediately; its first error stops rendering.
1374    /// The table still owns its source rows for column-width measurement.
1375    fn try_for_each_line<E>(
1376        &self,
1377        console: &Console,
1378        options: &ConsoleOptions,
1379        mut emit: impl FnMut(Vec<Segment>) -> Result<(), E>,
1380    ) -> Result<(), E> {
1381        if self.columns.is_empty() {
1382            return emit(vec![Segment::new("", None)]);
1383        }
1384        // Fall back to a terminal-safe box on legacy Windows / non-UTF-8, and
1385        // to a plain-headed box when there is no header to set apart.
1386        let box_set = self.box_set.substitute(
1387            console.legacy_windows(),
1388            self.safe_box.unwrap_or_else(|| console.safe_box()),
1389            console.ascii_only(),
1390        );
1391        let box_set = if self.show_header {
1392            box_set
1393        } else {
1394            box_set.get_plain_headed_box()
1395        };
1396        // `max_width = self.width` when the table has one.
1397        let max_width = self.width.unwrap_or(options.max_width);
1398        let extra_width = self.extra_width();
1399        let available = max_width.saturating_sub(extra_width);
1400
1401        let rendered_widths = self.column_widths(console, options, available);
1402        let border_style = self.style.combine(&self.border_style);
1403        let border = Some(border_style.clone());
1404
1405        // Full table width (for the title and caption): columns + borders.
1406        let table_width: usize = rendered_widths.iter().sum::<usize>() + extra_width;
1407
1408        if let Some(title) = &self.title {
1409            let style = self
1410                .title_style
1411                .clone()
1412                .unwrap_or_else(|| StyleType::Name("table.title".to_string()));
1413            for line in render_annotation(
1414                console,
1415                options,
1416                title,
1417                &style,
1418                self.title_justify,
1419                table_width,
1420            ) {
1421                emit(line)?;
1422            }
1423        }
1424
1425        let edge = self.show_edge;
1426        let boxed = !self.no_box;
1427        if boxed && edge {
1428            emit(vec![Segment::new(
1429                box_set.get_top(&rendered_widths, edge),
1430                border.clone(),
1431            )])?;
1432        }
1433
1434        // `box_segments[0 if first else (2 if last else 1)]`: the first row
1435        // drawn (header or not) takes the head glyphs and the last the foot's.
1436        let segments = |(left, vertical, right): (char, char, char)| {
1437            (
1438                Segment::new(left.to_string(), border.clone()),
1439                Segment::new(vertical.to_string(), border.clone()),
1440                Segment::new(right.to_string(), border.clone()),
1441            )
1442        };
1443        let head_edges = segments((box_set.head_left, box_set.head_vertical, box_set.head_right));
1444        let mid_edges = segments((box_set.mid_left, box_set.mid_vertical, box_set.mid_right));
1445        let foot_edges = segments((box_set.foot_left, box_set.foot_vertical, box_set.foot_right));
1446
1447        // Every row drawn, as upstream's `row_cells` holds them.
1448        let mut row_list: Vec<(RowKind, &[Cell], Option<&Row>)> = Vec::new();
1449        let header_cells: Vec<Cell> = self.columns.iter().map(|c| c.header.clone()).collect();
1450        let footer_cells: Vec<Cell> = self.columns.iter().map(|c| c.footer.clone()).collect();
1451        if self.show_header {
1452            row_list.push((RowKind::Header, &header_cells, None));
1453        }
1454        for (index, row) in self.rows.iter().enumerate() {
1455            row_list.push((
1456                RowKind::Body(self.get_row_style(console, index)),
1457                &row.cells,
1458                Some(row),
1459            ));
1460        }
1461        if self.show_footer {
1462            row_list.push((RowKind::Footer, &footer_cells, None));
1463        }
1464        let total = row_list.len();
1465        for (index, (kind, cells, row)) in row_list.iter().enumerate() {
1466            let first = index == 0;
1467            let last = index + 1 == total;
1468            let header_row = first && self.show_header;
1469            if boxed && last && self.show_footer {
1470                emit(vec![Segment::new(
1471                    box_set.get_row(&rendered_widths, RowLevel::Foot, edge),
1472                    border.clone(),
1473                )])?;
1474            }
1475            let edges = boxed.then(|| {
1476                let (left, divider, right) = if first {
1477                    head_edges.clone()
1478                } else if last {
1479                    foot_edges.clone()
1480                } else {
1481                    mid_edges.clone()
1482                };
1483                // A whitespace divider takes the row's background too.
1484                let divider = if divider.text.trim().is_empty() {
1485                    let background = match kind {
1486                        RowKind::Body(row_style) => {
1487                            Style::from_color(None, row_style.bgcolor().cloned())
1488                        }
1489                        _ => Style::new(),
1490                    };
1491                    Segment::new(
1492                        divider.text.clone(),
1493                        Some(background.combine(&border_style)),
1494                    )
1495                } else {
1496                    divider
1497                };
1498                (left, divider, right)
1499            });
1500            for line in self.render_row(
1501                console,
1502                options,
1503                cells,
1504                &rendered_widths,
1505                kind,
1506                (first, last),
1507                edges,
1508            ) {
1509                emit(line)?;
1510            }
1511            if boxed && header_row {
1512                emit(vec![Segment::new(
1513                    box_set.get_row(&rendered_widths, RowLevel::Head, edge),
1514                    border.clone(),
1515                )])?;
1516            }
1517            let end_section = row.is_some_and(|row| row.end_section);
1518            if boxed
1519                && (self.show_lines || self.leading > 0 || end_section)
1520                && !last
1521                && !(self.show_footer && index + 2 >= total)
1522                && !header_row
1523            {
1524                let line = if self.leading > 0 {
1525                    // Upstream repeats the row on one line, as it is here.
1526                    box_set
1527                        .get_row(&rendered_widths, RowLevel::Mid, edge)
1528                        .repeat(self.leading)
1529                } else {
1530                    box_set.get_row(&rendered_widths, RowLevel::Row, edge)
1531                };
1532                emit(vec![Segment::new(line, border.clone())])?;
1533            }
1534        }
1535
1536        if boxed && edge {
1537            emit(vec![Segment::new(
1538                box_set.get_bottom(&rendered_widths, edge),
1539                border.clone(),
1540            )])?;
1541        }
1542
1543        if let Some(caption) = &self.caption {
1544            let style = self
1545                .caption_style
1546                .clone()
1547                .unwrap_or_else(|| StyleType::Name("table.caption".to_string()));
1548            for line in render_annotation(
1549                console,
1550                options,
1551                caption,
1552                &style,
1553                self.caption_justify,
1554                table_width,
1555            ) {
1556                emit(line)?;
1557            }
1558        }
1559
1560        Ok(())
1561    }
1562}
1563
1564impl crate::protocol::OwnedTableRows for Table {
1565    fn extend_owned_rows(&mut self, rows: Vec<Vec<String>>) -> &mut Self {
1566        for row in rows {
1567            self.push_row(row.into_iter().map(Cell::Markup).collect(), None, false);
1568        }
1569        self
1570    }
1571}
1572
1573impl Renderable for Table {
1574    /// Port of `Table.__rich_measure__`: the column widths the table would
1575    /// render at, then each column measured within their total; an explicit
1576    /// `width` is the maximum, and `min_width` a floor.
1577    fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
1578        let max_width = self.width.unwrap_or(options.max_width);
1579        if self.columns.is_empty() {
1580            // `_extra_width` counts `len(columns) - 1` dividers, so an empty
1581            // boxed table measures `2 - 1` with edges and `-1` (normalized to
1582            // 0) without.
1583            let width = usize::from(!self.no_box && self.show_edge);
1584            return Measurement::new(width, self.width.unwrap_or(width))
1585                .clamp(self.min_width, None);
1586        }
1587        let extra_width = self.extra_width();
1588        let columns_width: usize = self
1589            .column_widths(console, options, max_width.saturating_sub(extra_width))
1590            .iter()
1591            .sum();
1592        let options = options.update_width(columns_width);
1593        let (minimum, maximum) = (0..self.columns.len())
1594            .map(|index| self.measure_column(console, &options, index))
1595            .fold((0, 0), |(minimum, maximum), width| {
1596                (minimum + width.minimum, maximum + width.maximum)
1597            });
1598        let maximum = self.width.unwrap_or(maximum + extra_width);
1599        Measurement::new(minimum + extra_width, maximum).clamp(self.min_width, None)
1600    }
1601
1602    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
1603        let mut segments = Vec::new();
1604        let mut first = true;
1605        let result: Result<(), std::convert::Infallible> =
1606            self.try_for_each_line(console, options, |line| {
1607                if !first {
1608                    segments.push(Segment::line());
1609                }
1610                first = false;
1611                segments.extend(line);
1612                Ok(())
1613            });
1614        match result {
1615            Ok(()) => segments,
1616            Err(never) => match never {},
1617        }
1618    }
1619}
1620
1621/// Port of `Table.__rich_console__.render_annotation`: a string title is
1622/// `console.render_str(text, style=style, highlight=False)` (markup and emoji
1623/// on, no highlighting); a `Text` renders as it is. Either renders at the
1624/// table's width with `justify` unless the text has its own.
1625fn render_annotation(
1626    console: &Console,
1627    options: &ConsoleOptions,
1628    annotation: &Cell,
1629    style: &StyleType,
1630    justify: Justify,
1631    width: usize,
1632) -> Vec<Vec<Segment>> {
1633    let text = match annotation {
1634        Cell::Markup(markup) => {
1635            if markup.is_empty() {
1636                return Vec::new();
1637            }
1638            let mut text = console.render_str(markup, Some(false));
1639            text.set_base_style(style.clone());
1640            text
1641        }
1642        Cell::Text(text) => {
1643            if text.plain().is_empty() {
1644                return Vec::new();
1645            }
1646            text.clone()
1647        }
1648        Cell::Renderable(_) => return Vec::new(),
1649    };
1650    if width == 0 {
1651        return Vec::new();
1652    }
1653    let justify = text.get_justify_option().unwrap_or(justify);
1654    let overflow = text
1655        .get_overflow()
1656        .or(options.overflow)
1657        .unwrap_or(Overflow::Fold);
1658    let no_wrap = text.get_no_wrap().or(options.no_wrap).unwrap_or(false);
1659    let tab_size = text.console_tab_size(console);
1660    text.render_lines_wrapped_tabs(
1661        console.theme(),
1662        console.base_style(),
1663        Some(width),
1664        justify,
1665        overflow,
1666        no_wrap,
1667        tab_size,
1668    )
1669}
1670
1671/// Round half to even (banker's rounding), matching Python's `round`.
1672fn round_half_even(value: f64) -> i64 {
1673    let floor = value.floor();
1674    let diff = value - floor;
1675    if (diff - 0.5).abs() < 1e-9 {
1676        let f = floor as i64;
1677        if f % 2 == 0 {
1678            f
1679        } else {
1680            f + 1
1681        }
1682    } else {
1683        value.round() as i64
1684    }
1685}
1686
1687/// Reduce `values` by `total`, distributed across slots by `ratios` (capped by
1688/// `maximums`). Direct port of `rich._ratio.ratio_reduce`.
1689fn ratio_reduce(total: i64, ratios: &[i64], maximums: &[i64], values: &[i64]) -> Vec<i64> {
1690    let ratios: Vec<i64> = ratios
1691        .iter()
1692        .zip(maximums)
1693        .map(|(&r, &m)| if m != 0 { r } else { 0 })
1694        .collect();
1695    let mut total_ratio: i128 = ratios.iter().map(|&r| i128::from(r)).sum();
1696    if total_ratio == 0 {
1697        return values.to_vec();
1698    }
1699    let mut total_remaining = total;
1700    let mut result = Vec::with_capacity(values.len());
1701    for ((&ratio, &maximum), &value) in ratios.iter().zip(maximums).zip(values) {
1702        if ratio != 0 && total_ratio > 0 {
1703            let distributed = maximum.min(round_half_even(
1704                ratio as f64 * total_remaining as f64 / total_ratio as f64,
1705            ));
1706            result.push(value - distributed);
1707            total_remaining -= distributed;
1708            total_ratio -= i128::from(ratio);
1709        } else {
1710            result.push(value);
1711        }
1712    }
1713    result
1714}
1715
1716/// Divide `total` across slots proportionally to `ratios` (ceil each share),
1717/// each share floored at the matching `minimums` entry when given. Port of
1718/// `rich._ratio.ratio_distribute`.
1719fn ratio_distribute(total: i64, ratios: &[i64], minimums: Option<&[i64]>) -> Vec<i64> {
1720    // Upstream zeroes the ratio of any slot whose minimum is 0 (falsy).
1721    let ratios: Vec<i64> = match minimums {
1722        Some(mins) => ratios
1723            .iter()
1724            .zip(mins)
1725            .map(|(&r, &m)| if m != 0 { r } else { 0 })
1726            .collect(),
1727        None => ratios.to_vec(),
1728    };
1729    // Python ints never overflow; `ratio * total_remaining` can exceed i64
1730    // for a huge ratio, so the arithmetic runs in i128.
1731    let mut total_ratio: i128 = ratios.iter().map(|&r| i128::from(r)).sum();
1732    let mut total_remaining = i128::from(total);
1733    let mut result = Vec::with_capacity(ratios.len());
1734    for (index, &ratio) in ratios.iter().enumerate() {
1735        let ratio = i128::from(ratio);
1736        let minimum = i128::from(minimums.map_or(0, |m| m[index]));
1737        let distributed = if total_ratio > 0 {
1738            // ceil(ratio * total_remaining / total_ratio) for positive values,
1739            // then floored at `minimum`.
1740            let numerator = ratio * total_remaining;
1741            let ceil_div = (numerator + total_ratio - 1) / total_ratio;
1742            minimum.max(ceil_div)
1743        } else {
1744            total_remaining
1745        };
1746        result.push(i64::try_from(distributed).unwrap_or(if distributed < 0 {
1747            i64::MIN
1748        } else {
1749            i64::MAX
1750        }));
1751        total_ratio -= ratio;
1752        total_remaining -= distributed;
1753    }
1754    result
1755}
1756
1757/// Reduce `widths` so their total is under `max_width`, shrinking the widest
1758/// wrapable columns first. Direct port of `Table._collapse_widths`.
1759fn collapse_widths(mut widths: Vec<i64>, wrapable: &[bool], max_width: i64) -> Vec<i64> {
1760    let mut total_width: i64 = widths.iter().sum();
1761    let mut excess_width = total_width - max_width;
1762    if wrapable.iter().any(|&w| w) {
1763        while total_width != 0 && excess_width > 0 {
1764            let max_column = widths
1765                .iter()
1766                .zip(wrapable)
1767                .filter(|(_, &w)| w)
1768                .map(|(&x, _)| x)
1769                .max()
1770                .unwrap_or(0);
1771            let second_max_column = widths
1772                .iter()
1773                .zip(wrapable)
1774                .map(|(&x, &w)| if w && x != max_column { x } else { 0 })
1775                .max()
1776                .unwrap_or(0);
1777            let column_difference = max_column - second_max_column;
1778            let ratios: Vec<i64> = widths
1779                .iter()
1780                .zip(wrapable)
1781                .map(|(&x, &w)| i64::from(x == max_column && w))
1782                .collect();
1783            if !ratios.iter().any(|&r| r != 0) || column_difference == 0 {
1784                break;
1785            }
1786            let max_reduce = vec![excess_width.min(column_difference); widths.len()];
1787            widths = ratio_reduce(excess_width, &ratios, &max_reduce, &widths);
1788            total_width = widths.iter().sum();
1789            excess_width = total_width - max_width;
1790        }
1791    }
1792    widths
1793}
1794
1795#[cfg(test)]
1796mod tests {
1797
1798    #[test]
1799    fn a_huge_column_ratio_does_not_overflow() {
1800        // Python ints never overflow. Expected output captured from rich 15.0.0
1801        // (`ratio=2**64 - 1`; the collapsed widths come out narrow there too).
1802        let console = crate::Console::builder()
1803            .width(40)
1804            .color_system(None)
1805            .build();
1806        let mut table = Table::new().expand(true);
1807        table.add_column("a").column_ratio(usize::MAX);
1808        table.add_column("b").column_ratio(1);
1809        table.add_row(&["x", "y"]);
1810        assert_eq!(
1811            console.render_to_string(&table) + "\n",
1812            "┏━━━┳━━━┓\n┃ a ┃ b ┃\n┡━━━╇━━━┩\n│ x │ y │\n└───┴───┘\n"
1813        );
1814    }
1815
1816    use super::*;
1817    use crate::color::ColorSystem;
1818    use crate::r#box::SQUARE;
1819
1820    fn console() -> Console {
1821        Console::builder()
1822            .force_terminal(true)
1823            .color_system(Some(ColorSystem::Truecolor))
1824            .width(40)
1825            .no_color(false)
1826            .build()
1827    }
1828
1829    #[test]
1830    fn owned_rows_preserve_measurement_styles_and_missing_cells() {
1831        use crate::protocol::OwnedTableRows;
1832        for width in [1, 12, 40, 80] {
1833            let console = Console::builder().width(width).force_terminal(true).build();
1834            let build = || {
1835                let mut table = Table::new()
1836                    .title("Rows")
1837                    .caption("owned or borrowed")
1838                    .show_lines(true);
1839                table.add_column("Name");
1840                table.add_column_justify("Value", Justify::Right);
1841                table
1842            };
1843            let mut borrowed = build();
1844            let mut owned = build();
1845            for row in [
1846                vec!["漢字\n🙂", "123"],
1847                vec!["short"],
1848                vec!["extra", "4", "ignored"],
1849            ] {
1850                borrowed.add_row(&row);
1851                owned.extend_owned_rows(vec![row.into_iter().map(str::to_owned).collect()]);
1852            }
1853            assert_eq!(
1854                console.render_to_string(&borrowed),
1855                console.render_to_string(&owned)
1856            );
1857        }
1858    }
1859
1860    #[test]
1861    fn simple_square_table() {
1862        let mut table = Table::new().box_set(SQUARE);
1863        table.add_column("Name");
1864        table.add_column("Age");
1865        table.add_row(&["Alice", "30"]);
1866        table.add_row(&["Bob", "7"]);
1867        let out = console().render_export(&table);
1868        let expected = concat!(
1869            "┌───────┬─────┐\n",
1870            "│\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",
1871            "├───────┼─────┤\n",
1872            "│ Alice │ 30  │\n",
1873            "│ Bob   │ 7   │\n",
1874            "└───────┴─────┘\n",
1875        );
1876        assert_eq!(out, expected);
1877    }
1878
1879    #[test]
1880    fn streamed_lines_match_styled_table_output() {
1881        let mut table = Table::new().box_set(SQUARE);
1882        table.add_column("Name");
1883        table.add_column("Age");
1884        table.add_row(&["Alice", "30"]);
1885        table.add_row(&["Bob", "7"]);
1886        let console = console();
1887        let mut streamed = String::new();
1888        table
1889            .try_for_each_line(&console, &console.options(), |line| {
1890                assert!(line.iter().all(|segment| !segment.text.contains('\n')));
1891                streamed.push_str(&console.segments_to_string(&line));
1892                streamed.push('\n');
1893                Ok::<_, std::convert::Infallible>(())
1894            })
1895            .unwrap();
1896        // `simple_square_table` above fixes these bytes independently of the
1897        // collection path, including distinct header-style segments.
1898        assert_eq!(streamed, console.render_export(&table));
1899        assert_eq!(streamed.lines().count(), 6);
1900    }
1901
1902    #[test]
1903    fn streamed_lines_stop_at_the_first_writer_error() {
1904        let mut table = Table::new()
1905            .box_set(SQUARE)
1906            .title("People")
1907            .caption("End")
1908            .show_lines(true);
1909        table.add_column("Name");
1910        table.add_row(&["Alice\nBob"]);
1911        table.add_row(&["Carol"]);
1912        let console = console();
1913        let mut visits = 0;
1914        let result = table.try_for_each_line(&console, &console.options(), |_| {
1915            visits += 1;
1916            if visits == 5 {
1917                Err("writer failed")
1918            } else {
1919                Ok(())
1920            }
1921        });
1922        assert_eq!(result, Err("writer failed"));
1923        assert_eq!(visits, 5);
1924    }
1925
1926    /// A column squeezed below its own padding still emitted a full left and
1927    /// right pad, so each such column spent two cells where its border spent
1928    /// one. The content row then overflowed the table and was cropped, losing
1929    /// its right-hand border while the border rows kept theirs.
1930    #[test]
1931    fn a_column_narrower_than_its_padding_stays_inside_the_border() {
1932        for ncols in [20usize, 29, 40] {
1933            let mut table = Table::new().box_set(SQUARE);
1934            for i in 0..ncols {
1935                table.add_column(format!("c{i}"));
1936            }
1937            let row: Vec<String> = (0..ncols).map(|i| i.to_string()).collect();
1938            table.add_row(&row.iter().map(String::as_str).collect::<Vec<_>>());
1939            let console = Console::builder().width(80).color_system(None).build();
1940            let out = console.render_to_string(&table);
1941            let rows: Vec<&str> = out.lines().filter(|l| !l.trim().is_empty()).collect();
1942            let widths: Vec<usize> = rows.iter().map(|r| r.chars().count()).collect();
1943            assert!(
1944                widths.iter().all(|w| *w == widths[0]),
1945                "{ncols} columns produced ragged rows: {widths:?}"
1946            );
1947            for (index, row) in rows.iter().enumerate() {
1948                let last = row.chars().last().expect("non-empty row");
1949                assert!(
1950                    !last.is_whitespace(),
1951                    "{ncols} columns: row {index} lost its right border: {row:?}"
1952                );
1953            }
1954        }
1955    }
1956
1957    /// A cell spanning several lines occupies its WIDEST line. Measuring the raw
1958    /// string made it as wide as all its lines summed — `\n` measures zero, so
1959    /// nothing capped it — and a quoted CSV cell holding two sentences blew its
1960    /// column out to 31 cells where upstream gives 23.
1961    #[test]
1962    fn a_multi_line_cell_is_measured_by_its_widest_line() {
1963        let mut table = Table::new().box_set(SQUARE);
1964        table.add_column("name");
1965        table.add_column("bio");
1966        table.add_row(&["Alice", "line one\nline two is much longer"]);
1967        table.add_row(&["Bob", "short"]);
1968        let console = Console::builder().width(60).color_system(None).build();
1969        let out = console.render_to_string(&table);
1970        let top = out.lines().next().expect("a top border");
1971        let width = top.chars().count();
1972        // "line two is much longer" is 23 cells; summing both lines would be 31.
1973        assert!(
1974            width < 40,
1975            "the multi-line cell was measured as the sum of its lines: {width} wide"
1976        );
1977        assert!(
1978            out.contains("line two is much longer"),
1979            "content lost: {out:?}"
1980        );
1981    }
1982}