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
35const METER: u16 = 6;
37
38#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct TreeNode {
41 key: String,
42 label: String,
43 icon: Option<String>,
44 icon_color: Option<String>,
45 detail: Option<String>,
46 children: Vec<TreeNode>,
47 expandable: bool,
48 expanded: bool,
49 loading: bool,
50 faint: bool,
51 meter: Option<(u16, String)>,
53}
54
55impl TreeNode {
56 #[must_use]
58 pub fn new(key: impl Into<String>, label: impl Into<String>) -> Self {
59 Self {
60 key: key.into(),
61 label: label.into(),
62 icon: None,
63 icon_color: None,
64 detail: None,
65 children: Vec::new(),
66 expandable: false,
67 expanded: false,
68 loading: false,
69 faint: false,
70 meter: None,
71 }
72 }
73
74 #[must_use]
76 pub fn children(mut self, children: impl IntoIterator<Item = Self>) -> Self {
77 self.children = children.into_iter().collect();
78 self.expandable = self.expandable || !self.children.is_empty();
79 self
80 }
81
82 #[must_use]
85 pub fn expandable(mut self, expandable: bool) -> Self {
86 self.expandable = expandable || !self.children.is_empty();
87 self
88 }
89
90 #[must_use]
92 pub fn expanded(mut self, expanded: bool) -> Self {
93 self.expanded = expanded;
94 self
95 }
96
97 #[must_use]
101 pub fn loading(mut self, loading: bool) -> Self {
102 self.loading = loading;
103 self
104 }
105
106 #[must_use]
108 pub fn icon(mut self, key: impl Into<String>, color: Option<&str>) -> Self {
109 self.icon = Some(key.into());
110 self.icon_color = color.map(str::to_owned);
111 self
112 }
113
114 #[must_use]
116 pub fn detail(mut self, detail: impl Into<String>) -> Self {
117 self.detail = Some(detail.into());
118 self
119 }
120
121 #[must_use]
127 pub fn meter(mut self, fraction: f32, tone: impl Into<String>) -> Self {
128 let fraction = if fraction.is_nan() { 0.0 } else { fraction.clamp(0.0, 1.0) };
129 self.meter = Some(((fraction * 1000.0).round() as u16, tone.into()));
131 self
132 }
133
134 #[must_use]
136 pub fn faint(mut self, faint: bool) -> Self {
137 self.faint = faint;
138 self
139 }
140}
141
142struct Flat<'a> {
144 node: &'a TreeNode,
145 depth: u16,
146 parent: Option<usize>,
147 index: usize,
149 chevrons: bool,
152}
153
154#[derive(Debug, Clone, Copy)]
156struct RowFlags {
157 hovered: bool,
158 selected: bool,
159 focused: bool,
160 pressed: bool,
161 spinning: bool,
162 pillar: bool,
165}
166
167#[derive(Debug, Default)]
169struct LoadingMarks(Vec<(String, DelayedIndicator)>);
170
171type KeyMessage<Msg> = Box<dyn Fn(&str) -> Msg>;
173
174type ExpandMessage<Msg> = Box<dyn Fn(&str, bool) -> Msg>;
176
177type MoveMessage<Msg> = Box<dyn Fn(TreeMove) -> Msg>;
179
180type SelectionMessage<Msg> = Box<dyn Fn(Vec<String>) -> Msg>;
182
183type DropMessage<Msg> = Box<dyn Fn(TreeDrop) -> Msg>;
185
186type MenuItems<Msg> = Box<dyn Fn(&str) -> Vec<ContextItem<Msg>>>;
188
189pub struct Tree<Msg> {
243 roots: Vec<TreeNode>,
244 selected: Option<String>,
245 empty: String,
246 on_select: Option<KeyMessage<Msg>>,
247 on_activate: Option<KeyMessage<Msg>>,
248 on_expand: Option<ExpandMessage<Msg>>,
249 on_move: Option<MoveMessage<Msg>>,
250 menu: Option<MenuItems<Msg>>,
251 chosen: Vec<String>,
252 on_choose: Option<SelectionMessage<Msg>>,
253 dropping: Option<Dropping<Msg>>,
254 copy_drop: Option<DropMessage<Msg>>,
255 activate_on: Click,
256 box_select: bool,
257 wrap: bool,
258}
259
260impl<Msg: 'static> Tree<Msg> {
261 #[must_use]
263 pub fn new(roots: impl IntoIterator<Item = TreeNode>) -> Self {
264 Self {
265 roots: roots.into_iter().collect(),
266 selected: None,
267 empty: String::new(),
268 on_select: None,
269 on_activate: None,
270 on_expand: None,
271 on_move: None,
272 menu: None,
273 chosen: Vec::new(),
274 on_choose: None,
275 dropping: None,
276 copy_drop: None,
277 activate_on: Click::Single,
278 box_select: false,
279 wrap: false,
280 }
281 }
282
283 #[must_use]
287 pub fn wrap(mut self, wrap: bool) -> Self {
288 self.wrap = wrap;
289 self
290 }
291
292 #[must_use]
294 pub fn selected(mut self, key: Option<&str>) -> Self {
295 self.selected = key.map(str::to_owned);
296 self
297 }
298
299 #[must_use]
310 pub fn multi_select(mut self, selected: &[String], message: impl Fn(Vec<String>) -> Msg + 'static) -> Self {
311 self.chosen = selected.to_vec();
312 self.on_choose = Some(Box::new(message));
313 self
314 }
315
316 #[must_use]
318 pub fn empty_text(mut self, text: impl Into<String>) -> Self {
319 self.empty = text.into();
320 self
321 }
322
323 #[must_use]
325 pub fn on_select(mut self, message: impl Fn(&str) -> Msg + 'static) -> Self {
326 self.on_select = Some(Box::new(message));
327 self
328 }
329
330 #[must_use]
332 pub fn on_activate(mut self, message: impl Fn(&str) -> Msg + 'static) -> Self {
333 self.on_activate = Some(Box::new(message));
334 self
335 }
336
337 #[must_use]
339 pub fn on_expand(mut self, message: impl Fn(&str, bool) -> Msg + 'static) -> Self {
340 self.on_expand = Some(Box::new(message));
341 self
342 }
343
344 #[must_use]
347 pub fn reorderable(mut self, message: impl Fn(TreeMove) -> Msg + 'static) -> Self {
348 self.on_move = Some(Box::new(message));
349 self
350 }
351
352 #[must_use]
366 pub fn droppable(
367 mut self,
368 message: impl Fn(TreeDrop) -> Msg + 'static,
369 accepts: impl Fn(&str) -> bool + 'static,
370 ) -> Self {
371 self.dropping = Some(Dropping::new(message, accepts));
372 self
373 }
374
375 #[must_use]
379 pub fn on_copy_drop(mut self, message: impl Fn(TreeDrop) -> Msg + 'static) -> Self {
380 self.copy_drop = Some(Box::new(message));
381 self
382 }
383
384 #[must_use]
389 pub fn activate_on(mut self, click: Click) -> Self {
390 self.activate_on = click;
391 self
392 }
393
394 #[must_use]
399 pub fn box_select(mut self, on: bool) -> Self {
400 self.box_select = on;
401 self
402 }
403
404 #[must_use]
407 pub fn context_menu(mut self, items: impl Fn(&str) -> Vec<ContextItem<Msg>> + 'static) -> Self {
408 self.menu = Some(Box::new(items));
409 self
410 }
411
412 fn flatten(&self) -> Vec<Flat<'_>> {
413 self.flatten_with(None)
414 }
415
416 fn flatten_with(&self, arrange: Option<&Arrange<'_>>) -> Vec<Flat<'_>> {
418 fn walk<'a>(
419 nodes: &'a [TreeNode],
420 parent_key: Option<&str>,
421 (depth, parent): (u16, Option<usize>),
422 arrange: Option<&Arrange<'_>>,
423 out: &mut Vec<Flat<'a>>,
424 ) {
425 let preview = arrange.filter(|arrange| arrange.parent == parent_key).and_then(|arrange| arrange.order);
426 for index in tab_model::preview_order(nodes.len(), preview) {
427 let node = &nodes[index];
428 let at = out.len();
429 out.push(Flat { node, depth, parent, index, chevrons: true });
430 let folded = arrange.is_some_and(|arrange| arrange.key == node.key);
431 if node.expanded && !folded {
432 walk(&node.children, Some(&node.key), (depth.saturating_add(1), Some(at)), arrange, out);
433 }
434 }
435 }
436 let mut out = Vec::new();
437 walk(&self.roots, None, (0, None), arrange, &mut out);
438 if !out.iter().any(|row| row.node.expandable) {
440 for row in &mut out {
441 row.chevrons = false;
442 }
443 }
444 out
445 }
446
447 fn selected_index(&self, flat: &[Flat<'_>]) -> Option<usize> {
448 let key = self.selected.as_deref()?;
449 flat.iter().position(|row| row.node.key == key)
450 }
451
452 fn select(&self, cx: &mut EventCx<'_, Msg>, flat: &[Flat<'_>], index: usize) {
453 let Some(row) = flat.get(index) else { return };
454 if self.selected.as_deref() != Some(row.node.key.as_str())
455 && let Some(message) = &self.on_select
456 {
457 cx.emit(message(&row.node.key));
458 }
459 }
460
461 fn expand(&self, cx: &mut EventCx<'_, Msg>, node: &TreeNode, open: bool) -> bool {
462 match &self.on_expand {
463 Some(message) if node.expandable && node.expanded != open => {
464 cx.emit(message(&node.key, open));
465 true
466 }
467 _ => false,
468 }
469 }
470
471 fn activate(&self, cx: &mut EventCx<'_, Msg>, index: usize, node: &TreeNode) -> bool {
472 let Some(message) = &self.on_activate else {
473 return false;
474 };
475 cx.memory::<RowScroll>().flashed = Some(index);
476 cx.flash();
477 cx.emit(message(&node.key));
478 true
479 }
480
481 fn open_or_activate(&self, cx: &mut EventCx<'_, Msg>, index: usize, node: &TreeNode) -> bool {
483 if node.expandable { self.expand(cx, node, !node.expanded) } else { self.activate(cx, index, node) }
484 }
485
486 fn loading_marks(cx: &mut PaintCx<'_>, flat: &[Flat<'_>]) -> Vec<usize> {
489 let now = cx.now();
490 let mut marks = std::mem::take(&mut cx.memory::<LoadingMarks>().0);
491 let mut kept = Vec::new();
492 let mut spinning = Vec::new();
493 let mut next: Option<std::time::Duration> = None;
494 for (index, row) in flat.iter().enumerate() {
495 let node = row.node;
496 let known = marks.iter().position(|(key, _)| *key == node.key);
497 if !node.expandable || (!node.loading && known.is_none()) {
498 continue;
499 }
500 let mut mark = known.map(|at| marks.swap_remove(at).1).unwrap_or_default();
501 if mark.update(node.loading, now) {
502 spinning.push(index);
503 }
504 if let Some(change) = mark.next_change(node.loading, now) {
505 next = Some(next.map_or(change, |soonest| soonest.min(change)));
506 }
507 if !mark.is_idle() {
508 kept.push((node.key.clone(), mark));
509 }
510 }
511 if let Some(delay) = next {
512 cx.request_frame_in(delay);
513 }
514 cx.memory::<LoadingMarks>().0 = kept;
515 spinning
516 }
517
518 fn paint_target(cx: &mut PaintCx<'_>, rect: Rect, row: &Flat<'_>, aim: &Aim) -> bool {
521 let (widget, variant) = match aim {
522 Aim::Into(Some(key)) if *key == row.node.key => ("tree-drop", None),
523 Aim::Refused(key) if *key == row.node.key => ("list-item", Some("faint")),
524 _ => return false,
525 };
526 let style = cx.style(widget, variant, &[]);
527 Self::paint_node(cx, rect, row, (&style, &[]), false, false);
528 true
529 }
530
531 fn chevron_x(area: Rect, depth: u16) -> i32 {
533 area.x + i32::from(LEAD) + i32::from(depth.saturating_mul(INDENT))
534 }
535
536 fn paint_row(&self, cx: &mut PaintCx<'_>, rect: Rect, index: usize, row: &Flat<'_>, flags: RowFlags) {
537 let flashed = cx.memory::<RowScroll>().flashed == Some(index);
538 let states = rows::row_states(flags.hovered, flags.selected, flags.focused, flags.pressed && flashed);
539 let style = cx.style("list-item", row.node.faint.then_some("faint"), &states);
540 let slide = flags.pillar && rows::slide(cx, &states) > 0;
543 let style = if flags.pillar { style } else { style.without("pillar") };
544 Self::paint_node(cx, rect, row, (&style, &states), flags.spinning, slide);
545 }
546
547 fn paint_node(
549 cx: &mut PaintCx<'_>,
550 rect: Rect,
551 row: &Flat<'_>,
552 (style, states): (&WidgetStyle, &[State]),
553 spinning: bool,
554 slide: bool,
555 ) {
556 let node = row.node;
557 let text_style = style.text();
558 let detail_width = node.detail.as_deref().map_or(0, |d| text::width(d).saturating_add(2));
559 let before = [
561 LEAD,
562 row.depth.saturating_mul(INDENT),
563 if row.chevrons { 2 } else { 0 },
564 node.icon.as_ref().map_or(0, |_| 2),
565 text::width(&node.label),
566 detail_width,
567 2,
568 ]
569 .into_iter()
570 .fold(0, u16::saturating_add);
571 let meter = node.meter.as_ref().filter(|_| rect.width >= before.saturating_add(METER + 1));
572 let meter_width = if meter.is_some() { METER + 1 } else { 0 };
573
574 let chevron = if !node.expandable {
577 (" ".to_owned(), CellStyle::default())
578 } else if spinning {
579 let style = cx.style("spinner", None, &[]).text();
580 let cell = cx.animation(SpinnerStyle::Dots.animation(), style, Some(std::time::Duration::ZERO));
581 (text::truncate(&cell.glyph, 1).into_owned(), cell.style)
582 } else {
583 let key = if node.expanded { "tree-expanded" } else { "tree-collapsed" };
584 let glyph = text::truncate(&cx.env().icons().glyph(key), 1).into_owned();
585 (glyph, cx.style("tree-chevron", None, states).text())
586 };
587 let icon: Vec<row::Mark> =
588 node.icon.iter().map(|key| row::icon(cx, key, node.icon_color.as_deref(), text_style.fg)).collect();
589 let chevrons = [chevron];
590 let parts = row::Parts {
591 indent: row.depth.saturating_mul(INDENT),
592 fixed: if row.chevrons { &chevrons } else { &[] },
593 sliding: &icon,
594 label: &node.label,
595 trailing: detail_width.saturating_add(meter_width),
596 };
597 row::paint_parts(cx, rect, style, slide, &parts);
598 if let Some((fraction, tone)) = meter {
599 let ground = text_style.bg.unwrap_or_else(|| cx.color("surface"));
600 let ink = text_style.fg.unwrap_or_else(|| cx.color("text"));
601 let mut fill = cx.color(tone);
602 if node.faint {
603 fill = fill.mix(ground, 0.45);
604 }
605 let end = rect.right() - 1 - i32::from(detail_width);
606 let bar = Rect::new(end - i32::from(METER), rect.y, METER, 1);
607 cx.fill(bar, ground.mix(ink, 0.12));
608 let fraction = f32::from(*fraction) / 1000.0;
609 super::eighths::horizontal(cx, bar, super::eighths::eighths(fraction, METER), fill);
610 }
611 if let Some(detail) = &node.detail {
612 let detail_style = cx.style("list-detail", None, states).text();
613 row::paint_trailing(cx, rect, detail, detail_style);
614 }
615 }
616}
617
618impl<Msg: 'static> Widget<Msg> for Tree<Msg> {
619 fn measure(&self, _cx: &mut MeasureCx<'_>, available: Size) -> Size {
620 let flat = self.flatten();
621 let widest = flat
622 .iter()
623 .map(|row| {
624 [
626 LEAD,
627 row.depth.saturating_mul(INDENT),
628 if row.chevrons { 2 } else { 0 },
629 row.node.icon.as_ref().map_or(0, |_| 2),
630 text::width(&row.node.label),
631 row.node.detail.as_deref().map_or(0, |d| text::width(d).saturating_add(2)),
632 if row.node.meter.is_some() { METER + 1 } else { 0 },
633 2,
634 ]
635 .into_iter()
636 .fold(0, u16::saturating_add)
637 })
638 .max()
639 .unwrap_or_else(|| text::width(&self.empty).saturating_add(LEAD));
640 let rows = clamp_u16(i32::try_from(flat.len().max(1)).unwrap_or(i32::MAX));
641 Size::new(widest, rows).min(available)
642 }
643
644 fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
645 cx.register_hit(area);
646 if self.roots.is_empty() {
647 let faint = cx.style("list-header", None, &[]).text();
648 cx.text(area.x + i32::from(LEAD), area.y, &self.empty, faint, area.width.saturating_sub(LEAD));
649 return;
650 }
651 let drag = self.drag(cx);
652 let menu_node = self.menu_node(cx);
655 if menu_node.is_some() {
656 cx.request_overlay(area);
657 }
658 let aim = drag.as_ref().map(|drag| {
659 let offset = cx.memory::<RowScroll>().offset;
660 self.aim(&drag.keys, drag.pointer, Self::rows_area(area, self.flatten().len()), offset)
661 });
662 let flat = match (&drag, &aim) {
663 (Some(drag), Some(Aim::Reorder(order))) => {
664 let parent = self.siblings(&drag.key).and_then(|(parent, _)| parent);
665 self.flatten_with(Some(&Arrange { key: &drag.key, parent, order: *order }))
666 }
667 (Some(drag), _) => self.drag_layout(&drag.keys),
668 (None, _) => self.flatten(),
669 };
670 let slot = drag.as_ref().filter(|drag| self.reorders(&drag.keys)).map(|drag| drag.key.as_str());
673 let focused = cx.is_focused();
674 let pressed = cx.is_pressed();
675 let selected = self.selected_index(&flat);
676 let visible = usize::from(area.height);
677 let offset = cx.memory::<RowScroll>().follow(selected, flat.len(), visible);
678 let width = Self::rows_area(area, flat.len()).width;
679 let spinning = Self::loading_marks(cx, &flat);
680 let pointer = cx.pointer().filter(|_| drag.is_none() && menu_node.is_none());
681 for (row, index) in (offset..flat.len()).take(visible).enumerate() {
682 let rect = Rect::new(area.x, area.y + i32::try_from(row).unwrap_or(0), width, 1);
683 let key = flat[index].node.key.as_str();
684 if slot == Some(key) {
685 tab_model::paint_drop_slot(cx, rect);
686 continue;
687 }
688 if let Some(aim) = &aim
689 && Self::paint_target(cx, rect, &flat[index], aim)
690 {
691 continue;
692 }
693 let touched = pointer.is_some_and(|(x, y)| rect.contains(x, y)) || menu_node.as_deref() == Some(key);
694 let cursor = selected == Some(index);
695 let chosen = self.is_chosen(key);
696 let hovered = touched || (cursor && !chosen);
699 let flags = RowFlags {
700 hovered,
701 selected: chosen,
702 focused: focused && cursor,
703 pressed,
704 spinning: spinning.contains(&index),
705 pillar: cursor || hovered || !self.is_multi(),
706 };
707 self.paint_row(cx, rect, index, &flat[index], flags);
708 }
709 if aim == Some(Aim::Into(None)) {
710 let used = i32::try_from(flat.len().saturating_sub(offset)).unwrap_or(i32::MAX);
712 let top = area.y.saturating_add(used);
713 if top < area.bottom() {
714 let free = Rect::new(area.x, top, width, clamp_u16(area.bottom() - top));
715 let bg = cx.style("tree-drop", None, &[]).text().bg;
716 if let Some(bg) = bg {
717 cx.fill(free, bg);
718 }
719 }
720 }
721 if let Some(drawn) = cx.memory::<TreeBox>().drawn() {
722 select_box::paint(cx, drawn, Self::rows_area(area, flat.len()));
723 }
724 if let Some(drag) = &drag
726 && matches!(aim, Some(Aim::Reorder(_)))
727 && let Some(row) = flat.iter().find(|row| row.node.key == drag.key)
728 && !area.is_empty()
729 {
730 let y = drag.pointer.1.clamp(area.y, area.bottom() - 1);
731 let rect = Rect::new(area.x, y, width, 1);
732 tab_model::paint_ghost_surface(cx, rect);
734 let ghost = cx.style("tab-ghost", None, &[]);
735 Self::paint_node(cx, rect, row, (&ghost, &[]), false, false);
736 }
737 rows::paint_scrollbar(cx, area, flat.len(), offset, None);
738 }
739
740 fn paint_overlay(&self, cx: &mut PaintCx<'_>, anchor: Rect) {
741 self.paint_menu(cx, anchor);
742 }
743
744 fn event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
745 let area = cx.area();
746 let flat = self.flatten();
747 if self.menu_event(cx, event, &flat) {
748 return true;
749 }
750 let current = self.selected_index(&flat);
751 match event {
752 Event::Key(key) => {
753 if self.move_key(cx, key) || self.selection_key(cx, key, &flat) {
754 return true;
755 }
756 if let Some(step) = Step::from_key(key) {
757 let Some(target) = step.apply_wrapping(current, flat.len(), usize::from(area.height), self.wrap)
758 else {
759 return false;
760 };
761 self.select_one(cx, &flat, target);
762 return true;
763 }
764 let Some(index) = current else { return false };
765 let row = &flat[index];
766 if key.is_plain(Key::Right) || key.is_plain(Key::Char('l')) {
767 if !row.node.expanded {
768 return self.expand(cx, row.node, true);
769 }
770 if !row.node.children.is_empty() {
771 self.select_one(cx, &flat, index + 1);
772 return true;
773 }
774 return false;
775 }
776 if key.is_plain(Key::Left) || key.is_plain(Key::Char('h')) {
777 if row.node.expanded {
778 return self.expand(cx, row.node, false);
779 }
780 return row.parent.is_some_and(|parent| {
781 self.select_one(cx, &flat, parent);
782 true
783 });
784 }
785 if key.is_plain(Key::Enter) {
786 return self.open_or_activate(cx, index, row.node);
787 }
788 if key.is_plain(Key::Space) {
789 return self.activate(cx, index, row.node);
790 }
791 false
792 }
793 Event::Mouse(mouse) => {
794 if rows::scroll_mouse(cx, mouse, area, flat.len()) {
795 return true;
796 }
797 let offset = cx.memory::<RowScroll>().offset;
798 let index = usize::try_from(mouse.y - area.y).ok().map(|r| offset + r).filter(|i| *i < flat.len());
799 let on_chevron = index.is_some_and(|index| {
800 let row = &flat[index];
801 let chevron = Self::chevron_x(area, row.depth);
802 row.node.expandable && (chevron..=chevron + 1).contains(&mouse.x)
803 });
804 if let Some(used) = self.box_pointer(cx, mouse, &flat, index) {
805 return used;
806 }
807 if (self.on_move.is_some() || self.dropping.is_some())
808 && !on_chevron
809 && let Some(used) = self.drag_pointer(cx, mouse, &flat, index)
810 {
811 return used;
812 }
813 if mouse.kind != MouseKind::Down(MouseButton::Left) {
814 return false;
815 }
816 let Some(index) = index else {
817 return false;
818 };
819 let row = &flat[index];
820 if on_chevron {
821 return self.expand(cx, row.node, !row.node.expanded);
822 }
823 if self.modified_press(cx, &flat, index, mouse.mods) {
824 return true;
825 }
826 self.select_one(cx, &flat, index);
827 if self.activate_on == Click::Single || self.double_press(cx, &row.node.key) {
828 self.open_or_activate(cx, index, row.node);
829 }
830 true
831 }
832 _ => false,
833 }
834 }
835
836 fn focusable(&self) -> bool {
837 !self.roots.is_empty()
838 }
839}