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