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