summaryrefslogtreecommitdiffstats
path: root/src/components/data_table.rs
blob: 5fb5cb2c1bd3809645c8dd8ec8d0ccfcc0119c2c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
use std::{convert::TryInto, marker::PhantomData};

pub mod column;
pub use column::*;

pub mod styling;
pub use styling::*;

pub mod props;
pub use props::DataTableProps;

pub mod state;
pub use state::{DataTableState, ScrollDirection};

pub mod draw;
pub use draw::*;

pub mod data_type;
pub use data_type::*;

pub mod sortable;
pub use sortable::*;

/// A [`DataTable`] is a component that displays data in a tabular form.
///
/// Note that [`DataTable`] takes a generic type `S`, bounded by [`SortType`]. This controls whether this table
/// expects sorted data or not, with two expected types:
///
/// - [`Unsortable`]: The default if otherwise not specified. This table does not expect sorted data.
/// - [`Sortable`]: This table expects sorted data, and there are helper functions to
///   facilitate things like sorting based on a selected column, shortcut column selection support, mouse column
///   selection support, etc.
pub struct DataTable<DataType, Header, S = Unsortable, C = Column<Header>> {
    pub columns: Vec<C>,
    pub state: DataTableState,
    pub props: DataTableProps,
    pub styling: DataTableStyling,
    data: Vec<DataType>,
    sort_type: S,
    first_draw: bool,
    _pd: PhantomData<(DataType, S, Header)>,
}

impl<DataType: DataToCell<H>, H: ColumnHeader> DataTable<DataType, H, Unsortable, Column<H>> {
    pub fn new<C: Into<Vec<Column<H>>>>(
        columns: C, props: DataTableProps, styling: DataTableStyling,
    ) -> Self {
        Self {
            columns: columns.into(),
            state: DataTableState::default(),
            props,
            styling,
            data: vec![],
            sort_type: Unsortable,
            first_draw: true,
            _pd: PhantomData,
        }
    }
}

