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::Event
s. Crucial for interactive applications.TickExtension
: DispatchesTickEvent
s at a specified rate, useful for animations or periodic updates.VelocityExtension
: Automatically updates theTransform
of widgets that have aVelocity
component, causing them to move.IdExtension
: Provides a way to retrieve specific widgets by a uniqueId
component. (Note: The currentIdExtension
implementation only stores a screen reference but doesn't actively do anything unless you manually call itsget_element
method.)
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_widget
or other hooks, an extension can usewidget.get::<C>()
to read components (likeTransform
orStyle
) 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,VelocityExtension
modifiesTransform
components. - 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'srender
method 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.