Skip to content

LayrzTreeView

Kenny Mochizuki Escalona edited this page Aug 28, 2026 · 1 revision

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.


Overview

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

LayrzTreeNode<T> uses an explicit id, not content equality

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.

Selection: both modes ship, independent is the default

selectionMode accepts two values:

  • independent (the default) — selecting a node affects only that node; its parent and children are untouched. Matches LayrzChip'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.

Keyboard navigation moves a cursor; it never mutates selection

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.

API

LayrzTreeView<T> / LayrzSliverTreeView<T>

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.

LayrzTreeNode<T>

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

LayrzTreeController extends ChangeNotifier

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.

LayrzTreeSelectionController<T> extends ChangeNotifier

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.

LayrzTreeSelectionMode (enum)

independent (default) · cascading — see the Selection section above.

LayrzTreeNodeBuilder<T> (typedef)

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.

LayrzTreeRow<T> / LayrzTreeIndentGuide

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.

Accessibility

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.

Design tokens used

  • Spacing: sp1sp2 (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), fg1fg3 (row text, chevron, checkbox border when unselected), divider (indent guide lines).
  • Motion: dHover for the expand/collapse chevron rotation.

Related documents

Component Catalog, Milestone 6


Last updated: 2026-08-28 (first documentation of this widget, including both selection modes and arrow-key navigation)

Clone this wiki locally