You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tui-rs/examples/user_input.rs

193 lines
5.9 KiB
Rust

/// A simple example demonstrating how to handle user input. This is
/// a bit out of the scope of the library as it does not provide any
/// input handling out of the box. However, it may helps some to get
/// started.
///
/// This is a very simple example:
/// * A input box always focused. Every character you type is registered
/// here
/// * Pressing Backspace erases a character
/// * Pressing Enter pushes the current input in the history of previous
/// messages
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{error::Error, io};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::{Span, Spans, Text},
widgets::{Block, Borders, List, ListItem, Paragraph},
Frame, Terminal,
};
use unicode_width::UnicodeWidthStr;
enum InputMode {
Normal,
Editing,
}
/// App holds the state of the application
struct App {
/// Current value of the input box
input: String,
/// Current input mode
input_mode: InputMode,
/// History of recorded messages
messages: Vec<String>,
}
impl Default for App {
fn default() -> App {
App {
input: String::new(),
input_mode: InputMode::Normal,
messages: Vec::new(),
}
}
}
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let app = App::default();
let res = run_app(&mut terminal, app);
feat: add stateful widgets Most widgets can be drawn directly based on the input parameters. However, some features may require some kind of associated state to be implemented. For example, the `List` widget can highlight the item currently selected. This can be translated in an offset, which is the number of elements to skip in order to have the selected item within the viewport currently allocated to this widget. The widget can therefore only provide the following behavior: whenever the selected item is out of the viewport scroll to a predefined position (make the selected item the last viewable item or the one in the middle). Nonetheless, if the widget has access to the last computed offset then it can implement a natural scrolling experience where the last offset is reused until the selected item is out of the viewport. To allow such behavior within the widgets, this commit introduces the following changes: - Add a `StatefulWidget` trait with an associated `State` type. Widgets that can take advantage of having a "memory" between two draw calls needs to implement this trait. - Add a `render_stateful_widget` method on `Frame` where the associated state is given as a parameter. The chosen approach is thus to let the developers manage their widgets' states themselves as they are already responsible for the lifecycle of the wigets (given that the crate exposes an immediate mode api). The following changes were also introduced: - `Widget::render` has been deleted. Developers should use `Frame::render_widget` instead. - `Widget::background` has been deleted. Developers should use `Buffer::set_background` instead. - `SelectableList` has been deleted. Developers can directly use `List` where `SelectableList` features have been back-ported.
4 years ago
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
feat: add stateful widgets Most widgets can be drawn directly based on the input parameters. However, some features may require some kind of associated state to be implemented. For example, the `List` widget can highlight the item currently selected. This can be translated in an offset, which is the number of elements to skip in order to have the selected item within the viewport currently allocated to this widget. The widget can therefore only provide the following behavior: whenever the selected item is out of the viewport scroll to a predefined position (make the selected item the last viewable item or the one in the middle). Nonetheless, if the widget has access to the last computed offset then it can implement a natural scrolling experience where the last offset is reused until the selected item is out of the viewport. To allow such behavior within the widgets, this commit introduces the following changes: - Add a `StatefulWidget` trait with an associated `State` type. Widgets that can take advantage of having a "memory" between two draw calls needs to implement this trait. - Add a `render_stateful_widget` method on `Frame` where the associated state is given as a parameter. The chosen approach is thus to let the developers manage their widgets' states themselves as they are already responsible for the lifecycle of the wigets (given that the crate exposes an immediate mode api). The following changes were also introduced: - `Widget::render` has been deleted. Developers should use `Frame::render_widget` instead. - `Widget::background` has been deleted. Developers should use `Buffer::set_background` instead. - `SelectableList` has been deleted. Developers can directly use `List` where `SelectableList` features have been back-ported.
4 years ago
if let Err(err) = res {
println!("{:?}", err)
}
Ok(())
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
loop {
terminal.draw(|f| ui(f, &app))?;
if let Event::Key(key) = event::read()? {
match app.input_mode {
InputMode::Normal => match key.code {
KeyCode::Char('e') => {
app.input_mode = InputMode::Editing;
}
KeyCode::Char('q') => {
return Ok(());
}
_ => {}
},
InputMode::Editing => match key.code {
KeyCode::Enter => {
app.messages.push(app.input.drain(..).collect());
}
KeyCode::Char(c) => {
app.input.push(c);
}
KeyCode::Backspace => {
app.input.pop();
}
KeyCode::Esc => {
app.input_mode = InputMode::Normal;
}
_ => {}
},
}
}
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints(
[
Constraint::Length(1),
Constraint::Length(3),
Constraint::Min(1),
]
.as_ref(),
)
.split(f.size());
let (msg, style) = match app.input_mode {
InputMode::Normal => (
vec![
Span::raw("Press "),
Span::styled("q", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to exit, "),
Span::styled("e", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to start editing."),
],
Style::default().add_modifier(Modifier::RAPID_BLINK),
),
InputMode::Editing => (
vec![
Span::raw("Press "),
Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to stop editing, "),
Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to record the message"),
],
Style::default(),
),
};
let mut text = Text::from(Spans::from(msg));
text.patch_style(style);
let help_message = Paragraph::new(text);
f.render_widget(help_message, chunks[0]);
let input = Paragraph::new(app.input.as_ref())
.style(match app.input_mode {
InputMode::Normal => Style::default(),
InputMode::Editing => Style::default().fg(Color::Yellow),
})
.block(Block::default().borders(Borders::ALL).title("Input"));
f.render_widget(input, chunks[1]);
match app.input_mode {
InputMode::Normal =>
// Hide the cursor. `Frame` does this by default, so we don't need to do anything here
{}
InputMode::Editing => {
// Make the cursor visible and ask tui-rs to put it at the specified coordinates after rendering
f.set_cursor(
// Put cursor past the end of the input text
chunks[1].x + app.input.width() as u16 + 1,
// Move one line down, from the border to the input line
chunks[1].y + 1,
)
}
}
let messages: Vec<ListItem> = app
.messages
.iter()
.enumerate()
.map(|(i, m)| {
let content = vec![Spans::from(Span::raw(format!("{}: {}", i, m)))];
ListItem::new(content)
})
.collect();
let messages =
List::new(messages).block(Block::default().borders(Borders::ALL).title("Messages"));
f.render_widget(messages, chunks[2]);
}