Improved docs
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
---
|
||||
sidebar_position: 0
|
||||
title: Core Architecture
|
||||
---
|
||||
|
||||
# Core Architecture
|
||||
|
||||
OSUI is designed with a clear separation of concerns, drawing inspiration from modern GUI frameworks like React. This modular architecture aims for flexibility, testability, and scalability. Here's a high-level overview of how the different parts of OSUI interact to form a functional TUI application.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Application Root (main.rs)] --> B(Engine::run(RootComponent))
|
||||
B --> C(Engine)
|
||||
C -- init --> D(Root Component Context)
|
||||
D -- refresh --> E(Root Component View)
|
||||
E -- generate_children --> F(Frontend::Rsx)
|
||||
F -- create Scopes & Contexts --> D
|
||||
D -- draw_children --> G(Render::DrawContext)
|
||||
G -- execute DrawInstructions --> H(Engine::draw_context)
|
||||
H -- actual terminal output --> I(crossterm)
|
||||
|
||||
subgraph State & Events
|
||||
J[Component Context] -- manage state via hooks --> K(State<T>)
|
||||
K -- notify dependents --> L(HookEffect)
|
||||
L -- trigger callbacks --> J
|
||||
J -- emit events --> M(Event Handlers)
|
||||
M -- propagate to children --> J
|
||||
end
|
||||
|
||||
subgraph User Interaction
|
||||
N[Input Polling Thread] --> O(Engine::CommandExecutor)
|
||||
O -- execute commands --> C
|
||||
O -- emit events --> J
|
||||
end
|
||||
|
||||
C -- continuous loop --> E
|
||||
D --> J
|
||||
J --> E
|
||||
```
|
||||
|
||||
## Key Architectural Components
|
||||
|
||||
### 1. **Component System (`osui::component`)**
|
||||
|
||||
* **`ComponentImpl`**: The fundamental trait defining a renderable UI unit. Any type implementing this can be an OSUI component.
|
||||
* **`Context`**: Each active component instance has its own `Context`. This is the central hub for:
|
||||
* Holding the component's `View` (its rendered output).
|
||||
* Managing its local state using hooks (e.g., `use_state`).
|
||||
* Registering and emitting events (`on_event`, `emit_event`).
|
||||
* Managing its children components and their `Scope`s.
|
||||
* Accessing the `CommandExecutor` to interact with the engine.
|
||||
* **`Scope`**: A `Context` can contain multiple child `Scope`s. A `Scope` is primarily a container that groups a set of child components (`Context`s) and their optional `ViewWrapper`s. `Rsx` fragments generate `Scope`s to manage their children.
|
||||
|
||||
**Why it works this way**: This component-based approach promotes modularity and reusability. `Context` provides a stable identity and state for each component instance, enabling independent updates and event handling. The tree structure formed by `Context`s and `Scope`s mirrors the UI hierarchy.
|
||||
|
||||
### 2. **State Management (`osui::state`)**
|
||||
|
||||
* **`State<T>`**: A reactive wrapper for any data `T`. When `State<T>` is updated, it automatically notifies all its registered "dependents".
|
||||
* **`use_state`**: The primary hook to create and manage `State<T>` within a component.
|
||||
* **`use_effect`**: A hook for performing side effects (e.g., logging, network requests) in response to `State<T>` changes or component mounting.
|
||||
* **`HookDependency`**: A trait that `State<T>` and `Mount` implement, allowing them to be tracked by `use_effect` and dynamic `rsx!` blocks.
|
||||
|
||||
**Why it works this way**: Inspired by React hooks, this system provides a predictable and efficient way to manage mutable state. By declaring explicit dependencies for effects and dynamic `rsx!` blocks, OSUI can minimize re-renders and computations, only updating parts of the UI that are truly affected by state changes.
|
||||
|
||||
### 3. **Frontend / Declarative UI (`osui::frontend` & `osui-macros`)**
|
||||
|
||||
* **`rsx!` macro**: A procedural macro that allows you to write UI using a declarative, XML-like syntax directly in Rust.
|
||||
* **`#[component]` macro**: A procedural macro that transforms a Rust function into an OSUI component, automatically handling prop parsing and `ComponentImpl` implementation.
|
||||
* **`Rsx`**: An internal representation (produced by `rsx!`) of a UI fragment, consisting of a vector of `RsxScope`s.
|
||||
* **`RsxScope`**: An enum defining different types of UI nodes (static text, components, dynamic conditional/loop blocks).
|
||||
|
||||
**Why it works this way**: Declarative UI is generally easier to reason about than imperative drawing commands. The `rsx!` macro provides a high-level abstraction that maps directly to the component tree and state management, significantly improving developer experience. The macro-generated `Rsx` object then serves as a blueprint for `Context` to build its children.
|
||||
|
||||
### 4. **Rendering Pipeline (`osui::render`)**
|
||||
|
||||
* **`View`**: The ultimate output of a component's rendering logic. It's a closure that, when called, populates a `DrawContext` with drawing instructions.
|
||||
* **`DrawContext`**: A mutable accumulator for `DrawInstruction`s. Components add text, child views, or custom drawing commands to this context. It also tracks the available `Area` and `allocated` space.
|
||||
* **`DrawInstruction`**: An enum representing atomic drawing operations (e.g., `Text`, `View`, `Child`).
|
||||
* **Geometric Primitives**: `Point`, `Size`, `Area` define positions and dimensions.
|
||||
|
||||
**Why it works this way**: This separation allows the rendering logic to be independent of the actual display medium. Components declare *what* to draw using high-level instructions, and the `Engine` then decides *how* to execute them on the specific backend (e.g., terminal).
|
||||
|
||||
### 5. **Engine (`osui::engine`)**
|
||||
|
||||
* **`Engine` trait**: Defines the interface for running an OSUI application, including initialization, continuous rendering, and managing the render loop.
|
||||
* **`Console`**: The default implementation of `Engine`, which uses `crossterm` to interact with the terminal.
|
||||
* **`CommandExecutor` trait**: An interface for executing system-level commands (e.g., `Stop`).
|
||||
* **`Benchmark`**: A wrapper `Engine` that measures and reports performance statistics.
|
||||
|
||||
**Why it works this way**: The `Engine` trait makes OSUI extensible. You can swap out the `Console` engine for a different backend (e.g., a web renderer, a headless testing engine) without changing your core component logic. The `CommandExecutor` provides a standardized way for components to request actions from the environment.
|
||||
|
||||
This interconnected architecture allows OSUI to offer a powerful, flexible, and developer-friendly experience for building sophisticated Terminal User Interfaces.
|
||||
|
||||
**Next:** Delve deeper into [The Component Model](./01-the-component-model.md).
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
title: The Component Model
|
||||
---
|
||||
|
||||
# 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:
|
||||
|
||||
```rust
|
||||
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:
|
||||
|
||||
```rust
|
||||
#[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:
|
||||
|
||||
```rust
|
||||
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 `Scope`s, 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 `Context`s:
|
||||
|
||||
```rust
|
||||
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 `Scope`s (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 `Scope`s based on conditions or iterations.
|
||||
|
||||
**How `Scope`s are used**:
|
||||
|
||||
* When you use `rsx!`, the macro emits calls to `context.scope()` or `context.dyn_scope()`, which create new `Scope`s.
|
||||
* Within these `Scope`s, children are added using `scope.child()` or `scope.view()`.
|
||||
* The `Context::draw_children` method then iterates through these `Scope`s 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 `Context`s (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](./02-reactive-state-flow.md).
|
||||
@@ -0,0 +1,114 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: Reactive State Flow
|
||||
---
|
||||
|
||||
# Reactive State Flow
|
||||
|
||||
OSUI's reactivity model is designed to efficiently update the UI in response to changes in application data. It centers around `State<T>`, `HookDependency`, and `use_effect`, forming a flow where data changes automatically trigger re-rendering of affected components.
|
||||
|
||||
## 1. `State<T>`: The Source of Truth
|
||||
|
||||
At the heart of reactivity is the `State<T>` type. When you declare state using `use_state(initial_value)`, you get a `State<T>` instance:
|
||||
|
||||
```rust
|
||||
let count = use_state(0); // count: State<i32>
|
||||
```
|
||||
|
||||
`State<T>` wraps your actual data `T` in an `Arc<Mutex<T>>`, allowing it to be safely shared and mutated across multiple threads and component scopes.
|
||||
|
||||
## 2. Updating `State<T>`
|
||||
|
||||
When the value held by `State<T>` changes, this initiates the reactive flow. There are two primary ways to update `State<T>`:
|
||||
|
||||
* **`state.set(new_value)`**: Replaces the entire value and explicitly triggers an update.
|
||||
* **`*state.get() = new_value`**: Acquires an `Inner<'_, T>` guard, which provides mutable access to the underlying value. When this `Inner` guard is dropped (goes out of scope), it automatically checks if the value was modified and, if so, triggers an update.
|
||||
|
||||
```rust
|
||||
// Method 1: using .set()
|
||||
count.set(count.get_dl() + 1);
|
||||
|
||||
// Method 2: using .get() for mutable access
|
||||
{
|
||||
let mut count_guard = count.get(); // Acquire Inner guard
|
||||
*count_guard += 1; // Mutate the value
|
||||
} // count_guard drops here, automatically triggering updates
|
||||
```
|
||||
|
||||
## 3. `HookDependency`: Declaring Reactivity
|
||||
|
||||
For an update to `State<T>` to have an effect, there must be something "listening" for that update. This is where the `HookDependency` trait comes in:
|
||||
|
||||
```rust
|
||||
pub trait HookDependency: Send + Sync {
|
||||
fn on_update(&self, hook: HookEffect);
|
||||
}
|
||||
```
|
||||
|
||||
* `State<T>` implements `HookDependency`. This means you can register an `HookEffect` with a `State<T>` instance, and `State<T>` will ensure that effect is called whenever its value changes.
|
||||
* `Mount` also implements `HookDependency` for managing component lifecycle effects.
|
||||
|
||||
## 4. `HookEffect`: The Callback
|
||||
|
||||
An `HookEffect` is essentially a wrapper around a closure (`Arc<Mutex<dyn FnMut() + Send + Sync>>`) that represents a side effect. When a `HookDependency` updates, it calls all registered `HookEffect`s.
|
||||
|
||||
```rust
|
||||
// An effect might look something like this internally:
|
||||
let my_effect = HookEffect::new(move || {
|
||||
// This code runs when the dependency updates
|
||||
println!("Dependency changed!");
|
||||
});
|
||||
```
|
||||
|
||||
## 5. `use_effect` and Dynamic `rsx!` Blocks: Consuming Reactivity
|
||||
|
||||
The `HookDependency` and `HookEffect` mechanism is consumed by two main features to enable reactive UI updates:
|
||||
|
||||
### a) `use_effect` Hook
|
||||
|
||||
`use_effect` allows you to run side effects when specified dependencies change:
|
||||
|
||||
```rust
|
||||
use_effect(
|
||||
{
|
||||
let count = count.clone(); // Clone State handle for closure
|
||||
move || {
|
||||
// This closure runs in a spawned thread when `count` updates
|
||||
println!("Count is now: {}", count.get_dl());
|
||||
}
|
||||
},
|
||||
&[&count], // `count` is the dependency
|
||||
);
|
||||
```
|
||||
|
||||
When `use_effect` is first called, it registers its internal `HookEffect` closure with each `HookDependency` in the provided slice. Each time `count` is updated, `count` calls its registered `HookEffect`, which then executes the provided closure.
|
||||
|
||||
### b) Dynamic `rsx!` Blocks (`%dep @if ...`, `%dep @for ...`)
|
||||
|
||||
OSUI's `rsx!` macro supports special syntax for dynamic UI segments that automatically re-render when dependencies change:
|
||||
|
||||
```rust
|
||||
rsx! {
|
||||
%count @if *count.get() > 0 { // This block re-renders if `count` changes
|
||||
format!("Count is positive: {}", count.get_dl())
|
||||
}
|
||||
%items @for item in items.get_dl() { // This block re-renders if `items` changes
|
||||
format!("- {}", item)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* When the `rsx!` macro encounters `%dep`, it also registers a special internal `HookEffect` with that dependency.
|
||||
* This `HookEffect` is responsible for re-evaluating the entire `dynamic_scope` (the `if` or `for` block) within the component's `Context`. This re-evaluation re-runs the `rsx!` logic for that block, generating potentially new children or text nodes, and thus updating the UI.
|
||||
|
||||
## The Reactive Flow in Summary
|
||||
|
||||
1. A component uses `use_state` to create a `State<T>`.
|
||||
2. The `State<T>` is passed as a dependency to `use_effect` or declared in a dynamic `rsx!` block (`%state`).
|
||||
3. When `State<T>`'s value is modified (`set()` or `get()` then drop), it triggers its registered `HookEffect`s.
|
||||
4. These `HookEffect`s then either execute a side-effect closure (from `use_effect`) or trigger a re-evaluation of the corresponding `dynamic_scope` (from `rsx!`).
|
||||
5. Re-evaluation of `dynamic_scope` leads to updated `DrawInstruction`s, which the `Engine` eventually renders to the terminal.
|
||||
|
||||
This elegant system ensures that your UI remains synchronized with your application's data, responding efficiently and predictably to changes, minimizing manual re-rendering logic.
|
||||
|
||||
**Next:** Understand how events traverse the component tree in [Event Propagation](./03-event-propagation.md).
|
||||
@@ -0,0 +1,104 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
title: Event Propagation
|
||||
---
|
||||
|
||||
# Event Propagation
|
||||
|
||||
Event propagation in OSUI describes how events traverse the component tree after they are emitted. Understanding this model is crucial for designing effective inter-component communication and responsive user interfaces.
|
||||
|
||||
## Unidirectional Propagation: Downwards
|
||||
|
||||
OSUI employs a unidirectional event propagation model, primarily **downwards** through the component tree. When an event is emitted from a component's `Context`, it follows this path:
|
||||
|
||||
1. **Current Context**: All `on_event` handlers registered on the `Context` that emitted the event are invoked first.
|
||||
2. **Child Contexts**: The event is then recursively propagated to all immediate children's `Context`s, and from there, further down to their children, and so on, until it reaches the leaves of the component tree. Each child `Context` will also invoke its own registered `on_event` handlers for that event type.
|
||||
|
||||
This means an event emitted by an ancestor component will reach all its descendants. An event emitted by a child component will reach its parents (if the parent `Context` has `on_event` handlers for it) and all its siblings and their descendants.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Root Component Context] --> B(Child A Context)
|
||||
A --> C(Child B Context)
|
||||
B --> D(Grandchild A1 Context)
|
||||
B --> E(Grandchild A2 Context)
|
||||
C --> F(Grandchild B1 Context)
|
||||
|
||||
subgraph Event Propagation (emit_event from D)
|
||||
D -- handlers on D --> D
|
||||
D -- propagate --> B
|
||||
B -- handlers on B --> B
|
||||
B -- propagate --> E
|
||||
E -- handlers on E --> E
|
||||
B -- propagate --> A
|
||||
A -- handlers on A --> A
|
||||
A -- propagate --> C
|
||||
C -- handlers on C --> C
|
||||
C -- propagate --> F
|
||||
F -- handlers on F --> F
|
||||
end
|
||||
```
|
||||
|
||||
### Methods for Event Emission
|
||||
|
||||
* **`cx.emit_event(event: E)`**:
|
||||
* This is the standard method for emitting events.
|
||||
* It processes event handlers synchronously in the current thread. This means that subsequent code execution will wait for all handlers (and their propagation to children) to complete.
|
||||
* Useful for events where the order of execution matters or where the handler logic is quick.
|
||||
|
||||
* **`cx.emit_event_threaded(event: &E)`**:
|
||||
* This method processes event handlers asynchronously by spawning a new `std::thread` for *each* registered handler.
|
||||
* The event object `E` must implement `Clone` because each handler receives its own cloned copy.
|
||||
* Useful for events that might trigger long-running or blocking operations, preventing them from freezing the UI. The event propagation down the tree also uses the threaded approach.
|
||||
|
||||
## Practical Implications
|
||||
|
||||
### Parent-to-Child Communication (Implicit)
|
||||
|
||||
If a parent component emits an event, all its child components (and their children) that have `on_event` handlers for that specific event type will receive it. This is a powerful way for ancestors to broadcast information or commands to their descendants.
|
||||
|
||||
```rust
|
||||
// Parent emits a "Refresh" event
|
||||
cx.emit_event(RefreshEvent {});
|
||||
|
||||
// Child listens for "Refresh" event
|
||||
cx.on_event(|_cx, _event: &RefreshEvent| {
|
||||
// Perform refresh logic
|
||||
});
|
||||
```
|
||||
|
||||
### Child-to-Parent/Sibling Communication (Explicit)
|
||||
|
||||
A child component can effectively communicate with its parent or siblings by emitting an event. Because events propagate downwards from the emitting `Context` *and then* to all its children (and subsequently to its parent's other children, if any), the parent and siblings will receive the event if they are listening for it.
|
||||
|
||||
```rust
|
||||
// Child component
|
||||
#[component]
|
||||
fn Child(cx: &Arc<Context>) -> View {
|
||||
// ... logic to decide when to emit
|
||||
cx.emit_event(ChildActionCompleted { data: "success".to_string() });
|
||||
// ...
|
||||
}
|
||||
|
||||
// Parent component
|
||||
#[component]
|
||||
fn Parent(cx: &Arc<Context>) -> View {
|
||||
cx.on_event(|_cx, event: &ChildActionCompleted| {
|
||||
println!("Parent received action from child: {:?}", event.data);
|
||||
});
|
||||
rsx! { Child {} }.view(&cx)
|
||||
}
|
||||
```
|
||||
|
||||
### Decoupling Components
|
||||
|
||||
Event handling promotes a decoupled architecture. Components don't need direct references to each other to communicate; they only need to agree on common event types. This makes components more independent and easier to reuse.
|
||||
|
||||
## When to use `emit_event` vs. `emit_event_threaded`
|
||||
|
||||
* **`emit_event`**: Use for most general-purpose events where handlers are quick, or you need strict sequential processing, or if you don't want the overhead of spawning many threads.
|
||||
* **`emit_event_threaded`**: Use for events that might trigger expensive, long-running, or I/O-bound operations in their handlers. This prevents the main rendering loop from blocking, ensuring a responsive UI. Be mindful of potential race conditions if multiple threads modify shared state (though `State<T>`'s `Mutex` helps mitigate this).
|
||||
|
||||
Understanding OSUI's downward event propagation is key to designing robust and reactive component interactions within your TUI applications.
|
||||
|
||||
**Next:** Get insights into how your UI transforms from `View`s to terminal output in [The Rendering Pipeline](./04-rendering-pipeline.md).
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
title: The Rendering Pipeline
|
||||
---
|
||||
|
||||
# The Rendering Pipeline
|
||||
|
||||
The OSUI rendering pipeline is the process by which your declarative component hierarchy is transformed into concrete drawing operations on the terminal. It's an abstraction layer that allows components to describe *what* to draw, while the `Engine` handles *how* to draw it.
|
||||
|
||||
## Stages of the Pipeline
|
||||
|
||||
The pipeline can be broken down into several distinct stages:
|
||||
|
||||
### 1. Component `call` and `View` Generation
|
||||
|
||||
* **`Engine::run`**: The main application loop starts by calling `Engine::run` with your root component.
|
||||
* **`Context::refresh`**: The engine initializes the root component's `Context` and calls `Context::refresh`.
|
||||
* **`ComponentImpl::call`**: Inside `refresh`, the component's `ComponentImpl::call` method is invoked. This is where your component function (decorated with `#[component]`) executes.
|
||||
* **`rsx!` Macro Expansion**: Within your component function, the `rsx!` macro generates an `osui::frontend::Rsx` object.
|
||||
* **`Rsx::view(&cx)`**: This method converts the `Rsx` object into a `View`. Critically, `Rsx::view` also triggers `Rsx::generate_children`.
|
||||
* **`Rsx::generate_children`**: This recursively processes the `Rsx` object, creating new child `Context`s and `Scope`s within the current `Context`. For `dynamic_scope`s (`@if`, `@for`), it also registers `use_effect` hooks to trigger re-evaluation when dependencies change.
|
||||
* **Result**: The component function ultimately returns a `View`. This `View` is a closure that, when executed, will populate a `DrawContext` by calling `context.draw_children()`.
|
||||
|
||||
### 2. `DrawContext` Construction (`render_view`)
|
||||
|
||||
* **`Engine::render`**: In the main rendering loop, the `Engine` calls `render` for the current `Context`.
|
||||
* **`Engine::render_view`**: The `Engine` creates a fresh, empty `DrawContext` for the entire screen `Area`. It then executes the root component's `View` (the closure generated in Stage 1) against this `DrawContext`.
|
||||
* **`Context::draw_children`**: The root `View`'s closure invokes `context.draw_children()`. This method iterates through all child `Scope`s and their contained `Context`s. For each child `Context`, it retrieves its `View` and adds a `DrawInstruction::View` to the current `DrawContext`, recursively starting the `render_view` process for children within their allocated `Area`.
|
||||
* **`DrawContext::draw_text`, `DrawContext::draw_view`, `DrawContext::allocate`**: As `View`s are executed, they add `DrawInstruction`s to the `DrawContext` using methods like `draw_text` for text, `draw_view` for child components, and `allocate` to mark used screen regions.
|
||||
* **Result**: A fully populated `DrawContext` containing a flat list of `DrawInstruction`s, ready for rendering.
|
||||
|
||||
### 3. `DrawInstruction` Execution (`draw_context`)
|
||||
|
||||
* **`Engine::draw_context`**: After `render_view` has produced a complete `DrawContext`, the `Engine`'s `draw_context` method is called. This is the stage where the actual terminal output happens.
|
||||
* **Instruction Iteration**: `draw_context` iterates through the `Vec<DrawInstruction>` inside the `DrawContext`.
|
||||
* **`Text`**: For `DrawInstruction::Text(point, text)`, the engine translates `point` (which is relative to the `DrawContext`'s `area`) into absolute terminal coordinates and uses `crossterm` to move the cursor and print the `text`.
|
||||
* **`View`**: For `DrawInstruction::View(area, view)`, the engine recursively calls `render_view` for the child `view` within its specific `area`, then processes the resulting `DrawContext`.
|
||||
* **`Child`**: For `DrawInstruction::Child(point, child_ctx)`, the engine recursively calls `draw_context` for the `child_ctx`, applying the `point` offset.
|
||||
* **Terminal Output**: The `Console` engine uses `crossterm` functions (like `MoveTo`, `Print`, `Clear`) to modify the terminal buffer.
|
||||
* **Result**: The visible TUI on the user's screen.
|
||||
|
||||
## `render_delay` and Loop
|
||||
|
||||
After `draw_context` completes, the `Engine` typically calls `render_delay()` (defaulting to 16ms for ~60 FPS) before the entire loop restarts with the next `Engine::render` call. This continuous loop maintains a responsive and updated UI.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Component Function (`#[component]`)] --> B(Generates `Rsx` object)
|
||||
B --> C(Rsx::view(&cx))
|
||||
C -- calls Rsx::generate_children --> D(Builds child Contexts & Scopes)
|
||||
D --> E(Returns a `View` closure)
|
||||
|
||||
subgraph Engine Loop
|
||||
F[Engine::render(root_cx)] --> G(Engine::render_view(full_screen_area, root_view))
|
||||
G -- creates empty DrawContext --> H(Executes root_view closure)
|
||||
H -- root_view calls Context::draw_children --> I(Recursively adds DrawInstruction::View for children)
|
||||
I -- children's Views populate DrawContext --> J(Result: Full DrawContext with instructions)
|
||||
J --> K(Engine::draw_context(full_DrawContext))
|
||||
K -- iterates DrawInstructions --> L(Executes terminal ops via crossterm)
|
||||
L --> M[Visible TUI]
|
||||
M -- optional delay --> N(Engine::render_delay)
|
||||
N --> F
|
||||
end
|
||||
```
|
||||
|
||||
## Key Principles
|
||||
|
||||
* **Declarative vs. Imperative**: Components declare *what* to draw (`View`, `DrawInstruction`), not *how* to directly manipulate the terminal. The engine handles the imperative *how*.
|
||||
* **Separation of Concerns**: Each stage focuses on a specific responsibility: component logic, state management, UI tree construction, and final rendering.
|
||||
* **Reactivity Integration**: Dynamic `rsx!` blocks and `use_effect` ensure that only affected parts of the `View` or `DrawContext` are re-generated efficiently when state changes, minimizing redundant work.
|
||||
* **Extensibility**: The `Engine` trait allows for different rendering backends (e.g., to a file, to a graphical window, or for benchmarking) without modifying component logic.
|
||||
|
||||
Understanding this pipeline helps in debugging rendering issues, optimizing performance, and building custom rendering logic within your OSUI applications.
|
||||
|
||||
**Next:** Explore advanced topics like [Performance Benchmarking](../advanced/00-performance-benchmarking.md).
|
||||
Reference in New Issue
Block a user