Files
ratatui/src/widgets/table/table.rs
2024-05-13 23:26:47 -04:00

2168 lines
81 KiB
Rust

use itertools::Itertools;
use super::*;
use crate::{layout::Flex, prelude::*, widgets::Block};
/// A widget to display data in formatted columns.
///
/// A `Table` is a collection of [`Row`]s, each composed of [`Cell`]s:
///
/// You can construct a [`Table`] using either [`Table::new`] or [`Table::default`] and then chain
/// builder style methods to set the desired properties.
///
/// Table cells can be aligned, for more details see [`Cell`].
///
/// Make sure to call the [`Table::widths`] method, otherwise the columns will all have a width of 0
/// and thus not be visible.
///
/// [`Table`] implements [`Widget`] and so it can be drawn using [`Frame::render_widget`].
///
/// [`Table`] is also a [`StatefulWidget`], which means you can use it with [`TableState`] to allow
/// the user to scroll through the rows and select one of them.
///
/// Note: if the `widths` field is empty, the table will be rendered with equal widths.
///
/// See the table example and the recipe and traceroute tabs in the demo2 example in the [Examples]
/// directory for a more in depth example of the various configuration options and for how to handle
/// state.
///
/// [Examples]: https://github.com/ratatui-org/ratatui/blob/master/examples/README.md
///
/// # Constructor methods
///
/// - [`Table::new`] creates a new [`Table`] with the given rows.
/// - [`Table::default`] creates an empty [`Table`]. You can then add rows using [`Table::rows`].
///
/// # Setter methods
///
/// These methods are fluent setters. They return a new `Table` with the specified property set.
///
/// - [`Table::rows`] sets the rows of the [`Table`].
/// - [`Table::header`] sets the header row of the [`Table`].
/// - [`Table::footer`] sets the footer row of the [`Table`].
/// - [`Table::widths`] sets the width constraints of each column.
/// - [`Table::column_spacing`] sets the spacing between each column.
/// - [`Table::block`] wraps the table in a [`Block`] widget.
/// - [`Table::style`] sets the base style of the widget.
/// - [`Table::highlight_style`] sets the style of the selected row.
/// - [`Table::highlight_symbol`] sets the symbol to be displayed in front of the selected row.
/// - [`Table::highlight_spacing`] sets when to show the highlight spacing.
///
/// # Example
///
/// ```rust
/// use ratatui::{prelude::*, widgets::*};
///
/// let rows = [Row::new(vec!["Cell1", "Cell2", "Cell3"])];
/// // Columns widths are constrained in the same way as Layout...
/// let widths = [
/// Constraint::Length(5),
/// Constraint::Length(5),
/// Constraint::Length(10),
/// ];
/// let table = Table::new(rows, widths)
/// // ...and they can be separated by a fixed spacing.
/// .column_spacing(1)
/// // You can set the style of the entire Table.
/// .style(Style::new().blue())
/// // It has an optional header, which is simply a Row always visible at the top.
/// .header(
/// Row::new(vec!["Col1", "Col2", "Col3"])
/// .style(Style::new().bold())
/// // To add space between the header and the rest of the rows, specify the margin
/// .bottom_margin(1),
/// )
/// // It has an optional footer, which is simply a Row always visible at the bottom.
/// .footer(Row::new(vec!["Updated on Dec 28"]))
/// // As any other widget, a Table can be wrapped in a Block.
/// .block(Block::new().title("Table"))
/// // The selected row and its content can also be styled.
/// .highlight_style(Style::new().reversed())
/// // ...and potentially show a symbol in front of the selection.
/// .highlight_symbol(">>");
/// ```
///
/// Rows can be created from an iterator of [`Cell`]s. Each row can have an associated height,
/// bottom margin, and style. See [`Row`] for more details.
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// // a Row can be created from simple strings.
/// let row = Row::new(vec!["Row11", "Row12", "Row13"]);
///
/// // You can style the entire row.
/// let row = Row::new(vec!["Row21", "Row22", "Row23"]).style(Style::new().red());
///
/// // If you need more control over the styling, create Cells directly
/// let row = Row::new(vec![
/// Cell::from("Row31"),
/// Cell::from("Row32").style(Style::default().fg(Color::Yellow)),
/// Cell::from(Line::from(vec![
/// Span::raw("Row"),
/// Span::styled("33", Style::default().fg(Color::Green)),
/// ])),
/// ]);
///
/// // If a Row need to display some content over multiple lines, specify the height.
/// let row = Row::new(vec![
/// Cell::from("Row\n41"),
/// Cell::from("Row\n42"),
/// Cell::from("Row\n43"),
/// ])
/// .height(2);
/// ```
///
/// Cells can be created from anything that can be converted to [`Text`]. See [`Cell`] for more
/// details.
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// Cell::from("simple string");
/// Cell::from("simple styled span".red());
/// Cell::from(Span::raw("raw span"));
/// Cell::from(Span::styled("styled span", Style::new().red()));
/// Cell::from(Line::from(vec![
/// Span::raw("a vec of "),
/// Span::styled("spans", Style::new().bold()),
/// ]));
/// Cell::from(Text::from("text"));
/// ```
///
/// Just as rows can be collected from iterators of `Cell`s, tables can be collected from iterators
/// of `Row`s. This will create a table with column widths evenly dividing the space available.
/// These default columns widths can be overridden using the `Table::widths` method.
///
/// ```rust
/// use ratatui::{prelude::*, widgets::*};
///
/// let text = "Mary had a\nlittle lamb.";
///
/// let table = text
/// .split("\n")
/// .map(|line: &str| -> Row { line.split_ascii_whitespace().collect() })
/// .collect::<Table>()
/// .widths([Constraint::Length(10); 3]);
/// ```
///
/// `Table` also implements the [`Styled`] trait, which means you can use style shorthands from
/// the [`Stylize`] trait to set the style of the widget more concisely.
///
/// ```rust
/// use ratatui::{prelude::*, widgets::*};
///
/// let rows = [Row::new(vec!["Cell1", "Cell2", "Cell3"])];
/// let widths = [
/// Constraint::Length(5),
/// Constraint::Length(5),
/// Constraint::Length(10),
/// ];
/// let table = Table::new(rows, widths).red().italic();
/// ```
///
/// # Stateful example
///
/// `Table` is a [`StatefulWidget`], which means you can use it with [`TableState`] to allow the
/// user to scroll through the rows and select one of them.
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # fn ui(frame: &mut Frame) {
/// # let area = Rect::default();
/// // Note: TableState should be stored in your application state (not constructed in your render
/// // method) so that the selected row is preserved across renders
/// let mut table_state = TableState::default();
/// let rows = [
/// Row::new(vec!["Row11", "Row12", "Row13"]),
/// Row::new(vec!["Row21", "Row22", "Row23"]),
/// Row::new(vec!["Row31", "Row32", "Row33"]),
/// ];
/// let widths = [Constraint::Length(5), Constraint::Length(5), Constraint::Length(10)];
/// let table = Table::new(rows, widths)
/// .block(Block::new().title("Table"))
/// .highlight_style(Style::new().add_modifier(Modifier::REVERSED))
/// .highlight_symbol(">>");
///
/// frame.render_stateful_widget(table, area, &mut table_state);
/// # }
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Table<'a> {
/// Data to display in each row
rows: Vec<Row<'a>>,
/// Optional header
header: Option<Row<'a>>,
/// Optional footer
footer: Option<Row<'a>>,
/// Width constraints for each column
widths: Vec<Constraint>,
/// Space between each column
column_spacing: u16,
/// A block to wrap the widget in
block: Option<Block<'a>>,
/// Base style for the widget
style: Style,
/// Style used to render the selected row
highlight_style: Style,
/// Symbol in front of the selected row
highlight_symbol: Text<'a>,
/// Symbol in front of the marked row
mark_symbol: Text<'a>,
/// Symbol in front of the unmarked row
unmark_symbol: Text<'a>,
/// Symbol in front of the marked and selected row
mark_highlight_symbol: Text<'a>,
/// Decides when to allocate spacing for the row selection
highlight_spacing: HighlightSpacing,
/// Controls how to distribute extra space among the columns
flex: Flex,
}
impl<'a> Default for Table<'a> {
fn default() -> Self {
Self {
rows: Vec::new(),
header: None,
footer: None,
widths: Vec::new(),
column_spacing: 1,
block: None,
style: Style::new(),
highlight_style: Style::new(),
highlight_symbol: Text::default(),
mark_symbol: Text::default(),
unmark_symbol: Text::default(),
mark_highlight_symbol: Text::default(),
highlight_spacing: HighlightSpacing::default(),
flex: Flex::Start,
}
}
}
impl<'a> Table<'a> {
/// Creates a new [`Table`] widget with the given rows.
///
/// The `rows` parameter accepts any value that can be converted into an iterator of [`Row`]s.
/// This includes arrays, slices, and [`Vec`]s.
///
/// The `widths` parameter accepts any type that implements `IntoIterator<Item =
/// Into<Constraint>>`. This includes arrays, slices, vectors, iterators. `Into<Constraint>` is
/// implemented on u16, so you can pass an array, vec, etc. of u16 to this function to create a
/// table with fixed width columns.
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let rows = [
/// Row::new(vec!["Cell1", "Cell2"]),
/// Row::new(vec!["Cell3", "Cell4"]),
/// ];
/// let widths = [Constraint::Length(5), Constraint::Length(5)];
/// let table = Table::new(rows, widths);
/// ```
pub fn new<R, C>(rows: R, widths: C) -> Self
where
R: IntoIterator,
R::Item: Into<Row<'a>>,
C: IntoIterator,
C::Item: Into<Constraint>,
{
let widths = widths.into_iter().map(Into::into).collect_vec();
ensure_percentages_less_than_100(&widths);
let rows = rows.into_iter().map(Into::into).collect();
Self {
rows,
widths,
..Default::default()
}
}
/// Set the rows
///
/// The `rows` parameter accepts any value that can be converted into an iterator of [`Row`]s.
/// This includes arrays, slices, and [`Vec`]s.
///
/// # Warning
///
/// This method does not currently set the column widths. You will need to set them manually by
/// calling [`Table::widths`].
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let rows = [
/// Row::new(vec!["Cell1", "Cell2"]),
/// Row::new(vec!["Cell3", "Cell4"]),
/// ];
/// let table = Table::default().rows(rows);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn rows<T>(mut self, rows: T) -> Self
where
T: IntoIterator<Item = Row<'a>>,
{
self.rows = rows.into_iter().collect();
self
}
/// Sets the header row
///
/// The `header` parameter is a [`Row`] which will be displayed at the top of the [`Table`]
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let header = Row::new(vec![
/// Cell::from("Header Cell 1"),
/// Cell::from("Header Cell 2"),
/// ]);
/// let table = Table::default().header(header);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn header(mut self, header: Row<'a>) -> Self {
self.header = Some(header);
self
}
/// Sets the footer row
///
/// The `footer` parameter is a [`Row`] which will be displayed at the bottom of the [`Table`]
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let footer = Row::new(vec![
/// Cell::from("Footer Cell 1"),
/// Cell::from("Footer Cell 2"),
/// ]);
/// let table = Table::default().footer(footer);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn footer(mut self, footer: Row<'a>) -> Self {
self.footer = Some(footer);
self
}
/// Set the widths of the columns.
///
/// The `widths` parameter accepts any type that implements `IntoIterator<Item =
/// Into<Constraint>>`. This includes arrays, slices, vectors, iterators. `Into<Constraint>` is
/// implemented on u16, so you can pass an array, vec, etc. of u16 to this function to create a
/// table with fixed width columns.
///
/// If the widths are empty, the table will be rendered with equal widths.
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let table = Table::default().widths([Constraint::Length(5), Constraint::Length(5)]);
/// let table = Table::default().widths(vec![Constraint::Length(5); 2]);
///
/// // widths could also be computed at runtime
/// let widths = [10, 10, 20].into_iter().map(|c| Constraint::Length(c));
/// let table = Table::default().widths(widths);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn widths<I>(mut self, widths: I) -> Self
where
I: IntoIterator,
I::Item: Into<Constraint>,
{
let widths = widths.into_iter().map(Into::into).collect_vec();
ensure_percentages_less_than_100(&widths);
self.widths = widths;
self
}
/// Set the spacing between columns
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let rows = [Row::new(vec!["Cell1", "Cell2"])];
/// # let widths = [Constraint::Length(5), Constraint::Length(5)];
/// let table = Table::new(rows, widths).column_spacing(1);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn column_spacing(mut self, spacing: u16) -> Self {
self.column_spacing = spacing;
self
}
/// Wraps the table with a custom [`Block`] widget.
///
/// The `block` parameter is of type [`Block`]. This holds the specified block to be
/// created around the [`Table`]
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let rows = [Row::new(vec!["Cell1", "Cell2"])];
/// # let widths = [Constraint::Length(5), Constraint::Length(5)];
/// let block = Block::bordered().title("Table");
/// let table = Table::new(rows, widths).block(block);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self
}
/// Sets the base style of the widget
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// All text rendered by the widget will use this style, unless overridden by [`Block::style`],
/// [`Row::style`], [`Cell::style`], or the styles of cell's content.
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let rows = [Row::new(vec!["Cell1", "Cell2"])];
/// # let widths = [Constraint::Length(5), Constraint::Length(5)];
/// let table = Table::new(rows, widths).style(Style::new().red().italic());
/// ```
///
/// `Table` also implements the [`Styled`] trait, which means you can use style shorthands from
/// the [`Stylize`] trait to set the style of the widget more concisely.
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let rows = [Row::new(vec!["Cell1", "Cell2"])];
/// # let widths = vec![Constraint::Length(5), Constraint::Length(5)];
/// let table = Table::new(rows, widths).red().italic();
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
self
}
/// Set the style of the selected row
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// This style will be applied to the entire row, including the selection symbol if it is
/// displayed, and will override any style set on the row or on the individual cells.
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let rows = [Row::new(vec!["Cell1", "Cell2"])];
/// # let widths = [Constraint::Length(5), Constraint::Length(5)];
/// let table = Table::new(rows, widths).highlight_style(Style::new().red().italic());
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn highlight_style<S: Into<Style>>(mut self, highlight_style: S) -> Self {
self.highlight_style = highlight_style.into();
self
}
/// Set the symbol to be displayed in front of the selected row
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let rows = [Row::new(vec!["Cell1", "Cell2"])];
/// # let widths = [Constraint::Length(5), Constraint::Length(5)];
/// let table = Table::new(rows, widths).highlight_symbol(">>");
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn highlight_symbol<T: Into<Text<'a>>>(mut self, highlight_symbol: T) -> Self {
self.highlight_symbol = highlight_symbol.into();
self
}
/// Set the symbol to be displayed in front of the marked row
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let rows = [Row::new(vec!["Cell1", "Cell2"])];
/// # let widths = [Constraint::Length(5), Constraint::Length(5)];
/// let table = Table::new(rows, widths).mark_symbol("\u{2714}");
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn mark_symbol<T: Into<Text<'a>>>(mut self, mark_symbol: T) -> Self {
self.mark_symbol = mark_symbol.into();
self
}
/// Set the symbol to be displayed in front of the unmarked row
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let rows = [Row::new(vec!["Cell1", "Cell2"])];
/// # let widths = [Constraint::Length(5), Constraint::Length(5)];
/// let table = Table::new(rows, widths).unmark_symbol(" ");
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn unmark_symbol<T: Into<Text<'a>>>(mut self, unmark_symbol: T) -> Self {
self.unmark_symbol = unmark_symbol.into();
self
}
/// Set the symbol to be displayed in front of the marked and selected row
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let rows = [Row::new(vec!["Cell1", "Cell2"])];
/// # let widths = [Constraint::Length(5), Constraint::Length(5)];
/// let table = Table::new(rows, widths).mark_highlight_symbol("\u{29bf}");
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn mark_highlight_symbol<T: Into<Text<'a>>>(mut self, mark_highlight_symbol: T) -> Self {
self.mark_highlight_symbol = mark_highlight_symbol.into();
self
}
/// Set when to show the highlight spacing
///
/// The highlight spacing is the spacing that is allocated for the selection symbol column (if
/// enabled) and is used to shift the table when a row is selected. This method allows you to
/// configure when this spacing is allocated.
///
/// - [`HighlightSpacing::Always`] will always allocate the spacing, regardless of whether a row
/// is selected or not. This means that the table will never change size, regardless of if a
/// row is selected or not.
/// - [`HighlightSpacing::WhenSelected`] will only allocate the spacing if a row is selected.
/// This means that the table will shift when a row is selected. This is the default setting
/// for backwards compatibility, but it is recommended to use `HighlightSpacing::Always` for a
/// better user experience.
/// - [`HighlightSpacing::Never`] will never allocate the spacing, regardless of whether a row
/// is selected or not. This means that the highlight symbol will never be drawn.
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let rows = [Row::new(vec!["Cell1", "Cell2"])];
/// # let widths = [Constraint::Length(5), Constraint::Length(5)];
/// let table = Table::new(rows, widths).highlight_spacing(HighlightSpacing::Always);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn highlight_spacing(mut self, value: HighlightSpacing) -> Self {
self.highlight_spacing = value;
self
}
/// Set how extra space is distributed amongst columns.
///
/// This determines how the space is distributed when the constraints are satisfied. By default,
/// the extra space is not distributed at all. But this can be changed to distribute all extra
/// space to the last column or to distribute it equally.
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// Create a table that needs at least 30 columns to display. Any extra space will be assigned
/// to the last column.
/// ```
/// # use ratatui::layout::{Constraint, Flex};
/// # use ratatui::widgets::{Table, Row};
/// let widths = [
/// Constraint::Min(10),
/// Constraint::Min(10),
/// Constraint::Min(10),
/// ];
/// let table = Table::new(Vec::<Row>::new(), widths).flex(Flex::Legacy);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn flex(mut self, flex: Flex) -> Self {
self.flex = flex;
self
}
}
impl Widget for Table<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
WidgetRef::render_ref(&self, area, buf);
}
}
impl WidgetRef for Table<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
let mut state = TableState::default();
StatefulWidget::render(self, area, buf, &mut state);
}
}
impl StatefulWidget for Table<'_> {
type State = TableState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
StatefulWidget::render(&self, area, buf, state);
}
}
// Note: remove this when StatefulWidgetRef is stabilized and replace with the blanket impl
impl StatefulWidget for &Table<'_> {
type State = TableState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
StatefulWidgetRef::render_ref(self, area, buf, state);
}
}
impl StatefulWidgetRef for Table<'_> {
type State = TableState;
fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
buf.set_style(area, self.style);
self.block.render_ref(area, buf);
let table_area = self.block.inner_if_some(area);
if table_area.is_empty() {
return;
}
let highlight_column_width = self.highlight_column_width(state);
let columns_widths = self.get_columns_widths(table_area.width, highlight_column_width);
let (header_area, rows_area, footer_area) = self.layout(table_area);
self.render_header(header_area, buf, &columns_widths);
self.render_rows(
rows_area,
buf,
state,
highlight_column_width,
(
&self.highlight_symbol,
&self.mark_symbol,
&self.unmark_symbol,
&self.mark_highlight_symbol,
),
&columns_widths,
);
self.render_footer(footer_area, buf, &columns_widths);
}
}
// private methods for rendering
impl Table<'_> {
/// Splits the table area into a header, rows area and a footer
fn layout(&self, area: Rect) -> (Rect, Rect, Rect) {
let header_top_margin = self.header.as_ref().map_or(0, |h| h.top_margin);
let header_height = self.header.as_ref().map_or(0, |h| h.height);
let header_bottom_margin = self.header.as_ref().map_or(0, |h| h.bottom_margin);
let footer_top_margin = self.footer.as_ref().map_or(0, |h| h.top_margin);
let footer_height = self.footer.as_ref().map_or(0, |f| f.height);
let footer_bottom_margin = self.footer.as_ref().map_or(0, |h| h.bottom_margin);
let layout = Layout::vertical([
Constraint::Length(header_top_margin),
Constraint::Length(header_height),
Constraint::Length(header_bottom_margin),
Constraint::Min(0),
Constraint::Length(footer_top_margin),
Constraint::Length(footer_height),
Constraint::Length(footer_bottom_margin),
])
.split(area);
let (header_area, rows_area, footer_area) = (layout[1], layout[3], layout[5]);
(header_area, rows_area, footer_area)
}
fn render_header(&self, area: Rect, buf: &mut Buffer, column_widths: &[(u16, u16)]) {
if let Some(ref header) = self.header {
buf.set_style(area, header.style);
for ((x, width), cell) in column_widths.iter().zip(header.cells.iter()) {
cell.render(Rect::new(area.x + x, area.y, *width, area.height), buf);
}
}
}
fn render_footer(&self, area: Rect, buf: &mut Buffer, column_widths: &[(u16, u16)]) {
if let Some(ref footer) = self.footer {
buf.set_style(area, footer.style);
for ((x, width), cell) in column_widths.iter().zip(footer.cells.iter()) {
cell.render(Rect::new(area.x + x, area.y, *width, area.height), buf);
}
}
}
fn render_rows(
&self,
area: Rect,
buf: &mut Buffer,
state: &mut TableState,
highlight_column_width: u16,
symbols: (&Text<'_>, &Text<'_>, &Text<'_>, &Text<'_>),
columns_widths: &[(u16, u16)],
) {
if self.rows.is_empty() {
return;
}
let (highlight_symbol, mark_symbol, unmark_symbol, mark_highlight_symbol) = symbols;
let (start_index, end_index) =
self.get_row_bounds(state.selected, state.offset, area.height);
state.offset = start_index;
let mut y_offset = 0;
for (i, row) in self
.rows
.iter()
.enumerate()
.skip(state.offset)
.take(end_index - start_index)
{
let row_area = Rect::new(
area.x,
area.y + y_offset + row.top_margin,
area.width,
row.height_with_margin() - row.top_margin,
);
buf.set_style(row_area, row.style);
let is_marked = state.marked().contains(&(i + state.offset));
let is_highlighted = state.selected().is_some_and(|index| index == i);
if highlight_column_width > 0 {
let area = Rect {
width: highlight_column_width,
..row_area
};
buf.set_style(area, row.style);
match (is_marked, is_highlighted) {
(true, true) => {
mark_highlight_symbol.render(area, buf);
}
(true, false) => {
mark_symbol.render(area, buf);
}
(false, true) => {
highlight_symbol.render(area, buf);
}
(false, false) => {
unmark_symbol.render(area, buf);
}
};
}
for ((x, width), cell) in columns_widths.iter().zip(row.cells.iter()) {
cell.render(
Rect::new(row_area.x + x, row_area.y, *width, row_area.height),
buf,
);
}
if is_highlighted {
buf.set_style(row_area, self.highlight_style);
}
y_offset += row.height_with_margin();
}
}
/// Get all offsets and widths of all user specified columns.
///
/// Returns (x, width). When self.widths is empty, it is assumed `.widths()` has not been called
/// and a default of equal widths is returned.
fn get_columns_widths(&self, max_width: u16, selection_width: u16) -> Vec<(u16, u16)> {
let widths = if self.widths.is_empty() {
let col_count = self
.rows
.iter()
.chain(self.header.iter())
.chain(self.footer.iter())
.map(|r| r.cells.len())
.max()
.unwrap_or(0);
// Divide the space between each column equally
vec![Constraint::Length(max_width / col_count.max(1) as u16); col_count]
} else {
self.widths.clone()
};
// this will always allocate a selection area
let [_selection_area, columns_area] =
Layout::horizontal([Constraint::Length(selection_width), Constraint::Fill(0)])
.areas(Rect::new(0, 0, max_width, 1));
let rects = Layout::horizontal(widths)
.flex(self.flex)
.spacing(self.column_spacing)
.split(columns_area);
rects.iter().map(|c| (c.x, c.width)).collect()
}
fn get_row_bounds(
&self,
selected: Option<usize>,
offset: usize,
max_height: u16,
) -> (usize, usize) {
let offset = offset.min(self.rows.len().saturating_sub(1));
let mut start = offset;
let mut end = offset;
let mut height = 0;
for item in self.rows.iter().skip(offset) {
if height + item.height > max_height {
break;
}
height += item.height_with_margin();
end += 1;
}
let selected = selected.unwrap_or(0).min(self.rows.len() - 1);
while selected >= end {
height = height.saturating_add(self.rows[end].height_with_margin());
end += 1;
while height > max_height {
height = height.saturating_sub(self.rows[start].height_with_margin());
start += 1;
}
}
while selected < start {
start -= 1;
height = height.saturating_add(self.rows[start].height_with_margin());
while height > max_height {
end -= 1;
height = height.saturating_sub(self.rows[end].height_with_margin());
}
}
(start, end)
}
/// Returns the width of the indicator column if a row is selected, rows are marked,
/// or the `highlight_spacing` is set to show the column always, otherwise 0.
fn highlight_column_width(&self, state: &TableState) -> u16 {
let has_highlight = state.selected().is_some() || state.marked().len() > 0;
let highlight_column_width = if self.highlight_spacing.should_add(has_highlight) {
self.highlight_symbol.width() as u16
} else {
0
};
let mark_column_width = if self.highlight_spacing.should_add(has_highlight) {
self.mark_symbol.width() as u16
} else {
0
};
let mark_highlight_column_width = if self.highlight_spacing.should_add(has_highlight) {
self.mark_highlight_symbol.width() as u16
} else {
0
};
let unmark_column_width = if self.highlight_spacing.should_add(has_highlight) {
self.unmark_symbol.width() as u16
} else {
0
};
highlight_column_width
.max(mark_column_width)
.max(mark_highlight_column_width)
.max(unmark_column_width)
}
}
fn ensure_percentages_less_than_100(widths: &[Constraint]) {
for w in widths {
if let Constraint::Percentage(p) = w {
assert!(
*p <= 100,
"Percentages should be between 0 and 100 inclusively."
);
}
}
}
impl<'a> Styled for Table<'a> {
type Item = Self;
fn style(&self) -> Style {
self.style
}
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
self.style(style)
}
}
impl<'a, Item> FromIterator<Item> for Table<'a>
where
Item: Into<Row<'a>>,
{
/// Collects an iterator of rows into a table.
///
/// When collecting from an iterator into a table, the user must provide the widths using
/// `Table::widths` after construction.
fn from_iter<Iter: IntoIterator<Item = Item>>(rows: Iter) -> Self {
let widths: [Constraint; 0] = [];
Self::new(rows, widths)
}
}
#[cfg(test)]
mod tests {
use std::vec;
use super::*;
use crate::{layout::Constraint::*, style::Style, text::Line};
#[test]
fn new() {
let rows = [Row::new(vec![Cell::from("")])];
let widths = [Constraint::Percentage(100)];
let table = Table::new(rows.clone(), widths);
assert_eq!(table.rows, rows);
assert_eq!(table.header, None);
assert_eq!(table.footer, None);
assert_eq!(table.widths, widths);
assert_eq!(table.column_spacing, 1);
assert_eq!(table.block, None);
assert_eq!(table.style, Style::default());
assert_eq!(table.highlight_style, Style::default());
assert_eq!(table.highlight_symbol, Text::default());
assert_eq!(table.highlight_spacing, HighlightSpacing::WhenSelected);
assert_eq!(table.flex, Flex::Start);
}
#[test]
fn default() {
let table = Table::default();
assert_eq!(table.rows, vec![]);
assert_eq!(table.header, None);
assert_eq!(table.footer, None);
assert_eq!(table.widths, vec![]);
assert_eq!(table.column_spacing, 1);
assert_eq!(table.block, None);
assert_eq!(table.style, Style::default());
assert_eq!(table.highlight_style, Style::default());
assert_eq!(table.highlight_symbol, Text::default());
assert_eq!(table.highlight_spacing, HighlightSpacing::WhenSelected);
assert_eq!(table.flex, Flex::Start);
}
#[test]
fn collect() {
let table = (0..4)
.map(|i| -> Row { (0..4).map(|j| format!("{i}*{j} = {}", i * j)).collect() })
.collect::<Table>()
.widths([Constraint::Percentage(25); 4]);
let expected_rows: Vec<Row> = vec![
Row::new(["0*0 = 0", "0*1 = 0", "0*2 = 0", "0*3 = 0"]),
Row::new(["1*0 = 0", "1*1 = 1", "1*2 = 2", "1*3 = 3"]),
Row::new(["2*0 = 0", "2*1 = 2", "2*2 = 4", "2*3 = 6"]),
Row::new(["3*0 = 0", "3*1 = 3", "3*2 = 6", "3*3 = 9"]),
];
assert_eq!(table.rows, expected_rows);
assert_eq!(table.widths, [Constraint::Percentage(25); 4]);
}
#[test]
fn widths() {
let table = Table::default().widths([Constraint::Length(100)]);
assert_eq!(table.widths, [Constraint::Length(100)]);
#[allow(clippy::needless_borrows_for_generic_args)]
let table = Table::default().widths(&[Constraint::Length(100)]);
assert_eq!(table.widths, [Constraint::Length(100)]);
let table = Table::default().widths(vec![Constraint::Length(100)]);
assert_eq!(table.widths, [Constraint::Length(100)]);
let table = Table::default().widths(&vec![Constraint::Length(100)]);
assert_eq!(table.widths, [Constraint::Length(100)]);
let table = Table::default().widths([100].into_iter().map(Constraint::Length));
assert_eq!(table.widths, [Constraint::Length(100)]);
}
#[test]
fn rows() {
let rows = [Row::new(vec![Cell::from("")])];
let table = Table::default().rows(rows.clone());
assert_eq!(table.rows, rows);
}
#[test]
fn column_spacing() {
let table = Table::default().column_spacing(2);
assert_eq!(table.column_spacing, 2);
}
#[test]
fn block() {
let block = Block::bordered().title("Table");
let table = Table::default().block(block.clone());
assert_eq!(table.block, Some(block));
}
#[test]
fn header() {
let header = Row::new(vec![Cell::from("")]);
let table = Table::default().header(header.clone());
assert_eq!(table.header, Some(header));
}
#[test]
fn footer() {
let footer = Row::new(vec![Cell::from("")]);
let table = Table::default().footer(footer.clone());
assert_eq!(table.footer, Some(footer));
}
#[test]
fn highlight_style() {
let style = Style::default().red().italic();
let table = Table::default().highlight_style(style);
assert_eq!(table.highlight_style, style);
}
#[test]
fn highlight_symbol() {
let table = Table::default().highlight_symbol(">>");
assert_eq!(table.highlight_symbol, Text::from(">>"));
}
#[test]
fn highlight_spacing() {
let table = Table::default().highlight_spacing(HighlightSpacing::Always);
assert_eq!(table.highlight_spacing, HighlightSpacing::Always);
}
#[test]
#[should_panic = "Percentages should be between 0 and 100 inclusively"]
fn table_invalid_percentages() {
let _ = Table::default().widths([Constraint::Percentage(110)]);
}
#[test]
fn widths_conversions() {
let array = [Constraint::Percentage(100)];
let table = Table::new(Vec::<Row>::new(), array);
assert_eq!(table.widths, vec![Constraint::Percentage(100)], "array");
let array_ref = &[Constraint::Percentage(100)];
let table = Table::new(Vec::<Row>::new(), array_ref);
assert_eq!(table.widths, vec![Constraint::Percentage(100)], "array ref");
let vec = vec![Constraint::Percentage(100)];
let slice = vec.as_slice();
let table = Table::new(Vec::<Row>::new(), slice);
assert_eq!(table.widths, vec![Constraint::Percentage(100)], "slice");
let vec = vec![Constraint::Percentage(100)];
let table = Table::new(Vec::<Row>::new(), vec);
assert_eq!(table.widths, vec![Constraint::Percentage(100)], "vec");
let vec_ref = &vec![Constraint::Percentage(100)];
let table = Table::new(Vec::<Row>::new(), vec_ref);
assert_eq!(table.widths, vec![Constraint::Percentage(100)], "vec ref");
}
#[cfg(test)]
mod render {
use super::*;
#[test]
fn render_empty_area() {
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
let rows = vec![Row::new(vec!["Cell1", "Cell2"])];
let table = Table::new(rows, vec![Constraint::Length(5); 2]);
Widget::render(table, Rect::new(0, 0, 0, 0), &mut buf);
assert_eq!(buf, Buffer::empty(Rect::new(0, 0, 15, 3)));
}
#[test]
fn render_default() {
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
let table = Table::default();
Widget::render(table, Rect::new(0, 0, 15, 3), &mut buf);
assert_eq!(buf, Buffer::empty(Rect::new(0, 0, 15, 3)));
}
#[test]
fn render_with_block() {
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
let rows = vec![
Row::new(vec!["Cell1", "Cell2"]),
Row::new(vec!["Cell3", "Cell4"]),
];
let block = Block::bordered().title("Block");
let table = Table::new(rows, vec![Constraint::Length(5); 2]).block(block);
Widget::render(table, Rect::new(0, 0, 15, 3), &mut buf);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"┌Block────────┐",
"│Cell1 Cell2 │",
"└─────────────┘",
]);
assert_eq!(buf, expected);
}
#[test]
fn render_with_header() {
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
let header = Row::new(vec!["Head1", "Head2"]);
let rows = vec![
Row::new(vec!["Cell1", "Cell2"]),
Row::new(vec!["Cell3", "Cell4"]),
];
let table = Table::new(rows, [Constraint::Length(5); 2]).header(header);
Widget::render(table, Rect::new(0, 0, 15, 3), &mut buf);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"Head1 Head2 ",
"Cell1 Cell2 ",
"Cell3 Cell4 ",
]);
assert_eq!(buf, expected);
}
#[test]
fn render_with_footer() {
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
let footer = Row::new(vec!["Foot1", "Foot2"]);
let rows = vec![
Row::new(vec!["Cell1", "Cell2"]),
Row::new(vec!["Cell3", "Cell4"]),
];
let table = Table::new(rows, [Constraint::Length(5); 2]).footer(footer);
Widget::render(table, Rect::new(0, 0, 15, 3), &mut buf);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"Cell1 Cell2 ",
"Cell3 Cell4 ",
"Foot1 Foot2 ",
]);
assert_eq!(buf, expected);
}
#[test]
fn render_with_header_and_footer() {
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
let header = Row::new(vec!["Head1", "Head2"]);
let footer = Row::new(vec!["Foot1", "Foot2"]);
let rows = vec![Row::new(vec!["Cell1", "Cell2"])];
let table = Table::new(rows, [Constraint::Length(5); 2])
.header(header)
.footer(footer);
Widget::render(table, Rect::new(0, 0, 15, 3), &mut buf);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"Head1 Head2 ",
"Cell1 Cell2 ",
"Foot1 Foot2 ",
]);
assert_eq!(buf, expected);
}
#[test]
fn render_with_header_margin() {
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
let header = Row::new(vec!["Head1", "Head2"]).bottom_margin(1);
let rows = vec![
Row::new(vec!["Cell1", "Cell2"]),
Row::new(vec!["Cell3", "Cell4"]),
];
let table = Table::new(rows, [Constraint::Length(5); 2]).header(header);
Widget::render(table, Rect::new(0, 0, 15, 3), &mut buf);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"Head1 Head2 ",
" ",
"Cell1 Cell2 ",
]);
assert_eq!(buf, expected);
}
#[test]
fn render_with_footer_margin() {
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
let footer = Row::new(vec!["Foot1", "Foot2"]).top_margin(1);
let rows = vec![Row::new(vec!["Cell1", "Cell2"])];
let table = Table::new(rows, [Constraint::Length(5); 2]).footer(footer);
Widget::render(table, Rect::new(0, 0, 15, 3), &mut buf);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"Cell1 Cell2 ",
" ",
"Foot1 Foot2 ",
]);
assert_eq!(buf, expected);
}
#[test]
fn render_with_row_margin() {
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
let rows = vec![
Row::new(vec!["Cell1", "Cell2"]).bottom_margin(1),
Row::new(vec!["Cell3", "Cell4"]),
];
let table = Table::new(rows, [Constraint::Length(5); 2]);
Widget::render(table, Rect::new(0, 0, 15, 3), &mut buf);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"Cell1 Cell2 ",
" ",
"Cell3 Cell4 ",
]);
assert_eq!(buf, expected);
}
#[test]
fn render_with_alignment() {
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 3));
let rows = vec![
Row::new(vec![Line::from("Left").alignment(Alignment::Left)]),
Row::new(vec![Line::from("Center").alignment(Alignment::Center)]),
Row::new(vec![Line::from("Right").alignment(Alignment::Right)]),
];
let table = Table::new(rows, [Percentage(100)]);
Widget::render(table, Rect::new(0, 0, 10, 3), &mut buf);
let expected = Buffer::with_lines(["Left ", " Center ", " Right"]);
assert_eq!(buf, expected);
}
#[test]
fn render_with_overflow_does_not_panic() {
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 3));
let table = Table::new(Vec::<Row>::new(), [Constraint::Min(20); 1])
.header(Row::new([Line::from("").alignment(Alignment::Right)]))
.footer(Row::new([Line::from("").alignment(Alignment::Right)]));
Widget::render(table, Rect::new(0, 0, 20, 3), &mut buf);
}
#[test]
fn render_with_selected() {
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
let rows = vec![
Row::new(vec!["Cell1", "Cell2"]),
Row::new(vec!["Cell3", "Cell4"]),
];
let table = Table::new(rows, [Constraint::Length(5); 2])
.highlight_style(Style::new().red())
.highlight_symbol(">>");
let mut state = TableState::new().with_selected(0);
StatefulWidget::render(table, Rect::new(0, 0, 15, 3), &mut buf, &mut state);
let expected = Buffer::with_lines([
">>Cell1 Cell2 ".red(),
" Cell3 Cell4 ".into(),
" ".into(),
]);
assert_eq!(buf, expected);
}
#[test]
fn render_with_selected_marked_unmarked() {
let rows = vec![
Row::new(vec!["Cell", "Cell"]),
Row::new(vec!["Cell", "Cell"]),
Row::new(vec!["Cell", "Cell"]),
Row::new(vec!["Cell", "Cell"]),
Row::new(vec!["Cell", "Cell"]),
Row::new(vec!["Cell", "Cell"]),
Row::new(vec!["Cell", "Cell"]),
];
let table = Table::new(rows, [Constraint::Length(5); 2])
.highlight_symbol("")
.mark_symbol("")
.unmark_symbol(" ")
.mark_highlight_symbol("⦿");
let mut state = TableState::new().with_selected(0);
state.mark(1);
state.mark(3);
state.mark(5);
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 10));
StatefulWidget::render(table.clone(), Rect::new(0, 0, 15, 10), &mut buf, &mut state);
let expected = Buffer::with_lines(Text::from(vec![
"• Cell Cell ".into(),
"⦾ Cell Cell ".into(),
" Cell Cell ".into(),
"⦾ Cell Cell ".into(),
" Cell Cell ".into(),
"⦾ Cell Cell ".into(),
" Cell Cell ".into(),
" ".into(),
" ".into(),
" ".into(),
]));
assert_eq!(buf, expected);
state.mark(0);
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 10));
StatefulWidget::render(table.clone(), Rect::new(0, 0, 15, 10), &mut buf, &mut state);
let expected = Buffer::with_lines(Text::from(vec![
"⦿ Cell Cell ".into(),
"⦾ Cell Cell ".into(),
" Cell Cell ".into(),
"⦾ Cell Cell ".into(),
" Cell Cell ".into(),
"⦾ Cell Cell ".into(),
" Cell Cell ".into(),
" ".into(),
" ".into(),
" ".into(),
]));
assert_eq!(buf, expected);
state.select(Some(1));
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 10));
StatefulWidget::render(table.clone(), Rect::new(0, 0, 15, 10), &mut buf, &mut state);
let expected = Buffer::with_lines(Text::from(vec![
"⦾ Cell Cell ".into(),
"⦿ Cell Cell ".into(),
" Cell Cell ".into(),
"⦾ Cell Cell ".into(),
" Cell Cell ".into(),
"⦾ Cell Cell ".into(),
" Cell Cell ".into(),
" ".into(),
" ".into(),
" ".into(),
]));
assert_eq!(buf, expected);
state.unmark(0);
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 10));
StatefulWidget::render(table.clone(), Rect::new(0, 0, 15, 10), &mut buf, &mut state);
let expected = Buffer::with_lines(Text::from(vec![
" Cell Cell ".into(),
"⦿ Cell Cell ".into(),
" Cell Cell ".into(),
"⦾ Cell Cell ".into(),
" Cell Cell ".into(),
"⦾ Cell Cell ".into(),
" Cell Cell ".into(),
" ".into(),
" ".into(),
" ".into(),
]));
assert_eq!(buf, expected);
}
}
// test how constraints interact with table column width allocation
mod column_widths {
use super::*;
#[test]
fn length_constraint() {
// without selection, more than needed width
let table = Table::default().widths([Length(4), Length(4)]);
assert_eq!(table.get_columns_widths(20, 0), [(0, 4), (5, 4)]);
// with selection, more than needed width
let table = Table::default().widths([Length(4), Length(4)]);
assert_eq!(table.get_columns_widths(20, 3), [(3, 4), (8, 4)]);
// without selection, less than needed width
let table = Table::default().widths([Length(4), Length(4)]);
assert_eq!(table.get_columns_widths(7, 0), [(0, 3), (4, 3)]);
// with selection, less than needed width
// <--------7px-------->
// ┌────────┐x┌────────┐
// │ (3, 2) │x│ (6, 1) │
// └────────┘x└────────┘
// column spacing (i.e. `x`) is always prioritized
let table = Table::default().widths([Length(4), Length(4)]);
assert_eq!(table.get_columns_widths(7, 3), [(3, 2), (6, 1)]);
}
#[test]
fn max_constraint() {
// without selection, more than needed width
let table = Table::default().widths([Max(4), Max(4)]);
assert_eq!(table.get_columns_widths(20, 0), [(0, 4), (5, 4)]);
// with selection, more than needed width
let table = Table::default().widths([Max(4), Max(4)]);
assert_eq!(table.get_columns_widths(20, 3), [(3, 4), (8, 4)]);
// without selection, less than needed width
let table = Table::default().widths([Max(4), Max(4)]);
assert_eq!(table.get_columns_widths(7, 0), [(0, 3), (4, 3)]);
// with selection, less than needed width
let table = Table::default().widths([Max(4), Max(4)]);
assert_eq!(table.get_columns_widths(7, 3), [(3, 2), (6, 1)]);
}
#[test]
fn min_constraint() {
// in its currently stage, the "Min" constraint does not grow to use the possible
// available length and enabling "expand_to_fill" will just stretch the last
// constraint and not split it with all available constraints
// without selection, more than needed width
let table = Table::default().widths([Min(4), Min(4)]);
assert_eq!(table.get_columns_widths(20, 0), [(0, 10), (11, 9)]);
// with selection, more than needed width
let table = Table::default().widths([Min(4), Min(4)]);
assert_eq!(table.get_columns_widths(20, 3), [(3, 8), (12, 8)]);
// without selection, less than needed width
// allocates spacer
let table = Table::default().widths([Min(4), Min(4)]);
assert_eq!(table.get_columns_widths(7, 0), [(0, 3), (4, 3)]);
// with selection, less than needed width
// always allocates selection and spacer
let table = Table::default().widths([Min(4), Min(4)]);
assert_eq!(table.get_columns_widths(7, 3), [(3, 2), (6, 1)]);
}
#[test]
fn percentage_constraint() {
// without selection, more than needed width
let table = Table::default().widths([Percentage(30), Percentage(30)]);
assert_eq!(table.get_columns_widths(20, 0), [(0, 6), (7, 6)]);
// with selection, more than needed width
let table = Table::default().widths([Percentage(30), Percentage(30)]);
assert_eq!(table.get_columns_widths(20, 3), [(3, 5), (9, 5)]);
// without selection, less than needed width
// rounds from positions: [0.0, 0.0, 2.1, 3.1, 5.2, 7.0]
let table = Table::default().widths([Percentage(30), Percentage(30)]);
assert_eq!(table.get_columns_widths(7, 0), [(0, 2), (3, 2)]);
// with selection, less than needed width
// rounds from positions: [0.0, 3.0, 5.1, 6.1, 7.0, 7.0]
let table = Table::default().widths([Percentage(30), Percentage(30)]);
assert_eq!(table.get_columns_widths(7, 3), [(3, 1), (5, 1)]);
}
#[test]
fn ratio_constraint() {
// without selection, more than needed width
// rounds from positions: [0.00, 0.00, 6.67, 7.67, 14.33]
let table = Table::default().widths([Ratio(1, 3), Ratio(1, 3)]);
assert_eq!(table.get_columns_widths(20, 0), [(0, 7), (8, 6)]);
// with selection, more than needed width
// rounds from positions: [0.00, 3.00, 10.67, 17.33, 20.00]
let table = Table::default().widths([Ratio(1, 3), Ratio(1, 3)]);
assert_eq!(table.get_columns_widths(20, 3), [(3, 6), (10, 5)]);
// without selection, less than needed width
// rounds from positions: [0.00, 2.33, 3.33, 5.66, 7.00]
let table = Table::default().widths([Ratio(1, 3), Ratio(1, 3)]);
assert_eq!(table.get_columns_widths(7, 0), [(0, 2), (3, 3)]);
// with selection, less than needed width
// rounds from positions: [0.00, 3.00, 5.33, 6.33, 7.00, 7.00]
let table = Table::default().widths([Ratio(1, 3), Ratio(1, 3)]);
assert_eq!(table.get_columns_widths(7, 3), [(3, 1), (5, 2)]);
}
/// When more width is available than requested, the behavior is controlled by flex
#[test]
fn underconstrained_flex() {
let table = Table::default().widths([Min(10), Min(10), Min(1)]);
assert_eq!(
table.get_columns_widths(62, 0),
&[(0, 20), (21, 20), (42, 20)]
);
let table = Table::default()
.widths([Min(10), Min(10), Min(1)])
.flex(Flex::Legacy);
assert_eq!(
table.get_columns_widths(62, 0),
&[(0, 10), (11, 10), (22, 40)]
);
let table = Table::default()
.widths([Min(10), Min(10), Min(1)])
.flex(Flex::SpaceBetween);
assert_eq!(
table.get_columns_widths(62, 0),
&[(0, 20), (21, 20), (42, 20)]
);
}
/// NOTE: `segment_size` is deprecated use flex instead!
#[allow(deprecated)]
#[test]
fn underconstrained_segment_size() {
let table = Table::default().widths([Min(10), Min(10), Min(1)]);
assert_eq!(
table.get_columns_widths(62, 0),
&[(0, 20), (21, 20), (42, 20)]
);
let table = Table::default()
.widths([Min(10), Min(10), Min(1)])
.flex(Flex::Legacy);
assert_eq!(
table.get_columns_widths(62, 0),
&[(0, 10), (11, 10), (22, 40)]
);
}
#[test]
fn no_constraint_with_rows() {
let table = Table::default()
.rows(vec![
Row::new(vec!["a", "b"]),
Row::new(vec!["c", "d", "e"]),
])
// rows should get precedence over header
.header(Row::new(vec!["f", "g"]))
.footer(Row::new(vec!["h", "i"]))
.column_spacing(0);
assert_eq!(
table.get_columns_widths(30, 0),
&[(0, 10), (10, 10), (20, 10)]
);
}
#[test]
fn no_constraint_with_header() {
let table = Table::default()
.rows(vec![])
.header(Row::new(vec!["f", "g"]))
.column_spacing(0);
assert_eq!(table.get_columns_widths(10, 0), [(0, 5), (5, 5)]);
}
#[test]
fn no_constraint_with_footer() {
let table = Table::default()
.rows(vec![])
.footer(Row::new(vec!["h", "i"]))
.column_spacing(0);
assert_eq!(table.get_columns_widths(10, 0), [(0, 5), (5, 5)]);
}
#[track_caller]
fn test_table_with_selection_and_marks<'line, Lines, Marks>(
highlight_spacing: HighlightSpacing,
columns: u16,
spacing: u16,
selection: Option<usize>,
marks: Marks,
expected: Lines,
) where
Lines: IntoIterator,
Lines::Item: Into<Line<'line>>,
Marks: IntoIterator<Item = usize>,
{
let table = Table::default()
.rows(vec![Row::new(vec!["ABCDE", "12345"])])
.highlight_spacing(highlight_spacing)
.highlight_symbol(">>>")
.mark_symbol(" MMM ")
.mark_highlight_symbol(" >M> ")
.column_spacing(spacing);
let area = Rect::new(0, 0, columns, 3);
let mut buf = Buffer::empty(area);
let mut state = TableState::default().with_selected(selection);
for mark in marks {
state.mark(mark);
}
StatefulWidget::render(table, area, &mut buf, &mut state);
assert_eq!(buf, Buffer::with_lines(expected));
}
#[test]
#[allow(clippy::too_many_lines)]
fn highlight_symbol_mark_symbol_and_column_spacing_with_highlight_spacing() {
// no highlight_symbol or mark_symbol rendered ever
test_table_with_selection_and_marks(
HighlightSpacing::Never,
15, // width
0, // spacing
None, // selection
[], // marks
[
"ABCDE 12345 ", /* default layout is Flex::Start but columns length
* constraints are calculated as `max_area / n_columns`,
* i.e. they are distributed amongst available space */
" ", // row 2
" ", // row 3
],
);
// no highlight_symbol or mark_symbol rendered ever
test_table_with_selection_and_marks(
HighlightSpacing::Never,
15, // width
0, // spacing
None, // selection
[0], // marks
[
"ABCDE 12345 ", /* default layout is Flex::Start but columns length
* constraints are calculated as `max_area / n_columns`,
* i.e. they are distributed amongst available space */
" ", // row 2
" ", // row 3
],
);
// no highlight_symbol or mark_symbol rendered ever
test_table_with_selection_and_marks(
HighlightSpacing::Never,
15, // width
0, // spacing
None, // selection
[], // marks
[
"ABCDE 12345 ", /* default layout is Flex::Start but columns length
* constraints are calculated as `max_area / n_columns`,
* i.e. they are distributed amongst available space */
" ", // row 2
" ", // row 3
],
);
// no highlight_symbol or mark_symbol rendered
test_table_with_selection_and_marks(
HighlightSpacing::WhenSelected,
15, // width
0, // spacing
None, // selection
[], // marks
[
"ABCDE 12345 ", /* default layout is Flex::Start but columns length
* constraints are calculated as `max_area / n_columns`,
* i.e. they are distributed amongst available space */
" ", // row 2
" ", // row 3
],
);
// mark_symbol rendered
test_table_with_selection_and_marks(
HighlightSpacing::WhenSelected,
15, // width
0, // spacing
None, // selection
[0], // marks
[
" MMM ABCDE12345", /* default layout is Flex::Start but columns length
* constraints are calculated as `max_area / n_columns`,
* i.e. they are distributed amongst available space */
" ", // row 2
" ", // row 3
],
);
// highlight symbol rendered with mark symbol width
test_table_with_selection_and_marks(
HighlightSpacing::WhenSelected,
15, // width
0, // spacing
Some(0), // selection
[], // marks
[
">>> ABCDE12345", /* default layout is Flex::Start but columns length
* constraints are calculated as `max_area / n_columns`,
* i.e. they are distributed amongst available space */
" ", // row 2
" ", // row 3
],
);
// mark highlight symbol rendered
test_table_with_selection_and_marks(
HighlightSpacing::WhenSelected,
15, // width
0, // spacing
Some(0), // selection
[0], // marks
[
" >M> ABCDE12345", /* default layout is Flex::Start but columns length
* constraints are calculated as `max_area / n_columns`,
* i.e. they are distributed amongst available space */
" ", // row 2
" ", // row 3
],
);
// no highlight_symbol or mark_symbol rendered
test_table_with_selection_and_marks(
HighlightSpacing::Always,
15, // width
0, // spacing
None, // selection
[], // marks
[
" ABCDE12345", /* default layout is Flex::Start but columns length
* constraints are calculated as `max_area / n_columns`,
* i.e. they are distributed amongst available space */
" ", // row 2
" ", // row 3
],
);
// mark_symbol rendered
test_table_with_selection_and_marks(
HighlightSpacing::Always,
15, // width
0, // spacing
None, // selection
[0], // marks
[
" MMM ABCDE12345", /* default layout is Flex::Start but columns length
* constraints are calculated as `max_area / n_columns`,
* i.e. they are distributed amongst available space */
" ", // row 2
" ", // row 3
],
);
// highlight symbol rendered with mark symbol width
test_table_with_selection_and_marks(
HighlightSpacing::Always,
15, // width
0, // spacing
Some(0), // selection
[], // marks
[
">>> ABCDE12345", /* default layout is Flex::Start but columns length
* constraints are calculated as `max_area / n_columns`,
* i.e. they are distributed amongst available space */
" ", // row 2
" ", // row 3
],
);
// mark highlight symbol rendered
test_table_with_selection_and_marks(
HighlightSpacing::Always,
15, // width
0, // spacing
Some(0), // selection
[0], // marks
[
" >M> ABCDE12345", /* default layout is Flex::Start but columns length
* constraints are calculated as `max_area / n_columns`,
* i.e. they are distributed amongst available space */
" ", // row 2
" ", // row 3
],
);
}
#[track_caller]
fn test_table_with_selection<'line, Lines>(
highlight_spacing: HighlightSpacing,
columns: u16,
spacing: u16,
selection: Option<usize>,
expected: Lines,
) where
Lines: IntoIterator,
Lines::Item: Into<Line<'line>>,
{
let table = Table::default()
.rows(vec![Row::new(vec!["ABCDE", "12345"])])
.highlight_spacing(highlight_spacing)
.highlight_symbol(">>>")
.column_spacing(spacing);
let area = Rect::new(0, 0, columns, 3);
let mut buf = Buffer::empty(area);
let mut state = TableState::default().with_selected(selection);
StatefulWidget::render(table, area, &mut buf, &mut state);
assert_eq!(buf, Buffer::with_lines(expected));
}
#[test]
fn excess_area_highlight_symbol_and_column_spacing_allocation() {
// no highlight_symbol rendered ever
test_table_with_selection(
HighlightSpacing::Never,
15, // width
0, // spacing
None, // selection
[
"ABCDE 12345 ", /* default layout is Flex::Start but columns length
* constraints are calculated as `max_area / n_columns`,
* i.e. they are distributed amongst available space */
" ", // row 2
" ", // row 3
],
);
let table = Table::default()
.rows(vec![Row::new(vec!["ABCDE", "12345"])])
.widths([5, 5])
.column_spacing(0);
let area = Rect::new(0, 0, 15, 3);
let mut buf = Buffer::empty(area);
Widget::render(table, area, &mut buf);
let expected = Buffer::with_lines([
"ABCDE12345 ", /* As reference, this is what happens when you manually
* specify widths */
" ", // row 2
" ", // row 3
]);
assert_eq!(buf, expected);
// no highlight_symbol rendered ever
test_table_with_selection(
HighlightSpacing::Never,
15, // width
0, // spacing
Some(0), // selection
[
"ABCDE 12345 ", // row 1
" ", // row 2
" ", // row 3
],
);
// no highlight_symbol rendered because no selection is made
test_table_with_selection(
HighlightSpacing::WhenSelected,
15, // width
0, // spacing
None, // selection
[
"ABCDE 12345 ", // row 1
" ", // row 2
" ", // row 3
],
);
// highlight_symbol rendered because selection is made
test_table_with_selection(
HighlightSpacing::WhenSelected,
15, // width
0, // spacing
Some(0), // selection
[
">>>ABCDE 12345 ", // row 1
" ", // row 2
" ", // row 3
],
);
// highlight_symbol always rendered even no selection is made
test_table_with_selection(
HighlightSpacing::Always,
15, // width
0, // spacing
None, // selection
[
" ABCDE 12345 ", // row 1
" ", // row 2
" ", // row 3
],
);
// no highlight_symbol rendered because no selection is made
test_table_with_selection(
HighlightSpacing::Always,
15, // width
0, // spacing
Some(0), // selection
[
">>>ABCDE 12345 ", // row 1
" ", // row 2
" ", // row 3
],
);
}
#[allow(clippy::too_many_lines)]
#[test]
fn insufficient_area_highlight_symbol_and_column_spacing_allocation() {
// column spacing is prioritized over every other constraint
test_table_with_selection(
HighlightSpacing::Never,
10, // width
1, // spacing
None, // selection
[
"ABCDE 1234", // spacing is prioritized and column is cut
" ", // row 2
" ", // row 3
],
);
test_table_with_selection(
HighlightSpacing::WhenSelected,
10, // width
1, // spacing
None, // selection
[
"ABCDE 1234", // spacing is prioritized and column is cut
" ", // row 2
" ", // row 3
],
);
// this test checks that space for highlight_symbol space is always allocated.
// this test also checks that space for column is allocated.
//
// Space for highlight_symbol is allocated first by splitting horizontal space
// into highlight_symbol area and column area.
// Then in a separate step, column widths are calculated.
// column spacing is prioritized when column widths are calculated and last column here
// ends up with just 1 wide
test_table_with_selection(
HighlightSpacing::Always,
10, // width
1, // spacing
None, // selection
[
" ABC 123", // highlight_symbol and spacing are prioritized
" ", // row 2
" ", // row 3
],
);
// the following are specification tests
test_table_with_selection(
HighlightSpacing::Always,
9, // width
1, // spacing
None, // selection
[
" ABC 12", // highlight_symbol and spacing are prioritized
" ", // row 2
" ", // row 3
],
);
test_table_with_selection(
HighlightSpacing::Always,
8, // width
1, // spacing
None, // selection
[
" AB 12", // highlight_symbol and spacing are prioritized
" ", // row 2
" ", // row 3
],
);
test_table_with_selection(
HighlightSpacing::Always,
7, // width
1, // spacing
None, // selection
[
" AB 1", // highlight_symbol and spacing are prioritized
" ", // row 2
" ", // row 3
],
);
let table = Table::default()
.rows(vec![Row::new(vec!["ABCDE", "12345"])])
.highlight_spacing(HighlightSpacing::Always)
.flex(Flex::Legacy)
.highlight_symbol(">>>")
.column_spacing(1);
let area = Rect::new(0, 0, 10, 3);
let mut buf = Buffer::empty(area);
Widget::render(table, area, &mut buf);
// highlight_symbol and spacing are prioritized but columns are evenly distributed
#[rustfmt::skip]
let expected = Buffer::with_lines([
" ABCDE 1",
" ",
" ",
]);
assert_eq!(buf, expected);
let table = Table::default()
.rows(vec![Row::new(vec!["ABCDE", "12345"])])
.highlight_spacing(HighlightSpacing::Always)
.flex(Flex::Start)
.highlight_symbol(">>>")
.column_spacing(1);
let area = Rect::new(0, 0, 10, 3);
let mut buf = Buffer::empty(area);
Widget::render(table, area, &mut buf);
// highlight_symbol and spacing are prioritized but columns are evenly distributed
#[rustfmt::skip]
let expected = Buffer::with_lines([
" ABC 123",
" ",
" ",
]);
assert_eq!(buf, expected);
test_table_with_selection(
HighlightSpacing::Never,
10, // width
1, // spacing
Some(0), // selection
[
"ABCDE 1234", // spacing is prioritized
" ",
" ",
],
);
test_table_with_selection(
HighlightSpacing::WhenSelected,
10, // width
1, // spacing
Some(0), // selection
[
">>>ABC 123", // row 1
" ", // row 2
" ", // row 3
],
);
test_table_with_selection(
HighlightSpacing::Always,
10, // width
1, // spacing
Some(0), // selection
[
">>>ABC 123", // highlight column and spacing are prioritized
" ", // row 2
" ", // row 3
],
);
}
#[test]
fn insufficient_area_highlight_symbol_allocation_with_no_column_spacing() {
test_table_with_selection(
HighlightSpacing::Never,
10, // width
0, // spacing
None, // selection
[
"ABCDE12345", // row 1
" ", // row 2
" ", // row 3
],
);
test_table_with_selection(
HighlightSpacing::WhenSelected,
10, // width
0, // spacing
None, // selection
[
"ABCDE12345", // row 1
" ", // row 2
" ", // row 3
],
);
// highlight symbol spacing is prioritized over all constraints
// even if the constraints are fixed length
// this is because highlight_symbol column is separated _before_ any of the constraint
// widths are calculated
test_table_with_selection(
HighlightSpacing::Always,
10, // width
0, // spacing
None, // selection
[
" ABCD123", // highlight column and spacing are prioritized
" ", // row 2
" ", // row 3
],
);
test_table_with_selection(
HighlightSpacing::Never,
10, // width
0, // spacing
Some(0), // selection
[
"ABCDE12345", // row 1
" ", // row 2
" ", // row 3
],
);
test_table_with_selection(
HighlightSpacing::WhenSelected,
10, // width
0, // spacing
Some(0), // selection
[
">>>ABCD123", // highlight column and spacing are prioritized
" ", // row 2
" ", // row 3
],
);
test_table_with_selection(
HighlightSpacing::Always,
10, // width
0, // spacing
Some(0), // selection
[
">>>ABCD123", // highlight column and spacing are prioritized
" ", // row 2
" ", // row 3
],
);
}
}
#[test]
fn stylize() {
assert_eq!(
Table::new(vec![Row::new(vec![Cell::from("")])], [Percentage(100)])
.black()
.on_white()
.bold()
.not_crossed_out()
.style,
Style::default()
.fg(Color::Black)
.bg(Color::White)
.add_modifier(Modifier::BOLD)
.remove_modifier(Modifier::CROSSED_OUT)
);
}
}