1use std::rc::Rc;
2
3use gpui::{
4 App, Context, ElementId, Entity, InteractiveElement as _, IntoElement, ParentElement as _,
5 RenderOnce, StyleRefinement, Styled, Window, div, prelude::FluentBuilder as _,
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 scroll_bounce: Option<bool>,
39}
40
41impl Tree {
42 pub fn new<R>(state: &Entity<TreeState>, render_item: R) -> Self
43 where
44 R: Fn(usize, &TreeEntry, bool, &mut Window, &mut App) -> ListItem + 'static,
45 {
46 Self {
47 id: ElementId::Name(format!("tree-{}", state.entity_id()).into()),
48 state: state.clone(),
49 style: StyleRefinement::default(),
50 render_item: Rc::new(render_item),
51 context_menu_builder: None,
52 scroll_bounce: None,
53 }
54 }
55
56 pub fn context_menu<F>(mut self, f: F) -> Self
58 where
59 F: Fn(usize, &TreeEntry, PopupMenu, &mut Window, &mut Context<TreeState>) -> PopupMenu
60 + 'static,
61 {
62 self.context_menu_builder = Some(Rc::new(f));
63 self
64 }
65
66 pub fn scroll_bounce(mut self, enabled: bool) -> Self {
69 self.scroll_bounce = Some(enabled);
70 self
71 }
72}
73
74impl Styled for Tree {
75 fn style(&mut self) -> &mut StyleRefinement {
76 &mut self.style
77 }
78}
79
80impl RenderOnce for Tree {
81 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
82 let state = self.state.clone();
83 let render_item = self.render_item;
84 let context_menu_builder = self.context_menu_builder;
85 let scroll_handle = self.state.read(cx).scroll_handle().clone();
86
87 div()
88 .id(self.id)
89 .size_full()
90 .child(
91 gpui_base::Tree::new(&self.state)
92 .item(move |ix, entry, entry_state, window, cx| {
93 let id = entry.item().id.clone();
96 let context_menu_builder = context_menu_builder.clone();
97 let context_menu_state = state.clone();
98 let item = render_item(ix, entry, entry_state.is_selected(), window, cx)
99 .disabled(entry.is_disabled())
100 .selected(entry_state.is_selected())
101 .secondary_selected(entry_state.is_right_clicked());
102
103 div()
104 .child(item)
105 .context_menu(move |menu, window, cx| {
106 let Some(build) = context_menu_builder.as_ref() else {
107 return menu;
108 };
109 context_menu_state.update(cx, |state, cx| {
110 let Some(entry) = state
111 .entry(ix)
112 .filter(|entry| entry.item().id == id)
113 .filter(|entry| !entry.is_disabled())
114 .cloned()
115 else {
116 return menu;
117 };
118 build(ix, &entry, menu, window, cx)
119 })
120 })
121 .into_any_element()
122 })
123 .list_style(StyleRefinement::default().flex_grow_1().size_full())
124 .relative()
125 .size_full()
126 .map(|tree| {
127 gpui_base::ScrollBounce::new("tree-scroll-bounce", &scroll_handle, tree)
128 .when_some(self.scroll_bounce, |bounce, on| bounce.enabled(on))
129 }),
130 )
131 .refine_style(&self.style)
132 .vertical_scrollbar(&scroll_handle)
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn legacy_tree_types_are_base_types() {
142 fn accepts_base(_: gpui_base::TreeItem) {}
143 accepts_base(TreeItem::new("id", "Label"));
144 }
145}