feat(widgets/table): allow one row to be selected

This commit is contained in:
Florian Dehau 2020-03-13 00:30:37 +01:00
parent 140db9b2e2
commit ae677099d6
3 changed files with 189 additions and 63 deletions

View file

@ -1,28 +1,26 @@
#[allow(dead_code)] #[allow(dead_code)]
mod util; mod util;
use std::io;
use termion::event::Key;
use termion::input::MouseTerminal;
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use tui::backend::TermionBackend;
use tui::layout::{Constraint, Layout};
use tui::style::{Color, Modifier, Style};
use tui::widgets::{Block, Borders, Row, Table};
use tui::Terminal;
use crate::util::event::{Event, Events}; use crate::util::event::{Event, Events};
use std::io;
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{
backend::TermionBackend,
layout::{Constraint, Layout},
style::{Color, Modifier, Style},
widgets::{Block, Borders, Row, Table, TableState},
Terminal,
};
struct App<'a> { pub struct StatefulTable<'a> {
state: TableState,
items: Vec<Vec<&'a str>>, items: Vec<Vec<&'a str>>,
selected: usize,
} }
impl<'a> App<'a> { impl<'a> StatefulTable<'a> {
fn new() -> App<'a> { fn new() -> StatefulTable<'a> {
App { StatefulTable {
state: TableState::default(),
items: vec![ items: vec![
vec!["Row11", "Row12", "Row13"], vec!["Row11", "Row12", "Row13"],
vec!["Row21", "Row22", "Row23"], vec!["Row21", "Row22", "Row23"],
@ -30,10 +28,49 @@ impl<'a> App<'a> {
vec!["Row41", "Row42", "Row43"], vec!["Row41", "Row42", "Row43"],
vec!["Row51", "Row52", "Row53"], vec!["Row51", "Row52", "Row53"],
vec!["Row61", "Row62", "Row63"], vec!["Row61", "Row62", "Row63"],
vec!["Row71", "Row72", "Row73"],
vec!["Row81", "Row82", "Row83"],
vec!["Row91", "Row92", "Row93"],
vec!["Row101", "Row102", "Row103"],
vec!["Row111", "Row112", "Row113"],
vec!["Row121", "Row122", "Row123"],
vec!["Row131", "Row132", "Row133"],
vec!["Row141", "Row142", "Row143"],
vec!["Row151", "Row152", "Row153"],
vec!["Row161", "Row162", "Row163"],
vec!["Row171", "Row172", "Row173"],
vec!["Row181", "Row182", "Row183"],
vec!["Row191", "Row192", "Row193"],
], ],
selected: 0,
} }
} }
pub fn next(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i >= self.items.len() - 1 {
0
} else {
i + 1
}
}
None => 0,
};
self.state.select(Some(i));
}
pub fn previous(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i == 0 {
self.items.len() - 1
} else {
i - 1
}
}
None => 0,
};
self.state.select(Some(i));
}
} }
fn main() -> Result<(), failure::Error> { fn main() -> Result<(), failure::Error> {
@ -47,36 +84,33 @@ fn main() -> Result<(), failure::Error> {
let events = Events::new(); let events = Events::new();
// App let mut table = StatefulTable::new();
let mut app = App::new();
// Input // Input
loop { loop {
terminal.draw(|mut f| { terminal.draw(|mut f| {
let selected_style = Style::default().fg(Color::Yellow).modifier(Modifier::BOLD);
let normal_style = Style::default().fg(Color::White);
let header = ["Header1", "Header2", "Header3"];
let rows = app.items.iter().enumerate().map(|(i, item)| {
if i == app.selected {
Row::StyledData(item.into_iter(), selected_style)
} else {
Row::StyledData(item.into_iter(), normal_style)
}
});
let rects = Layout::default() let rects = Layout::default()
.constraints([Constraint::Percentage(100)].as_ref()) .constraints([Constraint::Percentage(100)].as_ref())
.margin(5) .margin(5)
.split(f.size()); .split(f.size());
let table = Table::new(header.iter(), rows) let selected_style = Style::default().fg(Color::Yellow).modifier(Modifier::BOLD);
let normal_style = Style::default().fg(Color::White);
let header = ["Header1", "Header2", "Header3"];
let rows = table
.items
.iter()
.map(|i| Row::StyledData(i.into_iter(), normal_style));
let t = Table::new(header.iter(), rows)
.block(Block::default().borders(Borders::ALL).title("Table")) .block(Block::default().borders(Borders::ALL).title("Table"))
.highlight_style(selected_style)
.highlight_symbol(">> ")
.widths(&[ .widths(&[
Constraint::Percentage(50), Constraint::Percentage(50),
Constraint::Length(30), Constraint::Length(30),
Constraint::Max(10), Constraint::Max(10),
]); ]);
f.render_widget(table, rects[0]); f.render_stateful_widget(t, rects[0], &mut table.state);
})?; })?;
match events.next()? { match events.next()? {
@ -85,17 +119,10 @@ fn main() -> Result<(), failure::Error> {
break; break;
} }
Key::Down => { Key::Down => {
app.selected += 1; table.next();
if app.selected > app.items.len() - 1 {
app.selected = 0;
}
} }
Key::Up => { Key::Up => {
if app.selected > 0 { table.previous();
app.selected -= 1;
} else {
app.selected = app.items.len() - 1;
}
} }
_ => {} _ => {}
}, },

View file

@ -20,7 +20,7 @@ pub use self::gauge::Gauge;
pub use self::list::{List, ListState}; pub use self::list::{List, ListState};
pub use self::paragraph::Paragraph; pub use self::paragraph::Paragraph;
pub use self::sparkline::Sparkline; pub use self::sparkline::Sparkline;
pub use self::table::{Row, Table}; pub use self::table::{Row, Table, TableState};
pub use self::tabs::Tabs; pub use self::tabs::Tabs;
use crate::buffer::Buffer; use crate::buffer::Buffer;

View file

@ -1,15 +1,47 @@
use std::collections::HashMap; use crate::{
use std::fmt::Display; buffer::Buffer,
use std::iter::Iterator; layout::{Constraint, Rect},
style::Style,
widgets::{Block, StatefulWidget, Widget},
};
use cassowary::{
strength::{MEDIUM, REQUIRED, WEAK},
WeightedRelation::*,
{Expression, Solver},
};
use std::{
collections::HashMap,
fmt::Display,
iter::{self, Iterator},
};
use unicode_width::UnicodeWidthStr;
use cassowary::strength::{MEDIUM, REQUIRED, WEAK}; pub struct TableState {
use cassowary::WeightedRelation::*; offset: usize,
use cassowary::{Expression, Solver}; selected: Option<usize>,
}
use crate::buffer::Buffer; impl Default for TableState {
use crate::layout::{Constraint, Rect}; fn default() -> TableState {
use crate::style::Style; TableState {
use crate::widgets::{Block, Widget}; offset: 0,
selected: None,
}
}
}
impl TableState {
pub fn selected(&self) -> Option<usize> {
self.selected
}
pub fn select(&mut self, index: Option<usize>) {
self.selected = index;
if index.is_none() {
self.offset = 0;
}
}
}
/// Holds data to be displayed in a Table widget /// Holds data to be displayed in a Table widget
pub enum Row<D> pub enum Row<D>
@ -60,6 +92,10 @@ pub struct Table<'a, H, R> {
column_spacing: u16, column_spacing: u16,
/// Space between the header and the rows /// Space between the header and the rows
header_gap: u16, 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 /// Data to display in each row
rows: R, rows: R,
} }
@ -76,9 +112,11 @@ where
header: H::default(), header: H::default(),
header_style: Style::default(), header_style: Style::default(),
widths: &[], widths: &[],
rows: R::default(),
column_spacing: 1, column_spacing: 1,
header_gap: 1, header_gap: 1,
highlight_style: Style::default(),
highlight_symbol: None,
rows: R::default(),
} }
} }
} }
@ -96,9 +134,11 @@ where
header: header.into_iter(), header: header.into_iter(),
header_style: Style::default(), header_style: Style::default(),
widths: &[], widths: &[],
rows,
column_spacing: 1, column_spacing: 1,
header_gap: 1, header_gap: 1,
highlight_style: Style::default(),
highlight_symbol: None,
rows,
} }
} }
pub fn block(mut self, block: Block<'a>) -> Table<'a, H, R> { pub fn block(mut self, block: Block<'a>) -> Table<'a, H, R> {
@ -145,6 +185,16 @@ where
self 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> { pub fn column_spacing(mut self, spacing: u16) -> Table<'a, H, R> {
self.column_spacing = spacing; self.column_spacing = spacing;
self self
@ -156,7 +206,7 @@ where
} }
} }
impl<'a, H, D, R> Widget for Table<'a, H, R> impl<'a, H, D, R> StatefulWidget for Table<'a, H, R>
where where
H: Iterator, H: Iterator,
H::Item: Display, H::Item: Display,
@ -164,7 +214,9 @@ where
D::Item: Display, D::Item: Display,
R: Iterator<Item = Row<D>>, R: Iterator<Item = Row<D>>,
{ {
fn render(mut self, area: Rect, buf: &mut Buffer) { type State = TableState;
fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
// Render block if necessary and get the drawing area // Render block if necessary and get the drawing area
let table_area = match self.block { let table_area = match self.block {
Some(ref mut b) => { Some(ref mut b) => {
@ -234,18 +286,51 @@ where
} }
y += 1 + self.header_gap; 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 highlight_symbol = self.highlight_symbol.unwrap_or("");
let blank_symbol = iter::repeat(" ")
.take(highlight_symbol.width())
.collect::<String>();
// Draw rows // Draw rows
let default_style = Style::default(); let default_style = Style::default();
if y < table_area.bottom() { if y < table_area.bottom() {
let remaining = (table_area.bottom() - y) as usize; let remaining = (table_area.bottom() - y) as usize;
for (i, row) in self.rows.by_ref().take(remaining).enumerate() {
let (data, style) = match row { // Make sure the table shows the selected item
Row::Data(d) => (d, default_style), state.offset = if let Some(selected) = selected {
Row::StyledData(d, s) => (d, s), if selected >= remaining + state.offset - 1 {
selected + 1 - remaining
} else if selected < state.offset {
selected
} else {
state.offset
}
} else {
0
};
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(); x = table_area.left();
for (w, elt) in solved_widths.iter().zip(data) { for (c, (w, elt)) in solved_widths.iter().zip(data).enumerate() {
buf.set_stringn(x, y + i as u16, format!("{}", elt), *w as usize, style); 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; x += *w + self.column_spacing;
} }
} }
@ -253,6 +338,20 @@ where
} }
} }
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(self, area: Rect, buf: &mut Buffer) {
let mut state = TableState::default();
StatefulWidget::render(self, area, buf, &mut state);
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;