Repository navigation
[M2-UI-01] UI widget tree (retained, screen-space) - #90
Merged
Merged
Conversation
- New laige::render UI module (public header-only
laige/render/ui_widgets.h; no new .cpp, no GL calls, no new
dependency): the retained-data half of the UI work.
- UiTree (move-only; Options{maxWidgets, maxEvents, screenWidth,
screenHeight}; screen dims validated in [1, 2^20]): the pool-backed
retained tree of the ADR 0006 widget set -- exactly panel, button,
text, image, list, slider, input, in the ADR's canonical order
(the M2-DEC-02 Verify clause: the ADR widget list == the implemented
set). UiWidgetId = laige::PoolHandle (stable generation-checked ids;
kInvalidUiWidgetId never live; a stale id reads as nullptr, never
UB). Two allocations at create only (the widget pool -- maxWidgets
INCLUDING the root -- and the per-frame event array); the widget
budget failure is BudgetExhausted + one rate-limited Warn
ui_tree/widget_budget.
- Layout (layout()): one iterative preorder pass (the parent always
before its children -- a 65k-deep chain is safe), exact integer
math, deterministic (RENDER-003). The root is the layout-only
screen container (never pointer-hit, never focusable, never
removable). A child's rect = its anchor point in the parent's
content box (the rect inset by its padding, clamped to 0 x 0 when
the padding is wider) + its offset - its anchor offset in its own
size (the nine UiAnchor values). A list stacks its children
top-to-bottom in insertion order at the content's left edge (the
children's anchors/offsets ignored), shifted up by the normalized
scroll in [0, totalContentHeight - contentHeight]. Computed
coordinates saturate into +/- (2^21 - 1) (kUiMaxCoordPx: a widget
that far offscreen renders nothing -- the M2-UI-02 cull; the int32
storage stays exact). A node's rect is zero until the next layout
(a zero-rect node contains no pointer point).
- Focus: the focusable kinds are button, slider, input (ADR 0006).
focus()/focusWidget/focusNext/focusPrev over the deterministic
preorder traversal (wraparound; from unfocused, next -> first /
prev -> last); a pointer press on a focusable widget takes the
focus; removing the focused widget (or an ancestor) clears it. No
keyboard events here (action routing is M3-INPUT-04).
- Frame protocol + events: layout() then handlePointer(UiPointerState
{x, y, pressed}) (or beginFrame() for a pointerless frame).
handlePointer hit-tests the topmost node (the LAST node in
traversal order whose half-open rect contains the point; the root
excluded) and delivers, in order: leave-then-enter
PointerLeave/PointerEnter on a top change; PointerPress + focus +
the slider drag start on press; PointerRelease + ButtonPressed on
release iff the press started on that same button (a press that
moved off the button emits no ButtonPressed); the press-owner
slider keeps tracking on pressed frames (value = min + t*(max-min),
t clamped to [0, 1] over the slider's width -- a zero-width slider
takes no input). The per-frame event list is bounded: the first
maxEvents events are kept, a later one dropped + counted in
droppedEventsTotal() + one rate-limited Warn ui_tree/event_overflow
(the LOG-004 facade windows the repeats). The ADR 0006 event set:
pointer enter/leave/press/release, ButtonPressed, SliderChanged
(only the pointer drag emits it -- setSliderValue clamps and stays
silent), InputText (from setInputText; a no-op for the same
string handle).
- FR-4.4 hook: pointerHit() = the topmost node under the last
processed pointer position (a hit = the UI consumed the frame's
pointer input -- the UI-first routing hook). No sim coupling
(ARCH-009: presentation state, never mutates sim); owner-thread-only
(CONC-001).
- Tests: tests/laige-render/ui_tree_tests.cpp (new CTest entry
ui_tree = the step's Verify command; 24 tests / 6 suites; GL-free
-- no context, runs in every tree incl. ASan/TSan): UiTreeCreate
(the validation matrix, the no-log failure contract, the stopped
state, the two-alloc create, the move-only semantics), UiTreeCrud
(the seven kinds, the kind/parent/geometry/slider domain
validation, the widget budget + warn, the sibling-chain structure,
the subtree removal + LIFO slot recycling, the setter failures),
UiTreeLayout (the nine-anchor goldens against hand-computed rects,
offsets, container padding (incl. padding wider than the
container), list stacking + scroll normalization (incl. the
over-scroll clamp), negative rects + coordinate saturation, the
zero-rect-before-layout + idempotence), UiTreeFocus (the
focusable kinds only, the deterministic tab order with wraparound,
the pointer-press focus, the removal clears), UiTreeEvents (the
scripted pointer sequences against hand-computed per-frame event
goldens: the hover transitions, the button click (incl. the
drag-off no-press), the slider drag across frames, the
press-owner tracking across widgets, the input text events, the
zero-size widgets, the event overflow), UiTreeZeroAlloc (1 000
frames of layout + pointer + placement + input = 0 heap blocks,
the machine-greppable ui-tree-zeroalloc line).
- Docs (DOC-006/007): NEW docs/api/ui_widgets.md (the full contract
incl. the DOC-004 Performance section); docs/README.md API index
row; src/laige-render/README.md status; docs/concepts/coordinates.md
(section 4.13 the UI widget tree coordinate section + the section 5
conversion row + the Related link); roadmap M2-UI-01 box checked +
the Progress Board M2 25/33 (total 71/194) + the change log row.
- Verification: all six canonical trees build warning-free and
ctest -R ui_tree green in each (24/24); tools/laige-include-lint +
tools/laige-determinism-lint OK; laige-api.json regenerated LAST
(api-real-tree green); no budgets.json entry (the per-frame UI cost
is part of the composite 50k render-CPU budget, M2-PERF-01 -- the
pin count stays 17). No rendering (the UI draw pass is M2-UI-02).
The shared GitHub macOS runners' tail latency exceeds the 10-min build budget: the M2-UI-01 PR lane (run 38138278150) saw three consecutive 10-min cancellations of the macOS arm64 job (11m25s in Build, 10m18s in Test, 10m32s in Build) while the identical build + test finished in 5m37s on the macOS Intel lane. Raise the two macOS build jobs in both lanes (PR + merge) to timeout-minutes: 15; the macOS exception is documented in ci.yml's build-budget header. Nothing else changes (no engine, test, or budget-gate impact).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
M2-UI-01 — UI widget tree (retained, screen-space)
The retained-data half of the UI work:
laige::render::UiTree(header-only, no GL, no new dependency) — the pool-backed retained tree of the ADR 0006 widget set (panel, button, text, image, list, slider, input — exactly the M2-DEC-02 list), stable generation-checked widget ids, the deterministic per-frame layout pass (the nine anchors, the list stacking + scroll, the coordinate saturation), the focus system (button / slider / input; the deterministic preorder tab order), and the per-frame bounded pointer-event list (the ADR 0006 events; the first-maxEventsoverflow policy).pointerHit()is the FR-4.4 UI-first routing hook. No rendering (M2-UI-02), no sim coupling (ARCH-009).docs/api/ui_widgets.md(new; incl. the Performance section)ctest -R ui_tree— 24 tests / 6 suites (green in all six canonical trees)