Skip to main content
Version: 0.1.0

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 dispatches crossterm::event::Events. Crucial for interactive applications.
  • TickExtension: Dispatches TickEvents at a specified rate, useful for animations or periodic updates.
  • VelocityExtension: Automatically updates the Transform of widgets that have a Velocity component, causing them to move.
  • IdExtension: Provides a way to retrieve specific widgets by a unique Id component. (Note: The current IdExtension implementation only stores a screen reference but doesn't actively do anything unless you manually call its get_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 use widget.get::<C>() to read components (like Transform or Style) 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 modifies Transform 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 the RenderScope (e.g., apply a global offset or filter) before the widget's render 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.