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