Building Custom Elements
OSUI's component-based architecture allows you to create your own UI elements by implementing the Element
trait. This guide details how to define custom elements, render them, handle their children, and process events.
The Element
Trait
The Element
trait is the core interface for any renderable UI component in OSUI. It defines methods that the rendering engine calls during the UI lifecycle.
pub trait Element: Send + Sync {
/// Called to perform rendering for the element.
fn render(&mut self, scope: &mut RenderScope);
/// Called after rendering, for follow-up logic or cleanup.
fn after_render(&mut self, scope: &mut RenderScope);
/// Called to draw child widgets, if any.
fn draw_child(&mut self, element: &Arc<Widget>);
/// Called when an event occurs.
fn event(&mut self, event: &dyn Event);
/// Returns a type-erased reference to this object.
fn as_any(&self) -> &dyn Any;
/// Returns a mutable type-erased reference to this object.
fn as_any_mut(&mut self) -> &mut dyn Any;
}
Defining a Simple Custom Element
Let's create a very basic Box
element that just draws a rectangle and its text content.
use osui::prelude::*;
use std::sync::Arc;
pub struct MyBox {
// Children are typically stored if your element acts as a container
children: Vec<Arc<Widget>>,
// Internal state for managing the box's size, if not determined by children
calculated_size: (u16, u16),
pub border_color: u32, // Public field for RSX properties
pub fill_color: u32, // Public field for RSX properties
}
impl MyBox {
// Constructor for use with `rsx!`
pub fn new() -> Self {
MyBox {
children: Vec::new(),
calculated_size: (0, 0),
border_color: 0xAAAAAA, // Default gray border
fill_color: 0x333333, // Default dark fill
}
}
}
impl Element for MyBox {
// Called when the element needs to render its own content.
fn render(&mut self, scope: &mut RenderScope) {
// Draw the background rectangle first
scope.draw_rect(
0,
0,
scope.get_transform().width,
scope.get_transform().height,
self.fill_color,
);
// Draw a border (outline)
// Note: RenderScope's draw_rect doesn't draw outlines directly,
// so we'd typically rely on `Style::Background::Outline` applied
// as a component to the widget containing this element.
// For a simple filled box, we can just draw the background.
// If we wanted a distinct border *inside* the box, it'd be more complex.
// For simplicity, we'll assume `Style` component handles outlines.
// Get the current accumulated size from the scope,
// which might have been influenced by child rendering in `after_render`.
let (width, height) = scope.get_size_or(self.calculated_size.0, self.calculated_size.1);
// Ensure the scope tracks at least this area for its own calculations.
scope.use_area(width, height);
}
// Called after the element's `render` method and its children's `render` methods.
// This is where container elements typically render their children.
fn after_render(&mut self, scope: &mut RenderScope) {
// Store the original parent size for restoration later
let (original_parent_width, original_parent_height) = scope.get_parent_size();
// Pass this element's resolved size as the parent size for its children.
// This is crucial for children to correctly calculate their `Full` or `Center` dimensions.
let self_transform = scope.get_transform().clone(); // Clone resolved transform for current element
scope.set_parent_size(self_transform.width, self_transform.height);
// Track max dimensions used by children for this box's overall size
let mut max_child_width = 0;
let mut max_child_height = 0;
for child_widget in &self.children {
// Children marked with NoRender or NoRenderRoot are handled by their direct parent
// or the screen, not by this specific element's `after_render`.
// This prevents double-rendering if they are also top-level widgets.
if child_widget.get::<NoRender>().is_some() {
continue;
}
scope.clear(); // Clear the scope for each child's rendering context
// Apply any Transform or Style components attached directly to the child widget.
if let Some(child_style) = child_widget.get() {
scope.set_style(child_style);
}
if let Some(child_transform_comp) = child_widget.get() {
scope.set_transform(&child_transform_comp);
}
// Render the child's own content. This fills its render_stack.
child_widget.get_elem().render(scope);
// Re-apply the transform *after* child render to ensure any content-based sizing
// (Dimension::Content) is reflected in the child's `raw_transform.width/height`.
if let Some(child_transform_comp) = child_widget.get() {
scope.set_transform(&child_transform_comp);
}
// Get the child's now-resolved raw transform (position, size, padding)
let child_raw_transform = scope.get_transform_mut();
// Offset the child's actual drawing coordinates by the parent's position and padding.
// This ensures children are drawn relative to their parent's content area.
child_raw_transform.x += self_transform.x + self_transform.px;
child_raw_transform.y += self_transform.y + self_transform.py;
child_raw_transform.px += self_transform.px; // Accumulate padding
child_raw_transform.py += self_transform.py; // Accumulate padding
// Update the parent's (MyBox's) effective size based on its children.
// This is crucial for MyBox to "auto-size" if it's `Dimension::Content`.
max_child_width = max_child_width.max(
child_raw_transform.x
+ child_raw_transform.width
+ (child_raw_transform.px * 2)
- self_transform.x
- self_transform.px, // Relative to parent content area
);
max_child_height = max_child_height.max(
child_raw_transform.y
+ child_raw_transform.height
+ (child_raw_transform.py * 2)
- self_transform.y
- self_transform.py, // Relative to parent content area
);
scope.draw(); // Draw the child's accumulated render stack to the terminal.
child_widget.get_elem().after_render(scope); // Recursively call after_render for child
}
// After all children are processed, update MyBox's calculated size.
// Add back MyBox's own padding to the children's max extent.
self.calculated_size = (
max_child_width + (self_transform.px * 2),
max_child_height + (self_transform.py * 2),
);
// Ensure the scope's own transform reflects the newly calculated size
scope.use_area(self.calculated_size.0, self.calculated_size.1);
// Restore the parent size for subsequent elements at this level.
scope.set_parent_size(original_parent_width, original_parent_height);
}
// Called when a child widget is added to this element via `rsx!`.
fn draw_child(&mut self, element: &Arc<Widget>) {
// Mark the child as `NoRenderRoot` so the Screen doesn't try to render it directly.
// This indicates that its rendering will be managed by this parent element's `after_render`.
element.inject(|w| w.component(NoRenderRoot));
self.children.push(element.clone());
}
// Handles incoming events for this element.
fn event(&mut self, event: &dyn Event) {
// You can check for specific event types
if let Some(key_event) = event.get::<crossterm::event::Event>() {
// Example: Respond to a key press
if let crossterm::event::Event::Key(ke) = key_event {
// println!("MyBox received key: {:?}", ke.code); // For debugging
}
}
// You might also want to pass events to children,
// though OSUI's default event system often dispatches globally.
}
// Required for downcasting the trait object.
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
Using Your Custom Element in rsx!
After defining MyBox
, you can use it just like any other built-in element:
use osui::prelude::*;
// In your main or app function:
rsx! {
@Transform::new().dimensions(50, 10); // Set a fixed size for the box
MyBox, border_color: 0xFF0000, fill_color: 0x0000FF, {
("Hello from inside MyBox!", 0xFFFFFF)
Div {
"Another nested div!"
}
}
}.draw(&screen);
Key Considerations for Custom Elements
render()
vs.after_render()
:render()
: Use this for drawing the element's own content (e.g., text, background shapes). It should use theRenderScope
to queue drawing commands.after_render()
: Use this for container logic, specifically iterating throughself.children
and rendering them. It needs to manage theRenderScope
's parent size and transform for each child.
draw_child()
: This method is called by thersx!
macro when you nest elements inside your custom element. You must store theArc<Widget>
in aVec
or similar structure.- Crucially, you should also call
element.inject(|w| w.component(NoRenderRoot));
. This tells the mainScreen
loop to not render this child directly at the root level, as its rendering will be managed by its parent (MyBox
in this case).
- Crucially, you should also call
as_any()
/as_any_mut()
: These are boilerplate methods required for downcasting trait objects, allowing you to retrieve specificElement
orComponent
types from aBox<dyn Element>
orBox<dyn Component>
.RenderScope
Usage:scope.set_transform(&t)
: Applies aTransform
component's rules to calculate the absolute position and size (RawTransform
) for the current scope, based on its parent's size.scope.get_transform()
/scope.get_transform_mut()
: Accesses theRawTransform
that represents the current element's resolved position and size.scope.set_parent_size(width, height)
: Critical for nested elements. Before rendering a child, set thescope
's parent size to this element's resolved size so the child can correctly resolve itsDimension::Full
orPosition::Center
values. Remember to restore the original parent size after processing all children.scope.use_area(width, height)
: In yourrender
method, if your element's size depends on its content or a fixed size, use this to tell theRenderScope
what minimum area your element occupies. This helps when the element'sDimension
isContent
.
- State Management: If your custom element needs to hold dynamic data, consider using
osui::state::State<T>
for reactive updates, especially if you want your element to trigger re-renders of itself or its children when its internal data changes.
By following these guidelines, you can create sophisticated and well-integrated custom UI elements that extend OSUI's capabilities to fit your application's unique needs.