1use crate::TestSupportExt as _;
2use gpui::StatefulInteractiveElement as _;
3use std::{cell::RefCell, ops::Range, rc::Rc};
4
5use gpui::{
6 AnyElement, App, Context, ElementId, Entity, EventEmitter, FocusHandle, InteractiveElement,
7 IntoElement, KeyBinding, MouseButton, ParentElement, Render, RenderOnce, SharedString,
8 StyleRefinement, Styled, UniformListScrollHandle, Window, div, prelude::FluentBuilder as _,
9 uniform_list,
10};
11
12use crate::{
13 actions::{Confirm, SelectDown, SelectLeft, SelectRight, SelectUp},
14 styled::StyledExt as _,
15};
16
17const CONTEXT: &str = "Tree";
18
19#[doc(hidden)]
20pub fn init(cx: &mut App) {
21 cx.bind_keys([
22 KeyBinding::new("up", SelectUp, Some(CONTEXT)),
23 KeyBinding::new("down", SelectDown, Some(CONTEXT)),
24 KeyBinding::new("left", SelectLeft, Some(CONTEXT)),
25 KeyBinding::new("right", SelectRight, Some(CONTEXT)),
26 ]);
27}
28
29#[doc(hidden)]
30pub const fn key_context() -> &'static str {
31 CONTEXT
32}
33
34struct TreeItemState {
35 expanded: bool,
36 disabled: bool,
37}
38
39#[derive(Clone)]
41pub struct TreeItem {
42 pub id: SharedString,
43 pub label: SharedString,
44 pub children: Vec<TreeItem>,
45 state: Rc<RefCell<TreeItemState>>,
46}
47
48#[derive(Clone)]
50pub struct TreeEntry {
51 item: TreeItem,
52 depth: usize,
53}
54
55impl TreeEntry {
56 pub fn new(item: TreeItem, depth: usize) -> Self {
57 Self { item, depth }
58 }
59
60 #[inline]
61 pub fn item(&self) -> &TreeItem {
62 &self.item
63 }
64
65 #[inline]
66 pub fn depth(&self) -> usize {
67 self.depth
68 }
69
70 #[inline]
71 pub fn is_root(&self) -> bool {
72 self.depth == 0
73 }
74
75 #[inline]
76 pub fn is_folder(&self) -> bool {
77 self.item.is_folder()
78 }
79
80 #[inline]
81 pub fn is_expanded(&self) -> bool {
82 self.item.is_expanded()
83 }
84
85 #[inline]
86 pub fn is_disabled(&self) -> bool {
87 self.item.is_disabled()
88 }
89}
90
91#[derive(Clone, Debug, PartialEq, Eq)]
93pub enum TreeEvent {
94 Expanded(SharedString),
95 Collapsed(SharedString),
96}
97
98impl TreeItem {
99 pub fn new(id: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
100 Self {
101 id: id.into(),
102 label: label.into(),
103 children: Vec::new(),
104 state: Rc::new(RefCell::new(TreeItemState {
105 expanded: false,
106 disabled: false,
107 })),
108 }
109 }
110
111 pub fn child(mut self, child: TreeItem) -> Self {
112 self.children.push(child);
113 self
114 }
115
116 pub fn children(mut self, children: impl IntoIterator<Item = TreeItem>) -> Self {
117 self.children.extend(children);
118 self
119 }
120
121 pub fn expanded(self, expanded: bool) -> Self {
122 self.state.borrow_mut().expanded = expanded;
123 self
124 }
125
126 pub fn disabled(self, disabled: bool) -> Self {
127 self.state.borrow_mut().disabled = disabled;
128 self
129 }
130
131 #[inline]
132 pub fn is_folder(&self) -> bool {
133 !self.children.is_empty()
134 }
135
136 pub fn is_disabled(&self) -> bool {
137 self.state.borrow().disabled
138 }
139
140 #[inline]
141 pub fn is_expanded(&self) -> bool {
142 self.state.borrow().expanded
143 }
144
145 pub fn ancestors(&self, target_id: &SharedString) -> Option<Vec<TreeItem>> {
147 self.ancestor_refs(target_id)
148 .map(|path| path.into_iter().cloned().collect())
149 }
150
151 fn ancestor_refs(&self, target_id: &SharedString) -> Option<Vec<&TreeItem>> {
154 if self.id == *target_id {
155 return Some(Vec::new());
156 }
157
158 for child in &self.children {
159 if let Some(mut path) = child.ancestor_refs(target_id) {
160 path.push(self);
161 return Some(path);
162 }
163 }
164
165 None
166 }
167}
168
169#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
171pub struct TreeEntryState {
172 selected: bool,
173 right_clicked: bool,
174}
175
176impl TreeEntryState {
177 #[inline]
178 pub fn is_selected(self) -> bool {
179 self.selected
180 }
181
182 #[inline]
183 pub fn is_right_clicked(self) -> bool {
184 self.right_clicked
185 }
186}
187
188type RenderItem = dyn Fn(usize, &TreeEntry, TreeEntryState, &mut Window, &mut App) -> AnyElement;
189
190pub struct TreeState {
192 focus_handle: FocusHandle,
193 entries: Vec<TreeEntry>,
194 scroll_handle: UniformListScrollHandle,
195 selected_ix: Option<usize>,
196 right_clicked_ix: Option<usize>,
197 render_item: Rc<RenderItem>,
198 list_style: StyleRefinement,
199}
200
201impl EventEmitter<TreeEvent> for TreeState {}
202
203impl TreeState {
204 pub fn new(cx: &mut App) -> Self {
205 Self {
206 focus_handle: cx.focus_handle(),
207 entries: Vec::new(),
208 scroll_handle: UniformListScrollHandle::default(),
209 selected_ix: None,
210 right_clicked_ix: None,
211 render_item: Rc::new(|_, _, _, _, _| div().into_any_element()),
212 list_style: StyleRefinement::default(),
213 }
214 }
215
216 pub fn items(mut self, items: impl Into<Vec<TreeItem>>) -> Self {
217 self.replace_items(items.into());
218 self
219 }
220
221 pub fn set_items(&mut self, items: impl Into<Vec<TreeItem>>, cx: &mut Context<Self>) {
222 self.replace_items(items.into());
223 self.selected_ix = None;
224 self.right_clicked_ix = None;
225 cx.notify();
226 }
227
228 pub fn selected_index(&self) -> Option<usize> {
229 self.selected_ix
230 }
231
232 pub fn set_selected_index(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
233 self.selected_ix = ix;
234 cx.notify();
235 }
236
237 pub fn set_selected_item(&mut self, item: Option<&TreeItem>, cx: &mut Context<Self>) {
238 if let Some(item) = item {
239 self.selected_ix = self.index_of(&item.id);
240 if self.selected_ix.is_none() {
241 self.expand_ancestors(item.id.clone(), cx);
242 self.selected_ix = self.index_of(&item.id);
243 }
244 } else {
245 self.selected_ix = None;
246 }
247 cx.notify();
248 }
249
250 pub fn selected_item(&self) -> Option<&TreeItem> {
251 self.selected_ix
252 .and_then(|ix| self.entries.get(ix).map(TreeEntry::item))
253 }
254
255 pub fn selected_entry(&self) -> Option<&TreeEntry> {
256 self.selected_ix.and_then(|ix| self.entries.get(ix))
257 }
258
259 pub fn entry(&self, ix: usize) -> Option<&TreeEntry> {
260 self.entries.get(ix)
261 }
262
263 pub fn scroll_handle(&self) -> &UniformListScrollHandle {
264 &self.scroll_handle
265 }
266
267 pub fn scroll_to_item(&mut self, ix: usize, strategy: gpui::ScrollStrategy) {
268 self.scroll_handle.scroll_to_item(ix, strategy);
269 }
270
271 pub fn index_of(&self, id: &SharedString) -> Option<usize> {
272 self.entries.iter().position(|entry| &entry.item.id == id)
273 }
274
275 pub fn reveal_item(
276 &mut self,
277 id: &SharedString,
278 strategy: gpui::ScrollStrategy,
279 cx: &mut Context<Self>,
280 ) {
281 self.expand_ancestors(id.clone(), cx);
282 if let Some(ix) = self.index_of(id) {
283 self.scroll_to_item(ix, strategy);
284 }
285 }
286
287 pub fn focus(&mut self, window: &mut Window, cx: &mut App) {
288 self.focus_handle.focus(window, cx);
289 }
290
291 fn replace_items(&mut self, items: Vec<TreeItem>) {
292 self.entries.clear();
293 for item in items {
294 self.push_root(item);
295 }
296 }
297
298 fn expand_ancestors(&mut self, target_id: SharedString, cx: &mut Context<Self>) {
299 let ancestors = self
302 .entries
303 .iter()
304 .filter(|entry| entry.is_root())
305 .find_map(|entry| entry.item.ancestor_refs(&target_id))
306 .unwrap_or_default();
307
308 if ancestors.is_empty() {
309 return;
310 }
311
312 for ancestor in ancestors.into_iter().rev() {
313 if !ancestor.is_expanded() {
314 ancestor.state.borrow_mut().expanded = true;
315 cx.emit(TreeEvent::Expanded(ancestor.id.clone()));
316 }
317 }
318 self.rebuild_entries();
319 }
320
321 fn push_root(&mut self, mut item: TreeItem) {
324 let children = if item.is_expanded() {
325 std::mem::take(&mut item.children)
326 } else {
327 Vec::new()
328 };
329
330 let ix = self.entries.len();
331 self.entries.push(TreeEntry::new(item, 0));
332 if !children.is_empty() {
333 for child in &children {
334 self.add_entry(child, 1);
335 }
336 self.entries[ix].item.children = children;
337 }
338 }
339
340 fn add_entry(&mut self, item: &TreeItem, depth: usize) {
341 self.entries.push(TreeEntry::new(item.clone(), depth));
342 if item.is_expanded() {
343 for child in &item.children {
344 self.add_entry(child, depth + 1);
345 }
346 }
347 }
348
349 fn toggle_expand(&mut self, ix: usize, cx: &mut Context<Self>) {
350 let Some(entry) = self.entries.get(ix) else {
351 return;
352 };
353 if !entry.is_folder() {
354 return;
355 }
356
357 let expanded = !entry.is_expanded();
358 let id = entry.item.id.clone();
359 entry.item.state.borrow_mut().expanded = expanded;
360 cx.emit(if expanded {
361 TreeEvent::Expanded(id)
362 } else {
363 TreeEvent::Collapsed(id)
364 });
365 self.right_clicked_ix = None;
366 self.rebuild_entries();
367 }
368
369 fn rebuild_entries(&mut self) {
370 let roots = std::mem::take(&mut self.entries)
371 .into_iter()
372 .filter(TreeEntry::is_root)
373 .map(|entry| entry.item)
374 .collect::<Vec<_>>();
375 self.replace_items(roots);
376 }
377
378 fn on_action_confirm(&mut self, _: &Confirm, _: &mut Window, cx: &mut Context<Self>) {
379 if self
380 .selected_ix
381 .and_then(|ix| self.entries.get(ix).map(|entry| (ix, entry.is_folder())))
382 .is_some_and(|(ix, is_folder)| {
383 if is_folder {
384 self.toggle_expand(ix, cx);
385 }
386 is_folder
387 })
388 {
389 cx.notify();
390 }
391 }
392
393 fn on_action_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut Context<Self>) {
394 if let Some(ix) = self.selected_ix
395 && self
396 .entries
397 .get(ix)
398 .is_some_and(|entry| entry.is_folder() && entry.is_expanded())
399 {
400 self.toggle_expand(ix, cx);
401 cx.notify();
402 }
403 }
404
405 fn on_action_right(&mut self, _: &SelectRight, _: &mut Window, cx: &mut Context<Self>) {
406 if let Some(ix) = self.selected_ix
407 && self
408 .entries
409 .get(ix)
410 .is_some_and(|entry| entry.is_folder() && !entry.is_expanded())
411 {
412 self.toggle_expand(ix, cx);
413 cx.notify();
414 }
415 }
416
417 fn on_action_up(&mut self, _: &SelectUp, _: &mut Window, cx: &mut Context<Self>) {
418 let mut ix = self.selected_ix.unwrap_or(0);
419 ix = ix
420 .checked_sub(1)
421 .unwrap_or_else(|| self.entries.len().saturating_sub(1));
422 self.selected_ix = Some(ix);
423 self.scroll_handle
424 .scroll_to_item(ix, gpui::ScrollStrategy::Top);
425 cx.notify();
426 }
427
428 fn on_action_down(&mut self, _: &SelectDown, _: &mut Window, cx: &mut Context<Self>) {
429 let mut ix = self.selected_ix.unwrap_or(0);
430 ix = if ix + 1 < self.entries.len() {
431 ix + 1
432 } else {
433 0
434 };
435 self.selected_ix = Some(ix);
436 self.scroll_handle
437 .scroll_to_item(ix, gpui::ScrollStrategy::Bottom);
438 cx.notify();
439 }
440
441 fn on_entry_click(&mut self, ix: usize, cx: &mut Context<Self>) {
442 self.selected_ix = Some(ix);
443 self.toggle_expand(ix, cx);
444 cx.notify();
445 }
446}
447
448impl Render for TreeState {
449 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
450 let render_item = self.render_item.clone();
451 uniform_list("entries", self.entries.len(), {
452 cx.processor(move |state, visible_range: Range<usize>, window, cx| {
453 visible_range
454 .map(|ix| {
455 let entry = &state.entries[ix];
456 let entry_state = TreeEntryState {
457 selected: Some(ix) == state.selected_ix,
458 right_clicked: Some(ix) == state.right_clicked_ix,
459 };
460 div()
461 .id(ix)
462 .test_support()
463 .role(gpui::Role::TreeItem)
464 .aria_label(entry.item().label.clone())
465 .aria_selected(entry_state.selected)
466 .when(entry.is_folder(), |this| {
467 this.aria_expanded(entry.is_expanded())
468 })
469 .child((render_item)(ix, entry, entry_state, window, cx))
470 .when(!entry.is_disabled(), |this| {
471 this.on_click(cx.listener(move |state, _, _, cx| {
474 state.on_entry_click(ix, cx);
475 }))
476 .on_mouse_down(
477 MouseButton::Right,
478 cx.listener(move |state, _, _, cx| {
479 state.right_clicked_ix = Some(ix);
480 cx.notify();
481 }),
482 )
483 })
484 })
485 .collect()
486 })
487 })
488 .track_scroll(&self.scroll_handle)
489 .refine_style(&self.list_style)
490 }
491}
492
493#[derive(IntoElement)]
501pub struct Tree {
502 id: ElementId,
503 state: Entity<TreeState>,
504 style: StyleRefinement,
505 list_style: StyleRefinement,
506 render_item: Rc<RenderItem>,
507}
508
509impl Tree {
510 pub fn new(state: &Entity<TreeState>) -> Self {
511 Self {
512 id: ElementId::Name(format!("tree-{}", state.entity_id()).into()),
513 state: state.clone(),
514 style: StyleRefinement::default(),
515 list_style: StyleRefinement::default(),
516 render_item: Rc::new(|_, _, _, _, _| div().into_any_element()),
517 }
518 }
519
520 pub fn item<R>(mut self, render_item: R) -> Self
522 where
523 R: Fn(usize, &TreeEntry, TreeEntryState, &mut Window, &mut App) -> AnyElement + 'static,
524 {
525 self.render_item = Rc::new(render_item);
526 self
527 }
528
529 pub fn list_style(mut self, style: StyleRefinement) -> Self {
531 self.list_style = style;
532 self
533 }
534}
535
536impl Styled for Tree {
537 fn style(&mut self) -> &mut StyleRefinement {
538 &mut self.style
539 }
540}
541
542impl RenderOnce for Tree {
543 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
544 let focus_handle = self.state.read(cx).focus_handle.clone();
545 self.state.update(cx, |state, _| {
546 state.render_item = self.render_item;
547 state.list_style = self.list_style;
548 });
549
550 div()
551 .id(self.id)
552 .test_support()
553 .role(gpui::Role::Tree)
554 .key_context(CONTEXT)
555 .track_focus(&focus_handle)
556 .on_action(window.listener_for(&self.state, TreeState::on_action_confirm))
557 .on_action(window.listener_for(&self.state, TreeState::on_action_left))
558 .on_action(window.listener_for(&self.state, TreeState::on_action_right))
559 .on_action(window.listener_for(&self.state, TreeState::on_action_up))
560 .on_action(window.listener_for(&self.state, TreeState::on_action_down))
561 .child(self.state)
562 .refine_style(&self.style)
563 }
564}
565
566#[cfg(test)]
567mod tests {
568 use super::*;
569 use gpui::{
570 AppContext as _, Modifiers, MouseUpEvent, Pixels, Point, Subscription, TestAppContext,
571 VisualTestContext, point, px,
572 };
573
574 struct EventCollector {
575 events: Rc<RefCell<Vec<TreeEvent>>>,
576 _subscription: Subscription,
577 }
578
579 impl EventCollector {
580 fn new(state: &Entity<TreeState>, cx: &mut Context<Self>) -> Self {
581 let events = Rc::new(RefCell::new(Vec::new()));
582 let captured = events.clone();
583 let subscription = cx.subscribe(state, move |_, _, event, _| {
584 captured.borrow_mut().push(event.clone());
585 });
586 Self {
587 events,
588 _subscription: subscription,
589 }
590 }
591 }
592
593 impl Render for EventCollector {
594 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
595 div()
596 }
597 }
598
599 #[test]
600 fn clones_share_state_and_ancestors_keep_nearest_first_order() {
601 let leaf = TreeItem::new("leaf", "Leaf");
602 let branch = TreeItem::new("branch", "Branch").child(leaf.clone());
603 let root = TreeItem::new("root", "Root").child(branch.clone());
604
605 leaf.clone().disabled(true).expanded(true);
606 assert!(leaf.is_disabled());
607 assert!(leaf.is_expanded());
608
609 let ancestors = root.ancestors(&"leaf".into()).unwrap();
610 assert_eq!(
611 ancestors
612 .iter()
613 .map(|item| item.id.as_str())
614 .collect::<Vec<_>>(),
615 vec!["branch", "root"]
616 );
617 }
618
619 #[gpui::test]
620 fn state_flattens_expanded_items_and_resets_selection(cx: &mut gpui::TestAppContext) {
621 let items = vec![
622 TreeItem::new("src", "src")
623 .expanded(true)
624 .child(TreeItem::new("src/lib.rs", "lib.rs")),
625 TreeItem::new("README.md", "README.md"),
626 ];
627 let state = cx.new(|cx| TreeState::new(cx).items(items));
628
629 state.update(cx, |state, cx| {
630 assert_eq!(state.entries.len(), 3);
631 assert_eq!(state.entries[1].depth(), 1);
632 state.set_selected_index(Some(1), cx);
633 state.set_items(vec![TreeItem::new("Cargo.toml", "Cargo.toml")], cx);
634 assert_eq!(state.selected_index(), None);
635 assert_eq!(state.entries.len(), 1);
636 });
637 }
638
639 #[gpui::test]
640 fn selecting_hidden_item_expands_its_ancestors(cx: &mut gpui::TestAppContext) {
641 let target = TreeItem::new("src/ui/tree.rs", "tree.rs");
642 let root =
643 TreeItem::new("src", "src").child(TreeItem::new("src/ui", "ui").child(target.clone()));
644 let state = cx.new(|cx| TreeState::new(cx).items(vec![root]));
645
646 state.update(cx, |state, cx| {
647 state.set_selected_item(Some(&target), cx);
648 assert_eq!(state.entries.len(), 3);
649 assert_eq!(
650 state.selected_item().map(|item| item.id.as_str()),
651 Some("src/ui/tree.rs")
652 );
653 });
654 }
655
656 #[gpui::test]
657 fn revealing_item_under_later_root_keeps_other_subtrees(cx: &mut gpui::TestAppContext) {
658 let guide = TreeItem::new("docs/guide", "guide").child(TreeItem::new("docs/guide/a", "a"));
659 let docs = TreeItem::new("docs", "docs").expanded(true).child(guide);
660 let ui = TreeItem::new("src/ui", "ui").child(TreeItem::new("src/ui/tree.rs", "tree.rs"));
661 let src = TreeItem::new("src", "src").child(ui);
662 let state = cx.new(|cx| TreeState::new(cx).items(vec![docs, src]));
663
664 state.update(cx, |state, cx| {
665 assert_eq!(state.entries.len(), 3);
666 state.reveal_item(&"src/ui/tree.rs".into(), gpui::ScrollStrategy::Top, cx);
667 assert_eq!(
668 state
669 .entries
670 .iter()
671 .map(|entry| (entry.item().id.as_str(), entry.depth()))
672 .collect::<Vec<_>>(),
673 vec![
674 ("docs", 0),
675 ("docs/guide", 1),
676 ("src", 0),
677 ("src/ui", 1),
678 ("src/ui/tree.rs", 2),
679 ]
680 );
681 assert!(state.entries[0].is_folder());
682 assert_eq!(state.entries[0].item().children.len(), 1);
683
684 state.toggle_expand(1, cx);
685 assert_eq!(state.index_of(&"docs/guide/a".into()), Some(2));
686 });
687 }
688
689 struct TreeHarness {
690 state: Entity<TreeState>,
691 draggable: bool,
692 }
693
694 struct DraggedEntry;
695
696 impl Render for DraggedEntry {
697 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
698 div()
699 }
700 }
701
702 impl Render for TreeHarness {
703 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
704 let draggable = self.draggable;
705 div().size_full().child(
706 Tree::new(&self.state)
707 .size_full()
708 .list_style(StyleRefinement::default().size_full())
709 .item(move |ix, _, _, _, _| {
710 div()
711 .id(("tree-entry", ix))
712 .debug_selector(move || format!("tree-entry-{ix}"))
713 .h(px(20.))
714 .w_full()
715 .when(draggable, |this| {
716 this.on_drag(ix, |_, _, _, cx| cx.new(|_| DraggedEntry))
717 })
718 .into_any_element()
719 }),
720 )
721 }
722 }
723
724 fn open_tree(
727 cx: &mut TestAppContext,
728 items: Vec<TreeItem>,
729 draggable: bool,
730 ) -> (Entity<TreeState>, &mut VisualTestContext, Point<Pixels>) {
731 let state = cx.new(|cx| TreeState::new(cx).items(items));
732 let (_, cx) = cx.add_window_view({
733 let state = state.clone();
734 move |_, _| TreeHarness { state, draggable }
735 });
736 cx.update(|window, cx| window.draw(cx).clear(cx));
737 let entry = cx
738 .debug_bounds("tree-entry-0")
739 .expect("the first entry is painted")
740 .center();
741 (state, cx, entry)
742 }
743
744 fn folder() -> TreeItem {
745 TreeItem::new("src", "src").child(TreeItem::new("src/lib.rs", "lib.rs"))
746 }
747
748 #[gpui::test]
749 fn entries_select_and_toggle_on_click_not_mouse_down(cx: &mut TestAppContext) {
750 let (state, cx, entry) = open_tree(cx, vec![folder()], false);
751
752 cx.simulate_mouse_down(entry, MouseButton::Left, Modifiers::default());
754 state.read_with(cx, |state, _| {
755 assert_eq!(state.selected_index(), None);
756 assert_eq!(state.entries.len(), 1);
757 });
758 cx.simulate_event(MouseUpEvent {
760 position: point(px(10.), px(500.)),
761 button: MouseButton::Left,
762 modifiers: Modifiers::default(),
763 click_count: 1,
764 });
765 cx.update(|window, cx| window.draw(cx).clear(cx));
766 state.read_with(cx, |state, _| assert_eq!(state.selected_index(), None));
767
768 cx.simulate_click(entry, Modifiers::default());
769 state.read_with(cx, |state, _| {
770 assert_eq!(state.selected_index(), Some(0));
771 assert_eq!(state.entries.len(), 2);
772 });
773 }
774
775 #[gpui::test]
776 fn dragging_an_entry_with_on_drag_does_not_select_or_toggle_it(cx: &mut TestAppContext) {
777 let (state, cx, entry) = open_tree(cx, vec![folder()], true);
778
779 cx.simulate_mouse_down(entry, MouseButton::Left, Modifiers::default());
781 cx.simulate_mouse_move(
782 entry + point(px(8.), px(0.)),
783 MouseButton::Left,
784 Modifiers::default(),
785 );
786 cx.update(|window, cx| window.draw(cx).clear(cx));
787 cx.simulate_mouse_up(entry, MouseButton::Left, Modifiers::default());
788
789 state.read_with(cx, |state, _| {
790 assert_eq!(state.selected_index(), None);
791 assert_eq!(state.entries.len(), 1);
792 });
793 }
794
795 #[gpui::test]
796 fn right_click_neither_selects_nor_toggles(cx: &mut TestAppContext) {
797 let (state, cx, entry) = open_tree(cx, vec![folder()], false);
798
799 cx.simulate_mouse_down(entry, MouseButton::Right, Modifiers::default());
800 cx.simulate_mouse_up(entry, MouseButton::Right, Modifiers::default());
801
802 state.read_with(cx, |state, _| {
803 assert_eq!(state.right_clicked_ix, Some(0));
804 assert_eq!(state.selected_index(), None);
805 assert_eq!(state.entries.len(), 1);
806 });
807 }
808
809 #[gpui::test]
810 fn clicking_a_disabled_entry_does_nothing(cx: &mut TestAppContext) {
811 let (state, cx, entry) = open_tree(cx, vec![folder().disabled(true)], false);
812
813 cx.simulate_click(entry, Modifiers::default());
814
815 state.read_with(cx, |state, _| {
816 assert_eq!(state.selected_index(), None);
817 assert_eq!(state.entries.len(), 1);
818 });
819 }
820
821 #[gpui::test]
822 fn toggling_folder_rebuilds_visible_entries(cx: &mut gpui::TestAppContext) {
823 let root = TreeItem::new("src", "src").child(TreeItem::new("src/lib.rs", "lib.rs"));
824 let state = cx.new(|cx| TreeState::new(cx).items(vec![root]));
825
826 state.update(cx, |state, cx| {
827 state.toggle_expand(0, cx);
828 assert_eq!(state.entries.len(), 2);
829 state.toggle_expand(0, cx);
830 assert_eq!(state.entries.len(), 1);
831 });
832 }
833
834 #[gpui::test]
835 fn expansion_events_preserve_ids_and_set_items_stays_silent(cx: &mut gpui::TestAppContext) {
836 let root = TreeItem::new("src", "src").child(TreeItem::new("src/lib.rs", "lib.rs"));
837 let state = cx.new(|cx| TreeState::new(cx).items(vec![root]));
838 let collector = cx.new(|cx| EventCollector::new(&state, cx));
839
840 state.update(cx, |state, cx| {
841 state.toggle_expand(0, cx);
842 state.toggle_expand(0, cx);
843 state.set_items(vec![TreeItem::new("README.md", "README.md")], cx);
844 });
845
846 let events = collector.read_with(cx, |collector, _| collector.events.borrow().clone());
847 assert_eq!(
848 events,
849 vec![
850 TreeEvent::Expanded("src".into()),
851 TreeEvent::Collapsed("src".into()),
852 ]
853 );
854 }
855}