Skip to main content

urushi/component/
table.rs

1//! Renderer-neutral tables with an independent public model.
2
3use std::{fmt, sync::Arc};
4
5use crate::text::wrapped_line_count;
6use crate::view::{CanvasMeasure, CanvasRequirements, Claim, Kind, distribute, fit_text_lines};
7use crate::{
8    Align, BlockStyle, Canvas, CanvasContext, CanvasItem, CanvasSizing, Composition, Grapheme,
9    Length, LineGlyphs, LineNetwork, Overflow, Position, PrintableLines, PrintableText, Sides,
10    TableRole, TextStyle, VerticalAlign, View,
11};
12
13/// Which row of a bound table frame a cell belongs to.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15enum TableRowKind {
16    Header,
17    Body(usize),
18}
19
20/// Identifies one cell while a [`TableRowPresentation`] resolves its style.
21///
22/// Header styles are resolved by [`TablePresentation::header_styles`], so this
23/// context always identifies a body cell. The row number counts visible body
24/// rows after offsets are applied.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct TableCell<'a> {
27    text: &'a str,
28    row: usize,
29    column: usize,
30}
31
32impl<'a> TableCell<'a> {
33    /// Returns the zero-based visible body-row number.
34    pub const fn row(&self) -> usize {
35        self.row
36    }
37
38    /// Returns the zero-based column index.
39    pub const fn column(&self) -> usize {
40        self.column
41    }
42
43    /// Returns the cell's text, before wrapping and padding.
44    pub const fn text(&self) -> &'a str {
45        self.text
46    }
47}
48
49/// The visible position of one typed body row during composition.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct TableRowPosition {
52    index: usize,
53    len: usize,
54}
55
56impl TableRowPosition {
57    /// Returns this row's zero-based index after offsets are applied.
58    pub const fn index(self) -> usize {
59        self.index
60    }
61
62    /// Returns the number of visible body rows.
63    pub const fn len(self) -> usize {
64        self.len
65    }
66
67    /// Returns whether there are no visible body rows.
68    ///
69    /// A formatter is only called for an existing row, so this is always
70    /// `false` for positions supplied by [`TableRowPresentation`].
71    pub const fn is_empty(self) -> bool {
72        self.len == 0
73    }
74}
75
76/// A short-lived destination for the formatted cells of one row.
77#[derive(Debug, Default)]
78pub struct TableRowCells {
79    values: Vec<String>,
80}
81
82impl TableRowCells {
83    /// Appends one value using its canonical [`fmt::Display`] representation.
84    pub fn display(&mut self, value: impl fmt::Display) {
85        self.values.push(value.to_string());
86    }
87
88    /// Appends already formatted plain text.
89    pub fn text(&mut self, text: impl Into<String>) {
90        self.values.push(text.into());
91    }
92}
93
94/// Canonically formats one typed value as a table row.
95///
96/// Derive this trait with the [`TableRow` derive macro](macro@crate::TableRow)
97/// for the ordinary case.
98/// Non-skipped fields are formatted through [`fmt::Display`] in declaration
99/// order. Headers remain explicit Table data.
100pub trait TableRow {
101    /// Writes this row's cells in column order.
102    fn write_cells(&self, cells: &mut TableRowCells);
103}
104
105/// One normalized row used by [`Table::text`] for direct plain-text input.
106#[derive(Debug, Clone, Default, PartialEq, Eq)]
107pub struct TextTableRow(Vec<String>);
108
109impl<I, S> From<I> for TextTableRow
110where
111    I: IntoIterator<Item = S>,
112    S: Into<String>,
113{
114    fn from(values: I) -> Self {
115        Self(values.into_iter().map(Into::into).collect())
116    }
117}
118
119impl TableRow for TextTableRow {
120    fn write_cells(&self, cells: &mut TableRowCells) {
121        cells.values.extend(self.0.iter().cloned());
122    }
123}
124
125type RowFormatter<'a, T> = dyn Fn(&T, TableRowPosition, &mut TableRowCells) + 'a;
126type RowCellStyler<'a, T> = dyn Fn(&T, TableCell<'_>, &BlockStyle) -> Option<BlockStyle> + 'a;
127
128/// Typed row formatting and optional complete cell-style replacements.
129#[derive(Clone)]
130pub struct TableRowPresentation<'a, T> {
131    format: Arc<RowFormatter<'a, T>>,
132    cell_style: Option<Arc<RowCellStyler<'a, T>>>,
133}
134
135impl<T> fmt::Debug for TableRowPresentation<'_, T> {
136    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
137        formatter.write_str("TableRowPresentation { .. }")
138    }
139}
140
141impl<'a, T: 'a> TableRowPresentation<'a, T> {
142    /// Creates a custom typed row formatter with no cell overrides.
143    pub fn new<F>(format: F) -> Self
144    where
145        F: Fn(&T, TableRowPosition, &mut TableRowCells) + 'a,
146    {
147        Self {
148            format: Arc::new(format),
149            cell_style: None,
150        }
151    }
152
153    /// Replaces the optional typed cell-style policy.
154    ///
155    /// The callback receives the complete column or table fallback style.
156    /// `None` keeps that style; `Some` is a complete replacement.
157    #[must_use]
158    pub fn cell_style<S>(mut self, cell_style: S) -> Self
159    where
160        S: Fn(&T, TableCell<'_>, &BlockStyle) -> Option<BlockStyle> + 'a,
161    {
162        self.cell_style = Some(Arc::new(cell_style));
163        self
164    }
165}
166
167impl<'a, T> TableRowPresentation<'a, T>
168where
169    T: TableRow + 'a,
170{
171    /// Uses the row's canonical [`TableRow`] formatting implementation.
172    pub fn display() -> Self {
173        Self::new(|row: &T, _, cells| row.write_cells(cells))
174    }
175}
176
177#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
178struct RowOffset {
179    start: usize,
180    end: usize,
181}
182
183/// Owned table data independent of presentation policy.
184///
185/// `Table` owns semantic content, structural visibility, and offsets only. Every
186/// presentation concern — borders, padding, width, and cell styles — belongs to
187/// [`TablePresentation`].
188///
189/// Rows may be ragged: a row shorter than the widest row is padded with empty
190/// cells when its [`TableRow`] formatting is composed. Use [`Table::text`] for
191/// rows that are already plain text.
192///
193/// ```
194/// use urushi::{BlockStyle, Table, TablePresentation, TextStyle, measure};
195///
196/// let table = Table::text()
197///     .headers(["Name", "Location"])
198///     .row(["Kini", "New York"])
199///     .row(["Iris", "Paris"]);
200///
201/// let presentation =
202///     TablePresentation::new(BlockStyle::new(), BlockStyle::new(), TextStyle::new());
203/// assert_eq!(measure(&presentation.compose(&table)).height(), 6);
204/// ```
205#[derive(Debug, Clone, PartialEq, Eq)]
206pub struct Table<Row = TextTableRow> {
207    headers: Vec<String>,
208    rows: Vec<Row>,
209    hidden: bool,
210    offset: RowOffset,
211}
212
213impl<Row> Default for Table<Row> {
214    fn default() -> Self {
215        Self {
216            headers: Vec::new(),
217            rows: Vec::new(),
218            hidden: false,
219            offset: RowOffset::default(),
220        }
221    }
222}
223
224impl<Row> Table<Row> {
225    /// Creates an empty table.
226    pub fn new() -> Self {
227        Self::default()
228    }
229
230    /// Replaces the header cells, each of which is plain text.
231    ///
232    /// A table without headers composes its body rows only. Escape sequences
233    /// and cursor movement in a header break the plain-text contract:
234    /// debug builds panic, and release builds measure them as ordinary
235    /// characters and may split them when wrapping or truncating. Raw ANSI is not accepted as component text.
236    #[must_use]
237    pub fn headers<I, S>(mut self, headers: I) -> Self
238    where
239        I: IntoIterator<Item = S>,
240        S: Into<String>,
241    {
242        self.headers = headers.into_iter().map(Into::into).collect();
243        self
244    }
245
246    /// Appends one body row.
247    #[must_use]
248    pub fn row(mut self, row: Row) -> Self {
249        self.rows.push(row);
250        self
251    }
252
253    /// Appends typed body rows in iteration order.
254    #[must_use]
255    pub fn rows<I>(mut self, rows: I) -> Self
256    where
257        I: IntoIterator<Item = Row>,
258    {
259        self.rows.extend(rows);
260        self
261    }
262
263    /// Includes or excludes the complete table.
264    #[must_use]
265    pub const fn hidden(mut self, hidden: bool) -> Self {
266        self.hidden = hidden;
267        self
268    }
269
270    /// Omits `start` body rows from the front and `end` from the back.
271    ///
272    /// Headers are never omitted; the offset selects among body rows only.
273    #[must_use]
274    pub const fn offset(mut self, start: usize, end: usize) -> Self {
275        self.offset = RowOffset { start, end };
276        self
277    }
278
279    /// Returns whether the table is excluded.
280    pub const fn is_hidden(&self) -> bool {
281        self.hidden
282    }
283
284    fn visible_rows(&self) -> &[Row] {
285        let end = self.rows.len().saturating_sub(self.offset.end);
286        if self.offset.start >= end {
287            return &[];
288        }
289        &self.rows[self.offset.start..end]
290    }
291}
292
293impl<Row> Table<Row>
294where
295    Row: TableRow,
296{
297    /// Returns the column count from the widest canonical header or body row.
298    ///
299    /// This formats every owned row, including rows omitted by the offset.
300    /// Composition formats only visible rows, so an omitted wide row does not
301    /// add columns to the composed view.
302    pub fn column_count(&self) -> usize {
303        self.rows
304            .iter()
305            .map(|row| {
306                let mut cells = TableRowCells::default();
307                row.write_cells(&mut cells);
308                cells.values.len()
309            })
310            .chain(std::iter::once(self.headers.len()))
311            .max()
312            .unwrap_or_default()
313    }
314}
315
316impl Table<TextTableRow> {
317    /// Creates an empty table that accepts rows of direct plain text.
318    pub fn text() -> TextTable {
319        TextTable { table: Self::new() }
320    }
321}
322
323/// A direct-text Table builder that normalizes every row into owned strings.
324#[derive(Debug, Clone, Default, PartialEq, Eq)]
325pub struct TextTable {
326    table: Table<TextTableRow>,
327}
328
329impl TextTable {
330    /// Replaces the plain-text header cells.
331    #[must_use]
332    pub fn headers<I, S>(mut self, headers: I) -> Self
333    where
334        I: IntoIterator<Item = S>,
335        S: Into<String>,
336    {
337        self.table = self.table.headers(headers);
338        self
339    }
340
341    /// Appends one direct-text body row.
342    #[must_use]
343    pub fn row<I, S>(mut self, row: I) -> Self
344    where
345        I: IntoIterator<Item = S>,
346        S: Into<String>,
347    {
348        self.table.rows.push(TextTableRow::from(row));
349        self
350    }
351
352    /// Appends direct-text body rows in iteration order.
353    #[must_use]
354    pub fn rows<I, R, S>(mut self, rows: I) -> Self
355    where
356        I: IntoIterator<Item = R>,
357        R: IntoIterator<Item = S>,
358        S: Into<String>,
359    {
360        self.table
361            .rows
362            .extend(rows.into_iter().map(TextTableRow::from));
363        self
364    }
365
366    /// Includes or excludes the complete table.
367    #[must_use]
368    pub fn hidden(mut self, hidden: bool) -> Self {
369        self.table = self.table.hidden(hidden);
370        self
371    }
372
373    /// Omits body rows from the front and back.
374    #[must_use]
375    pub fn offset(mut self, start: usize, end: usize) -> Self {
376        self.table = self.table.offset(start, end);
377        self
378    }
379
380    /// Returns the widest owned row, including headers.
381    pub fn column_count(&self) -> usize {
382        self.table.column_count()
383    }
384}
385
386impl std::ops::Deref for TextTable {
387    type Target = Table<TextTableRow>;
388
389    fn deref(&self) -> &Self::Target {
390        &self.table
391    }
392}
393
394/// Line model used by a [`TablePresentation`].
395///
396/// Connected grids use a [`LineGlyphs`] repertoire so [`LineNetwork`] can
397/// derive every corner, tee, and crossing from the recorded geometry.
398/// Markdown and booktabs tables contain only independent straight rules and
399/// therefore use Canvas cell lines instead.
400///
401/// ```
402/// use urushi::{
403///     BlockStyle, LineGlyphs, Table, TableBorder, TablePresentation, TextStyle,
404/// };
405///
406/// let table = Table::text().headers(["Name"]).row(["Iris"]);
407/// let presentation = TablePresentation::new(
408///     BlockStyle::new(),
409///     BlockStyle::new(),
410///     TextStyle::new(),
411/// )
412/// .border(TableBorder::network(LineGlyphs::ROUNDED));
413/// let _view = presentation.compose(&table);
414/// ```
415#[derive(Debug, Clone, Copy, PartialEq, Eq)]
416pub struct TableBorder(TableBorderKind);
417
418#[derive(Debug, Clone, Copy, PartialEq, Eq)]
419enum TableBorderKind {
420    Network(LineGlyphs),
421    Markdown,
422    Booktabs,
423}
424
425impl TableBorder {
426    /// A square single-line grid.
427    pub const NORMAL: Self = Self::network(LineGlyphs::NORMAL);
428    /// A single-line grid with rounded outer corners.
429    pub const ROUNDED: Self = Self::network(LineGlyphs::ROUNDED);
430    /// A heavy-line grid.
431    pub const THICK: Self = Self::network(LineGlyphs::THICK);
432    /// A double-line grid.
433    pub const DOUBLE: Self = Self::network(LineGlyphs::DOUBLE);
434    /// An ASCII-only grid.
435    pub const ASCII: Self = Self::network(LineGlyphs::ASCII);
436    /// A Markdown table made from `-` and `|` cell lines.
437    pub const MARKDOWN: Self = Self(TableBorderKind::Markdown);
438    /// A booktabs table with heavy outer rules and light inner rules.
439    pub const BOOKTABS: Self = Self(TableBorderKind::Booktabs);
440    /// An invisible grid that still occupies its rule rows and columns.
441    pub const HIDDEN: Self = Self::network(LineGlyphs::HIDDEN);
442
443    /// Creates a connected grid with a caller-owned glyph repertoire.
444    pub const fn network(glyphs: LineGlyphs) -> Self {
445        Self(TableBorderKind::Network(glyphs))
446    }
447}
448
449/// Presentation policy used to compose a [`Table`] into a [`View`].
450///
451/// The policy owns the border, the edge toggles, cell padding, the total width
452/// constraint, and the per-cell styling strategy. It borrows the data and
453/// performs no terminal output.
454#[derive(Debug, Clone)]
455pub struct TablePresentation {
456    header_style: BlockStyle,
457    cell_style: BlockStyle,
458    header_styles: Vec<Option<BlockStyle>>,
459    column_styles: Vec<Option<BlockStyle>>,
460    border_style: TextStyle,
461    border: TableBorder,
462    border_top: bool,
463    border_bottom: bool,
464    border_left: bool,
465    border_right: bool,
466    border_header: bool,
467    border_column: bool,
468    border_row: bool,
469    padding: u16,
470    width: Option<u16>,
471}
472
473impl PartialEq for TablePresentation {
474    fn eq(&self, other: &Self) -> bool {
475        self.header_style == other.header_style
476            && self.cell_style == other.cell_style
477            && self.header_styles == other.header_styles
478            && self.column_styles == other.column_styles
479            && self.border_style == other.border_style
480            && self.border == other.border
481            && self.border_top == other.border_top
482            && self.border_bottom == other.border_bottom
483            && self.border_left == other.border_left
484            && self.border_right == other.border_right
485            && self.border_header == other.border_header
486            && self.border_column == other.border_column
487            && self.border_row == other.border_row
488            && self.padding == other.padding
489            && self.width == other.width
490    }
491}
492
493impl TablePresentation {
494    /// Creates a table presentation with a single-line border and one cell of padding.
495    pub fn new(header_style: BlockStyle, cell_style: BlockStyle, border_style: TextStyle) -> Self {
496        Self {
497            header_style,
498            cell_style,
499            header_styles: Vec::new(),
500            column_styles: Vec::new(),
501            border_style,
502            border: TableBorder::NORMAL,
503            border_top: true,
504            border_bottom: true,
505            border_left: true,
506            border_right: true,
507            border_header: true,
508            border_column: true,
509            border_row: false,
510            padding: 1,
511            width: None,
512        }
513    }
514
515    /// Returns the block style assigned to one logical table cell role.
516    pub const fn get_style(&self, role: TableRole) -> &BlockStyle {
517        match role {
518            TableRole::Header => &self.header_style,
519            TableRole::Cell => &self.cell_style,
520        }
521    }
522
523    /// Returns the style drawn on the table's rules.
524    pub const fn get_border_style(&self) -> &TextStyle {
525        &self.border_style
526    }
527
528    /// Replaces the block style assigned to one logical table cell role.
529    #[must_use]
530    pub fn style(mut self, role: TableRole, style: BlockStyle) -> Self {
531        match role {
532            TableRole::Header => self.header_style = style,
533            TableRole::Cell => self.cell_style = style,
534        }
535        self
536    }
537
538    /// Replaces the header-cell style.
539    #[must_use]
540    pub fn header_style(self, style: BlockStyle) -> Self {
541        self.style(TableRole::Header, style)
542    }
543
544    /// Replaces the body-cell style.
545    #[must_use]
546    pub fn cell_style(self, style: BlockStyle) -> Self {
547        self.style(TableRole::Cell, style)
548    }
549
550    /// Replaces positional complete header-cell styles.
551    ///
552    /// A missing or `None` entry keeps the table header style. Extra entries
553    /// do not create columns.
554    #[must_use]
555    pub fn header_styles<I>(mut self, styles: I) -> Self
556    where
557        I: IntoIterator<Item = Option<BlockStyle>>,
558    {
559        self.header_styles = styles.into_iter().collect();
560        self
561    }
562
563    /// Replaces positional complete body-column styles.
564    ///
565    /// A missing or `None` entry keeps the table cell style. Extra entries do
566    /// not create columns.
567    #[must_use]
568    pub fn column_styles<I>(mut self, styles: I) -> Self
569    where
570        I: IntoIterator<Item = Option<BlockStyle>>,
571    {
572        self.column_styles = styles.into_iter().collect();
573        self
574    }
575
576    /// Replaces the style drawn on the table's rules.
577    #[must_use]
578    pub fn border_style(mut self, style: TextStyle) -> Self {
579        self.border_style = style;
580        self
581    }
582
583    /// Replaces the table's line model and glyph repertoire.
584    #[must_use]
585    pub const fn border(mut self, border: TableBorder) -> Self {
586        self.border = border;
587        self
588    }
589
590    /// Draws or omits the top edge.
591    #[must_use]
592    pub const fn border_top(mut self, enabled: bool) -> Self {
593        self.border_top = enabled;
594        self
595    }
596
597    /// Draws or omits the bottom edge.
598    #[must_use]
599    pub const fn border_bottom(mut self, enabled: bool) -> Self {
600        self.border_bottom = enabled;
601        self
602    }
603
604    /// Draws or omits the left edge.
605    #[must_use]
606    pub const fn border_left(mut self, enabled: bool) -> Self {
607        self.border_left = enabled;
608        self
609    }
610
611    /// Draws or omits the right edge.
612    #[must_use]
613    pub const fn border_right(mut self, enabled: bool) -> Self {
614        self.border_right = enabled;
615        self
616    }
617
618    /// Draws or omits the rule between the header and the first body row.
619    #[must_use]
620    pub const fn border_header(mut self, enabled: bool) -> Self {
621        self.border_header = enabled;
622        self
623    }
624
625    /// Draws or omits the rules between columns.
626    ///
627    /// A connected table uses its [`LineGlyphs::vertical`] glyph for outer and
628    /// inner vertical rules. A straight-rule preset supplies its own vertical
629    /// marker.
630    #[must_use]
631    pub const fn border_column(mut self, enabled: bool) -> Self {
632        self.border_column = enabled;
633        self
634    }
635
636    /// Draws or omits the rules between body rows.
637    #[must_use]
638    pub const fn border_row(mut self, enabled: bool) -> Self {
639        self.border_row = enabled;
640        self
641    }
642
643    /// Sets the horizontal padding applied to both sides of every cell.
644    #[must_use]
645    pub const fn padding(mut self, padding: u16) -> Self {
646        self.padding = padding;
647        self
648    }
649
650    /// States the total width the table takes, in terminal cells.
651    ///
652    /// A total width is a statement about the box the table sits in, so
653    /// [`compose`](Self::compose) puts its Canvas in a borderless block of
654    /// exactly this width. The bound table frame then gives every column an
655    /// equal share, which is why a
656    /// width much wider than the content can leave a short column wider than a
657    /// long one, and a width narrower than the content narrows every column
658    /// rather than only the widest.
659    ///
660    /// Nothing is measured while composing. The bound frame decides columns
661    /// and the lines between them together when the view resolves, so they cannot
662    /// disagree, and a column floors at the narrowest grapheme it must show:
663    /// a table given less than its floor is as wide as that floor, wrapping
664    /// its content rather than dropping it.
665    ///
666    /// Without a stated width the table is as wide as its content, and an area
667    /// narrower than that shrinks it by the same rule. This states the width
668    /// for a caller who wants one regardless of the area.
669    ///
670    /// The width is a `u16` for the same reason [`BlockStyle::width`] is: a
671    /// terminal dimension, not an arbitrary count.
672    #[must_use]
673    pub const fn width(mut self, width: u16) -> Self {
674        self.width = Some(width);
675        self
676    }
677
678    /// Composes table data into an intrinsically sized, renderer-neutral Canvas.
679    ///
680    /// Composition binds an owned snapshot without receiving an available
681    /// area. During resolution, the bound item first supplies width and
682    /// width-dependent height requirements, then records cell and rule
683    /// commands after the final Canvas size is known.
684    pub fn compose<T>(&self, table: &Table<T>) -> View
685    where
686        T: TableRow,
687    {
688        self.compose_with(table, &TableRowPresentation::display())
689    }
690
691    /// Composes typed table data with custom row formatting and cell styles.
692    pub fn compose_with<T>(&self, table: &Table<T>, rows: &TableRowPresentation<'_, T>) -> View {
693        if table.is_hidden() {
694            return View::empty();
695        }
696
697        let visible_rows = table.visible_rows();
698        let mut formatted = Vec::with_capacity(visible_rows.len());
699        for (index, row) in visible_rows.iter().enumerate() {
700            let mut cells = TableRowCells::default();
701            (rows.format)(
702                row,
703                TableRowPosition {
704                    index,
705                    len: visible_rows.len(),
706                },
707                &mut cells,
708            );
709            formatted.push(cells.values);
710        }
711        // Only the visible rows may widen the composed table; a wide row the
712        // offset excluded must not leave empty columns behind.
713        let columns = column_count(&formatted, table.headers.len());
714        if columns == 0 {
715            return View::empty();
716        }
717
718        let item = TableItem(Arc::new(TableFrame::new(
719            table,
720            self,
721            visible_rows,
722            &formatted,
723            rows.cell_style.as_deref(),
724            columns,
725        )));
726        let canvas = Canvas::new().sizing(item.sizing()).item(item);
727        let frame = self.width.map_or_else(BlockStyle::new, |width| {
728            BlockStyle::new().width(Length::Cells(width))
729        });
730        View::block(frame, View::canvas(canvas))
731    }
732
733    fn fallback_style(&self, kind: TableRowKind, column: usize) -> &BlockStyle {
734        let styles = match kind {
735            TableRowKind::Header => &self.header_styles,
736            TableRowKind::Body(_) => &self.column_styles,
737        };
738        styles
739            .get(column)
740            .and_then(Option::as_ref)
741            .unwrap_or(match kind {
742                TableRowKind::Header => &self.header_style,
743                TableRowKind::Body(_) => &self.cell_style,
744            })
745    }
746}
747
748#[derive(Debug, Clone, PartialEq)]
749struct TableFrame {
750    presentation: TablePresentation,
751    rows: Vec<TableFrameRow>,
752    columns: usize,
753}
754
755impl TableFrame {
756    fn new<T>(
757        table: &Table<T>,
758        presentation: &TablePresentation,
759        visible_rows: &[T],
760        formatted_rows: &[Vec<String>],
761        cell_style: Option<&RowCellStyler<'_, T>>,
762        columns: usize,
763    ) -> Self {
764        let mut rows =
765            Vec::with_capacity(visible_rows.len() + usize::from(!table.headers.is_empty()));
766        if !table.headers.is_empty() {
767            rows.push(TableFrameRow::new(
768                &table.headers,
769                columns,
770                TableRowKind::Header,
771                presentation,
772                |_, _| None,
773            ));
774        }
775        for (index, (row, formatted)) in visible_rows.iter().zip(formatted_rows).enumerate() {
776            rows.push(TableFrameRow::new(
777                formatted,
778                columns,
779                TableRowKind::Body(index),
780                presentation,
781                |cell, fallback| cell_style.and_then(|style| style(row, cell, fallback)),
782            ));
783        }
784        let mut presentation = presentation.clone();
785        presentation.header_style =
786            frame_cell_style(presentation.header_style, presentation.padding);
787        presentation.cell_style = frame_cell_style(presentation.cell_style, presentation.padding);
788        for style in presentation
789            .header_styles
790            .iter_mut()
791            .chain(&mut presentation.column_styles)
792            .flatten()
793        {
794            *style = frame_cell_style(style.clone(), presentation.padding);
795        }
796        Self {
797            presentation,
798            rows,
799            columns,
800        }
801    }
802
803    fn line_width(&self) -> usize {
804        usize::from(self.presentation.border_left)
805            + usize::from(self.presentation.border_right)
806            + usize::from(self.presentation.border_column) * self.columns.saturating_sub(1)
807    }
808
809    fn line_height(&self) -> usize {
810        usize::from(self.presentation.border_top)
811            + usize::from(self.presentation.border_bottom)
812            + (0..self.rows.len().saturating_sub(1))
813                .filter(|gap| self.draws_row_rule(*gap))
814                .count()
815    }
816
817    fn draws_row_rule(&self, gap: usize) -> bool {
818        match self.rows.get(gap).map(|row| row.kind) {
819            Some(TableRowKind::Header) => self.presentation.border_header,
820            _ => self.presentation.border_row,
821        }
822    }
823
824    fn column_requirements(&self) -> Vec<(usize, usize)> {
825        (0..self.columns)
826            .map(|column| {
827                self.rows
828                    .iter()
829                    .map(|row| {
830                        let cell = &row.cells[column];
831                        cell.width_requirements(cell.style(&self.presentation, row.kind, column))
832                    })
833                    .fold((0, 0), |(demand, floor), next| {
834                        (demand.max(next.0), floor.max(next.1))
835                    })
836            })
837            .collect()
838    }
839
840    fn column_widths(&self, width: usize) -> Vec<usize> {
841        let requirements = self.column_requirements();
842        let kind = if self.presentation.width.is_some() {
843            Kind::fill(1)
844        } else {
845            Kind::Auto
846        };
847        let claims: Vec<Claim> = requirements
848            .iter()
849            .map(|(demand, floor)| Claim {
850                kind,
851                demand: *demand,
852                floor: *floor,
853            })
854            .collect();
855        distribute(width.saturating_sub(self.line_width()), &claims)
856    }
857
858    fn row_heights(&self, columns: &[usize]) -> Vec<usize> {
859        self.rows
860            .iter()
861            .map(|row| {
862                row.cells
863                    .iter()
864                    .enumerate()
865                    .zip(columns)
866                    .map(|((column, cell), width)| {
867                        cell.height_at(*width, cell.style(&self.presentation, row.kind, column))
868                    })
869                    .max()
870                    .unwrap_or(0)
871            })
872            .collect()
873    }
874
875    fn height_at(&self, width: usize) -> usize {
876        self.row_heights(&self.column_widths(width))
877            .iter()
878            .sum::<usize>()
879            .saturating_add(self.line_height())
880    }
881
882    fn draw(&self, context: &mut CanvasContext) {
883        let columns = self.column_widths(context.size().width());
884        let heights = self.row_heights(&columns);
885
886        self.record_cell_rows(context, &columns, &heights);
887        match self.presentation.border.0 {
888            TableBorderKind::Network(glyphs) => {
889                self.record_line_network(context, &columns, &heights, glyphs);
890            }
891            TableBorderKind::Markdown => {
892                self.record_straight_rules(context, &columns, &heights, '|', '-', '-', '-', true);
893            }
894            TableBorderKind::Booktabs => {
895                self.record_straight_rules(context, &columns, &heights, ' ', '━', '─', '━', false);
896            }
897        }
898    }
899
900    fn record_cell_rows(&self, context: &mut CanvasContext, columns: &[usize], heights: &[usize]) {
901        let mut y = usize::from(self.presentation.border_top);
902        for (row_index, (row, height)) in self.rows.iter().zip(heights).enumerate() {
903            let rendered = row
904                .cells
905                .iter()
906                .zip(columns)
907                .enumerate()
908                .map(|(column, (cell, width))| {
909                    cell.rendered_rows(
910                        *width,
911                        *height,
912                        cell.style(&self.presentation, row.kind, column),
913                    )
914                })
915                .collect::<Vec<_>>();
916            for line in 0..*height {
917                let mut runs = Vec::<TextRun>::new();
918                let mut x = 0;
919                if self.presentation.border_left {
920                    push_run(&mut runs, x, " ", TextStyle::new());
921                    x += 1;
922                }
923                for (column, ((cell, rows), width)) in
924                    row.cells.iter().zip(&rendered).zip(columns).enumerate()
925                {
926                    let style = cell.style(&self.presentation, row.kind, column);
927                    push_run(&mut runs, x, &rows[line], style.text_style().clone());
928                    x = x.saturating_add(*width);
929                    if column + 1 < columns.len() && self.presentation.border_column {
930                        push_run(&mut runs, x, " ", TextStyle::new());
931                        x += 1;
932                    }
933                }
934                if self.presentation.border_right {
935                    push_run(&mut runs, x, " ", TextStyle::new());
936                }
937                for run in runs {
938                    context.text_with(
939                        Position::new(position(run.x), position(y + line)),
940                        run.text,
941                        run.style,
942                        Composition::Replace,
943                    );
944                }
945            }
946            y = y.saturating_add(*height);
947            if row_index + 1 < self.rows.len() && self.draws_row_rule(row_index) {
948                y += 1;
949            }
950        }
951    }
952
953    fn rule_columns(&self, columns: &[usize]) -> Vec<usize> {
954        let mut rules = Vec::with_capacity(columns.len() + 1);
955        let mut x = 0usize;
956        if self.presentation.border_left {
957            rules.push(x);
958            x += 1;
959        }
960        for (index, width) in columns.iter().enumerate() {
961            if index > 0 && self.presentation.border_column {
962                rules.push(x);
963                x += 1;
964            }
965            x = x.saturating_add(*width);
966        }
967        if self.presentation.border_right {
968            rules.push(x);
969        }
970        rules
971    }
972
973    fn rule_rows(&self, heights: &[usize]) -> Vec<usize> {
974        let mut rules = Vec::with_capacity(self.rows.len() + 1);
975        let mut y = 0usize;
976        if self.presentation.border_top {
977            rules.push(y);
978            y += 1;
979        }
980        for (row_index, height) in heights.iter().enumerate() {
981            y = y.saturating_add(*height);
982            if row_index + 1 < heights.len() && self.draws_row_rule(row_index) {
983                rules.push(y);
984                y += 1;
985            }
986        }
987        if self.presentation.border_bottom {
988            rules.push(y);
989        }
990        rules
991    }
992
993    fn record_line_network(
994        &self,
995        context: &mut CanvasContext,
996        columns: &[usize],
997        heights: &[usize],
998        glyphs: LineGlyphs,
999    ) {
1000        let width = columns
1001            .iter()
1002            .sum::<usize>()
1003            .saturating_add(self.line_width());
1004        let height = heights
1005            .iter()
1006            .sum::<usize>()
1007            .saturating_add(self.line_height());
1008        if width == 0 || height == 0 {
1009            return;
1010        }
1011
1012        let rows = self.rule_rows(heights);
1013        let columns = self.rule_columns(columns);
1014        if rows.is_empty() && columns.is_empty() {
1015            return;
1016        }
1017
1018        let mut network = LineNetwork::new(glyphs, self.rule_style());
1019        // A disabled outer edge removes its rule cell, not the logical
1020        // continuation of a perpendicular rule. Keep that incidence just
1021        // outside the Canvas so clipping preserves straight lines and the
1022        // corners next to zero-width cells.
1023        let first_x = if self.presentation.border_left { 0 } else { -1 };
1024        let last_x = if self.presentation.border_right {
1025            position(width - 1)
1026        } else {
1027            position(width)
1028        };
1029        let first_y = if self.presentation.border_top { 0 } else { -1 };
1030        let last_y = if self.presentation.border_bottom {
1031            position(height - 1)
1032        } else {
1033            position(height)
1034        };
1035        for y in rows {
1036            network.horizontal(position(y), first_x..=last_x);
1037        }
1038        for x in columns {
1039            network.vertical(position(x), first_y..=last_y);
1040        }
1041        context.line_network(network);
1042    }
1043
1044    #[allow(clippy::too_many_arguments)]
1045    fn record_straight_rules(
1046        &self,
1047        context: &mut CanvasContext,
1048        columns: &[usize],
1049        heights: &[usize],
1050        vertical: char,
1051        top: char,
1052        middle: char,
1053        bottom: char,
1054        vertical_last: bool,
1055    ) {
1056        let width = columns
1057            .iter()
1058            .sum::<usize>()
1059            .saturating_add(self.line_width());
1060        let height = heights
1061            .iter()
1062            .sum::<usize>()
1063            .saturating_add(self.line_height());
1064        if width == 0 || height == 0 {
1065            return;
1066        }
1067
1068        let record_verticals = |context: &mut CanvasContext| {
1069            for x in self.rule_columns(columns) {
1070                self.record_cell_line(context, x, 0, x, height - 1, vertical);
1071            }
1072        };
1073        let record_horizontals = |context: &mut CanvasContext| {
1074            let rows = self.rule_rows(heights);
1075            for (index, y) in rows.iter().copied().enumerate() {
1076                let glyph = if self.presentation.border_top && index == 0 {
1077                    top
1078                } else if self.presentation.border_bottom && index + 1 == rows.len() {
1079                    bottom
1080                } else {
1081                    middle
1082                };
1083                self.record_cell_line(context, 0, y, width - 1, y, glyph);
1084            }
1085        };
1086
1087        if vertical_last {
1088            record_horizontals(context);
1089            record_verticals(context);
1090        } else {
1091            record_verticals(context);
1092            record_horizontals(context);
1093        }
1094    }
1095
1096    fn record_cell_line(
1097        &self,
1098        context: &mut CanvasContext,
1099        from_x: usize,
1100        from_y: usize,
1101        to_x: usize,
1102        to_y: usize,
1103        glyph: char,
1104    ) {
1105        let glyph = glyph.to_string();
1106        context.line(
1107            Position::new(position(from_x), position(from_y)),
1108            Position::new(position(to_x), position(to_y)),
1109            Grapheme::new(&glyph),
1110            self.rule_style(),
1111        );
1112    }
1113
1114    fn rule_style(&self) -> TextStyle {
1115        let mut style = TextStyle::new();
1116        if let Some(color) = self.presentation.border_style.get_foreground() {
1117            style = style.foreground(color);
1118        }
1119        if let Some(color) = self.presentation.border_style.get_background() {
1120            style = style.background(color);
1121        }
1122        style
1123    }
1124}
1125
1126#[derive(Debug, Clone, PartialEq)]
1127struct TableFrameRow {
1128    kind: TableRowKind,
1129    cells: Vec<TableFrameCell>,
1130}
1131
1132impl TableFrameRow {
1133    fn new<F>(
1134        values: &[String],
1135        columns: usize,
1136        kind: TableRowKind,
1137        presentation: &TablePresentation,
1138        mut override_style: F,
1139    ) -> Self
1140    where
1141        F: FnMut(TableCell<'_>, &BlockStyle) -> Option<BlockStyle>,
1142    {
1143        let cells = (0..columns)
1144            .map(|column| {
1145                let text = values.get(column).map_or("", String::as_str);
1146                let style_override = match kind {
1147                    TableRowKind::Header => None,
1148                    TableRowKind::Body(row) => {
1149                        let cell = TableCell { text, row, column };
1150                        override_style(cell, presentation.fallback_style(kind, column))
1151                    }
1152                };
1153                TableFrameCell {
1154                    text: text.to_owned(),
1155                    style_override: style_override
1156                        .map(|style| frame_cell_style(style, presentation.padding)),
1157                }
1158            })
1159            .collect();
1160        Self { kind, cells }
1161    }
1162}
1163
1164#[derive(Debug, Clone, PartialEq)]
1165struct TableFrameCell {
1166    text: String,
1167    style_override: Option<BlockStyle>,
1168}
1169
1170impl TableFrameCell {
1171    fn style<'a>(
1172        &'a self,
1173        presentation: &'a TablePresentation,
1174        kind: TableRowKind,
1175        column: usize,
1176    ) -> &'a BlockStyle {
1177        self.style_override
1178            .as_ref()
1179            .unwrap_or_else(|| presentation.fallback_style(kind, column))
1180    }
1181
1182    fn width_requirements(&self, style: &BlockStyle) -> (usize, usize) {
1183        let padding = style.get_padding();
1184        let frame = usize::from(padding.left) + usize::from(padding.right);
1185        let lines = PrintableLines::new(&self.text).lines();
1186        let demand = lines.iter().map(|line| line.width()).max().unwrap_or(0);
1187        let floor = lines
1188            .iter()
1189            .flat_map(|line| line.graphemes().map(Grapheme::width))
1190            .max()
1191            .unwrap_or(0);
1192        (demand.saturating_add(frame), floor.saturating_add(frame))
1193    }
1194
1195    fn height_at(&self, width: usize, style: &BlockStyle) -> usize {
1196        let padding = style.get_padding();
1197        let content_width = width
1198            .saturating_sub(usize::from(padding.left).saturating_add(usize::from(padding.right)));
1199        let content_height = match style.get_overflow() {
1200            Overflow::Wrap => wrapped_line_count(PrintableLines::new(&self.text), content_width),
1201            Overflow::Clip(_) => PrintableLines::new(&self.text).lines().len(),
1202        };
1203        content_height
1204            .saturating_add(usize::from(padding.top))
1205            .saturating_add(usize::from(padding.bottom))
1206    }
1207
1208    fn rendered_rows(&self, width: usize, height: usize, style: &BlockStyle) -> Vec<String> {
1209        let padding = style.get_padding();
1210        let content_width = width
1211            .saturating_sub(usize::from(padding.left).saturating_add(usize::from(padding.right)));
1212        let content_height = height
1213            .saturating_sub(usize::from(padding.top).saturating_add(usize::from(padding.bottom)));
1214        let mut lines = fit_text_lines(&self.text, Some(content_width), style.get_overflow());
1215        lines.truncate(content_height);
1216        let vertical_gap = content_height.saturating_sub(lines.len());
1217        let above = match style.get_vertical_align() {
1218            VerticalAlign::Top => 0,
1219            VerticalAlign::Center => vertical_gap / 2,
1220            VerticalAlign::Bottom => vertical_gap,
1221        };
1222        let blank = " ".repeat(width);
1223        let mut rows = Vec::with_capacity(height);
1224        rows.resize(
1225            usize::from(padding.top).saturating_add(above),
1226            blank.clone(),
1227        );
1228        for line in lines {
1229            let line_width = PrintableText::new(&line).width();
1230            let gap = content_width.saturating_sub(line_width);
1231            let before = match style.get_align() {
1232                Align::Left => 0,
1233                Align::Center => gap / 2,
1234                Align::Right => gap,
1235            };
1236            let after = gap.saturating_sub(before);
1237            rows.push(format!(
1238                "{}{}{}",
1239                " ".repeat(usize::from(padding.left).saturating_add(before)),
1240                line,
1241                " ".repeat(after.saturating_add(usize::from(padding.right)))
1242            ));
1243        }
1244        rows.resize(height, blank);
1245        rows
1246    }
1247
1248    #[cfg(test)]
1249    fn view(&self, style: &BlockStyle) -> View {
1250        let style = style.clone().width(Length::fill(1)).height(Length::fill(1));
1251        let text_style = style.text_style().clone();
1252        View::block(style, View::text(self.text.clone(), text_style))
1253    }
1254}
1255
1256fn frame_cell_style(mut style: BlockStyle, padding: u16) -> BlockStyle {
1257    style = style
1258        .reset_border()
1259        .margin(Sides::default())
1260        .reset_width()
1261        .reset_height()
1262        .reset_min_width()
1263        .reset_min_height()
1264        .reset_max_width()
1265        .reset_max_height();
1266    if style.get_padding() == Default::default() {
1267        style = style.padding((0, padding));
1268    }
1269    style
1270}
1271
1272struct TextRun {
1273    x: usize,
1274    text: String,
1275    style: TextStyle,
1276}
1277
1278fn push_run(runs: &mut Vec<TextRun>, x: usize, text: &str, style: TextStyle) {
1279    if let Some(run) = runs.last_mut().filter(|run| run.style == style) {
1280        run.text.push_str(text);
1281    } else {
1282        runs.push(TextRun {
1283            x,
1284            text: text.to_owned(),
1285            style,
1286        });
1287    }
1288}
1289
1290#[derive(Debug, Clone, PartialEq)]
1291struct TableItem(Arc<TableFrame>);
1292
1293impl TableItem {
1294    fn sizing(&self) -> CanvasSizing {
1295        CanvasSizing::intrinsic(self.clone())
1296    }
1297}
1298
1299impl CanvasMeasure for TableItem {
1300    fn width_requirements(&self) -> CanvasRequirements {
1301        let requirements = self.0.column_requirements();
1302        CanvasRequirements::new(
1303            requirements
1304                .iter()
1305                .map(|requirement| requirement.0)
1306                .sum::<usize>()
1307                .saturating_add(self.0.line_width()),
1308            requirements
1309                .iter()
1310                .map(|requirement| requirement.1)
1311                .sum::<usize>()
1312                .saturating_add(self.0.line_width()),
1313        )
1314    }
1315
1316    fn height_requirements(&self, width: usize) -> CanvasRequirements {
1317        CanvasRequirements::new(self.0.height_at(width), 0)
1318    }
1319}
1320
1321impl CanvasItem for TableItem {
1322    fn draw(&self, context: &mut CanvasContext) {
1323        self.0.draw(context);
1324    }
1325}
1326
1327fn position(value: usize) -> i64 {
1328    i64::try_from(value).unwrap_or(i64::MAX)
1329}
1330
1331/// Returns the column count spanned by `rows` and a header of `headers` cells.
1332fn column_count(rows: &[Vec<String>], headers: usize) -> usize {
1333    rows.iter()
1334        .map(Vec::len)
1335        .chain(std::iter::once(headers))
1336        .max()
1337        .unwrap_or_default()
1338}
1339
1340#[cfg(test)]
1341mod tests {
1342    use super::*;
1343    use crate::test_support::{plain_exact as plain, plain_rows, style_at};
1344    use crate::{
1345        Align, Available, Border, Color, ComponentTheme, Overflow, PrintableText, SemanticTokens,
1346        StyledGrapheme, VerticalAlign, measure, resolve,
1347    };
1348
1349    fn styles() -> ComponentTheme {
1350        ComponentTheme::from_tokens(&SemanticTokens {
1351            text: Color::WHITE,
1352            text_muted: Color::BRIGHT_BLACK,
1353            background: Color::BLACK,
1354            surface: Color::BLACK,
1355            accent: Color::CYAN,
1356            accent_text: Color::BLACK,
1357            success: Color::GREEN,
1358            warning: Color::YELLOW,
1359            error: Color::RED,
1360            border: Color::BRIGHT_BLACK,
1361        })
1362    }
1363
1364    fn sample() -> TextTable {
1365        Table::text()
1366            .headers(["Name", "Location"])
1367            .row(["Kini", "New York"])
1368            .row(["Iris", "Paris"])
1369    }
1370
1371    #[test]
1372    fn composes_headers_rows_and_borders() {
1373        assert_eq!(
1374            plain(&styles().table().compose(&sample())),
1375            "\
1376┌──────┬──────────┐
1377│ Name │ Location │
1378├──────┼──────────┤
1379│ Kini │ New York │
1380│ Iris │ Paris    │
1381└──────┴──────────┘"
1382        );
1383    }
1384
1385    #[test]
1386    fn empty_tables_compose_nothing() {
1387        let table_style = styles().table().clone();
1388        assert!(measure(&table_style.compose(&Table::text())).is_empty());
1389        assert!(measure(&table_style.compose(&sample().hidden(true))).is_empty());
1390
1391        let headers_only = Table::text().headers(["Name", "Location"]);
1392        assert_eq!(
1393            plain(&table_style.compose(&headers_only)),
1394            "\
1395┌──────┬──────────┐
1396│ Name │ Location │
1397└──────┴──────────┘"
1398        );
1399
1400        let rows_only = Table::text().row(["Kini", "New York"]);
1401        assert_eq!(
1402            plain(&table_style.compose(&rows_only)),
1403            "\
1404┌──────┬──────────┐
1405│ Kini │ New York │
1406└──────┴──────────┘"
1407        );
1408    }
1409
1410    #[test]
1411    fn ragged_rows_pad_missing_cells() {
1412        let table = Table::text()
1413            .headers(["A", "B", "C"])
1414            .row(["1"])
1415            .row(["1", "2", "3"]);
1416
1417        assert_eq!(
1418            plain(&styles().table().compose(&table)),
1419            "\
1420┌───┬───┬───┐
1421│ A │ B │ C │
1422├───┼───┼───┤
1423│ 1 │   │   │
1424│ 1 │ 2 │ 3 │
1425└───┴───┴───┘"
1426        );
1427    }
1428
1429    #[test]
1430    fn offsets_select_body_rows_and_keep_headers() {
1431        let table = sample().row(["Eli", "London"]).offset(1, 1);
1432
1433        assert_eq!(
1434            plain(&styles().table().compose(&table)),
1435            "\
1436┌──────┬──────────┐
1437│ Name │ Location │
1438├──────┼──────────┤
1439│ Iris │ Paris    │
1440└──────┴──────────┘",
1441            "an offset selects body rows; headers still set the column width"
1442        );
1443    }
1444
1445    #[test]
1446    fn aligns_cjk_content_by_terminal_cell_width() {
1447        let table = Table::text()
1448            .headers(["名前", "所在地"])
1449            .row(["日本語", "東京"])
1450            .row(["ab", "Paris"]);
1451        let view = styles().table().compose(&table);
1452
1453        assert_eq!(
1454            plain(&view),
1455            "\
1456┌────────┬────────┐
1457│ 名前   │ 所在地 │
1458├────────┼────────┤
1459│ 日本語 │ 東京   │
1460│ ab     │ Paris  │
1461└────────┴────────┘"
1462        );
1463        let widths: Vec<usize> = plain_rows(&view)
1464            .iter()
1465            .map(|row| PrintableText::new(row).width())
1466            .collect();
1467        assert!(widths.iter().all(|width| *width == widths[0]));
1468    }
1469
1470    #[test]
1471    fn multiline_cells_extend_the_row() {
1472        let table = Table::text()
1473            .headers(["Key", "Value"])
1474            .row(["a", "one\ntwo"]);
1475
1476        assert_eq!(
1477            plain(&styles().table().compose(&table)),
1478            "\
1479┌─────┬───────┐
1480│ Key │ Value │
1481├─────┼───────┤
1482│ a   │ one   │
1483│     │ two   │
1484└─────┴───────┘"
1485        );
1486    }
1487
1488    #[test]
1489    fn width_expands_and_shrinks_columns() {
1490        let table_style = styles().table().clone();
1491        let wide = table_style.clone().width(28);
1492        let view = wide.compose(&sample());
1493        for row in plain_rows(&view) {
1494            assert_eq!(PrintableText::new(&row).width(), 28);
1495        }
1496
1497        let narrow = table_style.width(16);
1498        assert_eq!(
1499            plain(&narrow.compose(&sample())),
1500            "\
1501┌──────┬───────┐
1502│ Name │ Locat │
1503│      │ ion   │
1504├──────┼───────┤
1505│ Kini │ New   │
1506│      │ York  │
1507│ Iris │ Paris │
1508└──────┴───────┘"
1509        );
1510    }
1511
1512    #[test]
1513    fn a_width_constraint_narrower_than_a_grapheme_keeps_rows_rectangular() {
1514        // A column narrower than one wide grapheme is where a constraint runs
1515        // out: the grapheme cannot be split, so the column stops at it. The
1516        // bound frame widens past the constraint rather than dropping the
1517        // content, and every row still matches it because column and line
1518        // geometry are selected together.
1519        let table = Table::text().headers(["A", "B"]).row(["日本語", "x"]);
1520        let view = styles().table().clone().width(9).compose(&table);
1521
1522        assert_eq!(
1523            plain(&view),
1524            "\
1525┌────┬───┐
1526│ A  │ B │
1527├────┼───┤
1528│ 日 │ x │
1529│ 本 │   │
1530│ 語 │   │
1531└────┴───┘",
1532            "the column floors at one wide grapheme and wraps the rest"
1533        );
1534
1535        let padded = styles()
1536            .table()
1537            .clone()
1538            .padding(0)
1539            .width(5)
1540            .compose(&Table::text().row(["日本", "ab"]));
1541        let widths: Vec<usize> = plain_rows(&padded)
1542            .iter()
1543            .map(|row| PrintableText::new(row).width())
1544            .collect();
1545        assert!(
1546            widths.iter().all(|width| *width == widths[0]),
1547            "rectangular at its floor: {widths:?}"
1548        );
1549    }
1550
1551    #[test]
1552    fn width_below_the_column_minimum_overflows() {
1553        let narrow = styles().table().clone().width(4);
1554        let view = narrow.compose(&sample());
1555        let width = PrintableText::new(&plain_rows(&view)[0]).width();
1556
1557        assert!(
1558            width > 4,
1559            "a table cannot shrink below its minimum: {width}"
1560        );
1561        assert_eq!(width, 9);
1562    }
1563
1564    #[test]
1565    fn border_presets_change_every_glyph() {
1566        let table_style = styles().table().clone();
1567        let cases = [
1568            (
1569                TableBorder::MARKDOWN,
1570                [
1571                    "|------|----------|",
1572                    "| Name | Location |",
1573                    "|------|----------|",
1574                    "| Kini | New York |",
1575                    "| Iris | Paris    |",
1576                    "|------|----------|",
1577                ]
1578                .join("\n"),
1579            ),
1580            (
1581                // The heavy outer rules and the light header rule come from
1582                // separate glyphs; the blank left and right edges still occupy
1583                // one cell each.
1584                TableBorder::BOOKTABS,
1585                [
1586                    "━━━━━━━━━━━━━━━━━━━",
1587                    "  Name   Location  ",
1588                    "───────────────────",
1589                    "  Kini   New York  ",
1590                    "  Iris   Paris     ",
1591                    "━━━━━━━━━━━━━━━━━━━",
1592                ]
1593                .join("\n"),
1594            ),
1595            (
1596                TableBorder::HIDDEN,
1597                [
1598                    "                   ",
1599                    "  Name   Location  ",
1600                    "                   ",
1601                    "  Kini   New York  ",
1602                    "  Iris   Paris     ",
1603                    "                   ",
1604                ]
1605                .join("\n"),
1606            ),
1607        ];
1608
1609        for (border, expected) in cases {
1610            assert_eq!(
1611                plain(&table_style.clone().border(border).compose(&sample())),
1612                expected
1613            );
1614        }
1615    }
1616
1617    #[test]
1618    fn edge_toggles_omit_their_glyphs() {
1619        let table_style = styles()
1620            .table()
1621            .clone()
1622            .border_top(false)
1623            .border_bottom(false)
1624            .border_left(false)
1625            .border_right(false)
1626            .border_column(false);
1627
1628        assert_eq!(
1629            plain(&table_style.compose(&sample())),
1630            [
1631                " Name  Location ",
1632                "────────────────",
1633                " Kini  New York ",
1634                " Iris  Paris    ",
1635            ]
1636            .join("\n")
1637        );
1638    }
1639
1640    #[test]
1641    fn row_rules_separate_every_body_row() {
1642        let table_style = styles().table().clone().border_row(true);
1643
1644        assert_eq!(
1645            plain(&table_style.compose(&sample())),
1646            "\
1647┌──────┬──────────┐
1648│ Name │ Location │
1649├──────┼──────────┤
1650│ Kini │ New York │
1651├──────┼──────────┤
1652│ Iris │ Paris    │
1653└──────┴──────────┘"
1654        );
1655    }
1656
1657    #[test]
1658    fn a_disabled_header_rule_leaves_the_first_gap_to_the_header_policy() {
1659        let table_style = styles()
1660            .table()
1661            .clone()
1662            .border_header(false)
1663            .border_row(true);
1664
1665        assert_eq!(
1666            plain(&table_style.compose(&sample())),
1667            "\
1668┌──────┬──────────┐
1669│ Name │ Location │
1670│ Kini │ New York │
1671├──────┼──────────┤
1672│ Iris │ Paris    │
1673└──────┴──────────┘",
1674            "the header owns its gap even when body rows carry rules"
1675        );
1676    }
1677
1678    #[test]
1679    fn a_disabled_column_rule_occupies_no_cell() {
1680        let table_style = styles().table().clone().border_column(false);
1681
1682        assert_eq!(
1683            plain(&table_style.compose(&sample())),
1684            "\
1685┌────────────────┐
1686│ Name  Location │
1687├────────────────┤
1688│ Kini  New York │
1689│ Iris  Paris    │
1690└────────────────┘",
1691            "the columns touch while the outer edges and row rules remain"
1692        );
1693    }
1694
1695    #[test]
1696    fn each_disabled_outer_edge_removes_its_own_cells() {
1697        let table = Table::text().headers(["A", "B"]).row(["1", "2"]);
1698        let table_style = styles().table().clone();
1699        let cases = [
1700            (
1701                table_style.clone().border_top(false),
1702                ["│ A │ B │", "├───┼───┤", "│ 1 │ 2 │", "└───┴───┘"].join("\n"),
1703            ),
1704            (
1705                table_style.clone().border_bottom(false),
1706                ["┌───┬───┐", "│ A │ B │", "├───┼───┤", "│ 1 │ 2 │"].join("\n"),
1707            ),
1708            (
1709                table_style.clone().border_left(false),
1710                ["───┬───┐", " A │ B │", "───┼───┤", " 1 │ 2 │", "───┴───┘"].join("\n"),
1711            ),
1712            (
1713                table_style.border_right(false),
1714                ["┌───┬───", "│ A │ B ", "├───┼───", "│ 1 │ 2 ", "└───┴───"].join("\n"),
1715            ),
1716        ];
1717
1718        for (presentation, expected) in cases {
1719            assert_eq!(plain(&presentation.compose(&table)), expected);
1720        }
1721    }
1722
1723    #[test]
1724    fn a_single_cell_has_no_internal_separator() {
1725        let table = Table::text().row(["only"]);
1726
1727        assert_eq!(
1728            plain(&styles().table().compose(&table)),
1729            "\
1730┌──────┐
1731│ only │
1732└──────┘"
1733        );
1734    }
1735
1736    #[test]
1737    fn a_standalone_network_rule_uses_its_straight_glyph() {
1738        let glyphs = LineGlyphs {
1739            isolated: 'o',
1740            end_up: 'u',
1741            end_right: 'r',
1742            end_down: 'd',
1743            end_left: 'l',
1744            vertical: 'v',
1745            horizontal: 'h',
1746            ..LineGlyphs::NORMAL
1747        };
1748        let one_column = Table::text().row([""]);
1749        let vertical = styles()
1750            .table()
1751            .clone()
1752            .border(TableBorder::network(glyphs))
1753            .padding(0)
1754            .border_top(false)
1755            .border_bottom(false)
1756            .border_right(false)
1757            .border_column(false)
1758            .compose(&one_column);
1759        assert_eq!(plain(&vertical), "v");
1760
1761        let one_cell = Table::text().row(["x"]);
1762        let horizontal = styles()
1763            .table()
1764            .clone()
1765            .border(TableBorder::network(glyphs))
1766            .padding(0)
1767            .border_bottom(false)
1768            .border_left(false)
1769            .border_right(false)
1770            .border_column(false)
1771            .compose(&one_cell);
1772        assert_eq!(plain(&horizontal), "h\nx");
1773    }
1774
1775    #[test]
1776    fn zero_width_cells_preserve_corners_next_to_disabled_edges() {
1777        let table = Table::text().row([""]);
1778        let left = styles()
1779            .table()
1780            .clone()
1781            .padding(0)
1782            .border_bottom(false)
1783            .border_right(false)
1784            .border_column(false)
1785            .compose(&table);
1786        assert_eq!(plain(&left), "┌\n│");
1787
1788        let right = styles()
1789            .table()
1790            .clone()
1791            .padding(0)
1792            .border_bottom(false)
1793            .border_left(false)
1794            .border_column(false)
1795            .compose(&table);
1796        assert_eq!(plain(&right), "┐\n│");
1797    }
1798
1799    fn accent_rows() -> TableRowPresentation<'static, TextTableRow> {
1800        TableRowPresentation::display().cell_style(|_, cell, _| match cell.row() {
1801            1 => Some(BlockStyle::new().foreground(Color::GREEN)),
1802            _ => None,
1803        })
1804    }
1805
1806    #[test]
1807    fn cell_replacements_override_column_and_table_styles() {
1808        let component_styles = styles();
1809        let table_style = component_styles
1810            .table()
1811            .clone()
1812            .header_styles([
1813                Some(BlockStyle::new().foreground(Color::CYAN).bold()),
1814                Some(BlockStyle::new().foreground(Color::CYAN).bold()),
1815            ])
1816            .column_styles([None, Some(BlockStyle::new().align(Align::Right))]);
1817        let view = table_style.compose_with(&sample(), &accent_rows());
1818
1819        // Header row: matched by the (Header, _) arm.
1820        assert_eq!(
1821            style_at(&view, 1, 2),
1822            TextStyle::new().foreground(Color::CYAN).bold()
1823        );
1824        // Whole body row 1: matched by the (Body(1), _) arm, both columns.
1825        for column in [2, 9] {
1826            assert_eq!(
1827                style_at(&view, 4, column),
1828                TextStyle::new().foreground(Color::GREEN)
1829            );
1830        }
1831        // Column 0 of body row 0 falls through to the role default.
1832        assert_eq!(
1833            style_at(&view, 3, 2),
1834            component_styles
1835                .table()
1836                .get_style(TableRole::Cell)
1837                .text_style()
1838                .clone()
1839        );
1840        // Column 1 of body row 0: matched by the (_, 1) arm, which right-aligns
1841        // and — because Some replaces rather than layers — drops the role
1842        // default's foreground.
1843        assert_eq!(style_at(&view, 3, 9), TextStyle::new());
1844        assert_eq!(
1845            plain_rows(&view)[3],
1846            "│ Kini │ New York │",
1847            "the widest cell of a column has no slack to realign"
1848        );
1849    }
1850
1851    #[test]
1852    fn a_styler_realigns_a_column_that_has_slack() {
1853        // Body row 2 escapes the (Body(1), _) arm, so the (_, 1) column arm
1854        // applies where the cell is narrower than its column.
1855        let table = sample().row(["Eli", "London"]);
1856        let table_style = styles()
1857            .table()
1858            .clone()
1859            .column_styles([None, Some(BlockStyle::new().align(Align::Right))]);
1860        let view = table_style.compose_with(&table, &accent_rows());
1861
1862        assert_eq!(
1863            plain_rows(&view)[5],
1864            "│ Eli  │   London │",
1865            "the (_, 1) arm right-aligns column 1 where the cell has slack"
1866        );
1867    }
1868
1869    #[test]
1870    fn a_typed_cell_override_keeps_padding_and_loses_the_rest_of_the_box() {
1871        let rows = TableRowPresentation::<TextTableRow>::display().cell_style(|_, _, _| {
1872            Some(
1873                BlockStyle::new()
1874                    .padding((0, 3))
1875                    .margin(2)
1876                    .width(30)
1877                    .height(4)
1878                    .min_width(24)
1879                    .min_height(3)
1880                    .max_width(28)
1881                    .max_height(3)
1882                    .border(Border::DOUBLE),
1883            )
1884        });
1885
1886        let table_style = styles().table().clone();
1887
1888        // A cell that states its own padding replaces the presentation's
1889        // rather than adding to it, and its column is as wide as it needs.
1890        // The bound frame owns column geometry, so the cell's stated sizes,
1891        // border, and margin do not survive.
1892        assert_eq!(
1893            plain(&table_style.compose_with(&sample(), &rows)),
1894            "\
1895┌──────────┬──────────────┐
1896│ Name     │ Location     │
1897├──────────┼──────────────┤
1898│   Kini   │   New York   │
1899│   Iris   │   Paris      │
1900└──────────┴──────────────┘"
1901        );
1902    }
1903
1904    #[test]
1905    fn an_offset_excluded_row_does_not_widen_the_table() {
1906        let table = Table::text()
1907            .headers(["A", "B"])
1908            .row(["1", "2"])
1909            .row(["w", "x", "y", "z"])
1910            .offset(0, 1);
1911
1912        assert_eq!(
1913            table.column_count(),
1914            4,
1915            "the data-level count still spans every owned row"
1916        );
1917        assert_eq!(
1918            plain(&styles().table().compose(&table)),
1919            "\
1920┌───┬───┐
1921│ A │ B │
1922├───┼───┤
1923│ 1 │ 2 │
1924└───┴───┘",
1925            "composition spans only the visible rows"
1926        );
1927    }
1928
1929    #[test]
1930    fn cell_alignment_comes_from_the_cell_style() {
1931        let table_style = styles().table().clone().column_styles([
1932            Some(BlockStyle::new().align(Align::Right)),
1933            Some(BlockStyle::new().align(Align::Right)),
1934        ]);
1935
1936        assert_eq!(
1937            plain(&table_style.compose(&sample())),
1938            "\
1939┌──────┬──────────┐
1940│ Name │ Location │
1941├──────┼──────────┤
1942│ Kini │ New York │
1943│ Iris │    Paris │
1944└──────┴──────────┘"
1945        );
1946    }
1947
1948    #[test]
1949    fn vertical_alignment_places_short_cells_within_a_tall_row() {
1950        let table = Table::text().row(["a", "one\ntwo"]);
1951        let table_style = styles().table().clone().column_styles([
1952            Some(BlockStyle::new().vertical_align(VerticalAlign::Bottom)),
1953            Some(BlockStyle::new().vertical_align(VerticalAlign::Bottom)),
1954        ]);
1955
1956        assert_eq!(
1957            plain(&table_style.compose(&table)),
1958            "\
1959┌───┬─────┐
1960│   │ one │
1961│ a │ two │
1962└───┴─────┘"
1963        );
1964    }
1965
1966    #[test]
1967    fn padding_widens_every_cell() {
1968        let table_style = styles().table().clone().padding(0);
1969
1970        assert_eq!(
1971            plain(&table_style.compose(&sample())),
1972            "\
1973┌────┬────────┐
1974│Name│Location│
1975├────┼────────┤
1976│Kini│New York│
1977│Iris│Paris   │
1978└────┴────────┘"
1979        );
1980    }
1981
1982    #[test]
1983    fn table_data_is_inspectable_without_a_style() {
1984        let table = sample().hidden(true).offset(1, 0);
1985
1986        assert_eq!(table.column_count(), 2);
1987        assert!(table.is_hidden());
1988
1989        let ragged = Table::text().headers(["A"]).row(["1", "2", "3"]);
1990        assert_eq!(ragged.column_count(), 3);
1991    }
1992
1993    #[test]
1994    fn the_typed_styler_sees_each_body_cell_its_row_and_its_text() {
1995        let rows = TableRowPresentation::<TextTableRow>::display().cell_style(|_, cell, _| {
1996            let expected = match cell.column() {
1997                0 => "Kini",
1998                _ => "New York",
1999            };
2000            assert_eq!(cell.text(), expected, "column {}", cell.column());
2001            assert_eq!(cell.row(), 0);
2002            None
2003        });
2004
2005        let table = Table::text()
2006            .headers(["Name", "Location"])
2007            .row(["Kini", "New York"]);
2008        let _ = styles().table().compose_with(&table, &rows);
2009    }
2010
2011    #[test]
2012    fn role_setters_replace_the_styles_they_name() {
2013        let underline = BlockStyle::new().underlined();
2014        let table_style = styles()
2015            .table()
2016            .clone()
2017            .header_style(underline.clone())
2018            .cell_style(underline.clone())
2019            .border_style(TextStyle::new().underlined());
2020
2021        for role in [TableRole::Header, TableRole::Cell] {
2022            assert_eq!(table_style.get_style(role), &underline);
2023        }
2024        assert_eq!(
2025            table_style.get_border_style(),
2026            &TextStyle::new().underlined()
2027        );
2028    }
2029
2030    #[test]
2031    fn border_drawing_preserves_only_the_legacy_color_properties() {
2032        let presentation = styles().table().clone().border_style(
2033            TextStyle::new()
2034                .foreground(Color::RED)
2035                .background(Color::BLUE)
2036                .bold()
2037                .underlined(),
2038        );
2039        let view = presentation.compose(&Table::text().row(["cell"]));
2040
2041        assert_eq!(
2042            style_at(&view, 0, 0),
2043            TextStyle::new()
2044                .foreground(Color::RED)
2045                .background(Color::BLUE)
2046        );
2047    }
2048
2049    /// A table whose natural width is 41 cells: the case #22 reproduced.
2050    fn wide() -> TextTable {
2051        Table::text()
2052            .headers(["N", "Description"])
2053            .row(["a", "the quick brown fox jumps again!!"])
2054            .row(["b", "over the lazy dog"])
2055    }
2056
2057    #[test]
2058    fn an_area_narrower_than_the_table_shrinks_its_columns_and_its_lines() {
2059        let view = styles().table().compose(&wide());
2060        assert_eq!(measure(&view).width(), 41, "the natural width");
2061
2062        let rows: Vec<String> = resolve(&view, Available::columns(18))
2063            .unwrap()
2064            .rows()
2065            .iter()
2066            .map(|row| row.iter().map(StyledGrapheme::symbol).collect())
2067            .collect();
2068
2069        for row in &rows {
2070            assert_eq!(
2071                PrintableText::new(row).width(),
2072                18,
2073                "every row fits the area: {row}"
2074            );
2075        }
2076        // The lines and the cells were decided together, so the verticals of
2077        // a rule land on the column boundaries of the rows around it.
2078        let boundaries = |row: &str| -> Vec<usize> {
2079            PrintableText::new(row)
2080                .as_str()
2081                .chars()
2082                .enumerate()
2083                .filter(|(_, glyph)| "│┼┬┴├┤┌┐└┘".contains(*glyph))
2084                .map(|(index, _)| index)
2085                .collect()
2086        };
2087        let expected = boundaries(&rows[0]);
2088        for row in &rows {
2089            assert_eq!(boundaries(row), expected, "columns line up: {row}");
2090        }
2091    }
2092
2093    #[test]
2094    fn an_area_wider_than_the_table_does_not_widen_it() {
2095        let view = styles().table().compose(&wide());
2096
2097        let rows: Vec<String> = resolve(&view, Available::columns(200))
2098            .unwrap()
2099            .rows()
2100            .iter()
2101            .map(|row| row.iter().map(StyledGrapheme::symbol).collect())
2102            .collect();
2103
2104        for row in &rows {
2105            assert_eq!(
2106                PrintableText::new(row).width(),
2107                41,
2108                "an area only caps a table; it never pads one out: {row}"
2109            );
2110        }
2111    }
2112
2113    #[test]
2114    fn table_presentations_compare_by_complete_style_lists() {
2115        let table_style = styles().table().clone();
2116        assert_eq!(table_style, styles().table().clone());
2117        assert_ne!(
2118            table_style.clone(),
2119            table_style
2120                .clone()
2121                .column_styles([None, Some(BlockStyle::new().bold())])
2122        );
2123        assert_ne!(table_style.clone(), table_style.border(TableBorder::ASCII));
2124    }
2125
2126    #[test]
2127    fn canonical_composition_binds_an_intrinsic_canvas_item() {
2128        let view = styles().table().compose(&sample());
2129        let View::Block(style, _, child) = view else {
2130            panic!("a Table is boxed only to state its outer sizing")
2131        };
2132        assert_eq!(style.get_border(), None);
2133        assert!(matches!(*child, View::Canvas(_)));
2134    }
2135
2136    #[test]
2137    fn narrow_height_measurement_matches_drawing_and_shorter_height_crops() {
2138        let view = styles().table().compose(&wide());
2139        let complete = resolve(&view, Available::columns(18)).unwrap();
2140        let exact = resolve(&view, Available::size(18, complete.size().height()));
2141        assert_eq!(
2142            exact.unwrap(),
2143            complete,
2144            "measurement and drawing use one bound frame"
2145        );
2146
2147        let cropped = resolve(&view, Available::size(18, 3)).unwrap();
2148        assert_eq!(cropped.size().height(), 3);
2149        assert_eq!(cropped.rows(), &complete.rows()[..3]);
2150    }
2151
2152    #[test]
2153    fn table_cell_height_matches_the_shared_block_text_model() {
2154        let cases = [
2155            (
2156                TableFrameCell {
2157                    text: "words that wrap\n日本語".to_owned(),
2158                    style_override: None,
2159                },
2160                BlockStyle::new().padding((2, 1)),
2161            ),
2162            (
2163                TableFrameCell {
2164                    text: "words that clip\n日本語".to_owned(),
2165                    style_override: None,
2166                },
2167                BlockStyle::new()
2168                    .padding((1, 2))
2169                    .overflow(Overflow::ellipsis()),
2170            ),
2171        ];
2172
2173        for (cell, style) in cases {
2174            let floor = cell.width_requirements(&style).1;
2175            for width in [floor, floor.saturating_add(3), floor.saturating_add(12)] {
2176                assert_eq!(
2177                    cell.height_at(width, &style),
2178                    resolve(&cell.view(&style), Available::columns(width))
2179                        .unwrap()
2180                        .size()
2181                        .height(),
2182                    "{cell:?} at width {width}"
2183                );
2184            }
2185        }
2186    }
2187
2188    #[test]
2189    fn an_outer_borderless_block_can_state_fixed_or_fill_table_size() {
2190        let table = styles().table().compose(&sample());
2191        let fixed = View::block(BlockStyle::new().width(30), table.clone());
2192        assert_eq!(resolve(&fixed, Available::NONE).unwrap().size().width(), 30);
2193
2194        let fill = View::block(BlockStyle::new().width(Length::fill(1)), table);
2195        assert_eq!(
2196            resolve(&fill, Available::columns(32))
2197                .unwrap()
2198                .size()
2199                .width(),
2200            32
2201        );
2202    }
2203
2204    #[test]
2205    fn every_border_preset_keeps_its_observable_glyphs() {
2206        let table = Table::text().headers(["A", "B"]).row(["1", "2"]);
2207        let cases = [
2208            (TableBorder::NORMAL, '┌', '┬', '┼', '└'),
2209            (TableBorder::ROUNDED, '╭', '┬', '┼', '╰'),
2210            (TableBorder::THICK, '┏', '┳', '╋', '┗'),
2211            (TableBorder::DOUBLE, '╔', '╦', '╬', '╚'),
2212            (TableBorder::ASCII, '+', '+', '+', '+'),
2213            (TableBorder::MARKDOWN, '|', '|', '|', '|'),
2214            (TableBorder::BOOKTABS, '━', '━', '─', '━'),
2215            (TableBorder::HIDDEN, ' ', ' ', ' ', ' '),
2216        ];
2217
2218        for (border, top_left, top_middle, middle, bottom_left) in cases {
2219            let rows = plain_rows(&styles().table().clone().border(border).compose(&table));
2220            assert_eq!(rows[0].chars().next(), Some(top_left));
2221            assert!(rows[0].contains(top_middle));
2222            assert!(rows[2].contains(middle));
2223            assert_eq!(rows[4].chars().next(), Some(bottom_left));
2224        }
2225    }
2226
2227    #[test]
2228    fn caller_owned_line_glyphs_derive_every_table_junction() {
2229        let glyphs = LineGlyphs {
2230            isolated: 'o',
2231            end_up: 'u',
2232            end_right: 'r',
2233            end_down: 'd',
2234            end_left: 'l',
2235            vertical: 'v',
2236            horizontal: 'h',
2237            corner_down_right: 'A',
2238            corner_down_left: 'B',
2239            corner_up_right: 'C',
2240            corner_up_left: 'D',
2241            tee_right: 'E',
2242            tee_down: 'H',
2243            tee_left: 'F',
2244            tee_up: 'I',
2245            cross: 'G',
2246        };
2247        let table = Table::text()
2248            .headers(["a", "b"])
2249            .row(["1", "2"])
2250            .row(["3", "4"]);
2251        let view = styles()
2252            .table()
2253            .clone()
2254            .border(TableBorder::network(glyphs))
2255            .border_row(true)
2256            .compose(&table);
2257
2258        assert_eq!(
2259            plain(&view),
2260            [
2261                "AhhhHhhhB",
2262                "v a v b v",
2263                "EhhhGhhhF",
2264                "v 1 v 2 v",
2265                "EhhhGhhhF",
2266                "v 3 v 4 v",
2267                "ChhhIhhhD",
2268            ]
2269            .join("\n")
2270        );
2271    }
2272
2273    #[test]
2274    fn theme_table_is_the_canonical_presentation_shortcut() {
2275        let tokens = SemanticTokens {
2276            text: Color::WHITE,
2277            text_muted: Color::BRIGHT_BLACK,
2278            background: Color::BLACK,
2279            surface: Color::BLACK,
2280            accent: Color::CYAN,
2281            accent_text: Color::BLACK,
2282            success: Color::GREEN,
2283            warning: Color::YELLOW,
2284            error: Color::RED,
2285            border: Color::BRIGHT_BLACK,
2286        };
2287        let theme = crate::Theme::from_tokens(tokens);
2288        assert_eq!(
2289            theme.table(&sample()),
2290            theme.components().table().compose(&sample())
2291        );
2292    }
2293}