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                        // Capture only the id: cloning the entry would deep-clone
85                        // its whole subtree for every visible row on every frame.
86                        let id = entry.item().id.clone();
87                        let context_menu_builder = context_menu_builder.clone();
88                        let context_menu_state = state.clone();
89                        let item = render_item(ix, entry, entry_state.is_selected(), window, cx)
90                            .disabled(entry.is_disabled())
91                            .selected(entry_state.is_selected())
92                            .secondary_selected(entry_state.is_right_clicked());
93
94                        div()
95                            .child(item)
96                            .context_menu(move |menu, window, cx| {
97                                let Some(build) = context_menu_builder.as_ref() else {
98                                    return menu;
99                                };
100                                context_menu_state.update(cx, |state, cx| {
101                                    let Some(entry) = state
102                                        .entry(ix)
103                                        .filter(|entry| entry.item().id == id)
104                                        .filter(|entry| !entry.is_disabled())
105                                        .cloned()
106                                    else {
107                                        return menu;
108                                    };
109                                    build(ix, &entry, menu, window, cx)
110                                })
111                            })
112                            .into_any_element()
113                    })
114                    .list_style(StyleRefinement::default().flex_grow_1().size_full())
115                    .relative()
116                    .size_full(),
117            )
118            .refine_style(&self.style)
119            .vertical_scrollbar(&scroll_handle)
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn legacy_tree_types_are_base_types() {
129        fn accepts_base(_: gpui_base::TreeItem) {}
130        accepts_base(TreeItem::new("id", "Label"));
131    }
132}