render records hit regions. Your Wayland loop passes pointer and keyboard events through dispatch, or you can call hitTest yourself.
Hit regions
Each interactive node withhit set appends a HitRegion:
render + finish:
hitTest picks the topmost enabled region containing the point (higher z wins).
dispatch event router
UiState.dispatch updates input snapshot and returns a DispatchResult:
| Event | DispatchAction | Typical app response |
|---|---|---|
pointer_motion | .hover | Track hovered id for button hover styles |
pointer_leave | .leave | Clear hover |
button_press | .press | Set pressed id, start slider drag |
button_release | .release | Fire click if still over same id |
scroll | .scroll | Adjust ScrollState or UniformList.scroll_offset |
keyboard_focus | .focus | Move focused id inside scope |
text_input_focus | .text_focus | Open IME for text_input nodes |
input snapshot back into the next frame’s FrameOptions.input so controls render hovered/pressed/focused states.
InputSnapshot
Focus traversal
Setfocus_scope = true on modal panels. keyboard_focus events with FocusMove (.next, .previous, .first, .last) walk focusable hits inside that scope.
Focusable kinds include buttons, text inputs, toggles, sliders, selects, and tabs.
Overlays
Popups that must float above the main tree go throughqueueOverlay, not stack.
finish() renders overlays after the root tree. Overlay hits set HitFlags.overlay = true.
Dismiss when clicking outside:
.below flips to .above automatically when the popup would clip past the viewport bottom.
Tooltips
text is skipped.
UniformList (virtualized rows)
Fixed-height lists without allocating per-row nodes. Compute which indices are visible, build only those list_row nodes each frame.
| Method | Purpose |
|---|---|
visibleRange() | Start/end index and y offset for scrolling |
hitIndex(list_rect, point) | Which row index was clicked |
scrollOffsetForSelection(selected) | Scroll so selection stays visible |
moveSelection(selected, move) | Keyboard up/down/page |
contentHeight() | Total scrollable height |
maxScrollOffset() | Clamp scroll offset |
UniformGrid adds column-aware scrolling and hit testing for grid layouts.
Scrollbars
- Keep scroll offset in app state.
- Pass
ui.ScrollStatewith content and viewport heights intoScrollbarSpec. - On
dispatch.scrollover the list area, adjust offset. - Optionally add a
scrollbarnode bound to the sameScrollState.
ui_state.scrollState(id) returns a persistent slot keyed by SurfaceId.
Text input and IME
text_inputnode withfocused = truewheninput.focusedmatches.- On
text_input_focusdispatch, enable Waylandtext-input-v3on that surface. - Pipe committed text and preedit into
InputBuffer. - Pass buffer contents back into
TextInputSpeceach frame.
otter-settings and otter-launcher for full IME wiring.
Damage and partial redraw
Afterrasterize, merge damage from:
DamageTrackerfor surface commitsoverlayDamageRect()when overlays animate open/close
Debug inspector
Ctrl+Shift+I toggles element rects and semantics. findElement(id) returns the laid-out Element after render.
Next
- Nodes and controls: spec fields per control
- Drawing primitives: manual command emission

