1use std::rc::Rc;
2
3use gpui::{
4 App, Context, ElementId, Entity, InteractiveElement as _, IntoElement, ParentElement as _,
5 RenderOnce, StyleRefinement, Styled, Window, div,
6};
7
8use crate::{
9 Selectable as _, StyledExt,
10 list::ListItem,
11 menu::{ContextMenuExt as _, PopupMenu},
12 scroll::ScrollableElement,
13};
14
15pub use gpui_base::{TreeEntry, TreeEvent, TreeItem, TreeState};
16
17pub fn tree<R>(state: &Entity<TreeState>, render_item: R) -> Tree
19where
20 R: Fn(usize, &TreeEntry, bool, &mut Window, &mut App) -> ListItem + 'static,
21{
22 Tree::new(state, render_item)
23}
24
25type RenderItem = dyn Fn(usize, &TreeEntry, bool, &mut Window, &mut App) -> ListItem;
26type ContextMenuBuilder =
27 dyn Fn(usize, &TreeEntry, PopupMenu, &mut Window, &mut Context<TreeState>) -> PopupMenu;
28
29#[derive(IntoElement)]
32pub struct Tree {
33 id: ElementId,
34 state: Entity<TreeState>,
35 style: StyleRefinement,
36 render_item: Rc<RenderItem>,
37 context_menu_builder: Option<Rc<ContextMenuBuilder>>,
38}
39
40impl Tree {
41 pub fn new<R>(state: &Entity<TreeState>, render_item: R) -> Self
42 where
43 R: Fn(usize, &TreeEntry, bool, &mut Window, &mut App) -> ListItem + 'static,
44 {
45 Self {
46 id: ElementId::Name(format!("tree-{}", state.entity_id()).into()),
47 state: state.clone(),
48 style: StyleRefinement::default(),
49 render_item: Rc::new(render_item),
50 context_menu_builder: None,
51 }
52 }
53
54 pub fn context_menu<F>(mut self, f: F) -> Self
56 where
57 F: Fn(usize, &TreeEntry, PopupMenu, &mut Window, &mut Context<TreeState>) -> PopupMenu
58 + 'static,
59 {
60 self.context_menu_builder = Some(Rc::new(f));
61 self
62 }
63}
64
65impl Styled for Tree {
66 fn style(&mut self) -> &mut StyleRefinement {
67 &mut self.style
68 }
69}
70
71impl RenderOnce for Tree {
72 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
73 let state = self.state.clone();
74 let render_item = self.render_item;
75 let context_menu_builder = self.context_menu_builder;
76 let scroll_handle = self.state.read(cx).scroll_handle().clone();
77
78 div()
79 .id(self.id)
80 .size_full()
81 .child(
82 gpui_base::Tree::new(&self.state)
83 .item(move |ix, entry, entry_state, window, cx| {
84 let entry = entry.clone();
85 let context_menu_builder = context_menu_builder.clone();
86 let context_menu_state = state.clone();
87 let item = render_item(ix, &entry, entry_state.is_selected(), window, cx)
88 .disabled(entry.is_disabled())
89 .selected(entry_state.is_selected())
90 .secondary_selected(entry_state.is_right_clicked());
91
92 div()
93 .child(item)
94 .context_menu(move |menu, window, cx| {
95 let Some(build) = context_menu_builder
96 .as_ref()
97 .filter(|_| !entry.is_disabled())
98 else {
99 return menu;
100 };
101 context_menu_state
102 .update(cx, |_, cx| build(ix, &entry, menu, window, cx))
103 })
104 .into_any_element()
105 })
106 .list_style(StyleRefinement::default().flex_grow_1().size_full())
107 .relative()
108 .size_full(),
109 )
110 .refine_style(&self.style)
111 .vertical_scrollbar(&scroll_handle)
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn legacy_tree_types_are_base_types() {
121 fn accepts_base(_: gpui_base::TreeItem) {}
122 accepts_base(TreeItem::new("id", "Label"));
123 }
124}