edui.widgets.tree (module)
The editor hierarchy tree primitive — edui.tree{...}.
The tree is generic over its nodes. It never reads an entity field: a
caller supplies the accessors and owns the state, and the tree only reports
clicks. That is what lets one primitive back the Entities hierarchy, an asset
folder listing, and a scene-layer list with no branch per caller.
- Accessors:
idOf(node),childrenOf(node),labelOf(node)— defaulting tonode.id,node.children, andnode.label/node.name. - Expand state is the caller's: pass an
expandedset ({ [id] = true }) or anisExpanded(node, id)predicate, and handleonToggle(node, id). - Selection is the caller's: pass
selected(an id, or an{ [id] = true }set for multi-selection) and handleonSelect(node, id). renderRow(node, row, selected)replaces the default label with your own content (badges, per-row icon buttons); return one widget or a list.
Flattening
flatten{ roots, ... } -> { { node, id, depth, hasChildren, expanded, parent } }
is exported separately (edui.widgets.flattenTree) and is pure — no ui.*
calls and no app — so display order is testable without a rendered frame. A
node's children appear only while that node is expanded, so a collapsed tree
costs nothing to build. A node that appears on its own ancestry is reported and
skipped instead of recursing forever.
Row rendering
Rows are uniform-height, which is exactly the shape scrollArea's virtual
prop wants — the renderer then draws only the band in view. Leaf rows still
reserve the disclosure-chevron slot so every label at a given depth starts on
the same column.
Row keys are stable ("<key>:<id>"), so the callback ids survive a rebuild and
anything the renderer tracks by id (focus, ui.widgetState) stays put.
The disclosure chevron and the select target are siblings, never nested — a click lands on exactly one, so opening a node cannot also select it. The select target grows to fill the rest of the row, which is what keeps the whole row clickable rather than just the label. The selection background sits on the non-clickable row container so it spans the indent and chevron too.
Note that virtual is render-side windowing: the row widgets are still built
for every visible node. Construction-side windowing (building only the scrolled
band) needs the virtualTotal/virtualFirst scrollArea props, which are not on
this branch.
Scoped to this part · feeds back into the world's score.