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::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
32/// Cells of indentation per level.
33const INDENT: u16 = 2;
34
35/// Cells of a row's meter, and the gap before it.
36const METER: u16 = 6;
37
38/// One node of a [`Tree`], identified by a key that stays the same while the tree changes.
39#[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    /// The meter's fill in thousandths, kept whole so a node still compares exactly, and its tone.
52    meter: Option<(u16, String)>,
53}
54
55impl TreeNode {
56    /// A leaf named `label`, identified by `key`.
57    #[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    /// Child nodes; a node with children can be opened.
75    #[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    /// Marks a node as openable before its children are known, for children loaded when it
83    /// opens. Opening it sends [`Tree::on_expand`]; supply the children in a later frame.
84    #[must_use]
85    pub fn expandable(mut self, expandable: bool) -> Self {
86        self.expandable = expandable || !self.children.is_empty();
87        self
88    }
89
90    /// Whether the node is open and shows its children.
91    #[must_use]
92    pub fn expanded(mut self, expanded: bool) -> Self {
93        self.expanded = expanded;
94        self
95    }
96
97    /// Marks the node's children as being loaded. A load that takes longer than about 300 ms
98    /// shows a spinner in place of the chevron, which then stays at least about 500 ms; quicker
99    /// loads keep the chevron, so they never flash a spinner.
100    #[must_use]
101    pub fn loading(mut self, loading: bool) -> Self {
102        self.loading = loading;
103        self
104    }
105
106    /// Icon key drawn before the label, optionally in theme colour `color`.
107    #[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    /// Faint text aligned right, e.g. a count.
115    #[must_use]
116    pub fn detail(mut self, detail: impl Into<String>) -> Self {
117        self.detail = Some(detail.into());
118        self
119    }
120
121    /// A small block meter at the right of the row, left of the [`detail`](Self::detail),
122    /// filled to `fraction` (0 to 1) in theme colour `tone`, such as the progress towards a goal.
123    /// A faint row draws it faint too. A row too narrow for its label and the meter drops the
124    /// meter first, so the name is never cut for it; in the ASCII glyph mode it fills whole
125    /// cells.
126    #[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        // In 0..=1000 after the clamp, so the rounded value fits.
130        self.meter = Some(((fraction * 1000.0).round() as u16, tone.into()));
131        self
132    }
133
134    /// Draws the node faint while keeping it selectable.
135    #[must_use]
136    pub fn faint(mut self, faint: bool) -> Self {
137        self.faint = faint;
138        self
139    }
140}
141
142/// A visible row of the flattened tree.
143struct Flat<'a> {
144    node: &'a TreeNode,
145    depth: u16,
146    parent: Option<usize>,
147    /// The node's position among its siblings as the application gave them.
148    index: usize,
149    /// Whether the tree keeps a column for chevrons: false when no row can open, so a flat list
150    /// of leaves starts where a [`Menu`](super::Menu) beside it does.
151    chevrons: bool,
152}
153
154/// How one row is drawn besides its node.
155#[derive(Debug, Clone, Copy)]
156struct RowFlags {
157    hovered: bool,
158    selected: bool,
159    focused: bool,
160    pressed: bool,
161    spinning: bool,
162    /// Whether the row keeps its pillar: every touched row of a plain tree, only the cursor's
163    /// (or the hovered) row among several selected ones.
164    pillar: bool,
165}
166
167/// Delayed loading spinners of the nodes on their way, by node key, kept in runtime memory.
168#[derive(Debug, Default)]
169struct LoadingMarks(Vec<(String, DelayedIndicator)>);
170
171/// Builds a message from a node key.
172type KeyMessage<Msg> = Box<dyn Fn(&str) -> Msg>;
173
174/// Builds a message from a node key and whether it should open.
175type ExpandMessage<Msg> = Box<dyn Fn(&str, bool) -> Msg>;
176
177/// Builds a message from a move among siblings.
178type MoveMessage<Msg> = Box<dyn Fn(TreeMove) -> Msg>;
179
180/// Builds a message from a whole new selection.
181type SelectionMessage<Msg> = Box<dyn Fn(Vec<String>) -> Msg>;
182
183/// Builds a message from a drop into a node.
184type DropMessage<Msg> = Box<dyn Fn(TreeDrop) -> Msg>;
185
186/// Builds the context menu entries of a node from its key.
187type MenuItems<Msg> = Box<dyn Fn(&str) -> Vec<ContextItem<Msg>>>;
188
189/// Nested rows that open and close, like folders.
190///
191/// The application owns the nodes, which are open and which one is selected, identified by
192/// node keys; the tree reports changes through messages. Only the open part of the tree is
193/// flattened and only the rows on screen are drawn, so large trees stay fast. Children can be
194/// loaded when a node opens: mark it [`TreeNode::expandable`], answer [`Tree::on_expand`] with a
195/// background command and mark the node [`TreeNode::loading`] meanwhile; its spinner only shows
196/// when the load is slow.
197///
198/// Rows are indented by space; openable rows carry a chevron. A tree where no row opens keeps no
199/// column for chevrons, so a flat list of leaves lines up with a [`Menu`](super::Menu) beside it. A hovered or selected row raises
200/// its surface and shows the pillar; only its icon and label slide one cell right. The
201/// indentation, the chevron (or the loading spinner in its place) and the detail never move, so
202/// the chevron is always where the pointer clicks it.
203///
204/// Keys while focused: ↑/↓ or k/j, PgUp/PgDn, Home/End move (↑/↓ round the ends with
205/// [`wrap`](Self::wrap)); → opens a node or moves to its
206/// first child; ← closes it or moves to its parent; Enter opens or closes a node with children
207/// and activates a leaf; Space activates. A click selects a row and opens, closes or activates
208/// it like Enter; a click on the chevron only opens or closes. With
209/// [`activate_on(Click::Double)`](Self::activate_on) a click only selects and a double click does
210/// what Enter does, while the chevron still opens and closes with one click.
211///
212/// Five capabilities are off until asked for:
213///
214/// - [`multi_select`](Self::multi_select): several nodes are selected at once with Ctrl+click,
215///   Shift+click, Shift+arrows, Ctrl+A and Space; they share the selection tone while only the cursor's
216///   row carries the pillar and slides.
217/// - [`reorderable`](Self::reorderable): drag a node to move it among its siblings; the siblings
218///   make room, a ghost row follows the pointer and a tinted slot shows where it lands, while the
219///   dragged node's own children fold away. Ctrl+Shift+↑/↓ moves the selected node one place. A
220///   node keeps its parent: moving under another parent is the application's own action, offered
221///   in the context menu. Held on the top or bottom row, or past them, a drag scrolls the tree one
222///   row after 400 ms and then every 150 ms. With reordering or dropping on, a click opens, closes or
223///   activates on release, so pressing a row to drag it does not open it.
224/// - [`droppable`](Self::droppable): drag the selection into a node that takes it, such as a
225///   folder; the target takes the accent tone, a refused one stays faint, and a closed one opens
226///   when the drag rests on it.
227/// - [`box_select`](Self::box_select): with several selected, a drag from the free space below the
228///   rows draws a box and the rows it covers become the selection.
229/// - [`context_menu`](Self::context_menu): a right click on a row opens a menu of actions for that
230///   node at the pointer and keeps the row raised while it is open; the menu key or Shift+F10
231///   opens the menu of the selected node below its row. With several nodes selected, the menu of
232///   a selected row is for the whole selection, and a right click outside it first makes that
233///   row the selection. Without it a right click does nothing.
234///
235/// Style keys: rows use `list-item` (`hover`, `selected`, `focus`, `pressed`), `list-item.faint`,
236/// `list-detail` and `list-header` (empty text) like [`List`](super::List); `tree-chevron`
237/// (`fg`) with `hover` and `selected`; `spinner` for loading nodes; `scrollbar`. Icons:
238/// `tree-collapsed`, `tree-expanded`, `spinner`. A drag uses `tab-drop` for the landing slot and
239/// `tab-ghost` for the row following the pointer, like the tabs; a drop target uses `tree-drop`
240/// (`bg`, `fg`, `bold`) and a refused one `list-item.faint`; the selection box uses
241/// `text-selection` (`bg`); the menu uses the keys of [`ContextItem`].
242pub 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    /// A tree with top-level nodes `roots`.
262    #[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    /// Lets ↓ (or j) on the last row shown go on to the first and ↑ (or k) on the first go to the
284    /// last, as a menu does. PgUp/PgDn and Home/End still stop at the ends, and Shift with an
285    /// arrow never wraps a range. Off by default: the keys stop at either end.
286    #[must_use]
287    pub fn wrap(mut self, wrap: bool) -> Self {
288        self.wrap = wrap;
289        self
290    }
291
292    /// The key of the selected node.
293    #[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    /// Lets several nodes be selected at once: `selected` holds their keys and `message(keys)`
300    /// asks the application to make `keys` the whole new selection.
301    ///
302    /// The node given to [`selected`](Self::selected) stays the cursor: the row the keys move
303    /// from, the only one with the pillar, while every selected row takes the selection tone.
304    /// Ctrl+click adds a row or takes it out and Shift+click selects the rows from the last plain
305    /// or Ctrl click to this one; Shift with ↑/↓, PgUp/PgDn or Home/End extends that range, Ctrl+A
306    /// selects every row shown, Space adds or takes out the cursor's row (instead of activating it) and Esc reduces several
307    /// selected nodes to the cursor's. A plain click or arrow selects that one row. Moving nodes
308    /// with the keys is the application's own cut and paste; the tree reports the selection.
309    #[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    /// Text shown when there are no nodes.
317    #[must_use]
318    pub fn empty_text(mut self, text: impl Into<String>) -> Self {
319        self.empty = text.into();
320        self
321    }
322
323    /// Message for moving the selection to a node.
324    #[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    /// Message for activating a node: Enter on a leaf, Space, a click on a leaf.
331    #[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    /// Message asking to open (`true`) or close (`false`) a node.
338    #[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    /// Makes nodes reorderable among their siblings: `message(TreeMove)` asks the application to
345    /// move one. [`TreeMove::apply`] applies it to the application's list of siblings.
346    #[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    /// Lets dragged nodes drop into other nodes, such as files into a folder: `accepts(key)` tells
353    /// whether the node with `key` takes drops (its folders, usually) and `message(TreeDrop)` asks
354    /// the application to move the nodes.
355    ///
356    /// A drag carries the pressed node, or the whole [selection](Self::multi_select) when it is
357    /// pressed on a selected row. The node under the pointer takes the accent tone when it can
358    /// take them; the dragged nodes themselves, their descendants and the node they are all in
359    /// already stay faint and refuse the drop. A closed node the drag rests on opens after a short
360    /// wait, so a drop reaches nodes inside it; the free space below the last row is the top level.
361    ///
362    /// With [`reorderable`](Self::reorderable) as well, a row that takes drops takes the node in
363    /// and any other row is a place among the dragged node's siblings, as without this option; a
364    /// drag of several nodes only drops. Ctrl+Shift+↑/↓ still reorders next to such rows.
365    #[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    /// A drop released with Ctrl held asks for a copy with `message` instead of the move of
376    /// [`droppable`](Self::droppable), the way a file explorer copies. A terminal that does not
377    /// report Ctrl with the pointer always moves. It does nothing without `droppable`.
378    #[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    /// How many clicks open a row: [`Click::Single`], the default, selects a row and opens,
385    /// closes or activates it at once; [`Click::Double`] only selects on a click and does what
386    /// Enter does on a second press on the same row within [`Click::INTERVAL`]. The chevron opens
387    /// and closes with one click either way, and so do ← and →.
388    #[must_use]
389    pub fn activate_on(mut self, click: Click) -> Self {
390        self.activate_on = click;
391        self
392    }
393
394    /// Lets a drag from the free space below the rows draw a box: the rows it covers become the
395    /// selection while it is drawn, or join it when Ctrl was held at the press, and a click there
396    /// without a drag clears the selection. The box is a tone laid over the cells it covers, never
397    /// a frame. It needs [`multi_select`](Self::multi_select) and does nothing without it.
398    #[must_use]
399    pub fn box_select(mut self, on: bool) -> Self {
400        self.box_select = on;
401        self
402    }
403
404    /// Gives every node a context menu: `items(key)` builds the entries for the node with that key,
405    /// such as Rename, Archive or Move to. Choosing an entry sends its message.
406    #[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    /// The visible rows, laid out for a drag when `arrange` is given.
417    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        // Every node that can open is on a visible row or under one, so the visible rows decide.
439        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    /// Enter and click: a node with children opens or closes, a leaf activates.
482    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    /// Moves the delayed spinner of every open row on to this frame and returns the rows whose
487    /// spinner shows. Marks of nodes that are gone or idle are forgotten.
488    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    /// Paints `row` as what a drag aims at, when it is: in `tree-drop` when it takes the drop,
519    /// faint and flat when it refuses it. True when painted.
520    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    /// Where the chevron of a row at `depth` starts, before any slide.
532    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        // Rows selected beside the cursor share its tone but neither its pillar nor its slide, so
541        // one row still reads as the place the keys move from.
542        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    /// Paints the node of `row` into `rect` in `style`, the look of a row in `states`.
548    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        // The meter only where the whole label still fits beside it.
560        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        // The chevron is a fixed mark: it stays in its column while the icon and label slide.
575        // Leaves keep the chevron's column empty so labels of one level line up.
576        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                // Saturating: labels and details can be wider than any screen.
625                [
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        // An open context menu takes the overlay and the pointer: only the row it acts on stays
653        // raised.
654        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        // Only a reordering drag leaves a slot where the node was and a ghost under the pointer;
671        // dropping into a node keeps the rows still and lights the target instead.
672        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            // A cursor outside the selection of a multi-select tree is raised like a hovered row,
697            // so the keys still show where they start.
698            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            // The top level takes the drop: the free rows below the last one light up.
711            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        // The dragged node follows the pointer as a ghost row, kept inside the tree.
725        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            // The ghost covers the row under it, so its surface is cleared first.
733            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}