Skip to main content

iced_cube/data/
data_table.rs

1//! Records in rows and columns, with sorting, filtering, selection and
2//! pages.
3//!
4//! [`State`] owns the rows, the [`Column`]s that read them, and everything
5//! the reader changes: the sort, the search and column filters, the page,
6//! the selection and which columns are hidden. Its
7//! [`update`](State::update) is pure, so it is tested without a renderer.
8//! The view only builds the rows of the current page that are in view, and
9//! the header stays put while they scroll.
10//!
11//! Narrower than its [breakpoint](DataTable::breakpoint), the table turns
12//! into a stack of cards, one per row, with each column as a labelled line,
13//! so it never overflows a phone screen.
14//!
15//! The table resolves its keys through a [`Keymap`] of [`Action`]s while it
16//! has focus, which it takes when pressed; see [`default_keymap`].
17
18use 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
44/// Height of the header row.
45pub const HEADER_HEIGHT: f32 = 40.0;
46/// Height of every body row.
47pub const ROW_HEIGHT: f32 = 44.0;
48/// Below this width, in logical pixels, rows become cards.
49pub const BREAKPOINT: f32 = 560.0;
50/// The page sizes offered unless the state sets its own.
51pub const PAGE_SIZES: [usize; 4] = [10, 20, 50, 100];
52/// Placeholder rows shown while loading.
53pub const SKELETON_ROWS: usize = 5;
54/// Height of one labelled line in a card.
55pub 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
61/// Names a column. Sorting, filters and hiding refer to columns by it.
62pub type ColumnId = &'static str;
63
64/// A cell's value, which sorting, searching and filtering read.
65#[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
122/// Orders two values: numbers by size, text alphabetically ignoring case,
123/// and numbers before text.
124pub 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/// Which way a column is sorted.
140#[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/// How a column's header and cells line up.
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
152pub enum Align {
153    #[default]
154    Start,
155    Center,
156    /// For numbers, such as amounts.
157    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
172/// One column: its header, how it reads a row, and how it is shown.
173pub 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    /// Offers a filter of the column's distinct values in the toolbar.
181    pub filterable: bool,
182    /// Whether the search box looks in this column.
183    pub searchable: bool,
184    value: fn(&Row) -> Value,
185    format: Option<fn(&Row) -> String>,
186}
187
188/// A sortable, searchable and hideable column that fills an equal share
189/// of the width. `value` reads the cell from a row.
190pub 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    /// A fixed width in pixels, or a share of the room with
235    /// `Length::FillPortion`.
236    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    /// The text shown, searched and filtered on, in place of the value's
267    /// own, such as an amount with its currency. Sorting still uses the
268    /// value.
269    pub fn format(mut self, format: fn(&Row) -> String) -> Self {
270        self.format = Some(format);
271        self
272    }
273
274    /// The cell's value in `row`.
275    pub fn value(&self, row: &Row) -> Value {
276        (self.value)(row)
277    }
278
279    /// The cell's text in `row`.
280    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/// Everything that changes a table.
289#[derive(Debug, Clone, PartialEq)]
290pub enum Event<Key> {
291    /// A header was pressed: unsorted, then ascending, then descending.
292    Sort(ColumnId),
293    /// Sorts by a column in a direction, or restores the row order.
294    SortBy(Option<(ColumnId, Direction)>),
295    Search(String),
296    /// Keeps only rows whose cell in the column shows this text, or clears
297    /// the column's filter.
298    Filter(ColumnId, Option<String>),
299    /// Clears the search and every filter.
300    ClearFilters,
301    PageSize(usize),
302    /// Goes to a page, counting from zero.
303    Page(usize),
304    NextPage,
305    PreviousPage,
306    /// A row's checkbox changed.
307    Select(Key, bool),
308    /// The header checkbox changed: selects or clears the rows on the page.
309    SelectPage(bool),
310    ClearSelection,
311    /// The pointer pressed a row.
312    Press(Key),
313    /// A row was double clicked.
314    Activate(Key),
315    /// Highlights the next row, moving to the next page after the last.
316    Next,
317    /// Highlights the previous row, moving to the previous page before the
318    /// first.
319    Previous,
320    First,
321    Last,
322    ToggleHighlighted,
323    ActivateHighlighted,
324    /// Navigation and choices in the Columns menu.
325    Columns(dropdown_menu::Event<ColumnId>),
326    /// The table gained or lost focus. The widget sends this itself.
327    Focus(bool),
328}
329
330/// What the app may need to act on after an [`Event`].
331#[derive(Debug, Clone, PartialEq)]
332pub enum Output<Key> {
333    /// A row was activated with Enter or a double click.
334    Activated(Key),
335    /// The selection changed. Holds every selected key, in row order.
336    Selected(Vec<Key>),
337}
338
339/// The rows, the columns and how the reader is looking at them.
340#[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    /// Indices into `rows` that pass the filters, in sorted order.
352    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    /// A table over `rows`, showing `columns` in order. `key` gives each
364    /// row an identity that survives sorting and new rows, such as an id.
365    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    /// Adds a checkbox to every row and a select-all box to the header.
395    pub fn with_selection(mut self, selectable: bool) -> Self {
396        self.selectable = selectable;
397        self
398    }
399
400    /// The sizes offered in the footer. The first is used to start with.
401    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    /// Starts with these columns hidden.
417    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    /// The columns on screen, in order.
433    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    /// The Columns menu, with a check for each hideable column.
449    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    /// Replaces every row. The sort, filters and selection of rows that
462    /// remain are kept.
463    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    /// Changes the row with this key, if there is one.
474    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    /// Keeps only the rows for which `keep` returns true.
483    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    /// The text a column is filtered to, if any.
497    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    /// Whether the search or any filter hides rows.
505    pub fn is_filtered(&self) -> bool {
506        !self.search.trim().is_empty() || !self.filters.is_empty()
507    }
508
509    /// The distinct texts of a filterable column, to choose from.
510    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    /// How many rows pass the search and filters.
518    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    /// The current page, counting from zero.
531    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    /// How many pages the filtered rows fill; at least one.
544    pub fn page_count(&self) -> usize {
545        self.order.len().div_ceil(self.page_size).max(1)
546    }
547
548    /// Positions of the current page's rows among the filtered rows.
549    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    /// The rows on the current page, in order.
555    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    /// The keys of the rows on the current page, in order.
563    pub fn page_keys(&self) -> Vec<Key> {
564        self.page_rows().into_iter().map(self.key).collect()
565    }
566
567    /// "Showing 1-10 of 243", for the footer.
568    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    /// Every selected key, on any page, in row order.
590    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    /// The header checkbox: checked when every row on the page is
599    /// selected, indeterminate when some are.
600    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    /// The row the keyboard acts on.
614    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    /// Applies an event and returns what the app may need to act on.
623    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                // Focus lands on the first selected row of the page, or its
699                // first row, so the keyboard has somewhere to start.
700                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    /// Turns a key press into an event through `keymap`.
714    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    /// Shows or hides a column. The last visible column cannot be hidden.
804    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    /// Recomputes the filtered, sorted order and everything that depends on
831    /// the rows.
832    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/// What a data table keyboard shortcut does.
920#[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    /// The [`Event`] this action sends to `state`, or `None` when it does
936    /// nothing, so the key is left for others.
937    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
1007/// The default data table shortcuts:
1008///
1009/// | Keys | Action |
1010/// | --- | --- |
1011/// | `ArrowDown` | [`Action::Next`] |
1012/// | `ArrowUp` | [`Action::Previous`] |
1013/// | `Home` | [`Action::First`] |
1014/// | `End` | [`Action::Last`] |
1015/// | `PageDown` | [`Action::NextPage`] |
1016/// | `PageUp` | [`Action::PreviousPage`] |
1017/// | `Space` | [`Action::Select`] |
1018/// | `Enter` | [`Action::Activate`] |
1019/// | `Mod+A` (Ctrl, or Cmd on macOS) | [`Action::SelectAll`] |
1020/// | `Escape` | [`Action::ClearSelection`] |
1021///
1022/// The table resolves these itself, only while it has focus, so they never
1023/// clash with other components. Pass a changed keymap with
1024/// [`DataTable::keymap`].
1025pub 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/// How a row is shown.
1040#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
1041pub struct Status {
1042    pub selected: bool,
1043    pub hovered: bool,
1044    /// Carries the keyboard highlight while the table has focus.
1045    pub highlighted: bool,
1046}
1047
1048/// The colours of a row.
1049#[derive(Debug, Clone, Copy, PartialEq)]
1050pub struct RowStyle {
1051    pub background: Option<Color>,
1052    /// The ring around the row carrying the keyboard highlight.
1053    pub ring: Option<Color>,
1054    /// The line between rows.
1055    pub divider: Color,
1056}
1057
1058/// Resolves a row's colours: muted when selected, a lighter tint under the
1059/// pointer.
1060pub 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
1075/// The frame around the table: a subtle border with rounded corners.
1076pub 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
1087/// A placeholder bar in a loading row.
1088pub 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
1099/// The sort indicator for a column: arrows up and down while unsorted, or
1100/// the direction.
1101pub 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
1114/// A data table builder. Convert it into an [`Element`] to render.
1115pub 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
1132/// Renders `state`. Without [`on_event`](DataTable::on_event) every control
1133/// renders disabled.
1134pub 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    /// Draws the cells of a column with `render`, such as a badge or a
1168    /// progress bar, in place of its text. Keep it within a row's height.
1169    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    /// Shows the search box, column filters and Columns menu above the
1180    /// table. On by default.
1181    pub fn toolbar(mut self, toolbar: bool) -> Self {
1182        self.toolbar = toolbar;
1183        self
1184    }
1185
1186    /// The search box placeholder.
1187    pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
1188        self.placeholder = placeholder.into();
1189        self
1190    }
1191
1192    /// Shows placeholder rows in place of the data.
1193    pub fn loading(mut self, loading: bool) -> Self {
1194        self.loading = loading;
1195        self
1196    }
1197
1198    /// The message shown when no rows pass the search and filters.
1199    pub fn empty(mut self, message: impl Into<String>) -> Self {
1200        self.empty = message.into();
1201        self
1202    }
1203
1204    /// A fixed or filling height scrolls the rows under the header.
1205    /// Defaults to shrinking to fit the page.
1206    pub fn height(mut self, height: impl Into<Length>) -> Self {
1207        self.height = height.into();
1208        self
1209    }
1210
1211    /// The height of each row, such as 32 for a compact table. Defaults to
1212    /// [`ROW_HEIGHT`]. Custom cells must fit inside it.
1213    pub fn row_height(mut self, height: f32) -> Self {
1214        self.row_height = height;
1215        self
1216    }
1217
1218    /// The width below which rows become cards. Defaults to [`BREAKPOINT`].
1219    pub fn breakpoint(mut self, width: f32) -> Self {
1220        self.breakpoint = width;
1221        self
1222    }
1223
1224    /// Replaces the [`default_keymap`] the focused table resolves keys with.
1225    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    /// Gives every row the context menu of `menu`, keyed by row key. It
1236    /// opens on a right-click or a long press, and on the highlighted row
1237    /// with its opening chord while the table has focus.
1238    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    /// Replaces the context menu's default keymap.
1255    pub fn context_menu_keymap(mut self, keymap: Keymap<context_menu::Action>) -> Self {
1256        self.menu_keymap = keymap;
1257        self
1258    }
1259
1260    /// Adds a button at the end of each row that opens its context menu,
1261    /// for touch screens and anyone who does not right-click.
1262    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
1319/// Everything the table's parts are built from, shared by the layouts.
1320struct 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    /// Lets the grid take focus and resolve its keys while it has it.
1371    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    /// The header above the rows of the page, placeholder rows while
1584    /// loading, or the empty message.
1585    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    /// Adds the press and double click messages and the context menu.
1704    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    /// The card height for the visible columns: the first column heads the
1787    /// card, and each other column is a labelled line.
1788    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/// An option in a column filter's select.
2028#[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/// An option in the sort select shown above cards.
2045#[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}