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, ringed, 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 let target = self.menu.as_ref().and_then(RowMenu::target);
1623 Status {
1624 selected: state.is_selectable() && state.is_selected(key),
1625 hovered: false,
1626 highlighted: ringed(
1627 target.as_ref(),
1628 key,
1629 state.is_focused() && state.highlighted() == Some(key),
1630 ),
1631 }
1632 }
1633
1634 fn checkbox_for(&self, key: &Key) -> Element<'a, Message> {
1635 let selected = self.state.is_selected(key);
1636 let on_event = self.on_event.clone();
1637 let key = key.clone();
1638 checkbox(selected)
1639 .on_toggle_maybe(
1640 on_event.map(|on_event| move |checked| on_event(Event::Select(key, checked))),
1641 )
1642 .into()
1643 }
1644
1645 fn actions_for(&self, key: &Key) -> Element<'a, Message> {
1646 let open = self.menu.as_ref().map(|menu| menu.open(key.clone()));
1647 icon_button(crate::lucide!(Ellipsis))
1648 .label("Row actions")
1649 .tooltip(None)
1650 .size(button::Size::Sm)
1651 .on_press_maybe(open)
1652 .into()
1653 }
1654
1655 fn content_for(
1656 &self,
1657 column: &Column<Row>,
1658 row: &'a Row,
1659 align: Horizontal,
1660 ) -> Element<'a, Message> {
1661 if let Some((_, render)) = self.cells.iter().find(|(id, _)| *id == column.id) {
1662 return render(row);
1663 }
1664 ellipsis(column.text(row)).align_x(align).into()
1665 }
1666
1667 fn table_row(&self, index: usize, last: bool) -> Element<'a, Message> {
1668 let state = self.state;
1669 let Some(row_data) = state.rows.get(index) else {
1670 return Space::new().into();
1671 };
1672 let key = state.key(row_data);
1673 let mut cells = row![].height(Length::Fill).align_y(Alignment::Center);
1674 if state.is_selectable() {
1675 cells = cells.push(container(self.checkbox_for(&key)).center_x(CHECK_WIDTH));
1676 }
1677 for column in state.visible_columns() {
1678 cells = cells.push(
1679 container(self.content_for(column, row_data, column.align.horizontal()))
1680 .width(column.width)
1681 .padding([0.0, space::SM])
1682 .align_x(column.align.horizontal())
1683 .center_y(Length::Fill),
1684 );
1685 }
1686 if self.row_actions {
1687 cells = cells.push(container(self.actions_for(&key)).center_x(ACTIONS_WIDTH));
1688 }
1689 let status = self.row_status(&key);
1690 let body = pressable(
1691 container(cells).height(self.row_height),
1692 move |theme, hovered| {
1693 let style = row_style(&Tokens::of(theme), Status { hovered, ..status });
1694 Look {
1695 background: style.background,
1696 border: Border {
1697 color: style.ring.unwrap_or(Color::TRANSPARENT),
1698 width: if style.ring.is_some() { 1.0 } else { 0.0 },
1699 radius: radius::SM.into(),
1700 },
1701 divider: (!last).then_some(style.divider),
1702 }
1703 },
1704 );
1705 self.finish_row(body, key)
1706 }
1707
1708 fn finish_row(
1710 &self,
1711 body: crate::data::interaction::Pressable<'a, Message>,
1712 key: Key,
1713 ) -> Element<'a, Message> {
1714 let Some(on_event) = self.on_event.clone() else {
1715 return body.into();
1716 };
1717 let activate = on_event(Event::Activate(key.clone()));
1718 let press_key = key.clone();
1719 let body: Element<'a, Message> = body
1720 .on_press(move |_| on_event(Event::Press(press_key.clone())))
1721 .on_double_click(Some(activate))
1722 .into();
1723 match &self.menu {
1724 Some(menu) => menu.wrap(key, body),
1725 None => body,
1726 }
1727 }
1728
1729 fn skeleton_row(&self, last: bool) -> Element<'a, Message> {
1730 let state = self.state;
1731 let bar = || {
1732 container(Space::new())
1733 .width(Length::Fill)
1734 .height(12)
1735 .style(|theme| skeleton_style(&Tokens::of(theme)))
1736 };
1737 let mut cells = row![].height(Length::Fill).align_y(Alignment::Center);
1738 if state.is_selectable() {
1739 cells = cells.push(Space::new().width(CHECK_WIDTH));
1740 }
1741 for column in state.visible_columns() {
1742 cells = cells.push(
1743 container(bar())
1744 .width(column.width)
1745 .padding([0.0, space::SM]),
1746 );
1747 }
1748 if self.row_actions {
1749 cells = cells.push(Space::new().width(ACTIONS_WIDTH));
1750 }
1751 pressable::<Message>(container(cells).height(self.row_height), move |theme, _| {
1752 Look {
1753 divider: (!last).then(|| Tokens::of(theme).border),
1754 ..Look::default()
1755 }
1756 })
1757 .into()
1758 }
1759
1760 fn empty_view(&self) -> Element<'a, Message> {
1761 let state = self.state;
1762 let glyph = if state.is_filtered() {
1763 crate::lucide!(SearchX)
1764 } else {
1765 crate::lucide!(Inbox)
1766 };
1767 let mut content = widget::column![
1768 themed(glyph, 24.0, 1.0, muted),
1769 text(self.empty.clone())
1770 .size(text_size::SM)
1771 .style(|theme| text::Style {
1772 color: Some(muted(theme)),
1773 }),
1774 ]
1775 .spacing(space::SM)
1776 .align_x(Alignment::Center);
1777 if state.is_filtered() {
1778 content = content.push(
1779 button("Clear filters")
1780 .variant(button::Variant::Outline)
1781 .size(button::Size::Sm)
1782 .on_press_maybe(self.send(Event::ClearFilters)),
1783 );
1784 }
1785 container(content)
1786 .padding(space::XL)
1787 .center_x(Length::Fill)
1788 .into()
1789 }
1790
1791 fn card_height(&self) -> f32 {
1794 let lines = self.state.visible_columns().len().max(1);
1795 CARD_PADDING * 2.0 + lines as f32 * CARD_LINE
1796 }
1797
1798 fn cards(self: &Rc<Self>) -> Element<'a, Message> {
1799 let state = self.state;
1800 let has_rows = !self.loading && !state.page_range().is_empty();
1801 let mut bar = row![].spacing(space::MD).align_y(Alignment::Center);
1802 if state.is_selectable() {
1803 let on_event = self.on_event.clone().filter(|_| has_rows);
1804 bar = bar.push(
1805 checkbox(state.page_check_state())
1806 .label("Select all")
1807 .on_toggle_maybe(
1808 on_event
1809 .map(|on_event| move |checked| on_event(Event::SelectPage(checked))),
1810 ),
1811 );
1812 }
1813 if has_rows {
1814 bar = bar.push(self.sort_select());
1815 }
1816
1817 let body: Element<'a, Message> = if self.loading {
1818 widget::column((0..SKELETON_ROWS).map(|_| self.skeleton_card()))
1819 .spacing(CARD_GAP)
1820 .into()
1821 } else if state.page_range().is_empty() {
1822 container(self.empty_view())
1823 .style(|theme| frame_style(&Tokens::of(theme)))
1824 .into()
1825 } else {
1826 let page = state.page_indices();
1827 let count = page.len();
1828 let view = Rc::clone(self);
1829 let height = self.card_height();
1830 rows(count, height + CARD_GAP, move |range: Range<usize>| {
1831 widget::column(
1832 page.get(range)
1833 .unwrap_or_default()
1834 .iter()
1835 .map(|&index| view.card(index, height)),
1836 )
1837 .into()
1838 })
1839 .highlight(self.highlight_position())
1840 .height(self.height)
1841 .into()
1842 };
1843 if !state.is_selectable() && !has_rows {
1844 return body;
1845 }
1846 widget::column![bar, body].spacing(space::SM).into()
1847 }
1848
1849 fn sort_select(&self) -> Element<'a, Message> {
1850 let state = self.state;
1851 let mut options = vec![SortChoice {
1852 sort: None,
1853 label: "Default order".to_owned(),
1854 }];
1855 for column in state
1856 .visible_columns()
1857 .into_iter()
1858 .filter(|column| column.sortable)
1859 {
1860 for direction in Direction::ALL {
1861 let way = match direction {
1862 Direction::Ascending => "ascending",
1863 Direction::Descending => "descending",
1864 };
1865 options.push(SortChoice {
1866 sort: Some((column.id, direction)),
1867 label: format!("{}, {way}", column.header),
1868 });
1869 }
1870 }
1871 let selected = options
1872 .iter()
1873 .find(|choice| choice.sort == state.sort())
1874 .cloned();
1875 let on_event = self.on_event.clone();
1876 select(options, selected)
1877 .width(Length::Fill)
1878 .on_select_maybe(
1879 on_event
1880 .map(|on_event| move |choice: SortChoice| on_event(Event::SortBy(choice.sort))),
1881 )
1882 .into()
1883 }
1884
1885 fn card(&self, index: usize, height: f32) -> Element<'a, Message> {
1886 let state = self.state;
1887 let Some(row_data) = state.rows.get(index) else {
1888 return Space::new().into();
1889 };
1890 let key = state.key(row_data);
1891 let columns = state.visible_columns();
1892 let mut lines = widget::column![];
1893
1894 let mut head = row![].spacing(space::SM).align_y(Alignment::Center);
1895 if state.is_selectable() {
1896 head = head.push(self.checkbox_for(&key));
1897 }
1898 if let Some(first) = columns.first() {
1899 let title: Element<'a, Message> =
1900 match self.cells.iter().find(|(id, _)| *id == first.id) {
1901 Some((_, render)) => container(render(row_data)).width(Length::Fill).into(),
1902 None => ellipsis(first.text(row_data)).font(semibold()).into(),
1903 };
1904 head = head.push(title);
1905 }
1906 if self.row_actions {
1907 head = head.push(self.actions_for(&key));
1908 }
1909 lines = lines.push(container(head).height(CARD_LINE).center_y(CARD_LINE));
1910
1911 for column in columns.iter().skip(1) {
1912 let label = ellipsis(column.header.clone())
1913 .colour(muted)
1914 .width(Length::FillPortion(2));
1915 let value = container(self.content_for(column, row_data, Horizontal::Right))
1916 .width(Length::FillPortion(3))
1917 .align_x(Horizontal::Right);
1918 lines = lines.push(
1919 container(
1920 row![label, value]
1921 .spacing(space::SM)
1922 .align_y(Alignment::Center),
1923 )
1924 .height(CARD_LINE)
1925 .center_y(CARD_LINE),
1926 );
1927 }
1928
1929 let status = self.row_status(&key);
1930 let card = pressable(
1931 container(lines).padding(CARD_PADDING).height(height),
1932 move |theme, hovered| {
1933 let tokens = Tokens::of(theme);
1934 let style = row_style(&tokens, Status { hovered, ..status });
1935 Look {
1936 background: style.background,
1937 border: Border {
1938 color: style.ring.unwrap_or(tokens.border),
1939 width: 1.0,
1940 radius: radius::MD.into(),
1941 },
1942 divider: None,
1943 }
1944 },
1945 );
1946 let card = self.finish_row(card, key);
1947 container(card)
1948 .padding(Padding::ZERO.bottom(CARD_GAP))
1949 .into()
1950 }
1951
1952 fn skeleton_card(&self) -> Element<'a, Message> {
1953 let lines = widget::column((0..self.state.visible_columns().len().max(1)).map(|_| {
1954 container(
1955 container(Space::new())
1956 .width(Length::Fill)
1957 .height(12)
1958 .style(|theme| skeleton_style(&Tokens::of(theme))),
1959 )
1960 .height(CARD_LINE)
1961 .center_y(CARD_LINE)
1962 .into()
1963 }));
1964 container(lines)
1965 .padding(CARD_PADDING)
1966 .width(Length::Fill)
1967 .style(|theme| frame_style(&Tokens::of(theme)))
1968 .into()
1969 }
1970
1971 fn footer(&self, narrow: bool) -> Element<'a, Message> {
1972 let state = self.state;
1973 let mut summary = state.showing();
1974 if state.is_selectable() {
1975 summary.push_str(&format!(", {} selected", state.selected.len()));
1976 }
1977 let summary = small(summary);
1978
1979 let on_event = self.on_event.clone().filter(|_| !self.loading);
1980 let sizes = select(state.page_sizes().to_vec(), Some(state.page_size()))
1981 .width(80)
1982 .on_select_maybe(on_event.map(|on_event| move |size| on_event(Event::PageSize(size))));
1983 let has_previous = state.page() > 0;
1984 let has_next = state.page() + 1 < state.page_count();
1985 let pager = row![
1986 small(format!(
1987 "Page {} of {}",
1988 state.page() + 1,
1989 state.page_count()
1990 )),
1991 icon_button(crate::lucide!(ChevronLeft))
1992 .label("Previous page")
1993 .variant(crate::primitives::icon_button::Variant::Outline)
1994 .size(button::Size::Sm)
1995 .on_press_maybe(
1996 self.send(Event::PreviousPage)
1997 .filter(|_| has_previous && !self.loading)
1998 ),
1999 icon_button(crate::lucide!(ChevronRight))
2000 .label("Next page")
2001 .variant(crate::primitives::icon_button::Variant::Outline)
2002 .size(button::Size::Sm)
2003 .on_press_maybe(
2004 self.send(Event::NextPage)
2005 .filter(|_| has_next && !self.loading)
2006 ),
2007 ]
2008 .spacing(space::SM)
2009 .align_y(Alignment::Center);
2010
2011 if narrow {
2012 return widget::column![
2013 summary,
2014 row![sizes, spacer::horizontal(), pager].align_y(Alignment::Center),
2015 ]
2016 .spacing(space::SM)
2017 .into();
2018 }
2019 row![
2020 summary,
2021 spacer::horizontal(),
2022 small("Rows per page"),
2023 sizes,
2024 pager,
2025 ]
2026 .spacing(space::MD)
2027 .align_y(Alignment::Center)
2028 .into()
2029 }
2030}
2031
2032#[derive(Debug, Clone, PartialEq)]
2034struct FilterChoice {
2035 column: ColumnId,
2036 header: String,
2037 value: Option<String>,
2038}
2039
2040impl fmt::Display for FilterChoice {
2041 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2042 match &self.value {
2043 Some(value) => write!(f, "{}: {value}", self.header),
2044 None => write!(f, "{}: All", self.header),
2045 }
2046 }
2047}
2048
2049#[derive(Debug, Clone, PartialEq)]
2051struct SortChoice {
2052 sort: Option<(ColumnId, Direction)>,
2053 label: String,
2054}
2055
2056impl fmt::Display for SortChoice {
2057 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2058 f.write_str(&self.label)
2059 }
2060}
2061
2062#[cfg(test)]
2063mod tests {
2064 use super::*;
2065 use crate::theme::{dark, light};
2066 use iced::keyboard::{Key, Modifiers};
2067
2068 #[derive(Debug, Clone, PartialEq)]
2069 struct Payment {
2070 id: u32,
2071 status: &'static str,
2072 email: String,
2073 amount: Option<f64>,
2074 }
2075
2076 fn payment(id: u32, status: &'static str, email: &str, amount: f64) -> Payment {
2077 Payment {
2078 id,
2079 status,
2080 email: email.to_owned(),
2081 amount: Some(amount),
2082 }
2083 }
2084
2085 fn columns() -> Vec<Column<Payment>> {
2086 vec![
2087 column("status", "Status", |p: &Payment| p.status.into()).filterable(true),
2088 column("email", "Email", |p: &Payment| p.email.clone().into()),
2089 column("amount", "Amount", |p: &Payment| p.amount.into())
2090 .align(Align::End)
2091 .format(|p| {
2092 p.amount
2093 .map_or_else(String::new, |amount| format!("${amount:.2}"))
2094 }),
2095 column("id", "ID", |p: &Payment| p.id.into())
2096 .sortable(false)
2097 .hideable(false),
2098 ]
2099 }
2100
2101 fn payments(count: u32) -> Vec<Payment> {
2102 (1..=count)
2103 .map(|id| {
2104 let status = ["paid", "pending", "failed"][(id % 3) as usize];
2105 payment(
2106 id,
2107 status,
2108 &format!("user{id}@example.com"),
2109 f64::from(id * 10),
2110 )
2111 })
2112 .collect()
2113 }
2114
2115 fn state(count: u32) -> State<Payment, u32> {
2116 State::new(columns(), payments(count), |p| p.id)
2117 }
2118
2119 fn ids(rows: Vec<&Payment>) -> Vec<u32> {
2120 rows.into_iter().map(|p| p.id).collect()
2121 }
2122
2123 fn selected(output: Option<Output<u32>>) -> Vec<u32> {
2124 match output {
2125 Some(Output::Selected(keys)) => keys,
2126 other => panic!("expected a selection, got {other:?}"),
2127 }
2128 }
2129
2130 #[test]
2131 fn values_display_and_convert() {
2132 assert_eq!(Value::from(12.0).to_string(), "12");
2133 assert_eq!(Value::from(12.5).to_string(), "12.5");
2134 assert_eq!(Value::from(7_u32), Value::Number(7.0));
2135 assert_eq!(Value::from("a"), Value::Text("a".into()));
2136 assert_eq!(Value::from(None::<f64>), Value::Empty);
2137 assert_eq!(Value::from(Some(3_i64)), Value::Number(3.0));
2138 assert_eq!(Value::Empty.to_string(), "");
2139 assert_eq!(Value::default(), Value::Empty);
2140 }
2141
2142 #[test]
2143 fn compare_orders_numbers_then_text_then_empty() {
2144 let mut values = vec![
2145 Value::from("banana"),
2146 Value::Empty,
2147 Value::from(10.0),
2148 Value::from("Apple"),
2149 Value::from(2.0),
2150 Value::from("apple"),
2151 ];
2152 values.sort_by(compare);
2153 assert_eq!(
2154 values,
2155 vec![
2156 Value::from(2.0),
2157 Value::from(10.0),
2158 Value::from("Apple"),
2159 Value::from("apple"),
2160 Value::from("banana"),
2161 Value::Empty,
2162 ]
2163 );
2164 }
2165
2166 #[test]
2167 fn starts_unsorted_on_the_first_page() {
2168 let state = state(23);
2169 assert_eq!(state.len(), 23);
2170 assert_eq!(state.filtered_len(), 23);
2171 assert_eq!(state.page(), 0);
2172 assert_eq!(state.page_size(), 10);
2173 assert_eq!(state.page_count(), 3);
2174 assert_eq!(state.page_range(), 0..10);
2175 assert_eq!(ids(state.page_rows()), (1..=10).collect::<Vec<_>>());
2176 assert_eq!(state.showing(), "Showing 1-10 of 23");
2177 assert_eq!(state.sort(), None);
2178 assert!(!state.is_selectable());
2179 assert!(!state.is_filtered());
2180 }
2181
2182 #[test]
2183 fn an_empty_table_has_one_empty_page() {
2184 let state = state(0);
2185 assert!(state.is_empty());
2186 assert_eq!(state.page_count(), 1);
2187 assert_eq!(state.page_range(), 0..0);
2188 assert_eq!(state.showing(), "Showing 0 of 0");
2189 assert_eq!(state.page_check_state(), CheckState::Unchecked);
2190 }
2191
2192 #[test]
2193 fn header_presses_cycle_ascending_descending_and_unsorted() {
2194 let mut state = state(5).with_page_sizes([5]);
2195 let _ = state.update(Event::Sort("amount"));
2196 assert_eq!(state.sort(), Some(("amount", Direction::Ascending)));
2197 assert_eq!(ids(state.page_rows()), [1, 2, 3, 4, 5]);
2198 let _ = state.update(Event::Sort("amount"));
2199 assert_eq!(state.sort(), Some(("amount", Direction::Descending)));
2200 assert_eq!(ids(state.page_rows()), [5, 4, 3, 2, 1]);
2201 let _ = state.update(Event::Sort("amount"));
2202 assert_eq!(state.sort(), None);
2203 assert_eq!(ids(state.page_rows()), [1, 2, 3, 4, 5]);
2204 let _ = state.update(Event::Sort("email"));
2205 assert_eq!(state.sort(), Some(("email", Direction::Ascending)));
2206 }
2207
2208 #[test]
2209 fn unsortable_and_unknown_columns_are_ignored() {
2210 let mut state = state(5).with_sort("amount", Direction::Descending);
2211 let _ = state.update(Event::Sort("id"));
2212 let _ = state.update(Event::SortBy(Some(("missing", Direction::Ascending))));
2213 assert_eq!(state.sort(), Some(("amount", Direction::Descending)));
2214 let _ = state.update(Event::SortBy(None));
2215 assert_eq!(state.sort(), None);
2216 }
2217
2218 #[test]
2219 fn empty_values_sort_last_both_ways() {
2220 let mut rows = payments(3);
2221 rows[1].amount = None;
2222 let mut state = State::new(columns(), rows, |p| p.id);
2223 let _ = state.update(Event::SortBy(Some(("amount", Direction::Ascending))));
2224 assert_eq!(ids(state.page_rows()), [1, 3, 2]);
2225 let _ = state.update(Event::SortBy(Some(("amount", Direction::Descending))));
2226 assert_eq!(ids(state.page_rows()), [3, 1, 2]);
2227 }
2228
2229 #[test]
2230 fn search_matches_visible_searchable_text_ignoring_case() {
2231 let mut state = state(23);
2232 let _ = state.update(Event::NextPage);
2233 let _ = state.update(Event::Search("USER2".into()));
2234 assert_eq!(state.page(), 0, "a new search starts on the first page");
2235 assert_eq!(ids(state.page_rows()), [2, 20, 21, 22, 23]);
2236 let _ = state.update(Event::Search("$30.00".into()));
2237 assert_eq!(ids(state.page_rows()), [3], "searches the formatted text");
2238 let _ = state.update(Event::Columns(dropdown_menu::Event::Activate("amount")));
2239 assert!(state.is_hidden("amount"));
2240 assert_eq!(state.filtered_len(), 0, "hidden columns are not searched");
2241 assert!(state.is_filtered());
2242 }
2243
2244 #[test]
2245 fn column_filters_offer_distinct_values_and_combine_with_search() {
2246 let mut state = state(9);
2247 assert_eq!(state.options("status"), ["failed", "paid", "pending"]);
2248 assert!(state.options("email").is_empty());
2249 let _ = state.update(Event::Filter("status", Some("paid".into())));
2250 assert_eq!(state.filter("status"), Some("paid"));
2251 assert_eq!(ids(state.page_rows()), [3, 6, 9]);
2252 let _ = state.update(Event::Search("user9".into()));
2253 assert_eq!(ids(state.page_rows()), [9]);
2254 let _ = state.update(Event::Filter("status", None));
2255 assert_eq!(ids(state.page_rows()), [9]);
2256 let _ = state.update(Event::Filter("status", Some("paid".into())));
2257 let _ = state.update(Event::ClearFilters);
2258 assert_eq!(state.filtered_len(), 9);
2259 assert_eq!(state.search(), "");
2260 assert_eq!(state.filter("status"), None);
2261 let _ = state.update(Event::Filter("missing", Some("x".into())));
2262 assert!(!state.is_filtered(), "unknown columns are not filtered");
2263 }
2264
2265 #[test]
2266 fn pages_clamp_and_page_size_keeps_the_first_row_in_view() {
2267 let mut state = state(23);
2268 let _ = state.update(Event::PreviousPage);
2269 assert_eq!(state.page(), 0);
2270 let _ = state.update(Event::Page(9));
2271 assert_eq!(state.page(), 2);
2272 assert_eq!(state.showing(), "Showing 21-23 of 23");
2273 let _ = state.update(Event::NextPage);
2274 assert_eq!(state.page(), 2);
2275 let _ = state.update(Event::Page(1));
2276 let _ = state.update(Event::PageSize(20));
2277 assert_eq!(state.page(), 0, "row 11 is on the first page of 20");
2278 let _ = state.update(Event::PageSize(5));
2279 assert_eq!(state.page(), 0);
2280 let _ = state.update(Event::Page(3));
2281 let _ = state.update(Event::PageSize(0));
2282 assert_eq!(state.page_size(), 1, "at least one row a page");
2283 assert_eq!(state.page(), 15);
2284 }
2285
2286 #[test]
2287 fn page_sizes_can_be_set() {
2288 let state = state(23).with_page_sizes([0, 5, 25]);
2289 assert_eq!(state.page_sizes(), [5, 25]);
2290 assert_eq!(state.page_size(), 5);
2291 assert_eq!(state.page_count(), 5);
2292 let unchanged = self::state(3).with_page_sizes([]);
2293 assert_eq!(unchanged.page_sizes(), PAGE_SIZES);
2294 }
2295
2296 #[test]
2297 fn selection_needs_a_selectable_table() {
2298 let mut state = state(5);
2299 assert!(state.update(Event::Select(1, true)).is_none());
2300 assert!(state.selected().is_empty());
2301 }
2302
2303 #[test]
2304 fn rows_select_and_the_header_acts_on_the_page_only() {
2305 let mut state = state(23).with_selection(true);
2306 assert_eq!(selected(state.update(Event::Select(2, true))), [2]);
2307 assert_eq!(state.page_check_state(), CheckState::Indeterminate);
2308 assert!(state.update(Event::Select(2, true)).is_none(), "no change");
2309 assert!(
2310 state.update(Event::Select(99, true)).is_none(),
2311 "unknown row"
2312 );
2313
2314 let _ = state.update(Event::NextPage);
2315 assert_eq!(state.page_check_state(), CheckState::Unchecked);
2316 assert_eq!(
2317 selected(state.update(Event::SelectPage(true))),
2318 [2, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
2319 );
2320 assert_eq!(state.page_check_state(), CheckState::Checked);
2321 assert_eq!(selected(state.update(Event::SelectPage(false))), [2]);
2322 assert_eq!(
2323 selected(state.update(Event::ClearSelection)),
2324 Vec::<u32>::new()
2325 );
2326 assert!(state.update(Event::ClearSelection).is_none());
2327 }
2328
2329 #[test]
2330 fn selection_follows_keys_through_sorting_and_row_changes() {
2331 let mut state = state(5).with_selection(true);
2332 let _ = state.update(Event::Select(1, true));
2333 let _ = state.update(Event::Select(4, true));
2334 let _ = state.update(Event::Sort("amount"));
2335 let _ = state.update(Event::Sort("amount"));
2336 assert!(state.is_selected(&1) && state.is_selected(&4));
2337 state.retain(|p| p.id != 4);
2338 assert_eq!(state.selected(), [1]);
2339 state.update_row(&1, |p| p.status = "failed");
2340 assert_eq!(state.row(&1).map(|p| p.status), Some("failed"));
2341 state.push(payment(6, "paid", "new@example.com", 5.0));
2342 assert_eq!(state.len(), 5);
2343 assert_eq!(ids(state.page_rows()), [5, 3, 2, 1, 6], "sorted into place");
2344 state.set_rows(payments(2));
2345 assert_eq!(state.selected(), [1]);
2346 }
2347
2348 #[test]
2349 fn arrows_move_through_the_page_and_across_pages() {
2350 let mut state = state(23);
2351 let _ = state.update(Event::Next);
2352 assert_eq!(state.highlighted(), Some(&1));
2353 let _ = state.update(Event::Last);
2354 assert_eq!(state.highlighted(), Some(&10));
2355 let _ = state.update(Event::Next);
2356 assert_eq!(state.page(), 1);
2357 assert_eq!(state.highlighted(), Some(&11));
2358 let _ = state.update(Event::Previous);
2359 assert_eq!(state.page(), 0);
2360 assert_eq!(state.highlighted(), Some(&10));
2361 let _ = state.update(Event::First);
2362 let _ = state.update(Event::Previous);
2363 assert_eq!(state.highlighted(), Some(&1), "stays at the very first row");
2364 let _ = state.update(Event::Page(2));
2365 let _ = state.update(Event::Last);
2366 let _ = state.update(Event::Next);
2367 assert_eq!(state.highlighted(), Some(&23), "stays at the very last row");
2368
2369 let mut fresh = self::state(23);
2370 let _ = fresh.update(Event::Previous);
2371 assert_eq!(fresh.highlighted(), Some(&10));
2372 }
2373
2374 #[test]
2375 fn changing_page_moves_the_highlight_onto_it() {
2376 let mut state = state(23);
2377 let _ = state.update(Event::Press(3));
2378 let _ = state.update(Event::NextPage);
2379 assert_eq!(state.highlighted(), Some(&11));
2380 let _ = state.update(Event::Press(3));
2381 assert_eq!(
2382 state.highlighted(),
2383 Some(&11),
2384 "rows on other pages are ignored"
2385 );
2386 let _ = state.update(Event::Search("zzz".into()));
2387 assert_eq!(state.highlighted(), None);
2388 }
2389
2390 #[test]
2391 fn space_toggles_and_enter_activates_the_highlighted_row() {
2392 let mut state = state(5).with_selection(true);
2393 assert!(state.update(Event::ToggleHighlighted).is_none());
2394 assert!(state.update(Event::ActivateHighlighted).is_none());
2395 let _ = state.update(Event::Press(2));
2396 assert_eq!(selected(state.update(Event::ToggleHighlighted)), [2]);
2397 assert_eq!(
2398 selected(state.update(Event::ToggleHighlighted)),
2399 Vec::<u32>::new()
2400 );
2401 assert_eq!(
2402 state.update(Event::ActivateHighlighted),
2403 Some(Output::Activated(2))
2404 );
2405 assert_eq!(state.update(Event::Activate(4)), Some(Output::Activated(4)));
2406 assert_eq!(state.highlighted(), Some(&4));
2407 assert!(state.update(Event::Activate(40)).is_none());
2408 }
2409
2410 #[test]
2411 fn the_columns_menu_hides_columns_but_never_the_last() {
2412 let mut state = state(3);
2413 let menu: Vec<ColumnId> = state
2414 .columns_menu()
2415 .entries()
2416 .iter()
2417 .filter_map(|entry| entry.as_item().map(|item| item.id))
2418 .collect();
2419 assert_eq!(menu, ["status", "email", "amount"], "ID is not hideable");
2420 let _ = state.update(Event::Columns(dropdown_menu::Event::Open));
2421 let _ = state.update(Event::Columns(dropdown_menu::Event::Activate("email")));
2422 assert!(state.is_hidden("email"));
2423 assert!(!state.columns_menu().is_checked("email"));
2424 assert_eq!(state.visible_columns().len(), 3);
2425 let _ = state.update(Event::Columns(dropdown_menu::Event::Activate("email")));
2426 assert!(!state.is_hidden("email"));
2427
2428 let mut single = State::new(
2429 [column("name", "Name", |p: &Payment| p.email.clone().into())],
2430 payments(1),
2431 |p| p.id,
2432 );
2433 let _ = single.update(Event::Columns(dropdown_menu::Event::Activate("name")));
2434 assert!(!single.is_hidden("name"));
2435 assert!(single.columns_menu().is_checked("name"));
2436 }
2437
2438 #[test]
2439 fn with_hidden_starts_with_columns_hidden() {
2440 let state = state(3).with_hidden(["email"]);
2441 assert!(state.is_hidden("email"));
2442 assert!(!state.columns_menu().is_checked("email"));
2443 assert!(state.columns_menu().is_checked("status"));
2444 let visible: Vec<ColumnId> = state.visible_columns().iter().map(|c| c.id).collect();
2445 assert_eq!(visible, ["status", "amount", "id"]);
2446 }
2447
2448 #[test]
2449 fn focus_is_tracked_and_lands_on_a_row() {
2450 let mut state = state(3);
2451 let _ = state.update(Event::Focus(true));
2452 assert!(state.is_focused());
2453 assert_eq!(state.highlighted(), Some(&1));
2454
2455 let mut selected = self::state(3).with_selection(true);
2456 let _ = selected.update(Event::Select(2, true));
2457 let _ = selected.update(Event::Focus(true));
2458 assert_eq!(selected.highlighted(), Some(&2), "the first selected row");
2459 let _ = selected.update(Event::Focus(false));
2460 assert!(!selected.is_focused());
2461
2462 let empty = &mut self::state(0);
2463 let _ = empty.update(Event::Focus(true));
2464 assert_eq!(empty.highlighted(), None);
2465 }
2466
2467 #[test]
2468 fn columns_read_and_format_cells() {
2469 let columns = columns();
2470 let row = payment(7, "paid", "a@b.c", 12.5);
2471 assert_eq!(columns[2].value(&row), Value::Number(12.5));
2472 assert_eq!(columns[2].text(&row), "$12.50");
2473 assert_eq!(columns[0].text(&row), "paid");
2474 let column = columns[0]
2475 .clone()
2476 .width(120)
2477 .align(Align::Center)
2478 .searchable(false);
2479 assert_eq!(column.width, Length::Fixed(120.0));
2480 assert_eq!(column.align, Align::Center);
2481 assert!(!column.searchable);
2482 assert!(format!("{column:?}").contains("status"));
2483 assert_eq!(Align::ALL.len(), 3);
2484 assert_eq!(Direction::ALL.len(), 2);
2485 }
2486
2487 fn press(keymap: &Keymap<Action>, chord: &str) -> Option<Action> {
2488 let chord: Chord = chord.parse().unwrap();
2489 keymap.resolve(chord.key(), chord.modifiers())
2490 }
2491
2492 #[test]
2493 fn default_keymap_covers_every_action() {
2494 let keymap = default_keymap();
2495 assert_eq!(press(&keymap, "ArrowDown"), Some(Action::Next));
2496 assert_eq!(press(&keymap, "ArrowUp"), Some(Action::Previous));
2497 assert_eq!(press(&keymap, "Home"), Some(Action::First));
2498 assert_eq!(press(&keymap, "End"), Some(Action::Last));
2499 assert_eq!(press(&keymap, "PageDown"), Some(Action::NextPage));
2500 assert_eq!(press(&keymap, "PageUp"), Some(Action::PreviousPage));
2501 assert_eq!(press(&keymap, "Space"), Some(Action::Select));
2502 assert_eq!(press(&keymap, "Enter"), Some(Action::Activate));
2503 assert_eq!(press(&keymap, "Mod+A"), Some(Action::SelectAll));
2504 assert_eq!(press(&keymap, "Escape"), Some(Action::ClearSelection));
2505 for action in <Action as keys::Action>::ALL {
2506 assert!(!keymap.chords(action).is_empty(), "{action:?}");
2507 assert!(keys::Action::description(*action).ends_with('.'));
2508 }
2509 assert_eq!(Keymap::<Action>::defaults(), keymap);
2510 }
2511
2512 #[test]
2513 fn keymap_overrides_and_unbinding() {
2514 let keymap = default_keymap()
2515 .unbind(&Chord::named(Named::Escape))
2516 .bind(Chord::character('j'), Action::Next);
2517 let mut state = state(5).with_selection(true);
2518 let _ = state.update(Event::Select(1, true));
2519 let escape = keys::Event {
2520 key: Key::Named(Named::Escape),
2521 modifiers: Modifiers::empty(),
2522 };
2523 assert_eq!(state.key_event(&keymap, &escape), None);
2524 let j = keys::Event {
2525 key: Key::Character("j".into()),
2526 modifiers: Modifiers::empty(),
2527 };
2528 assert_eq!(state.key_event(&keymap, &j), Some(Event::Next));
2529 assert!(default_keymap().clear().is_empty());
2530 }
2531
2532 #[test]
2533 fn actions_map_to_events_only_when_they_apply() {
2534 let mut state = state(23);
2535 assert_eq!(Action::Next.event(&state), Some(Event::Next));
2536 assert_eq!(Action::PreviousPage.event(&state), None);
2537 assert_eq!(Action::NextPage.event(&state), Some(Event::NextPage));
2538 assert_eq!(Action::Activate.event(&state), None);
2539 assert_eq!(Action::Select.event(&state), None, "not selectable");
2540 assert_eq!(Action::SelectAll.event(&state), None);
2541 let _ = state.update(Event::First);
2542 assert_eq!(
2543 Action::Activate.event(&state),
2544 Some(Event::ActivateHighlighted)
2545 );
2546
2547 let mut selectable = self::state(3).with_selection(true);
2548 assert_eq!(
2549 Action::SelectAll.event(&selectable),
2550 Some(Event::SelectPage(true))
2551 );
2552 assert_eq!(
2553 Action::ClearSelection.event(&selectable),
2554 None,
2555 "nothing selected"
2556 );
2557 let _ = selectable.update(Event::SelectPage(true));
2558 assert_eq!(
2559 Action::ClearSelection.event(&selectable),
2560 Some(Event::ClearSelection)
2561 );
2562 let _ = selectable.update(Event::First);
2563 assert_eq!(
2564 Action::Select.event(&selectable),
2565 Some(Event::ToggleHighlighted)
2566 );
2567
2568 let empty = self::state(0);
2569 for action in [Action::Next, Action::Previous, Action::First, Action::Last] {
2570 assert_eq!(action.event(&empty), None, "{action:?}");
2571 }
2572 }
2573
2574 #[test]
2575 fn rows_are_muted_when_selected_and_ringed_when_highlighted() {
2576 for theme in [light(), dark()] {
2577 let tokens = Tokens::of(&theme);
2578 let idle = row_style(&tokens, Status::default());
2579 assert_eq!(idle.background, None);
2580 assert_eq!(idle.ring, None);
2581 assert_eq!(idle.divider, tokens.border);
2582 let selected = row_style(
2583 &tokens,
2584 Status {
2585 selected: true,
2586 hovered: true,
2587 ..Status::default()
2588 },
2589 );
2590 assert_eq!(selected.background, Some(tokens.muted));
2591 let hovered = row_style(
2592 &tokens,
2593 Status {
2594 hovered: true,
2595 ..Status::default()
2596 },
2597 );
2598 assert_eq!(hovered.background, Some(fade(tokens.muted, 0.5)));
2599 let highlighted = row_style(
2600 &tokens,
2601 Status {
2602 highlighted: true,
2603 ..Status::default()
2604 },
2605 );
2606 assert_eq!(highlighted.ring, Some(tokens.ring));
2607
2608 let frame = frame_style(&tokens);
2609 assert_eq!(frame.border.color, tokens.border);
2610 assert_eq!(frame.border.radius, radius::MD.into());
2611 assert_eq!(
2612 skeleton_style(&tokens).background,
2613 Some(Background::Color(tokens.muted))
2614 );
2615 }
2616 }
2617
2618 #[test]
2619 fn sort_indicators_show_the_direction() {
2620 assert_eq!(sort_glyph(None), crate::lucide!(ArrowUpDown));
2621 assert_eq!(
2622 sort_glyph(Some(Direction::Ascending)),
2623 crate::lucide!(ArrowUp)
2624 );
2625 assert_eq!(
2626 sort_glyph(Some(Direction::Descending)),
2627 crate::lucide!(ArrowDown)
2628 );
2629 }
2630
2631 #[test]
2632 fn filter_and_sort_choices_read_well() {
2633 let all = FilterChoice {
2634 column: "status",
2635 header: "Status".into(),
2636 value: None,
2637 };
2638 assert_eq!(all.to_string(), "Status: All");
2639 let paid = FilterChoice {
2640 value: Some("paid".into()),
2641 ..all
2642 };
2643 assert_eq!(paid.to_string(), "Status: paid");
2644 let sort = SortChoice {
2645 sort: None,
2646 label: "Default order".into(),
2647 };
2648 assert_eq!(sort.to_string(), "Default order");
2649 }
2650
2651 #[test]
2652 fn builder_defaults_and_options() {
2653 let state = state(3);
2654 let table: DataTable<'_, Payment, u32, ()> = data_table(&state);
2655 assert!(table.toolbar && !table.loading && !table.row_actions);
2656 assert!(!table.is_enabled());
2657 assert_eq!(table.height, Length::Shrink);
2658 assert_eq!(table.breakpoint, BREAKPOINT);
2659 assert_eq!(table.keymap, default_keymap());
2660 assert_eq!(table.empty, "No results.");
2661 let table = table
2662 .toolbar(false)
2663 .loading(true)
2664 .empty("Nothing here")
2665 .placeholder("Filter emails...")
2666 .height(300)
2667 .row_height(32.0)
2668 .breakpoint(400.0)
2669 .keymap(Keymap::new())
2670 .row_actions(true)
2671 .cell("status", |p: &Payment| text(p.status).into())
2672 .cell("status", |p: &Payment| text(p.status).into())
2673 .on_event(|_| ());
2674 assert!(!table.toolbar && table.loading && table.row_actions);
2675 assert_eq!(table.cells.len(), 1, "a second renderer replaces the first");
2676 assert_eq!(table.placeholder, "Filter emails...");
2677 assert_eq!(table.height, Length::Fixed(300.0));
2678 assert_eq!(table.row_height, 32.0);
2679 assert!(table.is_enabled());
2680 assert!(format!("{table:?}").contains("DataTable"));
2681 }
2682}