-
Notifications
You must be signed in to change notification settings - Fork 0
LayrzTreeView
A hierarchical, expandable/collapsible tree with optional multi-node selection and keyboard navigation.
Metadata
Predecessor: None — no ThemedTreeView exists in layrz_theme
Phase: M6 (pulled forward as an M4/M5 prerequisite)
Domain: Data
Primitive: SDK TreeSliver / TreeSliverController (Material-free; not Material's TreeView)
Status: Merged · Review required.
LayrzTreeView (box form, self-scrolling) and LayrzSliverTreeView (sliver form, for composing
inside an existing CustomScrollView, e.g. one of LayrzScaffoldShell's panes) both wrap the SDK's
TreeSliver, rendering a caller-supplied List<LayrzTreeNode<T>> with expand/collapse chevrons,
indent guides, and optional selection checkboxes. The tree is implemented exactly once, in the
sliver form — the box form is a thin CustomScrollView wrapper around it.
LayrzTreeView<String>(
nodes: [
LayrzTreeNode(
id: 'fleet',
content: 'Fleet',
initiallyExpanded: true,
children: [
LayrzTreeNode(id: 'truck-1', content: 'Truck 1'),
LayrzTreeNode(id: 'truck-2', content: 'Truck 2'),
],
),
],
selectable: true,
onSelectionChanged: (selectedIds) => print(selectedIds),
)The SDK's own TreeSliverNode<T> resolves nodes via ==-equality on its content, which is
fragile for arbitrary caller data — two structurally distinct business objects can compare equal,
or the same object can fail to compare equal after a rebuild produces a new instance.
LayrzTreeNode<T> sidesteps this entirely by requiring an explicit id: identity is stated, not
inferred. The id must be unique across the whole tree, not just among siblings — two nodes
sharing an id is a caller error with undefined selection behavior.
children is fixed at construction time — there is no async/lazy-loading contract for children
appearing after a node's first expand.
selectionMode accepts two values:
-
independent(the default) — selecting a node affects only that node; its parent and children are untouched. MatchesLayrzChip's existing selection conventions and how most desktop file-tree explorers behave by default. -
cascading— selecting a parent selects (or deselects) every descendant along with it. A parent whose descendants are only partly selected reports as partially selected (isPartiallySelected) — a third, indeterminate visual state distinct from fully checked or fully unchecked, rendered as a dash glyph rather than a checkmark.
independent is the default because it is the more conservative, less surprising choice for a
design-system primitive: cascading is easy for a consumer to opt into explicitly, but a consumer
who did not expect cascading and gets it anyway has silently selected more than they intended —
the more dangerous failure direction for a shared building block to default to.
Selection is implemented entirely in LayrzTreeSelectionController<T>, independent of
TreeSliver/TreeSliverController — the SDK's own tree model has no selection concept at all,
only expansion.
Arrow keys move a separate keyboard-navigation cursor (LayrzTreeController.activeId) across the
tree's currently-visible rows — a collapsed subtree's descendants are not navigable, matching
what a sighted user can actually see:
- Up / Down — move the active row to the previous/next visible row.
- Right — expands the active row if collapsed, or descends into its first child if already expanded (a no-op on a leaf).
- Left — collapses the active row if expanded, or ascends to its parent if already collapsed (or a leaf).
Arrow keys never call into LayrzTreeSelectionController — moving the active row is a
distinct concept from selecting it. The active row renders as a constant-width outline whose color
changes (per D15: color only, never geometry), composing cleanly alongside isSelected /
isPartiallySelected rather than fighting them for the same visual slot.
| Parameter | Type | Notes |
|---|---|---|
nodes |
List<LayrzTreeNode<T>> |
Required. Root-level nodes; nested nodes come via each node's children. |
nodeBuilder |
LayrzTreeNodeBuilder<T>? |
Builds each row's content. When null, a Text from node.content.toString() is used, always wrapped in LayrzTreeRow for chrome and semantics. |
controller |
LayrzTreeController? |
Optional. Null means the widget creates, owns, and disposes its own. Non-null means the caller owns disposal and must never swap the instance (asserted). |
selectionController |
LayrzTreeSelectionController<T>? |
Optional. Only relevant when selectable is true; same ownership contract as controller. |
selectable |
bool |
Defaults to false. When false, no checkbox renders and no row carries selection semantics, regardless of selectionController. |
onSelectionChanged |
void Function(Set<Object> selectedIds)? |
Fires whenever the selected-id set changes. |
padding |
EdgeInsetsGeometry? |
LayrzTreeView only — padding around the scrollable content. |
| Field | Type | Notes |
|---|---|---|
id |
Object |
Required. Stable identity used for selection and lookup — must be unique across the whole tree. |
content |
T |
Required. The caller-supplied payload rendered by LayrzTreeNodeBuilder. |
children |
List<LayrzTreeNode<T>> |
Defaults to empty (a leaf). Fixed at construction — no lazy loading. |
initiallyExpanded |
bool |
Defaults to false. Only meaningful when children is non-empty; ignored after the tree is first built (expansion is then owned by the SDK's TreeSliver). |
| Member | Notes |
|---|---|
isExpanded(Object id) |
Returns false for an unbound or unknown id. |
expand(Object id) / collapse(Object id) / toggle(Object id)
|
Programmatic expand/collapse by caller id. |
expandAll() / collapseAll()
|
Bulk expand/collapse. |
activeId |
The id of the keyboard-navigation active row, or null. |
setActive(Object id) |
Moves keyboard focus to id, if currently visible. |
| Member | Notes |
|---|---|
mode |
LayrzTreeSelectionMode, get/set. Changing it does not retroactively alter the current selection — only future toggle calls. |
selectedIds |
Unmodifiable set of fully-selected node ids. Under cascading, a partially-selected parent is excluded from this set. |
isSelected(Object id) / isPartiallySelected(Object id)
|
Query selection state. isPartiallySelected always returns false under independent. |
toggle(Object id) |
Toggles selection, applying cascade/ancestor-recompute rules per the active mode. |
clear() |
Clears every selected id. |
updateRoots(List<LayrzTreeNode<T>> roots) |
Called internally when nodes changes, so cascade logic always walks the current tree shape. |
independent (default) · cascading — see the Selection section above.
Widget Function(
BuildContext context,
LayrzTreeNode<T> node,
int depth,
bool isExpanded,
bool isLeaf,
bool isSelected,
bool isPartiallySelected,
VoidCallback? onToggle,
VoidCallback? onSelect,
)onToggle is null for a leaf row. onSelect is provided only when selection is enabled for the
tree.
The default row chrome (LayrzTreeRow) and its decorative indent-guide lines
(LayrzTreeIndentGuide), exported for composition. LayrzTreeIndentGuide paints one vertical line
per ancestor level; it carries no semantics of its own — indentation depth is separately
communicated via each row's own Semantics label.
Each row's Semantics node states role, expansion state, depth ("Level 2 of 3"), and selection
state as one merged announcement, composed at the row level rather than bolted on afterward.
focused is only set (never false) when the row is actually the active row — passing false
would still imply isFocusable, which a non-active row should not claim.
-
Spacing:
sp1–sp2(row padding), 20px per indent level (kLayrzTreeIndentPerLevel). -
Colors:
primary(selected background tint, checkbox fill/border when selected, active-row outline),sf1/sf2(row background by hover state),fg1–fg3(row text, chevron, checkbox border when unselected),divider(indent guide lines). -
Motion:
dHoverfor the expand/collapse chevron rotation.
Component Catalog,
Milestone 6
Last updated: 2026-08-28 (first documentation of this widget, including both selection modes and arrow-key navigation)
Made with ❤️ by Golden M, Inc.
- LayrzAnchoredPanel
- LayrzBottomSheet
- LayrzDialog
- LayrzDropdownMenu
- LayrzResponsiveModal
- LayrzPageTransition
- LayrzTextInput
- LayrzSelectableAction
- LayrzSelectionToolbar
- LayrzTextSelectionControls
- LayrzSelectionMagnifier
- LayrzSelectionHandlePainter
- LayrzTextAreaInput
- LayrzComboBoxInput
- LayrzNumberInput
- LayrzPasswordInput
- LayrzCheckboxInput
- LayrzRadioInput
- LayrzSelectInput
- LayrzMultiSelectInput
- LayrzSearchInput
- LayrzDualListInput
- LayrzDurationInput
- LayrzSlider
- LayrzStepper