1use std::collections::HashSet;
29
30use gpui::prelude::*;
31use gpui::{
32 div, px, ClickEvent, Context, EventEmitter, FocusHandle, IntoElement, KeyDownEvent,
33 MouseButton, SharedString, Window,
34};
35
36use crate::icon::{Icon, IconName};
37use crate::reactive::Signal;
38use crate::theme::{theme, Size};
39
40#[derive(Debug, Clone)]
43pub struct TreeNode {
44 pub id: SharedString,
46 pub label: SharedString,
48 pub icon: Option<IconName>,
51 pub children: Vec<TreeNode>,
53}
54
55impl TreeNode {
56 pub fn new(id: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
57 TreeNode {
58 id: id.into(),
59 label: label.into(),
60 icon: None,
61 children: Vec::new(),
62 }
63 }
64
65 pub fn icon(mut self, icon: IconName) -> Self {
67 self.icon = Some(icon);
68 self
69 }
70
71 pub fn child(mut self, child: TreeNode) -> Self {
73 self.children.push(child);
74 self
75 }
76
77 pub fn children(mut self, children: impl IntoIterator<Item = TreeNode>) -> Self {
79 self.children.extend(children);
80 self
81 }
82
83 pub fn is_leaf(&self) -> bool {
85 self.children.is_empty()
86 }
87}
88
89#[derive(Debug, Clone)]
91pub enum TreeViewEvent {
92 Selected(SharedString),
94 Toggled(SharedString, bool),
96 Activated(SharedString),
98}
99
100#[derive(Debug, Clone, PartialEq)]
102struct VisibleRow {
103 id: SharedString,
104 label: SharedString,
105 icon: Option<IconName>,
106 depth: usize,
107 is_branch: bool,
108 expanded: bool,
109}
110
111fn flatten_visible(
113 nodes: &[TreeNode],
114 expanded: &HashSet<SharedString>,
115 depth: usize,
116 out: &mut Vec<VisibleRow>,
117) {
118 for node in nodes {
119 let is_branch = !node.children.is_empty();
120 let is_expanded = is_branch && expanded.contains(&node.id);
121 out.push(VisibleRow {
122 id: node.id.clone(),
123 label: node.label.clone(),
124 icon: node.icon,
125 depth,
126 is_branch,
127 expanded: is_expanded,
128 });
129 if is_expanded {
130 flatten_visible(&node.children, expanded, depth + 1, out);
131 }
132 }
133}
134
135fn visible(nodes: &[TreeNode], expanded: &HashSet<SharedString>) -> Vec<VisibleRow> {
137 let mut out = Vec::new();
138 flatten_visible(nodes, expanded, 0, &mut out);
139 out
140}
141
142fn collect_branch_ids(nodes: &[TreeNode], out: &mut HashSet<SharedString>) {
144 for node in nodes {
145 if !node.children.is_empty() {
146 out.insert(node.id.clone());
147 collect_branch_ids(&node.children, out);
148 }
149 }
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154enum KeyMove {
155 To(usize),
157 Set(usize, bool),
159 None,
161}
162
163fn step_down(len: usize, current: Option<usize>) -> Option<usize> {
165 match (len, current) {
166 (0, _) => None,
167 (_, None) => Some(0),
168 (len, Some(i)) => Some((i + 1).min(len - 1)),
169 }
170}
171
172fn step_up(len: usize, current: Option<usize>) -> Option<usize> {
174 match (len, current) {
175 (0, _) => None,
176 (len, None) => Some(len - 1),
177 (_, Some(i)) => Some(i.saturating_sub(1)),
178 }
179}
180
181fn step_right(rows: &[VisibleRow], current: usize) -> KeyMove {
183 let Some(row) = rows.get(current) else {
184 return KeyMove::None;
185 };
186 if !row.is_branch {
187 return KeyMove::None;
188 }
189 if !row.expanded {
190 return KeyMove::Set(current, true);
191 }
192 match rows.get(current + 1) {
193 Some(next) if next.depth == row.depth + 1 => KeyMove::To(current + 1),
194 _ => KeyMove::None,
195 }
196}
197
198fn step_left(rows: &[VisibleRow], current: usize) -> KeyMove {
200 let Some(row) = rows.get(current) else {
201 return KeyMove::None;
202 };
203 if row.is_branch && row.expanded {
204 return KeyMove::Set(current, false);
205 }
206 if row.depth == 0 {
207 return KeyMove::None;
208 }
209 (0..current)
211 .rev()
212 .find(|&i| rows[i].depth + 1 == row.depth)
213 .map(KeyMove::To)
214 .unwrap_or(KeyMove::None)
215}
216
217pub struct TreeView {
219 nodes: Vec<TreeNode>,
220 expanded: HashSet<SharedString>,
221 selected: Option<SharedString>,
222 expand_all: bool,
223 focus: FocusHandle,
224}
225
226impl EventEmitter<TreeViewEvent> for TreeView {}
227
228impl TreeView {
229 pub fn new(cx: &mut Context<Self>) -> Self {
230 TreeView {
231 nodes: Vec::new(),
232 expanded: HashSet::new(),
233 selected: None,
234 expand_all: false,
235 focus: cx.focus_handle(),
236 }
237 }
238
239 pub fn nodes(mut self, nodes: Vec<TreeNode>) -> Self {
241 self.nodes = nodes;
242 if self.expand_all {
243 collect_branch_ids(&self.nodes, &mut self.expanded);
244 }
245 self
246 }
247
248 pub fn bind_nodes(mut self, signal: &Signal<Vec<TreeNode>>, cx: &mut Context<Self>) -> Self {
252 self.nodes = signal.get(cx);
253 if self.expand_all {
254 collect_branch_ids(&self.nodes, &mut self.expanded);
255 }
256 cx.observe(signal.entity(), |this, observed, cx| {
257 this.nodes = observed.read(cx).clone();
258 if this.expand_all {
261 collect_branch_ids(&this.nodes, &mut this.expanded);
262 }
263 cx.notify();
264 })
265 .detach();
266 self
267 }
268
269 pub fn expand(mut self, id: impl Into<SharedString>) -> Self {
271 self.expanded.insert(id.into());
272 self
273 }
274
275 pub fn collapse(mut self, id: impl Into<SharedString>) -> Self {
277 self.expanded.remove(&id.into());
278 self
279 }
280
281 pub fn default_expanded(mut self, expanded: bool) -> Self {
284 self.expand_all = expanded;
285 if expanded {
286 collect_branch_ids(&self.nodes, &mut self.expanded);
287 }
288 self
289 }
290
291 pub fn expanded_ids(&self) -> Vec<SharedString> {
293 let mut ids: Vec<SharedString> = self.expanded.iter().cloned().collect();
294 ids.sort();
295 ids
296 }
297
298 pub fn selected_id(&self) -> Option<SharedString> {
300 self.selected.clone()
301 }
302
303 fn select(&mut self, id: SharedString, cx: &mut Context<Self>) {
305 if self.selected.as_ref() == Some(&id) {
306 return;
307 }
308 self.selected = Some(id.clone());
309 cx.emit(TreeViewEvent::Selected(id));
310 cx.notify();
311 }
312
313 fn toggle(&mut self, id: SharedString, cx: &mut Context<Self>) {
315 let open = !self.expanded.contains(&id);
316 self.set_expanded(id, open, cx);
317 }
318
319 fn set_expanded(&mut self, id: SharedString, open: bool, cx: &mut Context<Self>) {
321 let changed = if open {
322 self.expanded.insert(id.clone())
323 } else {
324 self.expanded.remove(&id)
325 };
326 if changed {
327 cx.emit(TreeViewEvent::Toggled(id, open));
328 cx.notify();
329 }
330 }
331
332 fn apply(&mut self, mv: KeyMove, rows: &[VisibleRow], cx: &mut Context<Self>) {
334 match mv {
335 KeyMove::To(i) => self.select(rows[i].id.clone(), cx),
336 KeyMove::Set(i, open) => self.set_expanded(rows[i].id.clone(), open, cx),
337 KeyMove::None => {}
338 }
339 }
340
341 fn on_key(&mut self, event: &KeyDownEvent, _window: &mut Window, cx: &mut Context<Self>) {
342 let rows = visible(&self.nodes, &self.expanded);
343 if rows.is_empty() {
344 return;
345 }
346 let current = self
347 .selected
348 .as_ref()
349 .and_then(|id| rows.iter().position(|row| &row.id == id));
350
351 let handled = match event.keystroke.key.as_str() {
352 "down" => {
353 if let Some(i) = step_down(rows.len(), current) {
354 self.select(rows[i].id.clone(), cx);
355 }
356 true
357 }
358 "up" => {
359 if let Some(i) = step_up(rows.len(), current) {
360 self.select(rows[i].id.clone(), cx);
361 }
362 true
363 }
364 "right" => match current {
365 Some(i) => {
366 self.apply(step_right(&rows, i), &rows, cx);
367 true
368 }
369 None => false,
370 },
371 "left" => match current {
372 Some(i) => {
373 self.apply(step_left(&rows, i), &rows, cx);
374 true
375 }
376 None => false,
377 },
378 "enter" => match self.selected.clone() {
379 Some(id) => {
380 cx.emit(TreeViewEvent::Activated(id));
381 true
382 }
383 None => false,
384 },
385 _ => false,
386 };
387 if handled {
388 cx.stop_propagation();
389 }
390 }
391}
392
393impl Render for TreeView {
394 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
395 let t = theme(cx);
396 let text = t.text().hsla();
397 let dimmed = t.dimmed().hsla();
398 let accent = t.primary().hsla();
399 let surface_hover = t.surface_hover().hsla();
400 let selected_bg = t.primary().alpha(0.12);
401 let indent = t.spacing(Size::Md);
402 let radius = t.radius(Size::Sm);
403 let font = t.font_size(Size::Sm);
404
405 let rows = visible(&self.nodes, &self.expanded);
406 let selected = self.selected.clone();
407
408 let mut root = div()
409 .id("guise-treeview")
410 .track_focus(&self.focus)
411 .on_key_down(cx.listener(Self::on_key))
412 .on_mouse_down(
413 MouseButton::Left,
414 cx.listener(|this, _ev, window, cx| {
415 window.focus(&this.focus);
416 cx.notify();
417 }),
418 )
419 .flex()
420 .flex_col()
421 .gap(px(2.0));
422
423 for (i, row) in rows.into_iter().enumerate() {
424 let is_selected = selected.as_ref() == Some(&row.id);
425 let is_branch = row.is_branch;
426 let id = row.id.clone();
427 let hover_bg = if is_selected {
428 selected_bg
429 } else {
430 surface_hover
431 };
432
433 let mut chevron = div()
435 .w(px(16.0))
436 .flex()
437 .items_center()
438 .justify_center()
439 .text_color(dimmed);
440 if is_branch {
441 chevron = chevron.child(SharedString::new_static(if row.expanded {
442 IconName::ChevronDown.glyph()
443 } else {
444 IconName::ChevronRight.glyph()
445 }));
446 }
447
448 let fallback = if is_branch {
449 IconName::Menu
450 } else {
451 IconName::Dot
452 };
453 let glyph = row.icon.unwrap_or(fallback);
454 let icon = div()
455 .text_color(if is_selected { accent } else { dimmed })
456 .child(Icon::new(glyph).size(Size::Xs));
457
458 let mut el = div()
459 .id(("guise-tree-row", i))
460 .flex()
461 .items_center()
462 .gap(px(6.0))
463 .pl(px(6.0 + indent * row.depth as f32))
464 .pr(px(8.0))
465 .py(px(4.0))
466 .rounded(px(radius))
467 .text_size(px(font))
468 .text_color(text)
469 .hover(move |s| s.bg(hover_bg))
470 .child(chevron)
471 .child(icon)
472 .child(row.label.clone())
473 .on_click(cx.listener(move |this, ev: &ClickEvent, _window, cx| {
474 this.select(id.clone(), cx);
475 if ev.click_count() > 1 {
476 cx.emit(TreeViewEvent::Activated(id.clone()));
477 } else if is_branch {
478 this.toggle(id.clone(), cx);
479 }
480 }));
481 if is_selected {
482 el = el.bg(selected_bg);
483 }
484 root = root.child(el);
485 }
486
487 root
488 }
489}
490
491#[cfg(test)]
492mod tests {
493 use super::*;
494
495 fn sample() -> Vec<TreeNode> {
496 vec![
497 TreeNode::new("src", "src")
498 .child(TreeNode::new("main", "main.rs"))
499 .child(TreeNode::new("data", "data").child(TreeNode::new("tree", "tree.rs"))),
500 TreeNode::new("readme", "README.md"),
501 ]
502 }
503
504 fn expanded(ids: &[&'static str]) -> HashSet<SharedString> {
505 ids.iter().map(|id| SharedString::from(*id)).collect()
506 }
507
508 fn ids(rows: &[VisibleRow]) -> Vec<&str> {
509 rows.iter().map(|row| row.id.as_ref()).collect()
510 }
511
512 #[test]
513 fn collapsed_tree_shows_only_roots() {
514 let rows = visible(&sample(), &expanded(&[]));
515 assert_eq!(ids(&rows), ["src", "readme"]);
516 assert!(rows[0].is_branch && !rows[0].expanded);
517 assert!(!rows[1].is_branch);
518 }
519
520 #[test]
521 fn expanded_branches_flatten_depth_first() {
522 let rows = visible(&sample(), &expanded(&["src", "data"]));
523 assert_eq!(ids(&rows), ["src", "main", "data", "tree", "readme"]);
524 let depths: Vec<usize> = rows.iter().map(|row| row.depth).collect();
525 assert_eq!(depths, [0, 1, 1, 2, 0]);
526 assert!(rows[2].expanded);
527 }
528
529 #[test]
530 fn collapsed_parent_hides_expanded_descendants() {
531 let rows = visible(&sample(), &expanded(&["data"]));
533 assert_eq!(ids(&rows), ["src", "readme"]);
534 }
535
536 #[test]
537 fn up_and_down_clamp_at_the_edges() {
538 assert_eq!(step_down(3, None), Some(0));
539 assert_eq!(step_down(3, Some(1)), Some(2));
540 assert_eq!(step_down(3, Some(2)), Some(2));
541 assert_eq!(step_up(3, None), Some(2));
542 assert_eq!(step_up(3, Some(1)), Some(0));
543 assert_eq!(step_up(3, Some(0)), Some(0));
544 assert_eq!(step_down(0, None), None);
545 assert_eq!(step_up(0, Some(1)), None);
546 }
547
548 #[test]
549 fn right_expands_then_steps_into_the_branch() {
550 let closed = visible(&sample(), &expanded(&[]));
551 assert_eq!(step_right(&closed, 0), KeyMove::Set(0, true));
552
553 let open = visible(&sample(), &expanded(&["src"]));
554 assert_eq!(step_right(&open, 0), KeyMove::To(1));
555 assert_eq!(step_right(&open, 1), KeyMove::None);
557 }
558
559 #[test]
560 fn left_collapses_then_walks_to_the_parent() {
561 let rows = visible(&sample(), &expanded(&["src", "data"]));
562 assert_eq!(step_left(&rows, 0), KeyMove::Set(0, false));
564 assert_eq!(step_left(&rows, 1), KeyMove::To(0));
566 assert_eq!(step_left(&rows, 3), KeyMove::To(2));
567 assert_eq!(step_left(&rows, 4), KeyMove::None);
569 }
570
571 #[test]
572 fn branch_ids_cover_nested_branches_only() {
573 let mut out = HashSet::new();
574 collect_branch_ids(&sample(), &mut out);
575 assert_eq!(out, expanded(&["src", "data"]));
576 }
577}