Custom Events
Beyond crossterm
events, OSUI's event system is designed to be extensible, allowing you to define and dispatch your own custom event types. This is essential for building complex applications where different parts of your UI or internal logic need to communicate.
1. Defining a Custom Event
You define custom events using the event!
macro, which automatically implements the necessary Event
trait for your struct.
Example: A UserLoginEvent
// In your `events.rs` or `lib.rs` file
use osui::prelude::*; // Import prelude for `event!` macro
event!(UserLoginEvent {
username: String,
success: bool,
});
// A simple unit event
event!(UserLoggedOut);
These events automatically get Debug
and Clone
derives, and implement the osui::extensions::Event
trait.
2. Dispatching a Custom Event
Events are dispatched to widgets using the widget.event(&my_event)
method. The Screen
's main loop automatically dispatches crossterm::event::Event
s to all top-level widgets. For custom events, you will typically dispatch them manually from:
- Event Handlers: A
Handler<crossterm::event::Event>
that captures input can then dispatch your custom event. - Background Threads: A thread performing some work can dispatch an event to the UI when its work is done.
- Custom Elements: An element's internal logic might dispatch an event based on user interaction or state changes.
- Extensions: An
Extension
might dispatch events to trigger behavior across multiple widgets.
To dispatch an event, you need an Arc<Widget>
reference.
Example: Dispatching from an Input
handler
Let's imagine you have an Input
widget where, when the user presses Enter, you want to dispatch a UserLoginEvent
.
use osui::prelude::*;
use std::sync::Arc;
use crossterm::event::{KeyCode, KeyEvent, Event as CrosstermEvent};
event!(UserLoginEvent {
username: String,
success: bool,
});
fn main() -> std::io::Result<()> {
let screen = Screen::new();
screen.extension(InputExtension);
let username_input_state = use_state(String::new());
rsx! {
// This handler is attached to the root widget, listening for ALL input events.
// It will then dispatch a custom UserLoginEvent.
@Handler::new({
let screen = screen.clone(); // Clone screen to get all widgets
let username_input_state = username_input_state.clone(); // Clone state to read username
move |current_widget, event: &CrosstermEvent| {
if let CrosstermEvent::Key(KeyEvent { code: KeyCode::Enter, .. }) = event {
let entered_username = username_input_state.get_dl();
// Simulate login logic
let login_success = entered_username == "admin";
// Create and dispatch the custom event to ALL widgets on the screen
// This is inefficient; normally you'd dispatch to specific widgets or use state updates
let login_event = UserLoginEvent {
username: entered_username,
success: login_success,
};
for w in screen.widgets.lock().unwrap().iter() {
w.event(&login_event);
}
}
}
});
Div {
"Enter username (type 'admin' for success):"
@Transform::new().y(1).dimensions(20, 1);
@Style { background: Background::Outline(0x888888) };
Input, state: username_input_state, { }
}
}.draw(&screen);
screen.run()
}
3. Handling a Custom Event
Widgets (or other entities) can subscribe to your custom event types using the Handler<E>
component.
Example: A LoginStatusDisplay
Widget
Now, let's create a widget that reacts to our UserLoginEvent
:
use osui::prelude::*;
use std::sync::Arc;
// Make sure UserLoginEvent is defined and visible
event!(UserLoginEvent {
username: String,
success: bool,
});
pub struct LoginStatusDisplay {
status_text: State<String>,
}
impl LoginStatusDisplay {
pub fn new() -> Self {
Self {
status_text: use_state("Awaiting login...".to_string()),
}
}
}
impl Element for LoginStatusDisplay {
fn render(&mut self, scope: &mut RenderScope) {
scope.draw_text(0, 0, &self.status_text.get_dl());
}
// Implement as_any, as_any_mut, draw_child, after_render as needed for container logic
fn as_any(&self) -> &dyn Any { self }
fn as_any_mut(&mut self) -> &mut dyn Any { self }
}
fn main() -> std::io::Result<()> {
let screen = Screen::new();
screen.extension(InputExtension);
let username_input_state = use_state(String::new());
let login_status_display = Arc::new(Widget::new_static(Box::new(LoginStatusDisplay::new())));
// Attach the handler for UserLoginEvent to the LoginStatusDisplay widget
login_status_display.component(Handler::new({
let status_state = login_status_display.get::<LoginStatusDisplay>().unwrap().status_text.clone();
move |_, event: &UserLoginEvent| {
if event.success {
status_state.set(format!("Welcome, {}!", event.username));
} else {
status_state.set(format!("Login failed for {}!", event.username));
}
}
}));
// Draw the LoginStatusDisplay and the Input field
screen.draw_widget(login_status_display.clone());
rsx! {
// This handler is now attached to a separate, root element.
@Handler::new({
let screen = screen.clone();
let username_input_state = username_input_state.clone();
move |_, event: &CrosstermEvent| {
if let CrosstermEvent::Key(KeyEvent { code: KeyCode::Enter, .. }) = event {
let entered_username = username_input_state.get_dl();
let login_success = entered_username == "admin";
let login_event = UserLoginEvent {
username: entered_username,
success: login_success,
};
// Dispatch to the specific login_status_display widget
// This is more efficient than iterating all screen widgets.
if let Some(w) = screen.widgets.lock().unwrap().iter().find(|w| {
// A more robust way to find the target widget, perhaps by an ID component
w.get_elem().as_any().is::<LoginStatusDisplay>()
}) {
w.event(&login_event);
}
}
}
});
@Transform::new().y(0); // Position the input above the status
Div {
"Enter username (type 'admin' for success):"
@Transform::new().y(1).dimensions(20, 1);
@Style { background: Background::Outline(0x888888) };
Input, state: username_input_state, { }
}
}.draw(&screen); // Add the input via rsx!
screen.run()
}
In this enhanced example:
- The
Input
field (rsx!
) has its ownHandler
forcrossterm::event::Event
. - When
Enter
is pressed in theInput
's handler, it constructs aUserLoginEvent
. - Instead of iterating all widgets on the screen, it tries to find the
LoginStatusDisplay
widget (e.g., by checking its innerElement
type, though using anId
component is more robust for production). - It dispatches the
UserLoginEvent
directly to that specificlogin_status_display
widget. - The
Handler<UserLoginEvent>
attached tologin_status_display
then updates its internalstatus_text
State
. - Because
status_text
is aState
, andLoginStatusDisplay
is aStaticWidget
whoserender
method readsstatus_text
, the display updates automatically.
Event Propagation (Important)
OSUI's current event system is primarily a global dispatch model.
InputExtension
(andTickExtension
) dispatches events to all top-level widgets (Screen.widgets.lock().unwrap().iter()
).Widget::event()
then checks forHandler
components and calls theElement::event
method.
This means if you have multiple Handler
s for the same event type on different widgets, they will all be called. For more complex scenarios, you might need to build your own event routing or bubbling system on top of this, or prefer using State
updates for communication over direct event dispatch between deeply nested components.
However, for simple communication like the UserLoginEvent
example, dispatching directly to the target widget (once found) is efficient.
Custom events are a powerful tool for decoupling concerns and enabling clear communication between different parts of your OSUI application, allowing you to build more complex and modular UIs.