mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-22 12:43:16 +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)]
|
||||
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<Vec<&'a str>>,
|
||||
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();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<usize>,
|
||||
}
|
||||
|
||||
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<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
|
||||
pub enum Row<D>
|
||||
|
@ -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<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
|
||||
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::<String>();
|
||||
|
||||
// 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<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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
Loading…
Reference in a new issue