1use core::ops::Range;
10
11#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
13pub enum Scroll {
14 #[default]
17 KeepInView,
18 Centred,
21}
22
23#[derive(Debug, Clone, Default, PartialEq, Eq)]
30pub struct ListCursor {
31 len: usize,
32 focus: usize,
33 first: usize,
35 scroll: Scroll,
36}
37
38impl ListCursor {
39 pub fn new(len: usize) -> Self {
41 Self {
42 len,
43 ..Self::default()
44 }
45 }
46
47 #[must_use]
49 pub fn scrolling(mut self, scroll: Scroll) -> Self {
50 self.scroll = scroll;
51 self
52 }
53
54 #[must_use]
57 pub fn at(mut self, index: usize) -> Self {
58 self.focus = index.min(self.len.saturating_sub(1));
59 self
60 }
61
62 pub fn focus(&self) -> usize {
64 self.focus
65 }
66
67 pub fn len(&self) -> usize {
69 self.len
70 }
71
72 pub fn is_empty(&self) -> bool {
74 self.len == 0
75 }
76
77 pub fn scroll(&self) -> Scroll {
79 self.scroll
80 }
81
82 pub fn first_shown(&self) -> usize {
85 self.first
86 }
87
88 pub fn step(&mut self, forward: bool, wrap: bool, enabled: impl Fn(usize) -> bool) -> bool {
93 let (n, at) = (self.len, self.focus);
94 let next = if wrap {
95 (1..n)
96 .map(|k| {
97 if forward {
98 (at + k) % n
99 } else {
100 (at + n - k) % n
101 }
102 })
103 .find(|&i| enabled(i))
104 } else if forward {
105 (at + 1..n).find(|&i| enabled(i))
106 } else {
107 (0..at.min(n)).rev().find(|&i| enabled(i))
108 };
109 match next {
110 Some(i) => {
111 self.focus = i;
112 true
113 }
114 None => false,
115 }
116 }
117
118 pub fn settle(&mut self, enabled: impl Fn(usize) -> bool) -> bool {
122 if self.is_empty() || enabled(self.focus) {
123 return false;
124 }
125 self.step(true, false, &enabled) || self.step(false, false, &enabled)
126 }
127
128 pub fn set_len(&mut self, len: usize) {
132 self.len = len;
133 self.focus = self.focus.min(len.saturating_sub(1));
134 }
135
136 pub fn window(&mut self, rows_shown: usize) -> Range<usize> {
141 if self.is_empty() {
142 self.first = 0;
143 return 0..0;
144 }
145 let rows = rows_shown.max(1).min(self.len);
146 let last_first = self.len - rows;
147 let first = match self.scroll {
148 Scroll::KeepInView => self
151 .first
152 .clamp((self.focus + 1).saturating_sub(rows), self.focus),
153 Scroll::Centred => self.focus.saturating_sub((rows - 1) / 2),
154 };
155 self.first = first.min(last_first);
156 self.first..self.first + rows
157 }
158
159 pub fn window_by_heights(&mut self, heights: &[usize], room: usize) -> Range<usize> {
165 if self.is_empty() {
166 self.first = 0;
167 return 0..0;
168 }
169 let (focus, len) = (self.focus, self.len);
170 let height = |r: Range<usize>| -> usize { heights[r].iter().sum() };
173 let first = match self.scroll {
174 Scroll::KeepInView => self.first.min(focus),
175 Scroll::Centred => {
177 let half = room.saturating_sub(heights[focus]) / 2;
178 (0..focus)
179 .find(|&f| height(f..focus) <= half)
180 .unwrap_or(focus)
181 }
182 };
183 let first = (first..focus)
185 .find(|&f| height(f..focus + 1) <= room)
186 .unwrap_or(focus);
187 let first = (0..first)
189 .find(|&f| height(f..len) <= room)
190 .unwrap_or(first);
191 let end = (focus + 1..len)
193 .find(|&e| height(first..e + 1) > room)
194 .unwrap_or(len);
195 self.first = first;
196 first..end
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use proptest::prelude::*;
204
205 fn all(_: usize) -> bool {
206 true
207 }
208
209 #[test]
210 fn stepping_wraps_or_stops_at_the_ends() {
211 let mut c = ListCursor::new(3);
212 assert!(c.step(false, true, all));
213 assert_eq!(c.focus(), 2, "wrapped to the end");
214 assert!(c.step(true, true, all));
215 assert_eq!(c.focus(), 0, "wrapped to the start");
216 assert!(!c.step(false, false, all), "stops at the start");
217 assert_eq!(c.focus(), 0);
218 let mut c = c.at(2);
219 assert!(!c.step(true, false, all), "stops at the end");
220 assert_eq!(c.focus(), 2);
221 assert!(c.step(false, false, all));
222 assert_eq!(c.focus(), 1);
223 }
224
225 #[test]
226 fn disabled_rows_are_skipped() {
227 let on = [true, false, false, true, false];
228 let enabled = |i: usize| on[i];
229 let mut c = ListCursor::new(5);
230 assert!(c.step(true, true, enabled));
231 assert_eq!(c.focus(), 3);
232 assert!(c.step(true, true, enabled));
233 assert_eq!(c.focus(), 0);
234 let mut c = c.at(3);
236 assert!(!c.step(true, false, enabled));
237 assert_eq!(c.focus(), 3);
238 }
239
240 #[test]
241 fn every_row_disabled_but_one_keeps_the_focus() {
242 let enabled = |i: usize| i == 2;
243 let mut c = ListCursor::new(5).at(2);
244 for (forward, wrap) in [(true, true), (false, true), (true, false), (false, false)] {
245 assert!(!c.step(forward, wrap, enabled));
246 assert_eq!(c.focus(), 2);
247 }
248 let mut c = ListCursor::new(5).at(4);
250 assert!(c.step(true, true, enabled));
251 assert_eq!(c.focus(), 2);
252 }
253
254 #[test]
255 fn an_empty_list_never_moves() {
256 let mut c = ListCursor::new(0);
257 assert!(!c.step(true, true, all));
258 assert!(!c.step(false, false, all));
259 assert!(!c.settle(all));
260 assert_eq!(c.focus(), 0);
261 assert_eq!(c.window(4), 0..0);
262 assert_eq!(c.window_by_heights(&[], 4), 0..0);
263 assert!(c.is_empty(), "{c:?}");
264 }
265
266 #[test]
267 fn a_removal_at_the_end_clamps_the_focus() {
268 let mut c = ListCursor::new(6).at(5);
269 assert_eq!(c.window(3), 3..6);
270 c.set_len(4);
271 assert_eq!((c.focus(), c.len()), (3, 4));
272 assert_eq!(c.window(3), 1..4);
273 c.set_len(0);
274 assert_eq!(c.focus(), 0);
275 assert_eq!(c.window(3), 0..0);
276 }
277
278 #[test]
279 fn settle_leaves_a_disabled_row() {
280 let on = [true, false, true, false];
281 let mut c = ListCursor::new(4).at(1);
282 assert!(c.settle(|i| on[i]));
283 assert_eq!(c.focus(), 2, "the next enabled row first");
284 let mut c = c.at(3);
285 assert!(c.settle(|i| on[i]));
286 assert_eq!(c.focus(), 2, "else the one before");
287 assert!(!c.settle(|i| on[i]), "already on one");
288 assert!(!c.settle(|_| false), "nowhere to go");
289 }
290
291 #[test]
292 fn keep_in_view_moves_the_window_by_one_at_its_edge() {
293 let mut c = ListCursor::new(10);
294 assert_eq!(c.window(4), 0..4);
295 for _ in 0..3 {
296 c.step(true, true, all);
297 assert_eq!(c.window(4), 0..4, "inside: the window stays");
298 }
299 c.step(true, true, all);
300 assert_eq!(c.window(4), 1..5, "past the bottom edge: by one");
301 c.step(true, true, all);
302 assert_eq!(c.window(4), 2..6);
303 for _ in 0..3 {
305 c.step(false, true, all);
306 assert_eq!(c.window(4), 2..6);
307 }
308 c.step(false, true, all);
309 assert_eq!((c.focus(), c.window(4)), (1, 1..5));
310 assert_eq!(c.first_shown(), 1);
311 c.step(false, true, all);
313 c.step(false, true, all);
314 assert_eq!((c.focus(), c.window(4)), (9, 6..10));
315 }
316
317 #[test]
318 fn centred_keeps_the_focus_in_the_middle() {
319 let mut c = ListCursor::new(20).scrolling(Scroll::Centred);
320 assert_eq!(c.scroll(), Scroll::Centred);
321 assert_eq!(c.window(5), 0..5, "the top stops at the first row");
322 let mut c = c.at(10);
323 assert_eq!(c.window(5), 8..13);
324 c.step(true, true, all);
325 assert_eq!(c.window(5), 9..14);
326 let mut c = c.at(19);
327 assert_eq!(c.window(5), 15..20, "the bottom stops at the last row");
328 let mut c = c.at(10);
330 assert_eq!(c.window(6), 8..14);
331 }
332
333 #[test]
334 fn a_short_list_shows_every_row() {
335 let mut c = ListCursor::new(3).at(2);
336 assert_eq!(c.window(6), 0..3);
337 assert_eq!(c.window(0), 2..3, "a box of no rows still shows the focus");
338 }
339
340 #[test]
341 fn rows_of_different_heights() {
342 let heights = [1, 3, 2, 1, 4, 1];
343 let mut c = ListCursor::new(6);
344 assert_eq!(c.window_by_heights(&heights, 6), 0..3);
345 c = c.at(3);
346 assert_eq!(c.window_by_heights(&heights, 6), 1..4, "drops the top row");
347 c = c.at(4);
348 assert_eq!(c.window_by_heights(&heights, 6), 3..6);
349 c = c.at(3);
351 assert_eq!(c.window_by_heights(&heights, 6), 3..6);
352 c = c.at(2);
353 assert_eq!(c.window_by_heights(&heights, 6), 2..4);
354 c = c.at(4);
356 assert_eq!(c.window_by_heights(&heights, 3), 4..5);
357 assert_eq!(c.window_by_heights(&heights, 20), 0..6);
359 let mut c = ListCursor::new(3).at(2);
361 assert_eq!(c.window_by_heights(&[1, 1, 1], 1), 2..3);
362 assert_eq!(c.window_by_heights(&[1, 1, 1], 3), 0..3);
363 }
364
365 #[test]
366 fn rows_of_different_heights_centred() {
367 let heights = [1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1];
368 let mut c = ListCursor::new(11).scrolling(Scroll::Centred).at(5);
369 assert_eq!(c.window_by_heights(&heights, 7), 3..8);
371 let mut c = c.at(0);
372 assert_eq!(c.window_by_heights(&heights, 7), 0..5);
373 }
374
375 proptest! {
376 #[test]
377 fn the_focus_is_in_the_window_and_on_an_enabled_row(
378 on in prop::collection::vec(any::<bool>(), 0..30),
379 heights in prop::collection::vec(1usize..5, 30),
380 moves in prop::collection::vec((any::<bool>(), any::<bool>()), 0..40),
381 rows in 0usize..10,
382 room in 0usize..15,
383 centred in any::<bool>(),
384 removed in 0usize..30,
385 ) {
386 let enabled = |i: usize| on[i];
387 let scroll = if centred { Scroll::Centred } else { Scroll::KeepInView };
388 let mut c = ListCursor::new(on.len()).scrolling(scroll);
389 c.settle(enabled);
390 let check = |c: &mut ListCursor| -> Result<(), TestCaseError> {
391 if c.is_empty() {
392 return Ok(());
393 }
394 prop_assert!(c.focus() < c.len());
395 let w = c.window(rows);
396 prop_assert!(w.contains(&c.focus()));
397 prop_assert!(w.len() <= rows.max(1));
398 let w = c.window_by_heights(&heights[..c.len()], room);
399 prop_assert!(w.contains(&c.focus()));
400 if on[..c.len()].iter().any(|&e| e) {
401 prop_assert!(on[c.focus()]);
402 }
403 Ok(())
404 };
405 check(&mut c)?;
406 for (forward, wrap) in moves {
407 c.step(forward, wrap, enabled);
408 check(&mut c)?;
409 }
410 c.set_len(on.len().saturating_sub(removed));
411 c.settle(enabled);
412 check(&mut c)?;
413 }
414 }
415}