1use std::collections::HashSet;
29use std::ops::Range;
30
31use gpui::prelude::*;
32use gpui::{
33 div, px, uniform_list, AnyElement, ClickEvent, Context, EventEmitter, FocusHandle, IntoElement,
34 KeyDownEvent, MouseButton, MouseDownEvent, Pixels, Point, ScrollStrategy, SharedString,
35 UniformListScrollHandle, Window,
36};
37
38use crate::devtools::Probed;
39use crate::icon::{Glyph, Icon, IconName};
40use crate::reactive::Signal;
41use crate::theme::{theme, Size};
42
43#[derive(Debug, Clone)]
46pub struct TreeNode {
47 pub id: SharedString,
49 pub label: SharedString,
51 pub icon: Option<IconName>,
54 pub children: Vec<TreeNode>,
56}
57
58impl TreeNode {
59 pub fn new(id: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
60 TreeNode {
61 id: id.into(),
62 label: label.into(),
63 icon: None,
64 children: Vec::new(),
65 }
66 }
67
68 pub fn icon(mut self, icon: IconName) -> Self {
70 self.icon = Some(icon);
71 self
72 }
73
74 pub fn child(mut self, child: TreeNode) -> Self {
76 self.children.push(child);
77 self
78 }
79
80 pub fn children(mut self, children: impl IntoIterator<Item = TreeNode>) -> Self {
82 self.children.extend(children);
83 self
84 }
85
86 pub fn is_leaf(&self) -> bool {
88 self.children.is_empty()
89 }
90}
91
92#[derive(Debug, Clone)]
94pub enum TreeViewEvent {
95 Selected(SharedString),
97 Toggled(SharedString, bool),
99 Activated(SharedString),
101 ContextMenu(SharedString, Point<Pixels>),
107}
108
109#[derive(Debug, Clone, PartialEq)]
111struct VisibleRow {
112 id: SharedString,
113 label: SharedString,
114 icon: Option<IconName>,
115 depth: usize,
116 is_branch: bool,
117 expanded: bool,
118}
119
120fn flatten_visible(
122 nodes: &[TreeNode],
123 expanded: &HashSet<SharedString>,
124 depth: usize,
125 out: &mut Vec<VisibleRow>,
126) {
127 for node in nodes {
128 let is_branch = !node.children.is_empty();
129 let is_expanded = is_branch && expanded.contains(&node.id);
130 out.push(VisibleRow {
131 id: node.id.clone(),
132 label: node.label.clone(),
133 icon: node.icon,
134 depth,
135 is_branch,
136 expanded: is_expanded,
137 });
138 if is_expanded {
139 flatten_visible(&node.children, expanded, depth + 1, out);
140 }
141 }
142}
143
144fn visible(nodes: &[TreeNode], expanded: &HashSet<SharedString>) -> Vec<VisibleRow> {
146 let mut out = Vec::new();
147 flatten_visible(nodes, expanded, 0, &mut out);
148 out
149}
150
151fn collect_branch_ids(nodes: &[TreeNode], out: &mut HashSet<SharedString>) {
153 for node in nodes {
154 if !node.children.is_empty() {
155 out.insert(node.id.clone());
156 collect_branch_ids(&node.children, out);
157 }
158 }
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163enum KeyMove {
164 To(usize),
166 Set(usize, bool),
168 None,
170}
171
172fn step_down(len: usize, current: Option<usize>) -> Option<usize> {
174 match (len, current) {
175 (0, _) => None,
176 (_, None) => Some(0),
177 (len, Some(i)) => Some((i + 1).min(len - 1)),
178 }
179}
180
181fn step_up(len: usize, current: Option<usize>) -> Option<usize> {
183 match (len, current) {
184 (0, _) => None,
185 (len, None) => Some(len - 1),
186 (_, Some(i)) => Some(i.saturating_sub(1)),
187 }
188}
189
190fn step_right(rows: &[VisibleRow], current: usize) -> KeyMove {
192 let Some(row) = rows.get(current) else {
193 return KeyMove::None;
194 };
195 if !row.is_branch {
196 return KeyMove::None;
197 }
198 if !row.expanded {
199 return KeyMove::Set(current, true);
200 }
201 match rows.get(current + 1) {
202 Some(next) if next.depth == row.depth + 1 => KeyMove::To(current + 1),
203 _ => KeyMove::None,
204 }
205}
206
207fn step_left(rows: &[VisibleRow], current: usize) -> KeyMove {
209 let Some(row) = rows.get(current) else {
210 return KeyMove::None;
211 };
212 if row.is_branch && row.expanded {
213 return KeyMove::Set(current, false);
214 }
215 if row.depth == 0 {
216 return KeyMove::None;
217 }
218 (0..current)
220 .rev()
221 .find(|&i| rows[i].depth + 1 == row.depth)
222 .map(KeyMove::To)
223 .unwrap_or(KeyMove::None)
224}
225
226pub struct TreeView {
228 nodes: Vec<TreeNode>,
229 expanded: HashSet<SharedString>,
230 selected: Option<SharedString>,
231 expand_all: bool,
232 focus: FocusHandle,
233 height: Option<f32>,
234 scroll: UniformListScrollHandle,
235}
236
237impl EventEmitter<TreeViewEvent> for TreeView {}
238
239impl TreeView {
240 pub fn new(cx: &mut Context<Self>) -> Self {
241 TreeView {
242 nodes: Vec::new(),
243 expanded: HashSet::new(),
244 selected: None,
245 expand_all: false,
246 focus: cx.focus_handle(),
247 height: None,
248 scroll: UniformListScrollHandle::new(),
249 }
250 }
251
252 pub fn height(mut self, height: f32) -> Self {
256 self.height = Some(height.max(0.0));
257 self
258 }
259
260 pub fn nodes(mut self, nodes: Vec<TreeNode>) -> Self {
262 self.nodes = nodes;
263 if self.expand_all {
264 collect_branch_ids(&self.nodes, &mut self.expanded);
265 }
266 self
267 }
268
269 pub fn bind_nodes(mut self, signal: &Signal<Vec<TreeNode>>, cx: &mut Context<Self>) -> Self {
273 self.nodes = signal.get(cx);
274 if self.expand_all {
275 collect_branch_ids(&self.nodes, &mut self.expanded);
276 }
277 cx.observe(signal.entity(), |this, observed, cx| {
278 this.nodes = observed.read(cx).clone();
279 if this.expand_all {
282 collect_branch_ids(&this.nodes, &mut this.expanded);
283 }
284 cx.notify();
285 })
286 .detach();
287 self
288 }
289
290 pub fn expand(mut self, id: impl Into<SharedString>) -> Self {
292 self.expanded.insert(id.into());
293 self
294 }
295
296 pub fn collapse(mut self, id: impl Into<SharedString>) -> Self {
298 self.expanded.remove(&id.into());
299 self
300 }
301
302 pub fn default_expanded(mut self, expanded: bool) -> Self {
305 self.expand_all = expanded;
306 if expanded {
307 collect_branch_ids(&self.nodes, &mut self.expanded);
308 }
309 self
310 }
311
312 pub fn expanded_ids(&self) -> Vec<SharedString> {
314 let mut ids: Vec<SharedString> = self.expanded.iter().cloned().collect();
315 ids.sort();
316 ids
317 }
318
319 pub fn selected_id(&self) -> Option<SharedString> {
321 self.selected.clone()
322 }
323
324 fn select(&mut self, id: SharedString, cx: &mut Context<Self>) {
326 if self.selected.as_ref() == Some(&id) {
327 return;
328 }
329 self.selected = Some(id.clone());
330 cx.emit(TreeViewEvent::Selected(id));
331 cx.notify();
332 }
333
334 fn toggle(&mut self, id: SharedString, cx: &mut Context<Self>) {
336 let open = !self.expanded.contains(&id);
337 self.set_expanded(id, open, cx);
338 }
339
340 fn set_expanded(&mut self, id: SharedString, open: bool, cx: &mut Context<Self>) {
342 let changed = if open {
343 self.expanded.insert(id.clone())
344 } else {
345 self.expanded.remove(&id)
346 };
347 if changed {
348 cx.emit(TreeViewEvent::Toggled(id, open));
349 cx.notify();
350 }
351 }
352
353 fn apply(&mut self, mv: KeyMove, rows: &[VisibleRow], cx: &mut Context<Self>) {
355 match mv {
356 KeyMove::To(i) => {
357 self.select(rows[i].id.clone(), cx);
358 self.reveal(i);
359 }
360 KeyMove::Set(i, open) => self.set_expanded(rows[i].id.clone(), open, cx),
361 KeyMove::None => {}
362 }
363 }
364
365 fn reveal(&mut self, i: usize) {
367 if self.height.is_some() {
368 self.scroll.scroll_to_item(i, ScrollStrategy::Top);
369 }
370 }
371
372 fn on_key(&mut self, event: &KeyDownEvent, _window: &mut Window, cx: &mut Context<Self>) {
373 let rows = visible(&self.nodes, &self.expanded);
374 if rows.is_empty() {
375 return;
376 }
377 let current = self
378 .selected
379 .as_ref()
380 .and_then(|id| rows.iter().position(|row| &row.id == id));
381
382 let handled = match event.keystroke.key.as_str() {
383 "down" => {
384 if let Some(i) = step_down(rows.len(), current) {
385 self.select(rows[i].id.clone(), cx);
386 self.reveal(i);
387 }
388 true
389 }
390 "up" => {
391 if let Some(i) = step_up(rows.len(), current) {
392 self.select(rows[i].id.clone(), cx);
393 self.reveal(i);
394 }
395 true
396 }
397 "right" => match current {
398 Some(i) => {
399 self.apply(step_right(&rows, i), &rows, cx);
400 true
401 }
402 None => false,
403 },
404 "left" => match current {
405 Some(i) => {
406 self.apply(step_left(&rows, i), &rows, cx);
407 true
408 }
409 None => false,
410 },
411 "enter" => match self.selected.clone() {
412 Some(id) => {
413 cx.emit(TreeViewEvent::Activated(id));
414 true
415 }
416 None => false,
417 },
418 _ => false,
419 };
420 if handled {
421 cx.stop_propagation();
422 }
423 }
424}
425
426impl TreeView {
427 fn render_rows(&mut self, range: Range<usize>, cx: &mut Context<Self>) -> Vec<AnyElement> {
430 let t = theme(cx);
431 let text = t.text().hsla();
432 let dimmed = t.dimmed().hsla();
433 let accent = t.primary().hsla();
434 let surface_hover = t.surface_hover().hsla();
435 let selected_bg = t.primary().alpha(0.12);
436 let indent = t.spacing(Size::Md);
437 let radius = t.radius(Size::Sm);
438 let font = t.font_size(Size::Sm);
439
440 let rows = visible(&self.nodes, &self.expanded);
441 let selected = self.selected.clone();
442
443 let mut out = Vec::with_capacity(range.len());
444 for i in range {
445 let Some(row) = rows.get(i) else { break };
446 let is_selected = selected.as_ref() == Some(&row.id);
447 let is_branch = row.is_branch;
448 let id = row.id.clone();
449 let menu_id = row.id.clone();
450 let hover_bg = if is_selected {
451 selected_bg
452 } else {
453 surface_hover
454 };
455
456 let mut chevron = div()
458 .w(px(16.0))
459 .flex()
460 .items_center()
461 .justify_center()
462 .text_color(dimmed);
463 if is_branch {
464 chevron = chevron.child(Glyph::from(if row.expanded {
465 IconName::ChevronDown
466 } else {
467 IconName::ChevronRight
468 }));
469 }
470
471 let fallback = if is_branch {
472 IconName::Menu
473 } else {
474 IconName::Dot
475 };
476 let glyph = row.icon.unwrap_or(fallback);
477 let icon = div()
478 .text_color(if is_selected { accent } else { dimmed })
479 .child(Icon::new(glyph).size(Size::Xs));
480
481 let mut el = div()
482 .id(("guise-tree-row", i))
483 .flex()
484 .items_center()
485 .gap(px(6.0))
486 .pl(px(6.0 + indent * row.depth as f32))
487 .pr(px(8.0))
488 .py(px(4.0))
489 .rounded(px(radius))
490 .text_size(px(font))
491 .text_color(text)
492 .hover(move |s| s.bg(hover_bg))
493 .child(chevron)
494 .child(icon)
495 .child(row.label.clone())
496 .on_click(cx.listener(move |this, ev: &ClickEvent, _window, cx| {
497 this.select(id.clone(), cx);
498 if ev.click_count() > 1 {
499 cx.emit(TreeViewEvent::Activated(id.clone()));
500 } else if is_branch {
501 this.toggle(id.clone(), cx);
502 }
503 }))
504 .on_mouse_down(
505 MouseButton::Right,
506 cx.listener(move |this, ev: &MouseDownEvent, _window, cx| {
507 this.select(menu_id.clone(), cx);
511 cx.emit(TreeViewEvent::ContextMenu(menu_id.clone(), ev.position));
512 cx.stop_propagation();
513 }),
514 );
515 if is_selected {
516 el = el.bg(selected_bg);
517 }
518 out.push(el.into_any_element());
519 }
520 out
521 }
522}
523
524impl Render for TreeView {
525 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
526 let count = visible(&self.nodes, &self.expanded).len();
527
528 let root = div()
529 .id("guise-treeview")
530 .track_focus(&self.focus)
531 .on_key_down(cx.listener(Self::on_key))
532 .on_mouse_down(
533 MouseButton::Left,
534 cx.listener(|this, _ev, window, cx| {
535 window.focus(&this.focus);
536 cx.notify();
537 }),
538 )
539 .flex()
540 .flex_col();
541
542 let element = if let Some(height) = self.height {
543 root.child(
544 uniform_list(
545 "guise-treeview-rows",
546 count,
547 cx.processor(|this, range: Range<usize>, _window, cx| this.render_rows(range, cx)),
548 )
549 .h(px(height))
550 .w_full()
551 .track_scroll(self.scroll.clone()),
552 )
553 } else {
554 root.gap(px(2.0)).children(self.render_rows(0..count, cx))
555 };
556
557 element.probe("TreeView")
558 }
559}
560
561#[cfg(test)]
562mod tests {
563 use super::*;
564
565 fn sample() -> Vec<TreeNode> {
566 vec![
567 TreeNode::new("src", "src")
568 .child(TreeNode::new("main", "main.rs"))
569 .child(TreeNode::new("data", "data").child(TreeNode::new("tree", "tree.rs"))),
570 TreeNode::new("readme", "README.md"),
571 ]
572 }
573
574 fn expanded(ids: &[&'static str]) -> HashSet<SharedString> {
575 ids.iter().map(|id| SharedString::from(*id)).collect()
576 }
577
578 fn ids(rows: &[VisibleRow]) -> Vec<&str> {
579 rows.iter().map(|row| row.id.as_ref()).collect()
580 }
581
582 #[test]
583 fn collapsed_tree_shows_only_roots() {
584 let rows = visible(&sample(), &expanded(&[]));
585 assert_eq!(ids(&rows), ["src", "readme"]);
586 assert!(rows[0].is_branch && !rows[0].expanded);
587 assert!(!rows[1].is_branch);
588 }
589
590 #[test]
591 fn expanded_branches_flatten_depth_first() {
592 let rows = visible(&sample(), &expanded(&["src", "data"]));
593 assert_eq!(ids(&rows), ["src", "main", "data", "tree", "readme"]);
594 let depths: Vec<usize> = rows.iter().map(|row| row.depth).collect();
595 assert_eq!(depths, [0, 1, 1, 2, 0]);
596 assert!(rows[2].expanded);
597 }
598
599 #[test]
600 fn collapsed_parent_hides_expanded_descendants() {
601 let rows = visible(&sample(), &expanded(&["data"]));
603 assert_eq!(ids(&rows), ["src", "readme"]);
604 }
605
606 #[test]
607 fn up_and_down_clamp_at_the_edges() {
608 assert_eq!(step_down(3, None), Some(0));
609 assert_eq!(step_down(3, Some(1)), Some(2));
610 assert_eq!(step_down(3, Some(2)), Some(2));
611 assert_eq!(step_up(3, None), Some(2));
612 assert_eq!(step_up(3, Some(1)), Some(0));
613 assert_eq!(step_up(3, Some(0)), Some(0));
614 assert_eq!(step_down(0, None), None);
615 assert_eq!(step_up(0, Some(1)), None);
616 }
617
618 #[test]
619 fn right_expands_then_steps_into_the_branch() {
620 let closed = visible(&sample(), &expanded(&[]));
621 assert_eq!(step_right(&closed, 0), KeyMove::Set(0, true));
622
623 let open = visible(&sample(), &expanded(&["src"]));
624 assert_eq!(step_right(&open, 0), KeyMove::To(1));
625 assert_eq!(step_right(&open, 1), KeyMove::None);
627 }
628
629 #[test]
630 fn left_collapses_then_walks_to_the_parent() {
631 let rows = visible(&sample(), &expanded(&["src", "data"]));
632 assert_eq!(step_left(&rows, 0), KeyMove::Set(0, false));
634 assert_eq!(step_left(&rows, 1), KeyMove::To(0));
636 assert_eq!(step_left(&rows, 3), KeyMove::To(2));
637 assert_eq!(step_left(&rows, 4), KeyMove::None);
639 }
640
641 #[test]
642 fn branch_ids_cover_nested_branches_only() {
643 let mut out = HashSet::new();
644 collect_branch_ids(&sample(), &mut out);
645 assert_eq!(out, expanded(&["src", "data"]));
646 }
647}