Skip to main content

qframe/widgets/card_grid/
mod.rs

1//! Card grids: cards in as many columns as fit, moved through with the keys in two dimensions.
2
3mod layout;
4#[cfg(test)]
5mod tests;
6
7use crate::env::Env;
8use crate::event::{Event, MouseButton, MouseKind};
9use crate::geometry::{Padding, Rect, Size, clamp_u16};
10use crate::keymap::Key;
11use crate::style::CellStyle;
12use crate::text;
13use crate::theme::State;
14use crate::widget::{Axis, EventCx, Flex, IdleScope, Length, MeasureCx, Node, PaintCx, View, Widget};
15
16use super::empty_state::EmptyState;
17use super::row_menu::{self, RowAnchor, RowMenuItems};
18use super::scrollbar::{self, ScrollbarStyle};
19use super::{ContextItem, IndexMessage};
20use layout::{Layout, Sizing, Step};
21
22/// Builds what one card shows.
23type CardBuilder<Msg> = Box<dyn Fn(&mut View<'_, Msg>, usize)>;
24
25/// Cards laid out in as many columns as fit, for a store's apps, a launcher's programs or a
26/// choice of profiles. Only the cards on screen are built and drawn, so a grid of ten thousand
27/// cards costs what one screen of them costs.
28///
29/// Every card is a surface one step above its background, with no frame. Under the pointer a
30/// card rises one tone with a soft pillar `▌` down its left edge; the selected card takes the
31/// selected surface, and its pillar breathes while the grid has focus reached with the keyboard.
32/// Nothing slides: a card is a surface, not a list row. Only one card is lit at a time: while
33/// the pointer moves over the grid it carries the highlight and the selected card rests; the
34/// next key goes on from the card the pointer is on.
35///
36/// The column count follows the width: cards are at least [`card_width`](Self::card_width)'s
37/// least width and share the room left, up to the widest. An area narrower than one card shows
38/// one column as wide as the area, and the card's content is cut there (build it with
39/// [`Text::no_wrap`](super::Text::no_wrap) so it ends in `…`).
40///
41/// The application owns the selection and the checked cards; the grid reports changes through
42/// messages. Keys while focused: arrows move between cards and stop at the edges (Right on the
43/// last card of a row stays there), Home and End go to the first and last card, PgUp and PgDn
44/// move a screen of rows, Enter activates, and Space toggles the check when checks are on,
45/// activating otherwise. A click selects and activates a card; with checks on, a click on the
46/// mark in a card's top right corner only toggles it. The wheel scrolls a row of cards at a
47/// time, and the scrollbar can be pressed and dragged.
48///
49/// ```
50/// use std::rc::Rc;
51///
52/// use qframe::prelude::*;
53/// use qframe::widgets::CardGrid;
54///
55/// struct Store {
56///     apps: Rc<[(String, String)]>,
57///     selected: Option<usize>,
58/// }
59///
60/// #[derive(Clone)]
61/// enum Msg {
62///     Select(usize),
63///     Open(usize),
64/// }
65///
66/// impl App for Store {
67///     type Msg = Msg;
68///     fn update(&mut self, msg: Msg) -> Command<Msg> {
69///         if let Msg::Select(index) | Msg::Open(index) = msg {
70///             self.selected = Some(index);
71///         }
72///         Command::none()
73///     }
74///     fn view(&self, ui: &mut View<'_, Msg>) {
75///         let apps = Rc::clone(&self.apps);
76///         let grid = CardGrid::new(self.apps.len())
77///             .card_width(24, 32)
78///             .card_height(2)
79///             .selected(self.selected)
80///             .on_select(Msg::Select)
81///             .on_activate(Msg::Open)
82///             .card(move |ui, index| {
83///                 let (name, summary) = &apps[index];
84///                 ui.add(Text::new(name.as_str()).role("title").no_wrap());
85///                 ui.add(Text::new(summary.as_str()).role("secondary").no_wrap());
86///             });
87///         ui.add(grid).fill();
88///     }
89/// }
90///
91/// let apps: Rc<[(String, String)]> = (0..40).map(|n| (format!("App {n}"), "Does a thing".to_owned())).collect();
92/// let mut store = Harness::new(Store { apps, selected: None }, 80, 10);
93/// store.press("tab").press("right").press("right").press("down");
94/// assert_eq!(store.app().selected, Some(4));
95/// ```
96///
97/// Cards are built while the grid paints, once for each card on screen, by the closure given to
98/// [`card`](Self::card). It lives as long as the widget, so it owns what it reads, e.g. an
99/// `Rc<[App]>` cloned from the state. Widgets inside a card are drawn but take no input of their
100/// own: the card is the pressable surface, and a press anywhere on it is the card's. Idle
101/// watches ([`View::on_idle`]) belong in the application's own view, not in a card.
102///
103/// Style keys: `card` (`bg`, `padding`, `pillar`) with `hover`, `selected`, `focus`, `pressed`;
104/// `card-mark` for a checked card's mark and `card-mark.off` for the faint mark a lit card
105/// offers while checks are on; `scrollbar`.
106pub struct CardGrid<Msg> {
107    count: usize,
108    min_width: u16,
109    max_width: u16,
110    height: u16,
111    column_gap: u16,
112    row_gap: u16,
113    selected: Option<usize>,
114    checked: Option<Vec<bool>>,
115    disabled: bool,
116    scrollbar: Option<ScrollbarStyle>,
117    on_select: Option<IndexMessage<Msg>>,
118    on_activate: Option<IndexMessage<Msg>>,
119    on_toggle: Option<IndexMessage<Msg>>,
120    card: Option<CardBuilder<Msg>>,
121    menu: Option<RowMenuItems<Msg>>,
122    empty: Vec<Node<Msg>>,
123}
124
125/// What a grid remembers between frames.
126#[derive(Debug, Default)]
127struct GridMemory {
128    /// First row of cards shown.
129    offset: usize,
130    /// The selection and column count the offset last followed: a new selection, or a resize
131    /// that moves it to another row, scrolls it into view once.
132    followed: Option<(Option<usize>, usize)>,
133    /// Whether the scrollbar thumb is being dragged.
134    dragging: bool,
135    /// The card activated last, for the press flash.
136    flashed: Option<usize>,
137    /// Where the pointer was when the grid was painted last.
138    pointer: Option<(i32, i32)>,
139    /// Whether the pointer moved over the grid since the last key, so it carries the highlight.
140    pointed: bool,
141    /// The layout painted last, which input is read against.
142    layout: Layout,
143}
144
145impl<Msg: 'static> CardGrid<Msg> {
146    /// A grid of `count` cards, 24 to 32 cells wide and three rows of content high, two cells
147    /// apart in a row and one row apart between rows. Give it what cards show with
148    /// [`card`](Self::card).
149    #[must_use]
150    pub fn new(count: usize) -> Self {
151        Self {
152            count,
153            min_width: 24,
154            max_width: 32,
155            height: 3,
156            column_gap: 2,
157            row_gap: 1,
158            selected: None,
159            checked: None,
160            disabled: false,
161            scrollbar: None,
162            on_select: None,
163            on_activate: None,
164            on_toggle: None,
165            card: None,
166            menu: None,
167            empty: Vec::new(),
168        }
169    }
170
171    /// The narrowest and the widest a card gets, in cells. As many columns as fit at `min`
172    /// share the width, each at most `max`.
173    #[must_use]
174    pub fn card_width(mut self, min: u16, max: u16) -> Self {
175        self.min_width = min.max(1);
176        self.max_width = max.max(self.min_width);
177        self
178    }
179
180    /// Rows of content in every card, without the card's padding.
181    #[must_use]
182    pub fn card_height(mut self, rows: u16) -> Self {
183        self.height = rows.max(1);
184        self
185    }
186
187    /// Cells between two cards of a row, and rows between two rows of cards.
188    #[must_use]
189    pub fn gap(mut self, columns: u16, rows: u16) -> Self {
190        self.column_gap = columns;
191        self.row_gap = rows;
192        self
193    }
194
195    /// The selected card's index.
196    #[must_use]
197    pub fn selected(mut self, index: Option<usize>) -> Self {
198        self.selected = index;
199        self
200    }
201
202    /// Turns checks on: `checked[i]` tells whether card `i` is checked, and a checked card
203    /// carries a mark in its top right corner. Space and a click on the mark report toggles
204    /// through [`on_toggle`](Self::on_toggle).
205    #[must_use]
206    pub fn checked(mut self, checked: Vec<bool>) -> Self {
207        self.checked = Some(checked);
208        self
209    }
210
211    /// Keeps the grid from being hovered, focused or pressed; its messages are not sent. The
212    /// cards fade and the selected card still shows.
213    #[must_use]
214    pub fn disabled(mut self, disabled: bool) -> Self {
215        self.disabled = disabled;
216        self
217    }
218
219    /// Draws the scrollbar in `style` whatever the theme chooses.
220    #[must_use]
221    pub fn scrollbar(mut self, style: ScrollbarStyle) -> Self {
222        self.scrollbar = Some(style);
223        self
224    }
225
226    /// Message for moving the selection to a card.
227    #[must_use]
228    pub fn on_select(mut self, message: impl Fn(usize) -> Msg + 'static) -> Self {
229        self.on_select = Some(Box::new(message));
230        self
231    }
232
233    /// Message for opening a card (Enter, a click).
234    #[must_use]
235    pub fn on_activate(mut self, message: impl Fn(usize) -> Msg + 'static) -> Self {
236        self.on_activate = Some(Box::new(message));
237        self
238    }
239
240    /// Message for checking or unchecking a card while checks are on (Space, a click on the
241    /// mark).
242    #[must_use]
243    pub fn on_toggle(mut self, message: impl Fn(usize) -> Msg + 'static) -> Self {
244        self.on_toggle = Some(Box::new(message));
245        self
246    }
247
248    /// Builds what card `index` shows, into the card's padded content area. Called only for the
249    /// cards on screen, every time the grid paints.
250    #[must_use]
251    pub fn card(mut self, build: impl Fn(&mut View<'_, Msg>, usize) + 'static) -> Self {
252        self.card = Some(Box::new(build));
253        self
254    }
255
256    /// Gives every card a context menu: `items(index)` builds the entries for the card of that
257    /// index, and the menu acts on the card it was opened on rather than on the selected one.
258    ///
259    /// A right press on a card opens the menu at the pointer; the menu key or Shift+F10 opens the
260    /// menu of the card the keys are on, scrolling it into view first. That card stays raised
261    /// while the menu is open, so it is clear what the entries act on. A right press on a card
262    /// that is not checked makes it the selection first, so a menu never acts on cards the person
263    /// did not mean.
264    #[must_use]
265    pub fn context_menu(mut self, items: impl Fn(usize) -> Vec<ContextItem<Msg>> + 'static) -> Self {
266        self.menu = Some(Box::new(items));
267        self
268    }
269
270    /// Offers `event` to the card menu; see [`context_menu`](Self::context_menu).
271    fn menu_event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
272        row_menu::event(
273            cx,
274            event,
275            self.menu.as_ref(),
276            self.count,
277            |cx, x, y| {
278                let memory = cx.memory::<GridMemory>();
279                let (layout, offset) = (memory.layout, memory.offset);
280                let index = layout.index_at(x, y, offset)?;
281                if !self.is_checked(index).unwrap_or(false) {
282                    self.select(cx, index);
283                }
284                Some(RowAnchor { row: index, at: Rect::new(x, y, 1, 1), keyboard: false })
285            },
286            |cx| {
287                let index = self.current(cx)?;
288                let memory = cx.memory::<GridMemory>();
289                let layout = memory.layout;
290                memory.offset = layout.reveal(index, memory.offset);
291                let at = layout.card_rect(index, memory.offset);
292                Some(RowAnchor { row: index, at, keyboard: true })
293            },
294        )
295    }
296
297    fn active(&self) -> bool {
298        !self.disabled && self.count > 0
299    }
300
301    fn is_checked(&self, index: usize) -> Option<bool> {
302        self.checked.as_ref().map(|checked| checked.get(index).copied().unwrap_or(false))
303    }
304
305    fn toggles(&self) -> bool {
306        self.checked.is_some() && self.on_toggle.is_some()
307    }
308
309    fn sizing(&self, padding: Padding) -> Sizing {
310        Sizing {
311            min_width: self.min_width,
312            max_width: self.max_width,
313            height: self.height.saturating_add(padding.vertical()),
314            column_gap: self.column_gap,
315            row_gap: self.row_gap,
316        }
317    }
318
319    fn select(&self, cx: &mut EventCx<'_, Msg>, index: usize) {
320        if Some(index) != self.selected
321            && let Some(message) = &self.on_select
322        {
323            cx.emit(message(index));
324        }
325    }
326
327    fn activate(&self, cx: &mut EventCx<'_, Msg>, index: usize) -> bool {
328        let Some(message) = &self.on_activate else {
329            return false;
330        };
331        cx.memory::<GridMemory>().flashed = Some(index);
332        cx.flash();
333        cx.emit(message(index));
334        true
335    }
336
337    fn toggle(&self, cx: &mut EventCx<'_, Msg>, index: usize) -> bool {
338        match (&self.checked, &self.on_toggle) {
339            (Some(_), Some(message)) => {
340                cx.emit(message(index));
341                true
342            }
343            _ => false,
344        }
345    }
346
347    /// The card the keys act on: the one the pointer carries the highlight to, else the
348    /// selected one.
349    fn current(&self, cx: &mut EventCx<'_, Msg>) -> Option<usize> {
350        let memory = cx.memory::<GridMemory>();
351        let pointed = memory.pointed.then_some(memory.pointer).flatten();
352        pointed.and_then(|(x, y)| memory.layout.index_at(x, y, memory.offset)).or(self.selected)
353    }
354
355    fn on_key(&self, cx: &mut EventCx<'_, Msg>, key: &crate::event::KeyEvent) -> bool {
356        let steps = [
357            (Key::Left, Step::Left),
358            (Key::Right, Step::Right),
359            (Key::Up, Step::Up),
360            (Key::Down, Step::Down),
361            (Key::PageUp, Step::PageUp),
362            (Key::PageDown, Step::PageDown),
363            (Key::Home, Step::Home),
364            (Key::End, Step::End),
365        ];
366        let step = steps.into_iter().find(|(k, _)| key.is_plain(*k)).map(|(_, step)| step);
367        let activates = key.is_plain(Key::Enter) || key.is_plain(Key::Space);
368        if step.is_none() && !activates {
369            return false;
370        }
371        let current = self.current(cx);
372        // A key takes the highlight back from a pointer resting on the grid, until it moves.
373        cx.memory::<GridMemory>().pointed = false;
374        if let Some(step) = step {
375            let target = cx.memory::<GridMemory>().layout.step(step, current);
376            if let Some(target) = target {
377                self.select(cx, target);
378            }
379            return target.is_some();
380        }
381        let Some(index) = current else {
382            return false;
383        };
384        self.select(cx, index);
385        if key.is_plain(Key::Space) && self.toggle(cx, index) {
386            return true;
387        }
388        self.activate(cx, index)
389    }
390
391    fn on_mouse(&self, cx: &mut EventCx<'_, Msg>, mouse: &crate::event::MouseEvent) -> bool {
392        let memory = cx.memory::<GridMemory>();
393        let layout = memory.layout;
394        let bar = layout.bar();
395        let on_bar = bar.is_some_and(|bar| bar.contains(mouse.x, mouse.y));
396        let track = |y: i32| clamp_u16(y - layout.body.y);
397        match mouse.kind {
398            MouseKind::ScrollUp | MouseKind::ScrollDown => {
399                memory.offset = if mouse.kind == MouseKind::ScrollUp {
400                    memory.offset.saturating_sub(1)
401                } else {
402                    (memory.offset + 1).min(layout.max_offset())
403                };
404                // The wheel is the pointer at work: the card now under it takes the highlight.
405                memory.pointed = true;
406                true
407            }
408            MouseKind::Down(MouseButton::Left) if on_bar => {
409                memory.dragging = true;
410                memory.offset = layout.metrics(memory.offset).offset_at(track(mouse.y), layout.body.height);
411                cx.capture_pointer();
412                true
413            }
414            MouseKind::Drag(MouseButton::Left) if memory.dragging => {
415                memory.offset = layout.metrics(memory.offset).offset_at(track(mouse.y), layout.body.height);
416                true
417            }
418            MouseKind::Up(MouseButton::Left) if memory.dragging => {
419                memory.dragging = false;
420                true
421            }
422            MouseKind::Down(MouseButton::Left) => {
423                let offset = memory.offset;
424                let Some(index) = layout.index_at(mouse.x, mouse.y, offset) else {
425                    return false;
426                };
427                if self.toggles() {
428                    let card = layout.card_rect(index, offset);
429                    let padding = card_padding(cx.env());
430                    if mark_zone(cx.env(), card, padding).contains(mouse.x, mouse.y) {
431                        return self.toggle(cx, index);
432                    }
433                }
434                self.select(cx, index);
435                self.activate(cx, index);
436                true
437            }
438            _ => false,
439        }
440    }
441}
442
443/// The card surface's padding from the theme.
444fn card_padding(env: &Env) -> Padding {
445    let style = env.theme().style("card", None, &[]);
446    style.pair("padding").map_or(Padding::default(), |(vertical, horizontal)| Padding::symmetric(vertical, horizontal))
447}
448
449/// Width of the check mark glyph.
450fn mark_width(env: &Env) -> u16 {
451    text::width(&env.icons().glyph("check")).max(1)
452}
453
454/// Where a card's check mark sits: in its top right corner, with a cell of air to its right.
455fn mark_cell(env: &Env, card: Rect, padding: Padding) -> Rect {
456    let width = mark_width(env);
457    Rect::new(card.right() - 1 - i32::from(width), card.y + i32::from(padding.top), width, 1)
458}
459
460/// The cells a click toggles the check in: the mark and a cell on each side of it.
461fn mark_zone(env: &Env, card: Rect, padding: Padding) -> Rect {
462    let mark = mark_cell(env, card, padding);
463    Rect::new(mark.x - 1, mark.y, mark.width + 2, 1)
464}
465
466impl<Msg: Clone + 'static> CardGrid<Msg> {
467    /// What the grid shows when it has no cards, e.g. "No apps match" with a way out. Without it
468    /// an empty grid draws nothing. Set the count with [`new`](Self::new) first; a grid with
469    /// cards leaves the empty state out.
470    #[must_use]
471    pub fn empty(mut self, state: EmptyState<Msg>) -> Self {
472        if self.count > 0 {
473            return self;
474        }
475        let mut node = Node::new(state, 0);
476        node.layout.width = Length::Fill(1);
477        node.layout.height = Length::Fill(1);
478        self.empty = vec![node];
479        self
480    }
481
482    /// Paints card `index` in `rect` in `states`.
483    fn paint_card(&self, cx: &mut PaintCx<'_>, rect: Rect, index: usize, states: &[State], padding: Padding) {
484        let style = cx.style("card", None, states);
485        let background = style.text().bg.unwrap_or_else(|| cx.color("raised"));
486        cx.clear(rect, background);
487        if let Some(pillar) = style.color("pillar") {
488            // A card is a whole surface, so its mark runs down its full left edge.
489            for row in 0..rect.height {
490                cx.pillar(rect.x, rect.y + i32::from(row), pillar);
491            }
492        }
493        let env = cx.env;
494        let mut inner = rect.inset(padding);
495        if self.checked.is_some() {
496            // The mark's corner is kept free on every row, so content never runs under it.
497            let reserve = mark_width(env) + 2;
498            inner.width = inner.width.saturating_sub(reserve.saturating_sub(padding.right));
499        }
500        if let Some(build) = &self.card {
501            let mut children = Vec::new();
502            let screen = Size::new(cx.buf.area.width, cx.buf.area.height);
503            let idle = IdleScope::new(cx.idle);
504            build(&mut View::new(&mut children, env, screen, &idle), index);
505            // Keyed by the card's index, so what a card's widgets remember follows the card.
506            let mut node = Node::new(Flex::new(Axis::Column, children), index);
507            node.layout.width = Length::Fill(1);
508            node.assign_ids(cx.id());
509            cx.paint_child(&node, inner);
510        }
511        if let Some(checked) = self.is_checked(index) {
512            let lit = states.iter().any(|state| matches!(state, State::Hover | State::Selected));
513            if checked || (lit && self.on_toggle.is_some() && !self.disabled) {
514                let variant = (!checked).then_some("off");
515                let fg = cx.style("card-mark", variant, &[]).text().fg.unwrap_or_else(|| cx.color("accent"));
516                let mark = mark_cell(env, rect, padding);
517                cx.text(mark.x, mark.y, &env.icons().glyph("check"), CellStyle::fg(fg), mark.width);
518            }
519        }
520        if self.disabled {
521            cx.tint(rect, background, 0.5);
522        }
523    }
524}
525
526impl<Msg: Clone + 'static> Widget<Msg> for CardGrid<Msg> {
527    fn measure(&self, cx: &mut MeasureCx<'_>, available: Size) -> Size {
528        if self.count == 0 {
529            return self.empty.first().map_or(Size::default(), |empty| cx.measure_child(empty, available));
530        }
531        let sizing = self.sizing(card_padding(cx.env()));
532        let (columns, _) = layout::columns(available.width, sizing);
533        let height = layout::height_of(self.count.div_ceil(columns), sizing);
534        Size::new(available.width, height).min(available)
535    }
536
537    fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
538        if area.is_empty() {
539            return;
540        }
541        if self.count == 0 {
542            if let Some(empty) = self.empty.first() {
543                cx.paint_child(empty, area);
544            }
545            return;
546        }
547        cx.register_hit(area);
548        let padding = cx.style("card", None, &[]).padding();
549        let layout = Layout::new(area, self.count, self.sizing(padding));
550        let active = self.active();
551        let focus_visible = active && cx.is_focus_visible();
552        let pressed = cx.is_pressed();
553        let pointer = if active { cx.pointer_within() } else { None };
554        let (offset, pointed, flashed, dragging) = {
555            let memory = cx.memory::<GridMemory>();
556            if pointer != memory.pointer {
557                // Only a moving pointer takes the highlight; one resting where the keys left it
558                // does not pull it back. Leaving the grid hands it back to the selection.
559                memory.pointer = pointer;
560                memory.pointed = pointer.is_some();
561            }
562            let follow = (self.selected, layout.columns);
563            if memory.followed != Some(follow) {
564                if let Some(selected) = self.selected.filter(|index| *index < self.count) {
565                    memory.offset = layout.reveal(selected, memory.offset);
566                }
567                memory.followed = Some(follow);
568            }
569            memory.offset = memory.offset.min(layout.max_offset());
570            memory.layout = layout;
571            (memory.offset, memory.pointed && pointer.is_some(), memory.flashed, memory.dragging)
572        };
573        // An open card menu takes the pointer: only the card it acts on stays raised, so the menu
574        // and the card it belongs to are read together.
575        let menu_card = row_menu::open_row(cx, self.menu.as_ref());
576        if menu_card.is_some() {
577            cx.request_overlay(area);
578        }
579        let hovered = match menu_card {
580            Some(card) => Some(card),
581            None => pointer.filter(|_| pointed).and_then(|(x, y)| layout.index_at(x, y, offset)),
582        };
583
584        cx.with_clip(layout.body, |cx| {
585            for index in layout.shown(offset) {
586                let rect = layout.card_rect(index, offset);
587                let is_hovered = hovered == Some(index);
588                // While the pointer carries the highlight, the selected card rests unless it is
589                // the one under the pointer.
590                let lit = self.selected == Some(index) && (!pointed || is_hovered);
591                let mut states = Vec::new();
592                if is_hovered {
593                    states.push(State::Hover);
594                }
595                if lit {
596                    states.push(State::Selected);
597                    if focus_visible {
598                        states.push(State::Focus);
599                    }
600                }
601                if pressed && flashed == Some(index) {
602                    states.push(State::Pressed);
603                }
604                self.paint_card(cx, rect, index, &states, padding);
605            }
606        });
607
608        if let Some(bar) = layout.bar() {
609            let lit = dragging || pointer.is_some_and(|(x, y)| bar.contains(x, y));
610            scrollbar::paint(cx, bar, layout.metrics(offset), lit, self.scrollbar);
611        }
612    }
613
614    fn paint_overlay(&self, cx: &mut PaintCx<'_>, anchor: Rect) {
615        row_menu::paint(cx, self.menu.as_ref(), anchor);
616    }
617
618    fn event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
619        if !self.active() {
620            return false;
621        }
622        if self.menu_event(cx, event) {
623            return true;
624        }
625        match event {
626            Event::Key(key) => self.on_key(cx, key),
627            Event::Mouse(mouse) => self.on_mouse(cx, mouse),
628            _ => false,
629        }
630    }
631
632    fn focusable(&self) -> bool {
633        self.active()
634    }
635
636    fn children(&self) -> &[Node<Msg>] {
637        &self.empty
638    }
639
640    fn children_mut(&mut self) -> &mut [Node<Msg>] {
641        &mut self.empty
642    }
643}