Skip to main content

gpui_component/
tree.rs

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
17/// Create a [`Tree`].
18pub 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/// A styled tree view that preserves the legacy `gpui-component` API while
30/// delegating tree behavior and interaction state to `gpui-base`.
31#[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    /// Add a context menu to the tree.
55    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}