Core Widget Model
OSUI's user interface is built upon a flexible and extensible widget model. At its heart, this model separates rendering logic from data and behavior using Element
s and Component
s, all encapsulated within Widget
s.
Elements: The Renderable Unit
The Element
trait is the fundamental building block for anything that can be rendered on the screen.
pub trait Element: Send + Sync {
fn render(&mut self, scope: &mut RenderScope, render_context: &RenderContext);
fn after_render(&mut self, scope: &mut RenderScope, render_context: &RenderContext);
fn draw_child(&mut self, element: &Arc<Widget>);
fn event(&mut self, event: &dyn Event);
fn is_ghost(&mut self) -> bool;
// ... Any and AnyMut methods for downcasting
}
render
: This is where the element defines what it draws. It uses the providedRenderScope
to issue drawing commands (e.g.,draw_text
,draw_rect
). It does not handle child rendering; that's done by the system or a parent element'safter_render
.after_render
: Called after the element'srender
method and any extensions have processed it. This is typically where container elements (likeDiv
orFlexRow
) would recursively trigger the rendering of their children, using theRenderScope
for layout calculations.draw_child
: Used by thersx!
macro andRsx
structure to register a child widget with a parentElement
.event
: Allows the element to react to various system events (e.g., keyboard input, custom events).is_ghost
: A "ghost" element is one that primarily serves as a layout or logical container and does not draw itself, but manages the rendering of its children. Examples includeDiv
andFlexRow
. They receive aRenderScope
but might not add anything to itsrender_stack
directly.
By default, simple text (String
) is also an Element
, allowing you to embed strings directly in rsx!
.
Components: Attaching Behavior and Data
The Component
trait is an optional marker trait used for attaching arbitrary data or behavior to a Widget
.
pub trait Component: Send + Sync {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
}
Components are stored in a HashMap
within a Widget
, keyed by TypeId
. This allows a widget to dynamically acquire and retrieve different functionalities or data.
Why Components?
- Separation of Concerns: Keep common behaviors (like styling, focus, velocity) separate from the core rendering logic of an
Element
. - Flexibility: Widgets can gain new capabilities at runtime by adding or removing components without modifying their fundamental
Element
implementation. - Extensibility: Extensions often operate by attaching or querying specific components (e.g.,
Transform
for layout,Style
for appearance,Focused
for focus management).
Examples of built-in components include Transform
, Style
, Velocity
, Focused
, Handler<E>
, etc. You can easily define your own using the component!
macro.
Widgets: The Container for Elements and Components
The Widget
enum wraps an Element
and its associated Component
s. It's the primary way to interact with UI nodes in the OSUI tree.
pub enum Widget {
Static(StaticWidget),
Dynamic(DynWidget),
}
Widget
provides a unified interface to access its underlying Element
and Component
s, regardless of whether it's static or dynamic. Most interactions with the UI tree, such as drawing children or querying properties, are done via an Arc<Widget>
.
WidgetLoad
: Building Widgets
WidgetLoad
is a temporary struct used during the initial construction of a widget. It encapsulates the root BoxedElement
and a HashMap
of BoxedComponent
s.
pub struct WidgetLoad(BoxedElement, HashMap<TypeId, BoxedComponent>);
impl WidgetLoad {
pub fn new<E: Element + 'static>(e: E) -> Self { /* ... */ }
pub fn component<C: Component + 'static>(mut self, c: C) -> Self { /* ... */ }
pub fn set_component<C: Component + 'static>(mut self, c: C) -> Self { /* ... */ }
pub fn get<C: Component + 'static + Clone>(&self) -> Option<C> { /* ... */ }
}
It acts as a builder pattern for setting up a widget's initial state and attaching components, often used by the rsx!
macro.
StaticWidget
vs. DynWidget
OSUI differentiates between two types of widgets to optimize for different use cases:
-
StaticWidget
:- Purpose: Represents UI elements whose content and components do not change after initial creation.
- When to Use: Ideal for static text, immutable labels, or simple decorative elements that don't react to state changes.
- Performance: More efficient as they don't carry the overhead of dependency tracking or re-evaluation logic.
- Creation: Typically created directly via
Widget::new_static
orScreen::draw
for directElement
s, or byrsx!
when no dependencies are specified.
-
DynWidget
:- Purpose: Represents UI elements whose content can change reactively based on external state.
- When to Use: Essential for displaying dynamic data, user input fields, or any widget that needs to update its appearance or children when underlying data changes (e.g., a counter, a list that filters based on input).
- Mechanism: Stores a closure (
FnMut() -> WidgetLoad
) that rebuilds itsElement
andComponent
s. It tracks dependencies (viaDependencyHandler
) and automaticallyrefresh
es itself when those dependencies signal a change. - Performance: Carries a small overhead for dependency checking and re-evaluation.
- Creation: Created via
Widget::new_dyn
orScreen::draw_dyn
, or byrsx!
when state dependencies (%state_var
) are provided.
The distinction allows OSUI to efficiently render static parts of the UI while providing powerful reactivity for dynamic sections. When you use the rsx!
macro, OSUI automatically determines whether to create a StaticWidget
or DynWidget
based on the presence of dependencies.
Dependency Tracking
DynWidget
s are at the core of OSUI's reactivity. They listen for changes in their registered dependencies. When a dependency changes, the widget's internal load
closure is re-executed, effectively rebuilding its Element
and Component
s, leading to a re-render. This mechanism is explained in detail in State Management.