Component Architecture
If you are interested in a more object oriented approach to organizing TUIs, you can use a
Component
based approach.
A couple of projects in the wild use this approach
We also have a ratatui-async-template
that has an example of this Component
based approach:
We already covered TEA in the previous section. The Component
architecture takes a slightly more object oriented trait based approach.
Each component encapsulates its own state, event handlers, and rendering logic.
-
Component Initialization (
init
) - This is where a component can set up any initial state or resources it needs. It’s a separate process from handling events or rendering. -
Event Handling (
handle_events
,handle_key_events
,handle_mouse_events
) - Each component has its own event handlers. This allows for a finer-grained approach to event handling, with each component only dealing with the events it’s interested in. This contrasts with Elm’s single update function that handles messages for the entire application. -
State Update (
update
) - Components can have their own local state and can update it in response to actions. This state is private to the component, which differs from Elm’s global model. -
Rendering (
render
) - Each component defines its own rendering logic. It knows how to draw itself, given a rendering context. This is similar to Elm’s view function but on a component-by-component basis.
Here’s an example of the Component
trait implementation you might use:
use anyhow::Result;
use crossterm::event::{KeyEvent, MouseEvent};
use ratatui::layout::Rect;
use crate::{action::Action, event::Event, terminal::Frame};
pub trait Component {
fn init(&mut self) -> Result<()> {
Ok(())
}
fn handle_events(&mut self, event: Option<Event>) -> Action {
match event {
Some(Event::Quit) => Action::Quit,
Some(Event::Tick) => Action::Tick,
Some(Event::Key(key_event)) => self.handle_key_events(key_event),
Some(Event::Mouse(mouse_event)) => self.handle_mouse_events(mouse_event),
Some(Event::Resize(x, y)) => Action::Resize(x, y),
Some(_) => Action::Noop,
None => Action::Noop,
}
}
fn handle_key_events(&mut self, key: KeyEvent) -> Action {
Action::Noop
}
fn handle_mouse_events(&mut self, mouse: MouseEvent) -> Action {
Action::Noop
}
fn update(&mut self, action: Action) -> Action {
Action::Noop
}
fn render(&mut self, f: &mut Frame<'_>, rect: Rect);
}
One advantage of this approach is that it incentivizes co-locating the handle_events
, update
and
render
functions on a component level.