1use std::sync::Arc;
2
3use gpui::{AnyElement, AnyView, ClickEvent, MouseButton, MouseDownEvent};
4
5use crate::{Disclosure, prelude::*};
6
7#[derive(IntoElement, RegisterComponent)]
8pub struct TreeViewItem {
9 id: ElementId,
10 group_name: Option<SharedString>,
11 label: SharedString,
12 expanded: bool,
13 selected: bool,
14 disabled: bool,
15 focused: bool,
16 default_expanded: bool,
17 root_item: bool,
18 tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
19 on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
20 on_hover: Option<Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>>,
21 on_toggle: Option<Arc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
22 on_secondary_mouse_down: Option<Box<dyn Fn(&MouseDownEvent, &mut Window, &mut App) + 'static>>,
23 tab_index: Option<isize>,
24}
25
26impl TreeViewItem {
27 pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
28 Self {
29 id: id.into(),
30 group_name: None,
31 label: label.into(),
32 expanded: false,
33 selected: false,
34 disabled: false,
35 focused: false,
36 default_expanded: false,
37 root_item: false,
38 tooltip: None,
39 on_click: None,
40 on_hover: None,
41 on_toggle: None,
42 on_secondary_mouse_down: None,
43 tab_index: None,
44 }
45 }
46
47 pub fn group_name(mut self, group_name: impl Into<SharedString>) -> Self {
48 self.group_name = Some(group_name.into());
49 self
50 }
51
52 pub fn on_click(
53 mut self,
54 handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
55 ) -> Self {
56 self.on_click = Some(Box::new(handler));
57 self
58 }
59
60 pub fn on_hover(mut self, handler: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
61 self.on_hover = Some(Box::new(handler));
62 self
63 }
64
65 pub fn on_secondary_mouse_down(
66 mut self,
67 handler: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static,
68 ) -> Self {
69 self.on_secondary_mouse_down = Some(Box::new(handler));
70 self
71 }
72
73 pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
74 self.tooltip = Some(Box::new(tooltip));
75 self
76 }
77
78 pub fn tab_index(mut self, tab_index: isize) -> Self {
79 self.tab_index = Some(tab_index);
80 self
81 }
82
83 pub fn expanded(mut self, toggle: bool) -> Self {
84 self.expanded = toggle;
85 self
86 }
87
88 pub fn default_expanded(mut self, default_expanded: bool) -> Self {
89 self.default_expanded = default_expanded;
90 self
91 }
92
93 pub fn on_toggle(
94 mut self,
95 on_toggle: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
96 ) -> Self {
97 self.on_toggle = Some(Arc::new(on_toggle));
98 self
99 }
100
101 pub fn root_item(mut self, root_item: bool) -> Self {
102 self.root_item = root_item;
103 self
104 }
105
106 pub fn focused(mut self, focused: bool) -> Self {
107 self.focused = focused;
108 self
109 }
110}
111
112impl Disableable for TreeViewItem {
113 fn disabled(mut self, disabled: bool) -> Self {
114 self.disabled = disabled;
115 self
116 }
117}
118
119impl Toggleable for TreeViewItem {
120 fn toggle_state(mut self, selected: bool) -> Self {
121 self.selected = selected;
122 self
123 }
124}
125
126impl RenderOnce for TreeViewItem {
127 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
128 let selected_bg = cx.theme().colors().element_active.opacity(0.5);
129
130 let transparent_border = cx.theme().colors().border.opacity(0.);
131 let selected_border = cx.theme().colors().border.opacity(0.6);
132 let focused_border = cx.theme().colors().border_focused;
133
134 let item_size = rems_from_px(28.);
135 let indentation_line = h_flex().size(item_size).flex_none().justify_center().child(
136 div()
137 .w_px()
138 .h_full()
139 .bg(cx.theme().colors().border.opacity(0.5)),
140 );
141
142 h_flex()
143 .id(self.id)
144 .when_some(self.group_name, |this, group| this.group(group))
145 .w_full()
146 .child(
147 h_flex()
148 .id("inner_tree_view_item")
149 .cursor_pointer()
150 .size_full()
151 .map(|this| {
152 let label = self.label;
153
154 if self.root_item {
155 this.h(item_size)
156 .px_1()
157 .gap_2p5()
158 .rounded_sm()
159 .border_1()
160 .border_color(transparent_border)
161 .when(self.selected, |this| {
162 this.border_color(selected_border).bg(selected_bg)
163 })
164 .focus(|s| s.border_color(focused_border))
165 .hover(|s| s.bg(cx.theme().colors().element_hover))
166 .when_some(self.tab_index, |this, index| this.tab_index(index))
167 .child(
168 Disclosure::new("toggle", self.expanded)
169 .when_some(
170 self.on_toggle.clone(),
171 |disclosure, on_toggle| {
172 disclosure.on_toggle_expanded(on_toggle)
173 },
174 )
175 .opened_icon(IconName::ChevronDown)
176 .closed_icon(IconName::ChevronRight),
177 )
178 .child(
179 Label::new(label)
180 .when(!self.selected, |this| this.color(Color::Muted)),
181 )
182 .when_some(self.on_hover, |this, on_hover| this.on_hover(on_hover))
183 .when_some(
184 self.on_click.filter(|_| !self.disabled),
185 |this, on_click| {
186 if self.root_item
187 && let Some(on_toggle) = self.on_toggle.clone()
188 {
189 this.on_click(move |event, window, cx| {
190 if event.is_keyboard() {
191 on_click(event, window, cx);
192 on_toggle(event, window, cx);
193 } else {
194 on_click(event, window, cx);
195 }
196 })
197 } else {
198 this.on_click(on_click)
199 }
200 },
201 )
202 .when_some(self.on_secondary_mouse_down, |this, on_mouse_down| {
203 this.on_mouse_down(
204 MouseButton::Right,
205 move |event, window, cx| (on_mouse_down)(event, window, cx),
206 )
207 })
208 .when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip))
209 } else {
210 this.child(indentation_line).child(
211 h_flex()
212 .id("nested_inner_tree_view_item")
213 .w_full()
214 .flex_grow()
215 .px_1()
216 .rounded_sm()
217 .border_1()
218 .border_color(transparent_border)
219 .when(self.selected, |this| {
220 this.border_color(selected_border).bg(selected_bg)
221 })
222 .focus(|s| s.border_color(focused_border))
223 .hover(|s| s.bg(cx.theme().colors().element_hover))
224 .when_some(self.tab_index, |this, index| this.tab_index(index))
225 .child(
226 Label::new(label)
227 .when(!self.selected, |this| this.color(Color::Muted)),
228 )
229 .when_some(self.on_hover, |this, on_hover| {
230 this.on_hover(on_hover)
231 })
232 .when_some(
233 self.on_click.filter(|_| !self.disabled),
234 |this, on_click| this.on_click(on_click),
235 )
236 .when_some(
237 self.on_secondary_mouse_down,
238 |this, on_mouse_down| {
239 this.on_mouse_down(
240 MouseButton::Right,
241 move |event, window, cx| {
242 (on_mouse_down)(event, window, cx)
243 },
244 )
245 },
246 )
247 .when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip)),
248 )
249 }
250 }),
251 )
252 }
253}
254
255impl Component for TreeViewItem {
256 fn scope() -> ComponentScope {
257 ComponentScope::Navigation
258 }
259
260 fn description() -> Option<&'static str> {
261 Some(
262 "A hierarchical list of items that may have a parent-child relationship where children can be toggled into view by expanding or collapsing their parent item.",
263 )
264 }
265
266 fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
267 let container = || {
268 v_flex()
269 .p_2()
270 .w_64()
271 .border_1()
272 .border_color(cx.theme().colors().border_variant)
273 .bg(cx.theme().colors().panel_background)
274 };
275
276 Some(
277 example_group(vec![
278 single_example(
279 "Basic Tree View",
280 container()
281 .child(
282 TreeViewItem::new("index-1", "Tree Item Root #1")
283 .root_item(true)
284 .toggle_state(true),
285 )
286 .child(TreeViewItem::new("index-2", "Tree Item #2"))
287 .child(TreeViewItem::new("index-3", "Tree Item #3"))
288 .child(TreeViewItem::new("index-4", "Tree Item Root #2").root_item(true))
289 .child(TreeViewItem::new("index-5", "Tree Item #5"))
290 .child(TreeViewItem::new("index-6", "Tree Item #6"))
291 .into_any_element(),
292 ),
293 single_example(
294 "Active Child",
295 container()
296 .child(TreeViewItem::new("index-1", "Tree Item Root #1").root_item(true))
297 .child(TreeViewItem::new("index-2", "Tree Item #2").toggle_state(true))
298 .child(TreeViewItem::new("index-3", "Tree Item #3"))
299 .into_any_element(),
300 ),
301 single_example(
302 "Focused Parent",
303 container()
304 .child(
305 TreeViewItem::new("index-1", "Tree Item Root #1")
306 .root_item(true)
307 .focused(true)
308 .toggle_state(true),
309 )
310 .child(TreeViewItem::new("index-2", "Tree Item #2"))
311 .child(TreeViewItem::new("index-3", "Tree Item #3"))
312 .into_any_element(),
313 ),
314 single_example(
315 "Focused Child",
316 container()
317 .child(
318 TreeViewItem::new("index-1", "Tree Item Root #1")
319 .root_item(true)
320 .toggle_state(true),
321 )
322 .child(TreeViewItem::new("index-2", "Tree Item #2").focused(true))
323 .child(TreeViewItem::new("index-3", "Tree Item #3"))
324 .into_any_element(),
325 ),
326 ])
327 .into_any_element(),
328 )
329 }
330}