Using Extensions
OSUI features an extensible architecture that allows you to add global behaviors, custom rendering logic, or integrate third-party functionalities by implementing the Extension trait. This guide explains what extensions are, how to implement them, and how to register them with your Screen.
What are Extensions?
Extensions are separate modules or structs that provide lifecycle hooks for the Screen and can interact with widgets at a global level. They are ideal for:
- Global Event Handling: Such as processing keyboard or mouse input across all widgets (e.g.,
InputExtension). - Periodic Tasks: Running logic on a fixed interval (e.g.,
TickExtension,VelocityExtension). - Custom Rendering Overrides: Injecting logic before or after a widget's rendering.
- Managing Global State: Maintaining state accessible to multiple widgets or other extensions.
- Debugging or Logging: Observing the UI tree or rendering process.
The Extension Trait
The Extension trait defines the interface for all extensions:
pub trait Extension {
/// Called once when the screen starts running.
fn init(&mut self, _screen: Arc<Screen>) {}
/// Called when the screen is being closed.
fn on_close(&mut self, _screen: Arc<Screen>) {}
/// Called for each widget before its `render` method is invoked.
fn render_widget(&mut self, _scope: &mut RenderScope, _widget: &Arc<Widget>) {}
}
All methods have default empty implementations, meaning you only need to implement the hooks relevant to your extension's functionality.
Implementing a Custom Extension
Let's create a simple extension that logs when the screen initializes and when a widget is rendered.
use osui::prelude::*;
use std::sync::Arc;
pub struct MyLoggerExtension;
impl Extension for MyLoggerExtension {
fn init(&mut self, screen: Arc<Screen>) {
println!("MyLoggerExtension: Screen initialized!");
// You could store the screen Arc if needed for later interaction
// self.screen = Some(screen);
}
fn on_close(&mut self, screen: Arc<Screen>) {
println!("MyLoggerExtension: Screen closing!");
}
fn render_widget(&mut self, scope: &mut RenderScope, widget: &Arc<Widget>) {
// This hook is called for *every* widget about to be rendered.
// It's useful for debugging or applying global styles/transforms.
// Note: The widget here is the Arc<Widget>, not its inner Element.
// You can use `widget.get_elem()` to access the Element.
// For simplicity, we'll just log the widget's address and a component if it has one.
if let Some(t) = widget.get::<Transform>() {
println!(
"MyLoggerExtension: Rendering widget at {:p} with transform: x={:?} y={:?}",
Arc::as_ptr(widget), t.x, t.y
);
} else {
println!("MyLoggerExtension: Rendering widget at {:p}", Arc::as_ptr(widget));
}
}
}
Registering an Extension
To activate your extension, you must register it with the Screen instance using the screen.extension() method. This is typically done at the beginning of your main function.
use osui::prelude::*;
use std::sync::Arc; // Needed for Arc<Screen> in the main function
// ... (MyLoggerExtension definition from above) ...
fn main() -> std::io::Result<()> {
let screen = Screen::new();
// Register your custom extension
screen.extension(MyLoggerExtension);
// Register other built-in extensions if needed
screen.extension(InputExtension);
screen.extension(TickExtension(10)); // Example: Tick every 10ms
rsx! {
Div { "Hello, OSUI!" }
}.draw(&screen);
screen.run()
}
Once registered, the Screen will call the appropriate lifecycle methods of your extension at the right times during its operation.
Built-in Extensions
OSUI comes with several useful built-in extensions:
InputExtension: Handles keyboard input and dispatchescrossterm::event::Events. Crucial for interactive applications.TickExtension: DispatchesTickEvents at a specified rate, useful for animations or periodic updates.VelocityExtension: Automatically updates theTransformof widgets that have aVelocitycomponent, causing them to move.IdExtension: Provides a way to retrieve specific widgets by a uniqueIdcomponent. (Note: The currentIdExtensionimplementation only stores a screen reference but doesn't actively do anything unless you manually call itsget_elementmethod.)
You use these built-in extensions by simply calling screen.extension(...) with an instance of them, just like MyLoggerExtension.
Interaction between Extensions and Widgets
Extensions can interact with widgets in various ways:
- Reading Components: Within
render_widgetor other hooks, an extension can usewidget.get::<C>()to read components (likeTransformorStyle) attached to a widget, influencing how it renders or behaves. - Setting Components: An extension can use
widget.set_component(c)to dynamically add or modify components on a widget. For example,VelocityExtensionmodifiesTransformcomponents. - Dispatching Events: Extensions can dispatch custom events to widgets using
widget.event(&my_custom_event). - Modifying RenderScope: In
render_widget, an extension can directly modify theRenderScope(e.g., apply a global offset or filter) before the widget'srendermethod is called.
By leveraging extensions, you can keep your core UI logic clean and declarative, while offloading cross-cutting concerns or global features into reusable and modular units.