bezel

Components / Data

Tree view

Markdown crates/ui/src/tree.rs docs.rs

use ui::tree::{self, Row};

tree::init(cx);   // once, at startup

let rows = self.flatten();   // Vec<(Row, label)>

tree::tree().children(rows.iter().enumerate().map(|(index, (row, label))| {
    tree::tree_row(&theme, row, self.selected == Some(index), self.cursor == index)
        .id(("row", index))
        .child(label.clone())
}))

The app flattens what is currently visible — which it has to do to render it anyway. A depth-annotated flat list is a complete navigation model: down and up are neighbouring indices, a first child is the next row, and a parent is the nearest row above with a smaller depth.

Keys

match tree::step(&rows, self.cursor, tree::Direction::Right) {
    Some(tree::Move::To(index)) => self.cursor = index,
    Some(tree::Move::Expand(index)) => self.open.insert(index),
    Some(tree::Move::Collapse(index)) => self.open.remove(&index),
    None => {}
}

API

// ui::tree

pub fn init(cx: &mut App);
pub fn tree() -> gpui::Div;

/// `selected` is what the app considers chosen, `cursor` is where the keyboard
/// is — the same pair a menu row draws with.
pub fn tree_row(theme: &Theme, row: &Row, selected: bool, cursor: bool) -> gpui::Div;

/// `expanded: None` is a leaf, which is a different thing from a closed branch
/// — the difference is what stops `right` pretending a file can open.
pub struct Row {
    pub depth: usize,
    pub expanded: Option<bool>,
}

/// Reports an intent rather than performing one: applying it means touching
/// the expansion set the app owns. Neither end wraps.
pub fn step(rows: &[Row], cursor: usize, direction: Direction) -> Option<Move>;

pub enum Move { To(usize), Expand(usize), Collapse(usize) }

// ...

Scrolling is the caller's, through scroll. Expansion stays with the app because it is app data: a file tree's open folders often outlive the window.

Tree view