Skip to content

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:

src/app.rs
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:

  1. when the Prompt is in focus,

  2. when the Results are in focus.

You can represent the state of the “focus” using an enum called Mode:

src/app.rs
#[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 Modes are active:

src/app.rs
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:

src/app.rs
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

src/app.rs
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:

src/app.rs
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:

src/app.rs
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)
}
}