1use crate::event::{Event, MouseButton, MouseKind};
4use crate::geometry::{Rect, Size, clamp_u16};
5use crate::keymap::Key;
6use crate::style::{CellStyle, WidgetStyle};
7use crate::text;
8use crate::theme::State;
9use crate::widget::{EventCx, MeasureCx, PaintCx, Widget};
10
11use super::click::Click;
12use super::delayed::DelayedIndicator;
13use super::row::{self, LEAD};
14use super::rows::{self, RowScroll, Step};
15use super::select_box;
16use super::{ContextItem, SpinnerStyle, tab_model};
17
18mod drop;
19mod edit;
20#[cfg(test)]
21mod multi_tests;
22mod select;
23#[cfg(test)]
24mod tests;
25
26pub use drop::TreeDrop;
27use drop::{Aim, Dropping};
28use edit::Arrange;
29pub use edit::TreeMove;
30use select::TreeBox;
31
32const INDENT: u16 = 2;
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct TreeNode {
38 key: String,
39 label: String,
40 icon: Option<String>,
41 icon_color: Option<String>,
42 detail: Option<String>,
43 children: Vec<TreeNode>,
44 expandable: bool,
45 expanded: bool,
46 loading: bool,
47 faint: bool,
48}
49
50impl TreeNode {
51 #[must_use]
53 pub fn new(key: impl Into<String>, label: impl Into<String>) -> Self {
54 Self {
55 key: key.into(),
56 label: label.into(),
57 icon: None,
58 icon_color: None,
59 detail: None,
60 children: Vec::new(),
61 expandable: false,
62 expanded: false,
63 loading: false,
64 faint: false,
65 }
66 }
67
68 #[must_use]
70 pub fn children(mut self, children: impl IntoIterator<Item = Self>) -> Self {
71 self.children = children.into_iter().collect();
72 self.expandable = self.expandable || !self.children.is_empty();
73 self
74 }
75
76 #[must_use]
79 pub fn expandable(mut self, expandable: bool) -> Self {
80 self.expandable = expandable || !self.children.is_empty();
81 self
82 }
83
84 #[must_use]
86 pub fn expanded(mut self, expanded: bool) -> Self {
87 self.expanded = expanded;
88 self
89 }
90
91 #[must_use]
95 pub fn loading(mut self, loading: bool) -> Self {
96 self.loading = loading;
97 self
98 }
99
100 #[must_use]
102 pub fn icon(mut self, key: impl Into<String>, color: Option<&str>) -> Self {
103 self.icon = Some(key.into());
104 self.icon_color = color.map(str::to_owned);
105 self
106 }
107
108 #[must_use]
110 pub fn detail(mut self, detail: impl Into<String>) -> Self {
111 self.detail = Some(detail.into());
112 self
113 }
114
115 #[must_use]
117 pub fn faint(mut self, faint: bool) -> Self {
118 self.faint = faint;
119 self
120 }
121}
122
123struct Flat<'a> {
125 node: &'a TreeNode,
126 depth: u16,
127 parent: Option<usize>,
128 index: usize,
130}
131
132#[derive(Debug, Clone, Copy)]
134struct RowFlags {
135 hovered: bool,
136 selected: bool,
137 focused: bool,
138 pressed: bool,
139 spinning: bool,
140 pillar: bool,
143}
144
145#[derive(Debug, Default)]
147struct LoadingMarks(Vec<(String, DelayedIndicator)>);
148
149type KeyMessage<Msg> = Box<dyn Fn(&str) -> Msg>;
151
152type ExpandMessage<Msg> = Box<dyn Fn(&str, bool) -> Msg>;
154
155type MoveMessage<Msg> = Box<dyn Fn(TreeMove) -> Msg>;
157
158type SelectionMessage<Msg> = Box<dyn Fn(Vec<String>) -> Msg>;
160
161type DropMessage<Msg> = Box<dyn Fn(TreeDrop) -> Msg>;
163
164type MenuItems<Msg> = Box<dyn Fn(&str) -> Vec<ContextItem<Msg>>>;
166
167pub struct Tree<Msg> {
219 roots: Vec<TreeNode>,
220 selected: Option<String>,
221 empty: String,
222 on_select: Option<KeyMessage<Msg>>,
223 on_activate: Option<KeyMessage<Msg>>,
224 on_expand: Option<ExpandMessage<Msg>>,
225 on_move: Option<MoveMessage<Msg>>,
226 menu: Option<MenuItems<Msg>>,
227 chosen: Vec<String>,
228 on_choose: Option<SelectionMessage<Msg>>,
229 dropping: Option<Dropping<Msg>>,
230 copy_drop: Option<DropMessage<Msg>>,
231 activate_on: Click,
232 box_select: bool,
233}
234
235impl<Msg: 'static> Tree<Msg> {
236 #[must_use]
238 pub fn new(roots: impl IntoIterator<Item = TreeNode>) -> Self {
239 Self {
240 roots: roots.into_iter().collect(),
241 selected: None,
242 empty: String::new(),
243 on_select: None,
244 on_activate: None,
245 on_expand: None,
246 on_move: None,
247 menu: None,
248 chosen: Vec::new(),
249 on_choose: None,
250 dropping: None,
251 copy_drop: None,
252 activate_on: Click::Single,
253 box_select: false,
254 }
255 }
256
257 #[must_use]
259 pub fn selected(mut self, key: Option<&str>) -> Self {
260 self.selected = key.map(str::to_owned);
261 self
262 }
263
264 #[must_use]
275 pub fn multi_select(mut self, selected: &[String], message: impl Fn(Vec<String>) -> Msg + 'static) -> Self {
276 self.chosen = selected.to_vec();
277 self.on_choose = Some(Box::new(message));
278 self
279 }
280
281 #[must_use]
283 pub fn empty_text(mut self, text: impl Into<String>) -> Self {
284 self.empty = text.into();
285 self
286 }
287
288 #[must_use]
290 pub fn on_select(mut self, message: impl Fn(&str) -> Msg + 'static) -> Self {
291 self.on_select = Some(Box::new(message));
292 self
293 }
294
295 #[must_use]
297 pub fn on_activate(mut self, message: impl Fn(&str) -> Msg + 'static) -> Self {
298 self.on_activate = Some(Box::new(message));
299 self
300 }
301
302 #[must_use]
304 pub fn on_expand(mut self, message: impl Fn(&str, bool) -> Msg + 'static) -> Self {
305 self.on_expand = Some(Box::new(message));
306 self
307 }
308
309 #[must_use]
312 pub fn reorderable(mut self, message: impl Fn(TreeMove) -> Msg + 'static) -> Self {
313 self.on_move = Some(Box::new(message));
314 self
315 }
316
317 #[must_use]
331 pub fn droppable(
332 mut self,
333 message: impl Fn(TreeDrop) -> Msg + 'static,
334 accepts: impl Fn(&str) -> bool + 'static,
335 ) -> Self {
336 self.dropping = Some(Dropping::new(message, accepts));
337 self
338 }
339
340 #[must_use]
344 pub fn on_copy_drop(mut self, message: impl Fn(TreeDrop) -> Msg + 'static) -> Self {
345 self.copy_drop = Some(Box::new(message));
346 self
347 }
348
349 #[must_use]
354 pub fn activate_on(mut self, click: Click) -> Self {
355 self.activate_on = click;
356 self
357 }
358
359 #[must_use]
364 pub fn box_select(mut self, on: bool) -> Self {
365 self.box_select = on;
366 self
367 }
368
369 #[must_use]
372 pub fn context_menu(mut self, items: impl Fn(&str) -> Vec<ContextItem<Msg>> + 'static) -> Self {
373 self.menu = Some(Box::new(items));
374 self
375 }
376
377 fn flatten(&self) -> Vec<Flat<'_>> {
378 self.flatten_with(None)
379 }
380
381 fn flatten_with(&self, arrange: Option<&Arrange<'_>>) -> Vec<Flat<'_>> {
383 fn walk<'a>(
384 nodes: &'a [TreeNode],
385 parent_key: Option<&str>,
386 (depth, parent): (u16, Option<usize>),
387 arrange: Option<&Arrange<'_>>,
388 out: &mut Vec<Flat<'a>>,
389 ) {
390 let preview = arrange.filter(|arrange| arrange.parent == parent_key).and_then(|arrange| arrange.order);
391 for index in tab_model::preview_order(nodes.len(), preview) {
392 let node = &nodes[index];
393 let at = out.len();
394 out.push(Flat { node, depth, parent, index });
395 let folded = arrange.is_some_and(|arrange| arrange.key == node.key);
396 if node.expanded && !folded {
397 walk(&node.children, Some(&node.key), (depth.saturating_add(1), Some(at)), arrange, out);
398 }
399 }
400 }
401 let mut out = Vec::new();
402 walk(&self.roots, None, (0, None), arrange, &mut out);
403 out
404 }
405
406 fn selected_index(&self, flat: &[Flat<'_>]) -> Option<usize> {
407 let key = self.selected.as_deref()?;
408 flat.iter().position(|row| row.node.key == key)
409 }
410
411 fn select(&self, cx: &mut EventCx<'_, Msg>, flat: &[Flat<'_>], index: usize) {
412 let Some(row) = flat.get(index) else { return };
413 if self.selected.as_deref() != Some(row.node.key.as_str())
414 && let Some(message) = &self.on_select
415 {
416 cx.emit(message(&row.node.key));
417 }
418 }
419
420 fn expand(&self, cx: &mut EventCx<'_, Msg>, node: &TreeNode, open: bool) -> bool {
421 match &self.on_expand {
422 Some(message) if node.expandable && node.expanded != open => {
423 cx.emit(message(&node.key, open));
424 true
425 }
426 _ => false,
427 }
428 }
429
430 fn activate(&self, cx: &mut EventCx<'_, Msg>, index: usize, node: &TreeNode) -> bool {
431 let Some(message) = &self.on_activate else {
432 return false;
433 };
434 cx.memory::<RowScroll>().flashed = Some(index);
435 cx.flash();
436 cx.emit(message(&node.key));
437 true
438 }
439
440 fn open_or_activate(&self, cx: &mut EventCx<'_, Msg>, index: usize, node: &TreeNode) -> bool {
442 if node.expandable { self.expand(cx, node, !node.expanded) } else { self.activate(cx, index, node) }
443 }
444
445 fn loading_marks(cx: &mut PaintCx<'_>, flat: &[Flat<'_>]) -> Vec<usize> {
448 let now = cx.now();
449 let mut marks = std::mem::take(&mut cx.memory::<LoadingMarks>().0);
450 let mut kept = Vec::new();
451 let mut spinning = Vec::new();
452 let mut next: Option<std::time::Duration> = None;
453 for (index, row) in flat.iter().enumerate() {
454 let node = row.node;
455 let known = marks.iter().position(|(key, _)| *key == node.key);
456 if !node.expandable || (!node.loading && known.is_none()) {
457 continue;
458 }
459 let mut mark = known.map(|at| marks.swap_remove(at).1).unwrap_or_default();
460 if mark.update(node.loading, now) {
461 spinning.push(index);
462 }
463 if let Some(change) = mark.next_change(node.loading, now) {
464 next = Some(next.map_or(change, |soonest| soonest.min(change)));
465 }
466 if !mark.is_idle() {
467 kept.push((node.key.clone(), mark));
468 }
469 }
470 if let Some(delay) = next {
471 cx.request_frame_in(delay);
472 }
473 cx.memory::<LoadingMarks>().0 = kept;
474 spinning
475 }
476
477 fn paint_target(cx: &mut PaintCx<'_>, rect: Rect, row: &Flat<'_>, aim: &Aim) -> bool {
480 let (widget, variant) = match aim {
481 Aim::Into(Some(key)) if *key == row.node.key => ("tree-drop", None),
482 Aim::Refused(key) if *key == row.node.key => ("list-item", Some("faint")),
483 _ => return false,
484 };
485 let style = cx.style(widget, variant, &[]);
486 Self::paint_node(cx, rect, row, (&style, &[]), false, false);
487 true
488 }
489
490 fn chevron_x(area: Rect, depth: u16) -> i32 {
492 area.x + i32::from(LEAD) + i32::from(depth.saturating_mul(INDENT))
493 }
494
495 fn paint_row(&self, cx: &mut PaintCx<'_>, rect: Rect, index: usize, row: &Flat<'_>, flags: RowFlags) {
496 let flashed = cx.memory::<RowScroll>().flashed == Some(index);
497 let states = rows::row_states(flags.hovered, flags.selected, flags.focused, flags.pressed && flashed);
498 let style = cx.style("list-item", row.node.faint.then_some("faint"), &states);
499 let slide = flags.pillar && rows::slide(cx, &states) > 0;
502 let style = if flags.pillar { style } else { style.without("pillar") };
503 Self::paint_node(cx, rect, row, (&style, &states), flags.spinning, slide);
504 }
505
506 fn paint_node(
508 cx: &mut PaintCx<'_>,
509 rect: Rect,
510 row: &Flat<'_>,
511 (style, states): (&WidgetStyle, &[State]),
512 spinning: bool,
513 slide: bool,
514 ) {
515 let node = row.node;
516 let text_style = style.text();
517 let detail_width = node.detail.as_deref().map_or(0, |d| text::width(d).saturating_add(2));
518
519 let chevron = if !node.expandable {
522 (" ".to_owned(), CellStyle::default())
523 } else if spinning {
524 let style = cx.style("spinner", None, &[]).text();
525 let cell = cx.animation(SpinnerStyle::Dots.animation(), style, Some(std::time::Duration::ZERO));
526 (text::truncate(&cell.glyph, 1).into_owned(), cell.style)
527 } else {
528 let key = if node.expanded { "tree-expanded" } else { "tree-collapsed" };
529 let glyph = text::truncate(&cx.env().icons().glyph(key), 1).into_owned();
530 (glyph, cx.style("tree-chevron", None, states).text())
531 };
532 let icon: Vec<row::Mark> =
533 node.icon.iter().map(|key| row::icon(cx, key, node.icon_color.as_deref(), text_style.fg)).collect();
534 let parts = row::Parts {
535 indent: row.depth.saturating_mul(INDENT),
536 fixed: &[chevron],
537 sliding: &icon,
538 label: &node.label,
539 trailing: detail_width,
540 };
541 row::paint_parts(cx, rect, style, slide, &parts);
542 if let Some(detail) = &node.detail {
543 let detail_style = cx.style("list-detail", None, states).text();
544 row::paint_trailing(cx, rect, detail, detail_style);
545 }
546 }
547}
548
549impl<Msg: 'static> Widget<Msg> for Tree<Msg> {
550 fn measure(&self, _cx: &mut MeasureCx<'_>, available: Size) -> Size {
551 let flat = self.flatten();
552 let widest = flat
553 .iter()
554 .map(|row| {
555 [
557 LEAD,
558 row.depth.saturating_mul(INDENT),
559 2,
560 row.node.icon.as_ref().map_or(0, |_| 2),
561 text::width(&row.node.label),
562 row.node.detail.as_deref().map_or(0, |d| text::width(d).saturating_add(2)),
563 2,
564 ]
565 .into_iter()
566 .fold(0, u16::saturating_add)
567 })
568 .max()
569 .unwrap_or_else(|| text::width(&self.empty).saturating_add(LEAD));
570 let rows = clamp_u16(i32::try_from(flat.len().max(1)).unwrap_or(i32::MAX));
571 Size::new(widest, rows).min(available)
572 }
573
574 fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
575 cx.register_hit(area);
576 if self.roots.is_empty() {
577 let faint = cx.style("list-header", None, &[]).text();
578 cx.text(area.x + i32::from(LEAD), area.y, &self.empty, faint, area.width.saturating_sub(LEAD));
579 return;
580 }
581 let drag = self.drag(cx);
582 let menu_node = self.menu_node(cx);
585 if menu_node.is_some() {
586 cx.request_overlay(area);
587 }
588 let aim = drag.as_ref().map(|drag| {
589 let offset = cx.memory::<RowScroll>().offset;
590 self.aim(&drag.keys, drag.pointer, Self::rows_area(area, self.flatten().len()), offset)
591 });
592 let flat = match (&drag, &aim) {
593 (Some(drag), Some(Aim::Reorder(order))) => {
594 let parent = self.siblings(&drag.key).and_then(|(parent, _)| parent);
595 self.flatten_with(Some(&Arrange { key: &drag.key, parent, order: *order }))
596 }
597 (Some(drag), _) => self.drag_layout(&drag.keys),
598 (None, _) => self.flatten(),
599 };
600 let slot = drag.as_ref().filter(|drag| self.reorders(&drag.keys)).map(|drag| drag.key.as_str());
603 let focused = cx.is_focused();
604 let pressed = cx.is_pressed();
605 let selected = self.selected_index(&flat);
606 let visible = usize::from(area.height);
607 let offset = cx.memory::<RowScroll>().follow(selected, flat.len(), visible);
608 let width = Self::rows_area(area, flat.len()).width;
609 let spinning = Self::loading_marks(cx, &flat);
610 let pointer = cx.pointer().filter(|_| drag.is_none() && menu_node.is_none());
611 for (row, index) in (offset..flat.len()).take(visible).enumerate() {
612 let rect = Rect::new(area.x, area.y + i32::try_from(row).unwrap_or(0), width, 1);
613 let key = flat[index].node.key.as_str();
614 if slot == Some(key) {
615 tab_model::paint_drop_slot(cx, rect);
616 continue;
617 }
618 if let Some(aim) = &aim
619 && Self::paint_target(cx, rect, &flat[index], aim)
620 {
621 continue;
622 }
623 let touched = pointer.is_some_and(|(x, y)| rect.contains(x, y)) || menu_node.as_deref() == Some(key);
624 let cursor = selected == Some(index);
625 let chosen = self.is_chosen(key);
626 let hovered = touched || (cursor && !chosen);
629 let flags = RowFlags {
630 hovered,
631 selected: chosen,
632 focused: focused && cursor,
633 pressed,
634 spinning: spinning.contains(&index),
635 pillar: cursor || hovered || !self.is_multi(),
636 };
637 self.paint_row(cx, rect, index, &flat[index], flags);
638 }
639 if aim == Some(Aim::Into(None)) {
640 let used = i32::try_from(flat.len().saturating_sub(offset)).unwrap_or(i32::MAX);
642 let top = area.y.saturating_add(used);
643 if top < area.bottom() {
644 let free = Rect::new(area.x, top, width, clamp_u16(area.bottom() - top));
645 let bg = cx.style("tree-drop", None, &[]).text().bg;
646 if let Some(bg) = bg {
647 cx.fill(free, bg);
648 }
649 }
650 }
651 if let Some(drawn) = cx.memory::<TreeBox>().drawn() {
652 select_box::paint(cx, drawn, Self::rows_area(area, flat.len()));
653 }
654 if let Some(drag) = &drag
656 && matches!(aim, Some(Aim::Reorder(_)))
657 && let Some(row) = flat.iter().find(|row| row.node.key == drag.key)
658 && !area.is_empty()
659 {
660 let y = drag.pointer.1.clamp(area.y, area.bottom() - 1);
661 let rect = Rect::new(area.x, y, width, 1);
662 tab_model::paint_ghost_surface(cx, rect);
664 let ghost = cx.style("tab-ghost", None, &[]);
665 Self::paint_node(cx, rect, row, (&ghost, &[]), false, false);
666 }
667 rows::paint_scrollbar(cx, area, flat.len(), offset, None);
668 }
669
670 fn paint_overlay(&self, cx: &mut PaintCx<'_>, anchor: Rect) {
671 self.paint_menu(cx, anchor);
672 }
673
674 fn event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
675 let area = cx.area();
676 let flat = self.flatten();
677 if self.menu_event(cx, event, &flat) {
678 return true;
679 }
680 let current = self.selected_index(&flat);
681 match event {
682 Event::Key(key) => {
683 if self.move_key(cx, key) || self.selection_key(cx, key, &flat) {
684 return true;
685 }
686 if let Some(step) = Step::from_key(key) {
687 let Some(target) = step.apply(current, flat.len(), usize::from(area.height)) else {
688 return false;
689 };
690 self.select_one(cx, &flat, target);
691 return true;
692 }
693 let Some(index) = current else { return false };
694 let row = &flat[index];
695 if key.is_plain(Key::Right) || key.is_plain(Key::Char('l')) {
696 if !row.node.expanded {
697 return self.expand(cx, row.node, true);
698 }
699 if !row.node.children.is_empty() {
700 self.select_one(cx, &flat, index + 1);
701 return true;
702 }
703 return false;
704 }
705 if key.is_plain(Key::Left) || key.is_plain(Key::Char('h')) {
706 if row.node.expanded {
707 return self.expand(cx, row.node, false);
708 }
709 return row.parent.is_some_and(|parent| {
710 self.select_one(cx, &flat, parent);
711 true
712 });
713 }
714 if key.is_plain(Key::Enter) {
715 return self.open_or_activate(cx, index, row.node);
716 }
717 if key.is_plain(Key::Space) {
718 return self.activate(cx, index, row.node);
719 }
720 false
721 }
722 Event::Mouse(mouse) => {
723 if rows::scroll_mouse(cx, mouse, area, flat.len()) {
724 return true;
725 }
726 let offset = cx.memory::<RowScroll>().offset;
727 let index = usize::try_from(mouse.y - area.y).ok().map(|r| offset + r).filter(|i| *i < flat.len());
728 let on_chevron = index.is_some_and(|index| {
729 let row = &flat[index];
730 let chevron = Self::chevron_x(area, row.depth);
731 row.node.expandable && (chevron..=chevron + 1).contains(&mouse.x)
732 });
733 if let Some(used) = self.box_pointer(cx, mouse, &flat, index) {
734 return used;
735 }
736 if (self.on_move.is_some() || self.dropping.is_some())
737 && !on_chevron
738 && let Some(used) = self.drag_pointer(cx, mouse, &flat, index)
739 {
740 return used;
741 }
742 if mouse.kind != MouseKind::Down(MouseButton::Left) {
743 return false;
744 }
745 let Some(index) = index else {
746 return false;
747 };
748 let row = &flat[index];
749 if on_chevron {
750 return self.expand(cx, row.node, !row.node.expanded);
751 }
752 if self.modified_press(cx, &flat, index, mouse.mods) {
753 return true;
754 }
755 self.select_one(cx, &flat, index);
756 if self.activate_on == Click::Single || self.double_press(cx, &row.node.key) {
757 self.open_or_activate(cx, index, row.node);
758 }
759 true
760 }
761 _ => false,
762 }
763 }
764
765 fn focusable(&self) -> bool {
766 !self.roots.is_empty()
767 }
768}