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 Elements and Components, all encapsulated within Widgets.
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 providedRenderScopeto 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'srendermethod and any extensions have processed it. This is typically where container elements (likeDivorFlexRow) would recursively trigger the rendering of their children, using theRenderScopefor layout calculations.draw_child: Used by thersx!macro andRsxstructure 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 includeDivandFlexRow. They receive aRenderScopebut might not add anything to itsrender_stackdirectly.
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
Elementimplementation. - Extensibility: Extensions often operate by attaching or querying specific components (e.g.,
Transformfor layout,Stylefor appearance,Focusedfor 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 Components. 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 Components, 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 BoxedComponents.
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_staticorScreen::drawfor directElements, 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 itsElementandComponents. It tracks dependencies (viaDependencyHandler) and automaticallyrefreshes itself when those dependencies signal a change. - Performance: Carries a small overhead for dependency checking and re-evaluation.
- Creation: Created via
Widget::new_dynorScreen::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
DynWidgets 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 Components, leading to a re-render. This mechanism is explained in detail in State Management.