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