Native GUI

The native platform runs the same Signals engine behind GPUI. Application state, commands, equality pruning, keyed rows, and scope disposal use the APIs described in State, Events, and Forms and Dynamic Structure. Import pf.Gui for native controls. The current targets are Apple Silicon macOS, Linux x86_64 with glibc and a Wayland/GPU session, and Windows x86_64; see contributing for the pinned compiler, host build, executable build, and test commands.

Controls and layout

Gui.row, Gui.column, and Gui.panel take an attribute list followed by a child list. Gui.style accepts a complete record based on Gui.style_default; Gui.style_s changes that record through normal signal propagation. Each element accepts one style. A supplied style replaces the control’s defaults, so include padding or borders explicitly when you want them.

Styles specify logical-pixel dimensions, spacing, padding, colors, borders, radius, font size, and overflow. Lengths are Auto, Fill, or Px(value); colors are Default or Rgb(value). Zero font size and default colors inherit. These are native presentation properties. Semantic labels, test IDs, selected state, and enabled state are separate attributes. The initial window is 1200 × 820 logical pixels and can be moved, resized, minimized, and maximized. The host requests client decorations on Wayland and supplies a draggable title bar and resize borders when the compositor delegates them to the app. The title-bar Close button uses the same Gui.window_lifecycle close guard as an OS close request. The minimum window size is 360 × 240 logical pixels. Apps own their content padding; the frame sits outside that content.

Window overflow, scrollable panels, virtual lists, and editors show draggable scrollbars when content exceeds the viewport. Clicking a track positions its thumb at the pointer; wheel and touchpad scrolling remain available. Clip does not acquire scrolling controls. Scroll offsets belong to native views and do not dispatch application events.

ControlInputs
heading, textliteral string
text_sstring signal
buttonlabel and unit message
action_button{ label, enabled } signals, attributes, unit message
text_input, textarea{ label, value }, attributes, string message
checkbox{ label, checked }, attributes, boolean message

Text controls are controlled: their value comes from a signal and committed edits enter the corresponding message handler. Native editors retain selection, clipboard behavior, and IME preedit locally. textarea preserves hard line breaks and wraps paragraphs to the available viewport width without changing the document text. Caret movement, pointer selection, and composition bounds use those visual rows. Text input is bounded at 1 MiB. Control+Z undoes native edits; Control+Shift+Z or Control+Y redoes them. Contiguous typing groups until whitespace, cursor movement, or a one-second pause. Paste and composition form separate edit groups. Undo restores selection and sends the restored text through the ordinary input handler. Each editor retains at most 128 history boundaries and 8 MiB of text across undo and redo; oldest boundaries expire first. A different authoritative document value clears history, while equal input echoes preserve it. Disabled editors refuse undo and redo. An explicit textarea height (Px or Fill) includes its caption and padding and constrains the retained editing viewport. Auto keeps a 320-pixel editor. Use Fill inside a container with a defined height to grow and shrink with its space. Gui.enabled_s and Gui.disabled_s change availability while preserving the control’s identity. Tab and Shift-Tab traverse enabled controls in native layout order. Focused control actions and declared shortcuts run first; modal dialogs own their Tab navigation while open.

Use Gui.test_id for stable spec locators and Gui.label for semantic names. Labels do not establish native screen-reader support, which is not implemented.

Use Gui.dialog({ label, on_dismiss }, attrs, children) inside Ui.when so mounting and disposal explicitly own the modal lifetime. label supplies the semantic dialog name; on_dismiss is a normal unit message bound to Escape. Closing the dialog is the application’s state transition, never hidden host state. Native file choosers are separate Files tasks.

The host focuses the first enabled button, checkbox, or text control when a dialog opens. Tab and Shift-Tab wrap through its current child order. Buttons activate with Enter or Space and checkboxes with Space; native editing actions keep precedence over region shortcuts. Disabled controls remain mounted but cannot activate. The innermost dialog blocks pointer and keyboard events from the background, and disposal restores the prior control when it is still live and enabled. An empty dialog retains focus itself; if a saved focus owner was disposed or disabled, the parent dialog receives focus, or focus clears when no modal remains.

Concurrent dialogs must form one nested chain, limited to eight dialogs. Each modal admits at most 1,024 nodes and 256 enabled focus targets. Exceeding these programmer limits, or 1,024 parent links during an ancestry check, terminates the native host. The host checks current child order when opening or navigating the modal, without scanning the application during reactive updates. Native semantic specs can locate (role dialog :name "Discard your changes?"); GPUI tests verify actual focus and pointer behavior. This capability is native only and does not add browser modal behavior or a window-close guard.

