terminal_ui/data/
lazy_stateful_table.rs

1use tui::widgets::TableState;
2
3use super::Stateful;
4
5pub const CAPACITY: usize = 1000;
6const ROOM: usize = 100;
7
8pub trait LazySource<T> {
9    fn source(&self, from: usize, to: usize) -> Vec<T>;
10    fn source_elements_containing(&self, element: usize, quantity: usize) -> (Vec<T>, usize, usize);
11}
12
13enum Area {
14    Below,
15    Inside,
16    Above,
17}
18
19impl Area {
20    fn current_area(i: usize, elements: usize) -> Area {
21        match i {
22            i if i < ((elements / 2).overflowing_sub(ROOM).0) => Area::Below,
23            i if (((elements / 2).overflowing_sub(ROOM).0)..=(elements / 2 + ROOM)).contains(&i) => Area::Inside,
24            i if i > (elements / 2 + ROOM) => Area::Above,
25            _ => Area::Below,
26        }
27    }
28}
29
30pub struct LazyStatefulTable<T> {
31    pub state: TableState,
32    pub items: Vec<T>,
33    offset: usize,
34    source: Box<dyn LazySource<T>>,
35}
36
37impl<T: Clone> LazyStatefulTable<T> {
38    pub fn new(source: Box<dyn LazySource<T>>) -> LazyStatefulTable<T> {
39        let items = source.source(0, CAPACITY);
40        LazyStatefulTable {
41            state: TableState::default(),
42            items,
43            offset: 0,
44            source,
45        }
46    }
47
48    pub fn reload(&mut self) {
49        self.items = self.source.source(self.offset, CAPACITY);
50
51        self.state.select(match self.state.selected() {
52            Some(i) => Some(i.min(if !self.items.is_empty() {self.items.len() - 1} else {0})),
53            _ => None,
54        });
55    }
56
57
58    pub fn navigate_to(&mut self, element: usize) {
59        let source = self.source.source_elements_containing(element, CAPACITY);
60
61        self.items = source.0;
62        self.offset = source.1;
63        self.state.select(Some(source.2));
64
65    }
66
67
68    pub fn navigate_to_bottom(&mut self) {
69        let mut current = self.next();
70        let mut next = self.next();
71        while current != next {
72            current = next;
73            next = self.next();
74        }
75    }
76
77    pub fn get_selected_item(&self) -> Option<T>{
78        match self.state.selected() {
79            Some(i) => self.items.get(i).cloned(),
80            None => None
81        }
82    }
83
84    pub fn clear(&mut self) {
85        self.state.select(None);
86        self.items.clear();
87    }
88
89    fn select_and_set_scroll_on_top(&mut self, index: usize) {
90        // Need to manually set private field offset when scrolling up for smooth experience
91        // Requested to make this public https://github.com/fdehau/tui-rs/issues/626
92        // but using unsafe in the meantime
93        unsafe {
94            self.state = std::mem::transmute::<(usize, Option<usize>), TableState>((index, None))
95        }
96    }
97
98}
99
100impl<T: Clone> Stateful<T> for LazyStatefulTable<T> {
101    fn next(&mut self) -> usize {
102        if self.items.is_empty() {
103            self.items = self.source.source(0, CAPACITY)
104        }
105        if !self.items.is_empty() {
106            let i = match self.state.selected() {
107                Some(i) => match Area::current_area(i, self.items.len()) {
108                    Area::Below | Area::Inside => {
109                        if (i + 1) < self.items.len() {
110                            i + 1
111                        } else {
112                            i
113                        }
114                    }
115                    Area::Above => {
116                        let len = self.items.len();
117                        let last_element = len + self.offset;
118
119                        let new_data = self.source.source(last_element, last_element + ROOM);
120
121                        let received_elements = new_data.len();
122                        self.items.rotate_left(received_elements);
123
124                        self.items[(len - received_elements)..len]
125                            .iter_mut()
126                            .zip(new_data)
127                            .for_each(|(current, new_data)| *current = new_data);
128                        self.offset += received_elements;
129
130                        self.state.select(None);
131                        i - received_elements + if (i + 1) < len { 1 } else { 0 }
132                    }
133                },
134
135                None => 0,
136            };
137            self.state.select(Some(i));
138        }
139
140        self.state.selected().unwrap_or_default()
141    }
142
143    fn previous(&mut self) -> usize {
144        if self.items.is_empty() {
145            self.items = self.source.source(0, CAPACITY)
146        }
147        if !self.items.is_empty() {
148            let i = match self.state.selected() {
149                Some(i) => match Area::current_area(i, self.items.len()) {
150                    Area::Above | Area::Inside => {
151                        if i > 0 {
152                            i - 1
153                        } else {
154                            i
155                        }
156                    }
157                    Area::Below => {
158                        let initial_element = if self.offset > ROOM {
159                            self.offset - ROOM
160                        } else {
161                            0
162                        };
163
164                        let new_data = self.source.source(initial_element, self.offset);
165
166                        let received_elements = new_data.len();
167
168                        let selected = i + received_elements - if i > 0 { 1 } else { 0 };
169
170                        if received_elements > 0 {
171                            self.items.rotate_right(received_elements);
172
173                            self.items[0..received_elements]
174                                .iter_mut()
175                                .zip(new_data)
176                                .for_each(|(current, new_data)| *current = new_data);
177                            self.offset -= received_elements;
178
179
180                            self.select_and_set_scroll_on_top(selected);
181
182
183                        }
184                        selected
185                    }
186                },
187
188                None => 0,
189            };
190            self.state.select(Some(i));
191        }
192
193        self.state.selected().unwrap_or_default()
194    }
195
196    fn unselect(&mut self) {
197        self.state.select(None);
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    struct TestSourcer<T> {
206        items: Vec<T>,
207    }
208
209    impl<T: Clone> LazySource<T> for TestSourcer<T> {
210        fn source(&self, from: usize, to: usize) -> Vec<T> {
211            self.items[from.min(self.items.len())..to.min(self.items.len())].to_vec()
212        }
213
214        fn source_elements_containing(
215            &self,
216            _element: usize,
217            _quantity: usize,
218        ) -> (Vec<T>, usize, usize) {
219            todo!()
220        }
221    }
222
223    #[test]
224    fn source_init() {
225        let test_source = TestSourcer {
226            items: (0..2000_usize).collect(),
227        };
228        let lazy_table = LazyStatefulTable::new(Box::new(test_source));
229
230        assert!(lazy_table.items.len() == CAPACITY)
231    }
232
233    #[test]
234    fn single_next_doesnt_source() {
235        let test_source = TestSourcer {
236            items: (0..2000_usize).collect(),
237        };
238        let mut lazy_table = LazyStatefulTable::new(Box::new(test_source));
239        lazy_table.next();
240        assert!(lazy_table.items[0] == 0 && *lazy_table.items.last().unwrap() == 999);
241    }
242
243    #[test]
244    fn double_next_doesnt_source() {
245        let test_source = TestSourcer {
246            items: (0..2000_usize).collect(),
247        };
248        let mut lazy_table = LazyStatefulTable::new(Box::new(test_source));
249        lazy_table.next();
250        lazy_table.next();
251        assert!(lazy_table.items[0] == 0 && *lazy_table.items.last().unwrap() == 999);
252    }
253
254    #[test]
255    fn next_inside_doesnt_source() {
256        let test_source = TestSourcer {
257            items: (0..2000_usize).collect(),
258        };
259        let mut lazy_table = LazyStatefulTable::new(Box::new(test_source));
260        for _ in 0..(CAPACITY / 2) {
261            lazy_table.next();
262        }
263        assert!(lazy_table.items[0] == 0 && *lazy_table.items.last().unwrap() == 999);
264    }
265
266    #[test]
267    fn next_outside_sources() {
268        let test_source = TestSourcer {
269            items: (0..2000_usize).collect(),
270        };
271        let mut lazy_table = LazyStatefulTable::new(Box::new(test_source));
272        lazy_table.state.select(Some(CAPACITY / 2 + ROOM + 1));
273        lazy_table.next();
274        assert!(lazy_table.items[0] == 100 && *lazy_table.items.last().unwrap() == 1099);
275    }
276
277    #[test]
278    fn previous_outside_sources() {
279        let test_source = TestSourcer {
280            items: (0..2000_usize).collect(),
281        };
282        let mut lazy_table = LazyStatefulTable::new(Box::new(test_source));
283        lazy_table.state.select(Some(CAPACITY / 2 + ROOM + 1));
284        lazy_table.next();
285        lazy_table.state.select(Some(CAPACITY / 2 - ROOM - 1));
286        lazy_table.previous();
287        assert!(lazy_table.items[0] == 0 && *lazy_table.items.last().unwrap() == 999);
288    }
289}