retroglyph_widgets/state/
list.rs1#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
9pub enum SelectionWrap {
10 #[default]
13 Clamp,
14 Wrap,
17}
18
19#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
30pub struct ListState {
31 selected: Option<usize>,
32 offset: usize,
33 wrap: SelectionWrap,
34}
35
36impl ListState {
37 #[must_use]
39 pub const fn new() -> Self {
40 Self {
41 selected: None,
42 offset: 0,
43 wrap: SelectionWrap::Clamp,
44 }
45 }
46
47 #[must_use]
49 pub const fn selected(&self) -> Option<usize> {
50 self.selected
51 }
52
53 #[must_use]
55 pub const fn offset(&self) -> usize {
56 self.offset
57 }
58
59 #[must_use]
62 pub const fn wrap(&self) -> SelectionWrap {
63 self.wrap
64 }
65
66 pub const fn set_wrap(&mut self, wrap: SelectionWrap) {
68 self.wrap = wrap;
69 }
70
71 pub const fn select(&mut self, index: Option<usize>) {
73 self.selected = index;
74 }
75
76 pub const fn set_offset(&mut self, offset: usize) {
78 self.offset = offset;
79 }
80
81 pub const fn reset(&mut self) {
84 self.selected = None;
85 self.offset = 0;
86 }
87
88 pub const fn ensure_visible(&mut self, visible_height: usize) {
98 let Some(selected) = self.selected else {
99 return;
100 };
101 if visible_height == 0 {
102 return;
103 }
104 if selected < self.offset {
105 self.offset = selected;
106 } else if selected >= self.offset + visible_height {
107 self.offset = selected + 1 - visible_height;
108 }
109 }
110
111 pub fn scroll_by(&mut self, delta: i32) {
115 let next = i64::from(delta).saturating_add(i64::try_from(self.offset).unwrap_or(i64::MAX));
116 self.offset = next.max(0).try_into().unwrap_or(usize::MAX);
117 }
118
119 pub fn select_next(&mut self, len: usize) {
123 self.selected = Self::stepped(self.selected, 1, len, self.wrap);
124 }
125
126 pub fn select_previous(&mut self, len: usize) {
130 self.selected = Self::stepped(self.selected, -1, len, self.wrap);
131 }
132
133 pub fn select_first(&mut self, len: usize) {
135 self.selected = (len > 0).then_some(0);
136 }
137
138 pub fn select_last(&mut self, len: usize) {
140 self.selected = (len > 0).then(|| len - 1);
141 }
142
143 fn stepped(
148 current: Option<usize>,
149 delta: i32,
150 len: usize,
151 mode: SelectionWrap,
152 ) -> Option<usize> {
153 if len == 0 {
154 return None;
155 }
156 let Some(i) = current else {
157 return Some(if delta > 0 { 0 } else { len - 1 });
158 };
159 let Ok(len) = i32::try_from(len) else {
160 return current; };
162 let next = i32::try_from(i).unwrap_or(0) + delta;
163 let idx = match mode {
164 SelectionWrap::Wrap => next.rem_euclid(len),
165 SelectionWrap::Clamp => next.clamp(0, len - 1),
166 };
167 usize::try_from(idx).ok()
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn starts_empty() {
177 let s = ListState::new();
178 assert_eq!(s.selected(), None);
179 assert_eq!(s.offset(), 0);
180 }
181
182 #[test]
183 fn next_from_none_selects_first() {
184 let mut s = ListState::new();
185 s.select_next(3);
186 assert_eq!(s.selected(), Some(0));
187 }
188
189 #[test]
190 fn previous_from_none_selects_last() {
191 let mut s = ListState::new();
192 s.select_previous(3);
193 assert_eq!(s.selected(), Some(2));
194 }
195
196 #[test]
197 fn next_clamps_at_the_end_by_default() {
198 let mut s = ListState::new();
199 assert_eq!(s.wrap(), SelectionWrap::Clamp);
200 s.select(Some(2));
201 s.select_next(3);
202 assert_eq!(s.selected(), Some(2)); }
204
205 #[test]
206 fn previous_clamps_at_the_start_by_default() {
207 let mut s = ListState::new();
208 s.select(Some(0));
209 s.select_previous(3);
210 assert_eq!(s.selected(), Some(0)); }
212
213 #[test]
214 fn next_wraps_past_the_end_when_wrap_is_set() {
215 let mut s = ListState::new();
216 s.set_wrap(SelectionWrap::Wrap);
217 s.select(Some(2));
218 s.select_next(3);
219 assert_eq!(s.selected(), Some(0));
220 }
221
222 #[test]
223 fn previous_wraps_past_the_start_when_wrap_is_set() {
224 let mut s = ListState::new();
225 s.set_wrap(SelectionWrap::Wrap);
226 s.select(Some(0));
227 s.select_previous(3);
228 assert_eq!(s.selected(), Some(2));
229 }
230
231 #[test]
232 fn zero_length_clears_selection() {
233 let mut s = ListState::new();
234 s.select(Some(0));
235 s.select_next(0);
236 assert_eq!(s.selected(), None);
237 s.select(Some(0));
238 s.select_previous(0);
239 assert_eq!(s.selected(), None);
240 }
241
242 #[test]
243 fn select_first_and_last() {
244 let mut s = ListState::new();
245 s.select_last(5);
246 assert_eq!(s.selected(), Some(4));
247 s.select_first(5);
248 assert_eq!(s.selected(), Some(0));
249 s.select_first(0);
250 assert_eq!(s.selected(), None);
251 }
252
253 #[test]
254 fn ensure_visible_is_a_no_op_when_already_in_view() {
255 let mut s = ListState::new();
256 s.select(Some(3));
257 s.set_offset(2);
258 s.ensure_visible(5); assert_eq!(s.offset(), 2);
260 }
261
262 #[test]
263 fn ensure_visible_scrolls_down_to_reveal_a_later_selection() {
264 let mut s = ListState::new();
265 s.select(Some(10));
266 s.set_offset(0);
267 s.ensure_visible(4); assert_eq!(s.offset(), 7); assert!(s.offset() <= 10 && 10 < s.offset() + 4);
270 }
271
272 #[test]
273 fn ensure_visible_scrolls_up_to_reveal_an_earlier_selection() {
274 let mut s = ListState::new();
275 s.select(Some(1));
276 s.set_offset(5);
277 s.ensure_visible(3); assert_eq!(s.offset(), 1);
279 }
280
281 #[test]
282 fn ensure_visible_is_a_no_op_with_nothing_selected_or_zero_height() {
283 let mut s = ListState::new();
284 s.set_offset(5);
285 s.ensure_visible(10); assert_eq!(s.offset(), 5);
287
288 s.select(Some(20));
289 s.ensure_visible(0); assert_eq!(s.offset(), 5);
291 }
292
293 #[test]
294 fn reset_clears_selection_and_offset() {
295 let mut s = ListState::new();
296 s.select(Some(2));
297 s.set_offset(5);
298 s.reset();
299 assert_eq!(s.selected(), None);
300 assert_eq!(s.offset(), 0);
301 }
302
303 #[test]
304 fn scroll_by_clamps_at_zero() {
305 let mut s = ListState::new();
306 s.scroll_by(-5);
307 assert_eq!(s.offset(), 0);
308 s.scroll_by(3);
309 assert_eq!(s.offset(), 3);
310 s.scroll_by(-1);
311 assert_eq!(s.offset(), 2);
312 }
313}