Skip to main content
Surface Description is the frame API. Build a 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’s DefaultCommandList
  • elements: laid-out element rects (for inspector/debug)
  • hit_regions: interactive regions for hitTest and dispatch
  • overlays / tooltips: queued after the root tree renders
  • input: last pointer, hover, active, and focus ids
  • scroll_states, animation_states, text_states: keyed by SurfaceId
Create once with explicit capacity limits. Overflow returns FrameError.Overflow.

UiFrame

Short-lived handle from ui_state.begin(options). Call render, queueOverlay, queueTooltip, then finish. Don’t keep it across frames.

FrameOptions

Pass 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)

  1. Measure the node subtree (content intrinsic size).
  2. Apply LayoutSpec sizing rules inside bounds.
  3. Place children and emit draw commands for leaf content.
  4. Register hit regions for nodes with hit set.
Returns the final rectangle the root occupied.

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 runtime
  • namedComptime("settings.save") hashes at compile time (preferred)
  • child(parent, "suffix") / childInt(parent, n) builds hierarchical ids for list rows
Use the same id in your input handler and in the node tree.

Building node trees

Static children array

Comptime tuple via staticChildren

Store child nodes in struct fields when ids or specs depend on runtime state (see otter-note/src/surface.zig).

Theme and disabled state

Leaf emitters read colors from FrameOptions.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

Next