1use crate::common::PageSize;
2
3#[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 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 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 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 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 pub fn filter_push(&mut self, c: char) {
153 self.filter.push(c);
154 self.reset();
155 }
156
157 pub fn filter_pop(&mut self) {
159 self.filter.pop();
160 self.reset();
161 }
162
163 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}