refactor(widgets/table): more flexible table

- control over the style of each cell and its content using the styling capabilities of Text.
- rows with multiple lines.
- fix panics on small areas.
- less generic type parameters.
pull/425/head
Florian Dehau 4 years ago
parent 23a9280db7
commit 5ea54792c0

@ -308,18 +308,20 @@ where
let failure_style = Style::default() let failure_style = Style::default()
.fg(Color::Red) .fg(Color::Red)
.add_modifier(Modifier::RAPID_BLINK | Modifier::CROSSED_OUT); .add_modifier(Modifier::RAPID_BLINK | Modifier::CROSSED_OUT);
let header = ["Server", "Location", "Status"];
let rows = app.servers.iter().map(|s| { let rows = app.servers.iter().map(|s| {
let style = if s.status == "Up" { let style = if s.status == "Up" {
up_style up_style
} else { } else {
failure_style 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)) .block(Block::default().title("Servers").borders(Borders::ALL))
.header_style(Style::default().fg(Color::Yellow))
.widths(&[ .widths(&[
Constraint::Length(15), Constraint::Length(15),
Constraint::Length(15), Constraint::Length(15),

@ -8,7 +8,7 @@ use tui::{
backend::TermionBackend, backend::TermionBackend,
layout::{Constraint, Layout}, layout::{Constraint, Layout},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
widgets::{Block, Borders, Row, Table, TableState}, widgets::{Block, Borders, Cell, Row, Table, TableState},
Terminal, Terminal,
}; };
@ -27,7 +27,7 @@ impl<'a> StatefulTable<'a> {
vec!["Row31", "Row32", "Row33"], vec!["Row31", "Row32", "Row33"],
vec!["Row41", "Row42", "Row43"], vec!["Row41", "Row42", "Row43"],
vec!["Row51", "Row52", "Row53"], vec!["Row51", "Row52", "Row53"],
vec!["Row61", "Row62", "Row63"], vec!["Row61", "Row62\nTest", "Row63"],
vec!["Row71", "Row72", "Row73"], vec!["Row71", "Row72", "Row73"],
vec!["Row81", "Row82", "Row83"], vec!["Row81", "Row82", "Row83"],
vec!["Row91", "Row92", "Row93"], vec!["Row91", "Row92", "Row93"],
@ -93,16 +93,27 @@ fn main() -> Result<(), Box<dyn Error>> {
.margin(5) .margin(5)
.split(f.size()); .split(f.size());
let selected_style = Style::default() let selected_style = Style::default().add_modifier(Modifier::REVERSED);
.fg(Color::Yellow) let normal_style = Style::default().bg(Color::Blue);
.add_modifier(Modifier::BOLD); let header_cells = ["Header1", "Header2", "Header3"]
let normal_style = Style::default().fg(Color::White);
let header = ["Header1", "Header2", "Header3"];
let rows = table
.items
.iter() .iter()
.map(|i| Row::StyledData(i.iter(), normal_style)); .map(|h| Cell::from(*h).style(Style::default().fg(Color::Red)));
let t = Table::new(header.iter(), rows) 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")) .block(Block::default().borders(Borders::ALL).title("Table"))
.highlight_style(selected_style) .highlight_style(selected_style)
.highlight_symbol(">> ") .highlight_symbol(">> ")

@ -7,7 +7,7 @@ use crate::{
widgets::{Borders, Widget}, widgets::{Borders, Widget},
}; };
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy, PartialEq)]
pub enum BorderType { pub enum BorderType {
Plain, Plain,
Rounded, Rounded,
@ -41,7 +41,7 @@ impl BorderType {
/// .border_type(BorderType::Rounded) /// .border_type(BorderType::Rounded)
/// .style(Style::default().bg(Color::Black)); /// .style(Style::default().bg(Color::Black));
/// ``` /// ```
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq)]
pub struct Block<'a> { pub struct Block<'a> {
/// Optional title place on the upper left of the block /// Optional title place on the upper left of the block
title: Option<Spans<'a>>, title: Option<Spans<'a>>,

@ -36,7 +36,7 @@ pub use self::gauge::{Gauge, LineGauge};
pub use self::list::{List, ListItem, ListState}; pub use self::list::{List, ListItem, ListState};
pub use self::paragraph::{Paragraph, Wrap}; pub use self::paragraph::{Paragraph, Wrap};
pub use self::sparkline::Sparkline; 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; pub use self::tabs::Tabs;
use crate::{buffer::Buffer, layout::Rect}; use crate::{buffer::Buffer, layout::Rect};

@ -2,6 +2,7 @@ use crate::{
buffer::Buffer, buffer::Buffer,
layout::{Constraint, Rect}, layout::{Constraint, Rect},
style::Style, style::Style,
text::Text,
widgets::{Block, StatefulWidget, Widget}, widgets::{Block, StatefulWidget, Widget},
}; };
use cassowary::{ use cassowary::{
@ -11,158 +12,224 @@ use cassowary::{
}; };
use std::{ use std::{
collections::HashMap, collections::HashMap,
fmt::Display,
iter::{self, Iterator}, iter::{self, Iterator},
}; };
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
#[derive(Debug, Clone)] /// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`].
pub struct TableState { ///
offset: usize, /// It can be created from anything that can be converted to a [`Text`].
selected: Option<usize>, /// ```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 { impl<'a> Cell<'a> {
fn default() -> TableState { /// Set the `Style` of this cell.
TableState { pub fn style(mut self, style: Style) -> Self {
offset: 0, self.style = style;
selected: None, self
}
} }
} }
impl TableState { impl<'a, T> From<T> for Cell<'a>
pub fn selected(&self) -> Option<usize> { where
self.selected T: Into<Text<'a>>,
{
fn from(content: T) -> Cell<'a> {
Cell {
content: content.into(),
style: Style::default(),
}
} }
}
pub fn select(&mut self, index: Option<usize>) { /// Holds data to be displayed in a [`Table`] widget.
self.selected = index; ///
if index.is_none() { /// A [`Row`] is a collection of cells. It can be created from simple strings:
self.offset = 0; /// ```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<Cell<'a>>,
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<T>(cells: T) -> Self
where
T: IntoIterator,
T::Item: Into<Cell<'a>>,
{
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 /// Set the fixed height of the [`Row`]. Any [`Cell`] whose content has more lines than this
#[derive(Debug, Clone)] /// height will see its content truncated.
pub enum Row<D> pub fn height(mut self, height: u16) -> Self {
where self.height = height;
D: Iterator, self
D::Item: Display, }
{
Data(D), /// Set the [`Style`] of the entire row. This [`Style`] can be overriden by the [`Style`] of a
StyledData(D, Style), /// 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 /// A widget to display data in formatted columns.
///
/// # Examples
/// ///
/// ``` /// It is a collection of [`Row`]s, themselves composed of [`Cell`]s:
/// # use tui::widgets::{Block, Borders, Table, Row}; /// ```rust
/// # use tui::widgets::{Block, Borders, Table, Row, Cell};
/// # use tui::layout::Constraint; /// # use tui::layout::Constraint;
/// # use tui::style::{Style, Color}; /// # use tui::style::{Style, Color, Modifier};
/// let row_style = Style::default().fg(Color::White); /// # use tui::text::{Text, Spans, Span};
/// Table::new( /// Table::new(vec![
/// ["Col1", "Col2", "Col3"].into_iter(), /// // Row can be created from simple strings.
/// vec![ /// Row::new(vec!["Row11", "Row12", "Row13"]),
/// Row::StyledData(["Row11", "Row12", "Row13"].into_iter(), row_style), /// // You can style the entire row.
/// Row::StyledData(["Row21", "Row22", "Row23"].into_iter(), row_style), /// Row::new(vec!["Row21", "Row22", "Row23"]).style(Style::default().fg(Color::Blue)),
/// Row::StyledData(["Row31", "Row32", "Row33"].into_iter(), row_style), /// // If you need more control over the styling you may need to create Cells directly
/// Row::Data(["Row41", "Row42", "Row43"].into_iter()) /// Row::new(vec![
/// ].into_iter() /// Cell::from("Row31"),
/// ) /// Cell::from("Row32").style(Style::default().fg(Color::Yellow)),
/// .block(Block::default().title("Table")) /// Cell::from(Spans::from(vec![
/// .header_style(Style::default().fg(Color::Yellow)) /// Span::raw("Row"),
/// .widths(&[Constraint::Length(5), Constraint::Length(5), Constraint::Length(10)]) /// Span::styled("33", Style::default().fg(Color::Green))
/// .style(Style::default().fg(Color::White)) /// ])),
/// .column_spacing(1); /// ]),
/// // 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)] #[derive(Debug, Clone, PartialEq)]
pub struct Table<'a, H, R> { pub struct Table<'a> {
/// A block to wrap the widget in /// A block to wrap the widget in
block: Option<Block<'a>>, block: Option<Block<'a>>,
/// Base style for the widget /// Base style for the widget
style: Style, style: Style,
/// Header row for all columns
header: H,
/// Style for the header
header_style: Style,
/// Width constraints for each column /// Width constraints for each column
widths: &'a [Constraint], widths: &'a [Constraint],
/// Space between each column /// Space between each column
column_spacing: u16, column_spacing: u16,
/// Space between the header and the rows
header_gap: u16,
/// Style used to render the selected row /// Style used to render the selected row
highlight_style: Style, highlight_style: Style,
/// Symbol in front of the selected rom /// Symbol in front of the selected rom
highlight_symbol: Option<&'a str>, highlight_symbol: Option<&'a str>,
/// Optional header
header: Option<Row<'a>>,
/// Data to display in each row /// Data to display in each row
rows: R, rows: Vec<Row<'a>>,
} }
impl<'a, H, R> Default for Table<'a, H, R> impl<'a> Table<'a> {
where pub fn new<T>(rows: T) -> Self
H: Iterator + Default, where
R: Iterator + Default, T: IntoIterator<Item = Row<'a>>,
{ {
fn default() -> Table<'a, H, R> { Self {
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<Item = Row<D>>,
{
pub fn new(header: H, rows: R) -> Table<'a, H, R> {
Table {
block: None, block: None,
style: Style::default(), style: Style::default(),
header,
header_style: Style::default(),
widths: &[], widths: &[],
column_spacing: 1, column_spacing: 1,
header_gap: 1,
highlight_style: Style::default(), highlight_style: Style::default(),
highlight_symbol: None, 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<II>(mut self, header: II) -> Table<'a, H, R> pub fn block(mut self, block: Block<'a>) -> Self {
where self.block = Some(block);
II: IntoIterator<Item = H::Item, IntoIter = H>,
{
self.header = header.into_iter();
self self
} }
pub fn header_style(mut self, style: Style) -> Table<'a, H, R> { pub fn header(mut self, header: Row<'a>) -> Self {
self.header_style = style; self.header = Some(header);
self 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 { let between_0_and_100 = |&w| match w {
Constraint::Percentage(p) => p <= 100, Constraint::Percentage(p) => p <= 100,
_ => true, _ => true,
@ -175,63 +242,27 @@ where
self self
} }
pub fn rows<II>(mut self, rows: II) -> Table<'a, H, R> pub fn style(mut self, style: Style) -> Self {
where
II: IntoIterator<Item = Row<D>, IntoIter = R>,
{
self.rows = rows.into_iter();
self
}
pub fn style(mut self, style: Style) -> Table<'a, H, R> {
self.style = style; self.style = style;
self 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.highlight_symbol = Some(highlight_symbol);
self 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.highlight_style = highlight_style;
self 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.column_spacing = spacing;
self self
} }
pub fn header_gap(mut self, gap: u16) -> Table<'a, H, R> { fn get_columns_widths(&self, max_width: u16, has_selection: bool) -> Vec<u16> {
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<Item = Row<D>>,
{
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,
};
let mut solver = Solver::new(); let mut solver = Solver::new();
let mut var_indices = HashMap::new(); let mut var_indices = HashMap::new();
let mut ccs = Vec::new(); let mut ccs = Vec::new();
@ -241,17 +272,24 @@ where
variables.push(var); variables.push(var);
var_indices.insert(var, i); 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() { for (i, constraint) in self.widths.iter().enumerate() {
ccs.push(variables[i] | GE(WEAK) | 0.); ccs.push(variables[i] | GE(WEAK) | 0.);
ccs.push(match *constraint { ccs.push(match *constraint {
Constraint::Length(v) => variables[i] | EQ(MEDIUM) | f64::from(v), Constraint::Length(v) => variables[i] | EQ(MEDIUM) | f64::from(v),
Constraint::Percentage(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) => { Constraint::Ratio(n, d) => {
variables[i] variables[i]
| EQ(WEAK) | 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::Min(v) => variables[i] | GE(WEAK) | f64::from(v),
Constraint::Max(v) => variables[i] | LE(WEAK) | f64::from(v), Constraint::Max(v) => variables[i] | LE(WEAK) | f64::from(v),
@ -263,13 +301,11 @@ where
.iter() .iter()
.fold(Expression::from_constant(0.), |acc, v| acc + *v) .fold(Expression::from_constant(0.), |acc, v| acc + *v)
| LE(REQUIRED) | LE(REQUIRED)
| f64::from( | f64::from(available_width),
area.width - 2 - (self.column_spacing * (variables.len() as u16 - 1)),
),
) )
.unwrap(); .unwrap();
solver.add_constraints(&ccs).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() { for &(var, value) in solver.fetch_changes() {
let index = var_indices[&var]; let index = var_indices[&var];
let value = if value.is_sign_negative() { let value = if value.is_sign_negative() {
@ -277,81 +313,213 @@ where
} else { } else {
value.round() as u16 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(); fn get_row_bounds(
let mut x = table_area.left(); &self,
selected: Option<usize>,
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 let selected = selected.unwrap_or(0).min(self.rows.len() - 1);
if y < table_area.bottom() { while selected >= end {
for (w, t) in solved_widths.iter().zip(self.header.by_ref()) { height = height.saturating_add(self.rows[end].total_height());
buf.set_stringn(x, y, format!("{}", t), *w as usize, self.header_style); end += 1;
x += *w + self.column_spacing; 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<usize>,
}
impl Default for TableState {
fn default() -> TableState {
TableState {
offset: 0,
selected: None,
}
}
}
// Use highlight_style only if something is selected impl TableState {
let (selected, highlight_style) = match state.selected { pub fn selected(&self) -> Option<usize> {
Some(i) => (Some(i), self.highlight_style), self.selected
None => (None, self.style), }
pub fn select(&mut self, index: Option<usize>) {
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 highlight_symbol = self.highlight_symbol.unwrap_or("");
let blank_symbol = iter::repeat(" ") let blank_symbol = iter::repeat(" ")
.take(highlight_symbol.width()) .take(highlight_symbol.width())
.collect::<String>(); .collect::<String>();
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 // Draw rows
let default_style = Style::default(); let (start, end) = self.get_row_bounds(state.selected, state.offset, rows_height);
if y < table_area.bottom() { state.offset = start;
let remaining = (table_area.bottom() - y) as usize; for (i, table_row) in self
.rows
// Make sure the table shows the selected item .iter_mut()
state.offset = if let Some(selected) = selected { .enumerate()
if selected >= remaining + state.offset - 1 { .skip(state.offset)
selected + 1 - remaining .take(end - start)
} else if selected < state.offset { {
selected 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 { } else {
state.offset &blank_symbol
} };
let (col, _) =
buf.set_stringn(col, row, symbol, table_area.width as usize, table_row.style);
col
} else { } else {
0 col
}; };
for (i, row) in self.rows.skip(state.offset).take(remaining).enumerate() { let mut col = table_row_start_col;
let (data, style, symbol) = match row { for (width, cell) in columns_widths.iter().zip(table_row.cells.iter()) {
Row::Data(d) | Row::StyledData(d, _) render_cell(
if Some(i) == state.selected.map(|s| s - state.offset) => buf,
{ cell,
(d, highlight_style, highlight_symbol) Rect {
} x: col,
Row::Data(d) => (d, default_style, blank_symbol.as_ref()), y: row,
Row::StyledData(d, s) => (d, s, blank_symbol.as_ref()), width: *width,
}; height: table_row.height,
x = table_area.left(); },
for (c, (w, elt)) in solved_widths.iter().zip(data).enumerate() { );
let s = if c == 0 { col += *width + self.column_spacing;
format!("{}{}", symbol, elt) }
} else { if is_selected {
format!("{}", elt) buf.set_style(table_row_area, self.highlight_style);
};
buf.set_stringn(x, y + i as u16, s, *w as usize, style);
x += *w + self.column_spacing;
}
} }
} }
} }
} }
impl<'a, H, D, R> Widget for Table<'a, H, R> fn render_cell(buf: &mut Buffer, cell: &Cell, area: Rect) {
where buf.set_style(area, cell.style);
H: Iterator, for (i, spans) in cell.content.lines.iter().enumerate() {
H::Item: Display, if i as u16 >= area.height {
D: Iterator, break;
D::Item: Display, }
R: Iterator<Item = Row<D>>, 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) { fn render(self, area: Rect, buf: &mut Buffer) {
let mut state = TableState::default(); let mut state = TableState::default();
StatefulWidget::render(self, area, buf, &mut state); StatefulWidget::render(self, area, buf, &mut state);
@ -365,7 +533,6 @@ mod tests {
#[test] #[test]
#[should_panic] #[should_panic]
fn table_invalid_percentages() { fn table_invalid_percentages() {
Table::new([""].iter(), vec![Row::Data([""].iter())].into_iter()) Table::new(vec![]).widths(&[Constraint::Percentage(110)]);
.widths(&[Constraint::Percentage(110)]);
} }
} }

@ -1,8 +1,10 @@
use tui::backend::TestBackend; use tui::{
use tui::buffer::Buffer; backend::TestBackend,
use tui::layout::Constraint; buffer::Buffer,
use tui::widgets::{Block, Borders, Row, Table}; layout::Constraint,
use tui::Terminal; widgets::{Block, Borders, Row, Table},
Terminal,
};
#[test] #[test]
fn widgets_table_column_spacing_can_be_changed() { fn widgets_table_column_spacing_can_be_changed() {
@ -13,16 +15,13 @@ fn widgets_table_column_spacing_can_be_changed() {
terminal terminal
.draw(|f| { .draw(|f| {
let size = f.size(); let size = f.size();
let table = Table::new( let table = Table::new(vec![
["Head1", "Head2", "Head3"].iter(), Row::new(vec!["Row11", "Row12", "Row13"]),
vec![ Row::new(vec!["Row21", "Row22", "Row23"]),
Row::Data(["Row11", "Row12", "Row13"].iter()), Row::new(vec!["Row31", "Row32", "Row33"]),
Row::Data(["Row21", "Row22", "Row23"].iter()), Row::new(vec!["Row41", "Row42", "Row43"]),
Row::Data(["Row31", "Row32", "Row33"].iter()), ])
Row::Data(["Row41", "Row42", "Row43"].iter()), .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
]
.into_iter(),
)
.block(Block::default().borders(Borders::ALL)) .block(Block::default().borders(Borders::ALL))
.widths(&[ .widths(&[
Constraint::Length(5), Constraint::Length(5),
@ -114,16 +113,13 @@ fn widgets_table_columns_widths_can_use_fixed_length_constraints() {
terminal terminal
.draw(|f| { .draw(|f| {
let size = f.size(); let size = f.size();
let table = Table::new( let table = Table::new(vec![
["Head1", "Head2", "Head3"].iter(), Row::new(vec!["Row11", "Row12", "Row13"]),
vec![ Row::new(vec!["Row21", "Row22", "Row23"]),
Row::Data(["Row11", "Row12", "Row13"].iter()), Row::new(vec!["Row31", "Row32", "Row33"]),
Row::Data(["Row21", "Row22", "Row23"].iter()), Row::new(vec!["Row41", "Row42", "Row43"]),
Row::Data(["Row31", "Row32", "Row33"].iter()), ])
Row::Data(["Row41", "Row42", "Row43"].iter()), .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
]
.into_iter(),
)
.block(Block::default().borders(Borders::ALL)) .block(Block::default().borders(Borders::ALL))
.widths(widths); .widths(widths);
f.render_widget(table, size); f.render_widget(table, size);
@ -205,16 +201,13 @@ fn widgets_table_columns_widths_can_use_percentage_constraints() {
terminal terminal
.draw(|f| { .draw(|f| {
let size = f.size(); let size = f.size();
let table = Table::new( let table = Table::new(vec![
["Head1", "Head2", "Head3"].iter(), Row::new(vec!["Row11", "Row12", "Row13"]),
vec![ Row::new(vec!["Row21", "Row22", "Row23"]),
Row::Data(["Row11", "Row12", "Row13"].iter()), Row::new(vec!["Row31", "Row32", "Row33"]),
Row::Data(["Row21", "Row22", "Row23"].iter()), Row::new(vec!["Row41", "Row42", "Row43"]),
Row::Data(["Row31", "Row32", "Row33"].iter()), ])
Row::Data(["Row41", "Row42", "Row43"].iter()), .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
]
.into_iter(),
)
.block(Block::default().borders(Borders::ALL)) .block(Block::default().borders(Borders::ALL))
.widths(widths) .widths(widths)
.column_spacing(0); .column_spacing(0);
@ -314,16 +307,13 @@ fn widgets_table_columns_widths_can_use_mixed_constraints() {
terminal terminal
.draw(|f| { .draw(|f| {
let size = f.size(); let size = f.size();
let table = Table::new( let table = Table::new(vec![
["Head1", "Head2", "Head3"].iter(), Row::new(vec!["Row11", "Row12", "Row13"]),
vec![ Row::new(vec!["Row21", "Row22", "Row23"]),
Row::Data(["Row11", "Row12", "Row13"].iter()), Row::new(vec!["Row31", "Row32", "Row33"]),
Row::Data(["Row21", "Row22", "Row23"].iter()), Row::new(vec!["Row41", "Row42", "Row43"]),
Row::Data(["Row31", "Row32", "Row33"].iter()), ])
Row::Data(["Row41", "Row42", "Row43"].iter()), .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
]
.into_iter(),
)
.block(Block::default().borders(Borders::ALL)) .block(Block::default().borders(Borders::ALL))
.widths(widths); .widths(widths);
f.render_widget(table, size); f.render_widget(table, size);
@ -426,16 +416,13 @@ fn widgets_table_columns_widths_can_use_ratio_constraints() {
terminal terminal
.draw(|f| { .draw(|f| {
let size = f.size(); let size = f.size();
let table = Table::new( let table = Table::new(vec![
["Head1", "Head2", "Head3"].iter(), Row::new(vec!["Row11", "Row12", "Row13"]),
vec![ Row::new(vec!["Row21", "Row22", "Row23"]),
Row::Data(["Row11", "Row12", "Row13"].iter()), Row::new(vec!["Row31", "Row32", "Row33"]),
Row::Data(["Row21", "Row22", "Row23"].iter()), Row::new(vec!["Row41", "Row42", "Row43"]),
Row::Data(["Row31", "Row32", "Row33"].iter()), ])
Row::Data(["Row41", "Row42", "Row43"].iter()), .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
]
.into_iter(),
)
.block(Block::default().borders(Borders::ALL)) .block(Block::default().borders(Borders::ALL))
.widths(widths) .widths(widths)
.column_spacing(0); .column_spacing(0);

Loading…
Cancel
Save