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