Skip to main content

qframe/widgets/tree/
mod.rs

1//! Trees: nested rows that open and close, flattened to what is visible and virtualised.
2
3use 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
29/// Cells of indentation per level.
30const INDENT: u16 = 2;
31
32/// One node of a [`Tree`], identified by a key that stays the same while the tree changes.
33#[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    /// A leaf named `label`, identified by `key`.
49    #[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    /// Child nodes; a node with children can be opened.
66    #[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    /// Marks a node as openable before its children are known, for children loaded when it
74    /// opens. Opening it sends [`Tree::on_expand`]; supply the children in a later frame.
75    #[must_use]
76    pub fn expandable(mut self, expandable: bool) -> Self {
77        self.expandable = expandable || !self.children.is_empty();
78        self
79    }
80
81    /// Whether the node is open and shows its children.
82    #[must_use]
83    pub fn expanded(mut self, expanded: bool) -> Self {
84        self.expanded = expanded;
85        self
86    }
87
88    /// Marks the node's children as being loaded. A load that takes longer than about 300 ms
89    /// shows a spinner in place of the chevron, which then stays at least about 500 ms; quicker
90    /// loads keep the chevron, so they never flash a spinner.
91    #[must_use]
92    pub fn loading(mut self, loading: bool) -> Self {
93        self.loading = loading;
94        self
95    }
96
97    /// Icon key drawn before the label, optionally in theme colour `color`.
98    #[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    /// Faint text aligned right, e.g. a count.
106    #[must_use]
107    pub fn detail(mut self, detail: impl Into<String>) -> Self {
108        self.detail = Some(detail.into());
109        self
110    }
111
112    /// Draws the node faint while keeping it selectable.
113    #[must_use]
114    pub fn faint(mut self, faint: bool) -> Self {
115        self.faint = faint;
116        self
117    }
118}
119
120/// A visible row of the flattened tree.
121struct Flat<'a> {
122    node: &'a TreeNode,
123    depth: u16,
124    parent: Option<usize>,
125    /// The node's position among its siblings as the application gave them.
126    index: usize,
127}
128
129/// How one row is drawn besides its node.
130#[derive(Debug, Clone, Copy)]
131struct RowFlags {
132    hovered: bool,
133    selected: bool,
134    focused: bool,
135    pressed: bool,
136    spinning: bool,
137    /// Whether the row keeps its pillar: every touched row of a plain tree, only the cursor's
138    /// (or the hovered) row among several selected ones.
139    pillar: bool,
140}
141
142/// Delayed loading spinners of the nodes on their way, by node key, kept in runtime memory.
143#[derive(Debug, Default)]
144struct LoadingMarks(Vec<(String, DelayedIndicator)>);
145
146/// Builds a message from a node key.
147type KeyMessage<Msg> = Box<dyn Fn(&str) -> Msg>;
148
149/// Builds a message from a node key and whether it should open.
150type ExpandMessage<Msg> = Box<dyn Fn(&str, bool) -> Msg>;
151
152/// Builds a message from a move among siblings.
153type MoveMessage<Msg> = Box<dyn Fn(TreeMove) -> Msg>;
154
155/// Builds a message from a whole new selection.
156type SelectionMessage<Msg> = Box<dyn Fn(Vec<String>) -> Msg>;
157
158/// Builds the context menu entries of a node from its key.
159type MenuItems<Msg> = Box<dyn Fn(&str) -> Vec<ContextItem<Msg>>>;
160
161/// Nested rows that open and close, like folders.
162///
163/// The application owns the nodes, which are open and which one is selected, identified by
164/// node keys; the tree reports changes through messages. Only the open part of the tree is
165/// flattened and only the rows on screen are drawn, so large trees stay fast. Children can be
166/// loaded when a node opens: mark it [`TreeNode::expandable`], answer [`Tree::on_expand`] with a
167/// background command and mark the node [`TreeNode::loading`] meanwhile; its spinner only shows
168/// when the load is slow.
169///
170/// Rows are indented by space; openable rows carry a chevron. A hovered or selected row raises
171/// its surface and shows the pillar; only its icon and label slide one cell right. The
172/// indentation, the chevron (or the loading spinner in its place) and the detail never move, so
173/// the chevron is always where the pointer clicks it.
174///
175/// Keys while focused: ↑/↓ or k/j, PgUp/PgDn, Home/End move; → opens a node or moves to its
176/// first child; ← closes it or moves to its parent; Enter opens or closes a node with children
177/// and activates a leaf; Space activates. A click selects a row and opens, closes or activates
178/// it like Enter; a click on the chevron only opens or closes.
179///
180/// Four capabilities are off until asked for:
181///
182/// - [`multi_select`](Self::multi_select): several nodes are selected at once with Ctrl+click,
183///   Shift+click, Shift+arrows and Space; they share the selection tone while only the cursor's
184///   row carries the pillar and slides.
185/// - [`reorderable`](Self::reorderable): drag a node to move it among its siblings; the siblings
186///   make room, a ghost row follows the pointer and a tinted slot shows where it lands, while the
187///   dragged node's own children fold away. Ctrl+Shift+↑/↓ moves the selected node one place. A
188///   node keeps its parent: moving under another parent is the application's own action, offered
189///   in the context menu. Held on the top or bottom row, or past them, a drag scrolls the tree one
190///   row after 400 ms and then every 150 ms. With reordering or dropping on, a click opens, closes or
191///   activates on release, so pressing a row to drag it does not open it.
192/// - [`droppable`](Self::droppable): drag the selection into a node that takes it, such as a
193///   folder; the target takes the accent tone, a refused one stays faint, and a closed one opens
194///   when the drag rests on it.
195/// - [`context_menu`](Self::context_menu): a right click on a row opens a menu of actions for that
196///   node at the pointer and keeps the row raised while it is open; the menu key or Shift+F10
197///   opens the menu of the selected node below its row. With several nodes selected, the menu of
198///   a selected row is for the whole selection, and a right click outside it first makes that
199///   row the selection. Without it a right click does nothing.
200///
201/// Style keys: rows use `list-item` (`hover`, `selected`, `focus`, `pressed`), `list-item.faint`,
202/// `list-detail` and `list-header` (empty text) like [`List`](super::List); `tree-chevron`
203/// (`fg`) with `hover` and `selected`; `spinner` for loading nodes; `scrollbar`. Icons:
204/// `tree-collapsed`, `tree-expanded`, `spinner`. A drag uses `tab-drop` for the landing slot and
205/// `tab-ghost` for the row following the pointer, like the tabs; a drop target uses `tree-drop`
206/// (`bg`, `fg`, `bold`) and a refused one `list-item.faint`; the menu uses the keys of
207/// [`ContextItem`].
208pub 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    /// A tree with top-level nodes `roots`.
224    #[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    /// The key of the selected node.
242    #[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    /// Lets several nodes be selected at once: `selected` holds their keys and `message(keys)`
249    /// asks the application to make `keys` the whole new selection.
250    ///
251    /// The node given to [`selected`](Self::selected) stays the cursor: the row the keys move
252    /// from, the only one with the pillar, while every selected row takes the selection tone.
253    /// Ctrl+click adds a row or takes it out and Shift+click selects the rows from the last plain
254    /// or Ctrl click to this one; Shift with ↑/↓, PgUp/PgDn or Home/End extends that range, Space
255    /// adds or takes out the cursor's row (instead of activating it) and Esc reduces several
256    /// selected nodes to the cursor's. A plain click or arrow selects that one row. Moving nodes
257    /// with the keys is the application's own cut and paste; the tree reports the selection.
258    #[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    /// Text shown when there are no nodes.
266    #[must_use]
267    pub fn empty_text(mut self, text: impl Into<String>) -> Self {
268        self.empty = text.into();
269        self
270    }
271
272    /// Message for moving the selection to a node.
273    #[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    /// Message for activating a node: Enter on a leaf, Space, a click on a leaf.
280    #[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    /// Message asking to open (`true`) or close (`false`) a node.
287    #[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    /// Makes nodes reorderable among their siblings: `message(TreeMove)` asks the application to
294    /// move one. [`TreeMove::apply`] applies it to the application's list of siblings.
295    #[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    /// Lets dragged nodes drop into other nodes, such as files into a folder: `accepts(key)` tells
302    /// whether the node with `key` takes drops (its folders, usually) and `message(TreeDrop)` asks
303    /// the application to move the nodes.
304    ///
305    /// A drag carries the pressed node, or the whole [selection](Self::multi_select) when it is
306    /// pressed on a selected row. The node under the pointer takes the accent tone when it can
307    /// take them; the dragged nodes themselves, their descendants and the node they are all in
308    /// already stay faint and refuse the drop. A closed node the drag rests on opens after a short
309    /// wait, so a drop reaches nodes inside it; the free space below the last row is the top level.
310    ///
311    /// With [`reorderable`](Self::reorderable) as well, a row that takes drops takes the node in
312    /// and any other row is a place among the dragged node's siblings, as without this option; a
313    /// drag of several nodes only drops. Ctrl+Shift+↑/↓ still reorders next to such rows.
314    #[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    /// Gives every node a context menu: `items(key)` builds the entries for the node with that key,
325    /// such as Rename, Archive or Move to. Choosing an entry sends its message.
326    #[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    /// The visible rows, laid out for a drag when `arrange` is given.
337    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    /// Enter and click: a node with children opens or closes, a leaf activates.
396    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    /// Moves the delayed spinner of every open row on to this frame and returns the rows whose
401    /// spinner shows. Marks of nodes that are gone or idle are forgotten.
402    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    /// Paints `row` as what a drag aims at, when it is: in `tree-drop` when it takes the drop,
433    /// faint and flat when it refuses it. True when painted.
434    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    /// Where the chevron of a row at `depth` starts, before any slide.
446    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        // Rows selected beside the cursor share its tone but neither its pillar nor its slide, so
455        // one row still reads as the place the keys move from.
456        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    /// Paints the node of `row` into `rect` in `style`, the look of a row in `states`.
462    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        // The chevron is a fixed mark: it stays in its column while the icon and label slide.
475        // Leaves keep the chevron's column empty so labels of one level line up.
476        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                // Saturating: labels and details can be wider than any screen.
511                [
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        // An open context menu takes the overlay and the pointer: only the row it acts on stays
538        // raised.
539        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        // Only a reordering drag leaves a slot where the node was and a ghost under the pointer;
556        // dropping into a node keeps the rows still and lights the target instead.
557        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            // A cursor outside the selection of a multi-select tree is raised like a hovered row,
582            // so the keys still show where they start.
583            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            // The top level takes the drop: the free rows below the last one light up.
596            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        // The dragged node follows the pointer as a ghost row, kept inside the tree.
607        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            // The ghost covers the row under it, so its surface is cleared first.
615            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}