mirror of
https://github.com/ratatui-org/ratatui
synced 2025-02-16 14:08:44 +00:00
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.
This commit is contained in:
parent
23a9280db7
commit
5ea54792c0
6 changed files with 525 additions and 358 deletions
|
@ -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),
|
||||
|
|
|
@ -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<dyn Error>> {
|
|||
.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(">> ")
|
||||
|
|
|
@ -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<Spans<'a>>,
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -2,6 +2,7 @@ use crate::{
|
|||
buffer::Buffer,
|
||||
layout::{Constraint, Rect},
|
||||
style::Style,
|
||||
text::Text,
|
||||
widgets::{Block, StatefulWidget, Widget},
|
||||
};
|
||||
use cassowary::{
|
||||
|
@ -11,11 +12,360 @@ use cassowary::{
|
|||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::Display,
|
||||
iter::{self, Iterator},
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
/// 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<'a> Cell<'a> {
|
||||
/// Set the `Style` of this cell.
|
||||
pub fn style(mut self, style: Style) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> From<T> for Cell<'a>
|
||||
where
|
||||
T: Into<Text<'a>>,
|
||||
{
|
||||
fn from(content: T) -> Cell<'a> {
|
||||
Cell {
|
||||
content: content.into(),
|
||||
style: Style::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<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,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// 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, 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, PartialEq)]
|
||||
pub struct Table<'a> {
|
||||
/// A block to wrap the widget in
|
||||
block: Option<Block<'a>>,
|
||||
/// Base style for the widget
|
||||
style: Style,
|
||||
/// Width constraints for each column
|
||||
widths: &'a [Constraint],
|
||||
/// Space between each column
|
||||
column_spacing: 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<Row<'a>>,
|
||||
/// Data to display in each row
|
||||
rows: Vec<Row<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Table<'a> {
|
||||
pub fn new<T>(rows: T) -> Self
|
||||
where
|
||||
T: IntoIterator<Item = Row<'a>>,
|
||||
{
|
||||
Self {
|
||||
block: None,
|
||||
style: Style::default(),
|
||||
widths: &[],
|
||||
column_spacing: 1,
|
||||
highlight_style: Style::default(),
|
||||
highlight_symbol: None,
|
||||
header: None,
|
||||
rows: rows.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn block(mut self, block: Block<'a>) -> Self {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn header(mut self, header: Row<'a>) -> Self {
|
||||
self.header = Some(header);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn widths(mut self, widths: &'a [Constraint]) -> Self {
|
||||
let between_0_and_100 = |&w| match w {
|
||||
Constraint::Percentage(p) => p <= 100,
|
||||
_ => true,
|
||||
};
|
||||
assert!(
|
||||
widths.iter().all(between_0_and_100),
|
||||
"Percentages should be between 0 and 100 inclusively."
|
||||
);
|
||||
self.widths = widths;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
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) -> Self {
|
||||
self.highlight_style = highlight_style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn column_spacing(mut self, spacing: u16) -> Self {
|
||||
self.column_spacing = spacing;
|
||||
self
|
||||
}
|
||||
|
||||
fn get_columns_widths(&self, max_width: u16, has_selection: bool) -> Vec<u16> {
|
||||
let mut solver = Solver::new();
|
||||
let mut var_indices = HashMap::new();
|
||||
let mut ccs = Vec::new();
|
||||
let mut variables = Vec::new();
|
||||
for i in 0..self.widths.len() {
|
||||
let var = cassowary::Variable::new();
|
||||
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 * available_width) / 100.0)
|
||||
}
|
||||
Constraint::Ratio(n, d) => {
|
||||
variables[i]
|
||||
| EQ(WEAK)
|
||||
| (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),
|
||||
})
|
||||
}
|
||||
solver
|
||||
.add_constraint(
|
||||
variables
|
||||
.iter()
|
||||
.fold(Expression::from_constant(0.), |acc, v| acc + *v)
|
||||
| LE(REQUIRED)
|
||||
| f64::from(available_width),
|
||||
)
|
||||
.unwrap();
|
||||
solver.add_constraints(&ccs).unwrap();
|
||||
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() {
|
||||
0
|
||||
} else {
|
||||
value.round() as u16
|
||||
};
|
||||
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
|
||||
}
|
||||
|
||||
fn get_row_bounds(
|
||||
&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;
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
(start, end)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TableState {
|
||||
offset: usize,
|
||||
|
@ -44,185 +394,17 @@ impl TableState {
|
|||
}
|
||||
}
|
||||
|
||||
/// Holds data to be displayed in a Table widget
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Row<D>
|
||||
where
|
||||
D: Iterator,
|
||||
D::Item: Display,
|
||||
{
|
||||
Data(D),
|
||||
StyledData(D, Style),
|
||||
}
|
||||
|
||||
/// A widget to display data in formatted columns
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use tui::widgets::{Block, Borders, Table, Row};
|
||||
/// # 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);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Table<'a, H, R> {
|
||||
/// A block to wrap the widget in
|
||||
block: Option<Block<'a>>,
|
||||
/// 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>,
|
||||
/// Data to display in each row
|
||||
rows: R,
|
||||
}
|
||||
|
||||
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<Item = Row<D>>,
|
||||
{
|
||||
pub fn new(header: H, rows: R) -> Table<'a, H, R> {
|
||||
Table {
|
||||
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,
|
||||
}
|
||||
}
|
||||
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>
|
||||
where
|
||||
II: IntoIterator<Item = H::Item, IntoIter = H>,
|
||||
{
|
||||
self.header = header.into_iter();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn header_style(mut self, style: Style) -> Table<'a, H, R> {
|
||||
self.header_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn widths(mut self, widths: &'a [Constraint]) -> Table<'a, H, R> {
|
||||
let between_0_and_100 = |&w| match w {
|
||||
Constraint::Percentage(p) => p <= 100,
|
||||
_ => true,
|
||||
};
|
||||
assert!(
|
||||
widths.iter().all(between_0_and_100),
|
||||
"Percentages should be between 0 and 100 inclusively."
|
||||
);
|
||||
self.widths = widths;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn rows<II>(mut self, rows: II) -> Table<'a, H, R>
|
||||
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
|
||||
}
|
||||
|
||||
pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> Table<'a, H, R> {
|
||||
self.highlight_symbol = Some(highlight_symbol);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn highlight_style(mut self, highlight_style: Style) -> Table<'a, H, R> {
|
||||
self.highlight_style = highlight_style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn column_spacing(mut self, spacing: u16) -> Table<'a, H, R> {
|
||||
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<Item = Row<D>>,
|
||||
{
|
||||
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);
|
||||
|
||||
// Render block if necessary and get the drawing area
|
||||
if self.rows.is_empty() {
|
||||
return;
|
||||
}
|
||||
let table_area = match self.block.take() {
|
||||
Some(b) => {
|
||||
let inner_area = b.inner(area);
|
||||
|
@ -232,126 +414,112 @@ where
|
|||
None => area,
|
||||
};
|
||||
|
||||
let mut solver = Solver::new();
|
||||
let mut var_indices = HashMap::new();
|
||||
let mut ccs = Vec::new();
|
||||
let mut variables = Vec::new();
|
||||
for i in 0..self.widths.len() {
|
||||
let var = cassowary::Variable::new();
|
||||
variables.push(var);
|
||||
var_indices.insert(var, i);
|
||||
}
|
||||
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)
|
||||
}
|
||||
Constraint::Ratio(n, d) => {
|
||||
variables[i]
|
||||
| EQ(WEAK)
|
||||
| (f64::from(table_area.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),
|
||||
})
|
||||
}
|
||||
solver
|
||||
.add_constraint(
|
||||
variables
|
||||
.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)),
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
solver.add_constraints(&ccs).unwrap();
|
||||
let mut solved_widths = vec![0; variables.len()];
|
||||
for &(var, value) in solver.fetch_changes() {
|
||||
let index = var_indices[&var];
|
||||
let value = if value.is_sign_negative() {
|
||||
0
|
||||
} else {
|
||||
value.round() as u16
|
||||
};
|
||||
solved_widths[index] = value
|
||||
}
|
||||
|
||||
let mut y = table_area.top();
|
||||
let mut x = table_area.left();
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
y += 1 + self.header_gap;
|
||||
|
||||
// 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),
|
||||
};
|
||||
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::<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
|
||||
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
|
||||
} else {
|
||||
state.offset
|
||||
}
|
||||
} else {
|
||||
0
|
||||
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,
|
||||
};
|
||||
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()),
|
||||
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 {
|
||||
&blank_symbol
|
||||
};
|
||||
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 (col, _) =
|
||||
buf.set_stringn(col, row, symbol, table_area.width as usize, table_row.style);
|
||||
col
|
||||
} else {
|
||||
col
|
||||
};
|
||||
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<Item = Row<D>>,
|
||||
{
|
||||
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)]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Reference in a new issue