Skip to main content
Version: 0.2.0

The Component Model

At the core of OSUI's design is a robust component model, defining how UI elements are created, composed, and managed. This model provides structure, promotes reusability, and facilitates a clear separation of concerns within your TUI application.

ComponentImpl: The Building Block

The ComponentImpl trait is the most fundamental concept for any renderable unit in OSUI:

pub trait ComponentImpl: Send + Sync {
/// Renders the component within the given context, returning a View
fn call(&self, cx: &Arc<Context>) -> View;
}
  • fn call(&self, cx: &Arc<Context>) -> View: This method is where a component's rendering logic resides. It takes a reference to the component instance itself and its Context (cx), and it must return a View. The View is OSUI's abstraction for "what to draw," essentially a closure that will populate a DrawContext with drawing instructions later in the rendering pipeline.
  • Send + Sync: Components must be Send and Sync to ensure they can be safely passed between threads, as OSUI leverages concurrency for various operations (e.g., use_effect hooks).

Most often, you won't implement ComponentImpl manually. Instead, you'll use the #[component] procedural macro:

#[component]
fn MyComponent(cx: &Arc<Context>, some_prop: &String) -> View {
// Component logic and RSX here
rsx! {
format!("Prop: {}", some_prop)
}.view(&cx)
}

The #[component] macro automatically generates a struct for MyComponent (with some_prop: String as a field) and implements ComponentImpl for it, delegating the call method to your function's body.

Context: The Component's Identity and State

Every active instance of a component in the UI tree has its own Context (osui::component::context::Context). The Context is the component's runtime identity and central hub for managing its internal state and interactions:

pub struct Context {
component: AccessCell<Component>,
view: AccessCell<View>,
event_handlers: AccessCell<HashMap<TypeId, Vec<EventHandler>>>,
pub(crate) scopes: Mutex<Vec<Arc<Scope>>>,
executor: Arc<dyn CommandExecutor>,
}

Why Context is crucial:

  • State Management: It's the entry point for all state hooks (use_state, use_effect, etc.), ensuring that each component instance manages its own isolated, reactive state.
  • Event Handling: Context provides methods (on_event, emit_event) for handling component-specific and application-wide events. Events propagate through the Context tree.
  • Rendering Result (View): It holds the View generated by the ComponentImpl::call method, which is later passed to the rendering engine.
  • Child Management (scopes): A Context aggregates child components through Scopes, forming the hierarchical UI tree.
  • Engine Interaction: It provides access to the CommandExecutor, allowing components to send commands (like Stop) to the underlying engine.

When a component is instantiated (e.g., MyComponent { ... } in rsx!), a new Context is created for it. This Context lives as long as the component is part of the active UI tree.

Scope: Organizing Children

While Context represents a single component instance, Scope (osui::component::scope::Scope) is responsible for grouping and managing collections of child Contexts:

pub struct Scope {
pub children: Mutex<Vec<(Arc<Context>, Option<ViewWrapper>)>>,
executor: Arc<dyn CommandExecutor>,
}

Relationship between Context and Scope:

  • A Context can contain multiple child Scopes (stored in Context::scopes).
  • Each Scope then contains a Vec of (Arc<Context>, Option<ViewWrapper>), representing the actual child components (and optional view modifiers) within that specific scope.
  • This distinction allows for flexibility in how children are managed, particularly for dynamic rsx! constructs like @if and @for which might create or destroy entire Scopes based on conditions or iterations.

How Scopes are used:

  • When you use rsx!, the macro emits calls to context.scope() or context.dyn_scope(), which create new Scopes.
  • Within these Scopes, children are added using scope.child() or scope.view().
  • The Context::draw_children method then iterates through these Scopes and their children to orchestrate their rendering.

The Component Tree

Together, ComponentImpl, Context, and Scope form a hierarchical component tree:

App Component (Context)
└── App Scope (from App's rsx!)
├── Child Component A (Context)
│ └── Child A Scope (from A's rsx!)
│ └── Grandchild Component X (Context)
├── Child Component B (Context)
│ └── Child B Scope
│ ├── Grandchild Component Y (Context)
│ └── Grandchild Component Z (Context)
└── Dynamic Scope (e.g., from an `@if` block)
└── (Conditionally rendered children)

This tree structure is fundamental to OSUI's rendering and event propagation. Events emitted by a child Context can traverse up the tree to parent Contexts (if they listen for them) and always propagate down to all descendants.

The component model provides a clear, organized, and powerful way to structure your TUI applications, promoting maintainability and scalability through modularity and a well-defined lifecycle for each UI element.

Next: Understand how State<T> and HookDependency enable efficient UI updates in Reactive State Flow.