-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture Core
Covers
rosace-core(Layer 2) and its role in the frame loop. This is the spine everything else hangs off — read it first.
You write Components that produce an Element tree by reading state through a Context; the framework turns that tree into on-screen widgets and rebuilds only the components whose state changed.
If you know React: Component ≈ a component, build() ≈ render(), Context::state ≈ useState, and an Atom ≈ a piece of reactive state. The key difference from React: there is no virtual-DOM diff of everything every frame. ROSACE tracks which components read which atoms, and when an atom changes it rebuilds only its subscribers.
graph TD
C["Component::build(ctx)"] -->|reads| A["Atom (state)"]
C -->|returns| EL["Element tree"]
EL --> WID["widgets (Column, Text…)"]
A -.->|"atom.set() marks the component dirty"| C
1. You implement Component. One method, called by the framework:
pub trait Component: Send + Sync + 'static {
fn build(&self, ctx: &mut Context) -> Element;
}2. build() reads state via Context. ctx.state(default) returns an Atom<T> — a reactive cell. State is keyed by call order (a hooks model, like React's useState), so call ctx.state(...) unconditionally and in a stable order:
pub fn state<T: Clone + Send + Sync + 'static>(&mut self, default: T) -> Atom<T>rosace-core/src/context.rs. There is also ctx.state_permanent(key, default) (survives restarts — see D114/D121), which is key-based because it persists to disk.
3. build() returns an Element. Element is the lightweight description of the tree — either a nested component or a native node carrying a boxed widget. You rarely construct Element directly: widgets provide .into_element(), and returning a widget from a Scaffold/layout does it for you.
4. The frame engine turns Elements into pixels — but only when dirty. The loop lives in FrameEngine::paint. Each frame it:
- drains the dirty set (
rosace_state::take_dirty_components()+is_global_dirty()); -
rebuilds only dirty components (clean ones reuse their cached Element — this is why
build()must not have side effects that need to run every frame); - lays out the tree (
rosace-layout), then paints it (rosace-render), then the compositor presents it to the GPU surface.
5. A state change re-enters the loop. atom.set(v) marks every subscribed component dirty and calls request_frame(), which wakes the platform loop to render one frame. See state-and-reactivity.md for the dirty-set details.
-
Component— what you implement;build(&self, &mut Context) -> Element. -
Context— the per-build handle:state,state_permanent, and the hooks bookkeeping. -
Element— the tree description (component vs native-widget node). -
Atom<T>— a reactive value:get(),set(),update(). -
FrameEngine— drives build → layout → paint each frame; the one place that decides what to rebuild.
-
Hooks-style
ctx.state(call-order keyed), not fields. Chosen so components stay plain structs and state co-locates with the code that uses it — see the state decisions in DECISIONS.md (D008/D121 for persistence tiers). -
Rebuild only the dirty subtree, never the whole tree. The whole point of the atom→subscriber tracking: 60fps means you cannot afford to re-run every
build()every frame. This is whybuild()is expected to be cheap and side-effect-light. -
Elementis deliberately thin. It's a description, not a widget; the actual widget objects (Column,Text) live one layer up inrosace-widgets. Keeping the tree description inrosace-coreis what lets layout/reconciliation reason about structure without depending on the widget set.
-
Call
ctx.state(...)in a stable order, unconditionally. State is matched by call order within abuild(). Puttingctx.statebehind anifshifts every later slot and corrupts state identity (the classic hooks rule). -
build()runs only when the component is dirty. Don't rely on it running every frame. If youatom.set()insidebuild()you can create a rebuild loop — state changes belong in event handlers, not inbuild(). - Clean frames reuse the cached Element. A component that isn't dirty is not rebuilt; its previously-returned Element is reused. If nothing you can see changed but you expected an update, check that the atom you changed is actually the one the component read.