feat(tui): add stateful list widget

This commit is contained in:
Orhun Parmaksız 2022-01-06 18:07:25 +03:00
parent 4df05f94fe
commit c950826eb4
No known key found for this signature in database
GPG key ID: F83424824B3E4B90
5 changed files with 133 additions and 4 deletions

View file

@ -1,4 +1,5 @@
use crate::command::Command;
use crate::widgets::StatefulList;
/// Application controller.
#[derive(Debug)]
@ -7,6 +8,8 @@ pub struct App {
pub running: bool,
/// Input buffer.
pub input: Option<String>,
/// List of sysctl variables.
pub variable_list: StatefulList<String>,
}
impl Default for App {
@ -14,6 +17,10 @@ impl Default for App {
Self {
running: true,
input: None,
variable_list: StatefulList::with_items(vec![
String::from("data1"),
String::from("data2"),
]),
}
}
}
@ -22,6 +29,12 @@ impl App {
/// Runs the given command and updates the application.
pub fn run_command(&mut self, command: Command) {
match command {
Command::ScrollUp => {
self.variable_list.previous();
}
Command::ScrollDown => {
self.variable_list.next();
}
Command::UpdateInput(v) => match self.input.as_mut() {
Some(input) => {
input.push(v);

View file

@ -3,6 +3,10 @@ use termion::event::Key;
/// Possible application commands.
#[derive(Debug)]
pub enum Command {
/// Scroll up on the widget.
ScrollUp,
/// Scroll down on the widget.
ScrollDown,
/// Update the input buffer.
UpdateInput(char),
/// Clear the input buffer.
@ -25,6 +29,8 @@ impl Command {
}
} else {
match key {
Key::Up => Command::ScrollUp,
Key::Down => Command::ScrollDown,
Key::Char(':') => Command::UpdateInput(' '),
Key::Esc => Command::Exit,
_ => Command::None,

View file

@ -12,6 +12,8 @@ pub mod error;
pub mod event;
/// User interface renderer.
pub mod ui;
/// Custom widgets.
pub mod widgets;
use crate::app::App;
use crate::command::Command;

View file

@ -1,8 +1,9 @@
use crate::app::App;
use tui::backend::Backend;
use tui::layout::{Constraint, Direction, Layout};
use tui::layout::{Constraint, Direction, Layout, Rect};
use tui::style::{Color, Style};
use tui::widgets::{Block, BorderType, Borders, Paragraph};
use tui::text::Span;
use tui::widgets::{Block, BorderType, Borders, List, ListItem, Paragraph};
use tui::Frame;
use unicode_width::UnicodeWidthStr;
@ -13,9 +14,42 @@ pub fn render<B: Backend>(frame: &mut Frame<'_, B>, app: &mut App) {
.direction(Direction::Vertical)
.constraints([Constraint::Min(rect.height - 3), Constraint::Min(3)].as_ref())
.split(rect);
render_variable_list(frame, chunks[0], app);
render_input_prompt(frame, chunks[1], rect.height - 2, app);
}
/// Renders the list that contains the sysctl variables.
fn render_variable_list<B: Backend>(frame: &mut Frame<'_, B>, rect: Rect, app: &mut App) {
frame.render_stateful_widget(
List::new(
app.variable_list
.items
.iter()
.map(|text| ListItem::new(Span::raw(text)))
.collect::<Vec<ListItem<'_>>>(),
)
.block(
Block::default()
.borders(Borders::all())
.border_style(Style::default().fg(Color::White))
.border_type(BorderType::Rounded)
.style(Style::default().bg(Color::Black)),
)
.highlight_symbol("> "),
rect,
&mut app.variable_list.state,
);
}
/// Renders the input prompt for running commands.
fn render_input_prompt<B: Backend>(
frame: &mut Frame<'_, B>,
rect: Rect,
cursor_y: u16,
app: &mut App,
) {
if let Some(input) = &app.input {
frame.set_cursor(input.width() as u16 + 2, rect.height - 2);
frame.set_cursor(input.width() as u16 + 2, cursor_y);
}
frame.render_widget(
Paragraph::new(match &app.input {
@ -29,6 +63,6 @@ pub fn render<B: Backend>(frame: &mut Frame<'_, B>, app: &mut App) {
.border_type(BorderType::Rounded)
.style(Style::default().bg(Color::Black)),
),
chunks[1],
rect,
);
}

View file

@ -0,0 +1,74 @@
use tui::widgets::ListState;
/// List widget with TUI controlled states.
#[derive(Debug)]
pub struct StatefulList<T> {
/// List items (states).
pub items: Vec<T>,
/// State that can be modified by TUI.
pub state: ListState,
}
impl<T> StatefulList<T> {
/// Constructs a new instance of `StatefulList`.
pub fn new(items: Vec<T>, mut state: ListState) -> StatefulList<T> {
state.select(Some(0));
Self { items, state }
}
/// Construct a new `StatefulList` with given items.
pub fn with_items(items: Vec<T>) -> StatefulList<T> {
Self::new(items, ListState::default())
}
/// Returns the selected item.
pub fn selected(&self) -> Option<&T> {
self.items.get(self.state.selected()?)
}
/// Selects the next item.
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));
}
/// Selects the previous item.
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));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stateful_list() {
let mut list = StatefulList::with_items(vec!["data1", "data2", "data3"]);
list.state.select(Some(1));
assert_eq!(Some(&"data2"), list.selected());
list.next();
assert_eq!(Some(2), list.state.selected());
list.previous();
assert_eq!(Some(1), list.state.selected());
}
}