Skip to main content

liora_components/
tree.rs

1use crate::gpui_compat::element_id;
2use crate::motion::pop_in;
3use gpui::{
4    AnyElement, App, Context, IntoElement, Pixels, Render, SharedString, Window, div, prelude::*,
5    px,
6};
7use liora_core::Config;
8use liora_icons::Icon;
9use liora_icons_lucide::IconName;
10use std::collections::HashSet;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct TreeNode {
14    pub id: SharedString,
15    pub label: SharedString,
16    pub children: Vec<TreeNode>,
17}
18
19pub struct Tree {
20    data: Vec<TreeNode>,
21    expanded_keys: HashSet<SharedString>,
22    selected_keys: HashSet<SharedString>,
23    multiple: bool,
24    indent: Pixels,
25    show_checkbox: bool,
26    on_node_click: Option<Box<dyn Fn(SharedString, &mut Window, &mut App) + 'static>>,
27}
28
29impl TreeNode {
30    pub fn new(id: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
31        Self {
32            id: id.into(),
33            label: label.into(),
34            children: vec![],
35        }
36    }
37
38    pub fn child(mut self, child: TreeNode) -> Self {
39        self.children.push(child);
40        self
41    }
42}
43
44impl Tree {
45    pub fn new(data: Vec<TreeNode>) -> Self {
46        Self {
47            data,
48            expanded_keys: HashSet::new(),
49            selected_keys: HashSet::new(),
50            multiple: false,
51            indent: px(18.0),
52            show_checkbox: false,
53            on_node_click: None,
54        }
55    }
56
57    pub fn indent(mut self, indent: impl Into<Pixels>) -> Self {
58        self.indent = indent.into();
59        self
60    }
61
62    pub fn show_checkbox(mut self, show: bool) -> Self {
63        self.show_checkbox = show;
64        self
65    }
66
67    pub fn multiple(mut self, multiple: bool) -> Self {
68        self.multiple = multiple;
69        self
70    }
71
72    pub fn on_node_click(
73        mut self,
74        f: impl Fn(SharedString, &mut Window, &mut App) + 'static,
75    ) -> Self {
76        self.on_node_click = Some(Box::new(f));
77        self
78    }
79
80    fn toggle_expand(&mut self, id: SharedString, cx: &mut Context<Self>) {
81        if self.expanded_keys.contains(&id) {
82            self.expanded_keys.remove(&id);
83        } else {
84            self.expanded_keys.insert(id);
85        }
86        cx.notify();
87    }
88
89    fn select_node(&mut self, id: SharedString, window: &mut Window, cx: &mut Context<Self>) {
90        if self.multiple {
91            if self.selected_keys.contains(&id) {
92                self.selected_keys.remove(&id);
93            } else {
94                self.selected_keys.insert(id.clone());
95            }
96        } else {
97            self.selected_keys.clear();
98            self.selected_keys.insert(id.clone());
99        }
100
101        if let Some(ref on_click) = self.on_node_click {
102            (on_click)(id, window, cx);
103        }
104        cx.notify();
105    }
106
107    fn click_node(
108        &mut self,
109        id: SharedString,
110        has_children: bool,
111        window: &mut Window,
112        cx: &mut Context<Self>,
113    ) {
114        if has_children {
115            if self.expanded_keys.contains(&id) {
116                self.expanded_keys.remove(&id);
117            } else {
118                self.expanded_keys.insert(id.clone());
119            }
120        }
121        self.select_node(id, window, cx);
122    }
123
124    fn render_node(
125        &self,
126        node: &TreeNode,
127        depth: u32,
128        theme: &liora_theme::Theme,
129        cx: &Context<Self>,
130    ) -> AnyElement {
131        let id = node.id.clone();
132        let is_expanded = self.expanded_keys.contains(&id);
133        let is_selected = self.selected_keys.contains(&id);
134        let has_children = !node.children.is_empty();
135        let padding_left = px(f32::from(self.indent) * depth as f32);
136
137        div()
138            .flex()
139            .flex_col()
140            .child(
141                div()
142                    .id(id.clone())
143                    .cursor_pointer()
144                    .flex()
145                    .flex_row()
146                    .items_center()
147                    .gap_1()
148                    .h(px(32.0))
149                    .pl(padding_left)
150                    .pr_4()
151                    .text_color(if is_selected {
152                        theme.primary.base
153                    } else {
154                        theme.neutral.text_1
155                    })
156                    .bg(if is_selected {
157                        theme.primary.base.opacity(0.1)
158                    } else {
159                        gpui::transparent_black()
160                    })
161                    .hover(|s| s.bg(theme.neutral.hover))
162                    .child(
163                        // Expand Icon
164                        div()
165                            .flex()
166                            .items_center()
167                            .justify_center()
168                            .w(px(20.0))
169                            .id(element_id(format!("expand-{}", id.clone())))
170                            .when(has_children, |s| {
171                                s.on_click(cx.listener({
172                                    let id = id.clone();
173                                    move |this, _, _, cx| {
174                                        this.toggle_expand(id.clone(), cx);
175                                        cx.stop_propagation();
176                                    }
177                                }))
178                                .child(
179                                    Icon::new(if is_expanded {
180                                        IconName::ChevronDown
181                                    } else {
182                                        IconName::ChevronRight
183                                    })
184                                    .size(px(14.0))
185                                    .color(theme.neutral.text_3),
186                                )
187                            }),
188                    )
189                    .on_click(cx.listener({
190                        let id = id.clone();
191                        move |this, _, window, cx| {
192                            this.click_node(id.clone(), has_children, window, cx);
193                        }
194                    }))
195                    .child(
196                        div()
197                            .flex_1()
198                            .id(element_id(format!("content-{}", id.clone())))
199                            .child(div().text_sm().child(node.label.clone())),
200                    ),
201            )
202            .when(is_expanded && has_children, |s| {
203                s.child(pop_in(
204                    element_id(format!("tree-children-motion-{}", id)),
205                    div().flex().flex_col().children(
206                        node.children
207                            .iter()
208                            .map(|child| self.render_node(child, depth + 1, theme, cx)),
209                    ),
210                ))
211            })
212            .into_any_element()
213    }
214}
215
216impl Render for Tree {
217    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
218        let theme = cx.global::<Config>().theme.clone();
219
220        div().flex().flex_col().w_full().children(
221            self.data
222                .iter()
223                .map(|node| self.render_node(node, 0, &theme, cx)),
224        )
225    }
226}