Files
2026-01-31 21:01:30 +01:00

9.5 KiB

sidebar_position, title
sidebar_position title
3 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 Contexts.

Defining Custom Events

Any Rust type that implements Send + Sync + Any + 'static can be used as an event. Typically, you'll define custom structs or enums for your events to carry specific data.

// 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.

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<Context>) -> View {
    let received_events = use_state(Vec::<String>::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: Any + 'static, F: Fn(&Arc<Self>, &T) + Send + Sync + 'static>(self: &Arc<Self>, handler: F):
    • T: The type of event you want to listen for (e.g., MyCustomEvent).
    • F: A closure that takes &Arc<Context> (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.

// ... (MyCustomEvent and EventListener component definitions from above)

#[component]
fn EventSender(cx: &Arc<Context>) -> 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<Context>) -> 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.

// ... (MyCustomEvent, EventListener definitions)

#[component]
fn ThreadedEventSender(cx: &Arc<Context>) -> 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<Context>) -> View {
    rsx! {
        ThreadedEventSender {}
        EventListener {} // This listener will receive the threaded event
    }.view(&cx)
}

// To run: engine.run(AppWithThreaded {})
  • cx.emit_event_threaded<E: Any + Send + Sync + Clone + 'static>(self: &Arc<Self>, 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.

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<Context>) -> 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<Context>) -> 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<Context>) -> 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.