Programming model
Views, entities and contexts, what render returns, and how the retained tree reaches a backend.
An app is a set of entities. Some of them are views: they implement Render and return an element tree. When an entity changes it calls cx.notify(), and the framework runs render again for that view only. Everything runs in one process on the main thread; there is no IPC layer to cross.

Captured on macOS 27.0, 2026-10-14.
Views and render
pub struct Counter {
count: u32,
}
impl Render for Counter {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = cx.theme();
scaffold()
.toolbar(toolbar().item(button("add", "Add one").on_click(cx.listener(
|this, _: &ClickEvent, _window, cx| {
this.count += 1;
cx.notify();
},
))))
.content(
v_flex()
.size_full()
.items_center()
.justify_center()
.gap_2()
.child(window_controls().at(point(px(16.), px(10.))))
.child(text(self.count.to_string()).text_size(px(48.)).tabular_nums())
.child(text("clicks").text_color(theme.text.secondary)),
)
}
}
From examples/hello/src/lib.rs, line 4.
Counter is a plain struct. impl Render gives it one method, render(&mut self, window, cx), which returns impl IntoElement. render describes the window as it should look now; it does not mutate anything on screen. Keep it cheap and free of side effects, because the framework calls it whenever the view is notified.
cx is a Context<Counter>. It dereferences to App, so everything an App can do (open a window, read the theme, set a global) works through it, and it adds what is specific to this entity:
cx.notify()marks this view dirty. The next frame re-renders it.cx.listener(|this, event, window, cx| ..)turns a closure over&mut Counterinto the handler an element stores. The entity is updated when the event is delivered.cx.observe(&other, |this, other, cx| ..)runs whenothernotifies. See state.cx.weak_entity()gives a handle that a callback can hold without keeping the view alive.
Entities
cx.new(|cx| value) builds an Entity<T>, a shared handle to a T that the app owns. An entity is read with entity.read(cx) and changed with entity.update(cx, |value, cx| ..). An update leases the value, so reading or updating the same entity from inside its own update panics. Views are entities; so are models that hold data and have no render. A WeakEntity returns None from update once the entity is gone, which is why callbacks use it.
pub fn app() -> Application {
Application::new(nd::manifest!())
}
pub fn open(cx: &mut App) -> WindowHandle<Counter> {
cx.open_window(WindowOptions::main("main").title("Hello"), |_window, cx| cx.new(|_cx| Counter { count: 0 }))
}
From examples/hello/src/lib.rs, line 35.
open_window builds the root view with a closure and returns a WindowHandle<Counter>. handle.id() is the window id; handle.root() is the root entity. An entity is not bound to a window, so a view can move to another window and keep its state.
Invalidation is per view
cx.notify() re-runs render of that one view. A child that is itself an Entity is a boundary: parent and child render only when each is notified, so a keystroke in a text editor does not re-render the list next to it. The cost of a change is the size of the dirty view, not the window.
After render, the framework compares the new element description with the retained tree and sends the backend only what changed: create, insert, move, remove, update, and for retained backends a frame. Event handlers sit on the retained node and are swapped without a backend call, so a new closure on every render costs nothing. Elements explains how a child is matched to its previous self.
The backend seam
The framework talks to a platform through one Backend trait. The AppKit backend, the GPUI backend and the test backend implement it, and the app never sees which one it has.
- One transaction per window per frame:
begin, the ops in a fixed order (creates, structure with parents first, updates, frames, focus, releases), thencommit. On AppKit that is oneCATransactionwith implicit animations off. - The backend holds one strong reference per node and drops it on
release. - Layout is Taffy everywhere. On AppKit the framework runs Taffy and pushes frames to the native views. On GPUI, Taffy runs inside GPUI's element tree.
- Everything happens on the main thread.
Native controls never call into your code while the framework is applying changes. AppKit fires delegate methods inside setters and during layout, and an app entity can be mid-update at that moment. A native control pushes its events onto a per-window queue instead, and the queue drains after the transaction, outside every entity lease. This is why a handler can safely change any entity.
Tests use the same tree
TestApp is an App over a TestBackend that records the ops and exposes the semantic tree. The test for the counter opens the real hello::open and clicks the button by id; see the hello tutorial and the testing page.