Component Model
OSUI's architecture is fundamentally component-based, drawing inspiration from modern GUI frameworks. This model centers around three core entities: Elements, Components, and Widgets, each serving a distinct purpose in building and managing the UI.
Element: The Renderable Unit
An Element is the foundational unit that knows how to render itself to the screen. It encapsulates the drawing logic and, for container elements, how to manage and draw its children.
- Responsibility:
- Rendering: Implementing the
rendermethod to draw its visual representation using aRenderScope. - Child Management: For container elements, implementing
draw_childto accept children andafter_renderto recursively render them. - Event Handling: Optionally implementing
eventto react to specific dispatched events.
- Rendering: Implementing the
- Characteristics:
Elementis a trait (pub trait Element: Send + Sync).- Common
Elementimplementations includeString(for text),Div,FlexRow,Input,Heading, etc. - An
Elementinstance typically holds its own internal state and references to its children if it's a container.
- Why a Trait Object?:
Elementis used as a trait object (Box<dyn Element>) because the type of element within a UI tree needs to be dynamic at runtime. AWidgetcan hold anyElementthat implements the trait, regardless of its concrete type.
Example Element (Div Simplified):
// src/elements/div.rs
pub struct Div {
children: Vec<Arc<Widget>>, // Stores children as Arc<Widget>
// ... other internal state like calculated_size
}
impl Element for Div {
fn render(&mut self, scope: &mut RenderScope) {
// Queue drawing commands for the Div itself (e.g., background)
// scope.draw_rect(...);
}
fn after_render(&mut self, scope: &mut RenderScope) {
// Iterate and render `self.children`
for child_widget in &self.children {
// ... setup scope for child, call child_widget.get_elem().render(scope) ...
}
}
fn draw_child(&mut self, element: &Arc<Widget>) {
// Add the new child to internal list
self.children.push(element.clone());
// Inform the screen not to render this child independently
element.inject(|w| w.component(NoRenderRoot));
}
// ... as_any, as_any_mut, event methods
}
Component: Data and Behavior Extension
A Component is a distinct piece of data or behavior that can be attached to a Widget. Unlike Elements, which define the primary visual and structural role, Components augment or modify that role.
- Responsibility:
- Data Storage: Holding configuration (e.g.,
Transform,Style), state (e.g.,State<T>), or IDs (e.g.,Id). - Behavior Attachment: Providing specific behaviors, often through closures (e.g.,
Handler<E>).
- Data Storage: Holding configuration (e.g.,
- Characteristics:
Componentis a trait (pub trait Component: Send + Sync).- Implemented by structs often defined with the
component!macro. - A
WidgetstoresComponents in aHashMap<TypeId, Box<dyn Component>>, meaning only one component of a given concrete type can be attached to a widget at a time (though it can be replaced). - Components are designed to be independent and reusable.
- Why a Trait Object?: Similar to
Element,Components are stored as trait objects (Box<dyn Component>) to allow aWidgetto hold various, arbitrary component types.
Examples of Components:
Transform: Defines layout properties (position, size, padding, margin).Style: Defines visual properties (background, foreground colors).State<T>: A reactive state variable. WhileState<T>is a generic struct, OSUI internally uses it as aComponentfor its reactivity.Handler<E>: A component that allows a widget to respond to specific events.Id: A simpleusizefor uniquely identifying a widget.Velocity: Defines movement properties for an element.
Widget: The Container and Lifecycle Manager
The Widget enum (Static(StaticWidget) or Dynamic(DynWidget)) is the central wrapper that brings Elements and Components together. It manages their lifecycle, provides access to them, and facilitates interactions like event dispatching and reactive updates.
- Responsibility:
- Aggregation: Holds one
Box<dyn Element>and aHashMap<TypeId, Box<dyn Component>>. - Lifecycle: For
DynWidgets, manages the rebuilding process based on dependencies. - Access: Provides methods to
get_elem()(access the innerElement),get<C>()(retrieve aComponent),set_component<C>()(add/replace aComponent). - Event Dispatch: Calls
Handlercomponents and the innerElement::eventwhen an event is dispatched to the widget. - Thread Safety: Uses
Mutexes internally to ensure safe concurrent access to itsElementandComponents from different threads (e.g., render thread, input thread, background threads updating state).
- Aggregation: Holds one
- Characteristics:
Widgetis anenumwithStaticWidgetandDynWidgetvariants.- Always wrapped in an
Arc<Widget>for shared ownership and efficient cloning, reflecting its place in the UI tree. StaticWidget: Holds a fixedElementinstance. Its content doesn't change unless explicitly replaced.DynWidget: Holds a closure that can rebuild itsElementand initialComponents. It tracksDependencyHandlers and automatically refreshes when they change.
Example Widget Structure:
// Internally in OSUI:
pub enum Widget {
Static(StaticWidget), // Wrapper for fixed content
Dynamic(DynWidget), // Wrapper for reactive content
}
// Simplified StaticWidget structure:
pub struct StaticWidget(
Mutex<BoxedElement>, // The main Element instance
Mutex<HashMap<TypeId, BoxedComponent>>, // Attached components
);
// Simplified DynWidget structure:
pub struct DynWidget(
Mutex<BoxedElement>, // The main Element instance
Mutex<HashMap<TypeId, BoxedComponent>>, // Attached components
Mutex<Box<dyn FnMut() -> WidgetLoad>>, // The function to rebuild the Element/Components
// ... dependencies and inject closure
);
How They Work Together (The Flow)
- Declarative UI (
rsx!macro): You define your UI using thersx!macro.Div { ... }generates aWidgetLoadcontaining aDivElement.@Transform(...)adds aTransformComponentto thisWidgetLoad.%my_stateaddsmy_state(aState<T>) as aDependencyHandlerto theDynWidget's list.
Screen::draw(): Thersx!output (anRsxobject) is passed toScreen::draw()(ordraw_parent).- The
ScreencreatesArc<Widget>instances (eitherStaticorDynamic) from theWidgetLoaddefinitions. - These
Arc<Widget>are stored in theScreen'swidgetslist, forming the top level of the UI tree. - For nested elements, the parent's
Element::draw_childis called, allowing the parent to store references to its children. Crucially, children rendered by a parent are marked withNoRenderRootto prevent theScreenfrom rendering them independently.
- The
- Rendering Loop (
Screen::render):- For each
Arc<Widget>in its top-levelwidgetslist:- It checks for
NoRenderorNoRenderRootto decide if this widget should be directly rendered or if its parent is handling it. - It obtains the widget's
TransformandStyleComponents and sets them on a freshRenderScope. - It calls
Extension::render_widgetfor all registered extensions. - It calls
widget.get_elem().render(scope)to let theElementqueue its drawing commands. - It calls
widget.get_elem().after_render(scope). This is where containerElements iterate through their own children, setting up a newRenderScopecontext for each child and recursively callingchild_widget.get_elem().renderandafter_render. - Finally,
scope.draw()is called to flush the accumulated commands to the terminal. - For
DynWidgets,widget.auto_refresh()is called, which checks dependencies and rebuilds theElementif needed.
- It checks for
- For each
- Event Handling (
Widget::event):- When an event occurs (e.g., keyboard input from
InputExtension),Screendispatches it to all top-level widgets by callingwidget.event(&event). Widget::eventfirst checks if aHandler<E>Componentis present for that event type and calls its closure.- Then, it calls
widget.get_elem().event(&event), allowing theElementitself to react.
- When an event occurs (e.g., keyboard input from
This robust component model allows for clear separation of concerns, reusability of UI parts, and a powerful reactive system, making it possible to build complex and dynamic terminal user interfaces.