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, 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
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    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    /// Add a context menu to the tree.
57    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    /// Bounce at the vertical boundaries. On by default on iOS, Android and
67    /// macOS, off elsewhere; reduced motion disables displacement.
68    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                        // Capture only the id: cloning the entry would deep-clone
94                        // its whole subtree for every visible row on every frame.
95                        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}