use itertools::Itertools;
#[allow(unused_imports)] use super::Cell;
use super::{HighlightSpacing, Row, TableState};
use crate::{layout::Flex, prelude::*, style::Styled, widgets::Block};
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Table<'a> {
rows: Vec<Row<'a>>,
header: Option<Row<'a>>,
footer: Option<Row<'a>>,
widths: Vec<Constraint>,
column_spacing: u16,
block: Option<Block<'a>>,
style: Style,
highlight_style: Style,
highlight_symbol: Text<'a>,
highlight_spacing: HighlightSpacing,
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(),
highlight_spacing: HighlightSpacing::default(),
flex: Flex::Start,
}
}
}
impl<'a> Table<'a> {
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()
}
}
#[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
}
#[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
}
#[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
}
#[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
}
#[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
}
#[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
}
#[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
}
#[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
}
#[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
}
#[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
}
#[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);
}
}
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;
}
if state.selected.is_some_and(|s| s >= self.rows.len()) {
state.select(Some(self.rows.len().saturating_sub(1)));
}
if self.rows.is_empty() {
state.select(None);
}
let selection_width = self.selection_width(state);
let columns_widths = self.get_columns_widths(table_area.width, selection_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,
selection_width,
&self.highlight_symbol,
&columns_widths,
);
self.render_footer(footer_area, buf, &columns_widths);
}
}
impl Table<'_> {
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,
selection_width: u16,
highlight_symbol: &Text<'_>,
columns_widths: &[(u16, u16)],
) {
if self.rows.is_empty() {
return;
}
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_selected = state.selected().is_some_and(|index| index == i);
if selection_width > 0 && is_selected {
let selection_area = Rect {
width: selection_width,
..row_area
};
buf.set_style(selection_area, row.style);
highlight_symbol.render_ref(selection_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_selected {
buf.set_style(row_area, self.highlight_style);
}
y_offset += row.height_with_margin();
}
}
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);
vec![Constraint::Length(max_width / col_count.max(1) as u16); col_count]
} else {
self.widths.clone()
};
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 Some(selected) = selected else {
return (start, end);
};
let selected = selected.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)
}
fn selection_width(&self, state: &TableState) -> u16 {
let has_selection = state.selected().is_some();
if self.highlight_spacing.should_add(has_selection) {
self.highlight_symbol.width() as u16
} else {
0
}
}
}
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>>,
{
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, widgets::Cell};
#[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, []);
assert_eq!(table.header, None);
assert_eq!(table.footer, None);
assert_eq!(table.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 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)]);
#[allow(clippy::needless_borrows_for_generic_args)]
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, [Constraint::Percentage(100)], "array");
let array_ref = &[Constraint::Percentage(100)];
let table = Table::new(Vec::<Row>::new(), array_ref);
assert_eq!(table.widths, [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, [Constraint::Percentage(100)], "slice");
let vec = vec![Constraint::Percentage(100)];
let table = Table::new(Vec::<Row>::new(), vec);
assert_eq!(table.widths, [Constraint::Percentage(100)], "vec");
let vec_ref = &vec![Constraint::Percentage(100)];
let table = Table::new(Vec::<Row>::new(), vec_ref);
assert_eq!(table.widths, [Constraint::Percentage(100)], "vec ref");
}
#[cfg(test)]
mod state {
use rstest::{fixture, rstest};
use super::TableState;
use crate::{
buffer::Buffer,
layout::{Constraint, Rect},
widgets::{Row, StatefulWidget, Table},
};
#[fixture]
fn table_buf() -> Buffer {
Buffer::empty(Rect::new(0, 0, 10, 10))
}
#[rstest]
fn test_list_state_empty_list(mut table_buf: Buffer) {
let mut state = TableState::default();
let rows: Vec<Row> = Vec::new();
let widths = vec![Constraint::Percentage(100)];
let table = Table::new(rows, widths);
state.select_first();
StatefulWidget::render(table, table_buf.area, &mut table_buf, &mut state);
assert_eq!(state.selected, None);
}
#[rstest]
fn test_list_state_single_item(mut table_buf: Buffer) {
let mut state = TableState::default();
let widths = vec![Constraint::Percentage(100)];
let items = vec![Row::new(vec!["Item 1"])];
let table = Table::new(items, widths);
state.select_first();
StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
assert_eq!(state.selected, Some(0));
state.select_last();
StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
assert_eq!(state.selected, Some(0));
state.select_previous();
StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
assert_eq!(state.selected, Some(0));
state.select_next();
StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
assert_eq!(state.selected, Some(0));
}
}
#[cfg(test)]
mod render {
use rstest::rstest;
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);
}
#[rstest]
#[case::no_selection(None, 50, ["50", "51", "52", "53", "54"])]
#[case::selection_before_offset(20, 20, ["20", "21", "22", "23", "24"])]
#[case::selection_immediately_before_offset(49, 49, ["49", "50", "51", "52", "53"])]
#[case::selection_at_start_of_offset(50, 50, ["50", "51", "52", "53", "54"])]
#[case::selection_at_end_of_offset(54, 50, ["50", "51", "52", "53", "54"])]
#[case::selection_immediately_after_offset(55, 51, ["51", "52", "53", "54", "55"])]
#[case::selection_after_offset(80, 76, ["76", "77", "78", "79", "80"])]
fn render_with_selection_and_offset<T: Into<Option<usize>>>(
#[case] selected_row: T,
#[case] expected_offset: usize,
#[case] expected_items: [&str; 5],
) {
let rows = (0..100).map(|i| Row::new([i.to_string()]));
let table = Table::new(rows, [Constraint::Length(2)]);
let mut buf = Buffer::empty(Rect::new(0, 0, 2, 5));
let mut state = TableState::new()
.with_offset(50)
.with_selected(selected_row);
StatefulWidget::render(table.clone(), Rect::new(0, 0, 5, 5), &mut buf, &mut state);
assert_eq!(buf, Buffer::with_lines(expected_items));
assert_eq!(state.offset, expected_offset);
}
}
mod column_widths {
use super::*;
#[test]
fn length_constraint() {
let table = Table::default().widths([Length(4), Length(4)]);
assert_eq!(table.get_columns_widths(20, 0), [(0, 4), (5, 4)]);
let table = Table::default().widths([Length(4), Length(4)]);
assert_eq!(table.get_columns_widths(20, 3), [(3, 4), (8, 4)]);
let table = Table::default().widths([Length(4), Length(4)]);
assert_eq!(table.get_columns_widths(7, 0), [(0, 3), (4, 3)]);
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() {
let table = Table::default().widths([Max(4), Max(4)]);
assert_eq!(table.get_columns_widths(20, 0), [(0, 4), (5, 4)]);
let table = Table::default().widths([Max(4), Max(4)]);
assert_eq!(table.get_columns_widths(20, 3), [(3, 4), (8, 4)]);
let table = Table::default().widths([Max(4), Max(4)]);
assert_eq!(table.get_columns_widths(7, 0), [(0, 3), (4, 3)]);
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() {
let table = Table::default().widths([Min(4), Min(4)]);
assert_eq!(table.get_columns_widths(20, 0), [(0, 10), (11, 9)]);
let table = Table::default().widths([Min(4), Min(4)]);
assert_eq!(table.get_columns_widths(20, 3), [(3, 8), (12, 8)]);
let table = Table::default().widths([Min(4), Min(4)]);
assert_eq!(table.get_columns_widths(7, 0), [(0, 3), (4, 3)]);
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() {
let table = Table::default().widths([Percentage(30), Percentage(30)]);
assert_eq!(table.get_columns_widths(20, 0), [(0, 6), (7, 6)]);
let table = Table::default().widths([Percentage(30), Percentage(30)]);
assert_eq!(table.get_columns_widths(20, 3), [(3, 5), (9, 5)]);
let table = Table::default().widths([Percentage(30), Percentage(30)]);
assert_eq!(table.get_columns_widths(7, 0), [(0, 2), (3, 2)]);
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() {
let table = Table::default().widths([Ratio(1, 3), Ratio(1, 3)]);
assert_eq!(table.get_columns_widths(20, 0), [(0, 7), (8, 6)]);
let table = Table::default().widths([Ratio(1, 3), Ratio(1, 3)]);
assert_eq!(table.get_columns_widths(20, 3), [(3, 6), (10, 5)]);
let table = Table::default().widths([Ratio(1, 3), Ratio(1, 3)]);
assert_eq!(table.get_columns_widths(7, 0), [(0, 2), (3, 3)]);
let table = Table::default().widths([Ratio(1, 3), Ratio(1, 3)]);
assert_eq!(table.get_columns_widths(7, 3), [(3, 1), (5, 2)]);
}
#[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)]
);
}
#[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"]),
])
.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<'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() {
test_table_with_selection(
HighlightSpacing::Never,
15, 0, None, [
"ABCDE 12345 ",
" ", " ", ],
);
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 ",
" ", " ", ]);
assert_eq!(buf, expected);
test_table_with_selection(
HighlightSpacing::Never,
15, 0, Some(0), [
"ABCDE 12345 ", " ", " ", ],
);
test_table_with_selection(
HighlightSpacing::WhenSelected,
15, 0, None, [
"ABCDE 12345 ", " ", " ", ],
);
test_table_with_selection(
HighlightSpacing::WhenSelected,
15, 0, Some(0), [
">>>ABCDE 12345 ", " ", " ", ],
);
test_table_with_selection(
HighlightSpacing::Always,
15, 0, None, [
" ABCDE 12345 ", " ", " ", ],
);
test_table_with_selection(
HighlightSpacing::Always,
15, 0, Some(0), [
">>>ABCDE 12345 ", " ", " ", ],
);
}
#[allow(clippy::too_many_lines)]
#[test]
fn insufficient_area_highlight_symbol_and_column_spacing_allocation() {
test_table_with_selection(
HighlightSpacing::Never,
10, 1, None, [
"ABCDE 1234", " ", " ", ],
);
test_table_with_selection(
HighlightSpacing::WhenSelected,
10, 1, None, [
"ABCDE 1234", " ", " ", ],
);
test_table_with_selection(
HighlightSpacing::Always,
10, 1, None, [
" ABC 123", " ", " ", ],
);
test_table_with_selection(
HighlightSpacing::Always,
9, 1, None, [
" ABC 12", " ", " ", ],
);
test_table_with_selection(
HighlightSpacing::Always,
8, 1, None, [
" AB 12", " ", " ", ],
);
test_table_with_selection(
HighlightSpacing::Always,
7, 1, None, [
" AB 1", " ", " ", ],
);
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);
#[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);
#[rustfmt::skip]
let expected = Buffer::with_lines([
" ABC 123",
" ",
" ",
]);
assert_eq!(buf, expected);
test_table_with_selection(
HighlightSpacing::Never,
10, 1, Some(0), [
"ABCDE 1234", " ",
" ",
],
);
test_table_with_selection(
HighlightSpacing::WhenSelected,
10, 1, Some(0), [
">>>ABC 123", " ", " ", ],
);
test_table_with_selection(
HighlightSpacing::Always,
10, 1, Some(0), [
">>>ABC 123", " ", " ", ],
);
}
#[test]
fn insufficient_area_highlight_symbol_allocation_with_no_column_spacing() {
test_table_with_selection(
HighlightSpacing::Never,
10, 0, None, [
"ABCDE12345", " ", " ", ],
);
test_table_with_selection(
HighlightSpacing::WhenSelected,
10, 0, None, [
"ABCDE12345", " ", " ", ],
);
test_table_with_selection(
HighlightSpacing::Always,
10, 0, None, [
" ABCD123", " ", " ", ],
);
test_table_with_selection(
HighlightSpacing::Never,
10, 0, Some(0), [
"ABCDE12345", " ", " ", ],
);
test_table_with_selection(
HighlightSpacing::WhenSelected,
10, 0, Some(0), [
">>>ABCD123", " ", " ", ],
);
test_table_with_selection(
HighlightSpacing::Always,
10, 0, Some(0), [
">>>ABCD123", " ", " ", ],
);
}
}
#[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)
);
}
}