1use std::ops::Range;
36
37pub const SUBCELL: usize = 8;
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum CellFill {
46 Empty,
48 Full,
50 Partial {
52 start: u8,
54 len: u8,
56 },
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum HitTest {
64 Thumb,
66 Track,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub struct ScrollMetrics {
79 content_len: usize,
80 viewport_len: usize,
81 offset: usize,
82 track_cells: usize,
83 track_len: usize,
84 thumb_len: usize,
85 thumb_start: usize,
86}
87
88impl ScrollMetrics {
89 pub fn from_lengths(lengths: crate::ScrollLengths, offset: usize, track_cells: u16) -> Self {
91 Self::new(lengths, offset, track_cells)
92 }
93
94 pub fn new(lengths: crate::ScrollLengths, offset: usize, track_cells: u16) -> Self {
101 let track_cells = track_cells as usize;
102 let track_len = track_cells.saturating_mul(SUBCELL);
103
104 if track_len == 0 {
105 return Self {
106 content_len: lengths.content_len,
107 viewport_len: lengths.viewport_len,
108 offset,
109 track_cells,
110 track_len,
111 thumb_len: 0,
112 thumb_start: 0,
113 };
114 }
115
116 let content_len = lengths.content_len.max(1);
117 let viewport_len = lengths.viewport_len.min(content_len).max(1);
118 let max_offset = content_len.saturating_sub(viewport_len);
119 let offset = offset.min(max_offset);
120
121 let (thumb_len, thumb_start) = if max_offset == 0 {
122 (track_len, 0)
123 } else {
124 let thumb_len = (track_len.saturating_mul(viewport_len) / content_len)
125 .max(SUBCELL)
126 .min(track_len);
127 let thumb_travel = track_len.saturating_sub(thumb_len);
128 let thumb_start = thumb_travel.saturating_mul(offset) / max_offset;
129 (thumb_len, thumb_start)
130 };
131
132 Self {
133 content_len,
134 viewport_len,
135 offset,
136 track_cells,
137 track_len,
138 thumb_len,
139 thumb_start,
140 }
141 }
142
143 pub const fn content_len(&self) -> usize {
145 self.content_len
146 }
147
148 pub const fn viewport_len(&self) -> usize {
150 self.viewport_len
151 }
152
153 pub const fn offset(&self) -> usize {
155 self.offset
156 }
157
158 pub const fn track_cells(&self) -> usize {
160 self.track_cells
161 }
162
163 pub const fn track_len(&self) -> usize {
165 self.track_len
166 }
167
168 pub const fn thumb_len(&self) -> usize {
170 self.thumb_len
171 }
172
173 pub const fn thumb_start(&self) -> usize {
175 self.thumb_start
176 }
177
178 pub const fn max_offset(&self) -> usize {
180 self.content_len.saturating_sub(self.viewport_len)
181 }
182
183 pub const fn thumb_travel(&self) -> usize {
185 self.track_len.saturating_sub(self.thumb_len)
186 }
187
188 pub const fn thumb_range(&self) -> Range<usize> {
190 self.thumb_start..self.thumb_start.saturating_add(self.thumb_len)
191 }
192
193 pub const fn hit_test(&self, position: usize) -> HitTest {
195 if position >= self.thumb_start
196 && position < self.thumb_start.saturating_add(self.thumb_len)
197 {
198 HitTest::Thumb
199 } else {
200 HitTest::Track
201 }
202 }
203
204 pub fn thumb_start_for_offset(&self, offset: usize) -> usize {
208 let max_offset = self.max_offset();
209 if max_offset == 0 {
210 return 0;
211 }
212 let offset = offset.min(max_offset);
213 self.thumb_travel().saturating_mul(offset) / max_offset
214 }
215
216 pub fn offset_for_thumb_start(&self, thumb_start: usize) -> usize {
220 let max_offset = self.max_offset();
221 if max_offset == 0 {
222 return 0;
223 }
224 let thumb_start = thumb_start.min(self.thumb_travel());
225 max_offset.saturating_mul(thumb_start) / self.thumb_travel()
226 }
227
228 pub fn cell_fill(&self, cell_index: usize) -> CellFill {
233 if self.thumb_len == 0 {
234 return CellFill::Empty;
235 }
236
237 let cell_start = cell_index.saturating_mul(SUBCELL);
238 let cell_end = cell_start.saturating_add(SUBCELL);
239
240 let thumb_end = self.thumb_start.saturating_add(self.thumb_len);
241 let start = self.thumb_start.max(cell_start);
242 let end = thumb_end.min(cell_end);
243
244 if end <= start {
245 return CellFill::Empty;
246 }
247
248 let len = end.saturating_sub(start).min(SUBCELL) as u8;
249 let start = start.saturating_sub(cell_start).min(SUBCELL) as u8;
250
251 if len as usize >= SUBCELL {
252 CellFill::Full
253 } else {
254 CellFill::Partial { start, len }
255 }
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262 #[test]
263 fn metrics_no_scroll_fills_track() {
264 let metrics = ScrollMetrics::new(
265 crate::ScrollLengths {
266 content_len: 10,
267 viewport_len: 10,
268 },
269 0,
270 4,
271 );
272 assert_eq!(metrics.thumb_len(), 32);
273 assert_eq!(metrics.thumb_start(), 0);
274 }
275
276 #[test]
277 fn metrics_clamps_offset() {
278 let metrics = ScrollMetrics::new(
279 crate::ScrollLengths {
280 content_len: 100,
281 viewport_len: 10,
282 },
283 200,
284 4,
285 );
286 assert_eq!(metrics.offset(), 90);
287 assert_eq!(metrics.thumb_start(), metrics.thumb_travel());
288 }
289
290 #[test]
291 fn metrics_cell_fill_partial() {
292 let metrics = ScrollMetrics::new(
293 crate::ScrollLengths {
294 content_len: 10,
295 viewport_len: 3,
296 },
297 1,
298 4,
299 );
300 assert_eq!(metrics.cell_fill(0), CellFill::Partial { start: 3, len: 5 });
301 assert_eq!(metrics.cell_fill(1), CellFill::Partial { start: 0, len: 4 });
302 assert_eq!(metrics.cell_fill(2), CellFill::Empty);
303 }
304
305 #[test]
306 fn metrics_hit_test_thumb_vs_track() {
307 let metrics = ScrollMetrics::new(
308 crate::ScrollLengths {
309 content_len: 10,
310 viewport_len: 3,
311 },
312 1,
313 4,
314 );
315 assert_eq!(metrics.hit_test(0), HitTest::Track);
316 assert_eq!(metrics.hit_test(4), HitTest::Thumb);
317 assert_eq!(metrics.hit_test(12), HitTest::Track);
318 }
319
320 #[test]
321 fn metrics_are_scale_invariant_for_logical_units() {
322 let track_cells = 10;
323 let base = ScrollMetrics::new(
324 crate::ScrollLengths {
325 content_len: 200,
326 viewport_len: 20,
327 },
328 10,
329 track_cells,
330 );
331 let scaled = ScrollMetrics::new(
332 crate::ScrollLengths {
333 content_len: 200 * SUBCELL,
334 viewport_len: 20 * SUBCELL,
335 },
336 10 * SUBCELL,
337 track_cells,
338 );
339 assert_eq!(base.thumb_len(), scaled.thumb_len());
340 assert_eq!(base.thumb_start(), scaled.thumb_start());
341 }
342}