Tree

The tree piece displays hierarchical data with expandable rows. Use it for document outlines, folders, and grouped navigation.

Signature

API excerpt; not a complete app. NodeSource and its slot/key types are defined in structure.rs.

pub fn tree<S, P>(source: S, build_row: impl Fn(S::Slot) -> P + 'static) -> TreePiece<S>
where
    S: NodeSource + 'static,
    P: Piece;

Use branches(items_fn, key_of, parent_of) for plain data, or a model store’s tree adapter with the model feature. A source key must be stable; slots are recycled values, not permanent row identity. branches preserves sibling input order and treats an absent parent key as a root. The row builder runs on the UI thread and returns a Piece; render changing slot values through tracked reads. Use generated accessors for app-owned row labels and keep user data separate.

Show hierarchical data

This module accepts document titles supplied by the user. They are data, while the tree’s accessible label comes from the app’s Fluent catalog. The recipe harness compiles the module; call outline with a signal of your entries.

use crate::res;
use day::prelude::*;

#[derive(Clone)]
pub struct Entry {
    pub key: u64,
    pub parent: Option<u64>,
    pub title: String,
}

pub fn outline(entries: Signal<Vec<Entry>>) -> impl Piece {
    tree(
        branches(
            move || entries.get(),
            |entry| entry.key,
            |entry| entry.parent,
        ),
        |row: ItemSlot<Entry, u64>| label(move || row.get().title),
    )
    .row_id(|key: &u64| id!(project_node, *key))
    .id(id!(project_tree))
    .a11y(|a| a.label(res::str::project_tree_label()))
}
project_tree_label = Project outline

A None parent creates a root row. A child names its parent’s stable key. Keep keys independent of the row’s title so renaming or translating a title does not change its test identity.

Defaults and state

BuilderDefault and behavior
row_heightRowHeight::Automatic; Uniform(h) supplies fixed row sizing
indentUnset: backend default indentation
section_headers()Off; when enabled, root branches are nonselectable headings and children must be leaves
expanded(binding)Optional app-owned HashSet<Key>; user disclosure writes it; app writes update the tree
on_expansion(f)Reports user/script changes, not programmatic expansion writes
expandable(f)Defaults to having children now; override for an empty branch that can accept children
selected(f) / on_selection(f)Optional selection source and callback; connect both for two-way selection
row_id(f)Optional stable element ID derived from the current key; reevaluated during recycling
row_context_menu(f)Optional menu built when summoned for the current key

Multi-selection and movement are off by default. The TreePiece API lists move guards, reveal, and type-ahead options. tree does not return a construction Result; malformed source data is not thereby validated.

Platform rendering

ToolkitRendering
AppKitNative NSOutlineView
UIKitNative collection-view list
GTKNative tree-list model and expander rows
QtNative QTreeWidget
WinUI / XAMLNative TreeView hierarchy
AndroidComposed list for arbitrary depth; native ExpandableListView for single-selection sections
ArkUI / DOMComposed tree built from list rows

The piece uses a native tree when Cap::Tree is Native. Otherwise it flattens expanded branches into list rows while retaining the same expansion and selection API. A two-level section_headers() tree can use Cap::TreeSections; multi-selection uses the full-tree path.

Use lists for flat rows and navigation for destinations. Tree design history describes the adapter decisions.