Version 0.1.1
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
# Declarative UI and the `rsx!` Macro
|
||||
|
||||
OSUI embraces a declarative approach to UI development, heavily inspired by modern web frameworks. This is primarily facilitated by the `rsx!` macro, which allows you to describe your UI's structure and behavior in a clear, nested, and expressive way.
|
||||
|
||||
## Why Declarative UI?
|
||||
|
||||
Traditional imperative UI development involves manually creating, positioning, and updating UI elements based on state changes. This can lead to complex, hard-to-maintain code, especially for dynamic UIs.
|
||||
|
||||
Declarative UI, in contrast:
|
||||
|
||||
* **Focuses on "What"**: You describe the desired UI state for a given data state, rather than the steps to get there.
|
||||
* **Simplicity**: The code is often more readable and easier to reason about, as it mirrors the visual structure of the UI.
|
||||
* **Reactivity**: When the underlying data changes, the framework (OSUI, in this case) efficiently updates the UI to reflect the new state, minimizing manual DOM manipulation.
|
||||
* **Composition**: Encourages breaking down complex UIs into smaller, reusable components.
|
||||
|
||||
## The `rsx!` Macro
|
||||
|
||||
The `rsx!` macro is the cornerstone of OSUI's declarative syntax. It transforms a nested, JSX-like structure into a tree of `RsxElement`s, which are then used to build `Widget`s on the `Screen`.
|
||||
|
||||
### Basic Syntax
|
||||
|
||||
```rust
|
||||
use osui::prelude::*;
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
let screen = Screen::new();
|
||||
screen.extension(InputExtension); // Needed for basic interaction
|
||||
|
||||
rsx! {
|
||||
// Simple text string as an Element
|
||||
"Hello, World!"
|
||||
|
||||
// Static element: No dynamic dependencies or state
|
||||
static Div {
|
||||
// Nested content
|
||||
"This is a static div."
|
||||
}
|
||||
|
||||
// Dynamic element: Reacts to state changes
|
||||
%my_state // This widget depends on `my_state`
|
||||
Div {
|
||||
// Content can be interpolated from state
|
||||
"Current value: {my_state}"
|
||||
}
|
||||
}.draw(&screen);
|
||||
|
||||
screen.run()?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Key Features of `rsx!` Syntax
|
||||
|
||||
1. **Direct Text**:
|
||||
Any string literal within `rsx!` is treated as a `String` element.
|
||||
```rust
|
||||
rsx! {
|
||||
"Just some text."
|
||||
format!("Dynamic text: {}", 123) // You can also use format!
|
||||
}
|
||||
```
|
||||
|
||||
2. **Element Declaration**:
|
||||
Elements are typically declared by their struct name (e.g., `Div`, `FlexRow`, `Input`).
|
||||
```rust
|
||||
rsx! {
|
||||
Div {} // A simple Div element
|
||||
}
|
||||
```
|
||||
|
||||
3. **Properties (Fields)**:
|
||||
You can set public fields on the element struct directly using comma-separated key-value pairs, similar to struct instantiation.
|
||||
```rust
|
||||
rsx! {
|
||||
Heading, smooth: true, { "Important Title" } // Sets the `smooth` field on Heading
|
||||
}
|
||||
```
|
||||
This expands to:
|
||||
```rust
|
||||
let mut elem = Heading::new();
|
||||
elem.smooth = true;
|
||||
// ... then `elem` is wrapped in a Widget
|
||||
```
|
||||
|
||||
4. **Children**:
|
||||
Content nested within curly braces `{}` after an element forms its children. These children are then drawn by the parent element's `after_render` method.
|
||||
```rust
|
||||
rsx! {
|
||||
Div {
|
||||
"First child"
|
||||
"Second child"
|
||||
FlexRow { "Nested FlexRow" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
5. **Static vs. Dynamic Widgets**:
|
||||
* **`static` keyword**: Prefix an element with `static` to explicitly declare it as a `StaticWidget`. This means its content and components won't change unless manually modified elsewhere. It incurs no dependency tracking overhead.
|
||||
```rust
|
||||
rsx! {
|
||||
static Div { "Always the same" }
|
||||
}
|
||||
```
|
||||
* **Dynamic (Reactive)**: By default, if an element has dependencies (see below) or is just a string literal without `static`, it becomes a `DynWidget`. This allows it to refresh when its dependencies change.
|
||||
|
||||
6. **Dependencies (`%dep_name`)**:
|
||||
Prefixing an element with `%variable_name` registers `variable_name` (which must implement `DependencyHandler`, like `State<T>`) as a dependency for that widget. If `variable_name` signals a change, the widget will automatically rebuild and re-render.
|
||||
```rust
|
||||
let counter = use_state(0);
|
||||
rsx! {
|
||||
%counter // This Div depends on `counter`
|
||||
Div {
|
||||
// `counter` can be used directly in its content
|
||||
"Count: {counter}"
|
||||
}
|
||||
}
|
||||
```
|
||||
Multiple dependencies can be listed: `%dep1 %dep2 Element {}`.
|
||||
|
||||
7. **Components (`@ComponentType`)**:
|
||||
Attach components to an element using the `@` symbol followed by the component's type and its constructor or a value.
|
||||
```rust
|
||||
use osui::prelude::*;
|
||||
|
||||
rsx! {
|
||||
// Attach a Transform component with specific dimensions
|
||||
@Transform::new().dimensions(10, 5);
|
||||
// Attach a Style component with a solid red background
|
||||
@Style { background: Background::Solid(0xFF0000) };
|
||||
Div {
|
||||
"A red box"
|
||||
}
|
||||
|
||||
// Attach a custom Handler component for events
|
||||
@Handler::new(|_, e: &MyEvent| { /* ... */ });
|
||||
Div { "Click me!" }
|
||||
}
|
||||
```
|
||||
You can attach multiple components to the same element, each on its own `@` line.
|
||||
|
||||
8. **Macro Expansion (`$expand => ($args)`)**:
|
||||
You can include the output of another `Rsx`-generating function or macro using `macro_name => (args)`. This is useful for creating reusable UI fragments.
|
||||
```rust
|
||||
fn my_button(text: &str) -> Rsx {
|
||||
rsx! {
|
||||
Div { format!("Button: {}", text) }
|
||||
}
|
||||
}
|
||||
|
||||
rsx! {
|
||||
my_button => ("Click Me")
|
||||
my_button => ("Another Button")
|
||||
}
|
||||
```
|
||||
|
||||
### How `rsx!` Works Internally
|
||||
|
||||
The `rsx!` macro recursively expands into a series of `Rsx::create_element` or `Rsx::create_element_static` calls. Each `RsxElement` stores either a `StaticWidget` directly or a closure that produces a `WidgetLoad` (for dynamic widgets), along with its dependencies and child `Rsx` tree.
|
||||
|
||||
When `Rsx::draw` or `Rsx::draw_parent` is called on the root `Rsx` object:
|
||||
|
||||
1. It iterates through its `RsxElement`s.
|
||||
2. For `RsxElement::DynElement`, it calls the stored closure to generate a `WidgetLoad`, then creates a `DynWidget` via `screen.draw_box_dyn`. It registers all specified dependencies with this new `DynWidget`.
|
||||
3. For `RsxElement::Element`, it directly creates a `StaticWidget` via `screen.draw_widget`.
|
||||
4. If a parent widget is provided (for nested elements), it calls `parent.get_elem().draw_child(&new_widget)`. This informs the parent `Element` about its new child.
|
||||
5. It then recursively calls `draw_parent` on the child `Rsx` tree, passing the newly created widget as the `parent`.
|
||||
|
||||
This process builds the complete `Arc<Widget>` tree managed by the `Screen`, setting up the initial hierarchy and reactive dependencies. The declarative `rsx!` syntax simplifies this complex creation process into an intuitive structure.
|
||||
@@ -0,0 +1,107 @@
|
||||
# Layout and Styling System
|
||||
|
||||
OSUI employs a two-phase approach to layout and rendering, driven by `Transform` and `RawTransform` structures, and provides expressive tools for styling elements using `Style`.
|
||||
|
||||
## The Two-Phase Transform Model
|
||||
|
||||
OSUI's layout system resolves abstract positioning and sizing rules into concrete pixel coordinates and dimensions. This is managed by two primary structures:
|
||||
|
||||
1. **`Transform` (Configured Layout)**:
|
||||
* This is the high-level struct that developers interact with.
|
||||
* It defines abstract rules for position (`Position` enum) and dimension (`Dimension` enum), along with explicit margins (`mx`, `my`) and padding (`px`, `py`).
|
||||
* `Transform` instances are typically attached to widgets as components using the `Transform` component or via the `transform!` macro.
|
||||
* Examples: `Position::Center`, `Dimension::Full`, `Position::Const(10)`.
|
||||
|
||||
```rust
|
||||
// Example Transform definition
|
||||
component!(Transform {
|
||||
pub x: Position,
|
||||
pub y: Position,
|
||||
pub mx: i32,
|
||||
pub my: i32,
|
||||
pub px: u16,
|
||||
pub py: u16,
|
||||
pub width: Dimension,
|
||||
pub height: Dimension,
|
||||
});
|
||||
```
|
||||
* Methods like `center()`, `bottom()`, `right()`, `margin()`, `padding()`, and `dimensions()` provide a fluent API for configuration.
|
||||
|
||||
2. **`RawTransform` (Resolved Layout)**:
|
||||
* This struct holds the *concrete, absolute* `u16` values for `x`, `y`, `width`, `height`, `px`, and `py` after layout calculations have occurred.
|
||||
* It represents the final calculated bounds and offsets for a widget on the virtual screen.
|
||||
* Developers generally don't manipulate `RawTransform` directly; it's used internally by the `RenderScope` during the rendering phase.
|
||||
|
||||
```rust
|
||||
// Example RawTransform definition
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RawTransform {
|
||||
pub x: u16,
|
||||
pub y: u16,
|
||||
pub width: u16,
|
||||
pub height: u16,
|
||||
pub px: u16,
|
||||
pub py: u16,
|
||||
}
|
||||
```
|
||||
|
||||
### How Resolution Works (`use_dimensions`, `use_position`)
|
||||
|
||||
When a widget is rendered, its `Transform` component is used by the `RenderScope` to resolve its abstract rules into a concrete `RawTransform`.
|
||||
|
||||
* **`Transform::use_dimensions(parent_width, parent_height, raw_transform)`**:
|
||||
* This method takes the `parent_width` and `parent_height` (the available space from the parent container) and updates the `raw_transform.width` and `raw_transform.height` based on the `Dimension` rules.
|
||||
* `Dimension::Full` will set the raw dimension to the parent's available size.
|
||||
* `Dimension::Const(n)` will set it to `n`.
|
||||
* `Dimension::Content` means the dimension will be determined by the content drawn by the element itself (e.g., text length or explicit `use_area` calls).
|
||||
|
||||
* **`Transform::use_position(parent_width, parent_height, raw_transform)`**:
|
||||
* This method uses the widget's *resolved* `raw_transform.width` and `raw_transform.height` (after `use_dimensions`) along with `parent_width` and `parent_height` to determine the absolute `raw_transform.x` and `raw_transform.y`.
|
||||
* `Position::Const(n)` sets the coordinate to `n`.
|
||||
* `Position::Center` calculates the coordinate to center the widget within the parent.
|
||||
* `Position::End` calculates the coordinate to align the widget to the end (right/bottom) of the parent.
|
||||
* Margins (`mx`, `my`) are applied as offsets after the base position is calculated.
|
||||
|
||||
This separation allows for a clear definition of layout rules at design time (`Transform`) and their efficient resolution into precise screen coordinates at runtime (`RawTransform`).
|
||||
|
||||
## Styling with `Style`
|
||||
|
||||
The `Style` component defines the visual appearance of a widget. It primarily controls background and foreground colors.
|
||||
|
||||
```rust
|
||||
component!(Style {
|
||||
pub background: Background,
|
||||
pub foreground: Option<u32>,
|
||||
});
|
||||
|
||||
pub enum Background {
|
||||
NoBackground,
|
||||
Outline(u32),
|
||||
RoundedOutline(u32),
|
||||
Solid(u32),
|
||||
}
|
||||
```
|
||||
|
||||
* **`background`**: Defines how the widget's background is rendered.
|
||||
* `NoBackground`: The widget's area is transparent.
|
||||
* `Outline(color)`: Draws a simple rectangular outline using the specified 24-bit RGB color.
|
||||
* `RoundedOutline(color)`: Draws a rounded rectangular outline.
|
||||
* `Solid(color)`: Fills the entire widget area with the specified 24-bit RGB color.
|
||||
* **`foreground`**: An `Option<u32>` representing the 24-bit RGB color for any text drawn within the widget. If `None`, default terminal foreground color is used.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```rust
|
||||
use osui::prelude::*;
|
||||
|
||||
// Example: A div with blue background and white text
|
||||
rsx! {
|
||||
@Transform::new().dimensions(20, 5).padding(1,1);
|
||||
@Style { background: Background::Solid(0x0000FF), foreground: Some(0xFFFFFF) };
|
||||
Div {
|
||||
"This is a blue box with white text."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
By combining `Transform` and `Style` components, developers have precise control over the visual presentation and spatial arrangement of UI elements.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Rendering Pipeline and `RenderScope`
|
||||
|
||||
OSUI's rendering process is a structured sequence of operations managed by the `RenderScope`. This module centralizes the accumulation of drawing commands and their eventual flush to the terminal.
|
||||
|
||||
## The Role of `RenderScope`
|
||||
|
||||
`RenderScope` is a mutable context passed through the rendering process for each widget. It serves several key purposes:
|
||||
|
||||
* **Layout Context**: It holds the current `RawTransform` (resolved position and dimensions) for the widget being rendered, derived from its `Transform` component and its parent's layout.
|
||||
* **Drawing Command Accumulation**: It acts as a temporary buffer for `RenderMethod` instructions (text, rectangles). Widgets and extensions add commands to this stack.
|
||||
* **Parent Size Tracking**: It keeps track of the available dimensions from the current widget's parent, crucial for resolving `Dimension::Full` and `Position::Center`/`End` rules.
|
||||
* **Style Application**: It carries the `Style` component for the current widget, influencing how drawing commands are eventually rendered (e.g., background fill, foreground color).
|
||||
|
||||
## The Rendering Sequence
|
||||
|
||||
When `Screen::render_widget` is called for a given `Arc<Widget>`, the following general pipeline is executed:
|
||||
|
||||
1. **Clear Scope**: The `RenderScope` is cleared of any previous draw instructions, ensuring a clean slate for the current widget.
|
||||
2. **Apply Style (if present)**: If the widget has a `Style` component, it's applied to the `RenderScope`.
|
||||
3. **Apply Transform (Dimensions First)**: If the widget has a `Transform` component, its `use_dimensions` method is called. This resolves `Dimension` rules (`Full`, `Const`, `Content`) into the `RawTransform` based on the parent's size.
|
||||
4. **Element `render` Call**: The widget's `Element::render` method is invoked.
|
||||
* Here, the `Element` issues its direct drawing commands (e.g., `scope.draw_text(...)`, `scope.draw_rect(...)`).
|
||||
* Crucially, if the `Dimension` was `Content`, the `Element` is responsible for updating the `RenderScope`'s `RawTransform` width/height to enclose its drawn content (e.g., `scope.use_area`).
|
||||
5. **Extension `render_widget` Calls**: Any registered extensions have their `render_widget` hook called. Extensions can observe or modify the `RenderScope` or widget state at this point.
|
||||
6. **Re-Apply Transform (Positions Second)**: After the `Element` and extensions have had a chance to determine the *content size* (for `Dimension::Content`), the widget's `Transform::use_position` is called. This resolves `Position` rules (`Const`, `Center`, `End`) into the `RawTransform` using the now-finalized dimensions. Margins are also applied here.
|
||||
7. **`ElementRenderer::before_draw` Call**: An `ElementRenderer` trait hook is called. This is specifically used by "ghost" elements (like `Div` or `FlexRow`) to adjust the `RenderScope`'s transform *for their children*. For example, a `FlexRow` would update the `x`/`y` of the `RenderScope`'s internal `RawTransform` so that the next child starts at the correct position within the row.
|
||||
8. **`RenderScope::draw`**: The accumulated `RenderMethod` instructions within the `RenderScope`'s `render_stack` are flushed to the terminal. This involves:
|
||||
* Drawing the background (if `Style::background` is `Solid`, `Outline`, or `RoundedOutline`).
|
||||
* Iterating through `render_stack` and printing text or drawing rectangles at their resolved coordinates, applying foreground colors from `Style` or specific `TextColored` commands.
|
||||
9. **Element `after_render` Call**: The widget's `Element::after_render` method is invoked.
|
||||
* This is typically where container elements (`Div`, `FlexRow`, `Paginator`) recursively trigger `scope.render_widget` for their children. They pass a *new* or modified `RenderScope` to their children, setting the `parent_width` and `parent_height` appropriately (e.g., the parent's own *content area*).
|
||||
10. **Extension `after_render_widget` Calls**: Any registered extensions have their `after_render_widget` hook called. This allows extensions to perform post-rendering logic, such as updating internal state based on the final rendered transform (e.g., `RelativeFocusExtension` records widget positions).
|
||||
11. **`widget.auto_refresh()`**: For `DynWidget`s, this checks if any registered dependencies have changed and, if so, triggers a `refresh()` to rebuild the widget for the next frame.
|
||||
|
||||
## Key Concepts in `RenderScope`
|
||||
|
||||
* **`RenderMethod`**: An internal enum representing a single primitive drawing operation (Text, TextInverted, TextColored, Rectangle).
|
||||
* **`draw_text`, `draw_rect`, etc.**: Methods on `RenderScope` to add `RenderMethod`s to the `render_stack`. These methods also update the `RenderScope`'s internal `RawTransform` to account for the content's size, enabling `Dimension::Content` to work.
|
||||
* **`use_area(width, height)`**: Allows an `Element` to explicitly declare a minimum required width and height, which is useful when the content itself doesn't automatically dictate a size (e.g., a `Div` that just reserves space).
|
||||
* **Coordinate System**: All coordinates `(x, y)` are relative to the *current* `RenderScope`'s top-left corner. When `RenderScope::draw` is called, these relative coordinates are offset by the `RenderScope`'s absolute `transform.x` and `transform.y`. Padding (`px`, `py`) is also applied as an offset for content rendering.
|
||||
|
||||
This pipeline ensures that layout calculations are performed efficiently, drawing commands are batched, and elements are rendered within their correctly determined bounds, respecting both parent constraints and self-determined content sizes.
|
||||
@@ -0,0 +1,109 @@
|
||||
# State Management and Reactivity
|
||||
|
||||
OSUI provides a built-in, lightweight state management system that enables reactive updates to your UI. This system is centered around the `State<T>` struct and the `DependencyHandler` trait, allowing `DynWidget`s to automatically re-render when their associated data changes.
|
||||
|
||||
## `State<T>`: Your Reactive Data Container
|
||||
|
||||
The `State<T>` struct is a wrapper around your data `T` that facilitates dependency tracking.
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct State<T> {
|
||||
inner: Arc<Mutex<Inner<T>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Inner<T> {
|
||||
value: T,
|
||||
dependencies: usize, // Number of widgets depending on this state
|
||||
changed: usize, // Counter for changes waiting to be processed by dependents
|
||||
}
|
||||
```
|
||||
|
||||
* **`Arc<Mutex<Inner<T>>>`**: The core of `State<T>` is its use of `Arc` and `Mutex`.
|
||||
* `Arc` allows `State<T>` instances to be shared across multiple widgets and threads without needing to clone the underlying data `T` itself, which is crucial for `DynWidget`s that track multiple dependencies.
|
||||
* `Mutex` ensures safe concurrent access to the `value` and metadata (`dependencies`, `changed`), preventing data races.
|
||||
|
||||
### Creating State (`use_state`)
|
||||
|
||||
You create a new `State<T>` instance using the `use_state` helper function:
|
||||
|
||||
```rust
|
||||
pub fn use_state<T>(v: T) -> State<T> { /* ... */ }
|
||||
```
|
||||
|
||||
**Example:**
|
||||
|
||||
```rust
|
||||
use osui::prelude::*;
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
let screen = Screen::new();
|
||||
screen.extension(InputExtension);
|
||||
screen.extension(RelativeFocusExtension::new());
|
||||
|
||||
// Create a new state variable for a counter
|
||||
let count = use_state(0);
|
||||
|
||||
// Spawn a thread to increment the counter every second
|
||||
std::thread::spawn({
|
||||
let count = count.clone(); // Clone the Arc<State<T>> for the new thread
|
||||
move || loop {
|
||||
// Get a mutable lock on the Inner<T> to modify the value
|
||||
// DerefMut implementation on Inner<T> automatically marks it as changed
|
||||
*count.get() += 1;
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
}
|
||||
});
|
||||
|
||||
rsx! {
|
||||
// Declare the widget as dependent on `count`
|
||||
%count
|
||||
Div {
|
||||
// Access the value using Deref on Inner<T>
|
||||
format!("This number increments: {}", count.get())
|
||||
}
|
||||
}.draw(&screen);
|
||||
|
||||
screen.run()?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Accessing and Modifying State
|
||||
|
||||
* **`State::get()`**: Returns a `MutexGuard<'_, Inner<T>>`. This provides mutable access to the underlying `value` within `Inner<T>`.
|
||||
* `Inner<T>` implements `Deref` and `DerefMut` for `T`. This means you can treat `count.get()` like a direct reference to `T`.
|
||||
* Crucially, when you use `DerefMut` (e.g., `*count.get() += 1`), the `changed` counter within `Inner<T>` is automatically incremented. This is how OSUI knows the state has been modified and needs to trigger a re-render.
|
||||
* **`State::get_dl()`**: (Short for "get, don't lock") Returns a cloned copy of the value. This is useful when you only need to read the value and want to avoid holding the `MutexGuard` for longer than necessary, which can prevent deadlocks in complex scenarios. However, it doesn't mark the state as changed.
|
||||
* **`State::set(v: T)`**: Replaces the entire value and marks the state as changed.
|
||||
* **`State::update()`**: Explicitly marks the state as changed *without* modifying its value. Useful if internal parts of `T` are modified outside of direct `DerefMut` access.
|
||||
|
||||
## `DependencyHandler` Trait
|
||||
|
||||
The `DependencyHandler` trait is the interface through which `DynWidget`s observe changes in their dependencies. `State<T>` implements this trait.
|
||||
|
||||
```rust
|
||||
pub trait DependencyHandler: std::fmt::Debug + Send + Sync {
|
||||
fn add(&self);
|
||||
fn check(&self) -> bool;
|
||||
}
|
||||
```
|
||||
|
||||
* **`add()`**: Called when a `DynWidget` registers itself as a dependent of this `State<T>`. It increments the `dependencies` counter within `Inner<T>`.
|
||||
* **`check()`**: Called by `DynWidget`s (specifically by `DynWidget::auto_refresh()`) to determine if the state has changed since the last check.
|
||||
* It decrements the `changed` counter if it's greater than zero, signifying that a change has been "consumed" by a dependent.
|
||||
* It returns `true` if `changed` was greater than zero, indicating a fresh update.
|
||||
|
||||
### How Reactivity Works
|
||||
|
||||
1. **Widget Creation**: When `rsx!` creates a `DynWidget` with a `%state_var` dependency, `state_var.add()` is called, incrementing `state_var.inner.dependencies`.
|
||||
2. **State Modification**: When `*state_var.get() = new_value` or `state_var.set(new_value)` is called, the `state_var.inner.changed` counter is set to `state_var.inner.dependencies`. This means *all* widgets currently depending on this state are marked for a refresh.
|
||||
3. **Automatic Refresh**: In each rendering frame, `DynWidget::auto_refresh()` is called.
|
||||
* It iterates through its registered `DependencyHandler`s.
|
||||
* For each dependency, it calls `dependency.check()`.
|
||||
* If `check()` returns `true` (meaning the state has changed and hasn't been consumed yet by this widget), the `DynWidget`'s internal `load` closure is re-executed (`self.refresh()`). This rebuilds the widget's `Element` and `Component`s, picking up the new state value.
|
||||
* The `check()` method decrements the `changed` counter, ensuring that a single modification to the state triggers exactly one rebuild for each dependent widget.
|
||||
4. **Re-render**: The rebuilt widget is then rendered on the next frame, reflecting the updated state.
|
||||
|
||||
This system provides a robust and efficient way to manage dynamic UI elements, abstracting away the complexities of manual DOM updates and allowing developers to focus on defining their UI's desired state.
|
||||
@@ -0,0 +1,100 @@
|
||||
# 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.
|
||||
|
||||
```rust
|
||||
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 provided `RenderScope` 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's `after_render`.
|
||||
* **`after_render`**: Called after the element's `render` method and any extensions have processed it. This is typically where container elements (like `Div` or `FlexRow`) would recursively trigger the rendering of their children, using the `RenderScope` for layout calculations.
|
||||
* **`draw_child`**: Used by the `rsx!` macro and `Rsx` structure to register a child widget with a parent `Element`.
|
||||
* **`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 include `Div` and `FlexRow`. They receive a `RenderScope` but might not add anything to its `render_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`.
|
||||
|
||||
```rust
|
||||
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.
|
||||
|
||||
```rust
|
||||
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.
|
||||
|
||||
```rust
|
||||
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` or `Screen::draw` for direct `Element`s, or by `rsx!` 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 its `Element` and `Component`s. It tracks dependencies (via `DependencyHandler`) and automatically `refresh`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` or `Screen::draw_dyn`, or by `rsx!` 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](../concepts/state-management.md).
|
||||
Reference in New Issue
Block a user