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 edit;
17#[cfg(test)]
18mod tests;
19
20use edit::Arrange;
21pub use edit::TreeMove;
22
23const INDENT: u16 = 2;
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct TreeNode {
29 key: String,
30 label: String,
31 icon: Option<String>,
32 icon_color: Option<String>,
33 detail: Option<String>,
34 children: Vec<TreeNode>,
35 expandable: bool,
36 expanded: bool,
37 loading: bool,
38 faint: bool,
39}
40
41impl TreeNode {
42 #[must_use]
44 pub fn new(key: impl Into<String>, label: impl Into<String>) -> Self {
45 Self {
46 key: key.into(),
47 label: label.into(),
48 icon: None,
49 icon_color: None,
50 detail: None,
51 children: Vec::new(),
52 expandable: false,
53 expanded: false,
54 loading: false,
55 faint: false,
56 }
57 }
58
59 #[must_use]
61 pub fn children(mut self, children: impl IntoIterator<Item = Self>) -> Self {
62 self.children = children.into_iter().collect();
63 self.expandable = self.expandable || !self.children.is_empty();
64 self
65 }
66
67 #[must_use]
70 pub fn expandable(mut self, expandable: bool) -> Self {
71 self.expandable = expandable || !self.children.is_empty();
72 self
73 }
74
75 #[must_use]
77 pub fn expanded(mut self, expanded: bool) -> Self {
78 self.expanded = expanded;
79 self
80 }
81
82 #[must_use]
86 pub fn loading(mut self, loading: bool) -> Self {
87 self.loading = loading;
88 self
89 }
90
91 #[must_use]
93 pub fn icon(mut self, key: impl Into<String>, color: Option<&str>) -> Self {
94 self.icon = Some(key.into());
95 self.icon_color = color.map(str::to_owned);
96 self
97 }
98
99 #[must_use]
101 pub fn detail(mut self, detail: impl Into<String>) -> Self {
102 self.detail = Some(detail.into());
103 self
104 }
105
106 #[must_use]
108 pub fn faint(mut self, faint: bool) -> Self {
109 self.faint = faint;
110 self
111 }
112}
113
114struct Flat<'a> {
116 node: &'a TreeNode,
117 depth: u16,
118 parent: Option<usize>,
119 index: usize,
121}
122
123#[derive(Debug, Clone, Copy)]
125struct RowFlags {
126 hovered: bool,
127 selected: bool,
128 focused: bool,
129 pressed: bool,
130 spinning: bool,
131}
132
133#[derive(Debug, Default)]
135struct LoadingMarks(Vec<(String, DelayedIndicator)>);
136
137type KeyMessage<Msg> = Box<dyn Fn(&str) -> Msg>;
139
140type ExpandMessage<Msg> = Box<dyn Fn(&str, bool) -> Msg>;
142
143type MoveMessage<Msg> = Box<dyn Fn(TreeMove) -> Msg>;
145
146type MenuItems<Msg> = Box<dyn Fn(&str) -> Vec<ContextItem<Msg>>>;
148
149pub struct Tree<Msg> {
188 roots: Vec<TreeNode>,
189 selected: Option<String>,
190 empty: String,
191 on_select: Option<KeyMessage<Msg>>,
192 on_activate: Option<KeyMessage<Msg>>,
193 on_expand: Option<ExpandMessage<Msg>>,
194 on_move: Option<MoveMessage<Msg>>,
195 menu: Option<MenuItems<Msg>>,
196}
197
198impl<Msg: 'static> Tree<Msg> {
199 #[must_use]
201 pub fn new(roots: impl IntoIterator<Item = TreeNode>) -> Self {
202 Self {
203 roots: roots.into_iter().collect(),
204 selected: None,
205 empty: String::new(),
206 on_select: None,
207 on_activate: None,
208 on_expand: None,
209 on_move: None,
210 menu: None,
211 }
212 }
213
214 #[must_use]
216 pub fn selected(mut self, key: Option<&str>) -> Self {
217 self.selected = key.map(str::to_owned);
218 self
219 }
220
221 #[must_use]
223 pub fn empty_text(mut self, text: impl Into<String>) -> Self {
224 self.empty = text.into();
225 self
226 }
227
228 #[must_use]
230 pub fn on_select(mut self, message: impl Fn(&str) -> Msg + 'static) -> Self {
231 self.on_select = Some(Box::new(message));
232 self
233 }
234
235 #[must_use]
237 pub fn on_activate(mut self, message: impl Fn(&str) -> Msg + 'static) -> Self {
238 self.on_activate = Some(Box::new(message));
239 self
240 }
241
242 #[must_use]
244 pub fn on_expand(mut self, message: impl Fn(&str, bool) -> Msg + 'static) -> Self {
245 self.on_expand = Some(Box::new(message));
246 self
247 }
248
249 #[must_use]
252 pub fn reorderable(mut self, message: impl Fn(TreeMove) -> Msg + 'static) -> Self {
253 self.on_move = Some(Box::new(message));
254 self
255 }
256
257 #[must_use]
260 pub fn context_menu(mut self, items: impl Fn(&str) -> Vec<ContextItem<Msg>> + 'static) -> Self {
261 self.menu = Some(Box::new(items));
262 self
263 }
264
265 fn flatten(&self) -> Vec<Flat<'_>> {
266 self.flatten_with(None)
267 }
268
269 fn flatten_with(&self, arrange: Option<&Arrange<'_>>) -> Vec<Flat<'_>> {
271 fn walk<'a>(
272 nodes: &'a [TreeNode],
273 parent_key: Option<&str>,
274 (depth, parent): (u16, Option<usize>),
275 arrange: Option<&Arrange<'_>>,
276 out: &mut Vec<Flat<'a>>,
277 ) {
278 let preview = arrange.filter(|arrange| arrange.parent == parent_key).and_then(|arrange| arrange.order);
279 for index in tab_model::preview_order(nodes.len(), preview) {
280 let node = &nodes[index];
281 let at = out.len();
282 out.push(Flat { node, depth, parent, index });
283 let folded = arrange.is_some_and(|arrange| arrange.key == node.key);
284 if node.expanded && !folded {
285 walk(&node.children, Some(&node.key), (depth.saturating_add(1), Some(at)), arrange, out);
286 }
287 }
288 }
289 let mut out = Vec::new();
290 walk(&self.roots, None, (0, None), arrange, &mut out);
291 out
292 }
293
294 fn selected_index(&self, flat: &[Flat<'_>]) -> Option<usize> {
295 let key = self.selected.as_deref()?;
296 flat.iter().position(|row| row.node.key == key)
297 }
298
299 fn select(&self, cx: &mut EventCx<'_, Msg>, flat: &[Flat<'_>], index: usize) {
300 let Some(row) = flat.get(index) else { return };
301 if self.selected.as_deref() != Some(row.node.key.as_str())
302 && let Some(message) = &self.on_select
303 {
304 cx.emit(message(&row.node.key));
305 }
306 }
307
308 fn expand(&self, cx: &mut EventCx<'_, Msg>, node: &TreeNode, open: bool) -> bool {
309 match &self.on_expand {
310 Some(message) if node.expandable && node.expanded != open => {
311 cx.emit(message(&node.key, open));
312 true
313 }
314 _ => false,
315 }
316 }
317
318 fn activate(&self, cx: &mut EventCx<'_, Msg>, index: usize, node: &TreeNode) -> bool {
319 let Some(message) = &self.on_activate else {
320 return false;
321 };
322 cx.memory::<RowScroll>().flashed = Some(index);
323 cx.flash();
324 cx.emit(message(&node.key));
325 true
326 }
327
328 fn open_or_activate(&self, cx: &mut EventCx<'_, Msg>, index: usize, node: &TreeNode) -> bool {
330 if node.expandable { self.expand(cx, node, !node.expanded) } else { self.activate(cx, index, node) }
331 }
332
333 fn loading_marks(cx: &mut PaintCx<'_>, flat: &[Flat<'_>]) -> Vec<usize> {
336 let now = cx.now();
337 let mut marks = std::mem::take(&mut cx.memory::<LoadingMarks>().0);
338 let mut kept = Vec::new();
339 let mut spinning = Vec::new();
340 let mut next: Option<std::time::Duration> = None;
341 for (index, row) in flat.iter().enumerate() {
342 let node = row.node;
343 let known = marks.iter().position(|(key, _)| *key == node.key);
344 if !node.expandable || (!node.loading && known.is_none()) {
345 continue;
346 }
347 let mut mark = known.map(|at| marks.swap_remove(at).1).unwrap_or_default();
348 if mark.update(node.loading, now) {
349 spinning.push(index);
350 }
351 if let Some(change) = mark.next_change(node.loading, now) {
352 next = Some(next.map_or(change, |soonest| soonest.min(change)));
353 }
354 if !mark.is_idle() {
355 kept.push((node.key.clone(), mark));
356 }
357 }
358 if let Some(delay) = next {
359 cx.request_frame_in(delay);
360 }
361 cx.memory::<LoadingMarks>().0 = kept;
362 spinning
363 }
364
365 fn chevron_x(area: Rect, depth: u16) -> i32 {
367 area.x + i32::from(LEAD) + i32::from(depth.saturating_mul(INDENT))
368 }
369
370 fn paint_row(&self, cx: &mut PaintCx<'_>, rect: Rect, index: usize, row: &Flat<'_>, flags: RowFlags) {
371 let flashed = cx.memory::<RowScroll>().flashed == Some(index);
372 let states = rows::row_states(flags.hovered, flags.selected, flags.focused, flags.pressed && flashed);
373 let style = cx.style("list-item", row.node.faint.then_some("faint"), &states);
374 let slide = rows::slide(cx, &states) > 0;
375 Self::paint_node(cx, rect, row, (&style, &states), flags.spinning, slide);
376 }
377
378 fn paint_node(
380 cx: &mut PaintCx<'_>,
381 rect: Rect,
382 row: &Flat<'_>,
383 (style, states): (&WidgetStyle, &[State]),
384 spinning: bool,
385 slide: bool,
386 ) {
387 let node = row.node;
388 let text_style = style.text();
389 let detail_width = node.detail.as_deref().map_or(0, |d| text::width(d).saturating_add(2));
390
391 let chevron = if !node.expandable {
394 (" ".to_owned(), CellStyle::default())
395 } else if spinning {
396 let style = cx.style("spinner", None, &[]).text();
397 let cell = cx.animation(SpinnerStyle::Dots.animation(), style, Some(std::time::Duration::ZERO));
398 (text::truncate(&cell.glyph, 1).into_owned(), cell.style)
399 } else {
400 let key = if node.expanded { "tree-expanded" } else { "tree-collapsed" };
401 let glyph = text::truncate(&cx.env().icons().glyph(key), 1).into_owned();
402 (glyph, cx.style("tree-chevron", None, states).text())
403 };
404 let icon: Vec<row::Mark> =
405 node.icon.iter().map(|key| row::icon(cx, key, node.icon_color.as_deref(), text_style.fg)).collect();
406 let parts = row::Parts {
407 indent: row.depth.saturating_mul(INDENT),
408 fixed: &[chevron],
409 sliding: &icon,
410 label: &node.label,
411 trailing: detail_width,
412 };
413 row::paint_parts(cx, rect, style, slide, &parts);
414 if let Some(detail) = &node.detail {
415 let detail_style = cx.style("list-detail", None, states).text();
416 row::paint_trailing(cx, rect, detail, detail_style);
417 }
418 }
419}
420
421impl<Msg: 'static> Widget<Msg> for Tree<Msg> {
422 fn measure(&self, _cx: &mut MeasureCx<'_>, available: Size) -> Size {
423 let flat = self.flatten();
424 let widest = flat
425 .iter()
426 .map(|row| {
427 [
429 LEAD,
430 row.depth.saturating_mul(INDENT),
431 2,
432 row.node.icon.as_ref().map_or(0, |_| 2),
433 text::width(&row.node.label),
434 row.node.detail.as_deref().map_or(0, |d| text::width(d).saturating_add(2)),
435 2,
436 ]
437 .into_iter()
438 .fold(0, u16::saturating_add)
439 })
440 .max()
441 .unwrap_or_else(|| text::width(&self.empty).saturating_add(LEAD));
442 let rows = clamp_u16(i32::try_from(flat.len().max(1)).unwrap_or(i32::MAX));
443 Size::new(widest, rows).min(available)
444 }
445
446 fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
447 cx.register_hit(area);
448 if self.roots.is_empty() {
449 let faint = cx.style("list-header", None, &[]).text();
450 cx.text(area.x + i32::from(LEAD), area.y, &self.empty, faint, area.width.saturating_sub(LEAD));
451 return;
452 }
453 let drag = self.drag(cx);
454 let menu_node = self.menu_node(cx);
457 if menu_node.is_some() {
458 cx.request_overlay(area);
459 }
460 let flat = match &drag {
461 Some(drag) => {
462 let offset = cx.memory::<RowScroll>().offset;
463 let rows = Self::rows_area(area, self.flatten().len());
464 let order = self.landing(&drag.key, drag.pointer, rows, offset);
465 let parent = self.siblings(&drag.key).and_then(|(parent, _)| parent);
466 let arrange = Arrange { key: &drag.key, parent, order };
467 self.flatten_with(Some(&arrange))
468 }
469 None => self.flatten(),
470 };
471 let focused = cx.is_focused();
472 let pressed = cx.is_pressed();
473 let selected = self.selected_index(&flat);
474 let visible = usize::from(area.height);
475 let offset = cx.memory::<RowScroll>().follow(selected, flat.len(), visible);
476 let width = Self::rows_area(area, flat.len()).width;
477 let spinning = Self::loading_marks(cx, &flat);
478 let pointer = cx.pointer().filter(|_| drag.is_none() && menu_node.is_none());
479 for (row, index) in (offset..flat.len()).take(visible).enumerate() {
480 let rect = Rect::new(area.x, area.y + i32::try_from(row).unwrap_or(0), width, 1);
481 let key = flat[index].node.key.as_str();
482 if drag.as_ref().is_some_and(|drag| drag.key == key) {
483 tab_model::paint_drop_slot(cx, rect);
484 continue;
485 }
486 let hovered = pointer.is_some_and(|(x, y)| rect.contains(x, y)) || menu_node.as_deref() == Some(key);
487 let flags = RowFlags {
488 hovered,
489 selected: selected == Some(index),
490 focused,
491 pressed,
492 spinning: spinning.contains(&index),
493 };
494 self.paint_row(cx, rect, index, &flat[index], flags);
495 }
496 if let Some(drag) = &drag
498 && let Some(row) = flat.iter().find(|row| row.node.key == drag.key)
499 && !area.is_empty()
500 {
501 let y = drag.pointer.1.clamp(area.y, area.bottom() - 1);
502 let rect = Rect::new(area.x, y, width, 1);
503 tab_model::paint_ghost_surface(cx, rect);
505 let ghost = cx.style("tab-ghost", None, &[]);
506 Self::paint_node(cx, rect, row, (&ghost, &[]), false, false);
507 }
508 rows::paint_scrollbar(cx, area, flat.len(), offset, None);
509 }
510
511 fn paint_overlay(&self, cx: &mut PaintCx<'_>, anchor: Rect) {
512 self.paint_menu(cx, anchor);
513 }
514
515 fn event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
516 let area = cx.area();
517 let flat = self.flatten();
518 if self.menu_event(cx, event, &flat) {
519 return true;
520 }
521 let current = self.selected_index(&flat);
522 match event {
523 Event::Key(key) => {
524 if self.move_key(cx, key) {
525 return true;
526 }
527 if let Some(step) = Step::from_key(key) {
528 let Some(target) = step.apply(current, flat.len(), usize::from(area.height)) else {
529 return false;
530 };
531 self.select(cx, &flat, target);
532 return true;
533 }
534 let Some(index) = current else { return false };
535 let row = &flat[index];
536 if key.is_plain(Key::Right) || key.is_plain(Key::Char('l')) {
537 if !row.node.expanded {
538 return self.expand(cx, row.node, true);
539 }
540 if !row.node.children.is_empty() {
541 self.select(cx, &flat, index + 1);
542 return true;
543 }
544 return false;
545 }
546 if key.is_plain(Key::Left) || key.is_plain(Key::Char('h')) {
547 if row.node.expanded {
548 return self.expand(cx, row.node, false);
549 }
550 return row.parent.is_some_and(|parent| {
551 self.select(cx, &flat, parent);
552 true
553 });
554 }
555 if key.is_plain(Key::Enter) {
556 return self.open_or_activate(cx, index, row.node);
557 }
558 if key.is_plain(Key::Space) {
559 return self.activate(cx, index, row.node);
560 }
561 false
562 }
563 Event::Mouse(mouse) => {
564 if rows::scroll_mouse(cx, mouse, area, flat.len()) {
565 return true;
566 }
567 let offset = cx.memory::<RowScroll>().offset;
568 let index = usize::try_from(mouse.y - area.y).ok().map(|r| offset + r).filter(|i| *i < flat.len());
569 let on_chevron = index.is_some_and(|index| {
570 let row = &flat[index];
571 let chevron = Self::chevron_x(area, row.depth);
572 row.node.expandable && (chevron..=chevron + 1).contains(&mouse.x)
573 });
574 if self.on_move.is_some()
575 && !on_chevron
576 && let Some(used) = self.reorder_pointer(cx, mouse, &flat, index)
577 {
578 return used;
579 }
580 if mouse.kind != MouseKind::Down(MouseButton::Left) {
581 return false;
582 }
583 let Some(index) = index else {
584 return false;
585 };
586 let row = &flat[index];
587 if on_chevron {
588 return self.expand(cx, row.node, !row.node.expanded);
589 }
590 self.select(cx, &flat, index);
591 self.open_or_activate(cx, index, row.node);
592 true
593 }
594 _ => false,
595 }
596 }
597
598 fn focusable(&self) -> bool {
599 !self.roots.is_empty()
600 }
601}