Skip to main content

denise_ui/widgets/
tree.rs

1//! Rows at a depth, with a disclosure triangle and a hierarchy.
2
3use alloc::string::String;
4use alloc::vec::Vec;
5
6use denise::Pen;
7use denise::{ElementState, InputEvent, KeyCode, Point, Radius, Rect, Role, Theme};
8use denise_text::{TextEngine, TextStyle};
9
10use crate::widget::{
11    Event, EventCtx, Handled, MeasureCtx, Measured, Offer, PaintCtx, VisualState, Widget,
12};
13use crate::widgets::describe::{
14    Describe, DynDescribe, Group, Mismatch, Payload, Property, PropertyKind, ROLES, Value,
15};
16use crate::widgets::style::{
17    Align, ClickPair, Intent, RowKind, columns, draw_aligned, focus_ring, hovered_row, row_colors,
18};
19
20/// One row of a tree: a label at a depth, and optionally something before and
21/// after it.
22///
23/// ```
24/// # use denise_ui::TreeItem;
25/// TreeItem::new("Nettverk");
26/// TreeItem::new("Wi-Fi").at_depth(1).with_trailing("på");
27/// TreeItem::new("Ikke ferdig").at_depth(1).disabled();
28/// ```
29#[derive(Clone, Debug, PartialEq, Eq)]
30pub struct TreeItem {
31    text: String,
32    leading: String,
33    trailing: String,
34    depth: u16,
35    open: bool,
36    enabled: bool,
37}
38
39impl TreeItem {
40    /// A row at the top level, open, enabled.
41    ///
42    /// Open by default because a tree that arrives entirely shut shows one row
43    /// per top-level branch and nothing of what it is for.
44    pub fn new(text: impl Into<String>) -> Self {
45        Self {
46            text: text.into(),
47            leading: String::new(),
48            trailing: String::new(),
49            depth: 0,
50            open: true,
51            enabled: true,
52        }
53    }
54
55    /// How deep this row sits. `0` is the top level.
56    ///
57    /// The hierarchy **is** this number: a row is a child of the nearest row
58    /// above it with a smaller depth. See the note on [`Tree`] for why the rows
59    /// are flat.
60    pub fn at_depth(mut self, depth: u16) -> Self {
61        self.depth = depth;
62        self
63    }
64
65    /// Starts this row shut, so what is under it is not drawn until it is opened.
66    pub fn shut(mut self) -> Self {
67        self.open = false;
68        self
69    }
70
71    /// Puts a short run of text in a column before the label.
72    pub fn with_leading(mut self, leading: impl Into<String>) -> Self {
73        self.leading = leading.into();
74        self
75    }
76
77    /// Puts text at the trailing edge of the row: a value, a unit, a state.
78    pub fn with_trailing(mut self, trailing: impl Into<String>) -> Self {
79        self.trailing = trailing.into();
80        self
81    }
82
83    /// Makes the row unselectable: skipped by the keyboard, inert to the mouse.
84    ///
85    /// A disabled row that has children still opens and shuts: what is under it
86    /// may well be reachable even when it is not.
87    pub fn disabled(mut self) -> Self {
88        self.enabled = false;
89        self
90    }
91
92    /// The row's label.
93    pub fn text(&self) -> &str {
94        &self.text
95    }
96
97    /// The text before the label.
98    pub fn leading(&self) -> &str {
99        &self.leading
100    }
101
102    /// The text at the trailing edge.
103    pub fn trailing(&self) -> &str {
104        &self.trailing
105    }
106
107    /// How deep this row sits.
108    #[inline]
109    pub const fn depth(&self) -> u16 {
110        self.depth
111    }
112
113    /// Whether what is under this row is drawn.
114    #[inline]
115    pub const fn is_open(&self) -> bool {
116        self.open
117    }
118
119    /// Whether this row can be selected.
120    #[inline]
121    pub const fn is_enabled(&self) -> bool {
122        self.enabled
123    }
124
125    /// Replaces the label.
126    pub fn set_text(&mut self, text: impl Into<String>) {
127        self.text = text.into();
128    }
129
130    /// Opens or shuts this row.
131    pub fn set_open(&mut self, open: bool) {
132        self.open = open;
133    }
134
135    /// Enables or disables this row.
136    pub fn set_enabled(&mut self, enabled: bool) {
137        self.enabled = enabled;
138    }
139
140    fn trailing_width(&self, engine: &mut TextEngine, style: TextStyle) -> i32 {
141        if self.trailing.is_empty() {
142            0
143        } else {
144            engine.measure_line(style, &self.trailing)
145        }
146    }
147}
148
149impl From<&str> for TreeItem {
150    fn from(text: &str) -> Self {
151        Self::new(text)
152    }
153}
154
155impl From<String> for TreeItem {
156    fn from(text: String) -> Self {
157        Self::new(text)
158    }
159}
160
161/// A hierarchy: rows at a depth, opened and shut by a disclosure triangle.
162///
163/// What [`List`](super::List) is to a column of choices, this is to a settings
164/// hierarchy, a menu of menus, or a file list on a panel.
165///
166/// ```
167/// # use denise_ui::{Tree, TreeItem};
168/// enum Message { Pick(usize), Open(usize), Fold(usize) }
169/// Tree::new(
170///     [
171///         TreeItem::new("Nettverk"),
172///         TreeItem::new("Wi-Fi").at_depth(1),
173///         TreeItem::new("Ethernet").at_depth(1),
174///         TreeItem::new("Skjerm"),
175///     ],
176///     Message::Pick,
177/// )
178/// .on_activate(Message::Open)
179/// .on_toggle(Message::Fold);
180/// ```
181///
182/// # Flat rows with a depth, not a nest
183///
184/// The rows are a slice and each carries how deep it is. A row's **parent is the
185/// nearest row above it with a smaller depth**, and its children are the run of
186/// deeper rows immediately below it. That is the whole data structure.
187///
188/// A nested type would be the obvious other choice and is worse here in three
189/// ways. It needs an allocation per branch in a crate that runs on a panel with
190/// no allocator to spare; it makes "which row is at this y" a walk instead of a
191/// count; and it cannot be handed to a widget from a form file without the file
192/// growing real nesting, which `.dform` deliberately does not have for content.
193///
194/// Nothing says `has_children` — it is **derived**, because in this
195/// representation it is not an independent fact: a row has children exactly when
196/// the row after it is deeper. A field for it could disagree with the depths, and
197/// then one of the two would be a lie.
198///
199/// # Scrolling belongs to the tree of nodes, and this widget cooperates with it
200///
201/// The same arrangement [`List`](super::List) documents at length: this draws the
202/// rows that fit and stops, and the scrolling version is this inside a node
203/// marked [`Ui::set_scrollable`](crate::Ui::set_scrollable), sized with
204/// [`preferred_height`](Tree::preferred_height). A keyboard selection below the
205/// fold reveals itself and the viewport follows.
206///
207/// [`preferred_height`](Tree::preferred_height) counts the rows that are
208/// **shown**, so opening a branch makes the widget want to be taller — which is
209/// the caller's cue to resize it and let the viewport scroll further.
210///
211/// # Keyboard
212///
213/// One node, so one tab stop. [`List`](super::List)'s keyboard, plus the two the
214/// hierarchy adds:
215///
216/// | | |
217/// |---|---|
218/// | Up, Down | The previous and next **shown** row. Does not wrap. |
219/// | Right | Opens a shut row; on an open one, moves to its first child. |
220/// | Left | Shuts an open row; on a shut one or a leaf, moves to its parent. |
221/// | Home, End | The first and last shown row. |
222/// | Enter | Activates. |
223///
224/// Right and Left are the convention every tree control has used since the
225/// Windows 95 explorer, and the reason they do two things each is that the
226/// obvious one-thing version leaves a person on a shut row pressing Left with
227/// nothing happening.
228///
229/// # Three messages, because there are three things a person does
230///
231/// Selecting a row, acting on it, and opening it are different, so they are
232/// separate: [`new`](Tree::new) takes the selection,
233/// [`on_activate`](Tree::on_activate) takes Enter and the double-click, and
234/// [`on_toggle`](Tree::on_toggle) takes the triangle. Each reports the row's
235/// index **into the rows as given** — not its position among the shown ones,
236/// which changes whenever a branch above it folds, and not a path, which a
237/// `fn(usize) -> M` could not carry.
238#[derive(Clone, Debug)]
239pub struct Tree<M> {
240    items: Vec<TreeItem>,
241    selected: Option<usize>,
242    hovered: Option<usize>,
243    row_height: Option<i32>,
244    indent: i32,
245    selection: Option<fn(usize) -> M>,
246    activation: Option<fn(usize) -> M>,
247    toggle: Option<fn(usize) -> M>,
248    single_click: bool,
249    clicks: ClickPair,
250    role: Role,
251    style: TextStyle,
252}
253
254/// How far one level is indented from the one above, when nobody says.
255const INDENT: i32 = 14;
256
257impl<M> Tree<M> {
258    /// A tree with nothing selected, reporting selection through `message`.
259    pub fn new(
260        items: impl IntoIterator<Item = impl Into<TreeItem>>,
261        message: fn(usize) -> M,
262    ) -> Self {
263        Self {
264            items: items.into_iter().map(Into::into).collect(),
265            selection: Some(message),
266            ..Self::bare()
267        }
268    }
269
270    /// A tree that emits nothing, for a hierarchy the application reads rather
271    /// than reacts to.
272    pub fn inert(items: impl IntoIterator<Item = impl Into<TreeItem>>) -> Self {
273        Self {
274            items: items.into_iter().map(Into::into).collect(),
275            ..Self::bare()
276        }
277    }
278
279    fn bare() -> Self {
280        Self {
281            items: Vec::new(),
282            selected: None,
283            hovered: None,
284            row_height: None,
285            indent: INDENT,
286            selection: None,
287            activation: None,
288            toggle: None,
289            single_click: false,
290            clicks: ClickPair::default(),
291            role: Role::Primary,
292            style: TextStyle::built_in(16),
293        }
294    }
295
296    /// Sets the message sent when a row is activated by Enter or a double-click.
297    pub fn on_activate(mut self, message: fn(usize) -> M) -> Self {
298        self.activation = Some(message);
299        self
300    }
301
302    /// Sets the message sent when a row is opened or shut.
303    ///
304    /// The row's own `open` has already changed by the time this is emitted: the
305    /// widget owns what it draws, and an application that wants to veto a fold
306    /// wants a different widget.
307    pub fn on_toggle(mut self, message: fn(usize) -> M) -> Self {
308        self.toggle = Some(message);
309        self
310    }
311
312    /// Makes a single click or tap activate as well as select.
313    ///
314    /// The touch-panel answer; see [`List::activate_on_click`](super::List::activate_on_click).
315    pub fn activate_on_click(mut self) -> Self {
316        self.single_click = true;
317        self
318    }
319
320    /// Sets the initially selected row, by its index into the rows as given.
321    pub fn with_selected(mut self, index: Option<usize>) -> Self {
322        self.set_selected(index);
323        self
324    }
325
326    /// Sets the height of every row, overriding the theme's field height.
327    pub fn with_row_height(mut self, height: i32) -> Self {
328        self.row_height = Some(height.max(1));
329        self
330    }
331
332    /// Sets how far one level is indented from the one above.
333    pub fn with_indent(mut self, indent: i32) -> Self {
334        self.indent = indent.max(0);
335        self
336    }
337
338    /// Sets the colour role of the selected row.
339    pub fn with_role(mut self, role: Role) -> Self {
340        self.role = role;
341        self
342    }
343
344    /// Sets the rows' font and size.
345    pub fn with_style(mut self, style: TextStyle) -> Self {
346        self.style = style;
347        self
348    }
349
350    /// The selected row, by its index into the rows as given.
351    #[inline]
352    pub const fn selected(&self) -> Option<usize> {
353        self.selected
354    }
355
356    /// The selected row's item, if any.
357    pub fn selected_item(&self) -> Option<&TreeItem> {
358        self.items.get(self.selected?)
359    }
360
361    /// Selects a row. Out of range, disabled, or hidden under a shut branch
362    /// selects nothing.
363    ///
364    /// Hidden is refused rather than silently opening the branch: a selection
365    /// nobody can see is one the keyboard would then move from a place the
366    /// person is not looking at.
367    pub fn set_selected(&mut self, index: Option<usize>) {
368        self.selected = index.filter(|index| {
369            self.items.get(*index).is_some_and(TreeItem::is_enabled) && self.is_shown(*index)
370        });
371    }
372
373    /// The rows, as given.
374    pub fn items(&self) -> &[TreeItem] {
375        &self.items
376    }
377
378    /// Replaces the rows.
379    pub fn set_items(&mut self, items: impl IntoIterator<Item = impl Into<TreeItem>>) {
380        self.items = items.into_iter().map(Into::into).collect();
381        let selected = self.selected;
382        self.set_selected(selected);
383        self.hovered = None;
384        // A remembered click points at a row that may now be something else.
385        self.clicks.forget();
386    }
387
388    /// Opens or shuts one row, without reporting it.
389    ///
390    /// For an application driving the tree rather than answering it — restoring
391    /// what was open when a screen was last shown, say.
392    pub fn set_open(&mut self, index: usize, open: bool) {
393        if let Some(item) = self.items.get_mut(index) {
394            item.open = open;
395        }
396        let selected = self.selected;
397        self.set_selected(selected);
398    }
399
400    /// Opens or shuts every row that has children.
401    pub fn set_all_open(&mut self, open: bool) {
402        for item in &mut self.items {
403            item.open = open;
404        }
405        let selected = self.selected;
406        self.set_selected(selected);
407    }
408
409    /// Enables or disables one row.
410    pub fn set_row_enabled(&mut self, index: usize, enabled: bool) {
411        if let Some(item) = self.items.get_mut(index) {
412            item.set_enabled(enabled);
413        }
414        let selected = self.selected;
415        self.set_selected(selected);
416    }
417
418    /// Replaces the colour role of the selected row.
419    pub fn set_role(&mut self, role: Role) {
420        self.role = role;
421    }
422
423    /// Replaces the rows' font and size.
424    pub fn set_style(&mut self, style: TextStyle) {
425        self.style = style;
426    }
427
428    /// Whether the row at `index` has any children.
429    ///
430    /// Derived from the depths rather than stored; see the note on the type.
431    pub fn has_children(&self, index: usize) -> bool {
432        has_children(&self.items, index)
433    }
434
435    /// Whether the row at `index` is drawn — every branch above it being open.
436    pub fn is_shown(&self, index: usize) -> bool {
437        Shown::new(&self.items).any(|(shown, _)| shown == index)
438    }
439
440    /// How many rows are drawn.
441    pub fn shown_rows(&self) -> usize {
442        Shown::new(&self.items).count()
443    }
444
445    /// Height every row is drawn at.
446    pub fn row_height(&self, theme: &Theme) -> i32 {
447        self.row_height.unwrap_or(theme.metrics.size_field).max(1)
448    }
449
450    /// How many rows fit in `height`.
451    pub fn visible_rows(&self, theme: &Theme, height: i32) -> usize {
452        if height <= 0 {
453            return 0;
454        }
455        (height / self.row_height(theme)) as usize
456    }
457
458    /// Height this tree needs to show every row that is **currently** shown.
459    ///
460    /// Changes as branches open and shut, which is the point: a caller sizing a
461    /// tree inside a scrolling viewport asks again after a toggle.
462    pub fn preferred_height(&self, theme: &Theme) -> i32 {
463        let rows = self.shown_rows().max(1) as i64;
464        (i64::from(self.row_height(theme)) * rows).min(i64::from(i32::MAX)) as i32
465    }
466
467    /// Width the widest shown row needs, indentation and columns included.
468    pub fn preferred_width(&self, engine: &mut TextEngine) -> i32 {
469        let pad = padding(self.style.size_px);
470        let gutter = self.gutter();
471        let mut widest = 0;
472        let mut trailing = 0;
473        for (index, item) in Shown::new(&self.items) {
474            let _ = index;
475            let text = engine.measure_line(self.style, &item.text);
476            let leading = if item.leading.is_empty() {
477                0
478            } else {
479                engine.measure_line(self.style, &item.leading) + pad
480            };
481            widest = widest.max(self.indent_of(item) + gutter + leading + text);
482            trailing = trailing.max(item.trailing_width(engine, self.style));
483        }
484        let gap = if trailing > 0 { pad } else { 0 };
485        pad * 2 + widest + trailing + gap
486    }
487
488    /// The column the disclosure triangle stands in, before a row's content.
489    ///
490    /// Always reserved, whether or not the row has children, so that siblings at
491    /// one depth line up whatever they hold.
492    fn gutter(&self) -> i32 {
493        (i32::from(self.style.size_px) * 3 / 4).max(8)
494    }
495
496    /// How far this row's content is pushed in.
497    fn indent_of(&self, item: &TreeItem) -> i32 {
498        self.indent.saturating_mul(i32::from(item.depth))
499    }
500
501    /// The triangle's box inside a row, and the row's content after it.
502    fn parts(&self, row: Rect, item: &TreeItem) -> (Rect, Rect) {
503        let pad = padding(self.style.size_px);
504        let start = row.x + pad;
505        let right = (row.right() - pad).max(start);
506        // Clamped into the row before anything is measured from it. A depth the
507        // rectangle has no room for collapses to nothing at the right edge —
508        // the text is clipped rather than drawn outside the widget, which is
509        // what a row indented past its own width has to mean.
510        let left = start
511            .saturating_add(self.indent_of(item))
512            .clamp(start, right);
513        let triangle = Rect::from_edges(
514            left,
515            row.y,
516            left.saturating_add(self.gutter()).min(right),
517            row.bottom(),
518        );
519        let content = Rect::from_edges(triangle.right(), row.y, right, row.bottom());
520        (triangle, content)
521    }
522
523    /// Which shown row is at `point`, and whether the point is on its triangle.
524    ///
525    /// The y is arithmetic — rows are a fixed height — and mapping that to a row
526    /// is one walk of the depths, which is what a flat representation costs and
527    /// is cheaper than the nest it replaces.
528    fn hit(&self, bounds: Rect, row_height: i32, point: Point) -> Option<(usize, bool)> {
529        if !bounds.contains(point) {
530            return None;
531        }
532        let nth = (i64::from(point.y - bounds.y) / i64::from(row_height.max(1))) as usize;
533        let (index, item) = Shown::new(&self.items).nth(nth)?;
534        let row = row_rect(bounds, row_height, nth);
535        let (triangle, _) = self.parts(row, item);
536        let on_triangle = has_children(&self.items, index) && triangle.contains(point);
537        Some((index, on_triangle))
538    }
539
540    /// Moves the selection, reporting it. `None` means the end of the tree.
541    fn select(&mut self, target: Option<usize>, ctx: &mut EventCtx<'_, M>) -> Handled {
542        let Some(target) = target else {
543            return Handled::Yes;
544        };
545        if self.selected == Some(target) {
546            return Handled::Yes;
547        }
548        self.selected = Some(target);
549        if let Some(nth) = Shown::new(&self.items).position(|(index, _)| index == target) {
550            ctx.reveal(row_rect(ctx.bounds, self.row_height(ctx.theme), nth));
551        }
552        if let Some(message) = self.selection {
553            ctx.emit(message(target));
554        }
555        Handled::Yes
556    }
557
558    /// Opens or shuts a row, reporting it.
559    fn toggle(&mut self, index: usize, ctx: &mut EventCtx<'_, M>) -> Handled {
560        if !has_children(&self.items, index) {
561            return Handled::No;
562        }
563        let Some(item) = self.items.get_mut(index) else {
564            return Handled::No;
565        };
566        item.open = !item.open;
567        // Shutting a branch can hide the selection, which would otherwise leave
568        // the keyboard walking from somewhere nobody can see.
569        let selected = self.selected;
570        self.set_selected(selected);
571        if self.selected.is_none() && selected.is_some() {
572            self.selected = Some(index);
573        }
574        if let Some(message) = self.toggle {
575            ctx.emit(message(index));
576        }
577        Handled::Yes
578    }
579
580    /// Reports an activation, if anybody asked for one.
581    fn activate(&mut self, row: usize, ctx: &mut EventCtx<'_, M>) -> Handled {
582        if let Some(message) = self.activation {
583            ctx.emit(message(row));
584        }
585        Handled::Yes
586    }
587
588    /// Right: open a shut row, or step into an open one.
589    fn go_in(&mut self, ctx: &mut EventCtx<'_, M>) -> Handled {
590        let Some(index) = self.selected else {
591            let target = step(&self.items, None, true);
592            return self.select(target, ctx);
593        };
594        if !has_children(&self.items, index) {
595            return Handled::Yes;
596        }
597        if self.items.get(index).is_some_and(TreeItem::is_open) {
598            let child = step(&self.items, Some(index), true);
599            return self.select(child, ctx);
600        }
601        self.toggle(index, ctx)
602    }
603
604    /// Left: shut an open row, or step out to its parent.
605    fn go_out(&mut self, ctx: &mut EventCtx<'_, M>) -> Handled {
606        let Some(index) = self.selected else {
607            let target = step(&self.items, None, false);
608            return self.select(target, ctx);
609        };
610        let open = self.items.get(index).is_some_and(TreeItem::is_open);
611        if has_children(&self.items, index) && open {
612            return self.toggle(index, ctx);
613        }
614        match parent_of(&self.items, index) {
615            // A parent that cannot be selected is still where Left goes; the
616            // step from there carries on past it.
617            Some(parent) if self.items[parent].enabled => self.select(Some(parent), ctx),
618            _ => Handled::Yes,
619        }
620    }
621}
622
623/// The rows that are drawn, in order, as `(index, item)`.
624///
625/// A single forward pass: everything below a shut row is skipped until the depth
626/// comes back up to it. No allocation, which is why every part of this widget
627/// that needs "the rows in order" uses it rather than collecting a `Vec`.
628struct Shown<'a> {
629    items: &'a [TreeItem],
630    at: usize,
631    /// The depth of the shut row whose subtree is being skipped.
632    shut_at: Option<u16>,
633}
634
635impl<'a> Shown<'a> {
636    fn new(items: &'a [TreeItem]) -> Self {
637        Self {
638            items,
639            at: 0,
640            shut_at: None,
641        }
642    }
643}
644
645impl<'a> Iterator for Shown<'a> {
646    type Item = (usize, &'a TreeItem);
647
648    fn next(&mut self) -> Option<Self::Item> {
649        loop {
650            let index = self.at;
651            let item = self.items.get(index)?;
652            self.at += 1;
653            if let Some(depth) = self.shut_at {
654                if item.depth > depth {
655                    continue;
656                }
657                self.shut_at = None;
658            }
659            if !item.open && has_children(self.items, index) {
660                self.shut_at = Some(item.depth);
661            }
662            return Some((index, item));
663        }
664    }
665}
666
667/// Whether the row at `index` has children: the next row is deeper.
668fn has_children(items: &[TreeItem], index: usize) -> bool {
669    let Some(item) = items.get(index) else {
670        return false;
671    };
672    items
673        .get(index + 1)
674        .is_some_and(|next| next.depth > item.depth)
675}
676
677/// The nearest row above `index` with a smaller depth.
678fn parent_of(items: &[TreeItem], index: usize) -> Option<usize> {
679    let depth = items.get(index)?.depth;
680    if depth == 0 {
681        return None;
682    }
683    items[..index].iter().rposition(|item| item.depth < depth)
684}
685
686/// The first shown row that can be selected.
687fn first_enabled(items: &[TreeItem]) -> Option<usize> {
688    Shown::new(items)
689        .find(|(_, item)| item.enabled)
690        .map(|(index, _)| index)
691}
692
693/// The last shown row that can be selected.
694fn last_enabled(items: &[TreeItem]) -> Option<usize> {
695    Shown::new(items)
696        .filter(|(_, item)| item.enabled)
697        .map(|(index, _)| index)
698        .last()
699}
700
701/// The next selectable **shown** row in the direction of travel.
702///
703/// `None` means *stay where you are*: like [`List`](super::List) and unlike
704/// [`RadioGroup`](super::RadioGroup), this does not wrap.
705fn step(items: &[TreeItem], from: Option<usize>, forward: bool) -> Option<usize> {
706    let shown: Option<usize> = match from {
707        None => {
708            return if forward {
709                first_enabled(items)
710            } else {
711                last_enabled(items)
712            };
713        }
714        Some(from) => Shown::new(items).position(|(index, _)| index == from),
715    };
716    let shown = shown?;
717    let mut walk = Shown::new(items)
718        .enumerate()
719        .filter(|(_, (_, item))| item.enabled)
720        .map(|(nth, (index, _))| (nth, index));
721    if forward {
722        walk.find(|(nth, _)| *nth > shown).map(|(_, index)| index)
723    } else {
724        walk.take_while(|(nth, _)| *nth < shown)
725            .map(|(_, index)| index)
726            .last()
727    }
728}
729
730/// Space between a row's content and its edge, on one side.
731#[inline]
732const fn padding(size_px: u16) -> i32 {
733    let half = size_px as i32 / 2;
734    if half < 4 { 4 } else { half }
735}
736
737/// Where the n-th **shown** row sits.
738fn row_rect(bounds: Rect, row_height: i32, nth: usize) -> Rect {
739    let height = row_height.max(1);
740    let nth = nth.min(i32::MAX as usize) as i64;
741    let ceiling = i64::from(i32::MAX - height);
742    let y = (i64::from(bounds.y) + i64::from(height) * nth).min(ceiling) as i32;
743    Rect::new(bounds.x, y, bounds.width, height)
744}
745
746/// Draws a disclosure triangle, pointing down when open and along when shut.
747///
748/// Filled from horizontal spans rather than drawn as a glyph: the built-in font
749/// covers ASCII and Latin-1, so `▾` would come out as the missing-character box
750/// on a panel with no font file — which is the configuration this toolkit is for.
751fn disclosure(canvas: &mut Pen<'_>, box_of: Rect, open: bool, color: denise::Color) {
752    // Sized off the row so it scales with the text, and odd so it has a point.
753    let size = (box_of.height / 3).clamp(3, 9) | 1;
754    let cx = box_of.x + box_of.width / 2;
755    let cy = box_of.y + box_of.height / 2;
756    if open {
757        // Pointing down: a wide span at the top narrowing to a point.
758        for step in 0..=size {
759            let half = size - step;
760            canvas.fill_rect(
761                Rect::new(cx - half, cy - size / 2 + step, half * 2 + 1, 1),
762                color,
763            );
764        }
765    } else {
766        // Pointing along: a tall span at the left narrowing to a point.
767        for step in 0..=size {
768            let half = size - step;
769            canvas.fill_rect(
770                Rect::new(cx - size / 2 + step, cy - half, 1, half * 2 + 1),
771                color,
772            );
773        }
774    }
775}
776
777impl<M: 'static> Widget<M> for Tree<M> {
778    fn describe(&self) -> Option<&dyn DynDescribe> {
779        Some(self)
780    }
781
782    fn describe_mut(&mut self) -> Option<&mut dyn DynDescribe> {
783        Some(self)
784    }
785
786    fn measure(&self, ctx: &mut MeasureCtx<'_>, _offered: Offer) -> Measured {
787        // The height follows what is open, so this answer changes as branches
788        // fold — which is the caller's cue to ask again.
789        Measured::both(
790            self.preferred_width(ctx.text),
791            self.preferred_height(ctx.theme),
792        )
793    }
794
795    fn paint(&self, ctx: &mut PaintCtx<'_>, canvas: &mut Pen<'_>) {
796        let bounds = ctx.bounds;
797        if bounds.is_empty() || self.items.is_empty() {
798            return;
799        }
800        let (backdrop, _) = row_colors(ctx.theme, ctx.state, self.role, RowKind::Resting, true);
801        canvas.fill_rect(bounds, backdrop);
802
803        let row_height = self.row_height(ctx.theme);
804        let pad = padding(self.style.size_px);
805        let radius = ctx.theme.radius(Radius::Field);
806        let hovered = hovered_row(ctx.state, self.hovered);
807
808        for (nth, (index, item)) in Shown::new(&self.items).enumerate() {
809            let row = row_rect(bounds, row_height, nth);
810            if row.y >= bounds.bottom() {
811                // Past the bottom edge; this widget does not scroll.
812                break;
813            }
814            let kind = if self.selected == Some(index) {
815                RowKind::Selected
816            } else if hovered == Some(index) {
817                RowKind::Hovered
818            } else {
819                RowKind::Resting
820            };
821            let (fill, content) = row_colors(ctx.theme, ctx.state, self.role, kind, item.enabled);
822            if kind != RowKind::Resting {
823                canvas.fill_rounded_rect(row, radius, fill);
824            }
825            if kind == RowKind::Selected && ctx.state.contains(VisualState::FOCUSED) {
826                focus_ring(ctx.theme, row, radius, canvas);
827            }
828
829            let (triangle, rest) = self.parts(row, item);
830            if has_children(&self.items, index) && !triangle.is_empty() {
831                disclosure(canvas, triangle, item.open, content);
832            }
833
834            let trailing_width = item.trailing_width(ctx.text, self.style);
835            let leading_width = if item.leading.is_empty() {
836                0
837            } else {
838                ctx.text.measure_line(self.style, &item.leading)
839            };
840            let (leading, label, trailing) = columns(rest, pad, leading_width, trailing_width);
841            for (box_of, text, align) in [
842                (leading, &item.leading, Align::Start),
843                (label, &item.text, Align::Start),
844                (trailing, &item.trailing, Align::End),
845            ] {
846                if text.is_empty() || box_of.is_empty() {
847                    continue;
848                }
849                let mut column = canvas.with_clip(box_of);
850                draw_aligned(
851                    &mut column,
852                    ctx.text,
853                    self.style,
854                    box_of,
855                    (align, Align::Center),
856                    text,
857                    content,
858                );
859            }
860        }
861
862        if ctx.state.contains(VisualState::FOCUSED) && self.selected.is_none() {
863            focus_ring(ctx.theme, bounds, radius, canvas);
864        }
865    }
866
867    fn on_event(&mut self, event: &Event<'_>, ctx: &mut EventCtx<'_, M>) -> Handled {
868        if self.items.is_empty() {
869            return Handled::No;
870        }
871        let row_height = self.row_height(ctx.theme);
872
873        match event {
874            Event::Input(InputEvent::PointerMoved { position }) => {
875                let row = self
876                    .hit(ctx.bounds, row_height, *position)
877                    .map(|(index, _)| index)
878                    .filter(|index| self.items[*index].enabled);
879                if row == self.hovered {
880                    return Handled::No;
881                }
882                self.hovered = row;
883                Handled::Yes
884            }
885            Event::Input(InputEvent::PointerButton {
886                state: ElementState::Up,
887                position,
888                ..
889            })
890            | Event::Input(InputEvent::TouchUp {
891                position,
892                cancelled: false,
893                ..
894            }) => {
895                let Some((index, on_triangle)) = self.hit(ctx.bounds, row_height, *position) else {
896                    return Handled::No;
897                };
898                // The triangle is its own target: pressing it opens the branch
899                // and leaves the selection where it was, which is what lets
900                // somebody look inside a branch without losing their place.
901                if on_triangle {
902                    return self.toggle(index, ctx);
903                }
904                if !self.items[index].enabled {
905                    return Handled::No;
906                }
907                let intent = self.clicks.classify(index, ctx.now_ms, self.single_click);
908                let handled = self.select(Some(index), ctx);
909                if intent == Intent::Activate {
910                    self.activate(index, ctx);
911                }
912                handled
913            }
914            Event::Input(InputEvent::Key {
915                code,
916                state: ElementState::Down,
917                ..
918            }) if ctx.state.contains(VisualState::FOCUSED) => match code {
919                KeyCode::ArrowDown => {
920                    let target = step(&self.items, self.selected, true);
921                    self.select(target, ctx)
922                }
923                KeyCode::ArrowUp => {
924                    let target = step(&self.items, self.selected, false);
925                    self.select(target, ctx)
926                }
927                KeyCode::ArrowRight => self.go_in(ctx),
928                KeyCode::ArrowLeft => self.go_out(ctx),
929                KeyCode::Home => {
930                    let target = first_enabled(&self.items);
931                    self.select(target, ctx)
932                }
933                KeyCode::End => {
934                    let target = last_enabled(&self.items);
935                    self.select(target, ctx)
936                }
937                KeyCode::Enter | KeyCode::NumpadEnter => match self.selected {
938                    Some(row) if self.items.get(row).is_some_and(TreeItem::is_enabled) => {
939                        self.activate(row, ctx)
940                    }
941                    _ => Handled::No,
942                },
943                _ => Handled::No,
944            },
945            _ => Handled::No,
946        }
947    }
948
949    fn accepts_pointer(&self) -> bool {
950        true
951    }
952
953    /// A tree with no row anybody can choose is not a tab stop.
954    fn focusable(&self) -> bool {
955        self.items.iter().any(TreeItem::is_enabled)
956    }
957}
958
959impl<M> Describe for Tree<M> {
960    const KIND: &'static str = "tree";
961    const DOC: &'static str = "A hierarchy of rows that open and shut, indented by depth.";
962    const GROUP: Group = Group::Data;
963    const ICON: &'static denise::icon::Icon = &super::icons::TREE;
964
965    const PROPERTIES: &'static [Property] = &[
966        Property::new(
967            "item",
968            PropertyKind::List,
969            "The rows, as `item` child nodes, each at its own `depth`. Real data, like a list's.",
970        ),
971        Property::new(
972            "selected",
973            PropertyKind::Int { min: 0, max: 9999 },
974            "Which row is selected, by its position in the file.",
975        ),
976        Property::new(
977            "on-select",
978            PropertyKind::Message(Payload::Index),
979            "Sent with the row when the selection moves.",
980        ),
981        Property::new(
982            "on-activate",
983            PropertyKind::Message(Payload::Index),
984            "Sent with the row on Enter or a double-click.",
985        ),
986        Property::new(
987            "on-toggle",
988            PropertyKind::Message(Payload::Index),
989            "Sent with the row when a branch is opened or shut.",
990        ),
991        Property::new(
992            "activate-on-click",
993            PropertyKind::Bool,
994            "Whether one tap both selects and activates. For a touch panel.",
995        ),
996        Property::new(
997            "row-height",
998            PropertyKind::Int { min: 16, max: 200 },
999            "Height of every row in logical pixels, overriding the theme's field height.",
1000        )
1001        .in_pixels(),
1002        Property::new(
1003            "indent",
1004            PropertyKind::Int { min: 0, max: 100 },
1005            "How far one level is indented from the one above, in logical pixels.",
1006        )
1007        .in_pixels(),
1008        Property::new(
1009            "role",
1010            PropertyKind::Enum(ROLES),
1011            "The colour of the selected row.",
1012        ),
1013        Property::new(
1014            "size",
1015            PropertyKind::Int { min: 6, max: 96 },
1016            "Text size in logical pixels.",
1017        )
1018        .in_pixels(),
1019    ];
1020
1021    fn get(&self, name: &str) -> Option<Value> {
1022        Some(match name {
1023            "selected" => Value::Int(i32::try_from(self.selected?).ok()?),
1024            "activate-on-click" => Value::Bool(self.single_click),
1025            "row-height" => Value::Int(self.row_height?),
1026            "indent" => Value::Int(self.indent),
1027            "role" => Value::role(self.role),
1028            "size" => Value::Int(i32::from(self.style.size_px)),
1029            _ => return None,
1030        })
1031    }
1032
1033    fn apply(&mut self, name: &str, value: Value) -> Result<(), Mismatch> {
1034        match name {
1035            // Through the setter, so a row that is hidden or disabled selects
1036            // nothing here exactly as it does everywhere else.
1037            "selected" => self.set_selected(Some(value.as_index()?)),
1038            // Built from the child nodes; an inspector edits them
1039            // where they live. See `PropertyKind::List`.
1040            "on-select" | "on-activate" | "on-toggle" | "item" => return Err(Mismatch::Supplied),
1041            "activate-on-click" => self.single_click = value.as_bool()?,
1042            "row-height" => self.row_height = Some(value.as_int()?.max(1)),
1043            "indent" => self.indent = value.as_int()?.max(0),
1044            "role" => self.role = value.as_role()?,
1045            "size" => self.style.size_px = value.as_size()?,
1046            _ => return Err(Mismatch::Unknown),
1047        }
1048        Ok(())
1049    }
1050}
1051
1052#[cfg(test)]
1053mod tests {
1054    use super::*;
1055
1056    /// Nettverk
1057    ///   Wi-Fi
1058    ///     Hjemme
1059    ///   Ethernet
1060    /// Skjerm
1061    ///   Lysstyrke
1062    fn items() -> Vec<TreeItem> {
1063        alloc::vec![
1064            TreeItem::new("Nettverk"),
1065            TreeItem::new("Wi-Fi").at_depth(1),
1066            TreeItem::new("Hjemme").at_depth(2),
1067            TreeItem::new("Ethernet").at_depth(1),
1068            TreeItem::new("Skjerm"),
1069            TreeItem::new("Lysstyrke").at_depth(1),
1070        ]
1071    }
1072
1073    fn tree() -> Tree<usize> {
1074        Tree::new(items(), |row| row)
1075    }
1076
1077    /// The rows that would be drawn, by index.
1078    fn shown(tree: &Tree<usize>) -> Vec<usize> {
1079        Shown::new(&tree.items).map(|(index, _)| index).collect()
1080    }
1081
1082    #[test]
1083    fn the_hierarchy_is_the_depths_and_nothing_else() {
1084        let items = items();
1085        // A row has children exactly when the row after it is deeper. Nothing
1086        // stores this, so nothing can contradict it.
1087        assert!(has_children(&items, 0), "Nettverk holds Wi-Fi");
1088        assert!(has_children(&items, 1), "Wi-Fi holds Hjemme");
1089        assert!(!has_children(&items, 2), "Hjemme holds nothing");
1090        assert!(!has_children(&items, 3), "Ethernet holds nothing");
1091        assert!(has_children(&items, 4), "Skjerm holds Lysstyrke");
1092        assert!(!has_children(&items, 5), "the last row holds nothing");
1093
1094        // A parent is the nearest row above with a smaller depth.
1095        assert_eq!(parent_of(&items, 0), None);
1096        assert_eq!(parent_of(&items, 1), Some(0));
1097        assert_eq!(parent_of(&items, 2), Some(1));
1098        assert_eq!(parent_of(&items, 3), Some(0), "past its deeper sibling");
1099        assert_eq!(parent_of(&items, 5), Some(4));
1100    }
1101
1102    #[test]
1103    fn shutting_a_branch_hides_everything_under_it_however_deep() {
1104        let mut tree = tree();
1105        assert_eq!(shown(&tree), alloc::vec![0, 1, 2, 3, 4, 5]);
1106
1107        // Shutting Wi-Fi hides only its own child.
1108        tree.set_open(1, false);
1109        assert_eq!(shown(&tree), alloc::vec![0, 1, 3, 4, 5]);
1110
1111        // Shutting Nettverk hides Wi-Fi, its child, and Ethernet — the whole
1112        // subtree, not one level of it.
1113        tree.set_open(0, false);
1114        assert_eq!(shown(&tree), alloc::vec![0, 4, 5]);
1115
1116        // And opening it again leaves Wi-Fi as it was found: shutting a branch
1117        // remembers what was inside rather than flattening it.
1118        tree.set_open(0, true);
1119        assert_eq!(shown(&tree), alloc::vec![0, 1, 3, 4, 5]);
1120    }
1121
1122    #[test]
1123    fn a_row_with_no_children_is_never_shut_around() {
1124        // `open` on a leaf is meaningless and must not swallow its siblings.
1125        let mut items = items();
1126        items[2].open = false;
1127        let tree = Tree::new(items, |row| row);
1128        assert_eq!(shown(&tree), alloc::vec![0, 1, 2, 3, 4, 5]);
1129    }
1130
1131    #[test]
1132    fn the_keyboard_walks_the_rows_that_are_shown() {
1133        let mut tree = tree();
1134        tree.set_open(1, false);
1135
1136        // Down from Wi-Fi skips its hidden child and lands on Ethernet.
1137        assert_eq!(step(&tree.items, Some(1), true), Some(3));
1138        // And back again.
1139        assert_eq!(step(&tree.items, Some(3), false), Some(1));
1140
1141        // The ends do not wrap.
1142        assert_eq!(step(&tree.items, Some(5), true), None);
1143        assert_eq!(step(&tree.items, Some(0), false), None);
1144
1145        // From nothing selected, the near end in the direction of travel.
1146        assert_eq!(step(&tree.items, None, true), Some(0));
1147        assert_eq!(step(&tree.items, None, false), Some(5));
1148    }
1149
1150    #[test]
1151    fn disabled_rows_are_stepped_over_and_hidden_ones_are_not_reachable() {
1152        let mut items = items();
1153        items[1].enabled = false;
1154        items[3].enabled = false;
1155        let tree = Tree::new(items, |row| row);
1156
1157        // Both children of Nettverk are disabled, so Down from it reaches its
1158        // grandchild rather than stopping.
1159        assert_eq!(step(&tree.items, Some(0), true), Some(2));
1160        assert_eq!(step(&tree.items, Some(2), true), Some(4));
1161
1162        assert_eq!(first_enabled(&tree.items), Some(0));
1163        assert_eq!(last_enabled(&tree.items), Some(5));
1164    }
1165
1166    #[test]
1167    fn a_selection_under_a_branch_that_shuts_moves_to_the_branch() {
1168        let mut tree = tree();
1169        tree.set_selected(Some(2));
1170        assert_eq!(tree.selected(), Some(2));
1171
1172        // Shutting Wi-Fi through the setter leaves nothing selected rather than
1173        // a selection nobody can see.
1174        tree.set_open(1, false);
1175        assert_eq!(tree.selected(), None, "a hidden row stayed selected");
1176    }
1177
1178    #[test]
1179    fn a_hidden_or_disabled_row_cannot_be_selected() {
1180        let mut tree = tree();
1181        tree.set_open(0, false);
1182
1183        tree.set_selected(Some(2));
1184        assert_eq!(
1185            tree.selected(),
1186            None,
1187            "selected something under a shut branch"
1188        );
1189
1190        tree.set_selected(Some(9));
1191        assert_eq!(tree.selected(), None, "selected a row that is not there");
1192
1193        tree.set_row_enabled(4, false);
1194        tree.set_selected(Some(4));
1195        assert_eq!(tree.selected(), None, "selected a disabled row");
1196    }
1197
1198    #[test]
1199    fn rows_are_a_fixed_height_stacked_from_the_top() {
1200        let bounds = Rect::new(10, 20, 300, 400);
1201        let mut previous = bounds.y;
1202        for nth in 0..6 {
1203            let row = row_rect(bounds, 36, nth);
1204            assert_eq!(row.y, previous);
1205            assert_eq!(row.height, 36);
1206            assert_eq!(row.x, bounds.x);
1207            assert_eq!(row.right(), bounds.right());
1208            previous = row.bottom();
1209        }
1210    }
1211
1212    #[test]
1213    fn an_absurdly_deep_tree_neither_overflows_nor_panics() {
1214        // Arithmetic on somebody else's numbers: a panic inside a paint loop on
1215        // a kiosk is a black screen.
1216        let bounds = Rect::new(0, 0, 300, 400);
1217        let row = row_rect(bounds, 36, usize::MAX / 2);
1218        assert!(row.height > 0);
1219        assert!(row.bottom() >= row.y, "the rectangle inverted");
1220
1221        let deep = Tree::<usize>::inert(alloc::vec![
1222            TreeItem::new("a").at_depth(0),
1223            TreeItem::new("b").at_depth(u16::MAX),
1224        ]);
1225        assert!(
1226            deep.indent_of(&deep.items[1]) > 0,
1227            "the indent saturated wrong"
1228        );
1229        let row = row_rect(bounds, 36, 1);
1230        let (triangle, content) = deep.parts(row, &deep.items[1]);
1231        assert!(
1232            triangle.width >= 0 && content.width >= 0,
1233            "a column inverted"
1234        );
1235        assert!(content.right() <= row.right(), "a column left the row");
1236    }
1237
1238    #[test]
1239    fn the_triangle_is_its_own_target_and_the_rest_of_the_row_is_not() {
1240        let tree = tree();
1241        let bounds = Rect::new(0, 0, 300, 240);
1242        let row = row_rect(bounds, 40, 0);
1243        let (triangle, _) = tree.parts(row, &tree.items[0]);
1244
1245        let on_triangle = Point::new(triangle.x + triangle.width / 2, row.y + row.height / 2);
1246        assert_eq!(tree.hit(bounds, 40, on_triangle), Some((0, true)));
1247
1248        let on_label = Point::new(row.right() - 10, row.y + row.height / 2);
1249        assert_eq!(tree.hit(bounds, 40, on_label), Some((0, false)));
1250
1251        // A leaf has no triangle, so the same place on its row is the row.
1252        let leaf_row = row_rect(bounds, 40, 2);
1253        let (leaf_triangle, _) = tree.parts(leaf_row, &tree.items[2]);
1254        let on_nothing = Point::new(
1255            leaf_triangle.x + leaf_triangle.width / 2,
1256            leaf_row.y + leaf_row.height / 2,
1257        );
1258        assert_eq!(tree.hit(bounds, 40, on_nothing), Some((2, false)));
1259    }
1260
1261    #[test]
1262    fn a_deeper_rows_triangle_is_indented_with_it() {
1263        let tree = tree();
1264        let bounds = Rect::new(0, 0, 300, 240);
1265        let (top, _) = tree.parts(row_rect(bounds, 40, 0), &tree.items[0]);
1266        let (nested, _) = tree.parts(row_rect(bounds, 40, 1), &tree.items[1]);
1267        assert_eq!(nested.x - top.x, INDENT, "one level is one indent");
1268
1269        let (deeper, _) = tree.parts(row_rect(bounds, 40, 2), &tree.items[2]);
1270        assert_eq!(deeper.x - top.x, INDENT * 2);
1271    }
1272
1273    #[test]
1274    fn a_point_below_the_last_shown_row_is_not_the_last_row() {
1275        let mut tree = tree();
1276        tree.set_open(0, false);
1277        tree.set_open(4, false);
1278        let bounds = Rect::new(0, 0, 300, 400);
1279        // Two rows shown, so the third row's worth of space is nobody's.
1280        assert_eq!(tree.shown_rows(), 2);
1281        let below = Point::new(50, bounds.y + 40 * 2 + 5);
1282        assert_eq!(tree.hit(bounds, 40, below), None);
1283    }
1284
1285    #[test]
1286    fn the_height_it_asks_for_follows_what_is_open() {
1287        let theme = &denise::theme::DARK;
1288        let mut tree = tree();
1289        tree = tree.with_row_height(20);
1290        assert_eq!(tree.preferred_height(theme), 120, "six rows");
1291
1292        tree.set_open(0, false);
1293        assert_eq!(tree.preferred_height(theme), 60, "three rows");
1294
1295        // Never zero: a widget with no height is a widget nobody can click on
1296        // to open again.
1297        tree.set_all_open(false);
1298        assert!(tree.preferred_height(theme) > 0);
1299    }
1300
1301    #[test]
1302    fn an_empty_tree_is_inert_rather_than_broken() {
1303        let tree = Tree::<usize>::inert(Vec::<TreeItem>::new());
1304        assert_eq!(tree.shown_rows(), 0);
1305        assert_eq!(tree.selected(), None);
1306        assert!(
1307            !tree.focusable(),
1308            "an empty tree is a tab stop with nothing in it"
1309        );
1310        assert_eq!(
1311            tree.hit(Rect::new(0, 0, 100, 100), 20, Point::new(5, 5)),
1312            None
1313        );
1314        assert!(tree.preferred_height(&denise::theme::DARK) > 0);
1315    }
1316
1317    #[test]
1318    fn replacing_the_rows_keeps_a_selection_that_still_makes_sense() {
1319        let mut tree = tree();
1320        tree.set_selected(Some(3));
1321
1322        tree.set_items(items());
1323        assert_eq!(tree.selected(), Some(3), "the same row is still there");
1324
1325        // And drops one that is not.
1326        tree.set_items(alloc::vec![TreeItem::new("only")]);
1327        assert_eq!(tree.selected(), None);
1328    }
1329}