1pub trait ListNavigation {
11 fn selected(&self) -> usize;
13
14 fn set_selected(&mut self, idx: usize);
16
17 fn total(&self) -> usize;
19
20 fn set_total(&mut self, total: usize);
22
23 fn select_next(&mut self) {
25 let total = self.total();
26 let selected = self.selected();
27 if total > 0 && selected < total.saturating_sub(1) {
28 self.set_selected(selected + 1);
29 }
30 }
31
32 fn select_prev(&mut self) {
34 let selected = self.selected();
35 if selected > 0 {
36 self.set_selected(selected - 1);
37 }
38 }
39
40 fn clamp_selection(&mut self) {
42 let total = self.total();
43 let selected = self.selected();
44 if total == 0 {
45 self.set_selected(0);
46 } else if selected >= total {
47 self.set_selected(total.saturating_sub(1));
48 }
49 }
50
51 fn page_up(&mut self) {
53 use super::constants::PAGE_SIZE;
54 let selected = self.selected();
55 self.set_selected(selected.saturating_sub(PAGE_SIZE));
56 }
57
58 fn page_down(&mut self) {
60 use super::constants::PAGE_SIZE;
61 let total = self.total();
62 let selected = self.selected();
63 if total > 0 {
64 self.set_selected((selected + PAGE_SIZE).min(total.saturating_sub(1)));
65 }
66 }
67
68 fn go_first(&mut self) {
70 self.set_selected(0);
71 }
72
73 fn go_last(&mut self) {
75 let total = self.total();
76 if total > 0 {
77 self.set_selected(total.saturating_sub(1));
78 }
79 }
80}
81
82#[derive(Debug, Clone, Default)]
87pub struct ListState {
88 pub selected: usize,
89 pub total: usize,
90 pub scroll_offset: usize,
91}
92
93impl ListState {
94 #[must_use]
95 pub fn new() -> Self {
96 Self::default()
97 }
98
99 #[must_use]
100 pub const fn with_total(total: usize) -> Self {
101 Self {
102 selected: 0,
103 total,
104 scroll_offset: 0,
105 }
106 }
107}
108
109impl ListNavigation for ListState {
110 fn selected(&self) -> usize {
111 self.selected
112 }
113
114 fn set_selected(&mut self, idx: usize) {
115 self.selected = idx;
116 }
117
118 fn total(&self) -> usize {
119 self.total
120 }
121
122 fn set_total(&mut self, total: usize) {
123 self.total = total;
124 }
125}
126
127pub trait TreeNavigation: ListNavigation {
132 fn is_expanded(&self, node_id: &str) -> bool;
134
135 fn expand(&mut self, node_id: &str);
137
138 fn collapse(&mut self, node_id: &str);
140
141 fn toggle_expand(&mut self, node_id: &str) {
143 if self.is_expanded(node_id) {
144 self.collapse(node_id);
145 } else {
146 self.expand(node_id);
147 }
148 }
149
150 fn expand_all(&mut self);
152
153 fn collapse_all(&mut self);
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn test_list_state_navigation() {
163 let mut state = ListState::with_total(10);
164
165 assert_eq!(state.selected(), 0);
166
167 state.select_next();
168 assert_eq!(state.selected(), 1);
169
170 state.select_prev();
171 assert_eq!(state.selected(), 0);
172
173 state.select_prev();
175 assert_eq!(state.selected(), 0);
176
177 state.go_last();
179 assert_eq!(state.selected(), 9);
180
181 state.select_next();
183 assert_eq!(state.selected(), 9);
184
185 state.go_first();
187 assert_eq!(state.selected(), 0);
188 }
189
190 #[test]
191 fn test_list_state_page_navigation() {
192 let mut state = ListState::with_total(50);
193
194 state.page_down();
195 assert_eq!(state.selected(), 10);
196
197 state.page_down();
198 assert_eq!(state.selected(), 20);
199
200 state.page_up();
201 assert_eq!(state.selected(), 10);
202
203 state.page_up();
204 assert_eq!(state.selected(), 0);
205
206 state.page_up();
208 assert_eq!(state.selected(), 0);
209 }
210
211 #[test]
212 fn test_list_state_clamp() {
213 let mut state = ListState::with_total(10);
214 state.selected = 15;
215
216 state.clamp_selection();
217 assert_eq!(state.selected(), 9);
218
219 state.set_total(0);
220 state.clamp_selection();
221 assert_eq!(state.selected(), 0);
222 }
223}