Follow one rendered frame from idea to pixels
You already know how to build a small app. Now let us open the hood gently and see how Ailloli UI keeps work focused.
By the end, you will know what rebuilds, what recomputes, what only repaints, and where each public API belongs.
01
A tree that remembers
Ailloli UI reconciles each new declarative view with a retained element tree. Stable identities let the runtime keep layout, input, and reactive relationships attached to the right elements.
Retained identities
The runtime keeps element identities instead of treating every frame as an unrelated drawing.
Precise subscriptions
A State read during Build, Layout, or Paint subscribes the exact active consumer.
Conditional reads
Only the branch that completed successfully remains observed, so unused branches stop requesting work.
02
Three levels, one strongest request
Requests are coalesced before the next frame. If Paint and Build are both requested, Build wins because it already includes layout and paint.
Paint
Reuse the reconciled tree and current layout, then produce a fresh scene.
Layout
Recompute geometry, then paint with the new positions and sizes.
Build
Rebuild the owning component, reconcile it, lay it out, then paint it.
A useful mental shortcut
Think Paint < Layout < Build. Several requests collapse into the strongest one, while the exact roots stay inside the runtime.
03
State moves, Memo derives
State owns a mutable UI value. Memo owns a lazy computation. Together they keep derived labels pleasant without hiding the cost model.
Lazy, not cached
Memo runs its calculation function on every read. Use it for convenient derivation, not as a general computation cache.
Reactive through reads
A memo derived from State observes that State when read inside a retained callback.
UI thread local
State and Memo use Rc internally. They are neither Send nor Sync and stay on their owning UI thread.
Mutations are explicit
State::set and State::update notify consumers. Setting an equal value still invalidates in beta.2.
Two windows, one reactive value
Read the verified application as three separate mechanisms: a visible delayed action, a derived value, and two startup windows.
Build it in small steps
Each code block changes one idea from the verified reference application.
Mechanism 1: a delayed visible action
AddOne changes the score and status, then DispatchAfter emits Celebrate so the status returns to Ready.
fn update(
state: &mut DashboardState,
_services: &mut (),
action: DashboardAction,
) -> Commands<DashboardAction> {
match action {
DashboardAction::AddOne => {
state.count.update(|count| *count += 1);
state.status.set("Saved".to_owned());
Commands::dispatch_after(DashboardAction::Celebrate, Duration::from_millis(250))
}
DashboardAction::Celebrate => {
state.status.set("Ready".to_owned());
Commands::none()
}
}
}Mechanism 2: one derived value
map recalculates the summary when count changes. Memo is lazy and reactive, but it is not a cached computation.
let state = DashboardState {
count: State::new(1),
status: State::new("Ready".to_owned()),
};
let main_state = state.clone();
let doubled = state
.count
.map(|count| format!("Two windows see {} points", count * 2));Mechanism 3: two startup windows
Windows preserves declaration order. main is a readable alias for the first push, not a special runtime window.
let windows = Windows::new()
.main(
Window::<DashboardAction>::new("main")
.title("Retained dashboard")
.size(800.0, 600.0)
.ailloli_ui_chrome()
.content(move || {
Column::new()
.padding(24.0)
.gap(16.0)
.child(
Text::new(
main_state
.count
.to_text_with(|count| format!("Score: {count}")),
)
.size(28.0),
)
.child(Text::new(main_state.status.clone()))
.child(Button::with_label("Add one").on_click(DashboardAction::AddOne))
}),
)
.push(
Window::<DashboardAction>::new("summary")
.title("Live summary")
.size(420.0, 240.0)
.content(move || Text::new(doubled.clone()).size(22.0)),
);Verified with cargo +1.88.0 check against the beta.2 source.
04
Commands leave the reducer in order
An update handler returns an ordered Commands batch. The host executes each public command in insertion order.
Quit
Ask the native event loop to exit.
Redraw
Request a redraw for every declared window.
Dispatch
Queue another typed action for the next drain cycle.
DispatchAfter
Queue a typed action after a relative delay, including a zero-duration delayed action.
Keep the boundary honest
Beta.2 has no arbitrary async task command. For bounded cross-thread actions, use RuntimeInbox and keep I/O ownership outside the UI reducer.
RuntimeInbox crosses the thread boundary
A bounded RuntimeInbox lets worker threads propose actions or redraws to the UI thread. A full or closed queue is reported explicitly, and each host callback drains at most 256 messages.
05
Multiple windows are a startup declaration
Windows is an ordered collection of Window definitions. Every window receives its own native surface while sharing the application state and action type.
Declared before run
Use Windows::new, main, and push, then pass the collection to App::windows.
Unique logical IDs
Duplicate IDs are retained by the builder and rejected before any native window is created.
Current beta limit
No public Command opens or closes a window dynamically in beta.2. The collection describes startup windows.
06
The complete route
Application code declares intent. The runtime retains and schedules. Winit owns desktop events and surfaces. WGPU turns the Scene into pixels.
Four responsibilities, one frame
Each layer has a narrow job. Keeping these boundaries visible makes advanced integrations much easier to reason about.
- 01 · Application
Declare intent
State, actions, commands, widgets, and windows.
- 02 · Runtime
Retain and schedule
Reconciliation, targeted invalidation, layout, input, and Scene production.
- 03 · Native host
Connect the desktop
Winit windows, events, clipboard, wakeups, and surfaces.
- 04 · GPU
Render the scene
The default WGPU renderer executes layers and draw commands.
A custom host can replace the last two layers without changing the retained widget model.
Try it yourself
Exercise: add a delayed reset
Extend the visible delayed-action pattern instead of rebuilding it.
- 1Add a Reset action that sets the score to zero and the status to Reset.
- 2Return DispatchAfter so Celebrate restores Ready after 500 ms.
Let the button emit Reset. Keep both State changes and the follow-up command inside update.
Reset shows zero and Reset immediately, then Ready returns about half a second later.