App Mode
In this section, you are going to expand on the App
struct to add a Mode
.
App
Define the following fields in the App
struct:
pub struct App { quit: bool, last_key_event: Option<crossterm::event::KeyEvent>, mode: Mode, // new}
Mode
Our app is going to have two focus modes:
-
when the
Prompt
is in focus, -
when the
Results
are in focus.
You can represent the state of the “focus” using an enum called Mode
:
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]pub enum Mode { #[default] Prompt, Results,}
The reason you want to do this is because you may want to do different things when receiving the
same event in different modes. For example, ESC
when the prompt is in focus should switch the mode
to results, but ESC
when the results are in focus should exit the app.
App::handle_event
Change the handle_event
function to do different things when Esc
is pressed and different
Mode
s are active:
impl App { fn handle_event(&mut self, e: Event, tui: &mut Tui) -> Result<()> { use crossterm::event::Event as CrosstermEvent; use crossterm::event::KeyCode; match e { Event::Crossterm(CrosstermEvent::Key(key)) => { self.last_key_event = Some(key); if key.code == KeyCode::Esc { match self.mode { Mode::Prompt => self.switch_mode(Mode::Results), Mode::Results => self.quit(), } } } Event::Render => self.draw(tui)?, _ => (), }; Ok(()) }}
You’ll need to add a new switch_mode
method:
impl App { fn switch_mode(&mut self, mode: Mode) { self.mode = mode; }}
Draw
Let’s make our view a little more interesting with some placeholder widgets.
Results
For the results, use a Table
with some mock data
use itertools::Itertools;
impl App { fn results(&self) -> Table<'_> { let widths = [ Constraint::Length(15), Constraint::Min(0), Constraint::Length(20), ];
let rows = vec![ ["hyper", "Fast and safe HTTP implementation", "1000000"], ["serde", "Rust data structures", "1500000"], ["tokio", "non-blocking I/O platform", "1300000"], ["rand", "random number generation", "900000"], ["actix-web", "fast web framework", "800000"], ["syn", "Parsing source code", "700000"], ["warp", "web server framework", "600000"], ["Ratatui", "terminal user interfaces", "500000"], ] .iter() .map(|row| Row::new(row.iter().map(|s| String::from(*s)).collect_vec())) .collect_vec();
Table::new(rows, widths).header(Row::new(vec![ "Name", "Description", "Downloads", ])) }}
Prompt
For the prompt, make a Block
that changes border color based on the mode:
impl App { fn prompt(&self) -> (Block, Paragraph) { let color = if matches!(self.mode, Mode::Prompt) { Color::Yellow } else { Color::Blue }; let block = Block::default().borders(Borders::ALL).border_style(color); let paragraph = Paragraph::new("prompt"); (block, paragraph) }}
Render
And in the render function for the StatefulWidget
we can call these widget constructors:
impl StatefulWidget for AppWidget { type State = App;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { let [last_key_event, results, prompt] = Layout::vertical([ Constraint::Length(1), Constraint::Fill(0), Constraint::Length(5), ]) .areas(area);
let table = state.results(); Widget::render(table, results, buf);
let (block, paragraph) = state.prompt(); block.render(prompt, buf); paragraph.render( prompt.inner(&Margin { horizontal: 2, vertical: 2, }), buf, );
if let Some(key) = state.last_key_event { Paragraph::new(format!("last key event: {:?}", key.code)) .right_aligned() .render(last_key_event, buf); } }}
Conclusion
If you run it, you should see something like this:
Here’s the full ./src/app.rs
file for your reference:
src/app.rs (click to expand)
use color_eyre::eyre::Result;use itertools::Itertools;use ratatui::prelude::*;use ratatui::widgets::*;
use crate::{ events::{Event, Events}, tui::Tui};
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]pub enum Mode { #[default] Prompt, Results,}
pub struct App { quit: bool, last_key_event: Option<crossterm::event::KeyEvent>, mode: Mode, // new}
impl App { pub fn new() -> Self { let quit = false; let mode = Mode::default(); let last_key_event = None; Self { quit, mode, last_key_event, } }
pub async fn run( &mut self, mut tui: Tui, mut events: Events, ) -> Result<()> { loop { if let Some(e) = events.next().await { self.handle_event(e, &mut tui)? } if self.should_quit() { break; } } Ok(()) }
fn handle_event(&mut self, e: Event, tui: &mut Tui) -> Result<()> { use crossterm::event::Event as CrosstermEvent; use crossterm::event::KeyCode; match e { Event::Crossterm(CrosstermEvent::Key(key)) => { self.last_key_event = Some(key); if key.code == KeyCode::Esc { match self.mode { Mode::Prompt => self.switch_mode(Mode::Results), Mode::Results => self.quit(), } } } Event::Render => self.draw(tui)?, _ => (), }; Ok(()) }
fn draw(&mut self, tui: &mut Tui) -> Result<()> { tui.draw(|frame| { frame.render_stateful_widget(AppWidget, frame.size(), self); })?; Ok(()) }
fn switch_mode(&mut self, mode: Mode) { self.mode = mode; }
fn should_quit(&self) -> bool { self.quit }
fn quit(&mut self) { self.quit = true }}
impl Default for App { fn default() -> Self { Self::new() }}
struct AppWidget;
impl StatefulWidget for AppWidget { type State = App;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { let [last_key_event, results, prompt] = Layout::vertical([ Constraint::Length(1), Constraint::Fill(0), Constraint::Length(5), ]) .areas(area);
let table = state.results(); Widget::render(table, results, buf);
let (block, paragraph) = state.prompt(); block.render(prompt, buf); paragraph.render( prompt.inner(&Margin { horizontal: 2, vertical: 2, }), buf, );
if let Some(key) = state.last_key_event { Paragraph::new(format!("last key event: {:?}", key.code)) .right_aligned() .render(last_key_event, buf); } }}
impl App { fn results(&self) -> Table<'_> { let widths = [ Constraint::Length(15), Constraint::Min(0), Constraint::Length(20), ];
let rows = vec![ ["hyper", "Fast and safe HTTP implementation", "1000000"], ["serde", "Rust data structures", "1500000"], ["tokio", "non-blocking I/O platform", "1300000"], ["rand", "random number generation", "900000"], ["actix-web", "fast web framework", "800000"], ["syn", "Parsing source code", "700000"], ["warp", "web server framework", "600000"], ["Ratatui", "terminal user interfaces", "500000"], ] .iter() .map(|row| Row::new(row.iter().map(|s| String::from(*s)).collect_vec())) .collect_vec();
Table::new(rows, widths).header(Row::new(vec![ "Name", "Description", "Downloads", ])) }
fn prompt(&self) -> (Block, Paragraph) { let color = if matches!(self.mode, Mode::Prompt) { Color::Yellow } else { Color::Blue }; let block = Block::default().borders(Borders::ALL).border_style(color); let paragraph = Paragraph::new("prompt"); (block, paragraph) }}