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