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}
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 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 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 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 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}