Event Bus
For apps with multiple focusable components, EventBus routes input based on focus state. You register handlers for components and the bus decides routing order (modal → hovered → focused → global).
For simple single-component apps, you can handle events directly in your main loop without EventBus—see the minimal example.
Quick Start: SimpleEventBus
For apps that don’t need context-specific keybindings, use SimpleEventBus with DefaultBindingContext:
use tui_dispatch::{SimpleEventBus, DefaultBindingContext, EventRoutingState, Keybindings};
#[derive(tui_dispatch::ComponentId, Clone, Copy, PartialEq, Eq, Hash, Debug)]enum AppComponentId { Main,}
impl EventRoutingState<AppComponentId, DefaultBindingContext> for AppState { fn focused(&self) -> Option<AppComponentId> { Some(AppComponentId::Main) } fn modal(&self) -> Option<AppComponentId> { None } fn binding_context(&self, _id: AppComponentId) -> DefaultBindingContext { DefaultBindingContext } fn default_context(&self) -> DefaultBindingContext { DefaultBindingContext }}
let mut bus: SimpleEventBus<AppState, Action, AppComponentId> = SimpleEventBus::new();let keybindings: Keybindings<DefaultBindingContext> = Keybindings::new();This reduces boilerplate by ~10 lines compared to defining a custom context enum.
Full API: Custom Binding Contexts
For apps with multiple modes (e.g., normal/search/modal), define custom contexts:
1) Define routing types
#[derive(tui_dispatch::ComponentId, Clone, Copy, PartialEq, Eq, Hash, Debug)]enum AppComponentId { Main, Search,}
#[derive(tui_dispatch::BindingContext, Clone, Copy, PartialEq, Eq, Hash)]enum AppContext { Main, Search,}2) Implement EventRoutingState on your app state
impl EventRoutingState<AppComponentId, AppContext> for AppState { fn focused(&self) -> Option<AppComponentId> { if self.search_open { Some(AppComponentId::Search) } else { Some(AppComponentId::Main) } }
fn modal(&self) -> Option<AppComponentId> { if self.search_open { Some(AppComponentId::Search) } else { None } }
fn binding_context(&self, id: AppComponentId) -> AppContext { match id { AppComponentId::Main => AppContext::Main, AppComponentId::Search => AppContext::Search, } }
fn default_context(&self) -> AppContext { AppContext::Main }}3) Register handlers
let mut bus: EventBus<AppState, Action, AppComponentId, AppContext> = EventBus::new();let keybindings: Keybindings<AppContext> = Keybindings::new();
bus.register(AppComponentId::Main, |event, state| { let actions = handle_main_event(&event.kind, state); if actions.is_empty() { HandlerResponse::ignored() } else { HandlerResponse::actions(actions) }});
bus.register_global(|event, _state| match event.kind { EventKind::Resize(_, height) => { HandlerResponse::action(Action::UiTerminalResize(height)).with_render() } _ => HandlerResponse::ignored(),});HandlerResponse
HandlerResponse controls propagation:
HandlerResponse::ignored() // No action, event continues routingHandlerResponse::action(action) // One action, event consumedHandlerResponse::actions(vec) // Multiple actions, event consumedHandlerResponse::actions_passthrough(v) // Multiple actions, event continues routing .with_render() // Force render even if state unchanged .with_consumed(true) // Explicitly set consumed flag4) Use run_with_bus
runtime .run_with_bus( terminal, &mut bus, &keybindings, |frame, area, state, render_ctx, event_ctx| { event_ctx.set_component_area(AppComponentId::Main, area); render_app(frame, area, state, render_ctx); }, |action| matches!(action, Action::Quit), ) .await?;Routing order
- Modal (if any)
- Hovered (mouse/scroll only)
- Focused
- Subscribers
- Global handlers
Resize and Tick events broadcast to subscribers (Resize/Tick) and global handlers regardless of consumed.