Skip to main content

ui/
tree.rs

1//! Tree view — nested rows with disclosure, indent guides and arrow keys.
2//!
3//! bezel cannot walk your tree. It has no idea what a node is, and a trait or a
4//! callback to find out would be a data model this library does not want to
5//! own — so the app flattens its own tree into the rows that are visible right
6//! now, which it has to do anyway to render them.
7//!
8//! That is not a compromise, because **a depth-annotated flat list is a
9//! complete navigation model**. Everything a tree does falls out of [`Row`]
10//! with no parent pointers and no tree walk: down and up are neighbouring
11//! indices, a first child is simply the next row, and a parent is the nearest
12//! row above with a smaller depth. [`step`] is that, and nothing else.
13//!
14//! ```ignore
15//! ui::tree::init(cx);   // once, at startup
16//!
17//! // Each frame: flatten what is open, paint it, and let `step` answer the keys.
18//! let rows = self.flatten();                       // Vec<(Row, label)>
19//! tree().children(rows.iter().enumerate().map(|(index, (row, label))| {
20//!     let selected = Selection::new(self.selected == Some(index), self.focused);
21//!     tree_row(&theme, row, selected, self.cursor == index)
22//!         .id(("row", index))
23//!         .child(label.clone())
24//! }))
25//! ```
26//!
27//! Expansion stays with the app because it *is* app data — a file tree's open
28//! folders often outlive the window — so [`step`] reports an intent, and the app
29//! applies it to the set it owns.
30
31use gpui::{App, KeyBinding, actions, div, prelude::*, px};
32
33use theme::{Selection, TextStyle, Theme, Typeset};
34
35use crate::widgets::Layout;
36
37/// One visible row: how deep it sits, and whether it is a branch.
38///
39/// `expanded` is `None` for a leaf — which is a different thing from a closed
40/// branch, and the difference is what stops `right` pretending a file can open.
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub struct Row {
43    pub depth: usize,
44    pub expanded: Option<bool>,
45}
46
47impl Row {
48    pub fn leaf(depth: usize) -> Self {
49        Self {
50            depth,
51            expanded: None,
52        }
53    }
54
55    pub fn branch(depth: usize, expanded: bool) -> Self {
56        Self {
57            depth,
58            expanded: Some(expanded),
59        }
60    }
61}
62
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64pub enum Direction {
65    Up,
66    Down,
67    Left,
68    Right,
69}
70
71/// What a keypress meant. An intent rather than a mutation: only the app can
72/// expand a row, because only the app knows what is under it.
73#[derive(Clone, Copy, Debug, PartialEq, Eq)]
74pub enum Move {
75    To(usize),
76    Expand(usize),
77    Collapse(usize),
78}
79
80/// The row `index` hangs under: the nearest row above it with a smaller depth.
81///
82/// Nearest, not previous — the row directly above is usually a sibling, and
83/// often the last leaf of a sibling's whole subtree. Walking up until the depth
84/// actually drops is what skips all of that.
85pub fn parent_of(rows: &[Row], index: usize) -> Option<usize> {
86    let depth = rows.get(index)?.depth;
87    rows[..index]
88        .iter()
89        .rposition(|candidate| candidate.depth < depth)
90}
91
92/// What an arrow key means at `cursor`, or `None` when it means nothing.
93///
94/// Neither end wraps. A menu wraps because it is a ring of choices; a tree is a
95/// document, and arriving back at the top because you pressed down once too
96/// often loses your place in it.
97pub fn step(rows: &[Row], cursor: usize, direction: Direction) -> Option<Move> {
98    let row = rows.get(cursor)?;
99    match direction {
100        Direction::Up => cursor.checked_sub(1).map(Move::To),
101        Direction::Down => (cursor + 1 < rows.len()).then_some(Move::To(cursor + 1)),
102        // A closed branch opens; an open one steps into it — and its first child
103        // is just the next row, because the list is already in visible order.
104        Direction::Right => match row.expanded {
105            Some(false) => Some(Move::Expand(cursor)),
106            Some(true) => (cursor + 1 < rows.len()).then_some(Move::To(cursor + 1)),
107            None => None,
108        },
109        // The mirror: an open branch closes, everything else goes up a level.
110        Direction::Left => match row.expanded {
111            Some(true) => Some(Move::Collapse(cursor)),
112            _ => parent_of(rows, cursor).map(Move::To),
113        },
114    }
115}
116
117actions!(bezel_tree, [SelectPrevious, SelectNext, Collapse, Expand]);
118
119/// The key context a tree claims. [`tree`] does not set it: the caller puts it,
120/// with focus tracking and a handler per action, on the element holding the
121/// rows.
122pub const KEY_CONTEXT: &str = "Tree";
123
124/// Install the bindings — [`bindings`], bound. Call once, alongside
125/// [`crate::input::init`].
126pub fn init(cx: &mut App) {
127    cx.bind_keys(bindings());
128}
129
130/// The tree's four arrows, as data, so an app can have it without having to
131/// take it — see [`crate::keys`] for layering over it or taking a chord
132/// away.
133///
134/// The handlers are the app's — like [`crate::focus`]
135/// and unlike the menubar, a tree cannot handle them itself, because applying a
136/// [`Move`] means touching the app's own expansion set. What bezel does here is
137/// name the four chords everyone already agrees on, once.
138pub fn bindings() -> Vec<KeyBinding> {
139    let mut bindings = Vec::new();
140    let ctx = Some(KEY_CONTEXT);
141    bindings.extend([
142        KeyBinding::new("up", SelectPrevious, ctx),
143        KeyBinding::new("down", SelectNext, ctx),
144        KeyBinding::new("left", Collapse, ctx),
145        KeyBinding::new("right", Expand, ctx),
146    ]);
147
148    bindings
149}
150
151/// How far a row sits in from either side of the tree.
152const ROW_INSET: f32 = 4.0;
153
154/// How far one level of nesting indents.
155pub const INDENT: f32 = 14.0;
156/// Width of the chevron column, kept by leaves as well so their labels line up
157/// with their siblings' rather than sliding under them.
158const CHEVRON: f32 = 16.0;
159
160/// The container. Rows go in it; scrolling is the caller's, via
161/// [`crate::scroll`].
162pub fn tree() -> gpui::Div {
163    div().flex().flex_col().w_full()
164}
165
166/// One row: its guides, its chevron, and then whatever the caller puts in it.
167///
168/// `selected` is what the app considers chosen; `cursor` is where the keyboard
169/// is. Two tones, because a tree shows both at once.
170pub fn tree_row(theme: &Theme, row: &Row, selected: Selection, cursor: bool) -> gpui::Div {
171    let mut frame = div()
172        .flex()
173        .flex_row()
174        .items_center()
175        .mx(px(ROW_INSET))
176        .rounded(px(Theme::control_radius()))
177        .py(px(3.0))
178        .pr(px(8.0))
179        .text_style(TextStyle::Callout)
180        .cursor_pointer();
181    frame = if let Some(fill) = selected.fill(theme) {
182        frame
183            .bg(fill)
184            .shadow(selected.shadows())
185            .text_color(theme.text)
186    } else if cursor {
187        frame.bg(theme::wash(0.05)).text_color(theme.text)
188    } else {
189        frame.text_color(theme.text_muted)
190    };
191    frame
192        // One segment per ancestor level, drawn by the row it passes through:
193        // the line is continuous down the page without any element having to
194        // span rows or know its neighbours.
195        .children((0..row.depth).map(|_| {
196            div()
197                .flex_none()
198                .w(px(INDENT))
199                .h(px(18.0))
200                .border_l_1()
201                .border_color(theme.border)
202        }))
203        .child(
204            div()
205                .flex_none()
206                .w(px(CHEVRON))
207                .flex()
208                .items_center()
209                .justify_center()
210                .when_some(row.expanded, |slot, expanded| {
211                    slot.child(theme.disclosure(expanded))
212                }),
213        )
214}