NativeDesktop

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.

Example: Hello. The smallest app: a toolbar button, a count and window controls placed by coordinates. Source in examples/hello. It also runs live in the docs app.
Hello on macOS

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:

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.

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.