SurfaceNode tree; each frame UiFrame measures it, places rects, emits otter-render commands, and records click targets.
Core objects
UiState(capacities)
Long-lived state on your app struct. Holds:
commands: the frame’sDefaultCommandListelements: laid-out element rects (for inspector/debug)hit_regions: interactive regions forhitTestanddispatchoverlays/tooltips: queued after the root tree rendersinput: last pointer, hover, active, and focus idsscroll_states,animation_states,text_states: keyed bySurfaceId
FrameError.Overflow.
UiFrame
Short-lived handle from ui_state.begin(options). Call render, queueOverlay, queueTooltip, then finish. Don’t keep it across frames.
FrameOptions
| Field | Required | Purpose |
|---|---|---|
viewport | yes | Root bounds (usually full window) |
scale | yes | HiDPI scale from the Wayland surface |
font | for text | Default font for labels and controls |
theme | recommended | Color tokens from otter-theme |
styles | optional | StyleSet overrides |
input | optional | Pointer/focus snapshot; defaults to state from last dispatch |
text_system / text_scratch | for RTL/CJK | HarfBuzz + bidi shaping via otter-render |
text_provider | optional | Lazy TextSystem creation callback |
allocator | optional | Needed for some overlay paths |
text_system and text_scratch when the UI shows user-typed text, RTL labels, or CJK. Plain ASCII labels can skip them.
Frame lifecycle
render(node, bounds)
- Measure the node subtree (content intrinsic size).
- Apply
LayoutSpecsizing rules insidebounds. - Place children and emit draw commands for leaf content.
- Register hit regions for nodes with
hitset.
finish()
Renders queued overlays and tooltips on top, updates debug metrics, finalizes overlay damage. Call finish() before rasterizing.
state.rasterize(surface, damage_rects, full_redraw)
Hands the command list to otter-render’s quad_renderer. Pass damage rects from otter-wayland DamageTracker for partial redraws.
Stable ids with SurfaceId
Every interactive or inspectable node needs an id:
named("settings.save")hashes a string at runtimenamedComptime("settings.save")hashes at compile time (preferred)child(parent, "suffix")/childInt(parent, n)builds hierarchical ids for list rows
Building node trees
Static children array
Comptime tuple via staticChildren
otter-note/src/surface.zig).
Theme and disabled state
Leaf emitters read colors fromFrameOptions.theme when spec color fields are null. Set disabled = true on control specs (for example ButtonSpec.disabled) to gray out content and mark hit regions non-enabled.
For app-specific chrome, pass styles: &my_style_set or set explicit colors on each spec.
Debug overlays
Toggle with Ctrl+Shift+I at runtime, or start with--inspect / --metrics. UiState records frame timing, command counts, and memory in debug_metrics.
Common mistakes
| Mistake | Fix |
|---|---|
Skipping finish() | Overlays and tooltips never draw |
Reusing one UiFrame across frames | Call begin() each frame; state resets internally |
No id on clickable nodes | Set hit on the node or use a constructor that sets it (button, toggle, etc.) |
| Capacity too small | Increase Capacities.elements or hit_regions; overflow is an error |
| Shaped text shows wrong glyphs | Pass text_system and text_scratch in FrameOptions |
Next
- Layout model:
NodeKind,SizeRule, flex, alignment - Nodes and controls: per-control spec fields
- Input and overlays: pointer routing and popups

