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::delayed::DelayedIndicator;
12use super::row::{self, LEAD};
13use super::rows::{self, RowScroll, Step};
14use super::{ContextItem, SpinnerStyle, tab_model};
15
16mod drop;
17mod edit;
18#[cfg(test)]
19mod multi_tests;
20mod select;
21#[cfg(test)]
22mod tests;
23
24pub use drop::TreeDrop;
25use drop::{Aim, Dropping};
26use edit::Arrange;
27pub use edit::TreeMove;
28
29const INDENT: u16 = 2;
31
32#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct TreeNode {
35 key: String,
36 label: String,
37 icon: Option<String>,
38 icon_color: Option<String>,
39 detail: Option<String>,
40 children: Vec<TreeNode>,
41 expandable: bool,
42 expanded: bool,
43 loading: bool,
44 faint: bool,
45}
46
47impl TreeNode {
48 #[must_use]
50 pub fn new(key: impl Into<String>, label: impl Into<String>) -> Self {
51 Self {
52 key: key.into(),
53 label: label.into(),
54 icon: None,
55 icon_color: None,
56 detail: None,
57 children: Vec::new(),
58 expandable: false,
59 expanded: false,
60 loading: false,
61 faint: false,
62 }
63 }
64
65 #[must_use]
67 pub fn children(mut self, children: impl IntoIterator<Item = Self>) -> Self {
68 self.children = children.into_iter().collect();
69 self.expandable = self.expandable || !self.children.is_empty();
70 self
71 }
72
73 #[must_use]
76 pub fn expandable(mut self, expandable: bool) -> Self {
77 self.expandable = expandable || !self.children.is_empty();
78 self
79 }
80
81 #[must_use]
83 pub fn expanded(mut self, expanded: bool) -> Self {
84 self.expanded = expanded;
85 self
86 }
87
88 #[must_use]
92 pub fn loading(mut self, loading: bool) -> Self {
93 self.loading = loading;
94 self
95 }
96
97 #[must_use]
99 pub fn icon(mut self, key: impl Into<String>, color: Option<&str>) -> Self {
100 self.icon = Some(key.into());
101 self.icon_color = color.map(str::to_owned);
102 self
103 }
104
105 #[must_use]
107 pub fn detail(mut self, detail: impl Into<String>) -> Self {
108 self.detail = Some(detail.into());
109 self
110 }
111
112 #[must_use]
114 pub fn faint(mut self, faint: bool) -> Self {
115 self.faint = faint;
116 self
117 }
118}
119
120struct Flat<'a> {
122 node: &'a TreeNode,
123 depth: u16,
124 parent: Option<usize>,
125 index: usize,
127}
128
129#[derive(Debug, Clone, Copy)]
131struct RowFlags {
132 hovered: bool,
133 selected: bool,
134 focused: bool,
135 pressed: bool,
136 spinning: bool,
137 pillar: bool,
140}
141
142#[derive(Debug, Default)]
144struct LoadingMarks(Vec<(String, DelayedIndicator)>);
145
146type KeyMessage<Msg> = Box<dyn Fn(&str) -> Msg>;
148
149type ExpandMessage<Msg> = Box<dyn Fn(&str, bool) -> Msg>;
151
152type MoveMessage<Msg> = Box<dyn Fn(TreeMove) -> Msg>;
154
155type SelectionMessage<Msg> = Box<dyn Fn(Vec<String>) -> Msg>;
157
158type MenuItems<Msg> = Box<dyn Fn(&str) -> Vec<ContextItem<Msg>>>;
160
161pub struct Tree<Msg> {
209 roots: Vec<TreeNode>,
210 selected: Option<String>,
211 empty: String,
212 on_select: Option<KeyMessage<Msg>>,
213 on_activate: Option<KeyMessage<Msg>>,
214 on_expand: Option<ExpandMessage<Msg>>,
215 on_move: Option<MoveMessage<Msg>>,
216 menu: Option<MenuItems<Msg>>,
217 chosen: Vec<String>,
218 on_choose: Option<SelectionMessage<Msg>>,
219 dropping: Option<Dropping<Msg>>,
220}
221
222impl<Msg: 'static> Tree<Msg> {
223 #[must_use]
225 pub fn new(roots: impl IntoIterator<Item = TreeNode>) -> Self {
226 Self {
227 roots: roots.into_iter().collect(),
228 selected: None,
229 empty: String::new(),
230 on_select: None,
231 on_activate: None,
232 on_expand: None,
233 on_move: None,
234 menu: None,
235 chosen: Vec::new(),
236 on_choose: None,
237 dropping: None,
238 }
239 }
240
241 #[must_use]
243 pub fn selected(mut self, key: Option<&str>) -> Self {
244 self.selected = key.map(str::to_owned);
245 self
246 }
247
248 #[must_use]
259 pub fn multi_select(mut self, selected: &[String], message: impl Fn(Vec<String>) -> Msg + 'static) -> Self {
260 self.chosen = selected.to_vec();
261 self.on_choose = Some(Box::new(message));
262 self
263 }
264
265 #[must_use]
267 pub fn empty_text(mut self, text: impl Into<String>) -> Self {
268 self.empty = text.into();
269 self
270 }
271
272 #[must_use]
274 pub fn on_select(mut self, message: impl Fn(&str) -> Msg + 'static) -> Self {
275 self.on_select = Some(Box::new(message));
276 self
277 }
278
279 #[must_use]
281 pub fn on_activate(mut self, message: impl Fn(&str) -> Msg + 'static) -> Self {
282 self.on_activate = Some(Box::new(message));
283 self
284 }
285
286 #[must_use]
288 pub fn on_expand(mut self, message: impl Fn(&str, bool) -> Msg + 'static) -> Self {
289 self.on_expand = Some(Box::new(message));
290 self
291 }
292
293 #[must_use]
296 pub fn reorderable(mut self, message: impl Fn(TreeMove) -> Msg + 'static) -> Self {
297 self.on_move = Some(Box::new(message));
298 self
299 }
300
301 #[must_use]
315 pub fn droppable(
316 mut self,
317 message: impl Fn(TreeDrop) -> Msg + 'static,
318 accepts: impl Fn(&str) -> bool + 'static,
319 ) -> Self {
320 self.dropping = Some(Dropping::new(message, accepts));
321 self
322 }
323
324 #[must_use]
327 pub fn context_menu(mut self, items: impl Fn(&str) -> Vec<ContextItem<Msg>> + 'static) -> Self {
328 self.menu = Some(Box::new(items));
329 self
330 }
331
332 fn flatten(&self) -> Vec<Flat<'_>> {
333 self.flatten_with(None)
334 }
335
336 fn flatten_with(&self, arrange: Option<&Arrange<'_>>) -> Vec<Flat<'_>> {
338 fn walk<'a>(
339 nodes: &'a [TreeNode],
340 parent_key: Option<&str>,
341 (depth, parent): (u16, Option<usize>),
342 arrange: Option<&Arrange<'_>>,
343 out: &mut Vec<Flat<'a>>,
344 ) {
345 let preview = arrange.filter(|arrange| arrange.parent == parent_key).and_then(|arrange| arrange.order);
346 for index in tab_model::preview_order(nodes.len(), preview) {
347 let node = &nodes[index];
348 let at = out.len();
349 out.push(Flat { node, depth, parent, index });
350 let folded = arrange.is_some_and(|arrange| arrange.key == node.key);
351 if node.expanded && !folded {
352 walk(&node.children, Some(&node.key), (depth.saturating_add(1), Some(at)), arrange, out);
353 }
354 }
355 }
356 let mut out = Vec::new();
357 walk(&self.roots, None, (0, None), arrange, &mut out);
358 out
359 }
360
361 fn selected_index(&self, flat: &[Flat<'_>]) -> Option<usize> {
362 let key = self.selected.as_deref()?;
363 flat.iter().position(|row| row.node.key == key)
364 }
365
366 fn select(&self, cx: &mut EventCx<'_, Msg>, flat: &[Flat<'_>], index: usize) {
367 let Some(row) = flat.get(index) else { return };
368 if self.selected.as_deref() != Some(row.node.key.as_str())
369 && let Some(message) = &self.on_select
370 {
371 cx.emit(message(&row.node.key));
372 }
373 }
374
375 fn expand(&self, cx: &mut EventCx<'_, Msg>, node: &TreeNode, open: bool) -> bool {
376 match &self.on_expand {
377 Some(message) if node.expandable && node.expanded != open => {
378 cx.emit(message(&node.key, open));
379 true
380 }
381 _ => false,
382 }
383 }
384
385 fn activate(&self, cx: &mut EventCx<'_, Msg>, index: usize, node: &TreeNode) -> bool {
386 let Some(message) = &self.on_activate else {
387 return false;
388 };
389 cx.memory::<RowScroll>().flashed = Some(index);
390 cx.flash();
391 cx.emit(message(&node.key));
392 true
393 }
394
395 fn open_or_activate(&self, cx: &mut EventCx<'_, Msg>, index: usize, node: &TreeNode) -> bool {
397 if node.expandable { self.expand(cx, node, !node.expanded) } else { self.activate(cx, index, node) }
398 }
399
400 fn loading_marks(cx: &mut PaintCx<'_>, flat: &[Flat<'_>]) -> Vec<usize> {
403 let now = cx.now();
404 let mut marks = std::mem::take(&mut cx.memory::<LoadingMarks>().0);
405 let mut kept = Vec::new();
406 let mut spinning = Vec::new();
407 let mut next: Option<std::time::Duration> = None;
408 for (index, row) in flat.iter().enumerate() {
409 let node = row.node;
410 let known = marks.iter().position(|(key, _)| *key == node.key);
411 if !node.expandable || (!node.loading && known.is_none()) {
412 continue;
413 }
414 let mut mark = known.map(|at| marks.swap_remove(at).1).unwrap_or_default();
415 if mark.update(node.loading, now) {
416 spinning.push(index);
417 }
418 if let Some(change) = mark.next_change(node.loading, now) {
419 next = Some(next.map_or(change, |soonest| soonest.min(change)));
420 }
421 if !mark.is_idle() {
422 kept.push((node.key.clone(), mark));
423 }
424 }
425 if let Some(delay) = next {
426 cx.request_frame_in(delay);
427 }
428 cx.memory::<LoadingMarks>().0 = kept;
429 spinning
430 }
431
432 fn paint_target(cx: &mut PaintCx<'_>, rect: Rect, row: &Flat<'_>, aim: &Aim) -> bool {
435 let (widget, variant) = match aim {
436 Aim::Into(Some(key)) if *key == row.node.key => ("tree-drop", None),
437 Aim::Refused(key) if *key == row.node.key => ("list-item", Some("faint")),
438 _ => return false,
439 };
440 let style = cx.style(widget, variant, &[]);
441 Self::paint_node(cx, rect, row, (&style, &[]), false, false);
442 true
443 }
444
445 fn chevron_x(area: Rect, depth: u16) -> i32 {
447 area.x + i32::from(LEAD) + i32::from(depth.saturating_mul(INDENT))
448 }
449
450 fn paint_row(&self, cx: &mut PaintCx<'_>, rect: Rect, index: usize, row: &Flat<'_>, flags: RowFlags) {
451 let flashed = cx.memory::<RowScroll>().flashed == Some(index);
452 let states = rows::row_states(flags.hovered, flags.selected, flags.focused, flags.pressed && flashed);
453 let style = cx.style("list-item", row.node.faint.then_some("faint"), &states);
454 let slide = flags.pillar && rows::slide(cx, &states) > 0;
457 let style = if flags.pillar { style } else { style.without("pillar") };
458 Self::paint_node(cx, rect, row, (&style, &states), flags.spinning, slide);
459 }
460
461 fn paint_node(
463 cx: &mut PaintCx<'_>,
464 rect: Rect,
465 row: &Flat<'_>,
466 (style, states): (&WidgetStyle, &[State]),
467 spinning: bool,
468 slide: bool,
469 ) {
470 let node = row.node;
471 let text_style = style.text();
472 let detail_width = node.detail.as_deref().map_or(0, |d| text::width(d).saturating_add(2));
473
474 let chevron = if !node.expandable {
477 (" ".to_owned(), CellStyle::default())
478 } else if spinning {
479 let style = cx.style("spinner", None, &[]).text();
480 let cell = cx.animation(SpinnerStyle::Dots.animation(), style, Some(std::time::Duration::ZERO));
481 (text::truncate(&cell.glyph, 1).into_owned(), cell.style)
482 } else {
483 let key = if node.expanded { "tree-expanded" } else { "tree-collapsed" };
484 let glyph = text::truncate(&cx.env().icons().glyph(key), 1).into_owned();
485 (glyph, cx.style("tree-chevron", None, states).text())
486 };
487 let icon: Vec<row::Mark> =
488 node.icon.iter().map(|key| row::icon(cx, key, node.icon_color.as_deref(), text_style.fg)).collect();
489 let parts = row::Parts {
490 indent: row.depth.saturating_mul(INDENT),
491 fixed: &[chevron],
492 sliding: &icon,
493 label: &node.label,
494 trailing: detail_width,
495 };
496 row::paint_parts(cx, rect, style, slide, &parts);
497 if let Some(detail) = &node.detail {
498 let detail_style = cx.style("list-detail", None, states).text();
499 row::paint_trailing(cx, rect, detail, detail_style);
500 }
501 }
502}
503
504impl<Msg: 'static> Widget<Msg> for Tree<Msg> {
505 fn measure(&self, _cx: &mut MeasureCx<'_>, available: Size) -> Size {
506 let flat = self.flatten();
507 let widest = flat
508 .iter()
509 .map(|row| {
510 [
512 LEAD,
513 row.depth.saturating_mul(INDENT),
514 2,
515 row.node.icon.as_ref().map_or(0, |_| 2),
516 text::width(&row.node.label),
517 row.node.detail.as_deref().map_or(0, |d| text::width(d).saturating_add(2)),
518 2,
519 ]
520 .into_iter()
521 .fold(0, u16::saturating_add)
522 })
523 .max()
524 .unwrap_or_else(|| text::width(&self.empty).saturating_add(LEAD));
525 let rows = clamp_u16(i32::try_from(flat.len().max(1)).unwrap_or(i32::MAX));
526 Size::new(widest, rows).min(available)
527 }
528
529 fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
530 cx.register_hit(area);
531 if self.roots.is_empty() {
532 let faint = cx.style("list-header", None, &[]).text();
533 cx.text(area.x + i32::from(LEAD), area.y, &self.empty, faint, area.width.saturating_sub(LEAD));
534 return;
535 }
536 let drag = self.drag(cx);
537 let menu_node = self.menu_node(cx);
540 if menu_node.is_some() {
541 cx.request_overlay(area);
542 }
543 let aim = drag.as_ref().map(|drag| {
544 let offset = cx.memory::<RowScroll>().offset;
545 self.aim(&drag.keys, drag.pointer, Self::rows_area(area, self.flatten().len()), offset)
546 });
547 let flat = match (&drag, &aim) {
548 (Some(drag), Some(Aim::Reorder(order))) => {
549 let parent = self.siblings(&drag.key).and_then(|(parent, _)| parent);
550 self.flatten_with(Some(&Arrange { key: &drag.key, parent, order: *order }))
551 }
552 (Some(drag), _) => self.drag_layout(&drag.keys),
553 (None, _) => self.flatten(),
554 };
555 let slot = drag.as_ref().filter(|drag| self.reorders(&drag.keys)).map(|drag| drag.key.as_str());
558 let focused = cx.is_focused();
559 let pressed = cx.is_pressed();
560 let selected = self.selected_index(&flat);
561 let visible = usize::from(area.height);
562 let offset = cx.memory::<RowScroll>().follow(selected, flat.len(), visible);
563 let width = Self::rows_area(area, flat.len()).width;
564 let spinning = Self::loading_marks(cx, &flat);
565 let pointer = cx.pointer().filter(|_| drag.is_none() && menu_node.is_none());
566 for (row, index) in (offset..flat.len()).take(visible).enumerate() {
567 let rect = Rect::new(area.x, area.y + i32::try_from(row).unwrap_or(0), width, 1);
568 let key = flat[index].node.key.as_str();
569 if slot == Some(key) {
570 tab_model::paint_drop_slot(cx, rect);
571 continue;
572 }
573 if let Some(aim) = &aim
574 && Self::paint_target(cx, rect, &flat[index], aim)
575 {
576 continue;
577 }
578 let touched = pointer.is_some_and(|(x, y)| rect.contains(x, y)) || menu_node.as_deref() == Some(key);
579 let cursor = selected == Some(index);
580 let chosen = self.is_chosen(key);
581 let hovered = touched || (cursor && !chosen);
584 let flags = RowFlags {
585 hovered,
586 selected: chosen,
587 focused: focused && cursor,
588 pressed,
589 spinning: spinning.contains(&index),
590 pillar: cursor || hovered || !self.is_multi(),
591 };
592 self.paint_row(cx, rect, index, &flat[index], flags);
593 }
594 if aim == Some(Aim::Into(None)) {
595 let used = i32::try_from(flat.len().saturating_sub(offset)).unwrap_or(i32::MAX);
597 let top = area.y.saturating_add(used);
598 if top < area.bottom() {
599 let free = Rect::new(area.x, top, width, clamp_u16(area.bottom() - top));
600 let bg = cx.style("tree-drop", None, &[]).text().bg;
601 if let Some(bg) = bg {
602 cx.fill(free, bg);
603 }
604 }
605 }
606 if let Some(drag) = &drag
608 && matches!(aim, Some(Aim::Reorder(_)))
609 && let Some(row) = flat.iter().find(|row| row.node.key == drag.key)
610 && !area.is_empty()
611 {
612 let y = drag.pointer.1.clamp(area.y, area.bottom() - 1);
613 let rect = Rect::new(area.x, y, width, 1);
614 tab_model::paint_ghost_surface(cx, rect);
616 let ghost = cx.style("tab-ghost", None, &[]);
617 Self::paint_node(cx, rect, row, (&ghost, &[]), false, false);
618 }
619 rows::paint_scrollbar(cx, area, flat.len(), offset, None);
620 }
621
622 fn paint_overlay(&self, cx: &mut PaintCx<'_>, anchor: Rect) {
623 self.paint_menu(cx, anchor);
624 }
625
626 fn event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
627 let area = cx.area();
628 let flat = self.flatten();
629 if self.menu_event(cx, event, &flat) {
630 return true;
631 }
632 let current = self.selected_index(&flat);
633 match event {
634 Event::Key(key) => {
635 if self.move_key(cx, key) || self.selection_key(cx, key, &flat) {
636 return true;
637 }
638 if let Some(step) = Step::from_key(key) {
639 let Some(target) = step.apply(current, flat.len(), usize::from(area.height)) else {
640 return false;
641 };
642 self.select_one(cx, &flat, target);
643 return true;
644 }
645 let Some(index) = current else { return false };
646 let row = &flat[index];
647 if key.is_plain(Key::Right) || key.is_plain(Key::Char('l')) {
648 if !row.node.expanded {
649 return self.expand(cx, row.node, true);
650 }
651 if !row.node.children.is_empty() {
652 self.select_one(cx, &flat, index + 1);
653 return true;
654 }
655 return false;
656 }
657 if key.is_plain(Key::Left) || key.is_plain(Key::Char('h')) {
658 if row.node.expanded {
659 return self.expand(cx, row.node, false);
660 }
661 return row.parent.is_some_and(|parent| {
662 self.select_one(cx, &flat, parent);
663 true
664 });
665 }
666 if key.is_plain(Key::Enter) {
667 return self.open_or_activate(cx, index, row.node);
668 }
669 if key.is_plain(Key::Space) {
670 return self.activate(cx, index, row.node);
671 }
672 false
673 }
674 Event::Mouse(mouse) => {
675 if rows::scroll_mouse(cx, mouse, area, flat.len()) {
676 return true;
677 }
678 let offset = cx.memory::<RowScroll>().offset;
679 let index = usize::try_from(mouse.y - area.y).ok().map(|r| offset + r).filter(|i| *i < flat.len());
680 let on_chevron = index.is_some_and(|index| {
681 let row = &flat[index];
682 let chevron = Self::chevron_x(area, row.depth);
683 row.node.expandable && (chevron..=chevron + 1).contains(&mouse.x)
684 });
685 if (self.on_move.is_some() || self.dropping.is_some())
686 && !on_chevron
687 && let Some(used) = self.drag_pointer(cx, mouse, &flat, index)
688 {
689 return used;
690 }
691 if mouse.kind != MouseKind::Down(MouseButton::Left) {
692 return false;
693 }
694 let Some(index) = index else {
695 return false;
696 };
697 let row = &flat[index];
698 if on_chevron {
699 return self.expand(cx, row.node, !row.node.expanded);
700 }
701 if self.modified_press(cx, &flat, index, mouse.mods) {
702 return true;
703 }
704 self.select_one(cx, &flat, index);
705 self.open_or_activate(cx, index, row.node);
706 true
707 }
708 _ => false,
709 }
710 }
711
712 fn focusable(&self) -> bool {
713 !self.roots.is_empty()
714 }
715}