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 let selected = self.selected();
54 self.set_selected(selected.saturating_sub(10));
55 }
56
57 fn page_down(&mut self) {
59 let total = self.total();
60 let selected = self.selected();
61 if total > 0 {
62 self.set_selected((selected + 10).min(total.saturating_sub(1)));
63 }
64 }
65
66 fn go_first(&mut self) {
68 self.set_selected(0);
69 }
70
71 fn go_last(&mut self) {
73 let total = self.total();
74 if total > 0 {
75 self.set_selected(total.saturating_sub(1));
76 }
77 }
78}
79
80#[derive(Debug, Clone, Default)]
85pub struct ListState {
86 pub selected: usize,
87 pub total: usize,
88 pub scroll_offset: usize,
89}
90
91impl ListState {
92 pub fn new() -> Self {
93 Self::default()
94 }
95
96 pub fn with_total(total: usize) -> Self {
97 Self {
98 selected: 0,
99 total,
100 scroll_offset: 0,
101 }
102 }
103}
104
105impl ListNavigation for ListState {
106 fn selected(&self) -> usize {
107 self.selected
108 }
109
110 fn set_selected(&mut self, idx: usize) {
111 self.selected = idx;
112 }
113
114 fn total(&self) -> usize {
115 self.total
116 }
117
118 fn set_total(&mut self, total: usize) {
119 self.total = total;
120 }
121}
122
123pub trait TreeNavigation: ListNavigation {
128 fn is_expanded(&self, node_id: &str) -> bool;
130
131 fn expand(&mut self, node_id: &str);
133
134 fn collapse(&mut self, node_id: &str);
136
137 fn toggle_expand(&mut self, node_id: &str) {
139 if self.is_expanded(node_id) {
140 self.collapse(node_id);
141 } else {
142 self.expand(node_id);
143 }
144 }
145
146 fn expand_all(&mut self);
148
149 fn collapse_all(&mut self);
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn test_list_state_navigation() {
159 let mut state = ListState::with_total(10);
160
161 assert_eq!(state.selected(), 0);
162
163 state.select_next();
164 assert_eq!(state.selected(), 1);
165
166 state.select_prev();
167 assert_eq!(state.selected(), 0);
168
169 state.select_prev();
171 assert_eq!(state.selected(), 0);
172
173 state.go_last();
175 assert_eq!(state.selected(), 9);
176
177 state.select_next();
179 assert_eq!(state.selected(), 9);
180
181 state.go_first();
183 assert_eq!(state.selected(), 0);
184 }
185
186 #[test]
187 fn test_list_state_page_navigation() {
188 let mut state = ListState::with_total(50);
189
190 state.page_down();
191 assert_eq!(state.selected(), 10);
192
193 state.page_down();
194 assert_eq!(state.selected(), 20);
195
196 state.page_up();
197 assert_eq!(state.selected(), 10);
198
199 state.page_up();
200 assert_eq!(state.selected(), 0);
201
202 state.page_up();
204 assert_eq!(state.selected(), 0);
205 }
206
207 #[test]
208 fn test_list_state_clamp() {
209 let mut state = ListState::with_total(10);
210 state.selected = 15;
211
212 state.clamp_selection();
213 assert_eq!(state.selected(), 9);
214
215 state.set_total(0);
216 state.clamp_selection();
217 assert_eq!(state.selected(), 0);
218 }
219}