1use std::cmp::Ordering;
19use std::collections::HashSet;
20use std::fmt;
21use std::hash::Hash;
22use std::ops::Range;
23use std::rc::Rc;
24
25use iced::alignment::Horizontal;
26use iced::keyboard::key::Named;
27use iced::widget::{self, Space, container, responsive, row, space as spacer, text};
28use iced::{Alignment, Background, Border, Color, Element, Length, Padding, Theme};
29
30use crate::data::ellipsis::ellipsis;
31use crate::data::interaction::{Look, RowMenu, pressable, row_menu, scope};
32use crate::data::rows::rows;
33use crate::forms::select::select;
34use crate::icon::{Glyph, themed};
35use crate::keys::{self, Chord, Keymap};
36use crate::overlay::context_menu;
37use crate::overlay::dropdown_menu::{self, checkbox_item, dropdown_menu};
38use crate::primitives::button::{self, button};
39use crate::primitives::checkbox::{CheckState, checkbox};
40use crate::primitives::icon_button::icon_button;
41use crate::primitives::input::input;
42use crate::theme::{Tokens, fade, radius, semibold, space, text_size};
43
44pub const HEADER_HEIGHT: f32 = 40.0;
46pub const ROW_HEIGHT: f32 = 44.0;
48pub const BREAKPOINT: f32 = 560.0;
50pub const PAGE_SIZES: [usize; 4] = [10, 20, 50, 100];
52pub const SKELETON_ROWS: usize = 5;
54pub const CARD_LINE: f32 = 28.0;
56const CARD_PADDING: f32 = 12.0;
57const CARD_GAP: f32 = 8.0;
58const CHECK_WIDTH: f32 = 40.0;
59const ACTIONS_WIDTH: f32 = 44.0;
60
61pub type ColumnId = &'static str;
63
64#[derive(Debug, Clone, PartialEq, Default)]
66pub enum Value {
67 #[default]
68 Empty,
69 Text(String),
70 Number(f64),
71}
72
73impl fmt::Display for Value {
74 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75 match self {
76 Value::Empty => Ok(()),
77 Value::Text(value) => f.write_str(value),
78 Value::Number(value) if value.fract() == 0.0 && value.abs() < 1e15 => {
79 write!(f, "{}", *value as i64)
80 }
81 Value::Number(value) => write!(f, "{value}"),
82 }
83 }
84}
85
86impl From<&str> for Value {
87 fn from(value: &str) -> Self {
88 Value::Text(value.to_owned())
89 }
90}
91
92impl From<String> for Value {
93 fn from(value: String) -> Self {
94 Value::Text(value)
95 }
96}
97
98impl From<f64> for Value {
99 fn from(value: f64) -> Self {
100 Value::Number(value)
101 }
102}
103
104macro_rules! number {
105 ($($ty:ty),*) => {$(
106 impl From<$ty> for Value {
107 fn from(value: $ty) -> Self {
108 Value::Number(value as f64)
109 }
110 }
111 )*};
112}
113
114number!(f32, i32, i64, u32, u64, usize);
115
116impl<T: Into<Value>> From<Option<T>> for Value {
117 fn from(value: Option<T>) -> Self {
118 value.map_or(Value::Empty, Into::into)
119 }
120}
121
122pub fn compare(a: &Value, b: &Value) -> Ordering {
125 match (a, b) {
126 (Value::Empty, Value::Empty) => Ordering::Equal,
127 (Value::Empty, _) => Ordering::Greater,
128 (_, Value::Empty) => Ordering::Less,
129 (Value::Number(a), Value::Number(b)) => a.total_cmp(b),
130 (Value::Number(_), Value::Text(_)) => Ordering::Less,
131 (Value::Text(_), Value::Number(_)) => Ordering::Greater,
132 (Value::Text(a), Value::Text(b)) => a
133 .to_lowercase()
134 .cmp(&b.to_lowercase())
135 .then_with(|| a.cmp(b)),
136 }
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
141pub enum Direction {
142 Ascending,
143 Descending,
144}
145
146impl Direction {
147 pub const ALL: [Direction; 2] = [Direction::Ascending, Direction::Descending];
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
152pub enum Align {
153 #[default]
154 Start,
155 Center,
156 End,
158}
159
160impl Align {
161 pub const ALL: [Align; 3] = [Align::Start, Align::Center, Align::End];
162
163 fn horizontal(self) -> Horizontal {
164 match self {
165 Align::Start => Horizontal::Left,
166 Align::Center => Horizontal::Center,
167 Align::End => Horizontal::Right,
168 }
169 }
170}
171
172pub struct Column<Row> {
174 pub id: ColumnId,
175 pub header: String,
176 pub width: Length,
177 pub align: Align,
178 pub sortable: bool,
179 pub hideable: bool,
180 pub filterable: bool,
182 pub searchable: bool,
184 value: fn(&Row) -> Value,
185 format: Option<fn(&Row) -> String>,
186}
187
188pub fn column<Row>(
191 id: ColumnId,
192 header: impl Into<String>,
193 value: fn(&Row) -> Value,
194) -> Column<Row> {
195 Column {
196 id,
197 header: header.into(),
198 width: Length::FillPortion(1),
199 align: Align::Start,
200 sortable: true,
201 hideable: true,
202 filterable: false,
203 searchable: true,
204 value,
205 format: None,
206 }
207}
208
209impl<Row> Clone for Column<Row> {
210 fn clone(&self) -> Self {
211 Self {
212 header: self.header.clone(),
213 ..*self
214 }
215 }
216}
217
218impl<Row> fmt::Debug for Column<Row> {
219 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220 f.debug_struct("Column")
221 .field("id", &self.id)
222 .field("header", &self.header)
223 .field("width", &self.width)
224 .field("align", &self.align)
225 .field("sortable", &self.sortable)
226 .field("hideable", &self.hideable)
227 .field("filterable", &self.filterable)
228 .field("searchable", &self.searchable)
229 .finish_non_exhaustive()
230 }
231}
232
233impl<Row> Column<Row> {
234 pub fn width(mut self, width: impl Into<Length>) -> Self {
237 self.width = width.into();
238 self
239 }
240
241 pub fn align(mut self, align: Align) -> Self {
242 self.align = align;
243 self
244 }
245
246 pub fn sortable(mut self, sortable: bool) -> Self {
247 self.sortable = sortable;
248 self
249 }
250
251 pub fn hideable(mut self, hideable: bool) -> Self {
252 self.hideable = hideable;
253 self
254 }
255
256 pub fn filterable(mut self, filterable: bool) -> Self {
257 self.filterable = filterable;
258 self
259 }
260
261 pub fn searchable(mut self, searchable: bool) -> Self {
262 self.searchable = searchable;
263 self
264 }
265
266 pub fn format(mut self, format: fn(&Row) -> String) -> Self {
270 self.format = Some(format);
271 self
272 }
273
274 pub fn value(&self, row: &Row) -> Value {
276 (self.value)(row)
277 }
278
279 pub fn text(&self, row: &Row) -> String {
281 match self.format {
282 Some(format) => format(row),
283 None => (self.value)(row).to_string(),
284 }
285 }
286}
287
288#[derive(Debug, Clone, PartialEq)]
290pub enum Event<Key> {
291 Sort(ColumnId),
293 SortBy(Option<(ColumnId, Direction)>),
295 Search(String),
296 Filter(ColumnId, Option<String>),
299 ClearFilters,
301 PageSize(usize),
302 Page(usize),
304 NextPage,
305 PreviousPage,
306 Select(Key, bool),
308 SelectPage(bool),
310 ClearSelection,
311 Press(Key),
313 Activate(Key),
315 Next,
317 Previous,
320 First,
321 Last,
322 ToggleHighlighted,
323 ActivateHighlighted,
324 Columns(dropdown_menu::Event<ColumnId>),
326 Focus(bool),
328}
329
330#[derive(Debug, Clone, PartialEq)]
332pub enum Output<Key> {
333 Activated(Key),
335 Selected(Vec<Key>),
337}
338
339#[derive(Debug, Clone)]
341pub struct State<Row, Key> {
342 rows: Vec<Row>,
343 columns: Vec<Column<Row>>,
344 key: fn(&Row) -> Key,
345 hidden: HashSet<ColumnId>,
346 columns_menu: dropdown_menu::State<ColumnId>,
347 sort: Option<(ColumnId, Direction)>,
348 search: String,
349 filters: Vec<(ColumnId, String)>,
350 options: Vec<(ColumnId, Vec<String>)>,
351 order: Vec<usize>,
353 page: usize,
354 page_size: usize,
355 page_sizes: Vec<usize>,
356 selectable: bool,
357 selected: HashSet<Key>,
358 highlighted: Option<Key>,
359 focused: bool,
360}
361
362impl<Row, Key: Clone + Eq + Hash> State<Row, Key> {
363 pub fn new(
366 columns: impl IntoIterator<Item = Column<Row>>,
367 rows: impl IntoIterator<Item = Row>,
368 key: fn(&Row) -> Key,
369 ) -> Self {
370 let mut state = Self {
371 rows: rows.into_iter().collect(),
372 columns: columns.into_iter().collect(),
373 key,
374 hidden: HashSet::new(),
375 columns_menu: dropdown_menu::State::new([]),
376 sort: None,
377 search: String::new(),
378 filters: Vec::new(),
379 options: Vec::new(),
380 order: Vec::new(),
381 page: 0,
382 page_size: PAGE_SIZES[0],
383 page_sizes: PAGE_SIZES.to_vec(),
384 selectable: false,
385 selected: HashSet::new(),
386 highlighted: None,
387 focused: false,
388 };
389 state.rebuild_menu();
390 state.refresh();
391 state
392 }
393
394 pub fn with_selection(mut self, selectable: bool) -> Self {
396 self.selectable = selectable;
397 self
398 }
399
400 pub fn with_page_sizes(mut self, sizes: impl IntoIterator<Item = usize>) -> Self {
402 let sizes: Vec<usize> = sizes.into_iter().filter(|&size| size > 0).collect();
403 if let Some(&first) = sizes.first() {
404 self.page_size = first;
405 self.page_sizes = sizes;
406 }
407 self.refresh();
408 self
409 }
410
411 pub fn with_sort(mut self, column: ColumnId, direction: Direction) -> Self {
412 let _ = self.update(Event::SortBy(Some((column, direction))));
413 self
414 }
415
416 pub fn with_hidden(mut self, columns: impl IntoIterator<Item = ColumnId>) -> Self {
418 self.hidden = columns.into_iter().collect();
419 self.rebuild_menu();
420 self.refresh();
421 self
422 }
423
424 pub fn rows(&self) -> &[Row] {
425 &self.rows
426 }
427
428 pub fn columns(&self) -> &[Column<Row>] {
429 &self.columns
430 }
431
432 pub fn visible_columns(&self) -> Vec<&Column<Row>> {
434 self.columns
435 .iter()
436 .filter(|column| !self.hidden.contains(column.id))
437 .collect()
438 }
439
440 pub fn column(&self, id: ColumnId) -> Option<&Column<Row>> {
441 self.columns.iter().find(|column| column.id == id)
442 }
443
444 pub fn is_hidden(&self, id: ColumnId) -> bool {
445 self.hidden.contains(id)
446 }
447
448 pub fn columns_menu(&self) -> &dropdown_menu::State<ColumnId> {
450 &self.columns_menu
451 }
452
453 pub fn key(&self, row: &Row) -> Key {
454 (self.key)(row)
455 }
456
457 pub fn row(&self, key: &Key) -> Option<&Row> {
458 self.rows.iter().find(|row| (self.key)(row) == *key)
459 }
460
461 pub fn set_rows(&mut self, rows: impl IntoIterator<Item = Row>) {
464 self.rows = rows.into_iter().collect();
465 self.refresh();
466 }
467
468 pub fn push(&mut self, row: Row) {
469 self.rows.push(row);
470 self.refresh();
471 }
472
473 pub fn update_row(&mut self, key: &Key, change: impl FnOnce(&mut Row)) {
475 let Some(row) = self.rows.iter_mut().find(|row| (self.key)(row) == *key) else {
476 return;
477 };
478 change(row);
479 self.refresh();
480 }
481
482 pub fn retain(&mut self, keep: impl FnMut(&Row) -> bool) {
484 self.rows.retain(keep);
485 self.refresh();
486 }
487
488 pub fn sort(&self) -> Option<(ColumnId, Direction)> {
489 self.sort
490 }
491
492 pub fn search(&self) -> &str {
493 &self.search
494 }
495
496 pub fn filter(&self, column: ColumnId) -> Option<&str> {
498 self.filters
499 .iter()
500 .find(|(id, _)| *id == column)
501 .map(|(_, value)| value.as_str())
502 }
503
504 pub fn is_filtered(&self) -> bool {
506 !self.search.trim().is_empty() || !self.filters.is_empty()
507 }
508
509 pub fn options(&self, column: ColumnId) -> &[String] {
511 self.options
512 .iter()
513 .find(|(id, _)| *id == column)
514 .map_or(&[], |(_, options)| options.as_slice())
515 }
516
517 pub fn filtered_len(&self) -> usize {
519 self.order.len()
520 }
521
522 pub fn len(&self) -> usize {
523 self.rows.len()
524 }
525
526 pub fn is_empty(&self) -> bool {
527 self.rows.is_empty()
528 }
529
530 pub fn page(&self) -> usize {
532 self.page
533 }
534
535 pub fn page_size(&self) -> usize {
536 self.page_size
537 }
538
539 pub fn page_sizes(&self) -> &[usize] {
540 &self.page_sizes
541 }
542
543 pub fn page_count(&self) -> usize {
545 self.order.len().div_ceil(self.page_size).max(1)
546 }
547
548 pub fn page_range(&self) -> Range<usize> {
550 let start = (self.page * self.page_size).min(self.order.len());
551 start..(start + self.page_size).min(self.order.len())
552 }
553
554 pub fn page_rows(&self) -> Vec<&Row> {
556 self.page_indices()
557 .into_iter()
558 .filter_map(|index| self.rows.get(index))
559 .collect()
560 }
561
562 pub fn page_keys(&self) -> Vec<Key> {
564 self.page_rows().into_iter().map(self.key).collect()
565 }
566
567 pub fn showing(&self) -> String {
569 let range = self.page_range();
570 if range.is_empty() {
571 return format!("Showing 0 of {}", self.order.len());
572 }
573 format!(
574 "Showing {}-{} of {}",
575 range.start + 1,
576 range.end,
577 self.order.len()
578 )
579 }
580
581 pub fn is_selectable(&self) -> bool {
582 self.selectable
583 }
584
585 pub fn is_selected(&self, key: &Key) -> bool {
586 self.selected.contains(key)
587 }
588
589 pub fn selected(&self) -> Vec<Key> {
591 self.rows
592 .iter()
593 .map(self.key)
594 .filter(|key| self.selected.contains(key))
595 .collect()
596 }
597
598 pub fn page_check_state(&self) -> CheckState {
601 let keys = self.page_keys();
602 let count = keys
603 .iter()
604 .filter(|key| self.selected.contains(key))
605 .count();
606 match count {
607 0 => CheckState::Unchecked,
608 n if n == keys.len() => CheckState::Checked,
609 _ => CheckState::Indeterminate,
610 }
611 }
612
613 pub fn highlighted(&self) -> Option<&Key> {
615 self.highlighted.as_ref()
616 }
617
618 pub fn is_focused(&self) -> bool {
619 self.focused
620 }
621
622 pub fn update(&mut self, event: Event<Key>) -> Option<Output<Key>> {
624 match event {
625 Event::Sort(id) => {
626 let next = match self.sort {
627 Some((sorted, Direction::Ascending)) if sorted == id => {
628 Some((id, Direction::Descending))
629 }
630 Some((sorted, Direction::Descending)) if sorted == id => None,
631 _ => Some((id, Direction::Ascending)),
632 };
633 self.sort_by(next);
634 }
635 Event::SortBy(sort) => self.sort_by(sort),
636 Event::Search(search) => {
637 self.search = search;
638 self.page = 0;
639 self.refresh();
640 }
641 Event::Filter(id, value) => {
642 self.filters.retain(|(column, _)| *column != id);
643 if let Some(value) = value.filter(|_| self.column(id).is_some()) {
644 self.filters.push((id, value));
645 }
646 self.page = 0;
647 self.refresh();
648 }
649 Event::ClearFilters => {
650 self.search.clear();
651 self.filters.clear();
652 self.page = 0;
653 self.refresh();
654 }
655 Event::PageSize(size) => {
656 let first = self.page_range().start;
657 self.page_size = size.max(1);
658 self.page = first / self.page_size;
659 self.refresh();
660 }
661 Event::Page(page) => self.go_to(page),
662 Event::NextPage => self.go_to(self.page + 1),
663 Event::PreviousPage => self.go_to(self.page.saturating_sub(1)),
664 Event::Select(key, selected) => return self.select([key], selected),
665 Event::SelectPage(selected) => return self.select(self.page_keys(), selected),
666 Event::ClearSelection => {
667 let changed = !self.selected.is_empty();
668 self.selected.clear();
669 return changed.then(|| Output::Selected(Vec::new()));
670 }
671 Event::Press(key) => self.highlight(key),
672 Event::Activate(key) => {
673 if !self.page_keys().contains(&key) {
674 return None;
675 }
676 self.highlighted = Some(key.clone());
677 return Some(Output::Activated(key));
678 }
679 Event::Next => self.step(true),
680 Event::Previous => self.step(false),
681 Event::First => self.highlighted = self.page_keys().first().cloned(),
682 Event::Last => self.highlighted = self.page_keys().last().cloned(),
683 Event::ToggleHighlighted => {
684 let key = self.highlighted.clone()?;
685 let selected = !self.selected.contains(&key);
686 return self.select([key], selected);
687 }
688 Event::ActivateHighlighted => return self.highlighted.clone().map(Output::Activated),
689 Event::Columns(event) => {
690 if let Some(dropdown_menu::Output::Toggled(id, shown)) =
691 self.columns_menu.update(event)
692 {
693 self.show_column(id, shown);
694 }
695 }
696 Event::Focus(focused) => {
697 self.focused = focused;
698 if focused && self.highlighted.is_none() {
701 let keys = self.page_keys();
702 self.highlighted = keys
703 .iter()
704 .find(|key| self.selected.contains(*key))
705 .or(keys.first())
706 .cloned();
707 }
708 }
709 }
710 None
711 }
712
713 pub fn key_event(&self, keymap: &Keymap<Action>, key: &keys::Event) -> Option<Event<Key>> {
715 keymap
716 .resolve_event(key)
717 .and_then(|action| action.event(self))
718 }
719
720 fn page_indices(&self) -> Vec<usize> {
721 self.order
722 .get(self.page_range())
723 .map(<[usize]>::to_vec)
724 .unwrap_or_default()
725 }
726
727 fn sort_by(&mut self, sort: Option<(ColumnId, Direction)>) {
728 let valid = sort.filter(|(id, _)| self.column(id).is_some_and(|column| column.sortable));
729 if sort.is_some() && valid.is_none() {
730 return;
731 }
732 self.sort = valid;
733 self.page = 0;
734 self.refresh();
735 }
736
737 fn go_to(&mut self, page: usize) {
738 self.page = page.min(self.page_count() - 1);
739 let keys = self.page_keys();
740 if self
741 .highlighted
742 .as_ref()
743 .is_some_and(|key| !keys.contains(key))
744 {
745 self.highlighted = keys.first().cloned();
746 }
747 }
748
749 fn highlight(&mut self, key: Key) {
750 if self.page_keys().contains(&key) {
751 self.highlighted = Some(key);
752 }
753 }
754
755 fn step(&mut self, forward: bool) {
756 let keys = self.page_keys();
757 let current = self
758 .highlighted
759 .as_ref()
760 .and_then(|key| keys.iter().position(|page| page == key));
761 let target = match (current, forward) {
762 (None, true) => keys.first().cloned(),
763 (None, false) => keys.last().cloned(),
764 (Some(index), true) if index + 1 < keys.len() => keys.get(index + 1).cloned(),
765 (Some(index), false) if index > 0 => keys.get(index - 1).cloned(),
766 (Some(_), true) if self.page + 1 < self.page_count() => {
767 self.page += 1;
768 self.page_keys().first().cloned()
769 }
770 (Some(_), false) if self.page > 0 => {
771 self.page -= 1;
772 self.page_keys().last().cloned()
773 }
774 (Some(_), _) => self.highlighted.clone(),
775 };
776 if target.is_some() {
777 self.highlighted = target;
778 }
779 }
780
781 fn select(
782 &mut self,
783 keys: impl IntoIterator<Item = Key>,
784 selected: bool,
785 ) -> Option<Output<Key>> {
786 if !self.selectable {
787 return None;
788 }
789 let mut changed = false;
790 for key in keys {
791 if self.row(&key).is_none() {
792 continue;
793 }
794 changed |= if selected {
795 self.selected.insert(key)
796 } else {
797 self.selected.remove(&key)
798 };
799 }
800 changed.then(|| Output::Selected(self.selected()))
801 }
802
803 fn show_column(&mut self, id: ColumnId, shown: bool) {
805 if shown {
806 let _ = self.hidden.remove(id);
807 } else if self.visible_columns().len() > 1 {
808 let _ = self.hidden.insert(id);
809 } else {
810 self.columns_menu.set_checked(id, true);
811 }
812 self.refresh();
813 }
814
815 fn rebuild_menu(&mut self) {
816 let items = self
817 .columns
818 .iter()
819 .filter(|column| column.hideable)
820 .map(|column| {
821 checkbox_item(
822 column.id,
823 column.header.clone(),
824 !self.hidden.contains(column.id),
825 )
826 });
827 self.columns_menu = dropdown_menu::State::new(items);
828 }
829
830 fn refresh(&mut self) {
833 self.options = self
834 .columns
835 .iter()
836 .filter(|column| column.filterable)
837 .map(|column| {
838 let mut values: Vec<(Value, String)> = self
839 .rows
840 .iter()
841 .map(|row| (column.value(row), column.text(row)))
842 .collect();
843 values.sort_by(|a, b| compare(&a.0, &b.0));
844 let mut texts: Vec<String> = Vec::new();
845 for (_, text) in values {
846 if !texts.contains(&text) {
847 texts.push(text);
848 }
849 }
850 (column.id, texts)
851 })
852 .collect();
853
854 let query = self.search.trim().to_lowercase();
855 let searched: Vec<&Column<Row>> = self
856 .columns
857 .iter()
858 .filter(|column| column.searchable && !self.hidden.contains(column.id))
859 .collect();
860 let filters: Vec<(&Column<Row>, &str)> = self
861 .filters
862 .iter()
863 .filter_map(|(id, value)| {
864 self.columns
865 .iter()
866 .find(|column| column.id == *id)
867 .map(|column| (column, value.as_str()))
868 })
869 .collect();
870 let mut order: Vec<usize> = self
871 .rows
872 .iter()
873 .enumerate()
874 .filter(|(_, row)| {
875 filters
876 .iter()
877 .all(|(column, value)| column.text(row) == *value)
878 })
879 .filter(|(_, row)| {
880 query.is_empty()
881 || searched
882 .iter()
883 .any(|column| column.text(row).to_lowercase().contains(&query))
884 })
885 .map(|(index, _)| index)
886 .collect();
887
888 if let Some((id, direction)) = self.sort
889 && let Some(column) = self.column(id)
890 {
891 let keys: Vec<Value> = self.rows.iter().map(|row| column.value(row)).collect();
892 order.sort_by(|&a, &b| {
893 let (a, b) = (&keys[a], &keys[b]);
894 match (a, b) {
895 (Value::Empty, Value::Empty) => Ordering::Equal,
896 (Value::Empty, _) => Ordering::Greater,
897 (_, Value::Empty) => Ordering::Less,
898 _ if direction == Direction::Descending => compare(b, a),
899 _ => compare(a, b),
900 }
901 });
902 }
903 self.order = order;
904
905 let keys: HashSet<Key> = self.rows.iter().map(self.key).collect();
906 self.selected.retain(|key| keys.contains(key));
907 self.page = self.page.min(self.page_count() - 1);
908 let page = self.page_keys();
909 if self
910 .highlighted
911 .as_ref()
912 .is_some_and(|key| !page.contains(key))
913 {
914 self.highlighted = None;
915 }
916 }
917}
918
919#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
921pub enum Action {
922 Next,
923 Previous,
924 First,
925 Last,
926 NextPage,
927 PreviousPage,
928 Select,
929 Activate,
930 SelectAll,
931 ClearSelection,
932}
933
934impl Action {
935 pub fn event<Row, Key: Clone + Eq + Hash>(self, state: &State<Row, Key>) -> Option<Event<Key>> {
938 let rows = !state.page_range().is_empty();
939 let highlighted = state.highlighted().is_some();
940 let selectable = state.is_selectable();
941 match self {
942 Action::Next => rows.then_some(Event::Next),
943 Action::Previous => rows.then_some(Event::Previous),
944 Action::First => rows.then_some(Event::First),
945 Action::Last => rows.then_some(Event::Last),
946 Action::NextPage => (state.page() + 1 < state.page_count()).then_some(Event::NextPage),
947 Action::PreviousPage => (state.page() > 0).then_some(Event::PreviousPage),
948 Action::Select => (selectable && highlighted).then_some(Event::ToggleHighlighted),
949 Action::Activate => highlighted.then_some(Event::ActivateHighlighted),
950 Action::SelectAll => (selectable && rows).then_some(Event::SelectPage(true)),
951 Action::ClearSelection => {
952 (selectable && !state.selected.is_empty()).then_some(Event::ClearSelection)
953 }
954 }
955 }
956}
957
958impl keys::Action for Action {
959 const ALL: &'static [Self] = &[
960 Action::Next,
961 Action::Previous,
962 Action::First,
963 Action::Last,
964 Action::NextPage,
965 Action::PreviousPage,
966 Action::Select,
967 Action::Activate,
968 Action::SelectAll,
969 Action::ClearSelection,
970 ];
971
972 fn defaults() -> Keymap<Self> {
973 default_keymap()
974 }
975
976 fn name(self) -> &'static str {
977 match self {
978 Action::Next => "Next",
979 Action::Previous => "Previous",
980 Action::First => "First",
981 Action::Last => "Last",
982 Action::NextPage => "NextPage",
983 Action::PreviousPage => "PreviousPage",
984 Action::Select => "Select",
985 Action::Activate => "Activate",
986 Action::SelectAll => "SelectAll",
987 Action::ClearSelection => "ClearSelection",
988 }
989 }
990
991 fn description(self) -> &'static str {
992 match self {
993 Action::Next => "Highlights the next row, going on to the next page after the last.",
994 Action::Previous => "Highlights the previous row, going back a page before the first.",
995 Action::First => "Highlights the first row on the page.",
996 Action::Last => "Highlights the last row on the page.",
997 Action::NextPage => "Shows the next page.",
998 Action::PreviousPage => "Shows the previous page.",
999 Action::Select => "Selects or clears the highlighted row.",
1000 Action::Activate => "Activates the highlighted row.",
1001 Action::SelectAll => "Selects every row on the page.",
1002 Action::ClearSelection => "Clears the selection.",
1003 }
1004 }
1005}
1006
1007pub fn default_keymap() -> Keymap<Action> {
1026 Keymap::new()
1027 .bind(Chord::named(Named::ArrowDown), Action::Next)
1028 .bind(Chord::named(Named::ArrowUp), Action::Previous)
1029 .bind(Chord::named(Named::Home), Action::First)
1030 .bind(Chord::named(Named::End), Action::Last)
1031 .bind(Chord::named(Named::PageDown), Action::NextPage)
1032 .bind(Chord::named(Named::PageUp), Action::PreviousPage)
1033 .bind(Chord::named(Named::Space), Action::Select)
1034 .bind(Chord::named(Named::Enter), Action::Activate)
1035 .bind(Chord::character('a').command(), Action::SelectAll)
1036 .bind(Chord::named(Named::Escape), Action::ClearSelection)
1037}
1038
1039#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
1041pub struct Status {
1042 pub selected: bool,
1043 pub hovered: bool,
1044 pub highlighted: bool,
1046}
1047
1048#[derive(Debug, Clone, Copy, PartialEq)]
1050pub struct RowStyle {
1051 pub background: Option<Color>,
1052 pub ring: Option<Color>,
1054 pub divider: Color,
1056}
1057
1058pub fn row_style(tokens: &Tokens, status: Status) -> RowStyle {
1061 let background = if status.selected {
1062 Some(tokens.muted)
1063 } else if status.hovered {
1064 Some(fade(tokens.muted, 0.5))
1065 } else {
1066 None
1067 };
1068 RowStyle {
1069 background,
1070 ring: status.highlighted.then_some(tokens.ring),
1071 divider: tokens.border,
1072 }
1073}
1074
1075pub fn frame_style(tokens: &Tokens) -> container::Style {
1077 container::Style {
1078 border: Border {
1079 color: tokens.border,
1080 width: 1.0,
1081 radius: radius::MD.into(),
1082 },
1083 ..container::Style::default()
1084 }
1085}
1086
1087pub fn skeleton_style(tokens: &Tokens) -> container::Style {
1089 container::Style {
1090 background: Some(Background::Color(tokens.muted)),
1091 border: Border {
1092 radius: radius::SM.into(),
1093 ..Border::default()
1094 },
1095 ..container::Style::default()
1096 }
1097}
1098
1099pub fn sort_glyph(direction: Option<Direction>) -> Glyph {
1102 match direction {
1103 None => crate::lucide!(ArrowUpDown),
1104 Some(Direction::Ascending) => crate::lucide!(ArrowUp),
1105 Some(Direction::Descending) => crate::lucide!(ArrowDown),
1106 }
1107}
1108
1109type OnEvent<'a, Key, Message> = dyn Fn(Event<Key>) -> Message + 'a;
1110type Cell<'a, Row, Message> = Box<dyn Fn(&'a Row) -> Element<'a, Message> + 'a>;
1111type BuildMenu<'a, Key, Message> =
1112 dyn FnOnce(Keymap<context_menu::Action>) -> RowMenu<'a, Key, Message> + 'a;
1113
1114pub struct DataTable<'a, Row, Key, Message> {
1116 state: &'a State<Row, Key>,
1117 cells: Vec<(ColumnId, Cell<'a, Row, Message>)>,
1118 toolbar: bool,
1119 placeholder: String,
1120 loading: bool,
1121 empty: String,
1122 height: Length,
1123 row_height: f32,
1124 breakpoint: f32,
1125 keymap: Keymap<Action>,
1126 menu: Option<Box<BuildMenu<'a, Key, Message>>>,
1127 menu_keymap: Keymap<context_menu::Action>,
1128 row_actions: bool,
1129 on_event: Option<Box<OnEvent<'a, Key, Message>>>,
1130}
1131
1132pub fn data_table<Row, Key, Message>(state: &State<Row, Key>) -> DataTable<'_, Row, Key, Message> {
1135 DataTable {
1136 state,
1137 cells: Vec::new(),
1138 toolbar: true,
1139 placeholder: "Search...".to_owned(),
1140 loading: false,
1141 empty: "No results.".to_owned(),
1142 height: Length::Shrink,
1143 row_height: ROW_HEIGHT,
1144 breakpoint: BREAKPOINT,
1145 keymap: default_keymap(),
1146 menu: None,
1147 menu_keymap: context_menu::default_keymap(),
1148 row_actions: false,
1149 on_event: None,
1150 }
1151}
1152
1153impl<Row: fmt::Debug, Key: fmt::Debug, Message> fmt::Debug for DataTable<'_, Row, Key, Message> {
1154 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1155 f.debug_struct("DataTable")
1156 .field("state", self.state)
1157 .field("toolbar", &self.toolbar)
1158 .field("loading", &self.loading)
1159 .field("height", &self.height)
1160 .field("breakpoint", &self.breakpoint)
1161 .field("keymap", &self.keymap)
1162 .finish_non_exhaustive()
1163 }
1164}
1165
1166impl<'a, Row, Key, Message> DataTable<'a, Row, Key, Message> {
1167 pub fn cell(
1170 mut self,
1171 column: ColumnId,
1172 render: impl Fn(&'a Row) -> Element<'a, Message> + 'a,
1173 ) -> Self {
1174 self.cells.retain(|(id, _)| *id != column);
1175 self.cells.push((column, Box::new(render)));
1176 self
1177 }
1178
1179 pub fn toolbar(mut self, toolbar: bool) -> Self {
1182 self.toolbar = toolbar;
1183 self
1184 }
1185
1186 pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
1188 self.placeholder = placeholder.into();
1189 self
1190 }
1191
1192 pub fn loading(mut self, loading: bool) -> Self {
1194 self.loading = loading;
1195 self
1196 }
1197
1198 pub fn empty(mut self, message: impl Into<String>) -> Self {
1200 self.empty = message.into();
1201 self
1202 }
1203
1204 pub fn height(mut self, height: impl Into<Length>) -> Self {
1207 self.height = height.into();
1208 self
1209 }
1210
1211 pub fn row_height(mut self, height: f32) -> Self {
1214 self.row_height = height;
1215 self
1216 }
1217
1218 pub fn breakpoint(mut self, width: f32) -> Self {
1220 self.breakpoint = width;
1221 self
1222 }
1223
1224 pub fn keymap(mut self, keymap: Keymap<Action>) -> Self {
1226 self.keymap = keymap;
1227 self
1228 }
1229
1230 pub fn on_event(mut self, on_event: impl Fn(Event<Key>) -> Message + 'a) -> Self {
1231 self.on_event = Some(Box::new(on_event));
1232 self
1233 }
1234
1235 pub fn context_menu<MenuId>(
1239 mut self,
1240 menu: &'a context_menu::State<MenuId, Key>,
1241 on_event: impl Fn(context_menu::Event<MenuId, Key>) -> Message + 'a,
1242 ) -> Self
1243 where
1244 MenuId: Copy + PartialEq + 'a,
1245 Key: Clone + PartialEq + 'a,
1246 Message: Clone + 'a,
1247 {
1248 let on_event: Rc<dyn Fn(context_menu::Event<MenuId, Key>) -> Message + 'a> =
1249 Rc::new(on_event);
1250 self.menu = Some(Box::new(move |keymap| row_menu(menu, keymap, on_event)));
1251 self
1252 }
1253
1254 pub fn context_menu_keymap(mut self, keymap: Keymap<context_menu::Action>) -> Self {
1256 self.menu_keymap = keymap;
1257 self
1258 }
1259
1260 pub fn row_actions(mut self, row_actions: bool) -> Self {
1263 self.row_actions = row_actions;
1264 self
1265 }
1266
1267 pub fn is_enabled(&self) -> bool {
1268 self.on_event.is_some()
1269 }
1270}
1271
1272impl<'a, Row, Key, Message> From<DataTable<'a, Row, Key, Message>> for Element<'a, Message>
1273where
1274 Row: 'a,
1275 Key: Clone + Eq + Hash + 'a,
1276 Message: Clone + 'a,
1277{
1278 fn from(table: DataTable<'a, Row, Key, Message>) -> Self {
1279 let DataTable {
1280 state,
1281 cells,
1282 toolbar,
1283 placeholder,
1284 loading,
1285 empty,
1286 height,
1287 row_height,
1288 breakpoint,
1289 keymap,
1290 menu,
1291 menu_keymap,
1292 row_actions,
1293 on_event,
1294 } = table;
1295 let on_event: Option<Rc<OnEvent<'a, Key, Message>>> = on_event.map(Rc::from);
1296 let menu = menu
1297 .filter(|_| on_event.is_some())
1298 .map(|build| build(menu_keymap));
1299 let view = Rc::new(View {
1300 state,
1301 cells,
1302 toolbar,
1303 placeholder,
1304 loading,
1305 empty,
1306 height,
1307 row_height: row_height.max(1.0),
1308 keymap,
1309 row_actions: row_actions && menu.is_some(),
1310 menu,
1311 on_event,
1312 });
1313 responsive(move |size| view.body(size.width < breakpoint))
1314 .height(Length::Shrink)
1315 .into()
1316 }
1317}
1318
1319struct View<'a, Row, Key, Message> {
1321 state: &'a State<Row, Key>,
1322 cells: Vec<(ColumnId, Cell<'a, Row, Message>)>,
1323 toolbar: bool,
1324 placeholder: String,
1325 loading: bool,
1326 empty: String,
1327 height: Length,
1328 row_height: f32,
1329 keymap: Keymap<Action>,
1330 menu: Option<RowMenu<'a, Key, Message>>,
1331 row_actions: bool,
1332 on_event: Option<Rc<OnEvent<'a, Key, Message>>>,
1333}
1334
1335fn muted(theme: &Theme) -> Color {
1336 Tokens::of(theme).muted_foreground
1337}
1338
1339fn small<'a>(label: impl text::IntoFragment<'a>) -> text::Text<'a> {
1340 text(label)
1341 .size(text_size::SM)
1342 .wrapping(text::Wrapping::None)
1343 .style(|theme| text::Style {
1344 color: Some(muted(theme)),
1345 })
1346}
1347
1348impl<'a, Row, Key, Message> View<'a, Row, Key, Message>
1349where
1350 Row: 'a,
1351 Key: Clone + Eq + Hash + 'a,
1352 Message: Clone + 'a,
1353{
1354 fn send(&self, event: Event<Key>) -> Option<Message> {
1355 self.on_event.as_ref().map(|on_event| on_event(event))
1356 }
1357
1358 fn body(self: &Rc<Self>, narrow: bool) -> Element<'a, Message> {
1359 let mut parts = widget::column![].spacing(space::MD);
1360 if self.toolbar {
1361 parts = parts.push(self.toolbar(narrow));
1362 }
1363 let grid = if narrow { self.cards() } else { self.table() };
1364 parts
1365 .push(self.scoped(grid))
1366 .push(self.footer(narrow))
1367 .into()
1368 }
1369
1370 fn scoped(self: &Rc<Self>, grid: Element<'a, Message>) -> Element<'a, Message> {
1372 let Some(on_event) = self.on_event.clone() else {
1373 return grid;
1374 };
1375 let view = Rc::clone(self);
1376 let on_focus = on_event.clone();
1377 scope(grid)
1378 .on_focus(move |focused| on_focus(Event::Focus(focused)))
1379 .on_key(move |key| {
1380 let state = view.state;
1381 let opened = view
1382 .menu
1383 .as_ref()
1384 .zip(state.highlighted().cloned())
1385 .and_then(|(menu, target)| menu.key(key, target));
1386 opened.or_else(|| {
1387 state
1388 .key_event(&view.keymap, key)
1389 .map(|event| on_event(event))
1390 })
1391 })
1392 .into()
1393 }
1394
1395 fn toolbar(&self, narrow: bool) -> Element<'a, Message> {
1396 let state = self.state;
1397 let search = {
1398 let on_event = self.on_event.clone();
1399 input(&self.placeholder, state.search())
1400 .icon(crate::lucide!(Search))
1401 .width(if narrow {
1402 Length::Fill
1403 } else {
1404 Length::Fixed(240.0)
1405 })
1406 .on_input_maybe(
1407 on_event.map(|on_event| move |query| on_event(Event::Search(query))),
1408 )
1409 };
1410
1411 let filters = state
1412 .columns
1413 .iter()
1414 .filter(|column| column.filterable && !state.is_hidden(column.id))
1415 .map(|column| self.filter(column, narrow));
1416
1417 let columns_menu: Element<'a, Message> = {
1418 let trigger = button("Columns")
1419 .trailing_icon(crate::lucide!(ChevronDown))
1420 .variant(button::Variant::Outline)
1421 .on_press_maybe(self.send(Event::Columns(dropdown_menu::Event::Toggle)));
1422 let menu = dropdown_menu(state.columns_menu(), trigger)
1423 .align(crate::overlay::anchored::Align::End)
1424 .width(200.0);
1425 match self.on_event.clone() {
1426 Some(on_event) => menu
1427 .on_event(move |event| on_event(Event::Columns(event)))
1428 .into(),
1429 None => menu.into(),
1430 }
1431 };
1432
1433 if narrow {
1434 return widget::column![
1435 search,
1436 row(filters)
1437 .push(columns_menu)
1438 .spacing(space::SM)
1439 .align_y(Alignment::Center),
1440 ]
1441 .spacing(space::SM)
1442 .into();
1443 }
1444 row![search]
1445 .extend(filters)
1446 .push(spacer::horizontal())
1447 .push(columns_menu)
1448 .spacing(space::SM)
1449 .align_y(Alignment::Center)
1450 .into()
1451 }
1452
1453 fn filter(&self, column: &Column<Row>, narrow: bool) -> Element<'a, Message> {
1454 let state = self.state;
1455 let choice = |value: Option<&str>| FilterChoice {
1456 column: column.id,
1457 header: column.header.clone(),
1458 value: value.map(str::to_owned),
1459 };
1460 let options: Vec<FilterChoice> = std::iter::once(choice(None))
1461 .chain(
1462 state
1463 .options(column.id)
1464 .iter()
1465 .map(|value| choice(Some(value))),
1466 )
1467 .collect();
1468 let selected = choice(state.filter(column.id));
1469 let on_event = self.on_event.clone();
1470 select(options, Some(selected))
1471 .width(if narrow {
1472 Length::Fill
1473 } else {
1474 Length::Fixed(170.0)
1475 })
1476 .on_select_maybe(on_event.map(|on_event| {
1477 move |choice: FilterChoice| on_event(Event::Filter(choice.column, choice.value))
1478 }))
1479 .into()
1480 }
1481
1482 fn table(self: &Rc<Self>) -> Element<'a, Message> {
1483 let state = self.state;
1484 let columns = state.visible_columns();
1485
1486 let mut header = row![].height(Length::Fill).align_y(Alignment::Center);
1487 if state.is_selectable() {
1488 let page_empty = state.page_range().is_empty();
1489 let on_event = self
1490 .on_event
1491 .clone()
1492 .filter(|_| !page_empty && !self.loading);
1493 header = header.push(
1494 container(checkbox(state.page_check_state()).on_toggle_maybe(
1495 on_event.map(|on_event| move |checked| on_event(Event::SelectPage(checked))),
1496 ))
1497 .center_x(CHECK_WIDTH),
1498 );
1499 }
1500 for column in &columns {
1501 header = header.push(self.header_cell(column));
1502 }
1503 if self.row_actions {
1504 header = header.push(Space::new().width(ACTIONS_WIDTH));
1505 }
1506 let divider = container(Space::new())
1507 .width(Length::Fill)
1508 .height(1)
1509 .style(|theme| container::Style {
1510 background: Some(Background::Color(Tokens::of(theme).border)),
1511 ..container::Style::default()
1512 });
1513 let header = widget::column![container(header).height(HEADER_HEIGHT), divider];
1514
1515 container(self.table_body(header.into()))
1516 .width(Length::Fill)
1517 .clip(true)
1518 .style(|theme| frame_style(&Tokens::of(theme)))
1519 .into()
1520 }
1521
1522 fn header_cell(&self, column: &Column<Row>) -> Element<'a, Message> {
1523 let state = self.state;
1524 let align = column.align.horizontal();
1525 let label = ellipsis(column.header.clone())
1526 .font(semibold())
1527 .width(Length::Shrink)
1528 .colour(muted);
1529 let cell = |content: Element<'a, Message>| -> Element<'a, Message> {
1530 container(content)
1531 .width(column.width)
1532 .height(Length::Fill)
1533 .padding([0.0, space::SM])
1534 .align_x(align)
1535 .center_y(Length::Fill)
1536 .into()
1537 };
1538 if !column.sortable {
1539 return cell(label.into());
1540 }
1541 let direction = state
1542 .sort()
1543 .filter(|(id, _)| *id == column.id)
1544 .map(|(_, direction)| direction);
1545 let indicator = themed(sort_glyph(direction), 14.0, 1.0, move |theme| {
1546 let tokens = Tokens::of(theme);
1547 if direction.is_some() {
1548 tokens.foreground
1549 } else {
1550 tokens.muted_foreground
1551 }
1552 });
1553 let content = row![label, indicator]
1554 .spacing(space::XS)
1555 .align_y(Alignment::Center);
1556 let press = pressable(
1557 container(content).padding([4.0, space::XS]),
1558 |theme, hovered| Look {
1559 background: hovered.then(|| Tokens::of(theme).accent),
1560 border: Border {
1561 radius: radius::SM.into(),
1562 ..Border::default()
1563 },
1564 divider: None,
1565 },
1566 );
1567 let press = match self.on_event.clone() {
1568 Some(on_event) => {
1569 let id = column.id;
1570 press.on_press(move |_| on_event(Event::Sort(id)))
1571 }
1572 None => press,
1573 };
1574 container(press)
1575 .width(column.width)
1576 .height(Length::Fill)
1577 .padding([0.0, space::XS])
1578 .align_x(align)
1579 .center_y(Length::Fill)
1580 .into()
1581 }
1582
1583 fn table_body(self: &Rc<Self>, header: Element<'a, Message>) -> Element<'a, Message> {
1586 let state = self.state;
1587 if self.loading {
1588 let skeleton =
1589 (0..SKELETON_ROWS).map(|index| self.skeleton_row(index + 1 == SKELETON_ROWS));
1590 return widget::column(std::iter::once(header).chain(skeleton)).into();
1591 }
1592 let page = state.page_indices();
1593 if page.is_empty() {
1594 return widget::column![header, self.empty_view()].into();
1595 }
1596 let highlight = self.highlight_position();
1597 let count = page.len();
1598 let view = Rc::clone(self);
1599 rows(count, self.row_height, move |range: Range<usize>| {
1600 widget::column(
1601 page.get(range.clone())
1602 .unwrap_or_default()
1603 .iter()
1604 .zip(range)
1605 .map(|(&index, position)| view.table_row(index, position + 1 == count)),
1606 )
1607 .into()
1608 })
1609 .header(header)
1610 .highlight(highlight)
1611 .height(self.height)
1612 .into()
1613 }
1614
1615 fn highlight_position(&self) -> Option<usize> {
1616 let key = self.state.highlighted()?;
1617 self.state.page_keys().iter().position(|page| page == key)
1618 }
1619
1620 fn row_status(&self, key: &Key) -> Status {
1621 let state = self.state;
1622 Status {
1623 selected: state.is_selectable() && state.is_selected(key),
1624 hovered: false,
1625 highlighted: state.is_focused() && state.highlighted() == Some(key),
1626 }
1627 }
1628
1629 fn checkbox_for(&self, key: &Key) -> Element<'a, Message> {
1630 let selected = self.state.is_selected(key);
1631 let on_event = self.on_event.clone();
1632 let key = key.clone();
1633 checkbox(selected)
1634 .on_toggle_maybe(
1635 on_event.map(|on_event| move |checked| on_event(Event::Select(key, checked))),
1636 )
1637 .into()
1638 }
1639
1640 fn actions_for(&self, key: &Key) -> Element<'a, Message> {
1641 let open = self.menu.as_ref().map(|menu| menu.open(key.clone()));
1642 icon_button(crate::lucide!(Ellipsis))
1643 .label("Row actions")
1644 .tooltip(None)
1645 .size(button::Size::Sm)
1646 .on_press_maybe(open)
1647 .into()
1648 }
1649
1650 fn content_for(
1651 &self,
1652 column: &Column<Row>,
1653 row: &'a Row,
1654 align: Horizontal,
1655 ) -> Element<'a, Message> {
1656 if let Some((_, render)) = self.cells.iter().find(|(id, _)| *id == column.id) {
1657 return render(row);
1658 }
1659 ellipsis(column.text(row)).align_x(align).into()
1660 }
1661
1662 fn table_row(&self, index: usize, last: bool) -> Element<'a, Message> {
1663 let state = self.state;
1664 let Some(row_data) = state.rows.get(index) else {
1665 return Space::new().into();
1666 };
1667 let key = state.key(row_data);
1668 let mut cells = row![].height(Length::Fill).align_y(Alignment::Center);
1669 if state.is_selectable() {
1670 cells = cells.push(container(self.checkbox_for(&key)).center_x(CHECK_WIDTH));
1671 }
1672 for column in state.visible_columns() {
1673 cells = cells.push(
1674 container(self.content_for(column, row_data, column.align.horizontal()))
1675 .width(column.width)
1676 .padding([0.0, space::SM])
1677 .align_x(column.align.horizontal())
1678 .center_y(Length::Fill),
1679 );
1680 }
1681 if self.row_actions {
1682 cells = cells.push(container(self.actions_for(&key)).center_x(ACTIONS_WIDTH));
1683 }
1684 let status = self.row_status(&key);
1685 let body = pressable(
1686 container(cells).height(self.row_height),
1687 move |theme, hovered| {
1688 let style = row_style(&Tokens::of(theme), Status { hovered, ..status });
1689 Look {
1690 background: style.background,
1691 border: Border {
1692 color: style.ring.unwrap_or(Color::TRANSPARENT),
1693 width: if style.ring.is_some() { 1.0 } else { 0.0 },
1694 radius: radius::SM.into(),
1695 },
1696 divider: (!last).then_some(style.divider),
1697 }
1698 },
1699 );
1700 self.finish_row(body, key)
1701 }
1702
1703 fn finish_row(
1705 &self,
1706 body: crate::data::interaction::Pressable<'a, Message>,
1707 key: Key,
1708 ) -> Element<'a, Message> {
1709 let Some(on_event) = self.on_event.clone() else {
1710 return body.into();
1711 };
1712 let activate = on_event(Event::Activate(key.clone()));
1713 let press_key = key.clone();
1714 let body: Element<'a, Message> = body
1715 .on_press(move |_| on_event(Event::Press(press_key.clone())))
1716 .on_double_click(Some(activate))
1717 .into();
1718 match &self.menu {
1719 Some(menu) => menu.wrap(key, body),
1720 None => body,
1721 }
1722 }
1723
1724 fn skeleton_row(&self, last: bool) -> Element<'a, Message> {
1725 let state = self.state;
1726 let bar = || {
1727 container(Space::new())
1728 .width(Length::Fill)
1729 .height(12)
1730 .style(|theme| skeleton_style(&Tokens::of(theme)))
1731 };
1732 let mut cells = row![].height(Length::Fill).align_y(Alignment::Center);
1733 if state.is_selectable() {
1734 cells = cells.push(Space::new().width(CHECK_WIDTH));
1735 }
1736 for column in state.visible_columns() {
1737 cells = cells.push(
1738 container(bar())
1739 .width(column.width)
1740 .padding([0.0, space::SM]),
1741 );
1742 }
1743 if self.row_actions {
1744 cells = cells.push(Space::new().width(ACTIONS_WIDTH));
1745 }
1746 pressable::<Message>(container(cells).height(self.row_height), move |theme, _| {
1747 Look {
1748 divider: (!last).then(|| Tokens::of(theme).border),
1749 ..Look::default()
1750 }
1751 })
1752 .into()
1753 }
1754
1755 fn empty_view(&self) -> Element<'a, Message> {
1756 let state = self.state;
1757 let glyph = if state.is_filtered() {
1758 crate::lucide!(SearchX)
1759 } else {
1760 crate::lucide!(Inbox)
1761 };
1762 let mut content = widget::column![
1763 themed(glyph, 24.0, 1.0, muted),
1764 text(self.empty.clone())
1765 .size(text_size::SM)
1766 .style(|theme| text::Style {
1767 color: Some(muted(theme)),
1768 }),
1769 ]
1770 .spacing(space::SM)
1771 .align_x(Alignment::Center);
1772 if state.is_filtered() {
1773 content = content.push(
1774 button("Clear filters")
1775 .variant(button::Variant::Outline)
1776 .size(button::Size::Sm)
1777 .on_press_maybe(self.send(Event::ClearFilters)),
1778 );
1779 }
1780 container(content)
1781 .padding(space::XL)
1782 .center_x(Length::Fill)
1783 .into()
1784 }
1785
1786 fn card_height(&self) -> f32 {
1789 let lines = self.state.visible_columns().len().max(1);
1790 CARD_PADDING * 2.0 + lines as f32 * CARD_LINE
1791 }
1792
1793 fn cards(self: &Rc<Self>) -> Element<'a, Message> {
1794 let state = self.state;
1795 let has_rows = !self.loading && !state.page_range().is_empty();
1796 let mut bar = row![].spacing(space::MD).align_y(Alignment::Center);
1797 if state.is_selectable() {
1798 let on_event = self.on_event.clone().filter(|_| has_rows);
1799 bar = bar.push(
1800 checkbox(state.page_check_state())
1801 .label("Select all")
1802 .on_toggle_maybe(
1803 on_event
1804 .map(|on_event| move |checked| on_event(Event::SelectPage(checked))),
1805 ),
1806 );
1807 }
1808 if has_rows {
1809 bar = bar.push(self.sort_select());
1810 }
1811
1812 let body: Element<'a, Message> = if self.loading {
1813 widget::column((0..SKELETON_ROWS).map(|_| self.skeleton_card()))
1814 .spacing(CARD_GAP)
1815 .into()
1816 } else if state.page_range().is_empty() {
1817 container(self.empty_view())
1818 .style(|theme| frame_style(&Tokens::of(theme)))
1819 .into()
1820 } else {
1821 let page = state.page_indices();
1822 let count = page.len();
1823 let view = Rc::clone(self);
1824 let height = self.card_height();
1825 rows(count, height + CARD_GAP, move |range: Range<usize>| {
1826 widget::column(
1827 page.get(range)
1828 .unwrap_or_default()
1829 .iter()
1830 .map(|&index| view.card(index, height)),
1831 )
1832 .into()
1833 })
1834 .highlight(self.highlight_position())
1835 .height(self.height)
1836 .into()
1837 };
1838 if !state.is_selectable() && !has_rows {
1839 return body;
1840 }
1841 widget::column![bar, body].spacing(space::SM).into()
1842 }
1843
1844 fn sort_select(&self) -> Element<'a, Message> {
1845 let state = self.state;
1846 let mut options = vec![SortChoice {
1847 sort: None,
1848 label: "Default order".to_owned(),
1849 }];
1850 for column in state
1851 .visible_columns()
1852 .into_iter()
1853 .filter(|column| column.sortable)
1854 {
1855 for direction in Direction::ALL {
1856 let way = match direction {
1857 Direction::Ascending => "ascending",
1858 Direction::Descending => "descending",
1859 };
1860 options.push(SortChoice {
1861 sort: Some((column.id, direction)),
1862 label: format!("{}, {way}", column.header),
1863 });
1864 }
1865 }
1866 let selected = options
1867 .iter()
1868 .find(|choice| choice.sort == state.sort())
1869 .cloned();
1870 let on_event = self.on_event.clone();
1871 select(options, selected)
1872 .width(Length::Fill)
1873 .on_select_maybe(
1874 on_event
1875 .map(|on_event| move |choice: SortChoice| on_event(Event::SortBy(choice.sort))),
1876 )
1877 .into()
1878 }
1879
1880 fn card(&self, index: usize, height: f32) -> Element<'a, Message> {
1881 let state = self.state;
1882 let Some(row_data) = state.rows.get(index) else {
1883 return Space::new().into();
1884 };
1885 let key = state.key(row_data);
1886 let columns = state.visible_columns();
1887 let mut lines = widget::column![];
1888
1889 let mut head = row![].spacing(space::SM).align_y(Alignment::Center);
1890 if state.is_selectable() {
1891 head = head.push(self.checkbox_for(&key));
1892 }
1893 if let Some(first) = columns.first() {
1894 let title: Element<'a, Message> =
1895 match self.cells.iter().find(|(id, _)| *id == first.id) {
1896 Some((_, render)) => container(render(row_data)).width(Length::Fill).into(),
1897 None => ellipsis(first.text(row_data)).font(semibold()).into(),
1898 };
1899 head = head.push(title);
1900 }
1901 if self.row_actions {
1902 head = head.push(self.actions_for(&key));
1903 }
1904 lines = lines.push(container(head).height(CARD_LINE).center_y(CARD_LINE));
1905
1906 for column in columns.iter().skip(1) {
1907 let label = ellipsis(column.header.clone())
1908 .colour(muted)
1909 .width(Length::FillPortion(2));
1910 let value = container(self.content_for(column, row_data, Horizontal::Right))
1911 .width(Length::FillPortion(3))
1912 .align_x(Horizontal::Right);
1913 lines = lines.push(
1914 container(
1915 row![label, value]
1916 .spacing(space::SM)
1917 .align_y(Alignment::Center),
1918 )
1919 .height(CARD_LINE)
1920 .center_y(CARD_LINE),
1921 );
1922 }
1923
1924 let status = self.row_status(&key);
1925 let card = pressable(
1926 container(lines).padding(CARD_PADDING).height(height),
1927 move |theme, hovered| {
1928 let tokens = Tokens::of(theme);
1929 let style = row_style(&tokens, Status { hovered, ..status });
1930 Look {
1931 background: style.background,
1932 border: Border {
1933 color: style.ring.unwrap_or(tokens.border),
1934 width: 1.0,
1935 radius: radius::MD.into(),
1936 },
1937 divider: None,
1938 }
1939 },
1940 );
1941 let card = self.finish_row(card, key);
1942 container(card)
1943 .padding(Padding::ZERO.bottom(CARD_GAP))
1944 .into()
1945 }
1946
1947 fn skeleton_card(&self) -> Element<'a, Message> {
1948 let lines = widget::column((0..self.state.visible_columns().len().max(1)).map(|_| {
1949 container(
1950 container(Space::new())
1951 .width(Length::Fill)
1952 .height(12)
1953 .style(|theme| skeleton_style(&Tokens::of(theme))),
1954 )
1955 .height(CARD_LINE)
1956 .center_y(CARD_LINE)
1957 .into()
1958 }));
1959 container(lines)
1960 .padding(CARD_PADDING)
1961 .width(Length::Fill)
1962 .style(|theme| frame_style(&Tokens::of(theme)))
1963 .into()
1964 }
1965
1966 fn footer(&self, narrow: bool) -> Element<'a, Message> {
1967 let state = self.state;
1968 let mut summary = state.showing();
1969 if state.is_selectable() {
1970 summary.push_str(&format!(", {} selected", state.selected.len()));
1971 }
1972 let summary = small(summary);
1973
1974 let on_event = self.on_event.clone().filter(|_| !self.loading);
1975 let sizes = select(state.page_sizes().to_vec(), Some(state.page_size()))
1976 .width(80)
1977 .on_select_maybe(on_event.map(|on_event| move |size| on_event(Event::PageSize(size))));
1978 let has_previous = state.page() > 0;
1979 let has_next = state.page() + 1 < state.page_count();
1980 let pager = row![
1981 small(format!(
1982 "Page {} of {}",
1983 state.page() + 1,
1984 state.page_count()
1985 )),
1986 icon_button(crate::lucide!(ChevronLeft))
1987 .label("Previous page")
1988 .variant(crate::primitives::icon_button::Variant::Outline)
1989 .size(button::Size::Sm)
1990 .on_press_maybe(
1991 self.send(Event::PreviousPage)
1992 .filter(|_| has_previous && !self.loading)
1993 ),
1994 icon_button(crate::lucide!(ChevronRight))
1995 .label("Next page")
1996 .variant(crate::primitives::icon_button::Variant::Outline)
1997 .size(button::Size::Sm)
1998 .on_press_maybe(
1999 self.send(Event::NextPage)
2000 .filter(|_| has_next && !self.loading)
2001 ),
2002 ]
2003 .spacing(space::SM)
2004 .align_y(Alignment::Center);
2005
2006 if narrow {
2007 return widget::column![
2008 summary,
2009 row![sizes, spacer::horizontal(), pager].align_y(Alignment::Center),
2010 ]
2011 .spacing(space::SM)
2012 .into();
2013 }
2014 row![
2015 summary,
2016 spacer::horizontal(),
2017 small("Rows per page"),
2018 sizes,
2019 pager,
2020 ]
2021 .spacing(space::MD)
2022 .align_y(Alignment::Center)
2023 .into()
2024 }
2025}
2026
2027#[derive(Debug, Clone, PartialEq)]
2029struct FilterChoice {
2030 column: ColumnId,
2031 header: String,
2032 value: Option<String>,
2033}
2034
2035impl fmt::Display for FilterChoice {
2036 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2037 match &self.value {
2038 Some(value) => write!(f, "{}: {value}", self.header),
2039 None => write!(f, "{}: All", self.header),
2040 }
2041 }
2042}
2043
2044#[derive(Debug, Clone, PartialEq)]
2046struct SortChoice {
2047 sort: Option<(ColumnId, Direction)>,
2048 label: String,
2049}
2050
2051impl fmt::Display for SortChoice {
2052 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2053 f.write_str(&self.label)
2054 }
2055}
2056
2057#[cfg(test)]
2058mod tests {
2059 use super::*;
2060 use crate::theme::{dark, light};
2061 use iced::keyboard::{Key, Modifiers};
2062
2063 #[derive(Debug, Clone, PartialEq)]
2064 struct Payment {
2065 id: u32,
2066 status: &'static str,
2067 email: String,
2068 amount: Option<f64>,
2069 }
2070
2071 fn payment(id: u32, status: &'static str, email: &str, amount: f64) -> Payment {
2072 Payment {
2073 id,
2074 status,
2075 email: email.to_owned(),
2076 amount: Some(amount),
2077 }
2078 }
2079
2080 fn columns() -> Vec<Column<Payment>> {
2081 vec![
2082 column("status", "Status", |p: &Payment| p.status.into()).filterable(true),
2083 column("email", "Email", |p: &Payment| p.email.clone().into()),
2084 column("amount", "Amount", |p: &Payment| p.amount.into())
2085 .align(Align::End)
2086 .format(|p| {
2087 p.amount
2088 .map_or_else(String::new, |amount| format!("${amount:.2}"))
2089 }),
2090 column("id", "ID", |p: &Payment| p.id.into())
2091 .sortable(false)
2092 .hideable(false),
2093 ]
2094 }
2095
2096 fn payments(count: u32) -> Vec<Payment> {
2097 (1..=count)
2098 .map(|id| {
2099 let status = ["paid", "pending", "failed"][(id % 3) as usize];
2100 payment(
2101 id,
2102 status,
2103 &format!("user{id}@example.com"),
2104 f64::from(id * 10),
2105 )
2106 })
2107 .collect()
2108 }
2109
2110 fn state(count: u32) -> State<Payment, u32> {
2111 State::new(columns(), payments(count), |p| p.id)
2112 }
2113
2114 fn ids(rows: Vec<&Payment>) -> Vec<u32> {
2115 rows.into_iter().map(|p| p.id).collect()
2116 }
2117
2118 fn selected(output: Option<Output<u32>>) -> Vec<u32> {
2119 match output {
2120 Some(Output::Selected(keys)) => keys,
2121 other => panic!("expected a selection, got {other:?}"),
2122 }
2123 }
2124
2125 #[test]
2126 fn values_display_and_convert() {
2127 assert_eq!(Value::from(12.0).to_string(), "12");
2128 assert_eq!(Value::from(12.5).to_string(), "12.5");
2129 assert_eq!(Value::from(7_u32), Value::Number(7.0));
2130 assert_eq!(Value::from("a"), Value::Text("a".into()));
2131 assert_eq!(Value::from(None::<f64>), Value::Empty);
2132 assert_eq!(Value::from(Some(3_i64)), Value::Number(3.0));
2133 assert_eq!(Value::Empty.to_string(), "");
2134 assert_eq!(Value::default(), Value::Empty);
2135 }
2136
2137 #[test]
2138 fn compare_orders_numbers_then_text_then_empty() {
2139 let mut values = vec![
2140 Value::from("banana"),
2141 Value::Empty,
2142 Value::from(10.0),
2143 Value::from("Apple"),
2144 Value::from(2.0),
2145 Value::from("apple"),
2146 ];
2147 values.sort_by(compare);
2148 assert_eq!(
2149 values,
2150 vec![
2151 Value::from(2.0),
2152 Value::from(10.0),
2153 Value::from("Apple"),
2154 Value::from("apple"),
2155 Value::from("banana"),
2156 Value::Empty,
2157 ]
2158 );
2159 }
2160
2161 #[test]
2162 fn starts_unsorted_on_the_first_page() {
2163 let state = state(23);
2164 assert_eq!(state.len(), 23);
2165 assert_eq!(state.filtered_len(), 23);
2166 assert_eq!(state.page(), 0);
2167 assert_eq!(state.page_size(), 10);
2168 assert_eq!(state.page_count(), 3);
2169 assert_eq!(state.page_range(), 0..10);
2170 assert_eq!(ids(state.page_rows()), (1..=10).collect::<Vec<_>>());
2171 assert_eq!(state.showing(), "Showing 1-10 of 23");
2172 assert_eq!(state.sort(), None);
2173 assert!(!state.is_selectable());
2174 assert!(!state.is_filtered());
2175 }
2176
2177 #[test]
2178 fn an_empty_table_has_one_empty_page() {
2179 let state = state(0);
2180 assert!(state.is_empty());
2181 assert_eq!(state.page_count(), 1);
2182 assert_eq!(state.page_range(), 0..0);
2183 assert_eq!(state.showing(), "Showing 0 of 0");
2184 assert_eq!(state.page_check_state(), CheckState::Unchecked);
2185 }
2186
2187 #[test]
2188 fn header_presses_cycle_ascending_descending_and_unsorted() {
2189 let mut state = state(5).with_page_sizes([5]);
2190 let _ = state.update(Event::Sort("amount"));
2191 assert_eq!(state.sort(), Some(("amount", Direction::Ascending)));
2192 assert_eq!(ids(state.page_rows()), [1, 2, 3, 4, 5]);
2193 let _ = state.update(Event::Sort("amount"));
2194 assert_eq!(state.sort(), Some(("amount", Direction::Descending)));
2195 assert_eq!(ids(state.page_rows()), [5, 4, 3, 2, 1]);
2196 let _ = state.update(Event::Sort("amount"));
2197 assert_eq!(state.sort(), None);
2198 assert_eq!(ids(state.page_rows()), [1, 2, 3, 4, 5]);
2199 let _ = state.update(Event::Sort("email"));
2200 assert_eq!(state.sort(), Some(("email", Direction::Ascending)));
2201 }
2202
2203 #[test]
2204 fn unsortable_and_unknown_columns_are_ignored() {
2205 let mut state = state(5).with_sort("amount", Direction::Descending);
2206 let _ = state.update(Event::Sort("id"));
2207 let _ = state.update(Event::SortBy(Some(("missing", Direction::Ascending))));
2208 assert_eq!(state.sort(), Some(("amount", Direction::Descending)));
2209 let _ = state.update(Event::SortBy(None));
2210 assert_eq!(state.sort(), None);
2211 }
2212
2213 #[test]
2214 fn empty_values_sort_last_both_ways() {
2215 let mut rows = payments(3);
2216 rows[1].amount = None;
2217 let mut state = State::new(columns(), rows, |p| p.id);
2218 let _ = state.update(Event::SortBy(Some(("amount", Direction::Ascending))));
2219 assert_eq!(ids(state.page_rows()), [1, 3, 2]);
2220 let _ = state.update(Event::SortBy(Some(("amount", Direction::Descending))));
2221 assert_eq!(ids(state.page_rows()), [3, 1, 2]);
2222 }
2223
2224 #[test]
2225 fn search_matches_visible_searchable_text_ignoring_case() {
2226 let mut state = state(23);
2227 let _ = state.update(Event::NextPage);
2228 let _ = state.update(Event::Search("USER2".into()));
2229 assert_eq!(state.page(), 0, "a new search starts on the first page");
2230 assert_eq!(ids(state.page_rows()), [2, 20, 21, 22, 23]);
2231 let _ = state.update(Event::Search("$30.00".into()));
2232 assert_eq!(ids(state.page_rows()), [3], "searches the formatted text");
2233 let _ = state.update(Event::Columns(dropdown_menu::Event::Activate("amount")));
2234 assert!(state.is_hidden("amount"));
2235 assert_eq!(state.filtered_len(), 0, "hidden columns are not searched");
2236 assert!(state.is_filtered());
2237 }
2238
2239 #[test]
2240 fn column_filters_offer_distinct_values_and_combine_with_search() {
2241 let mut state = state(9);
2242 assert_eq!(state.options("status"), ["failed", "paid", "pending"]);
2243 assert!(state.options("email").is_empty());
2244 let _ = state.update(Event::Filter("status", Some("paid".into())));
2245 assert_eq!(state.filter("status"), Some("paid"));
2246 assert_eq!(ids(state.page_rows()), [3, 6, 9]);
2247 let _ = state.update(Event::Search("user9".into()));
2248 assert_eq!(ids(state.page_rows()), [9]);
2249 let _ = state.update(Event::Filter("status", None));
2250 assert_eq!(ids(state.page_rows()), [9]);
2251 let _ = state.update(Event::Filter("status", Some("paid".into())));
2252 let _ = state.update(Event::ClearFilters);
2253 assert_eq!(state.filtered_len(), 9);
2254 assert_eq!(state.search(), "");
2255 assert_eq!(state.filter("status"), None);
2256 let _ = state.update(Event::Filter("missing", Some("x".into())));
2257 assert!(!state.is_filtered(), "unknown columns are not filtered");
2258 }
2259
2260 #[test]
2261 fn pages_clamp_and_page_size_keeps_the_first_row_in_view() {
2262 let mut state = state(23);
2263 let _ = state.update(Event::PreviousPage);
2264 assert_eq!(state.page(), 0);
2265 let _ = state.update(Event::Page(9));
2266 assert_eq!(state.page(), 2);
2267 assert_eq!(state.showing(), "Showing 21-23 of 23");
2268 let _ = state.update(Event::NextPage);
2269 assert_eq!(state.page(), 2);
2270 let _ = state.update(Event::Page(1));
2271 let _ = state.update(Event::PageSize(20));
2272 assert_eq!(state.page(), 0, "row 11 is on the first page of 20");
2273 let _ = state.update(Event::PageSize(5));
2274 assert_eq!(state.page(), 0);
2275 let _ = state.update(Event::Page(3));
2276 let _ = state.update(Event::PageSize(0));
2277 assert_eq!(state.page_size(), 1, "at least one row a page");
2278 assert_eq!(state.page(), 15);
2279 }
2280
2281 #[test]
2282 fn page_sizes_can_be_set() {
2283 let state = state(23).with_page_sizes([0, 5, 25]);
2284 assert_eq!(state.page_sizes(), [5, 25]);
2285 assert_eq!(state.page_size(), 5);
2286 assert_eq!(state.page_count(), 5);
2287 let unchanged = self::state(3).with_page_sizes([]);
2288 assert_eq!(unchanged.page_sizes(), PAGE_SIZES);
2289 }
2290
2291 #[test]
2292 fn selection_needs_a_selectable_table() {
2293 let mut state = state(5);
2294 assert!(state.update(Event::Select(1, true)).is_none());
2295 assert!(state.selected().is_empty());
2296 }
2297
2298 #[test]
2299 fn rows_select_and_the_header_acts_on_the_page_only() {
2300 let mut state = state(23).with_selection(true);
2301 assert_eq!(selected(state.update(Event::Select(2, true))), [2]);
2302 assert_eq!(state.page_check_state(), CheckState::Indeterminate);
2303 assert!(state.update(Event::Select(2, true)).is_none(), "no change");
2304 assert!(
2305 state.update(Event::Select(99, true)).is_none(),
2306 "unknown row"
2307 );
2308
2309 let _ = state.update(Event::NextPage);
2310 assert_eq!(state.page_check_state(), CheckState::Unchecked);
2311 assert_eq!(
2312 selected(state.update(Event::SelectPage(true))),
2313 [2, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
2314 );
2315 assert_eq!(state.page_check_state(), CheckState::Checked);
2316 assert_eq!(selected(state.update(Event::SelectPage(false))), [2]);
2317 assert_eq!(
2318 selected(state.update(Event::ClearSelection)),
2319 Vec::<u32>::new()
2320 );
2321 assert!(state.update(Event::ClearSelection).is_none());
2322 }
2323
2324 #[test]
2325 fn selection_follows_keys_through_sorting_and_row_changes() {
2326 let mut state = state(5).with_selection(true);
2327 let _ = state.update(Event::Select(1, true));
2328 let _ = state.update(Event::Select(4, true));
2329 let _ = state.update(Event::Sort("amount"));
2330 let _ = state.update(Event::Sort("amount"));
2331 assert!(state.is_selected(&1) && state.is_selected(&4));
2332 state.retain(|p| p.id != 4);
2333 assert_eq!(state.selected(), [1]);
2334 state.update_row(&1, |p| p.status = "failed");
2335 assert_eq!(state.row(&1).map(|p| p.status), Some("failed"));
2336 state.push(payment(6, "paid", "new@example.com", 5.0));
2337 assert_eq!(state.len(), 5);
2338 assert_eq!(ids(state.page_rows()), [5, 3, 2, 1, 6], "sorted into place");
2339 state.set_rows(payments(2));
2340 assert_eq!(state.selected(), [1]);
2341 }
2342
2343 #[test]
2344 fn arrows_move_through_the_page_and_across_pages() {
2345 let mut state = state(23);
2346 let _ = state.update(Event::Next);
2347 assert_eq!(state.highlighted(), Some(&1));
2348 let _ = state.update(Event::Last);
2349 assert_eq!(state.highlighted(), Some(&10));
2350 let _ = state.update(Event::Next);
2351 assert_eq!(state.page(), 1);
2352 assert_eq!(state.highlighted(), Some(&11));
2353 let _ = state.update(Event::Previous);
2354 assert_eq!(state.page(), 0);
2355 assert_eq!(state.highlighted(), Some(&10));
2356 let _ = state.update(Event::First);
2357 let _ = state.update(Event::Previous);
2358 assert_eq!(state.highlighted(), Some(&1), "stays at the very first row");
2359 let _ = state.update(Event::Page(2));
2360 let _ = state.update(Event::Last);
2361 let _ = state.update(Event::Next);
2362 assert_eq!(state.highlighted(), Some(&23), "stays at the very last row");
2363
2364 let mut fresh = self::state(23);
2365 let _ = fresh.update(Event::Previous);
2366 assert_eq!(fresh.highlighted(), Some(&10));
2367 }
2368
2369 #[test]
2370 fn changing_page_moves_the_highlight_onto_it() {
2371 let mut state = state(23);
2372 let _ = state.update(Event::Press(3));
2373 let _ = state.update(Event::NextPage);
2374 assert_eq!(state.highlighted(), Some(&11));
2375 let _ = state.update(Event::Press(3));
2376 assert_eq!(
2377 state.highlighted(),
2378 Some(&11),
2379 "rows on other pages are ignored"
2380 );
2381 let _ = state.update(Event::Search("zzz".into()));
2382 assert_eq!(state.highlighted(), None);
2383 }
2384
2385 #[test]
2386 fn space_toggles_and_enter_activates_the_highlighted_row() {
2387 let mut state = state(5).with_selection(true);
2388 assert!(state.update(Event::ToggleHighlighted).is_none());
2389 assert!(state.update(Event::ActivateHighlighted).is_none());
2390 let _ = state.update(Event::Press(2));
2391 assert_eq!(selected(state.update(Event::ToggleHighlighted)), [2]);
2392 assert_eq!(
2393 selected(state.update(Event::ToggleHighlighted)),
2394 Vec::<u32>::new()
2395 );
2396 assert_eq!(
2397 state.update(Event::ActivateHighlighted),
2398 Some(Output::Activated(2))
2399 );
2400 assert_eq!(state.update(Event::Activate(4)), Some(Output::Activated(4)));
2401 assert_eq!(state.highlighted(), Some(&4));
2402 assert!(state.update(Event::Activate(40)).is_none());
2403 }
2404
2405 #[test]
2406 fn the_columns_menu_hides_columns_but_never_the_last() {
2407 let mut state = state(3);
2408 let menu: Vec<ColumnId> = state
2409 .columns_menu()
2410 .entries()
2411 .iter()
2412 .filter_map(|entry| entry.as_item().map(|item| item.id))
2413 .collect();
2414 assert_eq!(menu, ["status", "email", "amount"], "ID is not hideable");
2415 let _ = state.update(Event::Columns(dropdown_menu::Event::Open));
2416 let _ = state.update(Event::Columns(dropdown_menu::Event::Activate("email")));
2417 assert!(state.is_hidden("email"));
2418 assert!(!state.columns_menu().is_checked("email"));
2419 assert_eq!(state.visible_columns().len(), 3);
2420 let _ = state.update(Event::Columns(dropdown_menu::Event::Activate("email")));
2421 assert!(!state.is_hidden("email"));
2422
2423 let mut single = State::new(
2424 [column("name", "Name", |p: &Payment| p.email.clone().into())],
2425 payments(1),
2426 |p| p.id,
2427 );
2428 let _ = single.update(Event::Columns(dropdown_menu::Event::Activate("name")));
2429 assert!(!single.is_hidden("name"));
2430 assert!(single.columns_menu().is_checked("name"));
2431 }
2432
2433 #[test]
2434 fn with_hidden_starts_with_columns_hidden() {
2435 let state = state(3).with_hidden(["email"]);
2436 assert!(state.is_hidden("email"));
2437 assert!(!state.columns_menu().is_checked("email"));
2438 assert!(state.columns_menu().is_checked("status"));
2439 let visible: Vec<ColumnId> = state.visible_columns().iter().map(|c| c.id).collect();
2440 assert_eq!(visible, ["status", "amount", "id"]);
2441 }
2442
2443 #[test]
2444 fn focus_is_tracked_and_lands_on_a_row() {
2445 let mut state = state(3);
2446 let _ = state.update(Event::Focus(true));
2447 assert!(state.is_focused());
2448 assert_eq!(state.highlighted(), Some(&1));
2449
2450 let mut selected = self::state(3).with_selection(true);
2451 let _ = selected.update(Event::Select(2, true));
2452 let _ = selected.update(Event::Focus(true));
2453 assert_eq!(selected.highlighted(), Some(&2), "the first selected row");
2454 let _ = selected.update(Event::Focus(false));
2455 assert!(!selected.is_focused());
2456
2457 let empty = &mut self::state(0);
2458 let _ = empty.update(Event::Focus(true));
2459 assert_eq!(empty.highlighted(), None);
2460 }
2461
2462 #[test]
2463 fn columns_read_and_format_cells() {
2464 let columns = columns();
2465 let row = payment(7, "paid", "a@b.c", 12.5);
2466 assert_eq!(columns[2].value(&row), Value::Number(12.5));
2467 assert_eq!(columns[2].text(&row), "$12.50");
2468 assert_eq!(columns[0].text(&row), "paid");
2469 let column = columns[0]
2470 .clone()
2471 .width(120)
2472 .align(Align::Center)
2473 .searchable(false);
2474 assert_eq!(column.width, Length::Fixed(120.0));
2475 assert_eq!(column.align, Align::Center);
2476 assert!(!column.searchable);
2477 assert!(format!("{column:?}").contains("status"));
2478 assert_eq!(Align::ALL.len(), 3);
2479 assert_eq!(Direction::ALL.len(), 2);
2480 }
2481
2482 fn press(keymap: &Keymap<Action>, chord: &str) -> Option<Action> {
2483 let chord: Chord = chord.parse().unwrap();
2484 keymap.resolve(chord.key(), chord.modifiers())
2485 }
2486
2487 #[test]
2488 fn default_keymap_covers_every_action() {
2489 let keymap = default_keymap();
2490 assert_eq!(press(&keymap, "ArrowDown"), Some(Action::Next));
2491 assert_eq!(press(&keymap, "ArrowUp"), Some(Action::Previous));
2492 assert_eq!(press(&keymap, "Home"), Some(Action::First));
2493 assert_eq!(press(&keymap, "End"), Some(Action::Last));
2494 assert_eq!(press(&keymap, "PageDown"), Some(Action::NextPage));
2495 assert_eq!(press(&keymap, "PageUp"), Some(Action::PreviousPage));
2496 assert_eq!(press(&keymap, "Space"), Some(Action::Select));
2497 assert_eq!(press(&keymap, "Enter"), Some(Action::Activate));
2498 assert_eq!(press(&keymap, "Mod+A"), Some(Action::SelectAll));
2499 assert_eq!(press(&keymap, "Escape"), Some(Action::ClearSelection));
2500 for action in <Action as keys::Action>::ALL {
2501 assert!(!keymap.chords(action).is_empty(), "{action:?}");
2502 assert!(keys::Action::description(*action).ends_with('.'));
2503 }
2504 assert_eq!(Keymap::<Action>::defaults(), keymap);
2505 }
2506
2507 #[test]
2508 fn keymap_overrides_and_unbinding() {
2509 let keymap = default_keymap()
2510 .unbind(&Chord::named(Named::Escape))
2511 .bind(Chord::character('j'), Action::Next);
2512 let mut state = state(5).with_selection(true);
2513 let _ = state.update(Event::Select(1, true));
2514 let escape = keys::Event {
2515 key: Key::Named(Named::Escape),
2516 modifiers: Modifiers::empty(),
2517 };
2518 assert_eq!(state.key_event(&keymap, &escape), None);
2519 let j = keys::Event {
2520 key: Key::Character("j".into()),
2521 modifiers: Modifiers::empty(),
2522 };
2523 assert_eq!(state.key_event(&keymap, &j), Some(Event::Next));
2524 assert!(default_keymap().clear().is_empty());
2525 }
2526
2527 #[test]
2528 fn actions_map_to_events_only_when_they_apply() {
2529 let mut state = state(23);
2530 assert_eq!(Action::Next.event(&state), Some(Event::Next));
2531 assert_eq!(Action::PreviousPage.event(&state), None);
2532 assert_eq!(Action::NextPage.event(&state), Some(Event::NextPage));
2533 assert_eq!(Action::Activate.event(&state), None);
2534 assert_eq!(Action::Select.event(&state), None, "not selectable");
2535 assert_eq!(Action::SelectAll.event(&state), None);
2536 let _ = state.update(Event::First);
2537 assert_eq!(
2538 Action::Activate.event(&state),
2539 Some(Event::ActivateHighlighted)
2540 );
2541
2542 let mut selectable = self::state(3).with_selection(true);
2543 assert_eq!(
2544 Action::SelectAll.event(&selectable),
2545 Some(Event::SelectPage(true))
2546 );
2547 assert_eq!(
2548 Action::ClearSelection.event(&selectable),
2549 None,
2550 "nothing selected"
2551 );
2552 let _ = selectable.update(Event::SelectPage(true));
2553 assert_eq!(
2554 Action::ClearSelection.event(&selectable),
2555 Some(Event::ClearSelection)
2556 );
2557 let _ = selectable.update(Event::First);
2558 assert_eq!(
2559 Action::Select.event(&selectable),
2560 Some(Event::ToggleHighlighted)
2561 );
2562
2563 let empty = self::state(0);
2564 for action in [Action::Next, Action::Previous, Action::First, Action::Last] {
2565 assert_eq!(action.event(&empty), None, "{action:?}");
2566 }
2567 }
2568
2569 #[test]
2570 fn rows_are_muted_when_selected_and_ringed_when_highlighted() {
2571 for theme in [light(), dark()] {
2572 let tokens = Tokens::of(&theme);
2573 let idle = row_style(&tokens, Status::default());
2574 assert_eq!(idle.background, None);
2575 assert_eq!(idle.ring, None);
2576 assert_eq!(idle.divider, tokens.border);
2577 let selected = row_style(
2578 &tokens,
2579 Status {
2580 selected: true,
2581 hovered: true,
2582 ..Status::default()
2583 },
2584 );
2585 assert_eq!(selected.background, Some(tokens.muted));
2586 let hovered = row_style(
2587 &tokens,
2588 Status {
2589 hovered: true,
2590 ..Status::default()
2591 },
2592 );
2593 assert_eq!(hovered.background, Some(fade(tokens.muted, 0.5)));
2594 let highlighted = row_style(
2595 &tokens,
2596 Status {
2597 highlighted: true,
2598 ..Status::default()
2599 },
2600 );
2601 assert_eq!(highlighted.ring, Some(tokens.ring));
2602
2603 let frame = frame_style(&tokens);
2604 assert_eq!(frame.border.color, tokens.border);
2605 assert_eq!(frame.border.radius, radius::MD.into());
2606 assert_eq!(
2607 skeleton_style(&tokens).background,
2608 Some(Background::Color(tokens.muted))
2609 );
2610 }
2611 }
2612
2613 #[test]
2614 fn sort_indicators_show_the_direction() {
2615 assert_eq!(sort_glyph(None), crate::lucide!(ArrowUpDown));
2616 assert_eq!(
2617 sort_glyph(Some(Direction::Ascending)),
2618 crate::lucide!(ArrowUp)
2619 );
2620 assert_eq!(
2621 sort_glyph(Some(Direction::Descending)),
2622 crate::lucide!(ArrowDown)
2623 );
2624 }
2625
2626 #[test]
2627 fn filter_and_sort_choices_read_well() {
2628 let all = FilterChoice {
2629 column: "status",
2630 header: "Status".into(),
2631 value: None,
2632 };
2633 assert_eq!(all.to_string(), "Status: All");
2634 let paid = FilterChoice {
2635 value: Some("paid".into()),
2636 ..all
2637 };
2638 assert_eq!(paid.to_string(), "Status: paid");
2639 let sort = SortChoice {
2640 sort: None,
2641 label: "Default order".into(),
2642 };
2643 assert_eq!(sort.to_string(), "Default order");
2644 }
2645
2646 #[test]
2647 fn builder_defaults_and_options() {
2648 let state = state(3);
2649 let table: DataTable<'_, Payment, u32, ()> = data_table(&state);
2650 assert!(table.toolbar && !table.loading && !table.row_actions);
2651 assert!(!table.is_enabled());
2652 assert_eq!(table.height, Length::Shrink);
2653 assert_eq!(table.breakpoint, BREAKPOINT);
2654 assert_eq!(table.keymap, default_keymap());
2655 assert_eq!(table.empty, "No results.");
2656 let table = table
2657 .toolbar(false)
2658 .loading(true)
2659 .empty("Nothing here")
2660 .placeholder("Filter emails...")
2661 .height(300)
2662 .row_height(32.0)
2663 .breakpoint(400.0)
2664 .keymap(Keymap::new())
2665 .row_actions(true)
2666 .cell("status", |p: &Payment| text(p.status).into())
2667 .cell("status", |p: &Payment| text(p.status).into())
2668 .on_event(|_| ());
2669 assert!(!table.toolbar && table.loading && table.row_actions);
2670 assert_eq!(table.cells.len(), 1, "a second renderer replaces the first");
2671 assert_eq!(table.placeholder, "Filter emails...");
2672 assert_eq!(table.height, Length::Fixed(300.0));
2673 assert_eq!(table.row_height, 32.0);
2674 assert!(table.is_enabled());
2675 assert!(format!("{table:?}").contains("DataTable"));
2676 }
2677}