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 chevrons: bool,
133}
134
135#[derive(Debug, Clone, Copy)]
137struct RowFlags {
138 hovered: bool,
139 selected: bool,
140 focused: bool,
141 pressed: bool,
142 spinning: bool,
143 pillar: bool,
146}
147
148#[derive(Debug, Default)]
150struct LoadingMarks(Vec<(String, DelayedIndicator)>);
151
152type KeyMessage<Msg> = Box<dyn Fn(&str) -> Msg>;
154
155type ExpandMessage<Msg> = Box<dyn Fn(&str, bool) -> Msg>;
157
158type MoveMessage<Msg> = Box<dyn Fn(TreeMove) -> Msg>;
160
161type SelectionMessage<Msg> = Box<dyn Fn(Vec<String>) -> Msg>;
163
164type DropMessage<Msg> = Box<dyn Fn(TreeDrop) -> Msg>;
166
167type MenuItems<Msg> = Box<dyn Fn(&str) -> Vec<ContextItem<Msg>>>;
169
170pub struct Tree<Msg> {
223 roots: Vec<TreeNode>,
224 selected: Option<String>,
225 empty: String,
226 on_select: Option<KeyMessage<Msg>>,
227 on_activate: Option<KeyMessage<Msg>>,
228 on_expand: Option<ExpandMessage<Msg>>,
229 on_move: Option<MoveMessage<Msg>>,
230 menu: Option<MenuItems<Msg>>,
231 chosen: Vec<String>,
232 on_choose: Option<SelectionMessage<Msg>>,
233 dropping: Option<Dropping<Msg>>,
234 copy_drop: Option<DropMessage<Msg>>,
235 activate_on: Click,
236 box_select: bool,
237}
238
239impl<Msg: 'static> Tree<Msg> {
240 #[must_use]
242 pub fn new(roots: impl IntoIterator<Item = TreeNode>) -> Self {
243 Self {
244 roots: roots.into_iter().collect(),
245 selected: None,
246 empty: String::new(),
247 on_select: None,
248 on_activate: None,
249 on_expand: None,
250 on_move: None,
251 menu: None,
252 chosen: Vec::new(),
253 on_choose: None,
254 dropping: None,
255 copy_drop: None,
256 activate_on: Click::Single,
257 box_select: false,
258 }
259 }
260
261 #[must_use]
263 pub fn selected(mut self, key: Option<&str>) -> Self {
264 self.selected = key.map(str::to_owned);
265 self
266 }
267
268 #[must_use]
279 pub fn multi_select(mut self, selected: &[String], message: impl Fn(Vec<String>) -> Msg + 'static) -> Self {
280 self.chosen = selected.to_vec();
281 self.on_choose = Some(Box::new(message));
282 self
283 }
284
285 #[must_use]
287 pub fn empty_text(mut self, text: impl Into<String>) -> Self {
288 self.empty = text.into();
289 self
290 }
291
292 #[must_use]
294 pub fn on_select(mut self, message: impl Fn(&str) -> Msg + 'static) -> Self {
295 self.on_select = Some(Box::new(message));
296 self
297 }
298
299 #[must_use]
301 pub fn on_activate(mut self, message: impl Fn(&str) -> Msg + 'static) -> Self {
302 self.on_activate = Some(Box::new(message));
303 self
304 }
305
306 #[must_use]
308 pub fn on_expand(mut self, message: impl Fn(&str, bool) -> Msg + 'static) -> Self {
309 self.on_expand = Some(Box::new(message));
310 self
311 }
312
313 #[must_use]
316 pub fn reorderable(mut self, message: impl Fn(TreeMove) -> Msg + 'static) -> Self {
317 self.on_move = Some(Box::new(message));
318 self
319 }
320
321 #[must_use]
335 pub fn droppable(
336 mut self,
337 message: impl Fn(TreeDrop) -> Msg + 'static,
338 accepts: impl Fn(&str) -> bool + 'static,
339 ) -> Self {
340 self.dropping = Some(Dropping::new(message, accepts));
341 self
342 }
343
344 #[must_use]
348 pub fn on_copy_drop(mut self, message: impl Fn(TreeDrop) -> Msg + 'static) -> Self {
349 self.copy_drop = Some(Box::new(message));
350 self
351 }
352
353 #[must_use]
358 pub fn activate_on(mut self, click: Click) -> Self {
359 self.activate_on = click;
360 self
361 }
362
363 #[must_use]
368 pub fn box_select(mut self, on: bool) -> Self {
369 self.box_select = on;
370 self
371 }
372
373 #[must_use]
376 pub fn context_menu(mut self, items: impl Fn(&str) -> Vec<ContextItem<Msg>> + 'static) -> Self {
377 self.menu = Some(Box::new(items));
378 self
379 }
380
381 fn flatten(&self) -> Vec<Flat<'_>> {
382 self.flatten_with(None)
383 }
384
385 fn flatten_with(&self, arrange: Option<&Arrange<'_>>) -> Vec<Flat<'_>> {
387 fn walk<'a>(
388 nodes: &'a [TreeNode],
389 parent_key: Option<&str>,
390 (depth, parent): (u16, Option<usize>),
391 arrange: Option<&Arrange<'_>>,
392 out: &mut Vec<Flat<'a>>,
393 ) {
394 let preview = arrange.filter(|arrange| arrange.parent == parent_key).and_then(|arrange| arrange.order);
395 for index in tab_model::preview_order(nodes.len(), preview) {
396 let node = &nodes[index];
397 let at = out.len();
398 out.push(Flat { node, depth, parent, index, chevrons: true });
399 let folded = arrange.is_some_and(|arrange| arrange.key == node.key);
400 if node.expanded && !folded {
401 walk(&node.children, Some(&node.key), (depth.saturating_add(1), Some(at)), arrange, out);
402 }
403 }
404 }
405 let mut out = Vec::new();
406 walk(&self.roots, None, (0, None), arrange, &mut out);
407 if !out.iter().any(|row| row.node.expandable) {
409 for row in &mut out {
410 row.chevrons = false;
411 }
412 }
413 out
414 }
415
416 fn selected_index(&self, flat: &[Flat<'_>]) -> Option<usize> {
417 let key = self.selected.as_deref()?;
418 flat.iter().position(|row| row.node.key == key)
419 }
420
421 fn select(&self, cx: &mut EventCx<'_, Msg>, flat: &[Flat<'_>], index: usize) {
422 let Some(row) = flat.get(index) else { return };
423 if self.selected.as_deref() != Some(row.node.key.as_str())
424 && let Some(message) = &self.on_select
425 {
426 cx.emit(message(&row.node.key));
427 }
428 }
429
430 fn expand(&self, cx: &mut EventCx<'_, Msg>, node: &TreeNode, open: bool) -> bool {
431 match &self.on_expand {
432 Some(message) if node.expandable && node.expanded != open => {
433 cx.emit(message(&node.key, open));
434 true
435 }
436 _ => false,
437 }
438 }
439
440 fn activate(&self, cx: &mut EventCx<'_, Msg>, index: usize, node: &TreeNode) -> bool {
441 let Some(message) = &self.on_activate else {
442 return false;
443 };
444 cx.memory::<RowScroll>().flashed = Some(index);
445 cx.flash();
446 cx.emit(message(&node.key));
447 true
448 }
449
450 fn open_or_activate(&self, cx: &mut EventCx<'_, Msg>, index: usize, node: &TreeNode) -> bool {
452 if node.expandable { self.expand(cx, node, !node.expanded) } else { self.activate(cx, index, node) }
453 }
454
455 fn loading_marks(cx: &mut PaintCx<'_>, flat: &[Flat<'_>]) -> Vec<usize> {
458 let now = cx.now();
459 let mut marks = std::mem::take(&mut cx.memory::<LoadingMarks>().0);
460 let mut kept = Vec::new();
461 let mut spinning = Vec::new();
462 let mut next: Option<std::time::Duration> = None;
463 for (index, row) in flat.iter().enumerate() {
464 let node = row.node;
465 let known = marks.iter().position(|(key, _)| *key == node.key);
466 if !node.expandable || (!node.loading && known.is_none()) {
467 continue;
468 }
469 let mut mark = known.map(|at| marks.swap_remove(at).1).unwrap_or_default();
470 if mark.update(node.loading, now) {
471 spinning.push(index);
472 }
473 if let Some(change) = mark.next_change(node.loading, now) {
474 next = Some(next.map_or(change, |soonest| soonest.min(change)));
475 }
476 if !mark.is_idle() {
477 kept.push((node.key.clone(), mark));
478 }
479 }
480 if let Some(delay) = next {
481 cx.request_frame_in(delay);
482 }
483 cx.memory::<LoadingMarks>().0 = kept;
484 spinning
485 }
486
487 fn paint_target(cx: &mut PaintCx<'_>, rect: Rect, row: &Flat<'_>, aim: &Aim) -> bool {
490 let (widget, variant) = match aim {
491 Aim::Into(Some(key)) if *key == row.node.key => ("tree-drop", None),
492 Aim::Refused(key) if *key == row.node.key => ("list-item", Some("faint")),
493 _ => return false,
494 };
495 let style = cx.style(widget, variant, &[]);
496 Self::paint_node(cx, rect, row, (&style, &[]), false, false);
497 true
498 }
499
500 fn chevron_x(area: Rect, depth: u16) -> i32 {
502 area.x + i32::from(LEAD) + i32::from(depth.saturating_mul(INDENT))
503 }
504
505 fn paint_row(&self, cx: &mut PaintCx<'_>, rect: Rect, index: usize, row: &Flat<'_>, flags: RowFlags) {
506 let flashed = cx.memory::<RowScroll>().flashed == Some(index);
507 let states = rows::row_states(flags.hovered, flags.selected, flags.focused, flags.pressed && flashed);
508 let style = cx.style("list-item", row.node.faint.then_some("faint"), &states);
509 let slide = flags.pillar && rows::slide(cx, &states) > 0;
512 let style = if flags.pillar { style } else { style.without("pillar") };
513 Self::paint_node(cx, rect, row, (&style, &states), flags.spinning, slide);
514 }
515
516 fn paint_node(
518 cx: &mut PaintCx<'_>,
519 rect: Rect,
520 row: &Flat<'_>,
521 (style, states): (&WidgetStyle, &[State]),
522 spinning: bool,
523 slide: bool,
524 ) {
525 let node = row.node;
526 let text_style = style.text();
527 let detail_width = node.detail.as_deref().map_or(0, |d| text::width(d).saturating_add(2));
528
529 let chevron = if !node.expandable {
532 (" ".to_owned(), CellStyle::default())
533 } else if spinning {
534 let style = cx.style("spinner", None, &[]).text();
535 let cell = cx.animation(SpinnerStyle::Dots.animation(), style, Some(std::time::Duration::ZERO));
536 (text::truncate(&cell.glyph, 1).into_owned(), cell.style)
537 } else {
538 let key = if node.expanded { "tree-expanded" } else { "tree-collapsed" };
539 let glyph = text::truncate(&cx.env().icons().glyph(key), 1).into_owned();
540 (glyph, cx.style("tree-chevron", None, states).text())
541 };
542 let icon: Vec<row::Mark> =
543 node.icon.iter().map(|key| row::icon(cx, key, node.icon_color.as_deref(), text_style.fg)).collect();
544 let chevrons = [chevron];
545 let parts = row::Parts {
546 indent: row.depth.saturating_mul(INDENT),
547 fixed: if row.chevrons { &chevrons } else { &[] },
548 sliding: &icon,
549 label: &node.label,
550 trailing: detail_width,
551 };
552 row::paint_parts(cx, rect, style, slide, &parts);
553 if let Some(detail) = &node.detail {
554 let detail_style = cx.style("list-detail", None, states).text();
555 row::paint_trailing(cx, rect, detail, detail_style);
556 }
557 }
558}
559
560impl<Msg: 'static> Widget<Msg> for Tree<Msg> {
561 fn measure(&self, _cx: &mut MeasureCx<'_>, available: Size) -> Size {
562 let flat = self.flatten();
563 let widest = flat
564 .iter()
565 .map(|row| {
566 [
568 LEAD,
569 row.depth.saturating_mul(INDENT),
570 if row.chevrons { 2 } else { 0 },
571 row.node.icon.as_ref().map_or(0, |_| 2),
572 text::width(&row.node.label),
573 row.node.detail.as_deref().map_or(0, |d| text::width(d).saturating_add(2)),
574 2,
575 ]
576 .into_iter()
577 .fold(0, u16::saturating_add)
578 })
579 .max()
580 .unwrap_or_else(|| text::width(&self.empty).saturating_add(LEAD));
581 let rows = clamp_u16(i32::try_from(flat.len().max(1)).unwrap_or(i32::MAX));
582 Size::new(widest, rows).min(available)
583 }
584
585 fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
586 cx.register_hit(area);
587 if self.roots.is_empty() {
588 let faint = cx.style("list-header", None, &[]).text();
589 cx.text(area.x + i32::from(LEAD), area.y, &self.empty, faint, area.width.saturating_sub(LEAD));
590 return;
591 }
592 let drag = self.drag(cx);
593 let menu_node = self.menu_node(cx);
596 if menu_node.is_some() {
597 cx.request_overlay(area);
598 }
599 let aim = drag.as_ref().map(|drag| {
600 let offset = cx.memory::<RowScroll>().offset;
601 self.aim(&drag.keys, drag.pointer, Self::rows_area(area, self.flatten().len()), offset)
602 });
603 let flat = match (&drag, &aim) {
604 (Some(drag), Some(Aim::Reorder(order))) => {
605 let parent = self.siblings(&drag.key).and_then(|(parent, _)| parent);
606 self.flatten_with(Some(&Arrange { key: &drag.key, parent, order: *order }))
607 }
608 (Some(drag), _) => self.drag_layout(&drag.keys),
609 (None, _) => self.flatten(),
610 };
611 let slot = drag.as_ref().filter(|drag| self.reorders(&drag.keys)).map(|drag| drag.key.as_str());
614 let focused = cx.is_focused();
615 let pressed = cx.is_pressed();
616 let selected = self.selected_index(&flat);
617 let visible = usize::from(area.height);
618 let offset = cx.memory::<RowScroll>().follow(selected, flat.len(), visible);
619 let width = Self::rows_area(area, flat.len()).width;
620 let spinning = Self::loading_marks(cx, &flat);
621 let pointer = cx.pointer().filter(|_| drag.is_none() && menu_node.is_none());
622 for (row, index) in (offset..flat.len()).take(visible).enumerate() {
623 let rect = Rect::new(area.x, area.y + i32::try_from(row).unwrap_or(0), width, 1);
624 let key = flat[index].node.key.as_str();
625 if slot == Some(key) {
626 tab_model::paint_drop_slot(cx, rect);
627 continue;
628 }
629 if let Some(aim) = &aim
630 && Self::paint_target(cx, rect, &flat[index], aim)
631 {
632 continue;
633 }
634 let touched = pointer.is_some_and(|(x, y)| rect.contains(x, y)) || menu_node.as_deref() == Some(key);
635 let cursor = selected == Some(index);
636 let chosen = self.is_chosen(key);
637 let hovered = touched || (cursor && !chosen);
640 let flags = RowFlags {
641 hovered,
642 selected: chosen,
643 focused: focused && cursor,
644 pressed,
645 spinning: spinning.contains(&index),
646 pillar: cursor || hovered || !self.is_multi(),
647 };
648 self.paint_row(cx, rect, index, &flat[index], flags);
649 }
650 if aim == Some(Aim::Into(None)) {
651 let used = i32::try_from(flat.len().saturating_sub(offset)).unwrap_or(i32::MAX);
653 let top = area.y.saturating_add(used);
654 if top < area.bottom() {
655 let free = Rect::new(area.x, top, width, clamp_u16(area.bottom() - top));
656 let bg = cx.style("tree-drop", None, &[]).text().bg;
657 if let Some(bg) = bg {
658 cx.fill(free, bg);
659 }
660 }
661 }
662 if let Some(drawn) = cx.memory::<TreeBox>().drawn() {
663 select_box::paint(cx, drawn, Self::rows_area(area, flat.len()));
664 }
665 if let Some(drag) = &drag
667 && matches!(aim, Some(Aim::Reorder(_)))
668 && let Some(row) = flat.iter().find(|row| row.node.key == drag.key)
669 && !area.is_empty()
670 {
671 let y = drag.pointer.1.clamp(area.y, area.bottom() - 1);
672 let rect = Rect::new(area.x, y, width, 1);
673 tab_model::paint_ghost_surface(cx, rect);
675 let ghost = cx.style("tab-ghost", None, &[]);
676 Self::paint_node(cx, rect, row, (&ghost, &[]), false, false);
677 }
678 rows::paint_scrollbar(cx, area, flat.len(), offset, None);
679 }
680
681 fn paint_overlay(&self, cx: &mut PaintCx<'_>, anchor: Rect) {
682 self.paint_menu(cx, anchor);
683 }
684
685 fn event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
686 let area = cx.area();
687 let flat = self.flatten();
688 if self.menu_event(cx, event, &flat) {
689 return true;
690 }
691 let current = self.selected_index(&flat);
692 match event {
693 Event::Key(key) => {
694 if self.move_key(cx, key) || self.selection_key(cx, key, &flat) {
695 return true;
696 }
697 if let Some(step) = Step::from_key(key) {
698 let Some(target) = step.apply(current, flat.len(), usize::from(area.height)) else {
699 return false;
700 };
701 self.select_one(cx, &flat, target);
702 return true;
703 }
704 let Some(index) = current else { return false };
705 let row = &flat[index];
706 if key.is_plain(Key::Right) || key.is_plain(Key::Char('l')) {
707 if !row.node.expanded {
708 return self.expand(cx, row.node, true);
709 }
710 if !row.node.children.is_empty() {
711 self.select_one(cx, &flat, index + 1);
712 return true;
713 }
714 return false;
715 }
716 if key.is_plain(Key::Left) || key.is_plain(Key::Char('h')) {
717 if row.node.expanded {
718 return self.expand(cx, row.node, false);
719 }
720 return row.parent.is_some_and(|parent| {
721 self.select_one(cx, &flat, parent);
722 true
723 });
724 }
725 if key.is_plain(Key::Enter) {
726 return self.open_or_activate(cx, index, row.node);
727 }
728 if key.is_plain(Key::Space) {
729 return self.activate(cx, index, row.node);
730 }
731 false
732 }
733 Event::Mouse(mouse) => {
734 if rows::scroll_mouse(cx, mouse, area, flat.len()) {
735 return true;
736 }
737 let offset = cx.memory::<RowScroll>().offset;
738 let index = usize::try_from(mouse.y - area.y).ok().map(|r| offset + r).filter(|i| *i < flat.len());
739 let on_chevron = index.is_some_and(|index| {
740 let row = &flat[index];
741 let chevron = Self::chevron_x(area, row.depth);
742 row.node.expandable && (chevron..=chevron + 1).contains(&mouse.x)
743 });
744 if let Some(used) = self.box_pointer(cx, mouse, &flat, index) {
745 return used;
746 }
747 if (self.on_move.is_some() || self.dropping.is_some())
748 && !on_chevron
749 && let Some(used) = self.drag_pointer(cx, mouse, &flat, index)
750 {
751 return used;
752 }
753 if mouse.kind != MouseKind::Down(MouseButton::Left) {
754 return false;
755 }
756 let Some(index) = index else {
757 return false;
758 };
759 let row = &flat[index];
760 if on_chevron {
761 return self.expand(cx, row.node, !row.node.expanded);
762 }
763 if self.modified_press(cx, &flat, index, mouse.mods) {
764 return true;
765 }
766 self.select_one(cx, &flat, index);
767 if self.activate_on == Click::Single || self.double_press(cx, &row.node.key) {
768 self.open_or_activate(cx, index, row.node);
769 }
770 true
771 }
772 _ => false,
773 }
774 }
775
776 fn focusable(&self) -> bool {
777 !self.roots.is_empty()
778 }
779}