mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-22 12:43:16 +00:00
Add a multiple-selection list widget
This commit is contained in:
parent
80a929ccc6
commit
77f8bbdf2d
4 changed files with 351 additions and 2 deletions
92
examples/multilist.rs
Normal file
92
examples/multilist.rs
Normal 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(())
|
||||
}
|
|
@ -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> {
|
||||
|
|
|
@ -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
255
src/widgets/multi_list.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue