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