Skip to main content

rusticity_term/
table.rs

1use crate::common::PageSize;
2
3/// Generic table state for list-based services
4#[derive(Debug, Clone)]
5pub struct TableState<T> {
6    pub items: Vec<T>,
7    pub selected: usize,
8    pub loading: bool,
9    pub filter: String,
10    pub page_size: PageSize,
11    pub expanded_item: Option<usize>,
12    pub scroll_offset: usize,
13    pub next_token: Option<String>,
14}
15
16impl<T> Default for TableState<T> {
17    fn default() -> Self {
18        Self::new()
19    }
20}
21
22impl<T> TableState<T> {
23    pub fn new() -> Self {
24        Self {
25            items: Vec::new(),
26            selected: 0,
27            loading: false,
28            filter: String::new(),
29            page_size: PageSize::Fifty,
30            expanded_item: None,
31            scroll_offset: 0,
32            next_token: None,
33        }
34    }
35
36    pub fn filtered<F>(&self, predicate: F) -> Vec<&T>
37    where
38        F: Fn(&T) -> bool,
39    {
40        self.items.iter().filter(|item| predicate(item)).collect()
41    }
42
43    pub fn paginate<'a>(&self, filtered: &'a [&'a T]) -> &'a [&'a T] {
44        let page_size = self.page_size.value();
45        let end_idx = (self.scroll_offset + page_size).min(filtered.len());
46        &filtered[self.scroll_offset..end_idx]
47    }
48
49    pub fn current_page(&self, _total_items: usize) -> usize {
50        self.scroll_offset / self.page_size.value()
51    }
52
53    pub fn total_pages(&self, total_items: usize) -> usize {
54        total_items.div_ceil(self.page_size.value())
55    }
56
57    pub fn next_item(&mut self, max: usize) {
58        if max > 0 {
59            let new_selected = (self.selected + 1).min(max - 1);
60            if new_selected != self.selected {
61                self.selected = new_selected;
62
63                // Adjust scroll_offset if selection goes below viewport
64                let page_size = self.page_size.value();
65                if self.selected >= self.scroll_offset + page_size {
66                    self.scroll_offset = self.selected - page_size + 1;
67                }
68            }
69        }
70    }
71
72    pub fn prev_item(&mut self) {
73        if self.selected > 0 {
74            self.selected -= 1;
75
76            // Adjust scroll_offset if selection goes above viewport
77            if self.selected < self.scroll_offset {
78                self.scroll_offset = self.selected;
79            }
80        }
81    }
82
83    pub fn page_down(&mut self, max: usize) {
84        if max > 0 {
85            let page_size = self.page_size.value();
86            self.selected = (self.selected + 10).min(max - 1);
87
88            // Snap scroll_offset to page boundary
89            let current_page = self.selected / page_size;
90            self.scroll_offset = current_page * page_size;
91        }
92    }
93
94    pub fn page_up(&mut self) {
95        let page_size = self.page_size.value();
96        self.selected = self.selected.saturating_sub(10);
97
98        // Snap scroll_offset to page boundary
99        let current_page = self.selected / page_size;
100        self.scroll_offset = current_page * page_size;
101    }
102
103    pub fn snap_to_page(&mut self) {
104        let page_size = self.page_size.value();
105        let current_page = self.selected / page_size;
106        self.scroll_offset = current_page * page_size;
107    }
108
109    pub fn toggle_expand(&mut self) {
110        self.expanded_item = if self.expanded_item == Some(self.selected) {
111            None
112        } else {
113            Some(self.selected)
114        };
115    }
116
117    pub fn collapse(&mut self) {
118        self.expanded_item = None;
119    }
120
121    pub fn expand(&mut self) {
122        self.expanded_item = Some(self.selected);
123    }
124
125    pub fn is_expanded(&self) -> bool {
126        self.expanded_item == Some(self.selected)
127    }
128
129    pub fn has_expanded_item(&self) -> bool {
130        self.expanded_item.is_some()
131    }
132
133    pub fn goto_page(&mut self, page: usize, total_items: usize) {
134        let page_size = self.page_size.value();
135        let target = (page - 1) * page_size;
136        let max = total_items.saturating_sub(1);
137        self.selected = target.min(max);
138        self.scroll_offset = target.min(total_items.saturating_sub(page_size));
139    }
140
141    pub fn reset(&mut self) {
142        self.selected = 0;
143        self.scroll_offset = 0;
144        self.expanded_item = None;
145    }
146
147    pub fn get_selected<'a>(&self, filtered: &'a [&'a T]) -> Option<&'a T> {
148        filtered.get(self.selected).copied()
149    }
150
151    /// Push a character to the filter and reset selection
152    pub fn filter_push(&mut self, c: char) {
153        self.filter.push(c);
154        self.reset();
155    }
156
157    /// Pop a character from the filter and reset selection
158    pub fn filter_pop(&mut self) {
159        self.filter.pop();
160        self.reset();
161    }
162
163    /// Clear the filter and reset selection
164    pub fn filter_clear(&mut self) {
165        self.filter.clear();
166        self.reset();
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn test_table_state_default() {
176        let state: TableState<String> = TableState::new();
177        assert_eq!(state.selected, 0);
178        assert!(!state.loading);
179        assert_eq!(state.filter, "");
180        assert_eq!(state.page_size, PageSize::Fifty);
181        assert_eq!(state.expanded_item, None);
182    }
183
184    #[test]
185    fn test_filtered() {
186        let mut state = TableState::new();
187        state.items = vec![
188            "apple".to_string(),
189            "banana".to_string(),
190            "apricot".to_string(),
191        ];
192
193        let filtered = state.filtered(|item| item.starts_with('a'));
194        assert_eq!(filtered.len(), 2);
195    }
196
197    #[test]
198    fn test_paginate() {
199        let state = TableState::<String> {
200            page_size: PageSize::Ten,
201            selected: 0,
202            ..TableState::new()
203        };
204
205        let items: Vec<String> = (0..25).map(|i| i.to_string()).collect();
206        let refs: Vec<&String> = items.iter().collect();
207
208        let page = state.paginate(&refs);
209        assert_eq!(page.len(), 10);
210    }
211
212    #[test]
213    fn test_navigation() {
214        let mut state = TableState::<String>::new();
215
216        state.next_item(10);
217        assert_eq!(state.selected, 1);
218
219        state.prev_item();
220        assert_eq!(state.selected, 0);
221
222        state.page_down(100);
223        assert_eq!(state.selected, 10);
224
225        state.page_up();
226        assert_eq!(state.selected, 0);
227    }
228
229    #[test]
230    fn test_expand_toggle() {
231        let mut state = TableState::<String>::new();
232
233        assert!(!state.is_expanded());
234
235        state.toggle_expand();
236        assert!(state.is_expanded());
237
238        state.toggle_expand();
239        assert!(!state.is_expanded());
240
241        state.toggle_expand();
242        state.collapse();
243        assert!(!state.is_expanded());
244    }
245}