tpt_appfront_core/
virtual_scroll.rs1use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
14pub struct VirtualScroll {
15 pub item_height: f32,
17 pub viewport_height: f32,
19 pub scroll_offset: f32,
23 pub overscan: usize,
26}
27
28impl VirtualScroll {
29 pub fn new(item_height: f32, viewport_height: f32) -> Self {
30 VirtualScroll {
31 item_height,
32 viewport_height,
33 scroll_offset: 0.0,
34 overscan: 3,
35 }
36 }
37
38 pub fn with_offset(mut self, offset: f32) -> Self {
39 self.scroll_offset = offset.max(0.0);
40 self
41 }
42
43 pub fn with_overscan(mut self, overscan: usize) -> Self {
44 self.overscan = overscan;
45 self
46 }
47
48 pub fn visible_range(&self, total_items: usize) -> VisibleRange {
53 if total_items == 0 || self.item_height <= 0.0 {
54 return VisibleRange {
55 start: 0,
56 end: 0,
57 top_spacer: 0.0,
58 bottom_spacer: 0.0,
59 };
60 }
61
62 let first_visible = (self.scroll_offset / self.item_height).floor() as usize;
63 let visible_count = (self.viewport_height / self.item_height).ceil() as usize + 1;
64
65 let start = first_visible.saturating_sub(self.overscan).min(total_items);
66 let end = (first_visible + visible_count + self.overscan).min(total_items);
67
68 let top_spacer = start as f32 * self.item_height;
69 let bottom_spacer = (total_items - end) as f32 * self.item_height;
70
71 VisibleRange {
72 start,
73 end,
74 top_spacer,
75 bottom_spacer,
76 }
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq)]
83pub struct VisibleRange {
84 pub start: usize,
86 pub end: usize,
88 pub top_spacer: f32,
90 pub bottom_spacer: f32,
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn empty_list_has_empty_range() {
100 let vs = VirtualScroll::new(20.0, 200.0);
101 let range = vs.visible_range(0);
102 assert_eq!(
103 range,
104 VisibleRange {
105 start: 0,
106 end: 0,
107 top_spacer: 0.0,
108 bottom_spacer: 0.0
109 }
110 );
111 }
112
113 #[test]
114 fn scrolled_to_top_renders_first_window_plus_overscan() {
115 let vs = VirtualScroll::new(20.0, 200.0);
117 let range = vs.visible_range(1000);
118 assert_eq!(range.start, 0);
119 assert_eq!(range.end, 14); assert_eq!(range.top_spacer, 0.0);
121 assert_eq!(range.bottom_spacer, (1000 - 14) as f32 * 20.0);
122 }
123
124 #[test]
125 fn scrolled_mid_list_windows_around_offset() {
126 let vs = VirtualScroll::new(20.0, 200.0).with_offset(1000.0); let range = vs.visible_range(1000);
128 assert_eq!(range.start, 47); assert_eq!(range.end, 64); assert!(range.top_spacer > 0.0);
131 assert!(range.bottom_spacer > 0.0);
132 }
133
134 #[test]
135 fn range_clamped_to_total_items() {
136 let vs = VirtualScroll::new(20.0, 200.0).with_offset(10_000.0);
137 let range = vs.visible_range(10);
138 assert_eq!(range.end, 10);
139 assert_eq!(range.bottom_spacer, 0.0);
140 }
141}