Add a multiple-selection list widget

This commit is contained in:
Nicholas Howard 2021-09-02 13:08:19 -05:00
parent 80a929ccc6
commit 77f8bbdf2d
4 changed files with 351 additions and 2 deletions

92
examples/multilist.rs Normal file
View file

@ -0,0 +1,92 @@
mod util;
use crate::util::event::{Event, Events};
use std::{error::Error, io};
use termion::{input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::layout::{Constraint, Direction, Layout};
use tui::style::{Color, Modifier, Style};
use tui::widgets::{Block, Borders, MultiListState, MutliList};
use tui::{backend::TermionBackend, widgets::ListItem, Terminal};
fn main() -> Result<(), Box<dyn Error>> {
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let events = Events::new();
let mut state = MultiListState::default();
state.highlight(Some(0));
let items: Vec<ListItem> = vec![
ListItem::new("Option 1"),
ListItem::new("Option 2"),
ListItem::new("Option 3"),
];
loop {
match events.next().unwrap() {
Event::Input(i) => match i {
termion::event::Key::Left => {
if let Some(h) = state.get_highlight() {
state.deselect(h);
}
}
termion::event::Key::Right => {
if let Some(h) = state.get_highlight() {
state.select(h);
}
}
termion::event::Key::Up => {
let mut h = state.get_highlight().unwrap_or(0);
h = if h == 0 { items.len() - 1 } else { h - 1 };
state.highlight(Some(h));
}
termion::event::Key::Down => {
let mut h = state.get_highlight().unwrap_or(0);
h = if h >= items.len() - 1 { 0 } else { h + 1 };
state.highlight(Some(h));
}
termion::event::Key::Char(c) => match c {
'\n' => {
if let Some(h) = state.get_highlight() {
state.toggle_selection(h);
}
}
_ => (),
},
termion::event::Key::Ctrl(c) => {
if c == 'c' {
break;
}
}
_ => (),
},
Event::Tick => (),
}
terminal.draw(|f| {
let chunk = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(100)].as_ref())
.split(f.size())[0];
let multilist = MutliList::new(items.clone())
.block(Block::default().borders(Borders::ALL))
.selected_style(
Style::default()
.fg(Color::Black)
.bg(Color::White)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol(">>");
f.render_stateful_widget(multilist, chunk, &mut state);
})?;
}
Ok(())
}

View file

