Extension System
OSUI's extension system is a powerful and flexible mechanism for adding global, cross-cutting concerns to your application without cluttering individual widget implementations. It allows you to inject custom logic into the Screen's lifecycle and rendering pipeline.
Why an Extension System?
In UI development, certain functionalities are not specific to a single widget but affect the entire application or many widgets. Examples include:
- Global Input Handling: Capturing keyboard events and routing them to relevant widgets.
- Periodic Updates: Driving animations or time-based logic across the UI.
- Custom Debugging/Logging: Observing the rendering process or widget tree.
- Theming/Styling Overrides: Applying consistent visual modifications dynamically.
- Data Persistence/Loading: Interacting with external systems at application startup/shutdown.
Instead of scattering this logic throughout your main function or within every widget, the extension system provides a centralized, modular approach.
The Extension Trait
The core of the system is the Extension trait, which defines a set of lifecycle hooks that the Screen will call at specific points.
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>) {}
}
init(&mut self, screen: Arc<Screen>):- When: Called exactly once when
Screen::run()is invoked, before the main rendering loop begins. - Purpose: Ideal for one-time setup tasks like spawning background threads (e.g., for input polling or tick generation), initializing external resources, or setting up global state the extension will manage. The
Arc<Screen>allows the extension to interact back with the screen (e.g., closing it, adding new widgets).
- When: Called exactly once when
on_close(&mut self, screen: Arc<Screen>):- When: Called exactly once when
Screen::close()is invoked and the main rendering loop has exited. - Purpose: Cleanup. Restore terminal settings (like
InputExtensiondisabling raw mode), release resources, save data, or perform final logging.
- When: Called exactly once when
render_widget(&mut self, scope: &mut RenderScope, widget: &Arc<Widget>):- When: Called for every top-level
Arc<Widget>in theScreen'swidgetslist, during each frame'sScreen::render()cycle. It's called before the widget'sElement::rendermethod. - Purpose: This is a powerful hook for inspecting or modifying the rendering context (
RenderScope) or theWidgetitself.- Inspection: You can use
widget.get::<C>()to check a widget's components (e.g., itsTransformorStyle). - Modification: You can use
widget.set_component(c)to dynamically add or change components (e.g.,VelocityExtensionmodifiesTransform). You can also directly modify theRenderScope(e.g., adding an offset, changing its style, or drawing overlay content). - Filtering/Debugging: Skip rendering certain widgets based on custom logic, or log their state.
- Inspection: You can use
- When: Called for every top-level
How Extensions are Integrated
- Instantiation: You create an instance of your struct that implements
Extension. - Registration: You register the instance with your
Screenusingscreen.extension(my_extension_instance). This typically happens at the start of yourmainfunction.- The
ScreenstoresArc<Mutex<Box<dyn Extension>>>to allow multiple extensions, shared access, and dynamic dispatch.
- The
- Execution: The
Screen's mainrun()andrender()methods are hardwired to call the respectiveExtensiontrait methods at the appropriate times.
Example: Custom Logging Extension
use osui::prelude::*;
use std::sync::Arc;
pub struct CustomLoggingExtension;
impl Extension for CustomLoggingExtension {
fn init(&mut self, screen: Arc<Screen>) {
println!("[LOG] CustomLoggingExtension initialized for screen {:p}", Arc::as_ptr(&screen));
}
fn on_close(&mut self, screen: Arc<Screen>) {
println!("[LOG] CustomLoggingExtension closing for screen {:p}", Arc::as_ptr(&screen));
}
fn render_widget(&mut self, scope: &mut RenderScope, widget: &Arc<Widget>) {
// Log the coordinates and size of every widget about to be rendered
let raw_transform = scope.get_transform(); // Get the already resolved raw transform
println!(
"[LOG] Rendering widget {:p} at ({}, {}) size ({}, {})",
Arc::as_ptr(widget),
raw_transform.x, raw_transform.y,
raw_transform.width, raw_transform.height
);
// Example: Apply a global offset for debugging
// let mut current_transform = scope.get_transform_mut();
// current_transform.x += 1;
// current_transform.y += 1;
}
}
fn main() -> std::io::Result<()> {
let screen = Screen::new();
screen.extension(InputExtension); // Always useful for interaction
screen.extension(CustomLoggingExtension); // Register our custom extension
rsx! {
Div { "Hello" }
@Transform::new().x(10).y(5);
Div { "World" }
}.draw(&screen);
screen.run()
}
When you run this, you'll see console output from CustomLoggingExtension as the screen initializes, renders each widget, and closes.
Benefits of the Extension System
- Modularity: Keeps distinct functionalities separate, improving code organization.
- Reusability: Extensions can be easily reused across different OSUI applications.
- Flexibility: Allows injection of custom behavior without modifying OSUI's core library code.
- Separation of Concerns: UI rendering logic is in
Elements, state is inStates, and cross-cutting behaviors are inExtensions.
By leveraging the extension system, developers can build highly customized and feature-rich terminal applications with a clean and maintainable codebase.