App Basic Structure
Before we proceed any further, we are going to refactor the code we already have to make it easier
to scale up in the future. We are going to move the event loop into a method on the App
struct.
App
Create a new file ./src/app.rs
:
pub struct App { quit: bool, frame_count: usize, last_key_event: Option<crossterm::event::KeyEvent>,}
Define some helper functions for initializing the App
:
impl App { pub fn new() -> Self { let quit = false; let frame_count = 0; let last_key_event = None; Self { quit, frame_count, last_key_event, } }}
impl Default for App { fn default() -> Self { Self::new() }}
App methods
App::run
Now define a run
method for App
:
impl App { 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(()) }}
App::quit and App::should_quit
The run
method uses a should_quit
method (and a corresponding quit
method) that you can define
like this:
impl App { fn should_quit(&self) -> bool { self.quit }
fn quit(&mut self) { self.quit = true }}
App::handle_event
This run
method also uses a handle_event
method that you can define like so:
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 { self.quit() } } Event::Render => self.draw(tui)?, _ => (), }; Ok(()) }}
App::draw
Finally, for the draw
method, you could define it like this:
use ratatui::widgets::*;
impl App { fn draw(&mut self, tui: &mut Tui) -> Result<()> { tui.draw(|frame| { frame.render_widget( Paragraph::new(format!( "frame counter: {}", frame.count() )), frame.size(), ); })?; Ok(()) }}
But let’s go one step further and set ourselves up for using the StatefulWidget
pattern.
StatefulWidget pattern
Define the draw
method like this:
impl App { fn draw(&mut self, tui: &mut Tui) -> Result<()> { tui.draw(|frame| { self.frame_count = frame.count(); frame.render_stateful_widget(AppWidget, frame.size(), self); })?; Ok(()) }}
This uses a unit-like struct called AppWidget
that can be rendered as a StatefulWidget
using
the App
struct as its state.
use ratatui::widgets::{StatefulWidget, Paragraph};
struct AppWidget;
impl StatefulWidget for AppWidget { type State = App;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { Paragraph::new(format!("frame counter: {}", state.frame_count)) .render(area, buf);
if let Some(key) = state.last_key_event { Paragraph::new(format!("last key event: {:?}", key.code)) .right_aligned() .render(area, buf); } }}
Here’s the full ./src/app.rs
file for your reference:
src/app.rs (click to expand)
use color_eyre::eyre::Result;use ratatui::prelude::*;use ratatui::widgets::*;
use crate::{ events::{Event, Events}, tui::Tui};
pub struct App { quit: bool, frame_count: usize, last_key_event: Option<crossterm::event::KeyEvent>,}
impl App { pub fn new() -> Self { let quit = false; let frame_count = 0; let last_key_event = None; Self { quit, frame_count, 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 { self.quit() } } Event::Render => self.draw(tui)?, _ => (), }; Ok(()) }
fn draw(&mut self, tui: &mut Tui) -> Result<()> { tui.draw(|frame| { self.frame_count = frame.count(); frame.render_stateful_widget(AppWidget, frame.size(), self); })?; Ok(()) }
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) { Paragraph::new(format!("frame counter: {}", state.frame_count)) .render(area, buf);
if let Some(key) = state.last_key_event { Paragraph::new(format!("last key event: {:?}", key.code)) .right_aligned() .render(area, buf); } }}
Conclusion
Now, run your application with a modified main.rs
that uses the App
struct you just created:
pub mod app;pub mod errors;pub mod events;pub mod tui;pub mod widgets;
#[tokio::main]async fn main() -> color_eyre::Result<()> { errors::install_hooks()?;
let tui = tui::init()?; let events = events::Events::new(); app::App::new().run(tui, events).await?; tui::restore()?;
Ok(())}
You should get the same results as before.
Your file structure should now look like this:
.├── Cargo.lock├── Cargo.toml└── src ├── app.rs ├── crates_io_api_helper.rs ├── errors.rs ├── events.rs ├── main.rs └── tui.rs