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