ratatui_widgets/
table.rs

1//! The [`Table`] widget is used to display multiple rows and columns in a grid and allows selecting
2//! one or multiple cells.
3
4use alloc::vec;
5use alloc::vec::Vec;
6
7use itertools::Itertools;
8use ratatui_core::buffer::Buffer;
9use ratatui_core::layout::{Constraint, Flex, Layout, Rect};
10use ratatui_core::style::{Style, Styled};
11use ratatui_core::text::Text;
12use ratatui_core::widgets::{StatefulWidget, Widget};
13
14pub use self::cell::Cell;
15pub use self::highlight_spacing::HighlightSpacing;
16pub use self::row::Row;
17pub use self::state::TableState;
18use crate::block::{Block, BlockExt};
19
20mod cell;
21mod highlight_spacing;
22mod row;
23mod state;
24
25/// A widget to display data in formatted columns.
26///
27/// A `Table` is a collection of [`Row`]s, each composed of [`Cell`]s:
28///
29/// You can construct a [`Table`] using either [`Table::new`] or [`Table::default`] and then chain
30/// builder style methods to set the desired properties.
31///
32/// Table cells can be aligned, for more details see [`Cell`].
33///
34/// Make sure to call the [`Table::widths`] method, otherwise the columns will all have a width of 0
35/// and thus not be visible.
36///
37/// [`Table`] implements [`Widget`] and so it can be drawn using `Frame::render_widget`.
38///
39/// [`Table`] is also a [`StatefulWidget`], which means you can use it with [`TableState`] to allow
40/// the user to scroll through the rows and select one of them. When rendering a [`Table`] with a
41/// [`TableState`], the selected row, column and cell will be highlighted. If the selected row is
42/// not visible (based on the offset), the table will be scrolled to make the selected row visible.
43///
44/// Note: if the `widths` field is empty, the table will be rendered with equal widths.
45/// Note: Highlight styles are applied in the following order: Row, Column, Cell.
46///
47/// See the table example and the recipe and traceroute tabs in the demo2 example in the [Examples]
48/// directory for a more in depth example of the various configuration options and for how to handle
49/// state.
50///
51/// [Examples]: https://github.com/ratatui/ratatui/blob/master/examples/README.md
52///
53/// # Constructor methods
54///
55/// - [`Table::new`] creates a new [`Table`] with the given rows.
56/// - [`Table::default`] creates an empty [`Table`]. You can then add rows using [`Table::rows`].
57///
58/// # Setter methods
59///
60/// These methods are fluent setters. They return a new `Table` with the specified property set.
61///
62/// - [`Table::rows`] sets the rows of the [`Table`].
63/// - [`Table::header`] sets the header row of the [`Table`].
64/// - [`Table::footer`] sets the footer row of the [`Table`].
65/// - [`Table::widths`] sets the width constraints of each column.
66/// - [`Table::column_spacing`] sets the spacing between each column.
67/// - [`Table::block`] wraps the table in a [`Block`] widget.
68/// - [`Table::style`] sets the base style of the widget.
69/// - [`Table::row_highlight_style`] sets the style of the selected row.
70/// - [`Table::column_highlight_style`] sets the style of the selected column.
71/// - [`Table::cell_highlight_style`] sets the style of the selected cell.
72/// - [`Table::highlight_symbol`] sets the symbol to be displayed in front of the selected row.
73/// - [`Table::highlight_spacing`] sets when to show the highlight spacing.
74///
75/// # Example
76///
77/// ```rust
78/// use ratatui::layout::Constraint;
79/// use ratatui::style::{Style, Stylize};
80/// use ratatui::widgets::{Block, Row, Table};
81///
82/// let rows = [Row::new(vec!["Cell1", "Cell2", "Cell3"])];
83/// // Columns widths are constrained in the same way as Layout...
84/// let widths = [
85///     Constraint::Length(5),
86///     Constraint::Length(5),
87///     Constraint::Length(10),
88/// ];
89/// let table = Table::new(rows, widths)
90///     // ...and they can be separated by a fixed spacing.
91///     .column_spacing(1)
92///     // You can set the style of the entire Table.
93///     .style(Style::new().blue())
94///     // It has an optional header, which is simply a Row always visible at the top.
95///     .header(
96///         Row::new(vec!["Col1", "Col2", "Col3"])
97///             .style(Style::new().bold())
98///             // To add space between the header and the rest of the rows, specify the margin
99///             .bottom_margin(1),
100///     )
101///     // It has an optional footer, which is simply a Row always visible at the bottom.
102///     .footer(Row::new(vec!["Updated on Dec 28"]))
103///     // As any other widget, a Table can be wrapped in a Block.
104///     .block(Block::new().title("Table"))
105///     // The selected row, column, cell and its content can also be styled.
106///     .row_highlight_style(Style::new().reversed())
107///     .column_highlight_style(Style::new().red())
108///     .cell_highlight_style(Style::new().blue())
109///     // ...and potentially show a symbol in front of the selection.
110///     .highlight_symbol(">>");
111/// ```
112///
113/// Rows can be created from an iterator of [`Cell`]s. Each row can have an associated height,
114/// bottom margin, and style. See [`Row`] for more details.
115///
116/// ```rust
117/// use ratatui::style::{Style, Stylize};
118/// use ratatui::text::{Line, Span};
119/// use ratatui::widgets::{Cell, Row, Table};
120///
121/// // a Row can be created from simple strings.
122/// let row = Row::new(vec!["Row11", "Row12", "Row13"]);
123///
124/// // You can style the entire row.
125/// let row = Row::new(vec!["Row21", "Row22", "Row23"]).style(Style::new().red());
126///
127/// // If you need more control over the styling, create Cells directly
128/// let row = Row::new(vec![
129///     Cell::from("Row31"),
130///     Cell::from("Row32").style(Style::new().yellow()),
131///     Cell::from(Line::from(vec![Span::raw("Row"), Span::from("33").green()])),
132/// ]);
133///
134/// // If a Row need to display some content over multiple lines, specify the height.
135/// let row = Row::new(vec![
136///     Cell::from("Row\n41"),
137///     Cell::from("Row\n42"),
138///     Cell::from("Row\n43"),
139/// ])
140/// .height(2);
141/// ```
142///
143/// Cells can be created from anything that can be converted to [`Text`]. See [`Cell`] for more
144/// details.
145///
146/// ```rust
147/// use ratatui::style::{Style, Stylize};
148/// use ratatui::text::{Line, Span, Text};
149/// use ratatui::widgets::Cell;
150///
151/// Cell::from("simple string");
152/// Cell::from("simple styled span".red());
153/// Cell::from(Span::raw("raw span"));
154/// Cell::from(Span::styled("styled span", Style::new().red()));
155/// Cell::from(Line::from(vec![
156///     Span::raw("a vec of "),
157///     Span::from("spans").bold(),
158/// ]));
159/// Cell::from(Text::from("text"));
160/// ```
161///
162/// Just as rows can be collected from iterators of `Cell`s, tables can be collected from iterators
163/// of `Row`s.  This will create a table with column widths evenly dividing the space available.
164/// These default columns widths can be overridden using the `Table::widths` method.
165///
166/// ```rust
167/// use ratatui::layout::Constraint;
168/// use ratatui::widgets::{Row, Table};
169///
170/// let text = "Mary had a\nlittle lamb.";
171///
172/// let table = text
173///     .split("\n")
174///     .map(|line: &str| -> Row { line.split_ascii_whitespace().collect() })
175///     .collect::<Table>()
176///     .widths([Constraint::Length(10); 3]);
177/// ```
178///
179/// `Table` also implements the [`Styled`] trait, which means you can use style shorthands from
180/// the [`Stylize`] trait to set the style of the widget more concisely.
181///
182/// ```rust
183/// use ratatui::layout::Constraint;
184/// use ratatui::style::Stylize;
185/// use ratatui::widgets::{Row, Table};
186///
187/// let rows = [Row::new(vec!["Cell1", "Cell2", "Cell3"])];
188/// let widths = [
189///     Constraint::Length(5),
190///     Constraint::Length(5),
191///     Constraint::Length(10),
192/// ];
193/// let table = Table::new(rows, widths).red().italic();
194/// ```
195///
196/// # Stateful example
197///
198/// `Table` is a [`StatefulWidget`], which means you can use it with [`TableState`] to allow the
199/// user to scroll through the rows and select one of them.
200///
201/// ```rust
202/// use ratatui::Frame;
203/// use ratatui::layout::{Constraint, Rect};
204/// use ratatui::style::{Style, Stylize};
205/// use ratatui::widgets::{Block, Row, Table, TableState};
206///
207/// # fn ui(frame: &mut Frame) {
208/// # let area = Rect::default();
209/// // Note: TableState should be stored in your application state (not constructed in your render
210/// // method) so that the selected row is preserved across renders
211/// let mut table_state = TableState::default();
212/// let rows = [
213///     Row::new(vec!["Row11", "Row12", "Row13"]),
214///     Row::new(vec!["Row21", "Row22", "Row23"]),
215///     Row::new(vec!["Row31", "Row32", "Row33"]),
216/// ];
217/// let widths = [
218///     Constraint::Length(5),
219///     Constraint::Length(5),
220///     Constraint::Length(10),
221/// ];
222/// let table = Table::new(rows, widths)
223///     .block(Block::new().title("Table"))
224///     .row_highlight_style(Style::new().reversed())
225///     .highlight_symbol(">>");
226///
227/// frame.render_stateful_widget(table, area, &mut table_state);
228/// # }
229/// ```
230///
231/// [`Stylize`]: ratatui_core::style::Stylize
232#[derive(Debug, Clone, Eq, PartialEq, Hash)]
233pub struct Table<'a> {
234    /// Data to display in each row
235    rows: Vec<Row<'a>>,
236
237    /// Optional header
238    header: Option<Row<'a>>,
239
240    /// Optional footer
241    footer: Option<Row<'a>>,
242
243    /// Width constraints for each column
244    widths: Vec<Constraint>,
245
246    /// Space between each column
247    column_spacing: u16,
248
249    /// A block to wrap the widget in
250    block: Option<Block<'a>>,
251
252    /// Base style for the widget
253    style: Style,
254
255    /// Style used to render the selected row
256    row_highlight_style: Style,
257
258    /// Style used to render the selected column
259    column_highlight_style: Style,
260
261    /// Style used to render the selected cell
262    cell_highlight_style: Style,
263
264    /// Symbol in front of the selected row
265    highlight_symbol: Text<'a>,
266
267    /// Decides when to allocate spacing for the row selection
268    highlight_spacing: HighlightSpacing,
269
270    /// Controls how to distribute extra space among the columns
271    flex: Flex,
272}
273
274impl Default for Table<'_> {
275    fn default() -> Self {
276        Self {
277            rows: Vec::new(),
278            header: None,
279            footer: None,
280            widths: Vec::new(),
281            column_spacing: 1,
282            block: None,
283            style: Style::new(),
284            row_highlight_style: Style::new(),
285            column_highlight_style: Style::new(),
286            cell_highlight_style: Style::new(),
287            highlight_symbol: Text::default(),
288            highlight_spacing: HighlightSpacing::default(),
289            flex: Flex::Start,
290        }
291    }
292}
293
294impl<'a> Table<'a> {
295    /// Creates a new [`Table`] widget with the given rows.
296    ///
297    /// The `rows` parameter accepts any value that can be converted into an iterator of [`Row`]s.
298    /// This includes arrays, slices, and [`Vec`]s.
299    ///
300    /// The `widths` parameter accepts any type that implements `IntoIterator<Item =
301    /// Into<Constraint>>`. This includes arrays, slices, vectors, iterators. `Into<Constraint>` is
302    /// implemented on u16, so you can pass an array, vec, etc. of u16 to this function to create a
303    /// table with fixed width columns.
304    ///
305    /// # Examples
306    ///
307    /// ```rust
308    /// use ratatui::layout::Constraint;
309    /// use ratatui::widgets::{Row, Table};
310    ///
311    /// let rows = [
312    ///     Row::new(vec!["Cell1", "Cell2"]),
313    ///     Row::new(vec!["Cell3", "Cell4"]),
314    /// ];
315    /// let widths = [Constraint::Length(5), Constraint::Length(5)];
316    /// let table = Table::new(rows, widths);
317    /// ```
318    pub fn new<R, C>(rows: R, widths: C) -> Self
319    where
320        R: IntoIterator,
321        R::Item: Into<Row<'a>>,
322        C: IntoIterator,
323        C::Item: Into<Constraint>,
324    {
325        let widths = widths.into_iter().map(Into::into).collect_vec();
326        ensure_percentages_less_than_100(&widths);
327
328        let rows = rows.into_iter().map(Into::into).collect();
329        Self {
330            rows,
331            widths,
332            ..Default::default()
333        }
334    }
335
336    /// Set the rows
337    ///
338    /// The `rows` parameter accepts any value that can be converted into an iterator of [`Row`]s.
339    /// This includes arrays, slices, and [`Vec`]s.
340    ///
341    /// # Warning
342    ///
343    /// This method does not currently set the column widths. You will need to set them manually by
344    /// calling [`Table::widths`].
345    ///
346    /// This is a fluent setter method which must be chained or used as it consumes self
347    ///
348    /// # Examples
349    ///
350    /// ```rust
351    /// use ratatui::widgets::{Row, Table};
352    ///
353    /// let rows = [
354    ///     Row::new(vec!["Cell1", "Cell2"]),
355    ///     Row::new(vec!["Cell3", "Cell4"]),
356    /// ];
357    /// let table = Table::default().rows(rows);
358    /// ```
359    #[must_use = "method moves the value of self and returns the modified value"]
360    pub fn rows<T>(mut self, rows: T) -> Self
361    where
362        T: IntoIterator<Item = Row<'a>>,
363    {
364        self.rows = rows.into_iter().collect();
365        self
366    }
367
368    /// Sets the header row
369    ///
370    /// The `header` parameter is a [`Row`] which will be displayed at the top of the [`Table`]
371    ///
372    /// This is a fluent setter method which must be chained or used as it consumes self
373    ///
374    /// # Examples
375    ///
376    /// ```rust
377    /// use ratatui::widgets::{Cell, Row, Table};
378    ///
379    /// let header = Row::new(vec![
380    ///     Cell::from("Header Cell 1"),
381    ///     Cell::from("Header Cell 2"),
382    /// ]);
383    /// let table = Table::default().header(header);
384    /// ```
385    #[must_use = "method moves the value of self and returns the modified value"]
386    pub fn header(mut self, header: Row<'a>) -> Self {
387        self.header = Some(header);
388        self
389    }
390
391    /// Sets the footer row
392    ///
393    /// The `footer` parameter is a [`Row`] which will be displayed at the bottom of the [`Table`]
394    ///
395    /// This is a fluent setter method which must be chained or used as it consumes self
396    ///
397    /// # Examples
398    ///
399    /// ```rust
400    /// use ratatui::widgets::{Cell, Row, Table};
401    ///
402    /// let footer = Row::new(vec![
403    ///     Cell::from("Footer Cell 1"),
404    ///     Cell::from("Footer Cell 2"),
405    /// ]);
406    /// let table = Table::default().footer(footer);
407    /// ```
408    #[must_use = "method moves the value of self and returns the modified value"]
409    pub fn footer(mut self, footer: Row<'a>) -> Self {
410        self.footer = Some(footer);
411        self
412    }
413
414    /// Set the widths of the columns.
415    ///
416    /// The `widths` parameter accepts any type that implements `IntoIterator<Item =
417    /// Into<Constraint>>`. This includes arrays, slices, vectors, iterators. `Into<Constraint>` is
418    /// implemented on u16, so you can pass an array, vec, etc. of u16 to this function to create a
419    /// table with fixed width columns.
420    ///
421    /// If the widths are empty, the table will be rendered with equal widths.
422    ///
423    /// This is a fluent setter method which must be chained or used as it consumes self
424    ///
425    /// # Examples
426    ///
427    /// ```rust
428    /// use ratatui::layout::Constraint;
429    /// use ratatui::widgets::{Cell, Row, Table};
430    ///
431    /// let table = Table::default().widths([Constraint::Length(5), Constraint::Length(5)]);
432    /// let table = Table::default().widths(vec![Constraint::Length(5); 2]);
433    ///
434    /// // widths could also be computed at runtime
435    /// let widths = [10, 10, 20].into_iter().map(|c| Constraint::Length(c));
436    /// let table = Table::default().widths(widths);
437    /// ```
438    #[must_use = "method moves the value of self and returns the modified value"]
439    pub fn widths<I>(mut self, widths: I) -> Self
440    where
441        I: IntoIterator,
442        I::Item: Into<Constraint>,
443    {
444        let widths = widths.into_iter().map(Into::into).collect_vec();
445        ensure_percentages_less_than_100(&widths);
446        self.widths = widths;
447        self
448    }
449
450    /// Set the spacing between columns
451    ///
452    /// This is a fluent setter method which must be chained or used as it consumes self
453    ///
454    /// # Examples
455    ///
456    /// ```rust
457    /// use ratatui::layout::Constraint;
458    /// use ratatui::widgets::{Row, Table};
459    ///
460    /// let rows = [Row::new(vec!["Cell1", "Cell2"])];
461    /// let widths = [Constraint::Length(5), Constraint::Length(5)];
462    /// let table = Table::new(rows, widths).column_spacing(1);
463    /// ```
464    #[must_use = "method moves the value of self and returns the modified value"]
465    pub const fn column_spacing(mut self, spacing: u16) -> Self {
466        self.column_spacing = spacing;
467        self
468    }
469
470    /// Wraps the table with a custom [`Block`] widget.
471    ///
472    /// The `block` parameter is of type [`Block`]. This holds the specified block to be
473    /// created around the [`Table`]
474    ///
475    /// This is a fluent setter method which must be chained or used as it consumes self
476    ///
477    /// # Examples
478    ///
479    /// ```rust
480    /// use ratatui::layout::Constraint;
481    /// use ratatui::widgets::{Block, Cell, Row, Table};
482    ///
483    /// let rows = [Row::new(vec!["Cell1", "Cell2"])];
484    /// let widths = [Constraint::Length(5), Constraint::Length(5)];
485    /// let block = Block::bordered().title("Table");
486    /// let table = Table::new(rows, widths).block(block);
487    /// ```
488    #[must_use = "method moves the value of self and returns the modified value"]
489    pub fn block(mut self, block: Block<'a>) -> Self {
490        self.block = Some(block);
491        self
492    }
493
494    /// Sets the base style of the widget
495    ///
496    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
497    /// your own type that implements [`Into<Style>`]).
498    ///
499    /// All text rendered by the widget will use this style, unless overridden by [`Block::style`],
500    /// [`Row::style`], [`Cell::style`], or the styles of cell's content.
501    ///
502    /// This is a fluent setter method which must be chained or used as it consumes self
503    ///
504    /// # Examples
505    ///
506    /// ```rust
507    /// use ratatui::layout::Constraint;
508    /// use ratatui::style::{Style, Stylize};
509    /// use ratatui::widgets::{Row, Table};
510    ///
511    /// # let rows = [Row::new(vec!["Cell1", "Cell2"])];
512    /// # let widths = [Constraint::Length(5), Constraint::Length(5)];
513    /// let table = Table::new(rows, widths).style(Style::new().red().italic());
514    /// ```
515    ///
516    /// `Table` also implements the [`Styled`] trait, which means you can use style shorthands from
517    /// the [`Stylize`] trait to set the style of the widget more concisely.
518    ///
519    /// ```rust
520    /// use ratatui::layout::Constraint;
521    /// use ratatui::style::Stylize;
522    /// use ratatui::widgets::{Cell, Row, Table};
523    ///
524    /// # let rows = [Row::new(vec!["Cell1", "Cell2"])];
525    /// # let widths = vec![Constraint::Length(5), Constraint::Length(5)];
526    /// let table = Table::new(rows, widths).red().italic();
527    /// ```
528    ///
529    /// [`Color`]: ratatui_core::style::Color
530    /// [`Stylize`]: ratatui_core::style::Stylize
531    #[must_use = "method moves the value of self and returns the modified value"]
532    pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
533        self.style = style.into();
534        self
535    }
536
537    /// Set the style of the selected row
538    ///
539    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
540    /// your own type that implements [`Into<Style>`]).
541    ///
542    /// This style will be applied to the entire row, including the selection symbol if it is
543    /// displayed, and will override any style set on the row or on the individual cells.
544    ///
545    /// This is a fluent setter method which must be chained or used as it consumes self
546    ///
547    /// # Examples
548    ///
549    /// ```rust
550    /// use ratatui::layout::Constraint;
551    /// use ratatui::style::{Style, Stylize};
552    /// use ratatui::widgets::{Cell, Row, Table};
553    ///
554    /// let rows = [Row::new(vec!["Cell1", "Cell2"])];
555    /// let widths = [Constraint::Length(5), Constraint::Length(5)];
556    /// let table = Table::new(rows, widths).highlight_style(Style::new().red().italic());
557    /// ```
558    ///
559    /// [`Color`]: ratatui_core::style::Color
560    #[must_use = "method moves the value of self and returns the modified value"]
561    #[deprecated(note = "use `row_highlight_style()` instead")]
562    pub fn highlight_style<S: Into<Style>>(self, highlight_style: S) -> Self {
563        self.row_highlight_style(highlight_style)
564    }
565
566    /// Set the style of the selected row
567    ///
568    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
569    /// your own type that implements [`Into<Style>`]).
570    ///
571    /// This style will be applied to the entire row, including the selection symbol if it is
572    /// displayed, and will override any style set on the row or on the individual cells.
573    ///
574    /// This is a fluent setter method which must be chained or used as it consumes self
575    ///
576    /// # Examples
577    ///
578    /// ```rust
579    /// # use ratatui::{layout::Constraint, style::{Style, Stylize}, widgets::{Row, Table}};
580    /// # let rows = [Row::new(vec!["Cell1", "Cell2"])];
581    /// # let widths = [Constraint::Length(5), Constraint::Length(5)];
582    /// let table = Table::new(rows, widths).row_highlight_style(Style::new().red().italic());
583    /// ```
584    /// [`Color`]: ratatui_core::style::Color
585    #[must_use = "method moves the value of self and returns the modified value"]
586    pub fn row_highlight_style<S: Into<Style>>(mut self, highlight_style: S) -> Self {
587        self.row_highlight_style = highlight_style.into();
588        self
589    }
590
591    /// Set the style of the selected column
592    ///
593    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
594    /// your own type that implements [`Into<Style>`]).
595    ///
596    /// This style will be applied to the entire column, and will override any style set on the
597    /// row or on the individual cells.
598    ///
599    /// This is a fluent setter method which must be chained or used as it consumes self
600    ///
601    /// # Examples
602    ///
603    /// ```rust
604    /// # use ratatui::{layout::Constraint, style::{Style, Stylize}, widgets::{Row, Table}};
605    /// # let rows = [Row::new(vec!["Cell1", "Cell2"])];
606    /// # let widths = [Constraint::Length(5), Constraint::Length(5)];
607    /// let table = Table::new(rows, widths).column_highlight_style(Style::new().red().italic());
608    /// ```
609    /// [`Color`]: ratatui_core::style::Color
610    #[must_use = "method moves the value of self and returns the modified value"]
611    pub fn column_highlight_style<S: Into<Style>>(mut self, highlight_style: S) -> Self {
612        self.column_highlight_style = highlight_style.into();
613        self
614    }
615
616    /// Set the style of the selected cell
617    ///
618    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
619    /// your own type that implements [`Into<Style>`]).
620    ///
621    /// This style will be applied to the selected cell, and will override any style set on the
622    /// row or on the individual cells.
623    ///
624    /// This is a fluent setter method which must be chained or used as it consumes self
625    ///
626    /// # Examples
627    ///
628    /// ```rust
629    /// # use ratatui::{layout::Constraint, style::{Style, Stylize}, widgets::{Row, Table}};
630    /// # let rows = [Row::new(vec!["Cell1", "Cell2"])];
631    /// # let widths = [Constraint::Length(5), Constraint::Length(5)];
632    /// let table = Table::new(rows, widths).cell_highlight_style(Style::new().red().italic());
633    /// ```
634    /// [`Color`]: ratatui_core::style::Color
635    #[must_use = "method moves the value of self and returns the modified value"]
636    pub fn cell_highlight_style<S: Into<Style>>(mut self, highlight_style: S) -> Self {
637        self.cell_highlight_style = highlight_style.into();
638        self
639    }
640
641    /// Set the symbol to be displayed in front of the selected row
642    ///
643    /// This is a fluent setter method which must be chained or used as it consumes self
644    ///
645    /// # Examples
646    ///
647    /// ```rust
648    /// use ratatui::layout::Constraint;
649    /// use ratatui::widgets::{Cell, Row, Table};
650    ///
651    /// # let rows = [Row::new(vec!["Cell1", "Cell2"])];
652    /// # let widths = [Constraint::Length(5), Constraint::Length(5)];
653    /// let table = Table::new(rows, widths).highlight_symbol(">>");
654    /// ```
655    #[must_use = "method moves the value of self and returns the modified value"]
656    pub fn highlight_symbol<T: Into<Text<'a>>>(mut self, highlight_symbol: T) -> Self {
657        self.highlight_symbol = highlight_symbol.into();
658        self
659    }
660
661    /// Set when to show the highlight spacing
662    ///
663    /// The highlight spacing is the spacing that is allocated for the selection symbol column (if
664    /// enabled) and is used to shift the table when a row is selected. This method allows you to
665    /// configure when this spacing is allocated.
666    ///
667    /// - [`HighlightSpacing::Always`] will always allocate the spacing, regardless of whether a row
668    ///   is selected or not. This means that the table will never change size, regardless of if a
669    ///   row is selected or not.
670    /// - [`HighlightSpacing::WhenSelected`] will only allocate the spacing if a row is selected.
671    ///   This means that the table will shift when a row is selected. This is the default setting
672    ///   for backwards compatibility, but it is recommended to use `HighlightSpacing::Always` for a
673    ///   better user experience.
674    /// - [`HighlightSpacing::Never`] will never allocate the spacing, regardless of whether a row
675    ///   is selected or not. This means that the highlight symbol will never be drawn.
676    ///
677    /// This is a fluent setter method which must be chained or used as it consumes self
678    ///
679    /// # Examples
680    ///
681    /// ```rust
682    /// use ratatui::layout::Constraint;
683    /// use ratatui::widgets::{HighlightSpacing, Row, Table};
684    ///
685    /// let rows = [Row::new(vec!["Cell1", "Cell2"])];
686    /// let widths = [Constraint::Length(5), Constraint::Length(5)];
687    /// let table = Table::new(rows, widths).highlight_spacing(HighlightSpacing::Always);
688    /// ```
689    #[must_use = "method moves the value of self and returns the modified value"]
690    pub const fn highlight_spacing(mut self, value: HighlightSpacing) -> Self {
691        self.highlight_spacing = value;
692        self
693    }
694
695    /// Set how extra space is distributed amongst columns.
696    ///
697    /// This determines how the space is distributed when the constraints are satisfied. By default,
698    /// the extra space is not distributed at all.  But this can be changed to distribute all extra
699    /// space to the last column or to distribute it equally.
700    ///
701    /// This is a fluent setter method which must be chained or used as it consumes self
702    ///
703    /// # Examples
704    ///
705    /// Create a table that needs at least 30 columns to display.  Any extra space will be assigned
706    /// to the last column.
707    /// ```
708    /// use ratatui::layout::{Constraint, Flex};
709    /// use ratatui::widgets::{Row, Table};
710    ///
711    /// let widths = [
712    ///     Constraint::Min(10),
713    ///     Constraint::Min(10),
714    ///     Constraint::Min(10),
715    /// ];
716    /// let table = Table::new(Vec::<Row>::new(), widths).flex(Flex::Legacy);
717    /// ```
718    #[must_use = "method moves the value of self and returns the modified value"]
719    pub const fn flex(mut self, flex: Flex) -> Self {
720        self.flex = flex;
721        self
722    }
723}
724
725impl Widget for Table<'_> {
726    fn render(self, area: Rect, buf: &mut Buffer) {
727        Widget::render(&self, area, buf);
728    }
729}
730
731impl Widget for &Table<'_> {
732    fn render(self, area: Rect, buf: &mut Buffer) {
733        let mut state = TableState::default();
734        StatefulWidget::render(self, area, buf, &mut state);
735    }
736}
737
738impl StatefulWidget for Table<'_> {
739    type State = TableState;
740
741    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
742        StatefulWidget::render(&self, area, buf, state);
743    }
744}
745
746impl StatefulWidget for &Table<'_> {
747    type State = TableState;
748
749    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
750        buf.set_style(area, self.style);
751        self.block.as_ref().render(area, buf);
752        let table_area = self.block.inner_if_some(area);
753        if table_area.is_empty() {
754            return;
755        }
756
757        if state.selected.is_some_and(|s| s >= self.rows.len()) {
758            state.select(Some(self.rows.len().saturating_sub(1)));
759        }
760
761        if self.rows.is_empty() {
762            state.select(None);
763        }
764
765        let column_count = self.column_count();
766        if state.selected_column.is_some_and(|s| s >= column_count) {
767            state.select_column(Some(column_count.saturating_sub(1)));
768        }
769        if column_count == 0 {
770            state.select_column(None);
771        }
772
773        let selection_width = self.selection_width(state);
774        let column_widths = self.get_column_widths(table_area.width, selection_width, column_count);
775        let (header_area, rows_area, footer_area) = self.layout(table_area);
776
777        self.render_header(header_area, buf, &column_widths);
778
779        self.render_rows(rows_area, buf, state, selection_width, &column_widths);
780
781        self.render_footer(footer_area, buf, &column_widths);
782    }
783}
784
785// private methods for rendering
786impl Table<'_> {
787    /// Splits the table area into a header, rows area and a footer
788    fn layout(&self, area: Rect) -> (Rect, Rect, Rect) {
789        let header_top_margin = self.header.as_ref().map_or(0, |h| h.top_margin);
790        let header_height = self.header.as_ref().map_or(0, |h| h.height);
791        let header_bottom_margin = self.header.as_ref().map_or(0, |h| h.bottom_margin);
792        let footer_top_margin = self.footer.as_ref().map_or(0, |h| h.top_margin);
793        let footer_height = self.footer.as_ref().map_or(0, |f| f.height);
794        let footer_bottom_margin = self.footer.as_ref().map_or(0, |h| h.bottom_margin);
795        let layout = Layout::vertical([
796            Constraint::Length(header_top_margin),
797            Constraint::Length(header_height),
798            Constraint::Length(header_bottom_margin),
799            Constraint::Min(0),
800            Constraint::Length(footer_top_margin),
801            Constraint::Length(footer_height),
802            Constraint::Length(footer_bottom_margin),
803        ])
804        .split(area);
805        let (header_area, rows_area, footer_area) = (layout[1], layout[3], layout[5]);
806        (header_area, rows_area, footer_area)
807    }
808
809    fn render_header(&self, area: Rect, buf: &mut Buffer, column_widths: &[(u16, u16)]) {
810        if let Some(ref header) = self.header {
811            buf.set_style(area, header.style);
812            for ((x, width), cell) in column_widths.iter().zip(header.cells.iter()) {
813                cell.render(Rect::new(area.x + x, area.y, *width, area.height), buf);
814            }
815        }
816    }
817
818    fn render_footer(&self, area: Rect, buf: &mut Buffer, column_widths: &[(u16, u16)]) {
819        if let Some(ref footer) = self.footer {
820            buf.set_style(area, footer.style);
821            for ((x, width), cell) in column_widths.iter().zip(footer.cells.iter()) {
822                cell.render(Rect::new(area.x + x, area.y, *width, area.height), buf);
823            }
824        }
825    }
826
827    fn render_rows(
828        &self,
829        area: Rect,
830        buf: &mut Buffer,
831        state: &mut TableState,
832        selection_width: u16,
833        columns_widths: &[(u16, u16)],
834    ) {
835        if self.rows.is_empty() {
836            return;
837        }
838
839        let (start_index, end_index) = self.visible_rows(state, area);
840        state.offset = start_index;
841
842        let mut y_offset = 0;
843
844        let mut selected_row_area = None;
845        for (i, row) in self
846            .rows
847            .iter()
848            .enumerate()
849            .skip(start_index)
850            .take(end_index - start_index)
851        {
852            let y = area.y + y_offset + row.top_margin;
853            let height = (y + row.height).min(area.bottom()).saturating_sub(y);
854            let row_area = Rect { y, height, ..area };
855            buf.set_style(row_area, row.style);
856
857            let is_selected = state.selected.is_some_and(|index| index == i);
858            if selection_width > 0 && is_selected {
859                let selection_area = Rect {
860                    width: selection_width,
861                    ..row_area
862                };
863                buf.set_style(selection_area, row.style);
864                (&self.highlight_symbol).render(selection_area, buf);
865            }
866            for ((x, width), cell) in columns_widths.iter().zip(row.cells.iter()) {
867                cell.render(
868                    Rect::new(row_area.x + x, row_area.y, *width, row_area.height),
869                    buf,
870                );
871            }
872            if is_selected {
873                selected_row_area = Some(row_area);
874            }
875            y_offset += row.height_with_margin();
876        }
877
878        let selected_column_area = state.selected_column.and_then(|s| {
879            // The selection is clamped by the column count. Since a user can manually specify an
880            // incorrect number of widths, we should use panic free methods.
881            columns_widths.get(s).map(|(x, width)| Rect {
882                x: x + area.x,
883                width: *width,
884                ..area
885            })
886        });
887
888        match (selected_row_area, selected_column_area) {
889            (Some(row_area), Some(col_area)) => {
890                buf.set_style(row_area, self.row_highlight_style);
891                buf.set_style(col_area, self.column_highlight_style);
892                let cell_area = row_area.intersection(col_area);
893                buf.set_style(cell_area, self.cell_highlight_style);
894            }
895            (Some(row_area), None) => {
896                buf.set_style(row_area, self.row_highlight_style);
897            }
898            (None, Some(col_area)) => {
899                buf.set_style(col_area, self.column_highlight_style);
900            }
901            (None, None) => (),
902        }
903    }
904
905    /// Return the indexes of the visible rows.
906    ///
907    /// The algorithm works as follows:
908    /// - start at the offset and calculate the height of the rows that can be displayed within the
909    ///   area.
910    /// - if the selected row is not visible, scroll the table to ensure it is visible.
911    /// - if there is still space to fill then there's a partial row at the end which should be
912    ///   included in the view.
913    fn visible_rows(&self, state: &TableState, area: Rect) -> (usize, usize) {
914        let last_row = self.rows.len().saturating_sub(1);
915        let mut start = state.offset.min(last_row);
916
917        if let Some(selected) = state.selected {
918            start = start.min(selected);
919        }
920
921        let mut end = start;
922        let mut height = 0;
923
924        for item in self.rows.iter().skip(start) {
925            if height + item.height > area.height {
926                break;
927            }
928            height += item.height_with_margin();
929            end += 1;
930        }
931
932        if let Some(selected) = state.selected {
933            let selected = selected.min(last_row);
934
935            // scroll down until the selected row is visible
936            while selected >= end {
937                height = height.saturating_add(self.rows[end].height_with_margin());
938                end += 1;
939                while height > area.height {
940                    height = height.saturating_sub(self.rows[start].height_with_margin());
941                    start += 1;
942                }
943            }
944        }
945
946        // Include a partial row if there is space
947        if height < area.height && end < self.rows.len() {
948            end += 1;
949        }
950
951        (start, end)
952    }
953
954    /// Get all offsets and widths of all user specified columns.
955    ///
956    /// Returns (x, width). When self.widths is empty, it is assumed `.widths()` has not been called
957    /// and a default of equal widths is returned.
958    fn get_column_widths(
959        &self,
960        max_width: u16,
961        selection_width: u16,
962        col_count: usize,
963    ) -> Vec<(u16, u16)> {
964        let widths = if self.widths.is_empty() {
965            // Divide the space between each column equally
966            vec![Constraint::Length(max_width / col_count.max(1) as u16); col_count]
967        } else {
968            self.widths.clone()
969        };
970        // this will always allocate a selection area
971        let [_selection_area, columns_area] =
972            Layout::horizontal([Constraint::Length(selection_width), Constraint::Fill(0)])
973                .areas(Rect::new(0, 0, max_width, 1));
974        let rects = Layout::horizontal(widths)
975            .flex(self.flex)
976            .spacing(self.column_spacing)
977            .split(columns_area);
978        rects.iter().map(|c| (c.x, c.width)).collect()
979    }
980
981    fn column_count(&self) -> usize {
982        self.rows
983            .iter()
984            .chain(self.footer.iter())
985            .chain(self.header.iter())
986            .map(|r| r.cells.len())
987            .max()
988            .unwrap_or_default()
989    }
990
991    /// Returns the width of the selection column if a row is selected, or the `highlight_spacing`
992    /// is set to show the column always, otherwise 0.
993    fn selection_width(&self, state: &TableState) -> u16 {
994        let has_selection = state.selected.is_some();
995        if self.highlight_spacing.should_add(has_selection) {
996            self.highlight_symbol.width() as u16
997        } else {
998            0
999        }
1000    }
1001}
1002
1003fn ensure_percentages_less_than_100(widths: &[Constraint]) {
1004    for w in widths {
1005        if let Constraint::Percentage(p) = w {
1006            assert!(
1007                *p <= 100,
1008                "Percentages should be between 0 and 100 inclusively."
1009            );
1010        }
1011    }
1012}
1013
1014impl Styled for Table<'_> {
1015    type Item = Self;
1016
1017    fn style(&self) -> Style {
1018        self.style
1019    }
1020
1021    fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
1022        self.style(style)
1023    }
1024}
1025
1026impl<'a, Item> FromIterator<Item> for Table<'a>
1027where
1028    Item: Into<Row<'a>>,
1029{
1030    /// Collects an iterator of rows into a table.
1031    ///
1032    /// When collecting from an iterator into a table, the user must provide the widths using
1033    /// `Table::widths` after construction.
1034    fn from_iter<Iter: IntoIterator<Item = Item>>(rows: Iter) -> Self {
1035        let widths: [Constraint; 0] = [];
1036        Self::new(rows, widths)
1037    }
1038}
1039
1040#[cfg(test)]
1041mod tests {
1042    use alloc::string::ToString;
1043    use alloc::{format, vec};
1044
1045    use ratatui_core::layout::Constraint::*;
1046    use ratatui_core::style::{Color, Modifier, Style, Stylize};
1047    use ratatui_core::text::Line;
1048    use rstest::{fixture, rstest};
1049
1050    use super::*;
1051    use crate::table::Cell;
1052
1053    #[test]
1054    fn new() {
1055        let rows = [Row::new(vec![Cell::from("")])];
1056        let widths = [Constraint::Percentage(100)];
1057        let table = Table::new(rows.clone(), widths);
1058        assert_eq!(table.rows, rows);
1059        assert_eq!(table.header, None);
1060        assert_eq!(table.footer, None);
1061        assert_eq!(table.widths, widths);
1062        assert_eq!(table.column_spacing, 1);
1063        assert_eq!(table.block, None);
1064        assert_eq!(table.style, Style::default());
1065        assert_eq!(table.row_highlight_style, Style::default());
1066        assert_eq!(table.highlight_symbol, Text::default());
1067        assert_eq!(table.highlight_spacing, HighlightSpacing::WhenSelected);
1068        assert_eq!(table.flex, Flex::Start);
1069    }
1070
1071    #[test]
1072    fn default() {
1073        let table = Table::default();
1074        assert_eq!(table.rows, []);
1075        assert_eq!(table.header, None);
1076        assert_eq!(table.footer, None);
1077        assert_eq!(table.widths, []);
1078        assert_eq!(table.column_spacing, 1);
1079        assert_eq!(table.block, None);
1080        assert_eq!(table.style, Style::default());
1081        assert_eq!(table.row_highlight_style, Style::default());
1082        assert_eq!(table.highlight_symbol, Text::default());
1083        assert_eq!(table.highlight_spacing, HighlightSpacing::WhenSelected);
1084        assert_eq!(table.flex, Flex::Start);
1085    }
1086
1087    #[test]
1088    fn collect() {
1089        let table = (0..4)
1090            .map(|i| -> Row { (0..4).map(|j| format!("{i}*{j} = {}", i * j)).collect() })
1091            .collect::<Table>()
1092            .widths([Constraint::Percentage(25); 4]);
1093
1094        let expected_rows: Vec<Row> = vec![
1095            Row::new(["0*0 = 0", "0*1 = 0", "0*2 = 0", "0*3 = 0"]),
1096            Row::new(["1*0 = 0", "1*1 = 1", "1*2 = 2", "1*3 = 3"]),
1097            Row::new(["2*0 = 0", "2*1 = 2", "2*2 = 4", "2*3 = 6"]),
1098            Row::new(["3*0 = 0", "3*1 = 3", "3*2 = 6", "3*3 = 9"]),
1099        ];
1100
1101        assert_eq!(table.rows, expected_rows);
1102        assert_eq!(table.widths, [Constraint::Percentage(25); 4]);
1103    }
1104
1105    #[test]
1106    fn widths() {
1107        let table = Table::default().widths([Constraint::Length(100)]);
1108        assert_eq!(table.widths, [Constraint::Length(100)]);
1109
1110        // ensure that code that uses &[] continues to work as there is a large amount of code that
1111        // uses this pattern
1112        #[expect(clippy::needless_borrows_for_generic_args)]
1113        let table = Table::default().widths(&[Constraint::Length(100)]);
1114        assert_eq!(table.widths, [Constraint::Length(100)]);
1115
1116        let table = Table::default().widths(vec![Constraint::Length(100)]);
1117        assert_eq!(table.widths, [Constraint::Length(100)]);
1118
1119        // ensure that code that uses &some_vec continues to work as there is a large amount of code
1120        // that uses this pattern
1121        #[expect(clippy::needless_borrows_for_generic_args)]
1122        let table = Table::default().widths(&vec![Constraint::Length(100)]);
1123        assert_eq!(table.widths, [Constraint::Length(100)]);
1124
1125        let table = Table::default().widths([100].into_iter().map(Constraint::Length));
1126        assert_eq!(table.widths, [Constraint::Length(100)]);
1127    }
1128
1129    #[test]
1130    fn rows() {
1131        let rows = [Row::new(vec![Cell::from("")])];
1132        let table = Table::default().rows(rows.clone());
1133        assert_eq!(table.rows, rows);
1134    }
1135
1136    #[test]
1137    fn column_spacing() {
1138        let table = Table::default().column_spacing(2);
1139        assert_eq!(table.column_spacing, 2);
1140    }
1141
1142    #[test]
1143    fn block() {
1144        let block = Block::bordered().title("Table");
1145        let table = Table::default().block(block.clone());
1146        assert_eq!(table.block, Some(block));
1147    }
1148
1149    #[test]
1150    fn header() {
1151        let header = Row::new(vec![Cell::from("")]);
1152        let table = Table::default().header(header.clone());
1153        assert_eq!(table.header, Some(header));
1154    }
1155
1156    #[test]
1157    fn footer() {
1158        let footer = Row::new(vec![Cell::from("")]);
1159        let table = Table::default().footer(footer.clone());
1160        assert_eq!(table.footer, Some(footer));
1161    }
1162
1163    #[test]
1164    #[expect(deprecated)]
1165    fn highlight_style() {
1166        let style = Style::default().red().italic();
1167        let table = Table::default().highlight_style(style);
1168        assert_eq!(table.row_highlight_style, style);
1169    }
1170
1171    #[test]
1172    fn row_highlight_style() {
1173        let style = Style::default().red().italic();
1174        let table = Table::default().row_highlight_style(style);
1175        assert_eq!(table.row_highlight_style, style);
1176    }
1177
1178    #[test]
1179    fn column_highlight_style() {
1180        let style = Style::default().red().italic();
1181        let table = Table::default().column_highlight_style(style);
1182        assert_eq!(table.column_highlight_style, style);
1183    }
1184
1185    #[test]
1186    fn cell_highlight_style() {
1187        let style = Style::default().red().italic();
1188        let table = Table::default().cell_highlight_style(style);
1189        assert_eq!(table.cell_highlight_style, style);
1190    }
1191
1192    #[test]
1193    fn highlight_symbol() {
1194        let table = Table::default().highlight_symbol(">>");
1195        assert_eq!(table.highlight_symbol, Text::from(">>"));
1196    }
1197
1198    #[test]
1199    fn highlight_spacing() {
1200        let table = Table::default().highlight_spacing(HighlightSpacing::Always);
1201        assert_eq!(table.highlight_spacing, HighlightSpacing::Always);
1202    }
1203
1204    #[test]
1205    #[should_panic = "Percentages should be between 0 and 100 inclusively"]
1206    fn table_invalid_percentages() {
1207        let _ = Table::default().widths([Constraint::Percentage(110)]);
1208    }
1209
1210    #[test]
1211    fn widths_conversions() {
1212        let array = [Constraint::Percentage(100)];
1213        let table = Table::new(Vec::<Row>::new(), array);
1214        assert_eq!(table.widths, [Constraint::Percentage(100)], "array");
1215
1216        let array_ref = &[Constraint::Percentage(100)];
1217        let table = Table::new(Vec::<Row>::new(), array_ref);
1218        assert_eq!(table.widths, [Constraint::Percentage(100)], "array ref");
1219
1220        let vec = vec![Constraint::Percentage(100)];
1221        let slice = vec.as_slice();
1222        let table = Table::new(Vec::<Row>::new(), slice);
1223        assert_eq!(table.widths, [Constraint::Percentage(100)], "slice");
1224
1225        let vec = vec![Constraint::Percentage(100)];
1226        let table = Table::new(Vec::<Row>::new(), vec);
1227        assert_eq!(table.widths, [Constraint::Percentage(100)], "vec");
1228
1229        let vec_ref = &vec![Constraint::Percentage(100)];
1230        let table = Table::new(Vec::<Row>::new(), vec_ref);
1231        assert_eq!(table.widths, [Constraint::Percentage(100)], "vec ref");
1232    }
1233
1234    #[cfg(test)]
1235    mod state {
1236        use ratatui_core::buffer::Buffer;
1237        use ratatui_core::layout::{Constraint, Rect};
1238        use ratatui_core::widgets::StatefulWidget;
1239
1240        use super::*;
1241        use crate::table::{Row, Table, TableState};
1242
1243        #[fixture]
1244        fn table_buf() -> Buffer {
1245            Buffer::empty(Rect::new(0, 0, 10, 10))
1246        }
1247
1248        #[rstest]
1249        fn test_list_state_empty_list(mut table_buf: Buffer) {
1250            let mut state = TableState::default();
1251
1252            let rows: Vec<Row> = Vec::new();
1253            let widths = vec![Constraint::Percentage(100)];
1254            let table = Table::new(rows, widths);
1255            state.select_first();
1256            StatefulWidget::render(table, table_buf.area, &mut table_buf, &mut state);
1257            assert_eq!(state.selected, None);
1258            assert_eq!(state.selected_column, None);
1259        }
1260
1261        #[rstest]
1262        fn test_list_state_single_item(mut table_buf: Buffer) {
1263            let mut state = TableState::default();
1264
1265            let widths = vec![Constraint::Percentage(100)];
1266
1267            let items = vec![Row::new(vec!["Item 1"])];
1268            let table = Table::new(items, widths);
1269            state.select_first();
1270            StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
1271            assert_eq!(state.selected, Some(0));
1272            assert_eq!(state.selected_column, None);
1273
1274            state.select_last();
1275            StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
1276            assert_eq!(state.selected, Some(0));
1277            assert_eq!(state.selected_column, None);
1278
1279            state.select_previous();
1280            StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
1281            assert_eq!(state.selected, Some(0));
1282            assert_eq!(state.selected_column, None);
1283
1284            state.select_next();
1285            StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
1286            assert_eq!(state.selected, Some(0));
1287            assert_eq!(state.selected_column, None);
1288
1289            let mut state = TableState::default();
1290
1291            state.select_first_column();
1292            StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
1293            assert_eq!(state.selected_column, Some(0));
1294            assert_eq!(state.selected, None);
1295
1296            state.select_last_column();
1297            StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
1298            assert_eq!(state.selected_column, Some(0));
1299            assert_eq!(state.selected, None);
1300
1301            state.select_previous_column();
1302            StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
1303            assert_eq!(state.selected_column, Some(0));
1304            assert_eq!(state.selected, None);
1305
1306            state.select_next_column();
1307            StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
1308            assert_eq!(state.selected_column, Some(0));
1309            assert_eq!(state.selected, None);
1310        }
1311    }
1312
1313    #[cfg(test)]
1314    mod render {
1315        use ratatui_core::layout::Alignment;
1316
1317        use super::*;
1318
1319        #[test]
1320        fn render_empty_area() {
1321            let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
1322            let rows = vec![Row::new(vec!["Cell1", "Cell2"])];
1323            let table = Table::new(rows, vec![Constraint::Length(5); 2]);
1324            Widget::render(table, Rect::new(0, 0, 0, 0), &mut buf);
1325            assert_eq!(buf, Buffer::empty(Rect::new(0, 0, 15, 3)));
1326        }
1327
1328        #[test]
1329        fn render_default() {
1330            let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
1331            let table = Table::default();
1332            Widget::render(table, Rect::new(0, 0, 15, 3), &mut buf);
1333            assert_eq!(buf, Buffer::empty(Rect::new(0, 0, 15, 3)));
1334        }
1335
1336        #[test]
1337        fn render_with_block() {
1338            let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
1339            let rows = vec![
1340                Row::new(vec!["Cell1", "Cell2"]),
1341                Row::new(vec!["Cell3", "Cell4"]),
1342            ];
1343            let block = Block::bordered().title("Block");
1344            let table = Table::new(rows, vec![Constraint::Length(5); 2]).block(block);
1345            Widget::render(table, Rect::new(0, 0, 15, 3), &mut buf);
1346            #[rustfmt::skip]
1347            let expected = Buffer::with_lines([
1348                "┌Block────────┐",
1349                "│Cell1 Cell2  │",
1350                "└─────────────┘",
1351            ]);
1352            assert_eq!(buf, expected);
1353        }
1354
1355        #[test]
1356        fn render_with_header() {
1357            let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
1358            let header = Row::new(vec!["Head1", "Head2"]);
1359            let rows = vec![
1360                Row::new(vec!["Cell1", "Cell2"]),
1361                Row::new(vec!["Cell3", "Cell4"]),
1362            ];
1363            let table = Table::new(rows, [Constraint::Length(5); 2]).header(header);
1364            Widget::render(table, Rect::new(0, 0, 15, 3), &mut buf);
1365            #[rustfmt::skip]
1366            let expected = Buffer::with_lines([
1367                "Head1 Head2    ",
1368                "Cell1 Cell2    ",
1369                "Cell3 Cell4    ",
1370            ]);
1371            assert_eq!(buf, expected);
1372        }
1373
1374        #[test]
1375        fn render_with_footer() {
1376            let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
1377            let footer = Row::new(vec!["Foot1", "Foot2"]);
1378            let rows = vec![
1379                Row::new(vec!["Cell1", "Cell2"]),
1380                Row::new(vec!["Cell3", "Cell4"]),
1381            ];
1382            let table = Table::new(rows, [Constraint::Length(5); 2]).footer(footer);
1383            Widget::render(table, Rect::new(0, 0, 15, 3), &mut buf);
1384            #[rustfmt::skip]
1385            let expected = Buffer::with_lines([
1386                "Cell1 Cell2    ",
1387                "Cell3 Cell4    ",
1388                "Foot1 Foot2    ",
1389            ]);
1390            assert_eq!(buf, expected);
1391        }
1392
1393        #[test]
1394        fn render_with_header_and_footer() {
1395            let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
1396            let header = Row::new(vec!["Head1", "Head2"]);
1397            let footer = Row::new(vec!["Foot1", "Foot2"]);
1398            let rows = vec![Row::new(vec!["Cell1", "Cell2"])];
1399            let table = Table::new(rows, [Constraint::Length(5); 2])
1400                .header(header)
1401                .footer(footer);
1402            Widget::render(table, Rect::new(0, 0, 15, 3), &mut buf);
1403            #[rustfmt::skip]
1404            let expected = Buffer::with_lines([
1405                "Head1 Head2    ",
1406                "Cell1 Cell2    ",
1407                "Foot1 Foot2    ",
1408            ]);
1409            assert_eq!(buf, expected);
1410        }
1411
1412        #[test]
1413        fn render_with_header_margin() {
1414            let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
1415            let header = Row::new(vec!["Head1", "Head2"]).bottom_margin(1);
1416            let rows = vec![
1417                Row::new(vec!["Cell1", "Cell2"]),
1418                Row::new(vec!["Cell3", "Cell4"]),
1419            ];
1420            let table = Table::new(rows, [Constraint::Length(5); 2]).header(header);
1421            Widget::render(table, Rect::new(0, 0, 15, 3), &mut buf);
1422            #[rustfmt::skip]
1423            let expected = Buffer::with_lines([
1424                "Head1 Head2    ",
1425                "               ",
1426                "Cell1 Cell2    ",
1427            ]);
1428            assert_eq!(buf, expected);
1429        }
1430
1431        #[test]
1432        fn render_with_footer_margin() {
1433            let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
1434            let footer = Row::new(vec!["Foot1", "Foot2"]).top_margin(1);
1435            let rows = vec![Row::new(vec!["Cell1", "Cell2"])];
1436            let table = Table::new(rows, [Constraint::Length(5); 2]).footer(footer);
1437            Widget::render(table, Rect::new(0, 0, 15, 3), &mut buf);
1438            #[rustfmt::skip]
1439            let expected = Buffer::with_lines([
1440                "Cell1 Cell2    ",
1441                "               ",
1442                "Foot1 Foot2    ",
1443            ]);
1444            assert_eq!(buf, expected);
1445        }
1446
1447        #[test]
1448        fn render_with_row_margin() {
1449            let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
1450            let rows = vec![
1451                Row::new(vec!["Cell1", "Cell2"]).bottom_margin(1),
1452                Row::new(vec!["Cell3", "Cell4"]),
1453            ];
1454            let table = Table::new(rows, [Constraint::Length(5); 2]);
1455            Widget::render(table, Rect::new(0, 0, 15, 3), &mut buf);
1456            #[rustfmt::skip]
1457            let expected = Buffer::with_lines([
1458                "Cell1 Cell2    ",
1459                "               ",
1460                "Cell3 Cell4    ",
1461            ]);
1462            assert_eq!(buf, expected);
1463        }
1464
1465        #[test]
1466        fn render_with_tall_row() {
1467            let mut buf = Buffer::empty(Rect::new(0, 0, 23, 3));
1468            let rows = vec![
1469                Row::new(vec!["Cell1", "Cell2"]),
1470                Row::new(vec![
1471                    Text::raw("Cell3-Line1\nCell3-Line2\nCell3-Line3"),
1472                    Text::raw("Cell4-Line1\nCell4-Line2\nCell4-Line3"),
1473                ])
1474                .height(3),
1475            ];
1476            let table = Table::new(rows, [Constraint::Length(11); 2]);
1477            Widget::render(table, Rect::new(0, 0, 23, 3), &mut buf);
1478            #[rustfmt::skip]
1479            let expected = Buffer::with_lines([
1480                "Cell1       Cell2      ",
1481                "Cell3-Line1 Cell4-Line1",
1482                "Cell3-Line2 Cell4-Line2",
1483            ]);
1484            assert_eq!(buf, expected);
1485        }
1486
1487        #[test]
1488        fn render_with_alignment() {
1489            let mut buf = Buffer::empty(Rect::new(0, 0, 10, 3));
1490            let rows = vec![
1491                Row::new(vec![Line::from("Left").alignment(Alignment::Left)]),
1492                Row::new(vec![Line::from("Center").alignment(Alignment::Center)]),
1493                Row::new(vec![Line::from("Right").alignment(Alignment::Right)]),
1494            ];
1495            let table = Table::new(rows, [Percentage(100)]);
1496            Widget::render(table, Rect::new(0, 0, 10, 3), &mut buf);
1497            let expected = Buffer::with_lines(["Left      ", "  Center  ", "     Right"]);
1498            assert_eq!(buf, expected);
1499        }
1500
1501        #[test]
1502        fn render_with_overflow_does_not_panic() {
1503            let mut buf = Buffer::empty(Rect::new(0, 0, 20, 3));
1504            let table = Table::new(Vec::<Row>::new(), [Constraint::Min(20); 1])
1505                .header(Row::new([Line::from("").alignment(Alignment::Right)]))
1506                .footer(Row::new([Line::from("").alignment(Alignment::Right)]));
1507            Widget::render(table, Rect::new(0, 0, 20, 3), &mut buf);
1508        }
1509
1510        #[test]
1511        fn render_with_selected_column_and_incorrect_width_count_does_not_panic() {
1512            let mut buf = Buffer::empty(Rect::new(0, 0, 20, 3));
1513            let table = Table::new(
1514                vec![Row::new(vec!["Row1", "Row2", "Row3"])],
1515                [Constraint::Length(10); 1],
1516            );
1517            let mut state = TableState::new().with_selected_column(2);
1518            StatefulWidget::render(table, Rect::new(0, 0, 20, 3), &mut buf, &mut state);
1519        }
1520
1521        #[test]
1522        fn render_with_selected() {
1523            let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
1524            let rows = vec![
1525                Row::new(vec!["Cell1", "Cell2"]),
1526                Row::new(vec!["Cell3", "Cell4"]),
1527            ];
1528            let table = Table::new(rows, [Constraint::Length(5); 2])
1529                .row_highlight_style(Style::new().red())
1530                .highlight_symbol(">>");
1531            let mut state = TableState::new().with_selected(Some(0));
1532            StatefulWidget::render(table, Rect::new(0, 0, 15, 3), &mut buf, &mut state);
1533            let expected = Buffer::with_lines([
1534                ">>Cell1 Cell2  ".red(),
1535                "  Cell3 Cell4  ".into(),
1536                "               ".into(),
1537            ]);
1538            assert_eq!(buf, expected);
1539        }
1540
1541        #[test]
1542        fn render_with_selected_column() {
1543            let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
1544            let rows = vec![
1545                Row::new(vec!["Cell1", "Cell2"]),
1546                Row::new(vec!["Cell3", "Cell4"]),
1547            ];
1548            let table = Table::new(rows, [Constraint::Length(5); 2])
1549                .column_highlight_style(Style::new().blue())
1550                .highlight_symbol(">>");
1551            let mut state = TableState::new().with_selected_column(Some(1));
1552            StatefulWidget::render(table, Rect::new(0, 0, 15, 3), &mut buf, &mut state);
1553            let expected = Buffer::with_lines::<[Line; 3]>([
1554                Line::from(vec![
1555                    "Cell1".into(),
1556                    " ".into(),
1557                    "Cell2".blue(),
1558                    "    ".into(),
1559                ]),
1560                Line::from(vec![
1561                    "Cell3".into(),
1562                    " ".into(),
1563                    "Cell4".blue(),
1564                    "    ".into(),
1565                ]),
1566                Line::from(vec!["      ".into(), "     ".blue(), "    ".into()]),
1567            ]);
1568            assert_eq!(buf, expected);
1569        }
1570
1571        #[test]
1572        fn render_with_selected_cell() {
1573            let mut buf = Buffer::empty(Rect::new(0, 0, 20, 4));
1574            let rows = vec![
1575                Row::new(vec!["Cell1", "Cell2", "Cell3"]),
1576                Row::new(vec!["Cell4", "Cell5", "Cell6"]),
1577                Row::new(vec!["Cell7", "Cell8", "Cell9"]),
1578            ];
1579            let table = Table::new(rows, [Constraint::Length(5); 3])
1580                .highlight_symbol(">>")
1581                .cell_highlight_style(Style::new().green());
1582            let mut state = TableState::new().with_selected_cell((1, 2));
1583            StatefulWidget::render(table, Rect::new(0, 0, 20, 4), &mut buf, &mut state);
1584            let expected = Buffer::with_lines::<[Line; 4]>([
1585                Line::from(vec!["  Cell1 ".into(), "Cell2 ".into(), "Cell3".into()]),
1586                Line::from(vec![">>Cell4 Cell5 ".into(), "Cell6".green(), " ".into()]),
1587                Line::from(vec!["  Cell7 ".into(), "Cell8 ".into(), "Cell9".into()]),
1588                Line::from(vec!["                    ".into()]),
1589            ]);
1590            assert_eq!(buf, expected);
1591        }
1592
1593        #[test]
1594        fn render_with_selected_row_and_column() {
1595            let mut buf = Buffer::empty(Rect::new(0, 0, 20, 4));
1596            let rows = vec![
1597                Row::new(vec!["Cell1", "Cell2", "Cell3"]),
1598                Row::new(vec!["Cell4", "Cell5", "Cell6"]),
1599                Row::new(vec!["Cell7", "Cell8", "Cell9"]),
1600            ];
1601            let table = Table::new(rows, [Constraint::Length(5); 3])
1602                .highlight_symbol(">>")
1603                .row_highlight_style(Style::new().red())
1604                .column_highlight_style(Style::new().blue());
1605            let mut state = TableState::new().with_selected(1).with_selected_column(2);
1606            StatefulWidget::render(table, Rect::new(0, 0, 20, 4), &mut buf, &mut state);
1607            let expected = Buffer::with_lines::<[Line; 4]>([
1608                Line::from(vec!["  Cell1 ".into(), "Cell2 ".into(), "Cell3".blue()]),
1609                Line::from(vec![">>Cell4 Cell5 ".red(), "Cell6".blue(), " ".red()]),
1610                Line::from(vec!["  Cell7 ".into(), "Cell8 ".into(), "Cell9".blue()]),
1611                Line::from(vec!["              ".into(), "     ".blue(), " ".into()]),
1612            ]);
1613            assert_eq!(buf, expected);
1614        }
1615
1616        #[test]
1617        fn render_with_selected_row_and_column_and_cell() {
1618            let mut buf = Buffer::empty(Rect::new(0, 0, 20, 4));
1619            let rows = vec![
1620                Row::new(vec!["Cell1", "Cell2", "Cell3"]),
1621                Row::new(vec!["Cell4", "Cell5", "Cell6"]),
1622                Row::new(vec!["Cell7", "Cell8", "Cell9"]),
1623            ];
1624            let table = Table::new(rows, [Constraint::Length(5); 3])
1625                .highlight_symbol(">>")
1626                .row_highlight_style(Style::new().red())
1627                .column_highlight_style(Style::new().blue())
1628                .cell_highlight_style(Style::new().green());
1629            let mut state = TableState::new().with_selected(1).with_selected_column(2);
1630            StatefulWidget::render(table, Rect::new(0, 0, 20, 4), &mut buf, &mut state);
1631            let expected = Buffer::with_lines::<[Line; 4]>([
1632                Line::from(vec!["  Cell1 ".into(), "Cell2 ".into(), "Cell3".blue()]),
1633                Line::from(vec![">>Cell4 Cell5 ".red(), "Cell6".green(), " ".red()]),
1634                Line::from(vec!["  Cell7 ".into(), "Cell8 ".into(), "Cell9".blue()]),
1635                Line::from(vec!["              ".into(), "     ".blue(), " ".into()]),
1636            ]);
1637            assert_eq!(buf, expected);
1638        }
1639
1640        /// Note that this includes a regression test for a bug where the table would not render the
1641        /// correct rows when there is no selection.
1642        /// <https://github.com/ratatui/ratatui/issues/1179>
1643        #[rstest]
1644        #[case::no_selection(None, 50, ["50", "51", "52", "53", "54"])]
1645        #[case::selection_before_offset(20, 20, ["20", "21", "22", "23", "24"])]
1646        #[case::selection_immediately_before_offset(49, 49, ["49", "50", "51", "52", "53"])]
1647        #[case::selection_at_start_of_offset(50, 50, ["50", "51", "52", "53", "54"])]
1648        #[case::selection_at_end_of_offset(54, 50, ["50", "51", "52", "53", "54"])]
1649        #[case::selection_immediately_after_offset(55, 51, ["51", "52", "53", "54", "55"])]
1650        #[case::selection_after_offset(80, 76, ["76", "77", "78", "79", "80"])]
1651        fn render_with_selection_and_offset<T: Into<Option<usize>>>(
1652            #[case] selected_row: T,
1653            #[case] expected_offset: usize,
1654            #[case] expected_items: [&str; 5],
1655        ) {
1656            // render 100 rows offset at 50, with a selected row
1657            let rows = (0..100).map(|i| Row::new([i.to_string()]));
1658            let table = Table::new(rows, [Constraint::Length(2)]);
1659            let mut buf = Buffer::empty(Rect::new(0, 0, 2, 5));
1660            let mut state = TableState::new()
1661                .with_offset(50)
1662                .with_selected(selected_row.into());
1663
1664            StatefulWidget::render(table.clone(), Rect::new(0, 0, 5, 5), &mut buf, &mut state);
1665
1666            assert_eq!(buf, Buffer::with_lines(expected_items));
1667            assert_eq!(state.offset, expected_offset);
1668        }
1669    }
1670
1671    // test how constraints interact with table column width allocation
1672    mod column_widths {
1673        use super::*;
1674
1675        #[test]
1676        fn length_constraint() {
1677            // without selection, more than needed width
1678            let table = Table::default().widths([Length(4), Length(4)]);
1679            assert_eq!(table.get_column_widths(20, 0, 0), [(0, 4), (5, 4)]);
1680
1681            // with selection, more than needed width
1682            let table = Table::default().widths([Length(4), Length(4)]);
1683            assert_eq!(table.get_column_widths(20, 3, 0), [(3, 4), (8, 4)]);
1684
1685            // without selection, less than needed width
1686            let table = Table::default().widths([Length(4), Length(4)]);
1687            assert_eq!(table.get_column_widths(7, 0, 0), [(0, 3), (4, 3)]);
1688
1689            // with selection, less than needed width
1690            // <--------7px-------->
1691            // ┌────────┐x┌────────┐
1692            // │ (3, 2) │x│ (6, 1) │
1693            // └────────┘x└────────┘
1694            // column spacing (i.e. `x`) is always prioritized
1695            let table = Table::default().widths([Length(4), Length(4)]);
1696            assert_eq!(table.get_column_widths(7, 3, 0), [(3, 2), (6, 1)]);
1697        }
1698
1699        #[test]
1700        fn max_constraint() {
1701            // without selection, more than needed width
1702            let table = Table::default().widths([Max(4), Max(4)]);
1703            assert_eq!(table.get_column_widths(20, 0, 0), [(0, 4), (5, 4)]);
1704
1705            // with selection, more than needed width
1706            let table = Table::default().widths([Max(4), Max(4)]);
1707            assert_eq!(table.get_column_widths(20, 3, 0), [(3, 4), (8, 4)]);
1708
1709            // without selection, less than needed width
1710            let table = Table::default().widths([Max(4), Max(4)]);
1711            assert_eq!(table.get_column_widths(7, 0, 0), [(0, 3), (4, 3)]);
1712
1713            // with selection, less than needed width
1714            let table = Table::default().widths([Max(4), Max(4)]);
1715            assert_eq!(table.get_column_widths(7, 3, 0), [(3, 2), (6, 1)]);
1716        }
1717
1718        #[test]
1719        fn min_constraint() {
1720            // in its currently stage, the "Min" constraint does not grow to use the possible
1721            // available length and enabling "expand_to_fill" will just stretch the last
1722            // constraint and not split it with all available constraints
1723
1724            // without selection, more than needed width
1725            let table = Table::default().widths([Min(4), Min(4)]);
1726            assert_eq!(table.get_column_widths(20, 0, 0), [(0, 10), (11, 9)]);
1727
1728            // with selection, more than needed width
1729            let table = Table::default().widths([Min(4), Min(4)]);
1730            assert_eq!(table.get_column_widths(20, 3, 0), [(3, 8), (12, 8)]);
1731
1732            // without selection, less than needed width
1733            // allocates spacer
1734            let table = Table::default().widths([Min(4), Min(4)]);
1735            assert_eq!(table.get_column_widths(7, 0, 0), [(0, 3), (4, 3)]);
1736
1737            // with selection, less than needed width
1738            // always allocates selection and spacer
1739            let table = Table::default().widths([Min(4), Min(4)]);
1740            assert_eq!(table.get_column_widths(7, 3, 0), [(3, 2), (6, 1)]);
1741        }
1742
1743        #[test]
1744        fn percentage_constraint() {
1745            // without selection, more than needed width
1746            let table = Table::default().widths([Percentage(30), Percentage(30)]);
1747            assert_eq!(table.get_column_widths(20, 0, 0), [(0, 6), (7, 6)]);
1748
1749            // with selection, more than needed width
1750            let table = Table::default().widths([Percentage(30), Percentage(30)]);
1751            assert_eq!(table.get_column_widths(20, 3, 0), [(3, 5), (9, 5)]);
1752
1753            // without selection, less than needed width
1754            // rounds from positions: [0.0, 0.0, 2.1, 3.1, 5.2, 7.0]
1755            let table = Table::default().widths([Percentage(30), Percentage(30)]);
1756            assert_eq!(table.get_column_widths(7, 0, 0), [(0, 2), (3, 2)]);
1757
1758            // with selection, less than needed width
1759            // rounds from positions: [0.0, 3.0, 5.1, 6.1, 7.0, 7.0]
1760            let table = Table::default().widths([Percentage(30), Percentage(30)]);
1761            assert_eq!(table.get_column_widths(7, 3, 0), [(3, 1), (5, 1)]);
1762        }
1763
1764        #[test]
1765        fn ratio_constraint() {
1766            // without selection, more than needed width
1767            // rounds from positions: [0.00, 0.00, 6.67, 7.67, 14.33]
1768            let table = Table::default().widths([Ratio(1, 3), Ratio(1, 3)]);
1769            assert_eq!(table.get_column_widths(20, 0, 0), [(0, 7), (8, 6)]);
1770
1771            // with selection, more than needed width
1772            // rounds from positions: [0.00, 3.00, 10.67, 17.33, 20.00]
1773            let table = Table::default().widths([Ratio(1, 3), Ratio(1, 3)]);
1774            assert_eq!(table.get_column_widths(20, 3, 0), [(3, 6), (10, 5)]);
1775
1776            // without selection, less than needed width
1777            // rounds from positions: [0.00, 2.33, 3.33, 5.66, 7.00]
1778            let table = Table::default().widths([Ratio(1, 3), Ratio(1, 3)]);
1779            assert_eq!(table.get_column_widths(7, 0, 0), [(0, 2), (3, 3)]);
1780
1781            // with selection, less than needed width
1782            // rounds from positions: [0.00, 3.00, 5.33, 6.33, 7.00, 7.00]
1783            let table = Table::default().widths([Ratio(1, 3), Ratio(1, 3)]);
1784            assert_eq!(table.get_column_widths(7, 3, 0), [(3, 1), (5, 2)]);
1785        }
1786
1787        /// When more width is available than requested, the behavior is controlled by flex
1788        #[test]
1789        fn underconstrained_flex() {
1790            let table = Table::default().widths([Min(10), Min(10), Min(1)]);
1791            assert_eq!(
1792                table.get_column_widths(62, 0, 0),
1793                &[(0, 20), (21, 20), (42, 20)]
1794            );
1795
1796            let table = Table::default()
1797                .widths([Min(10), Min(10), Min(1)])
1798                .flex(Flex::Legacy);
1799            assert_eq!(
1800                table.get_column_widths(62, 0, 0),
1801                &[(0, 10), (11, 10), (22, 40)]
1802            );
1803
1804            let table = Table::default()
1805                .widths([Min(10), Min(10), Min(1)])
1806                .flex(Flex::SpaceBetween);
1807            assert_eq!(
1808                table.get_column_widths(62, 0, 0),
1809                &[(0, 20), (21, 20), (42, 20)]
1810            );
1811        }
1812
1813        #[test]
1814        fn underconstrained_segment_size() {
1815            let table = Table::default().widths([Min(10), Min(10), Min(1)]);
1816            assert_eq!(
1817                table.get_column_widths(62, 0, 0),
1818                &[(0, 20), (21, 20), (42, 20)]
1819            );
1820
1821            let table = Table::default()
1822                .widths([Min(10), Min(10), Min(1)])
1823                .flex(Flex::Legacy);
1824            assert_eq!(
1825                table.get_column_widths(62, 0, 0),
1826                &[(0, 10), (11, 10), (22, 40)]
1827            );
1828        }
1829
1830        #[test]
1831        fn no_constraint_with_rows() {
1832            let table = Table::default()
1833                .rows(vec![
1834                    Row::new(vec!["a", "b"]),
1835                    Row::new(vec!["c", "d", "e"]),
1836                ])
1837                // rows should get precedence over header
1838                .header(Row::new(vec!["f", "g"]))
1839                .footer(Row::new(vec!["h", "i"]))
1840                .column_spacing(0);
1841            assert_eq!(
1842                table.get_column_widths(30, 0, 3),
1843                &[(0, 10), (10, 10), (20, 10)]
1844            );
1845        }
1846
1847        #[test]
1848        fn no_constraint_with_header() {
1849            let table = Table::default()
1850                .rows(vec![])
1851                .header(Row::new(vec!["f", "g"]))
1852                .column_spacing(0);
1853            assert_eq!(table.get_column_widths(10, 0, 2), [(0, 5), (5, 5)]);
1854        }
1855
1856        #[test]
1857        fn no_constraint_with_footer() {
1858            let table = Table::default()
1859                .rows(vec![])
1860                .footer(Row::new(vec!["h", "i"]))
1861                .column_spacing(0);
1862            assert_eq!(table.get_column_widths(10, 0, 2), [(0, 5), (5, 5)]);
1863        }
1864
1865        #[track_caller]
1866        fn test_table_with_selection<'line, Lines>(
1867            highlight_spacing: HighlightSpacing,
1868            columns: u16,
1869            spacing: u16,
1870            selection: Option<usize>,
1871            expected: Lines,
1872        ) where
1873            Lines: IntoIterator,
1874            Lines::Item: Into<Line<'line>>,
1875        {
1876            let table = Table::default()
1877                .rows(vec![Row::new(vec!["ABCDE", "12345"])])
1878                .highlight_spacing(highlight_spacing)
1879                .highlight_symbol(">>>")
1880                .column_spacing(spacing);
1881            let area = Rect::new(0, 0, columns, 3);
1882            let mut buf = Buffer::empty(area);
1883            let mut state = TableState::default().with_selected(selection);
1884            StatefulWidget::render(table, area, &mut buf, &mut state);
1885            assert_eq!(buf, Buffer::with_lines(expected));
1886        }
1887
1888        #[test]
1889        fn excess_area_highlight_symbol_and_column_spacing_allocation() {
1890            // no highlight_symbol rendered ever
1891            test_table_with_selection(
1892                HighlightSpacing::Never,
1893                15,   // width
1894                0,    // spacing
1895                None, // selection
1896                [
1897                    "ABCDE  12345   ", /* default layout is Flex::Start but columns length
1898                                        * constraints are calculated as `max_area / n_columns`,
1899                                        * i.e. they are distributed amongst available space */
1900                    "               ", // row 2
1901                    "               ", // row 3
1902                ],
1903            );
1904
1905            let table = Table::default()
1906                .rows(vec![Row::new(vec!["ABCDE", "12345"])])
1907                .widths([5, 5])
1908                .column_spacing(0);
1909            let area = Rect::new(0, 0, 15, 3);
1910            let mut buf = Buffer::empty(area);
1911            Widget::render(table, area, &mut buf);
1912            let expected = Buffer::with_lines([
1913                "ABCDE12345     ", /* As reference, this is what happens when you manually
1914                                    * specify widths */
1915                "               ", // row 2
1916                "               ", // row 3
1917            ]);
1918            assert_eq!(buf, expected);
1919
1920            // no highlight_symbol rendered ever
1921            test_table_with_selection(
1922                HighlightSpacing::Never,
1923                15,      // width
1924                0,       // spacing
1925                Some(0), // selection
1926                [
1927                    "ABCDE  12345   ", // row 1
1928                    "               ", // row 2
1929                    "               ", // row 3
1930                ],
1931            );
1932
1933            // no highlight_symbol rendered because no selection is made
1934            test_table_with_selection(
1935                HighlightSpacing::WhenSelected,
1936                15,   // width
1937                0,    // spacing
1938                None, // selection
1939                [
1940                    "ABCDE  12345   ", // row 1
1941                    "               ", // row 2
1942                    "               ", // row 3
1943                ],
1944            );
1945            // highlight_symbol rendered because selection is made
1946            test_table_with_selection(
1947                HighlightSpacing::WhenSelected,
1948                15,      // width
1949                0,       // spacing
1950                Some(0), // selection
1951                [
1952                    ">>>ABCDE 12345 ", // row 1
1953                    "               ", // row 2
1954                    "               ", // row 3
1955                ],
1956            );
1957
1958            // highlight_symbol always rendered even no selection is made
1959            test_table_with_selection(
1960                HighlightSpacing::Always,
1961                15,   // width
1962                0,    // spacing
1963                None, // selection
1964                [
1965                    "   ABCDE 12345 ", // row 1
1966                    "               ", // row 2
1967                    "               ", // row 3
1968                ],
1969            );
1970
1971            // no highlight_symbol rendered because no selection is made
1972            test_table_with_selection(
1973                HighlightSpacing::Always,
1974                15,      // width
1975                0,       // spacing
1976                Some(0), // selection
1977                [
1978                    ">>>ABCDE 12345 ", // row 1
1979                    "               ", // row 2
1980                    "               ", // row 3
1981                ],
1982            );
1983        }
1984
1985        #[expect(clippy::too_many_lines)]
1986        #[test]
1987        fn insufficient_area_highlight_symbol_and_column_spacing_allocation() {
1988            // column spacing is prioritized over every other constraint
1989            test_table_with_selection(
1990                HighlightSpacing::Never,
1991                10,   // width
1992                1,    // spacing
1993                None, // selection
1994                [
1995                    "ABCDE 1234", // spacing is prioritized and column is cut
1996                    "          ", // row 2
1997                    "          ", // row 3
1998                ],
1999            );
2000            test_table_with_selection(
2001                HighlightSpacing::WhenSelected,
2002                10,   // width
2003                1,    // spacing
2004                None, // selection
2005                [
2006                    "ABCDE 1234", // spacing is prioritized and column is cut
2007                    "          ", // row 2
2008                    "          ", // row 3
2009                ],
2010            );
2011
2012            // this test checks that space for highlight_symbol space is always allocated.
2013            // this test also checks that space for column is allocated.
2014            //
2015            // Space for highlight_symbol is allocated first by splitting horizontal space
2016            // into highlight_symbol area and column area.
2017            // Then in a separate step, column widths are calculated.
2018            // column spacing is prioritized when column widths are calculated and last column here
2019            // ends up with just 1 wide
2020            test_table_with_selection(
2021                HighlightSpacing::Always,
2022                10,   // width
2023                1,    // spacing
2024                None, // selection
2025                [
2026                    "   ABC 123", // highlight_symbol and spacing are prioritized
2027                    "          ", // row 2
2028                    "          ", // row 3
2029                ],
2030            );
2031
2032            // the following are specification tests
2033            test_table_with_selection(
2034                HighlightSpacing::Always,
2035                9,    // width
2036                1,    // spacing
2037                None, // selection
2038                [
2039                    "   ABC 12", // highlight_symbol and spacing are prioritized
2040                    "         ", // row 2
2041                    "         ", // row 3
2042                ],
2043            );
2044            test_table_with_selection(
2045                HighlightSpacing::Always,
2046                8,    // width
2047                1,    // spacing
2048                None, // selection
2049                [
2050                    "   AB 12", // highlight_symbol and spacing are prioritized
2051                    "        ", // row 2
2052                    "        ", // row 3
2053                ],
2054            );
2055            test_table_with_selection(
2056                HighlightSpacing::Always,
2057                7,    // width
2058                1,    // spacing
2059                None, // selection
2060                [
2061                    "   AB 1", // highlight_symbol and spacing are prioritized
2062                    "       ", // row 2
2063                    "       ", // row 3
2064                ],
2065            );
2066
2067            let table = Table::default()
2068                .rows(vec![Row::new(vec!["ABCDE", "12345"])])
2069                .highlight_spacing(HighlightSpacing::Always)
2070                .flex(Flex::Legacy)
2071                .highlight_symbol(">>>")
2072                .column_spacing(1);
2073            let area = Rect::new(0, 0, 10, 3);
2074            let mut buf = Buffer::empty(area);
2075            Widget::render(table, area, &mut buf);
2076            // highlight_symbol and spacing are prioritized but columns are evenly distributed
2077            #[rustfmt::skip]
2078            let expected = Buffer::with_lines([
2079                "   ABCDE 1",
2080                "          ",
2081                "          ",
2082            ]);
2083            assert_eq!(buf, expected);
2084
2085            let table = Table::default()
2086                .rows(vec![Row::new(vec!["ABCDE", "12345"])])
2087                .highlight_spacing(HighlightSpacing::Always)
2088                .flex(Flex::Start)
2089                .highlight_symbol(">>>")
2090                .column_spacing(1);
2091            let area = Rect::new(0, 0, 10, 3);
2092            let mut buf = Buffer::empty(area);
2093            Widget::render(table, area, &mut buf);
2094            // highlight_symbol and spacing are prioritized but columns are evenly distributed
2095            #[rustfmt::skip]
2096            let expected = Buffer::with_lines([
2097                "   ABC 123",
2098                "          ",
2099                "          ",
2100            ]);
2101            assert_eq!(buf, expected);
2102
2103            test_table_with_selection(
2104                HighlightSpacing::Never,
2105                10,      // width
2106                1,       // spacing
2107                Some(0), // selection
2108                [
2109                    "ABCDE 1234", // spacing is prioritized
2110                    "          ",
2111                    "          ",
2112                ],
2113            );
2114
2115            test_table_with_selection(
2116                HighlightSpacing::WhenSelected,
2117                10,      // width
2118                1,       // spacing
2119                Some(0), // selection
2120                [
2121                    ">>>ABC 123", // row 1
2122                    "          ", // row 2
2123                    "          ", // row 3
2124                ],
2125            );
2126
2127            test_table_with_selection(
2128                HighlightSpacing::Always,
2129                10,      // width
2130                1,       // spacing
2131                Some(0), // selection
2132                [
2133                    ">>>ABC 123", // highlight column and spacing are prioritized
2134                    "          ", // row 2
2135                    "          ", // row 3
2136                ],
2137            );
2138        }
2139
2140        #[test]
2141        fn insufficient_area_highlight_symbol_allocation_with_no_column_spacing() {
2142            test_table_with_selection(
2143                HighlightSpacing::Never,
2144                10,   // width
2145                0,    // spacing
2146                None, // selection
2147                [
2148                    "ABCDE12345", // row 1
2149                    "          ", // row 2
2150                    "          ", // row 3
2151                ],
2152            );
2153            test_table_with_selection(
2154                HighlightSpacing::WhenSelected,
2155                10,   // width
2156                0,    // spacing
2157                None, // selection
2158                [
2159                    "ABCDE12345", // row 1
2160                    "          ", // row 2
2161                    "          ", // row 3
2162                ],
2163            );
2164            // highlight symbol spacing is prioritized over all constraints
2165            // even if the constraints are fixed length
2166            // this is because highlight_symbol column is separated _before_ any of the constraint
2167            // widths are calculated
2168            test_table_with_selection(
2169                HighlightSpacing::Always,
2170                10,   // width
2171                0,    // spacing
2172                None, // selection
2173                [
2174                    "   ABCD123", // highlight column and spacing are prioritized
2175                    "          ", // row 2
2176                    "          ", // row 3
2177                ],
2178            );
2179            test_table_with_selection(
2180                HighlightSpacing::Never,
2181                10,      // width
2182                0,       // spacing
2183                Some(0), // selection
2184                [
2185                    "ABCDE12345", // row 1
2186                    "          ", // row 2
2187                    "          ", // row 3
2188                ],
2189            );
2190            test_table_with_selection(
2191                HighlightSpacing::WhenSelected,
2192                10,      // width
2193                0,       // spacing
2194                Some(0), // selection
2195                [
2196                    ">>>ABCD123", // highlight column and spacing are prioritized
2197                    "          ", // row 2
2198                    "          ", // row 3
2199                ],
2200            );
2201            test_table_with_selection(
2202                HighlightSpacing::Always,
2203                10,      // width
2204                0,       // spacing
2205                Some(0), // selection
2206                [
2207                    ">>>ABCD123", // highlight column and spacing are prioritized
2208                    "          ", // row 2
2209                    "          ", // row 3
2210                ],
2211            );
2212        }
2213    }
2214
2215    #[test]
2216    fn stylize() {
2217        assert_eq!(
2218            Table::new(vec![Row::new(vec![Cell::from("")])], [Percentage(100)])
2219                .black()
2220                .on_white()
2221                .bold()
2222                .not_crossed_out()
2223                .style,
2224            Style::default()
2225                .fg(Color::Black)
2226                .bg(Color::White)
2227                .add_modifier(Modifier::BOLD)
2228                .remove_modifier(Modifier::CROSSED_OUT)
2229        );
2230    }
2231
2232    #[rstest]
2233    #[case::no_columns(vec![], vec![], vec![], 0)]
2234    #[case::only_header(vec!["H1", "H2"], vec![], vec![], 2)]
2235    #[case::only_rows(
2236        vec![],
2237        vec![vec!["C1", "C2"], vec!["C1", "C2", "C3"]],
2238        vec![],
2239        3
2240    )]
2241    #[case::only_footer(vec![], vec![], vec!["F1", "F2", "F3", "F4"], 4)]
2242    #[case::rows_longer(
2243        vec!["H1", "H2", "H3", "H4"],
2244        vec![vec!["C1", "C2"],vec!["C1", "C2", "C3"]],
2245        vec!["F1", "F2"],
2246        4
2247    )]
2248    #[case::rows_longer(
2249        vec!["H1", "H2"],
2250        vec![vec!["C1", "C2"], vec!["C1", "C2", "C3", "C4"]],
2251        vec!["F1", "F2"],
2252        4
2253    )]
2254    #[case::footer_longer(
2255        vec!["H1", "H2"],
2256        vec![vec!["C1", "C2"], vec!["C1", "C2", "C3"]],
2257        vec!["F1", "F2", "F3", "F4"],
2258        4
2259    )]
2260
2261    fn column_count(
2262        #[case] header: Vec<&str>,
2263        #[case] rows: Vec<Vec<&str>>,
2264        #[case] footer: Vec<&str>,
2265        #[case] expected: usize,
2266    ) {
2267        let header = Row::new(header);
2268        let footer = Row::new(footer);
2269        let rows: Vec<Row> = rows.into_iter().map(Row::new).collect();
2270        let table = Table::new(rows, Vec::<Constraint>::new())
2271            .header(header)
2272            .footer(footer);
2273        let column_count = table.column_count();
2274        assert_eq!(column_count, expected);
2275    }
2276
2277    #[test]
2278    fn render_in_minimal_buffer() {
2279        let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
2280        let rows = vec![
2281            Row::new(vec!["Cell1", "Cell2", "Cell3"]),
2282            Row::new(vec!["Cell4", "Cell5", "Cell6"]),
2283        ];
2284        let table = Table::new(rows, [Constraint::Length(10); 3])
2285            .header(Row::new(vec!["Header1", "Header2", "Header3"]))
2286            .footer(Row::new(vec!["Footer1", "Footer2", "Footer3"]));
2287        // This should not panic, even if the buffer is too small to render the table.
2288        Widget::render(table, buffer.area, &mut buffer);
2289        assert_eq!(buffer, Buffer::with_lines([" "]));
2290    }
2291
2292    #[test]
2293    fn render_in_zero_size_buffer() {
2294        let mut buffer = Buffer::empty(Rect::ZERO);
2295        let rows = vec![
2296            Row::new(vec!["Cell1", "Cell2", "Cell3"]),
2297            Row::new(vec!["Cell4", "Cell5", "Cell6"]),
2298        ];
2299        let table = Table::new(rows, [Constraint::Length(10); 3])
2300            .header(Row::new(vec!["Header1", "Header2", "Header3"]))
2301            .footer(Row::new(vec!["Footer1", "Footer2", "Footer3"]));
2302        // This should not panic, even if the buffer has zero size.
2303        Widget::render(table, buffer.area, &mut buffer);
2304    }
2305}