GPUI separates renderer-independent application state from display lifecycle. This keeps view logic deterministic, permits local and remote presentation, and prevents native window ownership from leaking into application processes.
Session
GPUI.Session owns:
- window specifications and root-view assigns;
- rendered
%GPUI.Snapshot{}values; - resource metadata;
- event dispatch and bounded event history.
A session does not own a native event loop or platform window. Its boundary is serializable Elixir data.
Dynamic window topology
Initial windows may declare a stable application-owned key independently of the session's monotonic native window ID:
window "main", "Workspace" do
size 1100, 720
root WorkspaceView
endA running local session can add and remove windows without remounting the application:
details = %GPUI.WindowSpec{
key: "repository-details",
title: "Repository details",
root: {RepositoryDetailsView, %{repository: repository}}
}
{:ok, window_id, snapshot} = GPUI.Runtime.open_window(runtime, details)
{:ok, snapshot} = GPUI.Runtime.close_window(runtime, "repository-details")Keys are optional for initial windows, but dynamically managed windows should
use non-empty unique strings of at most 128 bytes. Titles are limited to 512
bytes and a session can own at most 32 windows. IDs are never reused in a
session, including after a keyed window closes and reopens. Closing
by key or ID removes only that window; other root assigns and native windows
remain intact. Snapshot synchronization performs the actual platform close.
Source refresh rerenders the current topology and preserves every window's
existing assigns, key, and session ID. It does not remount the application or
resurrect windows that have already closed. Dynamic ID allocation also remains
monotonic across reload, while newly compiled render and callback code applies
to every retained root module. Refresh renders the complete snapshot before
publishing it, so a failure in any window produces no display or subscriber
update. Development watchers can optionally report bounded :gpui_reload
results; syntax-invalid files leave the loaded module and live session usable
for the next valid edit.
These mutation calls are local runtime/session capabilities. Remote topology
mutation is intentionally not added as an imperative transport operation.
View-driven topology works remotely through ordinary :event requests: the
hosted session applies the typed outcome and returns one authoritative
multi-window snapshot. Protocol negotiation advertises :window_topology_v1,
and session resume restores existing keys, IDs, assigns, and monotonic ID
allocation.
Display
A GPUI.Display receives snapshots and returns normalized events. The package
ships two principal implementations:
GPUI.Display.Nativepresents snapshots through Rust GPUI windows;GPUI.Test.Displayrecords snapshots and accepts deterministic injected events.
Remote clients can also mount a native display against a session hosted by
GPUI.Remote.Server.
Custom displays implement GPUI.Display. Startup must return {:ok, pid} or
{:error, reason}; synchronization must return :ok or {:error, reason};
event draining must return {:ok, events} or {:error, reason}; and injection
must return an :ok or :error tuple. GPUI rejects invalid returns as
{:invalid_display_return, callback, value} and catches callback failures as
{:display_callback_failed, callback, kind, reason}.
Runtime
GPUI.Runtime composes one session with one display. It synchronizes snapshots,
polls display events, dispatches event batches through the session, and sends
the resulting snapshot back to the display.
{:ok, runtime} =
GPUI.Runtime.start_link(
app: MyApp.Desktop,
args: %{account_id: account_id},
display: GPUI.Display.Native,
poll_interval: 16
)Normally the application module is placed directly in a supervision tree and its generated child specification starts the runtime.
Supervised workers can update a root view without pretending that application work is native input:
{:ok, snapshot} = GPUI.Runtime.send_view(runtime, window_id, {:loaded, records})The selected view receives the message in handle_info/2. The runtime then
synchronizes and publishes the resulting snapshot. handle_event/3 and
handle_info/2 can also return typed window outcomes without receiving a
runtime PID or a generic effect bus:
def handle_event("open-details", _event, assigns) do
details = %GPUI.WindowSpec{
key: "repository-details",
title: "Repository details",
root: {RepositoryDetailsView, %{repository: assigns.repository}}
}
{:open_window, details, assigns}
end
def handle_event("close-details", _event, assigns) do
{:close_window, "repository-details", assigns}
endSupported view results are {:noreply, assigns}, {:close, assigns},
{:open_window, spec, assigns}, and
{:close_window, key_or_id, assigns}. A topology outcome and its originating
assigns update form one session transition and one synchronized snapshot.
Duplicate keys and missing close targets leave the authoritative topology and
originating assigns unchanged and are returned as normalized event errors.
Runtime updates are available through ordinary OTP messages:
:ok = GPUI.Runtime.subscribe(runtime)
receive do
{:gpui, ^runtime, %GPUI.Runtime.Update{revision: revision, events: events, snapshot: snapshot}} ->
# The snapshot has already been synchronized to the active display.
end
:ok = GPUI.Runtime.unsubscribe(runtime)Revisions increase monotonically within a runtime. Subscribers are monitored
and removed automatically when they exit. Frame synchronization is separate
from snapshot synchronization: GPUI.Runtime.await_frame/3 waits until the
current snapshot generation has completed a frame, without blocking the runtime
from processing messages.
Native-only changes such as hover, focus, tooltip timers, and IME state can be synchronized with a completed-frame token:
{:ok, generation} = GPUI.Runtime.frame_token(runtime, window_id)
# Send native platform input here.
:ok = GPUI.Runtime.await_frame_after(runtime, window_id, generation)GPUI.Runtime.request_frame/1 resynchronizes the current snapshot when callers
need an explicit frame without changing application state. Runtime operations
return structured :display_start_failed, :display_sync_failed,
:display_drain_failed, and :display_inject_failed errors instead of crashing
when a custom display fails its contract. Session mutations happen before display
synchronization; after a synchronization error, request_frame/1 retries the
current authoritative snapshot.
Native process model
Native displays share one process-global GPUI application loop. That loop owns all platform windows, while commands and registry state remain scoped to their originating runtime and window IDs.
Loop acquisition is platform-specific behind the same host boundary:
- on macOS, the host asks ERTS to hand the original process main thread to GPUI, satisfying AppKit's main-thread requirement without moving application state out of Elixir;
- on Linux and Windows, the host starts one permanent dedicated native GUI thread and keeps every window on that thread.
Startup is acknowledged only after the GPUI application has initialized. The last runtime releasing its windows does not tear down and recreate the process-global application loop.
Commands are acknowledged: callers receive success, timeout, disconnection, or stopped-runtime outcomes instead of enqueue-only success. Native frame barriers track snapshot and completed-frame generations per window and acknowledge after the corresponding frame has passed prepaint and presentation submission. Closing the final window does not terminate the shared loop. Runtime shutdown is non-blocking and cleans up only that runtime's windows and component state.
Declarative viewport
Each rendered window tree is wrapped by a renderer-internal :viewport node in
Elixir before it crosses a display boundary. The node establishes the current
window content bounds; it is included in native and remote snapshots but is not
a public template tag. Rust interprets those declared viewport semantics and
does not inject application-specific layout wrappers.
Application roots should declare how they consume the viewport. A flex root
normally uses grow and w-full; definite w-full and h-full lengths remain
available where the parent layout provides a definite corresponding dimension.
This keeps viewport declaration, layout values, defaults, and component policy
in Elixir while leaving platform window state in GPUI.
Snapshot reconciliation
A snapshot contains the complete declarative window set. Native synchronization:
- creates missing windows;
- updates existing window trees and resources;
- removes windows absent from the next snapshot;
- reconciles stateful controls by component kind and stable ID;
- drops registry entries no longer present in the rendered tree.
Window snapshots also carry bounded command ID/shortcut pairs. The native root
observes matched keystrokes after normal GPUI dispatch, emits :command events,
and suppresses standard editing combinations while an input is focused.
Component IDs never depend on traversal position. Duplicate structural IDs are rejected before crossing the display boundary.
RustQ boundary
RustQ generates repetitive native boundaries from GPUI's internal schema:
- source-discovered atoms and Rustler NIF exports;
- resource contracts and decoders;
- window, primitive-element, component, and event-value decoders authored as typed Rusty-Elixir where their behavior is renderer-independent;
- source-derived renderer routing and element dispatch;
- style dispatch;
- disabled-feature NIF implementations;
- stateful registry kinds and typed accessors.
Component defaults and attribute constraints come from the same schema used by public builders and native decoding. Renderer metadata is indexed from the real Rust functions in one pass instead of maintained in a parallel route table.
GPUI lifecycle, entities, focus, callbacks, reconciliation behavior, and platform integration remain handwritten Rust. Shared native collection mechanics live in a handwritten uniform-collection core; listbox and tree keyboard and accessibility semantics remain in their focused renderers. This keeps generated code structural while preserving readable native behavior.
See Remote displays for transport topology and Testing GPUI applications for renderer-independent testing.