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 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, Space adds or takes out the card the keys are on and a plain click selects that one
299    /// card. A right click on a selected card keeps the selection for its menu; on another card it
300    /// makes that card the selection first.
301    #[must_use]
302    pub fn multi_select(mut self, selected: &[usize], message: impl Fn(Vec<usize>) -> Msg + 'static) -> Self {
303        self.picking.chosen = selected.to_vec();
304        self.picking.on_choose = Some(Box::new(message));
305        self
306    }
307
308    /// Lets a drag from the free space between and after the cards draw a box: every card it
309    /// touches becomes the selection while it is drawn, or joins it when Ctrl was held at the
310    /// press, and a click there without a drag clears the selection. The box is a tone laid over
311    /// the cells it covers, never a frame. It needs [`multi_select`](Self::multi_select) and does
312    /// nothing without it.
313    #[must_use]
314    pub fn box_select(mut self, on: bool) -> Self {
315        self.picking.box_select = on;
316        self
317    }
318
319    /// Lets cards be dragged onto other cards, such as files onto a folder: `accepts(index)` tells
320    /// whether a card takes drops and `message(RowDrop)` asks the application to move the cards.
321    ///
322    /// A drag carries the pressed card, or the whole [selection](Self::multi_select) when it is
323    /// pressed on one of its cards; a click on a selected card without a drag makes it the one
324    /// selected card on release. The card under the pointer takes the accent tone while it can
325    /// take the drag. A release anywhere else, or on one of the dragged cards, does nothing. With
326    /// [`Click::Single`] a card activates on release rather than on press, so pressing a card to
327    /// drag it does not activate it.
328    #[must_use]
329    pub fn droppable(
330        mut self,
331        message: impl Fn(RowDrop) -> Msg + 'static,
332        accepts: impl Fn(usize) -> bool + 'static,
333    ) -> Self {
334        self.picking.dropping = Some((Box::new(message), Box::new(accepts)));
335        self
336    }
337
338    /// A drop released with Ctrl held asks for a copy with `message` instead of the move of
339    /// [`droppable`](Self::droppable), the way a file explorer copies. A terminal that does not
340    /// report Ctrl with the pointer always moves. It does nothing without `droppable`.
341    #[must_use]
342    pub fn on_copy_drop(mut self, message: impl Fn(RowDrop) -> Msg + 'static) -> Self {
343        self.picking.copy_drop = Some(Box::new(message));
344        self
345    }
346
347    /// Offers `event` to the card menu; see [`context_menu`](Self::context_menu).
348    fn menu_event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
349        row_menu::event(
350            cx,
351            event,
352            self.menu.as_ref(),
353            self.count,
354            |cx, x, y| {
355                let memory = cx.memory::<GridMemory>();
356                let (layout, offset) = (memory.layout, memory.offset);
357                let index = layout.index_at(x, y, offset)?;
358                if self.picking.is_multi() {
359                    if !self.picking.is_chosen(index) {
360                        self.picking.select_one(cx, self, index);
361                    }
362                } else if !self.is_checked(index).unwrap_or(false) {
363                    self.select(cx, index);
364                }
365                Some(RowAnchor { row: index, at: Rect::new(x, y, 1, 1), keyboard: false })
366            },
367            |cx| {
368                let index = self.current(cx)?;
369                let memory = cx.memory::<GridMemory>();
370                let layout = memory.layout;
371                memory.offset = layout.reveal(index, memory.offset);
372                let at = layout.card_rect(index, memory.offset);
373                Some(RowAnchor { row: index, at, keyboard: true })
374            },
375        )
376    }
377
378    fn active(&self) -> bool {
379        !self.disabled && self.count > 0
380    }
381
382    fn is_checked(&self, index: usize) -> Option<bool> {
383        self.checked.as_ref().map(|checked| checked.get(index).copied().unwrap_or(false))
384    }
385
386    fn toggles(&self) -> bool {
387        self.checked.is_some() && self.on_toggle.is_some()
388    }
389
390    fn sizing(&self, padding: Padding) -> Sizing {
391        Sizing {
392            min_width: self.min_width,
393            max_width: self.max_width,
394            height: self.height.saturating_add(padding.vertical()),
395            column_gap: self.column_gap,
396            row_gap: self.row_gap,
397        }
398    }
399
400    fn select(&self, cx: &mut EventCx<'_, Msg>, index: usize) {
401        if Some(index) != self.selected
402            && let Some(message) = &self.on_select
403        {
404            cx.emit(message(index));
405        }
406    }
407
408    fn activate(&self, cx: &mut EventCx<'_, Msg>, index: usize) -> bool {
409        let Some(message) = &self.on_activate else {
410            return false;
411        };
412        cx.memory::<GridMemory>().flashed = Some(index);
413        cx.flash();
414        cx.emit(message(index));
415        true
416    }
417
418    fn toggle(&self, cx: &mut EventCx<'_, Msg>, index: usize) -> bool {
419        match (&self.checked, &self.on_toggle) {
420            (Some(_), Some(message)) => {
421                cx.emit(message(index));
422                true
423            }
424            _ => false,
425        }
426    }
427
428    /// The card whose check mark is at `(x, y)`, when one is.
429    fn mark_at(&self, cx: &mut EventCx<'_, Msg>, x: i32, y: i32) -> Option<usize> {
430        let memory = cx.memory::<GridMemory>();
431        let (layout, offset) = (memory.layout, memory.offset);
432        let index = layout.index_at(x, y, offset)?;
433        let env = cx.env();
434        mark_zone(env, layout.card_rect(index, offset), card_padding(env)).contains(x, y).then_some(index)
435    }
436
437    /// The card the keys act on: the one the pointer carries the highlight to, else the
438    /// selected one.
439    fn current(&self, cx: &mut EventCx<'_, Msg>) -> Option<usize> {
440        let memory = cx.memory::<GridMemory>();
441        let pointed = memory.pointed.then_some(memory.pointer).flatten();
442        pointed.and_then(|(x, y)| memory.layout.index_at(x, y, memory.offset)).or(self.selected)
443    }
444
445    fn on_key(&self, cx: &mut EventCx<'_, Msg>, key: &crate::event::KeyEvent) -> bool {
446        let steps = [
447            (Key::Left, Step::Left),
448            (Key::Right, Step::Right),
449            (Key::Up, Step::Up),
450            (Key::Down, Step::Down),
451            (Key::PageUp, Step::PageUp),
452            (Key::PageDown, Step::PageDown),
453            (Key::Home, Step::Home),
454            (Key::End, Step::End),
455        ];
456        let step = steps.into_iter().find(|(k, _)| key.is_plain(*k)).map(|(_, step)| step);
457        let activates = key.is_plain(Key::Enter) || key.is_plain(Key::Space);
458        if step.is_none() && !activates {
459            return false;
460        }
461        let current = self.current(cx);
462        // A key takes the highlight back from a pointer resting on the grid, until it moves.
463        cx.memory::<GridMemory>().pointed = false;
464        if let Some(step) = step {
465            let target = cx.memory::<GridMemory>().layout.step(step, current);
466            if let Some(target) = target {
467                if self.picking.is_multi() {
468                    self.picking.select_one(cx, self, target);
469                } else {
470                    self.select(cx, target);
471                }
472            }
473            return target.is_some();
474        }
475        let Some(index) = current else {
476            return false;
477        };
478        self.select(cx, index);
479        if key.is_plain(Key::Space) && (self.picking.toggle(cx, self, index) || self.toggle(cx, index)) {
480            return true;
481        }
482        self.activate(cx, index)
483    }
484
485    fn on_mouse(&self, cx: &mut EventCx<'_, Msg>, mouse: &crate::event::MouseEvent) -> bool {
486        // A press on a card's check mark only toggles it.
487        let marked = self.toggles().then(|| self.mark_at(cx, mouse.x, mouse.y)).flatten();
488        let memory = cx.memory::<GridMemory>();
489        let layout = memory.layout;
490        let bar = layout.bar();
491        let on_bar = bar.is_some_and(|bar| bar.contains(mouse.x, mouse.y));
492        let track = |y: i32| clamp_u16(y - layout.body.y);
493        match mouse.kind {
494            MouseKind::ScrollUp | MouseKind::ScrollDown => {
495                memory.offset = if mouse.kind == MouseKind::ScrollUp {
496                    memory.offset.saturating_sub(1)
497                } else {
498                    (memory.offset + 1).min(layout.max_offset())
499                };
500                // The wheel is the pointer at work: the card now under it takes the highlight.
501                memory.pointed = true;
502                true
503            }
504            MouseKind::Down(MouseButton::Left) if on_bar => {
505                memory.dragging = true;
506                memory.offset = layout.metrics(memory.offset).offset_at(track(mouse.y), layout.body.height);
507                cx.capture_pointer();
508                true
509            }
510            MouseKind::Drag(MouseButton::Left) if memory.dragging => {
511                memory.offset = layout.metrics(memory.offset).offset_at(track(mouse.y), layout.body.height);
512                true
513            }
514            MouseKind::Up(MouseButton::Left) if memory.dragging => {
515                memory.dragging = false;
516                true
517            }
518            MouseKind::Down(MouseButton::Left) if let Some(index) = marked => self.toggle(cx, index),
519            _ => self.picking.mouse(cx, mouse, self).unwrap_or(false),
520        }
521    }
522}
523
524/// The card surface's padding from the theme.
525fn card_padding(env: &Env) -> Padding {
526    let style = env.theme().style("card", None, &[]);
527    style.pair("padding").map_or(Padding::default(), |(vertical, horizontal)| Padding::symmetric(vertical, horizontal))
528}
529
530/// Width of the check mark glyph.
531fn mark_width(env: &Env) -> u16 {
532    text::width(&env.icons().glyph("check")).max(1)
533}
534
535/// Where a card's check mark sits: in its top right corner, with a cell of air to its right.
536fn mark_cell(env: &Env, card: Rect, padding: Padding) -> Rect {
537    let width = mark_width(env);
538    Rect::new(card.right() - 1 - i32::from(width), card.y + i32::from(padding.top), width, 1)
539}
540
541/// The cells a click toggles the check in: the mark and a cell on each side of it.
542fn mark_zone(env: &Env, card: Rect, padding: Padding) -> Rect {
543    let mark = mark_cell(env, card, padding);
544    Rect::new(mark.x - 1, mark.y, mark.width + 2, 1)
545}
546
547impl<Msg: Clone + 'static> CardGrid<Msg> {
548    /// What the grid shows when it has no cards, e.g. "No apps match" with a way out. Without it
549    /// an empty grid draws nothing. Set the count with [`new`](Self::new) first; a grid with
550    /// cards leaves the empty state out.
551    #[must_use]
552    pub fn empty(mut self, state: EmptyState<Msg>) -> Self {
553        if self.count > 0 {
554            return self;
555        }
556        let mut node = Node::new(state, 0);
557        node.layout.width = Length::Fill(1);
558        node.layout.height = Length::Fill(1);
559        self.empty = vec![node];
560        self
561    }
562
563    /// Paints card `index` in `rect` in `states`.
564    fn paint_card(&self, cx: &mut PaintCx<'_>, rect: Rect, index: usize, states: &[State], padding: Padding) {
565        let style = cx.style("card", None, states);
566        let background = style.text().bg.unwrap_or_else(|| cx.color("raised"));
567        cx.clear(rect, background);
568        if let Some(pillar) = style.color("pillar") {
569            // A card is a whole surface, so its mark runs down its full left edge.
570            for row in 0..rect.height {
571                cx.pillar(rect.x, rect.y + i32::from(row), pillar);
572            }
573        }
574        let env = cx.env;
575        let mut inner = rect.inset(padding);
576        if self.checked.is_some() {
577            // The mark's corner is kept free on every row, so content never runs under it.
578            let reserve = mark_width(env) + 2;
579            inner.width = inner.width.saturating_sub(reserve.saturating_sub(padding.right));
580        }
581        if let Some(build) = &self.card {
582            let mut children = Vec::new();
583            let screen = Size::new(cx.buf.area.width, cx.buf.area.height);
584            let idle = IdleScope::new(cx.idle);
585            build(&mut View::new(&mut children, env, screen, &idle), index);
586            // Keyed by the card's index, so what a card's widgets remember follows the card.
587            let mut node = Node::new(Flex::new(Axis::Column, children), index);
588            node.layout.width = Length::Fill(1);
589            node.assign_ids(cx.id());
590            cx.paint_child(&node, inner);
591        }
592        if let Some(checked) = self.is_checked(index) {
593            let lit = states.iter().any(|state| matches!(state, State::Hover | State::Selected));
594            if checked || (lit && self.on_toggle.is_some() && !self.disabled) {
595                let variant = (!checked).then_some("off");
596                let fg = cx.style("card-mark", variant, &[]).text().fg.unwrap_or_else(|| cx.color("accent"));
597                let mark = mark_cell(env, rect, padding);
598                cx.text(mark.x, mark.y, &env.icons().glyph("check"), CellStyle::fg(fg), mark.width);
599            }
600        }
601        if self.disabled {
602            cx.tint(rect, background, 0.5);
603        }
604    }
605}
606
607impl<Msg: Clone + 'static> Widget<Msg> for CardGrid<Msg> {
608    fn measure(&self, cx: &mut MeasureCx<'_>, available: Size) -> Size {
609        if self.count == 0 {
610            return self.empty.first().map_or(Size::default(), |empty| cx.measure_child(empty, available));
611        }
612        let sizing = self.sizing(card_padding(cx.env()));
613        let (columns, _) = layout::columns(available.width, sizing);
614        let height = layout::height_of(self.count.div_ceil(columns), sizing);
615        Size::new(available.width, height).min(available)
616    }
617
618    fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
619        if area.is_empty() {
620            return;
621        }
622        if self.count == 0 {
623            if let Some(empty) = self.empty.first() {
624                cx.paint_child(empty, area);
625            }
626            return;
627        }
628        cx.register_hit(area);
629        let padding = cx.style("card", None, &[]).padding();
630        let layout = Layout::new(area, self.count, self.sizing(padding));
631        let active = self.active();
632        let focus_visible = active && cx.is_focus_visible();
633        let pressed = cx.is_pressed();
634        let pointer = if active { cx.pointer_within() } else { None };
635        let (offset, pointed, flashed, dragging) = {
636            let memory = cx.memory::<GridMemory>();
637            if pointer != memory.pointer {
638                // Only a moving pointer takes the highlight; one resting where the keys left it
639                // does not pull it back. Leaving the grid hands it back to the selection.
640                memory.pointer = pointer;
641                memory.pointed = pointer.is_some();
642            }
643            let follow = (self.selected, layout.columns);
644            if memory.followed != Some(follow) {
645                if let Some(selected) = self.selected.filter(|index| *index < self.count) {
646                    memory.offset = layout.reveal(selected, memory.offset);
647                }
648                memory.followed = Some(follow);
649            }
650            memory.offset = memory.offset.min(layout.max_offset());
651            memory.layout = layout;
652            (memory.offset, memory.pointed && pointer.is_some(), memory.flashed, memory.dragging)
653        };
654        // An open card menu takes the pointer: only the card it acts on stays raised, so the menu
655        // and the card it belongs to are read together.
656        let menu_card = row_menu::open_row(cx, self.menu.as_ref());
657        if menu_card.is_some() {
658            cx.request_overlay(area);
659        }
660        let hovered = match menu_card {
661            Some(card) => Some(card),
662            None => pointer.filter(|_| pointed).and_then(|(x, y)| layout.index_at(x, y, offset)),
663        };
664
665        // The card a drag is over takes the accent tone when it can take what is dragged.
666        let target = row_pointer::dragged(cx).and_then(|((x, y), carried)| {
667            layout.index_at(x, y, offset).filter(|index| self.picking.takes_drop(&carried, *index))
668        });
669        let drawn = row_pointer::drawn_box(cx);
670        cx.with_clip(layout.body, |cx| {
671            for index in layout.shown(offset) {
672                let rect = layout.card_rect(index, offset);
673                let is_hovered = hovered == Some(index);
674                // While the pointer carries the highlight, the selected card rests unless it is
675                // the one under the pointer.
676                let cursor = self.selected == Some(index) && (!pointed || is_hovered);
677                // With several selected, the selected cards take the selected surface and the
678                // card the keys are on is raised like a hovered one when it is not among them,
679                // so the keys still show where they start.
680                let (lit, raised) = if self.picking.is_multi() {
681                    let chosen = self.picking.is_chosen(index);
682                    (chosen, is_hovered || (cursor && !chosen))
683                } else {
684                    (cursor, is_hovered)
685                };
686                let mut states = Vec::new();
687                if raised {
688                    states.push(State::Hover);
689                }
690                if lit {
691                    states.push(State::Selected);
692                    if focus_visible && (cursor || !self.picking.is_multi()) {
693                        states.push(State::Focus);
694                    }
695                }
696                if pressed && flashed == Some(index) {
697                    states.push(State::Pressed);
698                }
699                self.paint_card(cx, rect, index, &states, padding);
700                if target == Some(index) {
701                    // The card keeps what it shows and takes the tone of a place that takes a drop.
702                    let drop = cx.style("tree-drop", None, &[]).text();
703                    let bg = drop.bg.unwrap_or_else(|| cx.color("accent"));
704                    let readable = drop.fg.unwrap_or_else(|| cx.color("text"));
705                    cx.fill_keeping_text_readable(rect, bg, readable);
706                }
707            }
708            if let Some(drawn) = drawn {
709                select_box::paint(cx, drawn, layout.body);
710            }
711        });
712
713        if let Some(bar) = layout.bar() {
714            let lit = dragging || pointer.is_some_and(|(x, y)| bar.contains(x, y));
715            scrollbar::paint(cx, bar, layout.metrics(offset), lit, self.scrollbar);
716        }
717    }
718
719    fn paint_overlay(&self, cx: &mut PaintCx<'_>, anchor: Rect) {
720        row_menu::paint(cx, self.menu.as_ref(), anchor);
721    }
722
723    fn event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
724        if !self.active() {
725            return false;
726        }
727        if self.menu_event(cx, event) {
728            return true;
729        }
730        match event {
731            Event::Key(key) => self.on_key(cx, key),
732            Event::Mouse(mouse) => self.on_mouse(cx, mouse),
733            _ => false,
734        }
735    }
736
737    fn focusable(&self) -> bool {
738        self.active()
739    }
740
741    fn children(&self) -> &[Node<Msg>] {
742        &self.empty
743    }
744
745    fn children_mut(&mut self) -> &mut [Node<Msg>] {
746        &mut self.empty
747    }
748}
749
750impl<Msg: 'static> PickedRows<Msg> for CardGrid<Msg> {
751    fn spot(&self, cx: &mut EventCx<'_, Msg>, x: i32, y: i32) -> Spot {
752        let memory = cx.memory::<GridMemory>();
753        if !memory.layout.body.contains(x, y) {
754            return Spot::Outside;
755        }
756        memory.layout.index_at(x, y, memory.offset).map_or(Spot::Free, Spot::Row)
757    }
758
759    fn covered(&self, cx: &mut EventCx<'_, Msg>, rect: Rect) -> Vec<usize> {
760        let memory = cx.memory::<GridMemory>();
761        let (layout, offset) = (memory.layout, memory.offset);
762        layout.shown(offset).filter(|index| !layout.card_rect(*index, offset).intersect(rect).is_empty()).collect()
763    }
764
765    fn cursor(&self) -> Option<usize> {
766        self.selected
767    }
768
769    fn select(&self, cx: &mut EventCx<'_, Msg>, index: usize) {
770        CardGrid::select(self, cx, index);
771    }
772
773    fn open(&self, cx: &mut EventCx<'_, Msg>, index: usize, _at: (i32, i32)) {
774        self.activate(cx, index);
775    }
776}