markdown --- sidebar_position: 3 title: Event Handling --- # Event Handling OSUI provides a flexible and type-safe event system that allows components to react to various occurrences, such as user input, internal state changes, or custom application-specific events. This guide explains how to define, emit, and listen for events using `on_event`, `emit_event`, and `emit_event_threaded`. ## Event Propagation Model In OSUI, events are propagated downwards through the component tree. When an event is emitted from a `Context`, it first triggers all registered handlers on that `Context`, and then recursively calls `emit_event` on all its child `Context`s. ## Defining Custom Events Any Rust type that implements `Send + Sync + Any + 'static` can be used as an event. Typically, you'll define custom `struct`s or `enum`s for your events to carry specific data. ```rust // Define a custom event #[derive(Debug, Clone)] pub struct ButtonClickEvent { pub button_id: usize, pub timestamp: std::time::Instant, } // Another custom event #[derive(Debug, Clone)] pub enum CustomAppEvent { Tick, ReloadData, } ``` ## Listening for Events: `cx.on_event()` Components can register event handlers using `cx.on_event()` to respond to specific event types. ```rust use osui::prelude::*; use std::sync::Arc; use std::time::Instant; // Custom event definition #[derive(Debug, Clone)] pub struct MyCustomEvent { pub value: String, } #[component] fn EventListener(cx: &Arc) -> View { let received_events = use_state(Vec::::new()); // Register an event handler for `MyCustomEvent` cx.on_event({ let received_events = received_events.clone(); // Clone State for the closure move |_ctx, event: &MyCustomEvent| { // This closure runs when MyCustomEvent is emitted let mut events_guard = received_events.get(); events_guard.push(format!("Received: {}", event.value)); // No need to call `update()` manually, `Inner` guard handles it on drop } }); rsx! { "Event Listener Component" @for msg in received_events.get_dl() { format!("- {}", msg) } }.view(&cx) } ``` * `cx.on_event, &T) + Send + Sync + 'static>(self: &Arc, handler: F)`: * `T`: The type of event you want to listen for (e.g., `MyCustomEvent`). * `F`: A closure that takes `&Arc` (the component's context) and `&T` (a reference to the event data). * You pass a closure that captures the necessary state (`received_events` in this case) and logic to execute when the event fires. ## Emitting Events: `cx.emit_event()` and `cx.emit_event_threaded()` Components or other parts of your application can send events using `cx.emit_event()` or `cx.emit_event_threaded()`. ### `cx.emit_event()` (Synchronous) `emit_event` processes event handlers sequentially in the current thread. ```rust // ... (MyCustomEvent and EventListener component definitions from above) #[component] fn EventSender(cx: &Arc) -> View { let button_clicks = use_state(0); // Simulate an action that emits an event let emit_click_event = { let cx = cx.clone(); // Clone Context for the closure let button_clicks = button_clicks.clone(); move || { let current_clicks = *button_clicks.get(); *button_clicks.get() += 1; let event = MyCustomEvent { value: format!("Button clicked {} times", current_clicks + 1), }; cx.emit_event(event); // Emit the event } }; // Simulate clicking the button every second use_effect( { let emit_click_event = emit_click_event.clone(); move || { loop { sleep(1000); // Wait 1 second emit_click_event(); } } }, &[], // No dependencies, run once on mount ); rsx! { format!("Button clicks: {}", button_clicks.get_dl()) }.view(&cx) } #[component] fn App(cx: &Arc) -> View { rsx! { // EventSender and EventListener are siblings in the tree. // Events emitted by EventSender will propagate down to EventListener. EventSender {} EventListener {} }.view(&cx) } pub fn main() { let engine = Console::new(); engine.run(App {}).expect("Failed to run App"); } ``` ### `cx.emit_event_threaded()` (Asynchronous) `emit_event_threaded` spawns a new thread for each registered event handler. This is useful for long-running or potentially blocking event handlers, preventing them from freezing your UI. ```rust // ... (MyCustomEvent, EventListener definitions) #[component] fn ThreadedEventSender(cx: &Arc) -> View { let cx_clone = cx.clone(); // Emit an event using `emit_event_threaded` on mount use_effect( move || { println!("Emitting threaded event on mount!"); let event = MyCustomEvent { value: "Threaded mount event".to_string(), }; cx_clone.emit_event_threaded(&event); // Notice the `&event` for threaded }, &[], // Run once on mount ); rsx! { "Threaded Event Sender" }.view(&cx) } #[component] fn AppWithThreaded(cx: &Arc) -> View { rsx! { ThreadedEventSender {} EventListener {} // This listener will receive the threaded event }.view(&cx) } // To run: engine.run(AppWithThreaded {}) ``` * `cx.emit_event_threaded(self: &Arc, event: &E)`: * Takes `&E` (a reference to the event), which must implement `Clone` because each spawned thread receives a clone of the event data. * Each handler for `E` will be executed in its own `std::thread::spawn`. ## Realistic Usage Scenario: Interactive Counter Let's create a more interactive counter that responds to keyboard input. ```rust use osui::prelude::*; use std::sync::Arc; use crossterm::event::{self, Event, KeyCode, KeyEventKind}; // Define a custom event for counter actions #[derive(Debug, Clone)] pub enum CounterAction { Increment, Decrement, } #[component] fn InteractiveCounter(cx: &Arc) -> View { let count = use_state(0); // Listen for CounterAction events cx.on_event({ let count = count.clone(); move |_ctx, action: &CounterAction| { let mut count_guard = count.get(); match action { CounterAction::Increment => *count_guard += 1, CounterAction::Decrement => *count_guard -= 1, } } }); rsx! { "Press 'q' to quit, '+' to increment, '-' to decrement." format!("Current Count: {}", count.get_dl()) }.view(&cx) } // A component (or `main` function logic) to poll keyboard input // and emit CounterAction events. #[component] fn KeyboardInputHandler(cx: &Arc) -> View { // This effect runs once on mount to start the keyboard polling thread use_effect( { let cx = cx.clone(); move || { let _ = crossterm::terminal::enable_raw_mode(); loop { if event::poll(std::time::Duration::from_millis(50)).unwrap() { if let Event::Key(key_event) = event::read().unwrap() { if key_event.kind == KeyEventKind::Press { match key_event.code { KeyCode::Char('+') => cx.emit_event(CounterAction::Increment), KeyCode::Char('-') => cx.emit_event(CounterAction::Decrement), KeyCode::Char('q') => { let _ = crossterm::terminal::disable_raw_mode(); cx.stop().expect("Failed to stop engine"); break; }, _ => {} } } } } } } }, &[], // Empty deps: run once on mount ); // This component doesn't render anything visible itself. // Its purpose is purely to handle input and emit events. rsx! { "" }.view(&cx) } pub fn main() { let engine = Console::new(); engine.run(AppWithKeyboardInput {}).expect("Failed to run interactive app"); let _ = crossterm::terminal::disable_raw_mode(); // Ensure raw mode is disabled on exit } #[component] fn AppWithKeyboardInput(cx: &Arc) -> View { rsx! { KeyboardInputHandler {} // Handles input and emits events InteractiveCounter {} // Listens for events and updates UI }.view(&cx) } ``` In this example: * `KeyboardInputHandler` runs in a separate thread (due to `use_effect`'s spawning behavior). * It polls for keyboard events using `crossterm`. * Upon detecting `+`, `-`, or `q`, it emits a `CounterAction` or `Stop` command to its `Context`. * `InteractiveCounter` listens for `CounterAction` events and updates its internal `count` state, which then causes it to re-render. * The `stop()` command, emitted by `KeyboardInputHandler`, instructs the `Console` engine to terminate its rendering loop. This showcases a full interaction loop using custom events for communication between components. **Next:** Explore specialized lifecycle management with `use_mount` and `use_mount_manual` in [Lifecycle Hooks](./04-lifecycle-hooks.md).