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