diff --git a/examples/demo/ui.rs b/examples/demo/ui.rs index 95c1246..76336ab 100644 --- a/examples/demo/ui.rs +++ b/examples/demo/ui.rs @@ -308,18 +308,20 @@ where let failure_style = Style::default() .fg(Color::Red) .add_modifier(Modifier::RAPID_BLINK | Modifier::CROSSED_OUT); - let header = ["Server", "Location", "Status"]; let rows = app.servers.iter().map(|s| { let style = if s.status == "Up" { up_style } else { failure_style }; - Row::StyledData(vec![s.name, s.location, s.status].into_iter(), style) + Row::new(vec![s.name, s.location, s.status]).style(style) }); - let table = Table::new(header.iter(), rows) + let table = Table::new(rows) + .header( + Row::new(vec!["Server", "Location", "Status"]) + .style(Style::default().fg(Color::Yellow)), + ) .block(Block::default().title("Servers").borders(Borders::ALL)) - .header_style(Style::default().fg(Color::Yellow)) .widths(&[ Constraint::Length(15), Constraint::Length(15), diff --git a/examples/table.rs b/examples/table.rs index 6a1163b..846a55c 100644 --- a/examples/table.rs +++ b/examples/table.rs @@ -8,7 +8,7 @@ use tui::{ backend::TermionBackend, layout::{Constraint, Layout}, style::{Color, Modifier, Style}, - widgets::{Block, Borders, Row, Table, TableState}, + widgets::{Block, Borders, Cell, Row, Table, TableState}, Terminal, }; @@ -27,7 +27,7 @@ impl<'a> StatefulTable<'a> { vec!["Row31", "Row32", "Row33"], vec!["Row41", "Row42", "Row43"], vec!["Row51", "Row52", "Row53"], - vec!["Row61", "Row62", "Row63"], + vec!["Row61", "Row62\nTest", "Row63"], vec!["Row71", "Row72", "Row73"], vec!["Row81", "Row82", "Row83"], vec!["Row91", "Row92", "Row93"], @@ -93,16 +93,27 @@ fn main() -> Result<(), Box> { .margin(5) .split(f.size()); - let selected_style = Style::default() - .fg(Color::Yellow) - .add_modifier(Modifier::BOLD); - let normal_style = Style::default().fg(Color::White); - let header = ["Header1", "Header2", "Header3"]; - let rows = table - .items + let selected_style = Style::default().add_modifier(Modifier::REVERSED); + let normal_style = Style::default().bg(Color::Blue); + let header_cells = ["Header1", "Header2", "Header3"] .iter() - .map(|i| Row::StyledData(i.iter(), normal_style)); - let t = Table::new(header.iter(), rows) + .map(|h| Cell::from(*h).style(Style::default().fg(Color::Red))); + let header = Row::new(header_cells) + .style(normal_style) + .height(1) + .bottom_margin(1); + let rows = table.items.iter().map(|item| { + let height = item + .iter() + .map(|content| content.chars().filter(|c| *c == '\n').count()) + .max() + .unwrap_or(0) + + 1; + let cells = item.iter().map(|c| Cell::from(*c)); + Row::new(cells).height(height as u16).bottom_margin(1) + }); + let t = Table::new(rows) + .header(header) .block(Block::default().borders(Borders::ALL).title("Table")) .highlight_style(selected_style) .highlight_symbol(">> ") diff --git a/src/widgets/block.rs b/src/widgets/block.rs index 08df019..4a380f0 100644 --- a/src/widgets/block.rs +++ b/src/widgets/block.rs @@ -7,7 +7,7 @@ use crate::{ widgets::{Borders, Widget}, }; -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum BorderType { Plain, Rounded, @@ -41,7 +41,7 @@ impl BorderType { /// .border_type(BorderType::Rounded) /// .style(Style::default().bg(Color::Black)); /// ``` -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct Block<'a> { /// Optional title place on the upper left of the block title: Option>, diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index af94bd1..d617893 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -36,7 +36,7 @@ pub use self::gauge::{Gauge, LineGauge}; pub use self::list::{List, ListItem, ListState}; pub use self::paragraph::{Paragraph, Wrap}; pub use self::sparkline::Sparkline; -pub use self::table::{Row, Table, TableState}; +pub use self::table::{Cell, Row, Table, TableState}; pub use self::tabs::Tabs; use crate::{buffer::Buffer, layout::Rect}; diff --git a/src/widgets/table.rs b/src/widgets/table.rs index b8957bf..e98f119 100644 --- a/src/widgets/table.rs +++ b/src/widgets/table.rs @@ -2,6 +2,7 @@ use crate::{ buffer::Buffer, layout::{Constraint, Rect}, style::Style, + text::Text, widgets::{Block, StatefulWidget, Widget}, }; use cassowary::{ @@ -11,158 +12,224 @@ use cassowary::{ }; use std::{ collections::HashMap, - fmt::Display, iter::{self, Iterator}, }; use unicode_width::UnicodeWidthStr; -#[derive(Debug, Clone)] -pub struct TableState { - offset: usize, - selected: Option, +/// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`]. +/// +/// It can be created from anything that can be converted to a [`Text`]. +/// ```rust +/// # use tui::widgets::Cell; +/// # use tui::style::{Style, Modifier}; +/// # use tui::text::{Span, Spans, Text}; +/// Cell::from("simple string"); +/// +/// Cell::from(Span::from("span")); +/// +/// Cell::from(Spans::from(vec![ +/// Span::raw("a vec of "), +/// Span::styled("spans", Style::default().add_modifier(Modifier::BOLD)) +/// ])); +/// +/// Cell::from(Text::from("a text")); +/// ``` +/// +/// You can apply a [`Style`] on the entire [`Cell`] using [`Cell::style`] or rely on the styling +/// capabilities of [`Text`]. +#[derive(Debug, Clone, PartialEq, Default)] +pub struct Cell<'a> { + content: Text<'a>, + style: Style, } -impl Default for TableState { - fn default() -> TableState { - TableState { - offset: 0, - selected: None, - } +impl<'a> Cell<'a> { + /// Set the `Style` of this cell. + pub fn style(mut self, style: Style) -> Self { + self.style = style; + self } } -impl TableState { - pub fn selected(&self) -> Option { - self.selected +impl<'a, T> From for Cell<'a> +where + T: Into>, +{ + fn from(content: T) -> Cell<'a> { + Cell { + content: content.into(), + style: Style::default(), + } } +} - pub fn select(&mut self, index: Option) { - self.selected = index; - if index.is_none() { - self.offset = 0; +/// Holds data to be displayed in a [`Table`] widget. +/// +/// A [`Row`] is a collection of cells. It can be created from simple strings: +/// ```rust +/// # use tui::widgets::Row; +/// Row::new(vec!["Cell1", "Cell2", "Cell3"]); +/// ``` +/// +/// But if you need a bit more control over individual cells, you can explicity create [`Cell`]s: +/// ```rust +/// # use tui::widgets::{Row, Cell}; +/// # use tui::style::{Style, Color}; +/// Row::new(vec![ +/// Cell::from("Cell1"), +/// Cell::from("Cell2").style(Style::default().fg(Color::Yellow)), +/// ]); +/// ``` +/// +/// By default, a row has a height of 1 but you can change this using [`Row::height`]. +#[derive(Debug, Clone, PartialEq, Default)] +pub struct Row<'a> { + cells: Vec>, + height: u16, + style: Style, + bottom_margin: u16, +} + +impl<'a> Row<'a> { + /// Creates a new [`Row`] from an iterator where items can be converted to a [`Cell`]. + pub fn new(cells: T) -> Self + where + T: IntoIterator, + T::Item: Into>, + { + Self { + height: 1, + cells: cells.into_iter().map(|c| c.into()).collect(), + style: Style::default(), + bottom_margin: 0, } } -} -/// Holds data to be displayed in a Table widget -#[derive(Debug, Clone)] -pub enum Row -where - D: Iterator, - D::Item: Display, -{ - Data(D), - StyledData(D, Style), + /// Set the fixed height of the [`Row`]. Any [`Cell`] whose content has more lines than this + /// height will see its content truncated. + pub fn height(mut self, height: u16) -> Self { + self.height = height; + self + } + + /// Set the [`Style`] of the entire row. This [`Style`] can be overriden by the [`Style`] of a + /// any individual [`Cell`] or event by their [`Text`] content. + pub fn style(mut self, style: Style) -> Self { + self.style = style; + self + } + + /// Set the bottom margin. By default, the bottom margin is `0`. + pub fn bottom_margin(mut self, margin: u16) -> Self { + self.bottom_margin = margin; + self + } + + /// Returns the total height of the row. + fn total_height(&self) -> u16 { + self.height.saturating_add(self.bottom_margin) + } } -/// A widget to display data in formatted columns -/// -/// # Examples +/// A widget to display data in formatted columns. /// -/// ``` -/// # use tui::widgets::{Block, Borders, Table, Row}; +/// It is a collection of [`Row`]s, themselves composed of [`Cell`]s: +/// ```rust +/// # use tui::widgets::{Block, Borders, Table, Row, Cell}; /// # use tui::layout::Constraint; -/// # use tui::style::{Style, Color}; -/// let row_style = Style::default().fg(Color::White); -/// Table::new( -/// ["Col1", "Col2", "Col3"].into_iter(), -/// vec![ -/// Row::StyledData(["Row11", "Row12", "Row13"].into_iter(), row_style), -/// Row::StyledData(["Row21", "Row22", "Row23"].into_iter(), row_style), -/// Row::StyledData(["Row31", "Row32", "Row33"].into_iter(), row_style), -/// Row::Data(["Row41", "Row42", "Row43"].into_iter()) -/// ].into_iter() -/// ) -/// .block(Block::default().title("Table")) -/// .header_style(Style::default().fg(Color::Yellow)) -/// .widths(&[Constraint::Length(5), Constraint::Length(5), Constraint::Length(10)]) -/// .style(Style::default().fg(Color::White)) -/// .column_spacing(1); +/// # use tui::style::{Style, Color, Modifier}; +/// # use tui::text::{Text, Spans, Span}; +/// Table::new(vec![ +/// // Row can be created from simple strings. +/// Row::new(vec!["Row11", "Row12", "Row13"]), +/// // You can style the entire row. +/// Row::new(vec!["Row21", "Row22", "Row23"]).style(Style::default().fg(Color::Blue)), +/// // If you need more control over the styling you may need to create Cells directly +/// Row::new(vec![ +/// Cell::from("Row31"), +/// Cell::from("Row32").style(Style::default().fg(Color::Yellow)), +/// Cell::from(Spans::from(vec![ +/// Span::raw("Row"), +/// Span::styled("33", Style::default().fg(Color::Green)) +/// ])), +/// ]), +/// // If a Row need to display some content over multiple lines, you just have to change +/// // its height. +/// Row::new(vec![ +/// Cell::from("Row\n41"), +/// Cell::from("Row\n42"), +/// Cell::from("Row\n43"), +/// ]).height(2), +/// ]) +/// // You can set the style of the entire Table. +/// .style(Style::default().fg(Color::White)) +/// // It has an optional header, which is simply a Row always visible at the top. +/// .header( +/// Row::new(vec!["Col1", "Col2", "Col3"]) +/// .style(Style::default().fg(Color::Yellow)) +/// // If you want some space between the header and the rest of the rows, you can always +/// // specify some margin at the bottom. +/// .bottom_margin(1) +/// ) +/// // As any other widget, a Table can be wrapped in a Block. +/// .block(Block::default().title("Table")) +/// // Columns widths are constrained in the same way as Layout... +/// .widths(&[Constraint::Length(5), Constraint::Length(5), Constraint::Length(10)]) +/// // ...and they can be separated by a fixed spacing. +/// .column_spacing(1) +/// // If you wish to highlight a row in any specific way when it is selected... +/// .highlight_style(Style::default().add_modifier(Modifier::BOLD)) +/// // ...and potentially show a symbol in front of the selection. +/// .highlight_symbol(">>"); /// ``` -#[derive(Debug, Clone)] -pub struct Table<'a, H, R> { +#[derive(Debug, Clone, PartialEq)] +pub struct Table<'a> { /// A block to wrap the widget in block: Option>, /// Base style for the widget style: Style, - /// Header row for all columns - header: H, - /// Style for the header - header_style: Style, /// Width constraints for each column widths: &'a [Constraint], /// Space between each column column_spacing: u16, - /// Space between the header and the rows - header_gap: u16, /// Style used to render the selected row highlight_style: Style, /// Symbol in front of the selected rom highlight_symbol: Option<&'a str>, + /// Optional header + header: Option>, /// Data to display in each row - rows: R, + rows: Vec>, } -impl<'a, H, R> Default for Table<'a, H, R> -where - H: Iterator + Default, - R: Iterator + Default, -{ - fn default() -> Table<'a, H, R> { - Table { - block: None, - style: Style::default(), - header: H::default(), - header_style: Style::default(), - widths: &[], - column_spacing: 1, - header_gap: 1, - highlight_style: Style::default(), - highlight_symbol: None, - rows: R::default(), - } - } -} -impl<'a, H, D, R> Table<'a, H, R> -where - H: Iterator, - D: Iterator, - D::Item: Display, - R: Iterator>, -{ - pub fn new(header: H, rows: R) -> Table<'a, H, R> { - Table { +impl<'a> Table<'a> { + pub fn new(rows: T) -> Self + where + T: IntoIterator>, + { + Self { block: None, style: Style::default(), - header, - header_style: Style::default(), widths: &[], column_spacing: 1, - header_gap: 1, highlight_style: Style::default(), highlight_symbol: None, - rows, + header: None, + rows: rows.into_iter().collect(), } } - pub fn block(mut self, block: Block<'a>) -> Table<'a, H, R> { - self.block = Some(block); - self - } - pub fn header(mut self, header: II) -> Table<'a, H, R> - where - II: IntoIterator, - { - self.header = header.into_iter(); + pub fn block(mut self, block: Block<'a>) -> Self { + self.block = Some(block); self } - pub fn header_style(mut self, style: Style) -> Table<'a, H, R> { - self.header_style = style; + pub fn header(mut self, header: Row<'a>) -> Self { + self.header = Some(header); self } - pub fn widths(mut self, widths: &'a [Constraint]) -> Table<'a, H, R> { + pub fn widths(mut self, widths: &'a [Constraint]) -> Self { let between_0_and_100 = |&w| match w { Constraint::Percentage(p) => p <= 100, _ => true, @@ -175,63 +242,27 @@ where self } - pub fn rows(mut self, rows: II) -> Table<'a, H, R> - where - II: IntoIterator, IntoIter = R>, - { - self.rows = rows.into_iter(); - self - } - - pub fn style(mut self, style: Style) -> Table<'a, H, R> { + pub fn style(mut self, style: Style) -> Self { self.style = style; self } - pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> Table<'a, H, R> { + pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> Self { self.highlight_symbol = Some(highlight_symbol); self } - pub fn highlight_style(mut self, highlight_style: Style) -> Table<'a, H, R> { + pub fn highlight_style(mut self, highlight_style: Style) -> Self { self.highlight_style = highlight_style; self } - pub fn column_spacing(mut self, spacing: u16) -> Table<'a, H, R> { + pub fn column_spacing(mut self, spacing: u16) -> Self { self.column_spacing = spacing; self } - pub fn header_gap(mut self, gap: u16) -> Table<'a, H, R> { - self.header_gap = gap; - self - } -} - -impl<'a, H, D, R> StatefulWidget for Table<'a, H, R> -where - H: Iterator, - H::Item: Display, - D: Iterator, - D::Item: Display, - R: Iterator>, -{ - type State = TableState; - - fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { - buf.set_style(area, self.style); - - // Render block if necessary and get the drawing area - let table_area = match self.block.take() { - Some(b) => { - let inner_area = b.inner(area); - b.render(area, buf); - inner_area - } - None => area, - }; - + fn get_columns_widths(&self, max_width: u16, has_selection: bool) -> Vec { let mut solver = Solver::new(); let mut var_indices = HashMap::new(); let mut ccs = Vec::new(); @@ -241,17 +272,24 @@ where variables.push(var); var_indices.insert(var, i); } + let spacing_width = (variables.len() as u16).saturating_sub(1) * self.column_spacing; + let mut available_width = max_width.saturating_sub(spacing_width); + if has_selection { + let highlight_symbol_width = + self.highlight_symbol.map(|s| s.width() as u16).unwrap_or(0); + available_width = available_width.saturating_sub(highlight_symbol_width); + } for (i, constraint) in self.widths.iter().enumerate() { ccs.push(variables[i] | GE(WEAK) | 0.); ccs.push(match *constraint { Constraint::Length(v) => variables[i] | EQ(MEDIUM) | f64::from(v), Constraint::Percentage(v) => { - variables[i] | EQ(WEAK) | (f64::from(v * table_area.width) / 100.0) + variables[i] | EQ(WEAK) | (f64::from(v * available_width) / 100.0) } Constraint::Ratio(n, d) => { variables[i] | EQ(WEAK) - | (f64::from(table_area.width) * f64::from(n) / f64::from(d)) + | (f64::from(available_width) * f64::from(n) / f64::from(d)) } Constraint::Min(v) => variables[i] | GE(WEAK) | f64::from(v), Constraint::Max(v) => variables[i] | LE(WEAK) | f64::from(v), @@ -263,13 +301,11 @@ where .iter() .fold(Expression::from_constant(0.), |acc, v| acc + *v) | LE(REQUIRED) - | f64::from( - area.width - 2 - (self.column_spacing * (variables.len() as u16 - 1)), - ), + | f64::from(available_width), ) .unwrap(); solver.add_constraints(&ccs).unwrap(); - let mut solved_widths = vec![0; variables.len()]; + let mut widths = vec![0; variables.len()]; for &(var, value) in solver.fetch_changes() { let index = var_indices[&var]; let value = if value.is_sign_negative() { @@ -277,81 +313,213 @@ where } else { value.round() as u16 }; - solved_widths[index] = value + widths[index] = value; + } + // Cassowary could still return columns widths greater than the max width when there are + // fixed length constraints that cannot be satisfied. Therefore, we clamp the widths from + // left to right. + let mut available_width = max_width; + for w in &mut widths { + *w = available_width.min(*w); + available_width = available_width + .saturating_sub(*w) + .saturating_sub(self.column_spacing); } + widths + } - let mut y = table_area.top(); - let mut x = table_area.left(); + fn get_row_bounds( + &self, + selected: Option, + offset: usize, + max_height: u16, + ) -> (usize, usize) { + 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.total_height(); + end += 1; + } - // Draw header - if y < table_area.bottom() { - for (w, t) in solved_widths.iter().zip(self.header.by_ref()) { - buf.set_stringn(x, y, format!("{}", t), *w as usize, self.header_style); - x += *w + self.column_spacing; + let selected = selected.unwrap_or(0).min(self.rows.len() - 1); + while selected >= end { + height = height.saturating_add(self.rows[end].total_height()); + end += 1; + while height > max_height { + height = height.saturating_sub(self.rows[start].total_height()); + start += 1; + } + } + while selected < start { + start -= 1; + height = height.saturating_add(self.rows[start].total_height()); + while height > max_height { + end -= 1; + height = height.saturating_sub(self.rows[end].total_height()); } } - y += 1 + self.header_gap; + (start, end) + } +} + +#[derive(Debug, Clone)] +pub struct TableState { + offset: usize, + selected: Option, +} + +impl Default for TableState { + fn default() -> TableState { + TableState { + offset: 0, + selected: None, + } + } +} - // Use highlight_style only if something is selected - let (selected, highlight_style) = match state.selected { - Some(i) => (Some(i), self.highlight_style), - None => (None, self.style), +impl TableState { + pub fn selected(&self) -> Option { + self.selected + } + + pub fn select(&mut self, index: Option) { + self.selected = index; + if index.is_none() { + self.offset = 0; + } + } +} + +impl<'a> StatefulWidget for Table<'a> { + type State = TableState; + + fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { + if area.area() == 0 { + return; + } + buf.set_style(area, self.style); + if self.rows.is_empty() { + return; + } + let table_area = match self.block.take() { + Some(b) => { + let inner_area = b.inner(area); + b.render(area, buf); + inner_area + } + None => area, }; + + let has_selection = state.selected.is_some(); + let columns_widths = self.get_columns_widths(table_area.width, has_selection); let highlight_symbol = self.highlight_symbol.unwrap_or(""); let blank_symbol = iter::repeat(" ") .take(highlight_symbol.width()) .collect::(); + let mut current_height = 0; + let mut rows_height = table_area.height; + + // Draw header + if let Some(ref header) = self.header { + let max_header_height = table_area.height.min(header.total_height()); + buf.set_style( + Rect { + x: table_area.left(), + y: table_area.top(), + width: table_area.width, + height: table_area.height.min(header.height), + }, + header.style, + ); + let mut col = table_area.left(); + if has_selection { + col += (highlight_symbol.width() as u16).min(table_area.width); + } + for (width, cell) in columns_widths.iter().zip(header.cells.iter()) { + render_cell( + buf, + cell, + Rect { + x: col, + y: table_area.top(), + width: *width, + height: max_header_height, + }, + ); + col += *width + self.column_spacing; + } + current_height += max_header_height; + rows_height = rows_height.saturating_sub(max_header_height); + } // Draw rows - let default_style = Style::default(); - if y < table_area.bottom() { - let remaining = (table_area.bottom() - y) as usize; - - // Make sure the table shows the selected item - state.offset = if let Some(selected) = selected { - if selected >= remaining + state.offset - 1 { - selected + 1 - remaining - } else if selected < state.offset { - selected + let (start, end) = self.get_row_bounds(state.selected, state.offset, rows_height); + state.offset = start; + for (i, table_row) in self + .rows + .iter_mut() + .enumerate() + .skip(state.offset) + .take(end - start) + { + let (row, col) = (table_area.top() + current_height, table_area.left()); + current_height += table_row.total_height(); + let table_row_area = Rect { + x: col, + y: row, + width: table_area.width, + height: table_row.height, + }; + buf.set_style(table_row_area, table_row.style); + let is_selected = state.selected.map(|s| s == i).unwrap_or(false); + let table_row_start_col = if has_selection { + let symbol = if is_selected { + highlight_symbol } else { - state.offset - } + &blank_symbol + }; + let (col, _) = + buf.set_stringn(col, row, symbol, table_area.width as usize, table_row.style); + col } else { - 0 + col }; - for (i, row) in self.rows.skip(state.offset).take(remaining).enumerate() { - let (data, style, symbol) = match row { - Row::Data(d) | Row::StyledData(d, _) - if Some(i) == state.selected.map(|s| s - state.offset) => - { - (d, highlight_style, highlight_symbol) - } - Row::Data(d) => (d, default_style, blank_symbol.as_ref()), - Row::StyledData(d, s) => (d, s, blank_symbol.as_ref()), - }; - x = table_area.left(); - for (c, (w, elt)) in solved_widths.iter().zip(data).enumerate() { - let s = if c == 0 { - format!("{}{}", symbol, elt) - } else { - format!("{}", elt) - }; - buf.set_stringn(x, y + i as u16, s, *w as usize, style); - x += *w + self.column_spacing; - } + let mut col = table_row_start_col; + for (width, cell) in columns_widths.iter().zip(table_row.cells.iter()) { + render_cell( + buf, + cell, + Rect { + x: col, + y: row, + width: *width, + height: table_row.height, + }, + ); + col += *width + self.column_spacing; + } + if is_selected { + buf.set_style(table_row_area, self.highlight_style); } } } } -impl<'a, H, D, R> Widget for Table<'a, H, R> -where - H: Iterator, - H::Item: Display, - D: Iterator, - D::Item: Display, - R: Iterator>, -{ +fn render_cell(buf: &mut Buffer, cell: &Cell, area: Rect) { + buf.set_style(area, cell.style); + for (i, spans) in cell.content.lines.iter().enumerate() { + if i as u16 >= area.height { + break; + } + buf.set_spans(area.x, area.y + i as u16, spans, area.width); + } +} + +impl<'a> Widget for Table<'a> { fn render(self, area: Rect, buf: &mut Buffer) { let mut state = TableState::default(); StatefulWidget::render(self, area, buf, &mut state); @@ -365,7 +533,6 @@ mod tests { #[test] #[should_panic] fn table_invalid_percentages() { - Table::new([""].iter(), vec![Row::Data([""].iter())].into_iter()) - .widths(&[Constraint::Percentage(110)]); + Table::new(vec![]).widths(&[Constraint::Percentage(110)]); } } diff --git a/tests/widgets_table.rs b/tests/widgets_table.rs index 2e26cba..1d858c0 100644 --- a/tests/widgets_table.rs +++ b/tests/widgets_table.rs @@ -1,8 +1,10 @@ -use tui::backend::TestBackend; -use tui::buffer::Buffer; -use tui::layout::Constraint; -use tui::widgets::{Block, Borders, Row, Table}; -use tui::Terminal; +use tui::{ + backend::TestBackend, + buffer::Buffer, + layout::Constraint, + widgets::{Block, Borders, Row, Table}, + Terminal, +}; #[test] fn widgets_table_column_spacing_can_be_changed() { @@ -13,16 +15,13 @@ fn widgets_table_column_spacing_can_be_changed() { terminal .draw(|f| { let size = f.size(); - let table = Table::new( - ["Head1", "Head2", "Head3"].iter(), - vec![ - Row::Data(["Row11", "Row12", "Row13"].iter()), - Row::Data(["Row21", "Row22", "Row23"].iter()), - Row::Data(["Row31", "Row32", "Row33"].iter()), - Row::Data(["Row41", "Row42", "Row43"].iter()), - ] - .into_iter(), - ) + let table = Table::new(vec![ + Row::new(vec!["Row11", "Row12", "Row13"]), + Row::new(vec!["Row21", "Row22", "Row23"]), + Row::new(vec!["Row31", "Row32", "Row33"]), + Row::new(vec!["Row41", "Row42", "Row43"]), + ]) + .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1)) .block(Block::default().borders(Borders::ALL)) .widths(&[ Constraint::Length(5), @@ -114,16 +113,13 @@ fn widgets_table_columns_widths_can_use_fixed_length_constraints() { terminal .draw(|f| { let size = f.size(); - let table = Table::new( - ["Head1", "Head2", "Head3"].iter(), - vec![ - Row::Data(["Row11", "Row12", "Row13"].iter()), - Row::Data(["Row21", "Row22", "Row23"].iter()), - Row::Data(["Row31", "Row32", "Row33"].iter()), - Row::Data(["Row41", "Row42", "Row43"].iter()), - ] - .into_iter(), - ) + let table = Table::new(vec![ + Row::new(vec!["Row11", "Row12", "Row13"]), + Row::new(vec!["Row21", "Row22", "Row23"]), + Row::new(vec!["Row31", "Row32", "Row33"]), + Row::new(vec!["Row41", "Row42", "Row43"]), + ]) + .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1)) .block(Block::default().borders(Borders::ALL)) .widths(widths); f.render_widget(table, size); @@ -205,16 +201,13 @@ fn widgets_table_columns_widths_can_use_percentage_constraints() { terminal .draw(|f| { let size = f.size(); - let table = Table::new( - ["Head1", "Head2", "Head3"].iter(), - vec![ - Row::Data(["Row11", "Row12", "Row13"].iter()), - Row::Data(["Row21", "Row22", "Row23"].iter()), - Row::Data(["Row31", "Row32", "Row33"].iter()), - Row::Data(["Row41", "Row42", "Row43"].iter()), - ] - .into_iter(), - ) + let table = Table::new(vec![ + Row::new(vec!["Row11", "Row12", "Row13"]), + Row::new(vec!["Row21", "Row22", "Row23"]), + Row::new(vec!["Row31", "Row32", "Row33"]), + Row::new(vec!["Row41", "Row42", "Row43"]), + ]) + .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1)) .block(Block::default().borders(Borders::ALL)) .widths(widths) .column_spacing(0); @@ -314,16 +307,13 @@ fn widgets_table_columns_widths_can_use_mixed_constraints() { terminal .draw(|f| { let size = f.size(); - let table = Table::new( - ["Head1", "Head2", "Head3"].iter(), - vec![ - Row::Data(["Row11", "Row12", "Row13"].iter()), - Row::Data(["Row21", "Row22", "Row23"].iter()), - Row::Data(["Row31", "Row32", "Row33"].iter()), - Row::Data(["Row41", "Row42", "Row43"].iter()), - ] - .into_iter(), - ) + let table = Table::new(vec![ + Row::new(vec!["Row11", "Row12", "Row13"]), + Row::new(vec!["Row21", "Row22", "Row23"]), + Row::new(vec!["Row31", "Row32", "Row33"]), + Row::new(vec!["Row41", "Row42", "Row43"]), + ]) + .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1)) .block(Block::default().borders(Borders::ALL)) .widths(widths); f.render_widget(table, size); @@ -426,16 +416,13 @@ fn widgets_table_columns_widths_can_use_ratio_constraints() { terminal .draw(|f| { let size = f.size(); - let table = Table::new( - ["Head1", "Head2", "Head3"].iter(), - vec![ - Row::Data(["Row11", "Row12", "Row13"].iter()), - Row::Data(["Row21", "Row22", "Row23"].iter()), - Row::Data(["Row31", "Row32", "Row33"].iter()), - Row::Data(["Row41", "Row42", "Row43"].iter()), - ] - .into_iter(), - ) + let table = Table::new(vec![ + Row::new(vec!["Row11", "Row12", "Row13"]), + Row::new(vec!["Row21", "Row22", "Row23"]), + Row::new(vec!["Row31", "Row32", "Row33"]), + Row::new(vec!["Row41", "Row42", "Row43"]), + ]) + .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1)) .block(Block::default().borders(Borders::ALL)) .widths(widths) .column_spacing(0);