diff --git a/examples/table.rs b/examples/table.rs index 9a2419b5..7abda475 100644 --- a/examples/table.rs +++ b/examples/table.rs @@ -1,28 +1,26 @@ #[allow(dead_code)] 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 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>, - selected: usize, } -impl<'a> App<'a> { - fn new() -> App<'a> { - App { +impl<'a> StatefulTable<'a> { + fn new() -> StatefulTable<'a> { + StatefulTable { + state: TableState::default(), items: vec![ vec!["Row11", "Row12", "Row13"], vec!["Row21", "Row22", "Row23"], @@ -30,10 +28,49 @@ impl<'a> App<'a> { vec!["Row41", "Row42", "Row43"], vec!["Row51", "Row52", "Row53"], 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> { @@ -47,36 +84,33 @@ fn main() -> Result<(), failure::Error> { let events = Events::new(); - // App - let mut app = App::new(); + let mut table = StatefulTable::new(); // Input loop { 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() .constraints([Constraint::Percentage(100)].as_ref()) .margin(5) .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")) + .highlight_style(selected_style) + .highlight_symbol(">> ") .widths(&[ Constraint::Percentage(50), Constraint::Length(30), Constraint::Max(10), ]); - f.render_widget(table, rects[0]); + f.render_stateful_widget(t, rects[0], &mut table.state); })?; match events.next()? { @@ -85,17 +119,10 @@ fn main() -> Result<(), failure::Error> { break; } Key::Down => { - app.selected += 1; - if app.selected > app.items.len() - 1 { - app.selected = 0; - } + table.next(); } Key::Up => { - if app.selected > 0 { - app.selected -= 1; - } else { - app.selected = app.items.len() - 1; - } + table.previous(); } _ => {} }, diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index ce8740bf..aa161c36 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -20,7 +20,7 @@ pub use self::gauge::Gauge; pub use self::list::{List, ListState}; pub use self::paragraph::Paragraph; pub use self::sparkline::Sparkline; -pub use self::table::{Row, Table}; +pub use self::table::{Row, Table, TableState}; pub use self::tabs::Tabs; use crate::buffer::Buffer; diff --git a/src/widgets/table.rs b/src/widgets/table.rs index d1398507..8a8ed67c 100644 --- a/src/widgets/table.rs +++ b/src/widgets/table.rs @@ -1,15 +1,47 @@ -use std::collections::HashMap; -use std::fmt::Display; -use std::iter::Iterator; +use crate::{ + buffer::Buffer, + 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}; -use cassowary::WeightedRelation::*; -use cassowary::{Expression, Solver}; +pub struct TableState { + offset: usize, + selected: Option, +} -use crate::buffer::Buffer; -use crate::layout::{Constraint, Rect}; -use crate::style::Style; -use crate::widgets::{Block, Widget}; +impl Default for TableState { + fn default() -> TableState { + TableState { + offset: 0, + selected: None, + } + } +} + +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; + } + } +} /// Holds data to be displayed in a Table widget pub enum Row @@ -60,6 +92,10 @@ pub struct Table<'a, H, R> { 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, } @@ -76,9 +112,11 @@ where header: H::default(), header_style: Style::default(), widths: &[], - rows: R::default(), column_spacing: 1, header_gap: 1, + highlight_style: Style::default(), + highlight_symbol: None, + rows: R::default(), } } } @@ -96,9 +134,11 @@ where header: header.into_iter(), header_style: Style::default(), widths: &[], - rows, 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> { @@ -145,6 +185,16 @@ where 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 @@ -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 H: Iterator, H::Item: Display, @@ -164,7 +214,9 @@ where D::Item: Display, R: Iterator>, { - 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 let table_area = match self.block { Some(ref mut b) => { @@ -234,18 +286,51 @@ where } 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::(); + // Draw rows let default_style = Style::default(); if y < table_area.bottom() { let remaining = (table_area.bottom() - y) as usize; - for (i, row) in self.rows.by_ref().take(remaining).enumerate() { - let (data, style) = match row { - Row::Data(d) => (d, default_style), - Row::StyledData(d, s) => (d, s), + + // 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 + }; + 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 (w, elt) in solved_widths.iter().zip(data) { - buf.set_stringn(x, y + i as u16, format!("{}", elt), *w as usize, style); + 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; } } @@ -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>, +{ + fn render(self, area: Rect, buf: &mut Buffer) { + let mut state = TableState::default(); + StatefulWidget::render(self, area, buf, &mut state); + } +} + #[cfg(test)] mod tests { use super::*;