Wide lists

Gui.virtual_list({ row_height, follow_tail }, attrs, children) lays out only the visible child range. Give every direct child the same fixed logical height; row_height must be between 1 and 16,384. Use Ui.each for keyed child rows. follow_tail is a boolean signal that keeps the final row visible as history changes when enabled.

Virtualization bounds native child lookup and layout work. Reactive row scopes remain mounted, so the application still owns its data retention policy. The Activity Monitor example caps replay and file history at 1,000 entries and 4 MiB of text. Ordinary containers enumerate their direct children when rendered; choose the virtual list for a wide collection.

Keyboard regions

Gui.on_shortcut(chord, message) binds a unit message within a focused region. A chord is { key, control, shift, alt, meta }, with every modifier explicit. Use lowercase letters, digits, or named keys: Enter, Escape, Tab, Space, ArrowLeft, ArrowRight, ArrowUp, ArrowDown, Home, End, PageUp, PageDown, Backspace, Delete, and F1 through F12. Matching is exact. The nearest matching ancestor receives the event; native text-editing actions take precedence.

A region accepts at most 32 shortcuts. Duplicate chords are errors. Registrations belong to their element’s scope and stop receiving events after disposal. See test/gui/shortcuts for a complete app and scoped routing spec.

Internal drag and drop

Add Gui.drag_source(key) to a card and Gui.drop_target(message) to a destination. Keys are nonempty strings of at most 256 UTF-8 bytes. Create the message with Ui.action_detail or Ui.State.on_detail to receive that key and return the same commands used by keyboard or button alternatives. The key is payload data; keyed row identity remains explicit in Ui.each.

Drops from disposed, replaced, disabled, or rebound controls are refused. This API supports drags inside one running application. It does not accept files or other external desktop drag payloads.

Scoped timers and files

Signal.interval(period_ms) registers a native timer while its declaring scope is live. Every tick enters the shared engine; scope disposal cancels the native job and rejects any stale callback. Native transactions reserve at most 256 live or newly declared timers. Activity Monitor uses a 500 ms interval whose scope is present only while replay is running or a followed log is waiting for more data.

pf.Files provides native file/directory choosers, UTF-8 reads, atomic text writes, recursive scans, direct-child directory listings, bounded previews, incremental log reads, and associated-application launches as typed tasks. Use Signal.from_task to observe results and Signal.cancel to invalidate pending work. See the task reference for signatures, errors, and bounds. A dismissed chooser returns Choice.Canceled; explicit task cancellation returns Error.Canceled. Keep the submitted write snapshot separate from the editable draft so a completed save cannot incorrectly mark later edits as saved.

Example coverage

The collection in examples-gui/ exercises platform features through ordinary application workflows. counter is the minimal starting point; keyed-rows shows retained row drafts and scoped structure.

AppMain workflow
task-boardOpen/save board documents, create/edit/drag keyed tasks, undo changes, and protect unsaved work on close
notes-editorEdit wrapped multiline documents with undo, open/save real files, and wait for saves before closing
folder-explorerNavigate real folders with breadcrumbs/history, filter/sort, preview text, and open files in their associated application
activity-monitorFollow a real UTF-8 log or run explicit replay; pause, retry, filter bounded history, and inspect complete records

Activity Monitor explicitly separates simulated replay from chosen plain-text log files. Folder Explorer labels its sample workspace and offers an explicit chooser for real directory navigation.

Notes uses the pinned Roc Unicode package for Unicode 17 grapheme counts and word segmentation. Its word count includes segments containing letters or numbers, excluding punctuation and emoji-only segments. Statistics preserve the original text and do not imply language-specific dictionary segmentation.

Every registered app has native semantic specs. The GUI suite also runs GPUI adapter tests with simulated input and layout. A native window smoke run checks the linked renderer and adapter; an actual pointer, keyboard, and IME walkthrough remains a separate form of validation.

Window close decisions

Wrap the app’s top-level content in Gui.window_lifecycle to protect work before closing. Its on_close_requested message receives a unit event through the ordinary graph. Its decision is a Signal(Gui.CloseDecision): KeepOpen cancels the request, AwaitDecision waits for confirmation or work, and Close completes the pending request. This lets a save result close the window only after the write succeeds. Close without a pending request is inert.

Declare exactly one wrapper directly beneath the app root. Repeated native close requests while awaiting a decision are ignored. Disposing or replacing the wrapper cancels its pending request; the native adapter validates registration lifetime and binding. Apps without a wrapper close immediately. The Notes example demonstrates Save and close, Discard and close, and Keep editing.