mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-26 06:30:29 +00:00
feat(widgets/table): allow one row to be selected
This commit is contained in:
parent
140db9b2e2
commit
ae677099d6
3 changed files with 189 additions and 63 deletions
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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::*;
|
||||||
|
|
Loading…
Reference in a new issue