1use std::{cell::RefCell, ops::Range, rc::Rc};
2
3use gpui::{
4 AnyElement, App, Context, ElementId, Entity, EventEmitter, FocusHandle, InteractiveElement,
5 IntoElement, KeyBinding, MouseButton, ParentElement, Render, RenderOnce, SharedString,
6 StyleRefinement, Styled, UniformListScrollHandle, Window, div, prelude::FluentBuilder as _,
7 uniform_list,
8};
9
10use crate::{
11 actions::{Confirm, SelectDown, SelectLeft, SelectRight, SelectUp},
12 styled::StyledExt as _,
13};
14
15const CONTEXT: &str = "Tree";
16
17#[doc(hidden)]
18pub fn init(cx: &mut App) {
19 cx.bind_keys([
20 KeyBinding::new("up", SelectUp, Some(CONTEXT)),
21 KeyBinding::new("down", SelectDown, Some(CONTEXT)),
22 KeyBinding::new("left", SelectLeft, Some(CONTEXT)),
23 KeyBinding::new("right", SelectRight, Some(CONTEXT)),
24 ]);
25}
26
27#[doc(hidden)]
28pub const fn key_context() -> &'static str {
29 CONTEXT
30}
31
32struct TreeItemState {
33 expanded: bool,
34 disabled: bool,
35}
36
37#[derive(Clone)]
39pub struct TreeItem {
40 pub id: SharedString,
41 pub label: SharedString,
42 pub children: Vec<TreeItem>,
43 state: Rc<RefCell<TreeItemState>>,
44}
45
46#[derive(Clone)]
48pub struct TreeEntry {
49 item: TreeItem,
50 depth: usize,
51}
52
53impl TreeEntry {
54 pub fn new(item: TreeItem, depth: usize) -> Self {
55 Self { item, depth }
56 }
57
58 #[inline]
59 pub fn item(&self) -> &TreeItem {
60 &self.item
61 }
62
63 #[inline]
64 pub fn depth(&self) -> usize {
65 self.depth
66 }
67
68 #[inline]
69 pub fn is_root(&self) -> bool {
70 self.depth == 0
71 }
72
73 #[inline]
74 pub fn is_folder(&self) -> bool {
75 self.item.is_folder()
76 }
77
78 #[inline]
79 pub fn is_expanded(&self) -> bool {
80 self.item.is_expanded()
81 }
82
83 #[inline]
84 pub fn is_disabled(&self) -> bool {
85 self.item.is_disabled()
86 }
87}
88
89#[derive(Clone, Debug, PartialEq, Eq)]
91pub enum TreeEvent {
92 Expanded(SharedString),
93 Collapsed(SharedString),
94}
95
96impl TreeItem {
97 pub fn new(id: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
98 Self {
99 id: id.into(),
100 label: label.into(),
101 children: Vec::new(),
102 state: Rc::new(RefCell::new(TreeItemState {
103 expanded: false,
104 disabled: false,
105 })),
106 }
107 }
108
109 pub fn child(mut self, child: TreeItem) -> Self {
110 self.children.push(child);
111 self
112 }
113
114 pub fn children(mut self, children: impl IntoIterator<Item = TreeItem>) -> Self {
115 self.children.extend(children);
116 self
117 }
118
119 pub fn expanded(self, expanded: bool) -> Self {
120 self.state.borrow_mut().expanded = expanded;
121 self
122 }
123
124 pub fn disabled(self, disabled: bool) -> Self {
125 self.state.borrow_mut().disabled = disabled;
126 self
127 }
128
129 #[inline]
130 pub fn is_folder(&self) -> bool {
131 !self.children.is_empty()
132 }
133
134 pub fn is_disabled(&self) -> bool {
135 self.state.borrow().disabled
136 }
137
138 #[inline]
139 pub fn is_expanded(&self) -> bool {
140 self.state.borrow().expanded
141 }
142
143 pub fn ancestors(&self, target_id: &SharedString) -> Option<Vec<TreeItem>> {
145 if self.id == *target_id {
146 return Some(Vec::new());
147 }
148
149 for child in &self.children {
150 if let Some(mut path) = child.ancestors(target_id) {
151 path.push(self.clone());
152 return Some(path);
153 }
154 }
155
156 None
157 }
158}
159
160#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
162pub struct TreeEntryState {
163 selected: bool,
164 right_clicked: bool,
165}
166
167impl TreeEntryState {
168 #[inline]
169 pub fn is_selected(self) -> bool {
170 self.selected
171 }
172
173 #[inline]
174 pub fn is_right_clicked(self) -> bool {
175 self.right_clicked
176 }
177}
178
179type RenderItem = dyn Fn(usize, &TreeEntry, TreeEntryState, &mut Window, &mut App) -> AnyElement;
180
181pub struct TreeState {
183 focus_handle: FocusHandle,
184 entries: Vec<TreeEntry>,
185 scroll_handle: UniformListScrollHandle,
186 selected_ix: Option<usize>,
187 right_clicked_ix: Option<usize>,
188 render_item: Rc<RenderItem>,
189 list_style: StyleRefinement,
190}
191
192impl EventEmitter<TreeEvent> for TreeState {}
193
194impl TreeState {
195 pub fn new(cx: &mut App) -> Self {
196 Self {
197 focus_handle: cx.focus_handle(),
198 entries: Vec::new(),
199 scroll_handle: UniformListScrollHandle::default(),
200 selected_ix: None,
201 right_clicked_ix: None,
202 render_item: Rc::new(|_, _, _, _, _| div().into_any_element()),
203 list_style: StyleRefinement::default(),
204 }
205 }
206
207 pub fn items(mut self, items: impl Into<Vec<TreeItem>>) -> Self {
208 self.replace_items(items.into());
209 self
210 }
211
212 pub fn set_items(&mut self, items: impl Into<Vec<TreeItem>>, cx: &mut Context<Self>) {
213 self.replace_items(items.into());
214 self.selected_ix = None;
215 self.right_clicked_ix = None;
216 cx.notify();
217 }
218
219 pub fn selected_index(&self) -> Option<usize> {
220 self.selected_ix
221 }
222
223 pub fn set_selected_index(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
224 self.selected_ix = ix;
225 cx.notify();
226 }
227
228 pub fn set_selected_item(&mut self, item: Option<&TreeItem>, cx: &mut Context<Self>) {
229 if let Some(item) = item {
230 self.selected_ix = self.index_of(&item.id);
231 if self.selected_ix.is_none() {
232 self.expand_ancestors(item.id.clone(), cx);
233 self.selected_ix = self.index_of(&item.id);
234 }
235 } else {
236 self.selected_ix = None;
237 }
238 cx.notify();
239 }
240
241 pub fn selected_item(&self) -> Option<&TreeItem> {
242 self.selected_ix
243 .and_then(|ix| self.entries.get(ix).map(TreeEntry::item))
244 }
245
246 pub fn selected_entry(&self) -> Option<&TreeEntry> {
247 self.selected_ix.and_then(|ix| self.entries.get(ix))
248 }
249
250 pub fn entry(&self, ix: usize) -> Option<&TreeEntry> {
251 self.entries.get(ix)
252 }
253
254 pub fn scroll_handle(&self) -> &UniformListScrollHandle {
255 &self.scroll_handle
256 }
257
258 pub fn scroll_to_item(&mut self, ix: usize, strategy: gpui::ScrollStrategy) {
259 self.scroll_handle.scroll_to_item(ix, strategy);
260 }
261
262 pub fn index_of(&self, id: &SharedString) -> Option<usize> {
263 self.entries.iter().position(|entry| &entry.item.id == id)
264 }
265
266 pub fn reveal_item(
267 &mut self,
268 id: &SharedString,
269 strategy: gpui::ScrollStrategy,
270 cx: &mut Context<Self>,
271 ) {
272 self.expand_ancestors(id.clone(), cx);
273 if let Some(ix) = self.index_of(id) {
274 self.scroll_to_item(ix, strategy);
275 }
276 }
277
278 pub fn focus(&mut self, window: &mut Window, cx: &mut App) {
279 self.focus_handle.focus(window, cx);
280 }
281
282 fn replace_items(&mut self, items: Vec<TreeItem>) {
283 self.entries.clear();
284 for item in items {
285 self.add_entry(item, 0);
286 }
287 }
288
289 fn expand_ancestors(&mut self, target_id: SharedString, cx: &mut Context<Self>) {
290 let ancestors = self
291 .entries
292 .iter()
293 .find_map(|entry| entry.item.ancestors(&target_id))
294 .unwrap_or_default();
295
296 if ancestors.is_empty() {
297 return;
298 }
299
300 for ancestor in ancestors.into_iter().rev() {
301 if !ancestor.is_expanded() {
302 ancestor.state.borrow_mut().expanded = true;
303 cx.emit(TreeEvent::Expanded(ancestor.id.clone()));
304 }
305 }
306 self.rebuild_entries();
307 }
308
309 fn add_entry(&mut self, item: TreeItem, depth: usize) {
310 self.entries.push(TreeEntry::new(item.clone(), depth));
311 if item.is_expanded() {
312 for child in &item.children {
313 self.add_entry(child.clone(), depth + 1);
314 }
315 }
316 }
317
318 fn toggle_expand(&mut self, ix: usize, cx: &mut Context<Self>) {
319 let Some(entry) = self.entries.get(ix) else {
320 return;
321 };
322 if !entry.is_folder() {
323 return;
324 }
325
326 let expanded = !entry.is_expanded();
327 let id = entry.item.id.clone();
328 entry.item.state.borrow_mut().expanded = expanded;
329 cx.emit(if expanded {
330 TreeEvent::Expanded(id)
331 } else {
332 TreeEvent::Collapsed(id)
333 });
334 self.right_clicked_ix = None;
335 self.rebuild_entries();
336 }
337
338 fn rebuild_entries(&mut self) {
339 let roots = self
340 .entries
341 .iter()
342 .filter(|entry| entry.is_root())
343 .map(|entry| entry.item.clone())
344 .collect::<Vec<_>>();
345 self.replace_items(roots);
346 }
347
348 fn on_action_confirm(&mut self, _: &Confirm, _: &mut Window, cx: &mut Context<Self>) {
349 if self
350 .selected_ix
351 .and_then(|ix| self.entries.get(ix).map(|entry| (ix, entry.is_folder())))
352 .is_some_and(|(ix, is_folder)| {
353 if is_folder {
354 self.toggle_expand(ix, cx);
355 }
356 is_folder
357 })
358 {
359 cx.notify();
360 }
361 }
362
363 fn on_action_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut Context<Self>) {
364 if let Some(ix) = self.selected_ix
365 && self
366 .entries
367 .get(ix)
368 .is_some_and(|entry| entry.is_folder() && entry.is_expanded())
369 {
370 self.toggle_expand(ix, cx);
371 cx.notify();
372 }
373 }
374
375 fn on_action_right(&mut self, _: &SelectRight, _: &mut Window, cx: &mut Context<Self>) {
376 if let Some(ix) = self.selected_ix
377 && self
378 .entries
379 .get(ix)
380 .is_some_and(|entry| entry.is_folder() && !entry.is_expanded())
381 {
382 self.toggle_expand(ix, cx);
383 cx.notify();
384 }
385 }
386
387 fn on_action_up(&mut self, _: &SelectUp, _: &mut Window, cx: &mut Context<Self>) {
388 let mut ix = self.selected_ix.unwrap_or(0);
389 ix = ix
390 .checked_sub(1)
391 .unwrap_or_else(|| self.entries.len().saturating_sub(1));
392 self.selected_ix = Some(ix);
393 self.scroll_handle
394 .scroll_to_item(ix, gpui::ScrollStrategy::Top);
395 cx.notify();
396 }
397
398 fn on_action_down(&mut self, _: &SelectDown, _: &mut Window, cx: &mut Context<Self>) {
399 let mut ix = self.selected_ix.unwrap_or(0);
400 ix = if ix + 1 < self.entries.len() {
401 ix + 1
402 } else {
403 0
404 };
405 self.selected_ix = Some(ix);
406 self.scroll_handle
407 .scroll_to_item(ix, gpui::ScrollStrategy::Bottom);
408 cx.notify();
409 }
410
411 fn on_entry_click(&mut self, ix: usize, cx: &mut Context<Self>) {
412 self.selected_ix = Some(ix);
413 self.toggle_expand(ix, cx);
414 cx.notify();
415 }
416}
417
418impl Render for TreeState {
419 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
420 let render_item = self.render_item.clone();
421 uniform_list("entries", self.entries.len(), {
422 cx.processor(move |state, visible_range: Range<usize>, window, cx| {
423 visible_range
424 .map(|ix| {
425 let entry = &state.entries[ix];
426 let entry_state = TreeEntryState {
427 selected: Some(ix) == state.selected_ix,
428 right_clicked: Some(ix) == state.right_clicked_ix,
429 };
430 div()
431 .id(ix)
432 .child((render_item)(ix, entry, entry_state, window, cx))
433 .when(!entry.is_disabled(), |this| {
434 this.on_mouse_down(
435 MouseButton::Left,
436 cx.listener(move |state, _, _, cx| {
437 state.on_entry_click(ix, cx);
438 }),
439 )
440 .on_mouse_down(
441 MouseButton::Right,
442 cx.listener(move |state, _, _, cx| {
443 state.right_clicked_ix = Some(ix);
444 cx.notify();
445 }),
446 )
447 })
448 })
449 .collect()
450 })
451 })
452 .track_scroll(&self.scroll_handle)
453 .refine_style(&self.list_style)
454 }
455}
456
457#[derive(IntoElement)]
459pub struct Tree {
460 id: ElementId,
461 state: Entity<TreeState>,
462 style: StyleRefinement,
463 list_style: StyleRefinement,
464 render_item: Rc<RenderItem>,
465}
466
467impl Tree {
468 pub fn new(state: &Entity<TreeState>) -> Self {
469 Self {
470 id: ElementId::Name(format!("tree-{}", state.entity_id()).into()),
471 state: state.clone(),
472 style: StyleRefinement::default(),
473 list_style: StyleRefinement::default(),
474 render_item: Rc::new(|_, _, _, _, _| div().into_any_element()),
475 }
476 }
477
478 pub fn item<R>(mut self, render_item: R) -> Self
480 where
481 R: Fn(usize, &TreeEntry, TreeEntryState, &mut Window, &mut App) -> AnyElement + 'static,
482 {
483 self.render_item = Rc::new(render_item);
484 self
485 }
486
487 pub fn list_style(mut self, style: StyleRefinement) -> Self {
489 self.list_style = style;
490 self
491 }
492}
493
494impl Styled for Tree {
495 fn style(&mut self) -> &mut StyleRefinement {
496 &mut self.style
497 }
498}
499
500impl RenderOnce for Tree {
501 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
502 let focus_handle = self.state.read(cx).focus_handle.clone();
503 self.state.update(cx, |state, _| {
504 state.render_item = self.render_item;
505 state.list_style = self.list_style;
506 });
507
508 div()
509 .id(self.id)
510 .key_context(CONTEXT)
511 .track_focus(&focus_handle)
512 .on_action(window.listener_for(&self.state, TreeState::on_action_confirm))
513 .on_action(window.listener_for(&self.state, TreeState::on_action_left))
514 .on_action(window.listener_for(&self.state, TreeState::on_action_right))
515 .on_action(window.listener_for(&self.state, TreeState::on_action_up))
516 .on_action(window.listener_for(&self.state, TreeState::on_action_down))
517 .child(self.state)
518 .refine_style(&self.style)
519 }
520}
521
522#[cfg(test)]
523mod tests {
524 use super::*;
525 use gpui::{AppContext as _, Subscription};
526
527 struct EventCollector {
528 events: Rc<RefCell<Vec<TreeEvent>>>,
529 _subscription: Subscription,
530 }
531
532 impl EventCollector {
533 fn new(state: &Entity<TreeState>, cx: &mut Context<Self>) -> Self {
534 let events = Rc::new(RefCell::new(Vec::new()));
535 let captured = events.clone();
536 let subscription = cx.subscribe(state, move |_, _, event, _| {
537 captured.borrow_mut().push(event.clone());
538 });
539 Self {
540 events,
541 _subscription: subscription,
542 }
543 }
544 }
545
546 impl Render for EventCollector {
547 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
548 div()
549 }
550 }
551
552 #[test]
553 fn clones_share_state_and_ancestors_keep_nearest_first_order() {
554 let leaf = TreeItem::new("leaf", "Leaf");
555 let branch = TreeItem::new("branch", "Branch").child(leaf.clone());
556 let root = TreeItem::new("root", "Root").child(branch.clone());
557
558 leaf.clone().disabled(true).expanded(true);
559 assert!(leaf.is_disabled());
560 assert!(leaf.is_expanded());
561
562 let ancestors = root.ancestors(&"leaf".into()).unwrap();
563 assert_eq!(
564 ancestors
565 .iter()
566 .map(|item| item.id.as_str())
567 .collect::<Vec<_>>(),
568 vec!["branch", "root"]
569 );
570 }
571
572 #[gpui::test]
573 fn state_flattens_expanded_items_and_resets_selection(cx: &mut gpui::TestAppContext) {
574 let items = vec![
575 TreeItem::new("src", "src")
576 .expanded(true)
577 .child(TreeItem::new("src/lib.rs", "lib.rs")),
578 TreeItem::new("README.md", "README.md"),
579 ];
580 let state = cx.new(|cx| TreeState::new(cx).items(items));
581
582 state.update(cx, |state, cx| {
583 assert_eq!(state.entries.len(), 3);
584 assert_eq!(state.entries[1].depth(), 1);
585 state.set_selected_index(Some(1), cx);
586 state.set_items(vec![TreeItem::new("Cargo.toml", "Cargo.toml")], cx);
587 assert_eq!(state.selected_index(), None);
588 assert_eq!(state.entries.len(), 1);
589 });
590 }
591
592 #[gpui::test]
593 fn selecting_hidden_item_expands_its_ancestors(cx: &mut gpui::TestAppContext) {
594 let target = TreeItem::new("src/ui/tree.rs", "tree.rs");
595 let root =
596 TreeItem::new("src", "src").child(TreeItem::new("src/ui", "ui").child(target.clone()));
597 let state = cx.new(|cx| TreeState::new(cx).items(vec![root]));
598
599 state.update(cx, |state, cx| {
600 state.set_selected_item(Some(&target), cx);
601 assert_eq!(state.entries.len(), 3);
602 assert_eq!(
603 state.selected_item().map(|item| item.id.as_str()),
604 Some("src/ui/tree.rs")
605 );
606 });
607 }
608
609 #[gpui::test]
610 fn toggling_folder_rebuilds_visible_entries(cx: &mut gpui::TestAppContext) {
611 let root = TreeItem::new("src", "src").child(TreeItem::new("src/lib.rs", "lib.rs"));
612 let state = cx.new(|cx| TreeState::new(cx).items(vec![root]));
613
614 state.update(cx, |state, cx| {
615 state.toggle_expand(0, cx);
616 assert_eq!(state.entries.len(), 2);
617 state.toggle_expand(0, cx);
618 assert_eq!(state.entries.len(), 1);
619 });
620 }
621
622 #[gpui::test]
623 fn expansion_events_preserve_ids_and_set_items_stays_silent(cx: &mut gpui::TestAppContext) {
624 let root = TreeItem::new("src", "src").child(TreeItem::new("src/lib.rs", "lib.rs"));
625 let state = cx.new(|cx| TreeState::new(cx).items(vec![root]));
626 let collector = cx.new(|cx| EventCollector::new(&state, cx));
627
628 state.update(cx, |state, cx| {
629 state.toggle_expand(0, cx);
630 state.toggle_expand(0, cx);
631 state.set_items(vec![TreeItem::new("README.md", "README.md")], cx);
632 });
633
634 let events = collector.read_with(cx, |collector, _| collector.events.borrow().clone());
635 assert_eq!(
636 events,
637 vec![
638 TreeEvent::Expanded("src".into()),
639 TreeEvent::Collapsed("src".into()),
640 ]
641 );
642 }
643}