1use std::{cell::RefCell, ops::Range, rc::Rc};
2
3use gpui::{
4 App, Context, ElementId, Entity, FocusHandle, InteractiveElement as _, IntoElement, KeyBinding,
5 ListSizingBehavior, MouseButton, ParentElement, Render, RenderOnce, SharedString,
6 StyleRefinement, Styled, UniformListScrollHandle, Window, div, prelude::FluentBuilder as _,
7 uniform_list,
8};
9
10use crate::{
11 StyledExt,
12 actions::{Confirm, SelectDown, SelectLeft, SelectRight, SelectUp},
13 list::ListItem,
14 scroll::ScrollableElement,
15};
16
17const CONTEXT: &str = "Tree";
18pub(crate) fn init(cx: &mut App) {
19 cx.bind_keys([
20 KeyBinding::new("up", SelectUp, Some(CONTEXT)),
21 KeyBinding::new("down", SelectDown, Some(CONTEXT)),
22 KeyBinding::new("left", SelectLeft, Some(CONTEXT)),
23 KeyBinding::new("right", SelectRight, Some(CONTEXT)),
24 ]);
25}
26
27pub fn tree<R>(state: &Entity<TreeState>, render_item: R) -> Tree
50where
51 R: Fn(usize, &TreeEntry, bool, &mut Window, &mut App) -> ListItem + 'static,
52{
53 Tree::new(state, render_item)
54}
55
56struct TreeItemState {
57 expanded: bool,
58 disabled: bool,
59}
60
61#[derive(Clone)]
63pub struct TreeItem {
64 pub id: SharedString,
65 pub label: SharedString,
66 pub children: Vec<TreeItem>,
67 state: Rc<RefCell<TreeItemState>>,
68}
69
70#[derive(Clone)]
72pub struct TreeEntry {
73 item: TreeItem,
74 depth: usize,
75}
76
77impl TreeEntry {
78 #[inline]
80 pub fn item(&self) -> &TreeItem {
81 &self.item
82 }
83
84 #[inline]
86 pub fn depth(&self) -> usize {
87 self.depth
88 }
89
90 #[inline]
91 fn is_root(&self) -> bool {
92 self.depth == 0
93 }
94
95 #[inline]
97 pub fn is_folder(&self) -> bool {
98 self.item.is_folder()
99 }
100
101 #[inline]
103 pub fn is_expanded(&self) -> bool {
104 self.item.is_expanded()
105 }
106
107 #[inline]
108 pub fn is_disabled(&self) -> bool {
109 self.item.is_disabled()
110 }
111}
112
113impl TreeItem {
114 pub fn new(id: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
125 Self {
126 id: id.into(),
127 label: label.into(),
128 children: Vec::new(),
129 state: Rc::new(RefCell::new(TreeItemState {
130 expanded: false,
131 disabled: false,
132 })),
133 }
134 }
135
136 pub fn child(mut self, child: TreeItem) -> Self {
138 self.children.push(child);
139 self
140 }
141
142 pub fn children(mut self, children: impl IntoIterator<Item = TreeItem>) -> Self {
144 self.children.extend(children);
145 self
146 }
147
148 pub fn expanded(self, expanded: bool) -> Self {
150 self.state.borrow_mut().expanded = expanded;
151 self
152 }
153
154 pub fn disabled(self, disabled: bool) -> Self {
156 self.state.borrow_mut().disabled = disabled;
157 self
158 }
159
160 #[inline]
162 pub fn is_folder(&self) -> bool {
163 self.children.len() > 0
164 }
165
166 pub fn is_disabled(&self) -> bool {
168 self.state.borrow().disabled
169 }
170
171 #[inline]
173 pub fn is_expanded(&self) -> bool {
174 self.state.borrow().expanded
175 }
176}
177
178pub struct TreeState {
180 focus_handle: FocusHandle,
181 entries: Vec<TreeEntry>,
182 scroll_handle: UniformListScrollHandle,
183 selected_ix: Option<usize>,
184 render_item: Rc<dyn Fn(usize, &TreeEntry, bool, &mut Window, &mut App) -> ListItem>,
185}
186
187impl TreeState {
188 pub fn new(cx: &mut App) -> Self {
190 Self {
191 selected_ix: None,
192 focus_handle: cx.focus_handle(),
193 scroll_handle: UniformListScrollHandle::default(),
194 entries: Vec::new(),
195 render_item: Rc::new(|_, _, _, _, _| ListItem::new(0)),
196 }
197 }
198
199 pub fn items(mut self, items: impl Into<Vec<TreeItem>>) -> Self {
201 let items = items.into();
202 self.entries.clear();
203 for item in items.into_iter() {
204 self.add_entry(item, 0);
205 }
206 self
207 }
208
209 pub fn set_items(&mut self, items: impl Into<Vec<TreeItem>>, cx: &mut Context<Self>) {
211 let items = items.into();
212 self.entries.clear();
213 for item in items.into_iter() {
214 self.add_entry(item, 0);
215 }
216 self.selected_ix = None;
217 cx.notify();
218 }
219
220 pub fn selected_index(&self) -> Option<usize> {
222 self.selected_ix
223 }
224
225 pub fn set_selected_index(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
227 self.selected_ix = ix;
228 cx.notify();
229 }
230
231 pub fn scroll_to_item(&mut self, ix: usize, strategy: gpui::ScrollStrategy) {
232 self.scroll_handle.scroll_to_item(ix, strategy);
233 }
234
235 pub fn selected_entry(&self) -> Option<&TreeEntry> {
237 self.selected_ix.and_then(|ix| self.entries.get(ix))
238 }
239
240 fn add_entry(&mut self, item: TreeItem, depth: usize) {
241 self.entries.push(TreeEntry {
242 item: item.clone(),
243 depth,
244 });
245 if item.is_expanded() {
246 for child in &item.children {
247 self.add_entry(child.clone(), depth + 1);
248 }
249 }
250 }
251
252 fn toggle_expand(&mut self, ix: usize) {
253 let Some(entry) = self.entries.get_mut(ix) else {
254 return;
255 };
256 if !entry.is_folder() {
257 return;
258 }
259
260 entry.item.state.borrow_mut().expanded = !entry.is_expanded();
261 self.rebuild_entries();
262 }
263
264 fn rebuild_entries(&mut self) {
265 let root_items: Vec<TreeItem> = self
266 .entries
267 .iter()
268 .filter(|e| e.is_root())
269 .map(|e| e.item.clone())
270 .collect();
271 self.entries.clear();
272 for item in root_items.into_iter() {
273 self.add_entry(item, 0);
274 }
275 }
276
277 fn on_action_confirm(&mut self, _: &Confirm, _: &mut Window, cx: &mut Context<Self>) {
278 if let Some(selected_ix) = self.selected_ix {
279 if let Some(entry) = self.entries.get(selected_ix) {
280 if entry.is_folder() {
281 self.toggle_expand(selected_ix);
282 cx.notify();
283 }
284 }
285 }
286 }
287
288 fn on_action_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut Context<Self>) {
289 if let Some(selected_ix) = self.selected_ix {
290 if let Some(entry) = self.entries.get(selected_ix) {
291 if entry.is_folder() && entry.is_expanded() {
292 self.toggle_expand(selected_ix);
293 cx.notify();
294 }
295 }
296 }
297 }
298
299 fn on_action_right(&mut self, _: &SelectRight, _: &mut Window, cx: &mut Context<Self>) {
300 if let Some(selected_ix) = self.selected_ix {
301 if let Some(entry) = self.entries.get(selected_ix) {
302 if entry.is_folder() && !entry.is_expanded() {
303 self.toggle_expand(selected_ix);
304 cx.notify();
305 }
306 }
307 }
308 }
309
310 fn on_action_up(&mut self, _: &SelectUp, _: &mut Window, cx: &mut Context<Self>) {
311 let mut selected_ix = self.selected_ix.unwrap_or(0);
312
313 if selected_ix > 0 {
314 selected_ix = selected_ix - 1;
315 } else {
316 selected_ix = self.entries.len().saturating_sub(1);
317 }
318
319 self.selected_ix = Some(selected_ix);
320 self.scroll_handle
321 .scroll_to_item(selected_ix, gpui::ScrollStrategy::Top);
322 cx.notify();
323 }
324
325 fn on_action_down(&mut self, _: &SelectDown, _: &mut Window, cx: &mut Context<Self>) {
326 let mut selected_ix = self.selected_ix.unwrap_or(0);
327 if selected_ix + 1 < self.entries.len() {
328 selected_ix = selected_ix + 1;
329 } else {
330 selected_ix = 0;
331 }
332
333 self.selected_ix = Some(selected_ix);
334 self.scroll_handle
335 .scroll_to_item(selected_ix, gpui::ScrollStrategy::Bottom);
336 cx.notify();
337 }
338
339 fn on_entry_click(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Self>) {
340 self.selected_ix = Some(ix);
341 self.toggle_expand(ix);
342 cx.notify();
343 }
344}
345
346impl Render for TreeState {
347 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
348 let render_item = self.render_item.clone();
349
350 div().id("tree-state").size_full().relative().child(
351 uniform_list("entries", self.entries.len(), {
352 cx.processor(move |state, visible_range: Range<usize>, window, cx| {
353 let mut items = Vec::with_capacity(visible_range.len());
354 for ix in visible_range {
355 let entry = &state.entries[ix];
356 let selected = Some(ix) == state.selected_ix;
357 let item = (render_item)(ix, entry, selected, window, cx);
358
359 let el = div()
360 .id(ix)
361 .child(item.disabled(entry.item().is_disabled()).selected(selected))
362 .when(!entry.item().is_disabled(), |this| {
363 this.on_mouse_down(
364 MouseButton::Left,
365 cx.listener({
366 move |this, _, window, cx| {
367 this.on_entry_click(ix, window, cx);
368 }
369 }),
370 )
371 });
372
373 items.push(el)
374 }
375
376 items
377 })
378 })
379 .flex_grow()
380 .size_full()
381 .track_scroll(self.scroll_handle.clone())
382 .with_sizing_behavior(ListSizingBehavior::Auto)
383 .into_any_element(),
384 )
385 }
386}
387
388#[derive(IntoElement)]
390pub struct Tree {
391 id: ElementId,
392 state: Entity<TreeState>,
393 style: StyleRefinement,
394 render_item: Rc<dyn Fn(usize, &TreeEntry, bool, &mut Window, &mut App) -> ListItem>,
395}
396
397impl Tree {
398 pub fn new<R>(state: &Entity<TreeState>, render_item: R) -> Self
399 where
400 R: Fn(usize, &TreeEntry, bool, &mut Window, &mut App) -> ListItem + 'static,
401 {
402 Self {
403 id: ElementId::Name(format!("tree-{}", state.entity_id()).into()),
404 state: state.clone(),
405 style: StyleRefinement::default(),
406 render_item: Rc::new(move |ix, item, selected, window, app| {
407 render_item(ix, item, selected, window, app)
408 }),
409 }
410 }
411}
412
413impl Styled for Tree {
414 fn style(&mut self) -> &mut StyleRefinement {
415 &mut self.style
416 }
417}
418
419impl RenderOnce for Tree {
420 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
421 let focus_handle = self.state.read(cx).focus_handle.clone();
422 let scroll_handle = self.state.read(cx).scroll_handle.clone();
423
424 self.state
425 .update(cx, |state, _| state.render_item = self.render_item);
426
427 div()
428 .id(self.id)
429 .key_context(CONTEXT)
430 .track_focus(&focus_handle)
431 .on_action(window.listener_for(&self.state, TreeState::on_action_confirm))
432 .on_action(window.listener_for(&self.state, TreeState::on_action_left))
433 .on_action(window.listener_for(&self.state, TreeState::on_action_right))
434 .on_action(window.listener_for(&self.state, TreeState::on_action_up))
435 .on_action(window.listener_for(&self.state, TreeState::on_action_down))
436 .size_full()
437 .child(self.state)
438 .refine_style(&self.style)
439 .vertical_scrollbar(&scroll_handle)
440 }
441}
442
443#[cfg(test)]
444mod tests {
445 use indoc::indoc;
446
447 use super::TreeState;
448 use gpui::AppContext as _;
449
450 fn assert_entries(entries: &Vec<super::TreeEntry>, expected: &str) {
451 let actual: Vec<String> = entries
452 .iter()
453 .map(|e| {
454 let mut s = String::new();
455 s.push_str(&" ".repeat(e.depth));
456 s.push_str(e.item().label.as_str());
457 s
458 })
459 .collect();
460 let actual = actual.join("\n");
461 assert_eq!(actual.trim(), expected.trim());
462 }
463
464 #[gpui::test]
465 fn test_tree_entry(cx: &mut gpui::TestAppContext) {
466 use super::TreeItem;
467
468 let items = vec![
469 TreeItem::new("src", "src")
470 .expanded(true)
471 .child(
472 TreeItem::new("src/ui", "ui")
473 .expanded(true)
474 .child(TreeItem::new("src/ui/button.rs", "button.rs"))
475 .child(TreeItem::new("src/ui/icon.rs", "icon.rs"))
476 .child(TreeItem::new("src/ui/mod.rs", "mod.rs")),
477 )
478 .child(TreeItem::new("src/lib.rs", "lib.rs")),
479 TreeItem::new("Cargo.toml", "Cargo.toml"),
480 TreeItem::new("Cargo.lock", "Cargo.lock").disabled(true),
481 TreeItem::new("README.md", "README.md"),
482 ];
483
484 let state = cx.new(|cx| TreeState::new(cx).items(items));
485 state.update(cx, |state, _| {
486 assert_entries(
487 &state.entries,
488 indoc! {
489 r#"
490 src
491 ui
492 button.rs
493 icon.rs
494 mod.rs
495 lib.rs
496 Cargo.toml
497 Cargo.lock
498 README.md
499 "#
500 },
501 );
502
503 let entry = state.entries.get(0).unwrap();
504 assert_eq!(entry.depth(), 0);
505 assert_eq!(entry.is_root(), true);
506 assert_eq!(entry.is_folder(), true);
507 assert_eq!(entry.is_expanded(), true);
508
509 let entry = state.entries.get(1).unwrap();
510 assert_eq!(entry.depth(), 1);
511 assert_eq!(entry.is_root(), false);
512 assert_eq!(entry.is_folder(), true);
513 assert_eq!(entry.is_expanded(), true);
514 assert_eq!(entry.item().label.as_str(), "ui");
515
516 state.toggle_expand(1);
517 let entry = state.entries.get(1).unwrap();
518 assert_eq!(entry.is_expanded(), false);
519 assert_entries(
520 &state.entries,
521 indoc! {
522 r#"
523 src
524 ui
525 lib.rs
526 Cargo.toml
527 Cargo.lock
528 README.md
529 "#
530 },
531 );
532 })
533 }
534}