1#[derive(Debug, Clone, Copy, Default)]
9pub struct ScrollState {
10 offset: usize,
12 total_rows: usize,
14 visible_rows: usize,
16 selected: Option<usize>,
18}
19
20impl ScrollState {
21 pub fn new(total_rows: usize, visible_rows: usize) -> Self {
27 Self {
28 offset: 0,
29 total_rows,
30 visible_rows,
31 selected: None,
32 }
33 }
34
35 #[inline]
37 pub fn offset(&self) -> usize {
38 self.offset
39 }
40
41 pub fn set_offset(&mut self, offset: usize) {
43 self.offset = self.clamp_offset(offset);
44 }
45
46 #[inline]
48 pub fn total_rows(&self) -> usize {
49 self.total_rows
50 }
51
52 pub fn set_total_rows(&mut self, total: usize) {
54 self.total_rows = total;
55 self.offset = self.clamp_offset(self.offset);
57 if let Some(sel) = self.selected {
59 if sel >= total {
60 self.selected = if total > 0 { Some(total - 1) } else { None };
61 }
62 }
63 }
64
65 #[inline]
67 pub fn visible_rows(&self) -> usize {
68 self.visible_rows
69 }
70
71 pub fn set_visible_rows(&mut self, visible: usize) {
73 self.visible_rows = visible;
74 self.offset = self.clamp_offset(self.offset);
76 }
77
78 #[inline]
80 pub fn selected(&self) -> Option<usize> {
81 self.selected
82 }
83
84 pub fn set_selected(&mut self, row: Option<usize>) {
86 self.selected = match row {
87 Some(r) if r >= self.total_rows => {
88 if self.total_rows > 0 {
89 Some(self.total_rows - 1)
90 } else {
91 None
92 }
93 }
94 other => other,
95 };
96
97 if let Some(sel) = self.selected {
99 self.ensure_visible(sel);
100 }
101 }
102
103 pub fn select_next(&mut self) {
105 let new_sel = match self.selected {
106 Some(sel) => {
107 if sel + 1 < self.total_rows {
108 Some(sel + 1)
109 } else {
110 Some(sel)
111 }
112 }
113 None if self.total_rows > 0 => Some(0),
114 None => None,
115 };
116 self.set_selected(new_sel);
117 }
118
119 pub fn select_prev(&mut self) {
121 let new_sel = match self.selected {
122 Some(sel) if sel > 0 => Some(sel - 1),
123 Some(sel) => Some(sel),
124 None if self.total_rows > 0 => Some(0),
125 None => None,
126 };
127 self.set_selected(new_sel);
128 }
129
130 pub fn scroll_down(&mut self) {
132 let max_offset = self.max_offset();
133 if self.offset < max_offset {
134 self.offset += 1;
135 }
136 }
137
138 pub fn scroll_up(&mut self) {
140 self.offset = self.offset.saturating_sub(1);
141 }
142
143 pub fn page_down(&mut self) {
145 let page_size = self.visible_rows.max(1);
146 let new_offset = self.offset.saturating_add(page_size);
147 self.offset = self.clamp_offset(new_offset);
148 }
149
150 pub fn page_up(&mut self) {
152 let page_size = self.visible_rows.max(1);
153 self.offset = self.offset.saturating_sub(page_size);
154 }
155
156 pub fn home(&mut self) {
158 self.offset = 0;
159 }
160
161 pub fn end(&mut self) {
163 self.offset = self.max_offset();
164 }
165
166 pub fn ensure_visible(&mut self, row: usize) {
168 if row < self.offset {
169 self.offset = row;
171 } else if row >= self.offset + self.visible_rows {
172 self.offset = row.saturating_sub(self.visible_rows.saturating_sub(1));
174 }
175 self.offset = self.clamp_offset(self.offset);
177 }
178
179 pub fn needs_scrollbar(&self) -> bool {
181 self.total_rows > self.visible_rows
182 }
183
184 #[allow(clippy::cast_precision_loss)]
186 pub fn scrollbar_position(&self) -> f32 {
187 if self.total_rows <= self.visible_rows {
188 return 0.0;
189 }
190 let max = self.max_offset();
191 if max == 0 {
192 return 0.0;
193 }
194 self.offset as f32 / max as f32
195 }
196
197 #[allow(clippy::cast_precision_loss)]
199 pub fn scrollbar_size(&self) -> f32 {
200 if self.total_rows == 0 {
201 return 1.0;
202 }
203 (self.visible_rows as f32 / self.total_rows as f32).min(1.0)
204 }
205
206 fn max_offset(&self) -> usize {
208 self.total_rows.saturating_sub(self.visible_rows)
209 }
210
211 fn clamp_offset(&self, offset: usize) -> usize {
213 offset.min(self.max_offset())
214 }
215
216 pub fn is_visible(&self, row: usize) -> bool {
218 row >= self.offset && row < self.offset + self.visible_rows
219 }
220
221 pub fn to_viewport_row(&self, global_row: usize) -> Option<usize> {
223 if self.is_visible(global_row) {
224 Some(global_row - self.offset)
225 } else {
226 None
227 }
228 }
229
230 pub fn to_global_row(&self, viewport_row: usize) -> usize {
232 self.offset + viewport_row
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239
240 #[test]
241 fn f_scroll_new() {
242 let scroll = ScrollState::new(100, 20);
243 assert_eq!(scroll.offset(), 0);
244 assert_eq!(scroll.total_rows(), 100);
245 assert_eq!(scroll.visible_rows(), 20);
246 }
247
248 #[test]
249 fn f_scroll_down() {
250 let mut scroll = ScrollState::new(100, 20);
251 scroll.scroll_down();
252 assert_eq!(scroll.offset(), 1);
253 }
254
255 #[test]
256 fn f_scroll_up() {
257 let mut scroll = ScrollState::new(100, 20);
258 scroll.set_offset(10);
259 scroll.scroll_up();
260 assert_eq!(scroll.offset(), 9);
261 }
262
263 #[test]
264 fn f_scroll_up_at_zero() {
265 let mut scroll = ScrollState::new(100, 20);
266 scroll.scroll_up();
267 assert_eq!(scroll.offset(), 0, "FALSIFIED: Should not go negative");
268 }
269
270 #[test]
271 fn f_scroll_down_at_max() {
272 let mut scroll = ScrollState::new(100, 20);
273 scroll.set_offset(80); scroll.scroll_down();
275 assert_eq!(scroll.offset(), 80, "FALSIFIED: Should not exceed max");
276 }
277
278 #[test]
279 fn f_scroll_page_down() {
280 let mut scroll = ScrollState::new(100, 20);
281 scroll.page_down();
282 assert_eq!(scroll.offset(), 20);
283 }
284
285 #[test]
286 fn f_scroll_page_up() {
287 let mut scroll = ScrollState::new(100, 20);
288 scroll.set_offset(50);
289 scroll.page_up();
290 assert_eq!(scroll.offset(), 30);
291 }
292
293 #[test]
294 fn f_scroll_home() {
295 let mut scroll = ScrollState::new(100, 20);
296 scroll.set_offset(50);
297 scroll.home();
298 assert_eq!(scroll.offset(), 0);
299 }
300
301 #[test]
302 fn f_scroll_end() {
303 let mut scroll = ScrollState::new(100, 20);
304 scroll.end();
305 assert_eq!(scroll.offset(), 80);
306 }
307
308 #[test]
309 fn f_scroll_ensure_visible_above() {
310 let mut scroll = ScrollState::new(100, 20);
311 scroll.set_offset(50);
312 scroll.ensure_visible(30);
313 assert_eq!(scroll.offset(), 30);
314 }
315
316 #[test]
317 fn f_scroll_ensure_visible_below() {
318 let mut scroll = ScrollState::new(100, 20);
319 scroll.set_offset(0);
320 scroll.ensure_visible(30);
321 assert!(scroll.offset() + scroll.visible_rows() > 30);
322 }
323
324 #[test]
325 fn f_scroll_ensure_visible_already() {
326 let mut scroll = ScrollState::new(100, 20);
327 scroll.set_offset(10);
328 scroll.ensure_visible(15);
329 assert_eq!(
330 scroll.offset(),
331 10,
332 "FALSIFIED: Should not change if visible"
333 );
334 }
335
336 #[test]
337 fn f_scroll_needs_scrollbar_yes() {
338 let scroll = ScrollState::new(100, 20);
339 assert!(scroll.needs_scrollbar());
340 }
341
342 #[test]
343 fn f_scroll_needs_scrollbar_no() {
344 let scroll = ScrollState::new(10, 20);
345 assert!(!scroll.needs_scrollbar());
346 }
347
348 #[test]
349 fn f_scroll_scrollbar_position() {
350 let mut scroll = ScrollState::new(100, 20);
351 scroll.set_offset(40);
352 let pos = scroll.scrollbar_position();
353 assert!(pos > 0.4 && pos < 0.6);
354 }
355
356 #[test]
357 fn f_scroll_scrollbar_size() {
358 let scroll = ScrollState::new(100, 20);
359 let size = scroll.scrollbar_size();
360 assert!((size - 0.2).abs() < 0.01);
361 }
362
363 #[test]
364 fn f_scroll_is_visible() {
365 let mut scroll = ScrollState::new(100, 20);
366 scroll.set_offset(10);
367 assert!(scroll.is_visible(15));
368 assert!(!scroll.is_visible(5));
369 assert!(!scroll.is_visible(35));
370 }
371
372 #[test]
373 fn f_scroll_to_viewport_row() {
374 let mut scroll = ScrollState::new(100, 20);
375 scroll.set_offset(10);
376 assert_eq!(scroll.to_viewport_row(15), Some(5));
377 assert_eq!(scroll.to_viewport_row(5), None);
378 }
379
380 #[test]
381 fn f_scroll_to_global_row() {
382 let mut scroll = ScrollState::new(100, 20);
383 scroll.set_offset(10);
384 assert_eq!(scroll.to_global_row(5), 15);
385 }
386
387 #[test]
388 fn f_scroll_select_next() {
389 let mut scroll = ScrollState::new(100, 20);
390 scroll.set_selected(Some(0));
391 scroll.select_next();
392 assert_eq!(scroll.selected(), Some(1));
393 }
394
395 #[test]
396 fn f_scroll_select_prev() {
397 let mut scroll = ScrollState::new(100, 20);
398 scroll.set_selected(Some(5));
399 scroll.select_prev();
400 assert_eq!(scroll.selected(), Some(4));
401 }
402
403 #[test]
404 fn f_scroll_select_prev_at_zero() {
405 let mut scroll = ScrollState::new(100, 20);
406 scroll.set_selected(Some(0));
407 scroll.select_prev();
408 assert_eq!(scroll.selected(), Some(0));
409 }
410
411 #[test]
412 fn f_scroll_select_next_at_end() {
413 let mut scroll = ScrollState::new(100, 20);
414 scroll.set_selected(Some(99));
415 scroll.select_next();
416 assert_eq!(scroll.selected(), Some(99));
417 }
418
419 #[test]
420 fn f_scroll_select_from_none() {
421 let mut scroll = ScrollState::new(100, 20);
422 scroll.select_next();
423 assert_eq!(scroll.selected(), Some(0));
424 }
425
426 #[test]
427 fn f_scroll_empty_dataset() {
428 let scroll = ScrollState::new(0, 20);
429 assert_eq!(scroll.offset(), 0);
430 assert!(!scroll.needs_scrollbar());
431 }
432
433 #[test]
434 fn f_scroll_set_total_rows_shrink() {
435 let mut scroll = ScrollState::new(100, 20);
436 scroll.set_offset(80);
437 scroll.set_selected(Some(90));
438 scroll.set_total_rows(50);
439 assert!(scroll.offset() <= 30);
440 assert!(scroll.selected().unwrap_or(0) < 50);
441 }
442
443 #[test]
444 fn f_scroll_default() {
445 let scroll = ScrollState::default();
446 assert_eq!(scroll.offset(), 0);
447 assert_eq!(scroll.total_rows(), 0);
448 assert_eq!(scroll.visible_rows(), 0);
449 }
450
451 #[test]
452 fn f_scroll_clone() {
453 let mut scroll = ScrollState::new(100, 20);
454 scroll.set_offset(50);
455 let cloned = scroll;
456 assert_eq!(scroll.offset(), cloned.offset());
457 }
458
459 #[test]
460 fn f_scroll_set_total_rows_shrink_to_zero() {
461 let mut scroll = ScrollState::new(100, 20);
463 scroll.set_selected(Some(50));
464 scroll.set_total_rows(0);
465 assert_eq!(scroll.selected(), None);
466 }
467
468 #[test]
469 fn f_scroll_set_selected_out_of_bounds_empty() {
470 let mut scroll = ScrollState::new(0, 20);
472 scroll.set_selected(Some(100));
473 assert_eq!(scroll.selected(), None);
475 }
476
477 #[test]
478 fn f_scroll_set_selected_out_of_bounds_clamps() {
479 let mut scroll = ScrollState::new(50, 20);
481 scroll.set_selected(Some(100));
482 assert_eq!(scroll.selected(), Some(49));
484 }
485
486 #[test]
487 fn f_scroll_set_total_rows_shrink_selection_out_of_bounds() {
488 let mut scroll = ScrollState::new(100, 20);
490 scroll.set_selected(Some(90));
491 scroll.set_total_rows(50);
492 assert_eq!(scroll.selected(), Some(49));
494 }
495
496 #[test]
497 fn f_scroll_page_up_at_zero() {
498 let mut scroll = ScrollState::new(100, 20);
499 scroll.page_up();
500 assert_eq!(scroll.offset(), 0);
501 }
502
503 #[test]
504 fn f_scroll_select_prev_from_none() {
505 let mut scroll = ScrollState::new(100, 20);
506 scroll.select_prev();
508 assert_eq!(scroll.selected(), Some(0));
509 }
510
511 #[test]
512 fn f_scroll_select_next_empty() {
513 let mut scroll = ScrollState::new(0, 20);
515 scroll.select_next();
516 assert_eq!(scroll.selected(), None);
518 }
519
520 #[test]
521 fn f_scroll_select_prev_empty() {
522 let mut scroll = ScrollState::new(0, 20);
524 scroll.select_prev();
525 assert_eq!(scroll.selected(), None);
527 }
528
529 #[test]
530 fn f_scroll_scrollbar_position_small_content() {
531 let scroll = ScrollState::new(10, 20);
533 let pos = scroll.scrollbar_position();
534 assert!((pos - 0.0).abs() < 0.001);
535 }
536
537 #[test]
538 fn f_scroll_scrollbar_position_max_zero() {
539 let scroll = ScrollState::new(20, 20);
541 let pos = scroll.scrollbar_position();
542 assert!((pos - 0.0).abs() < 0.001);
543 }
544
545 #[test]
546 fn f_scroll_scrollbar_size_empty() {
547 let scroll = ScrollState::new(0, 20);
549 let size = scroll.scrollbar_size();
550 assert!((size - 1.0).abs() < 0.001);
551 }
552
553 #[test]
554 fn f_scroll_set_total_rows_with_selection_clamps() {
555 let mut scroll = ScrollState::new(100, 20);
557 scroll.set_selected(Some(80));
558 scroll.set_total_rows(50);
560 assert_eq!(scroll.selected(), Some(49));
561 }
562}