@ -37,8 +37,8 @@ impl ListState {
#[derive(Debug, Clone, PartialEq)]
pub struct ListItem<'a> {
content: Text<'a>,
style: Style,
pub(crate) content: Text<'a>,
pub(crate) style: Style,
}
impl<'a> ListItem<'a> {

View file

@ -22,6 +22,7 @@ mod chart;
mod clear;
mod gauge;
mod list;
mod multi_list;
mod paragraph;
mod reflow;
mod sparkline;
@ -34,6 +35,7 @@ pub use self::chart::{Axis, Chart, Dataset, GraphType};
pub use self::clear::Clear;
pub use self::gauge::{Gauge, LineGauge};
pub use self::list::{List, ListItem, ListState};
pub use self::multi_list::{MultiListState, MutliList};
pub use self::paragraph::{Paragraph, Wrap};
pub use self::sparkline::Sparkline;
pub use self::table::{Cell, Row, Table, TableState};

255
src/widgets/multi_list.rs Normal file
View file

@ -0,0 +1,255 @@
use std::collections::HashSet;
use unicode_width::UnicodeWidthStr;
use super::{Block, ListItem, StatefulWidget, Widget};
use crate::{
layout::{Corner, Rect},
style::Style,
};
pub struct MultiListState {
selected: HashSet<usize>,
highlighted: Option<usize>,
offset: usize,
}
impl Default for MultiListState {
fn default() -> Self {
Self {
selected: HashSet::new(),
highlighted: None,
offset: 0,
}
}
}
impl MultiListState {
pub fn select(&mut self, i: usize) {
self.selected.insert(i);
}
pub fn deselect(&mut self, i: usize) {
self.selected.remove(&i);
}
pub fn toggle_selection(&mut self, i: usize) {
if self.selected.contains(&i) {
self.selected.remove(&i);
} else {
self.selected.insert(i);
}
}
pub fn highlight(&mut self, i: Option<usize>) {
self.highlighted = i;
}
pub fn get_highlight(&mut self) -> Option<usize> {
self.highlighted
}
}
#[derive(Debug, Clone)]
pub struct MutliList<'a> {
block: Option<Block<'a>>,
items: Vec<ListItem<'a>>,
style: Style,
start_corner: Corner,
selected_style: Style,
highlight_style: Style,
highlight_symbol: Option<&'a str>,
}
impl<'a> MutliList<'a> {
pub fn new<T>(items: T) -> Self
where
T: Into<Vec<ListItem<'a>>>,
{
Self {
block: None,
style: Style::default(),
items: items.into(),
start_corner: Corner::TopLeft,
selected_style: Style::default(),
highlight_style: Style::default(),
highlight_symbol: None,
}
}
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
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, style: Style) -> Self {
self.highlight_style = style;
self
}
pub fn start_corner(mut self, corner: Corner) -> Self {
self.start_corner = corner;
self
}
pub fn selected_style(mut self, selected_style: Style) -> Self {
self.selected_style = selected_style;
self
}
fn get_items_bounds(
&self,
highlighted: Option<usize>,
offset: usize,
max_height: usize,
) -> (usize, usize) {
let offset = offset.min(self.items.len().saturating_sub(1));
let mut start = offset;
let mut end = offset;
let mut height = 0;
for item in self.items.iter().skip(offset) {
if height + item.height() > max_height {
break;
}
height += item.height();
end += 1;
}
let selected = highlighted.unwrap_or(0).min(self.items.len() - 1);
while selected >= end {
height = height.saturating_add(self.items[end].height());
end += 1;
while height > max_height {
height = height.saturating_sub(self.items[start].height());
start += 1;
}
}
while selected < start {
start -= 1;
height = height.saturating_add(self.items[start].height());
while height > max_height {
end -= 1;
height = height.saturating_sub(self.items[end].height());
}
}
(start, end)
}
}
impl<'a> Widget for MutliList<'a> {
fn render(self, area: crate::layout::Rect, buf: &mut crate::buffer::Buffer) {
let mut state = MultiListState::default();
StatefulWidget::render(self, area, buf, &mut state);
}
}
impl<'a> StatefulWidget for MutliList<'a> {
type State = MultiListState;
fn render(
mut self,
area: crate::layout::Rect,
buf: &mut crate::buffer::Buffer,
state: &mut Self::State,
) {
buf.set_style(area, self.style);
let list_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
inner_area
}
None => area,
};
if list_area.width < 1 || list_area.height < 1 {
return;
}
if self.items.is_empty() {
return;
}
let list_height = list_area.height as usize;
let (start, end) = self.get_items_bounds(state.highlighted, state.offset, list_height);
state.offset = start;
let highlight_symbol = self
.highlight_symbol
.map(|s| String::from(s))
.unwrap_or("".into());
let mut current_height = 0;
for (i, item) in self
.items
.iter_mut()
.enumerate()
.skip(state.offset)
.take(end - start)
{
let (x, y) = match self.start_corner {
Corner::BottomLeft => {
current_height += item.height() as u16;
(list_area.left(), list_area.bottom() - current_height)
}
_ => {
let pos = (list_area.left(), list_area.top() + current_height);
current_height += item.height() as u16;
pos
}
};
let area = Rect {
x,
y,
width: list_area.width,
height: item.height() as u16,
};
let item_style = self.style.patch(item.style);
buf.set_style(area, item_style);
let is_selected = state.selected.contains(&i);
let is_highlighted = state.highlighted.map(|h| h == i).unwrap_or(false);
let elem_x = if is_highlighted {
let (x, _) = buf.set_stringn(
x,
y,
highlight_symbol.clone(),
list_area.width as usize,
item_style,
);
x
} else {
x
};
let max_element_width = (list_area.width - (elem_x - x)) as usize;
for (j, line) in item.content.lines.iter().enumerate() {
buf.set_spans(elem_x, y + j as u16, line, max_element_width as u16);
}
let mut style = if is_selected {
self.selected_style
} else {
self.style
};
if is_highlighted {
style = style.patch(self.highlight_style);
}
buf.set_style(area, style);
}
}
}