1mod 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
25type CardBuilder<Msg> = Box<dyn Fn(&mut View<'_, Msg>, usize)>;
27
28pub 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#[derive(Debug, Default)]
139struct GridMemory {
140 offset: usize,
142 followed: Option<(Option<usize>, usize)>,
145 dragging: bool,
147 flashed: Option<usize>,
149 pointer: Option<(i32, i32)>,
151 pointed: bool,
153 layout: Layout,
155}
156
157impl<Msg: 'static> CardGrid<Msg> {
158 #[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 #[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 #[must_use]
195 pub fn card_height(mut self, rows: u16) -> Self {
196 self.height = rows.max(1);
197 self
198 }
199
200 #[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 #[must_use]
210 pub fn selected(mut self, index: Option<usize>) -> Self {
211 self.selected = index;
212 self
213 }
214
215 #[must_use]
219 pub fn checked(mut self, checked: Vec<bool>) -> Self {
220 self.checked = Some(checked);
221 self
222 }
223
224 #[must_use]
227 pub fn disabled(mut self, disabled: bool) -> Self {
228 self.disabled = disabled;
229 self
230 }
231
232 #[must_use]
234 pub fn scrollbar(mut self, style: ScrollbarStyle) -> Self {
235 self.scrollbar = Some(style);
236 self
237 }
238
239 #[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 #[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 #[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 #[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 #[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 #[must_use]
287 pub fn activate_on(mut self, click: Click) -> Self {
288 self.picking.activate_on = click;
289 self
290 }
291
292 #[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 #[must_use]
315 pub fn box_select(mut self, on: bool) -> Self {
316 self.picking.box_select = on;
317 self
318 }
319
320 #[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 #[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 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 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 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 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 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 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
534fn 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
540fn mark_width(env: &Env) -> u16 {
542 text::width(&env.icons().glyph("check")).max(1)
543}
544
545fn 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
551fn 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 #[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 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 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 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 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 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 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 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 let cursor = self.selected == Some(index) && (!pointed || is_hovered);
687 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 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}