1use super::Orientation;
2use crate::Bounds;
3
4#[derive(Debug, Clone)]
15pub struct ScrollDimensions {
16 content_size: (usize, usize),
18 viewable_size: (usize, usize),
20 scroll_position: (f64, f64),
22 parent_bounds: Bounds,
24}
25
26impl ScrollDimensions {
27 pub fn new(
29 content_size: (usize, usize),
30 viewable_size: (usize, usize),
31 scroll_position: (f64, f64),
32 parent_bounds: Bounds,
33 ) -> Self {
34 Self {
35 content_size,
36 viewable_size,
37 scroll_position: (
38 scroll_position.0.clamp(0.0, 100.0),
39 scroll_position.1.clamp(0.0, 100.0),
40 ),
41 parent_bounds,
42 }
43 }
44
45 pub fn set_scroll_position(&mut self, horizontal: f64, vertical: f64) {
47 self.scroll_position = (horizontal.clamp(0.0, 100.0), vertical.clamp(0.0, 100.0));
48 }
49
50 pub fn is_scrollbar_needed(&self, orientation: Orientation) -> bool {
53 match orientation {
54 Orientation::Vertical => self.content_size.1 > self.viewable_size.1,
55 Orientation::Horizontal => self.content_size.0 > self.viewable_size.0,
56 }
57 }
58
59 pub fn get_track_bounds(&self, orientation: Orientation) -> TrackBounds {
62 match orientation {
63 Orientation::Vertical => {
64 let x = self.parent_bounds.right();
65 let start_y = self.parent_bounds.top() + 1;
66 let end_y = self.parent_bounds.bottom().saturating_sub(1);
67 TrackBounds {
68 position: x,
69 start: start_y,
70 end: end_y,
71 length: end_y.saturating_sub(start_y),
72 }
73 }
74 Orientation::Horizontal => {
75 let y = self.parent_bounds.bottom();
76 let start_x = self.parent_bounds.left() + 1;
77 let end_x = self.parent_bounds.right().saturating_sub(1);
78 TrackBounds {
79 position: y,
80 start: start_x,
81 end: end_x,
82 length: end_x.saturating_sub(start_x),
83 }
84 }
85 }
86 }
87
88 pub fn calculate_knob_size(&self, orientation: Orientation) -> usize {
91 let track = self.get_track_bounds(orientation);
92 let track_length = track.length;
93
94 if track_length == 0 {
95 return 1;
96 }
97
98 let content_ratio = match orientation {
99 Orientation::Vertical => {
100 if self.content_size.1 == 0 {
101 1.0
102 } else {
103 self.viewable_size.1 as f64 / self.content_size.1 as f64
104 }
105 }
106 Orientation::Horizontal => {
107 if self.content_size.0 == 0 {
108 1.0
109 } else {
110 self.viewable_size.0 as f64 / self.content_size.0 as f64
111 }
112 }
113 };
114
115 std::cmp::max(1, (track_length as f64 * content_ratio).round() as usize)
116 }
117
118 pub fn calculate_knob_position(&self, orientation: Orientation) -> usize {
121 let track = self.get_track_bounds(orientation);
122 let knob_size = self.calculate_knob_size(orientation);
123 let available_track = track.length.saturating_sub(knob_size);
124
125 if available_track == 0 {
126 return 0;
127 }
128
129 let scroll_percent = match orientation {
130 Orientation::Vertical => self.scroll_position.1,
131 Orientation::Horizontal => self.scroll_position.0,
132 };
133
134 ((scroll_percent / 100.0) * available_track as f64).round() as usize
135 }
136
137 pub fn get_knob_bounds(&self, orientation: Orientation) -> KnobBounds {
140 let track = self.get_track_bounds(orientation);
141 let knob_size = self.calculate_knob_size(orientation);
142 let knob_position = self.calculate_knob_position(orientation);
143
144 match orientation {
145 Orientation::Vertical => KnobBounds {
146 x: track.position,
147 y_start: track.start + knob_position,
148 y_end: track.start + knob_position + knob_size,
149 size: knob_size,
150 },
151 Orientation::Horizontal => KnobBounds {
152 x: track.start + knob_position,
153 y_start: track.position,
154 y_end: track.position,
155 size: knob_size,
156 },
157 }
158 }
159
160 pub fn calculate_scroll_offset(&self, orientation: Orientation) -> usize {
163 let max_offset = match orientation {
164 Orientation::Vertical => {
165 if self.content_size.1 <= self.viewable_size.1 {
166 0
167 } else {
168 self.content_size.1 - self.viewable_size.1
169 }
170 }
171 Orientation::Horizontal => {
172 if self.content_size.0 <= self.viewable_size.0 {
173 0
174 } else {
175 self.content_size.0 - self.viewable_size.0
176 }
177 }
178 };
179
180 if max_offset == 0 {
181 return 0;
182 }
183
184 let scroll_percent = match orientation {
185 Orientation::Vertical => self.scroll_position.1,
186 Orientation::Horizontal => self.scroll_position.0,
187 };
188
189 ((scroll_percent / 100.0) * max_offset as f64).floor() as usize
190 }
191
192 pub fn pixel_to_scroll_percent(
195 &self,
196 pixel_coordinate: usize,
197 orientation: Orientation,
198 ) -> f64 {
199 let track = self.get_track_bounds(orientation);
200
201 if track.length == 0 {
202 return 0.0;
203 }
204
205 let relative_position = pixel_coordinate.saturating_sub(track.start);
206 let click_position = relative_position as f64 / track.length as f64;
207
208 (click_position * 100.0).clamp(0.0, 100.0)
209 }
210
211 pub fn hits_knob(&self, x: usize, y: usize, orientation: Orientation) -> bool {
214 let knob = self.get_knob_bounds(orientation);
215
216 match orientation {
217 Orientation::Vertical => x == knob.x && y >= knob.y_start && y < knob.y_end,
218 Orientation::Horizontal => y == knob.y_start && x >= knob.x && x < (knob.x + knob.size),
219 }
220 }
221
222 pub fn hits_track(&self, x: usize, y: usize, orientation: Orientation) -> bool {
225 let track = self.get_track_bounds(orientation);
226
227 match orientation {
228 Orientation::Vertical => {
229 x == track.position
230 && y >= track.start
231 && y <= track.end
232 && !self.hits_knob(x, y, orientation)
233 }
234 Orientation::Horizontal => {
235 y == track.position
236 && x >= track.start
237 && x <= track.end
238 && !self.hits_knob(x, y, orientation)
239 }
240 }
241 }
242
243 pub fn calculate_drag_scroll(
246 &self,
247 start_pixel: usize,
248 current_pixel: usize,
249 orientation: Orientation,
250 ) -> f64 {
251 let track = self.get_track_bounds(orientation);
252 let knob_size = self.calculate_knob_size(orientation);
253 let available_track = track.length.saturating_sub(knob_size);
254
255 if available_track == 0 {
256 return match orientation {
257 Orientation::Vertical => self.scroll_position.1,
258 Orientation::Horizontal => self.scroll_position.0,
259 };
260 }
261
262 let pixel_delta = current_pixel as i32 - start_pixel as i32;
263 let percentage_delta = (pixel_delta as f64 / available_track as f64) * 100.0;
264
265 let current_scroll = match orientation {
266 Orientation::Vertical => self.scroll_position.1,
267 Orientation::Horizontal => self.scroll_position.0,
268 };
269
270 (current_scroll + percentage_delta).clamp(0.0, 100.0)
271 }
272
273 pub fn get_visible_range(&self, orientation: Orientation) -> (usize, usize) {
276 let scroll_offset = self.calculate_scroll_offset(orientation);
277 let viewable_size = match orientation {
278 Orientation::Vertical => self.viewable_size.1,
279 Orientation::Horizontal => self.viewable_size.0,
280 };
281
282 let start = scroll_offset;
283 let end = scroll_offset + viewable_size;
284
285 (start, end)
286 }
287
288 pub fn calculate_auto_scroll_to_line(
291 &self,
292 target_line: usize,
293 orientation: Orientation,
294 ) -> f64 {
295 let (visible_start, visible_end) = self.get_visible_range(orientation);
296 let viewable_size = match orientation {
297 Orientation::Vertical => self.viewable_size.1,
298 Orientation::Horizontal => self.viewable_size.0,
299 };
300 let content_size = match orientation {
301 Orientation::Vertical => self.content_size.1,
302 Orientation::Horizontal => self.content_size.0,
303 };
304
305 if target_line >= visible_start && target_line < visible_end {
307 return match orientation {
308 Orientation::Vertical => self.scroll_position.1,
309 Orientation::Horizontal => self.scroll_position.0,
310 };
311 }
312
313 let new_offset = if target_line < visible_start {
314 target_line
316 } else {
317 target_line.saturating_sub(viewable_size - 1)
319 };
320
321 if content_size <= viewable_size {
322 return 0.0;
323 }
324
325 let max_offset = content_size - viewable_size;
326 let scroll_percent = (new_offset as f64 / max_offset as f64) * 100.0;
327
328 scroll_percent.clamp(0.0, 100.0)
329 }
330}
331
332#[derive(Debug, Clone, PartialEq)]
333pub struct TrackBounds {
334 pub position: usize,
336 pub start: usize,
338 pub end: usize,
340 pub length: usize,
342}
343
344#[derive(Debug, Clone, PartialEq)]
345pub struct KnobBounds {
346 pub x: usize,
348 pub y_start: usize,
350 pub y_end: usize,
352 pub size: usize,
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359
360 #[test]
361 fn test_scrollbar_needed() {
362 let bounds = Bounds::new(0, 0, 10, 5);
363 let scroll_dims = ScrollDimensions::new(
364 (20, 15), (10, 5), (0.0, 0.0),
367 bounds,
368 );
369
370 assert!(scroll_dims.is_scrollbar_needed(Orientation::Horizontal));
371 assert!(scroll_dims.is_scrollbar_needed(Orientation::Vertical));
372 }
373
374 #[test]
375 fn test_knob_size_calculation() {
376 let bounds = Bounds::new(0, 0, 10, 10);
377 let scroll_dims = ScrollDimensions::new(
378 (20, 20), (10, 10), (0.0, 0.0),
381 bounds,
382 );
383
384 let knob_size = scroll_dims.calculate_knob_size(Orientation::Vertical);
388 assert_eq!(knob_size, 4);
389 }
390
391 #[test]
392 fn test_scroll_offset_calculation() {
393 let bounds = Bounds::new(0, 0, 10, 10);
394 let mut scroll_dims = ScrollDimensions::new(
395 (20, 20), (10, 10), (50.0, 50.0), bounds,
399 );
400
401 let offset = scroll_dims.calculate_scroll_offset(Orientation::Vertical);
404 assert_eq!(offset, 5);
405
406 scroll_dims.set_scroll_position(100.0, 100.0);
408 let offset = scroll_dims.calculate_scroll_offset(Orientation::Vertical);
409 assert_eq!(offset, 10);
410 }
411
412 #[test]
413 fn test_pixel_to_scroll_percent() {
414 let bounds = Bounds::new(0, 0, 10, 10);
415 let scroll_dims = ScrollDimensions::new((20, 20), (10, 10), (0.0, 0.0), bounds);
416
417 let track = scroll_dims.get_track_bounds(Orientation::Vertical);
420 let middle_pixel = track.start + track.length / 2;
421 let percent = scroll_dims.pixel_to_scroll_percent(middle_pixel, Orientation::Vertical);
422
423 assert!((percent - 50.0).abs() < 1.0); }
425
426 #[test]
427 fn test_knob_hit_testing() {
428 let bounds = Bounds::new(0, 0, 10, 10);
429 let scroll_dims = ScrollDimensions::new(
430 (20, 20),
431 (10, 10),
432 (0.0, 0.0), bounds,
434 );
435
436 let knob = scroll_dims.get_knob_bounds(Orientation::Vertical);
437
438 assert!(scroll_dims.hits_knob(knob.x, knob.y_start, Orientation::Vertical));
440
441 assert!(!scroll_dims.hits_knob(knob.x, knob.y_end + 1, Orientation::Vertical));
443 }
444
445 #[test]
446 fn test_auto_scroll_to_line() {
447 let bounds = Bounds::new(0, 0, 10, 10);
448 let scroll_dims = ScrollDimensions::new(
449 (10, 100), (10, 10), (0.0, 0.0), bounds,
453 );
454
455 let scroll_percent = scroll_dims.calculate_auto_scroll_to_line(50, Orientation::Vertical);
457
458 assert!(scroll_percent > 40.0 && scroll_percent < 60.0);
460 }
461}