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