impl<DataType: DataToCell<H>, H: ColumnHeader, S: SortType, C: DataTableColumn<H>>
    DataTable<DataType, H, S, C>
{
    /// Sets the scroll position to the first value.
    pub fn set_first(&mut self) {
        self.state.current_index = 0;
        self.state.scroll_direction = ScrollDirection::Up;
    }

    /// Sets the scroll position to the last value.
    pub fn set_last(&mut self) {
        self.state.current_index = self.data.len().saturating_sub(1);
        self.state.scroll_direction = ScrollDirection::Down;
    }

    /// Updates the scroll position to be valid for the number of entries.
    pub fn set_data(&mut self, data: Vec<DataType>) {
        self.data = data;
        let max_pos = self.data.len().saturating_sub(1);
        if self.state.current_index > max_pos {
            self.state.current_index = max_pos;
            self.state.display_start_index = 0;
            self.state.scroll_direction = ScrollDirection::Down;
        }
    }

    /// Increments the scroll position if possible by a positive/negative offset. If there is a
    /// valid change, this function will also return the new position wrapped in an [`Option`].
    pub fn increment_position(&mut self, change: i64) -> Option<usize> {
        let max_index = self.data.len();
        let current_index = self.state.current_index;

        if change == 0
            || (change > 0 && current_index == max_index)
            || (change < 0 && current_index == 0)
        {
            return None;
        }

        let csp: Result<i64, _> = self.state.current_index.try_into();
        if let Ok(csp) = csp {
            let proposed: Result<usize, _> = (csp + change).try_into();
            if let Ok(proposed) = proposed {
                if proposed < self.data.len() {
                    self.state.current_index = proposed;
                    self.state.scroll_direction = if change < 0 {
                        ScrollDirection::Up
                    } else {
                        ScrollDirection::Down
                    };

                    return Some(self.state.current_index);
                }
            }
        }

        None
    }

    /// Updates the scroll position to a selected index.
    #[allow(clippy::comparison_chain)]
    pub fn set_position(&mut self, new_index: usize) {
        let new_index = new_index.clamp(0, self.data.len().saturating_sub(1));
        if self.state.current_index < new_index {
            self.state.scroll_direction = ScrollDirection::Down;
        } else if self.state.current_index > new_index {
            self.state.scroll_direction = ScrollDirection::Up;
        }
        self.state.current_index = new_index;
    }

    /// Returns the current scroll index.
    pub fn current_index(&self) -> usize {
        self.state.current_index
    }

    /// Optionally returns the currently selected item, if there is one.
    pub fn current_item(&self) -> Option<&DataType> {
        self.data.get(self.state.current_index)
    }

    /// Returns tui-rs' internal selection.
    pub fn tui_selected(&self) -> Option<usize> {
        self.state.table_state.selected()
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[derive(Clone, PartialEq, Eq, Debug)]
    struct TestType {
        index: usize,
    }

    impl DataToCell<&'static str> for TestType {
        fn to_cell(
            &self, _column: &&'static str, _calculated_width: u16,
        ) -> Option<tui::text::Text<'_>> {
            None
        }

        fn column_widths<C: DataTableColumn<&'static str>>(
            _data: &[Self], _columns: &[C],
        ) -> Vec<u16>
        where
            Self: Sized,
        {
            vec![]
        }
    }

    #[test]
    fn test_data_table_operations() {
        let columns = [Column::hard("a", 10), Column::hard("b", 10)];
        let props = DataTableProps {
            title: Some("test".into()),
            table_gap: 1,
            left_to_right: false,
            is_basic: false,
            show_table_scroll_position: true,
            show_current_entry_when_unfocused: false,
        };
        let styling = DataTableStyling::default();

        let mut table = DataTable::new(columns, props, styling);
        table.set_data((0..=4).map(|index| TestType { index }).collect::<Vec<_>>());

        table.set_last();
        assert_eq!(table.current_index(), 4);
        assert_eq!(table.state.scroll_direction, ScrollDirection::Down);

        table.set_first();
        assert_eq!(table.current_index(), 0);
        assert_eq!(table.state.scroll_direction, ScrollDirection::Up);

        table.set_position(4);
        assert_eq!(table.current_index(), 4);
        assert_eq!(table.state.scroll_direction, ScrollDirection::Down);

        table.set_position(100);
        assert_eq!(table.current_index(), 4);
        assert_eq!(table.state.scroll_direction, ScrollDirection::Down);
        assert_eq!(table.current_item(), Some(&TestType { index: 4 }));

        table.increment_position(-1);
        assert_eq!(table.current_index(), 3);
        assert_eq!(table.state.scroll_direction, ScrollDirection::Up);
        assert_eq!(table.current_item(), Some(&TestType { index: 3 }));

        table.increment_position(-3);
        assert_eq!(table.current_index(), 0);
        assert_eq!(table.state.scroll_direction, ScrollDirection::Up);
        assert_eq!(table.current_item(), Some(&TestType { index: 0 }));

        table.increment_position(-3);
        assert_eq!(table.current_index(), 0);
        assert_eq!(table.state.scroll_direction, ScrollDirection::Up);
        assert_eq!(table.current_item(), Some(&TestType { index: 0 }));

        table.increment_position(1);
        assert_eq!(table.current_index(), 1);
        assert_eq!(table.state.scroll_direction, ScrollDirection::Down);
        assert_eq!(table.current_item(), Some(&TestType { index: 1 }));

        table.increment_position(3);
        assert_eq!(table.current_index(), 4);
        assert_eq!(table.state.scroll_direction, ScrollDirection::Down);
        assert_eq!(table.current_item(), Some(&TestType { index: 4 }));

        table.increment_position(10);
        assert_eq!(table.current_index(), 4);
        assert_eq!(table.state.scroll_direction, ScrollDirection::Down);
        assert_eq!(table.current_item(), Some(&TestType { index: 4 }));

        table.set_data((0..=2).map(|index| TestType { index }).collect::<Vec<_