Skip to content

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:

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:

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

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

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

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 {
self.quit()
}
}
Event::Render => self.draw(tui)?,
_ => (),
};
Ok(())
}
}

App::draw

Finally, for the draw method, you could define it like this:

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

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

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

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