Skip to main content

freya_components/scrollviews/
shared.rs

1use freya_core::prelude::*;
2use torin::{
3    prelude::Direction,
4    size::Size,
5};
6
7#[derive(Debug, PartialEq, Eq, Clone, Copy)]
8pub enum Axis {
9    X,
10    Y,
11}
12
13#[doc(hidden)]
14pub fn get_scroll_position_from_wheel(
15    wheel_movement: f32,
16    inner_size: f32,
17    viewport_size: f32,
18    scroll_position: f32,
19) -> i32 {
20    if viewport_size >= inner_size {
21        return 0;
22    }
23
24    let new_position = scroll_position + wheel_movement;
25
26    if new_position >= 0.0 && wheel_movement > 0.0 {
27        return 0;
28    }
29
30    if new_position <= -(inner_size - viewport_size) && wheel_movement < 0.0 {
31        return -(inner_size - viewport_size) as i32;
32    }
33
34    new_position as i32
35}
36
37#[doc(hidden)]
38pub fn get_corrected_scroll_position(
39    inner_size: f32,
40    viewport_size: f32,
41    scroll_position: f32,
42) -> f32 {
43    // Considering it was a vertical scroll view, the start would be on top and the end on bottom.
44    let overscrolled_start = scroll_position > 0.0;
45    let overscrolled_end = (-scroll_position + viewport_size) > inner_size;
46
47    if overscrolled_start {
48        0f32
49    } else if overscrolled_end {
50        if viewport_size < inner_size {
51            -(inner_size - viewport_size)
52        } else {
53            0f32
54        }
55    } else {
56        scroll_position
57    }
58}
59
60#[doc(hidden)]
61pub fn get_container_sizes(size: Size) -> (Size, Size) {
62    if size == Size::Inner {
63        (size.clone(), size)
64    } else {
65        (Size::percent(100.), Size::fill())
66    }
67}
68
69#[doc(hidden)]
70pub fn is_scrollbar_visible(
71    is_scrollbar_enabled: bool,
72    inner_size: f32,
73    viewport_size: f32,
74) -> bool {
75    is_scrollbar_enabled && viewport_size > MIN_SCROLLBAR_SIZE && viewport_size < inner_size
76}
77
78const MIN_SCROLLBAR_SIZE: f32 = 50.0;
79
80pub const SCROLLBAR_MARGIN: f32 = 3.0;
81
82/// Thumb size, scrollable content range and thumb travel range inside the track.
83fn get_thumb_ranges(inner_size: f32, viewport_size: f32) -> (f32, f32, f32) {
84    let track_size = viewport_size - SCROLLBAR_MARGIN * 2.0;
85    let viewable_ratio = viewport_size / inner_size;
86    let minimum_thumb_size = if track_size > MIN_SCROLLBAR_SIZE {
87        MIN_SCROLLBAR_SIZE
88    } else {
89        0.
90    };
91    let scrollbar_size = (track_size * viewable_ratio).max(minimum_thumb_size);
92
93    let available_scroll_range = inner_size - viewport_size;
94    let available_thumb_range = track_size - scrollbar_size;
95
96    (
97        scrollbar_size,
98        available_scroll_range,
99        available_thumb_range,
100    )
101}
102
103#[doc(hidden)]
104pub fn get_scrollbar_pos_and_size(
105    inner_size: f32,
106    viewport_size: f32,
107    scroll_position: f32,
108) -> (f32, f32) {
109    if viewport_size <= MIN_SCROLLBAR_SIZE || viewport_size >= inner_size {
110        return (0.0, 0.0);
111    }
112
113    let (scrollbar_size, available_scroll_range, available_thumb_range) =
114        get_thumb_ranges(inner_size, viewport_size);
115
116    let normalized_scroll = -scroll_position / available_scroll_range;
117    let scrollbar_position = normalized_scroll * available_thumb_range;
118
119    (scrollbar_position, scrollbar_size)
120}
121#[doc(hidden)]
122pub fn get_scroll_position_from_cursor(
123    cursor_position: f32,
124    inner_size: f32,
125    viewport_size: f32,
126) -> i32 {
127    if viewport_size <= MIN_SCROLLBAR_SIZE || viewport_size >= inner_size {
128        return 0;
129    }
130
131    let (_, available_scroll_range, available_thumb_range) =
132        get_thumb_ranges(inner_size, viewport_size);
133
134    // Clamp cursor position
135    let cursor_clamped = (cursor_position - SCROLLBAR_MARGIN).clamp(0.0, available_thumb_range);
136
137    let normalized_scroll = cursor_clamped / available_thumb_range;
138    let new_position = -(normalized_scroll * available_scroll_range);
139
140    new_position as i32
141}
142
143pub fn handle_key_event(
144    key: &Key,
145    (mut x, mut y): (f32, f32),
146    inner_height: f32,
147    inner_width: f32,
148    viewport_height: f32,
149    viewport_width: f32,
150    direction: Direction,
151) -> Option<(f32, f32)> {
152    let y_page_delta = viewport_height;
153    let y_line_delta = y_page_delta / 5.0;
154    let x_page_delta = viewport_width;
155    let x_line_delta = x_page_delta / 5.0;
156
157    // TODO(tropix126): Handle spacebar and spacebar + shift as Home and End
158
159    match key {
160        Key::Named(NamedKey::ArrowUp) => {
161            y = get_corrected_scroll_position(inner_height, viewport_height, y + y_line_delta)
162        }
163        Key::Named(NamedKey::ArrowDown) => {
164            y = get_corrected_scroll_position(inner_height, viewport_height, y - y_line_delta)
165        }
166        Key::Named(NamedKey::PageUp) => {
167            y = get_corrected_scroll_position(inner_height, viewport_height, y + y_line_delta)
168        }
169        Key::Named(NamedKey::PageDown) => {
170            y = get_corrected_scroll_position(inner_height, viewport_height, y - y_line_delta)
171        }
172        Key::Named(NamedKey::ArrowLeft) => {
173            x = get_corrected_scroll_position(inner_width, viewport_width, x + x_line_delta)
174        }
175        Key::Named(NamedKey::ArrowRight) => {
176            x = get_corrected_scroll_position(inner_width, viewport_width, x - x_line_delta)
177        }
178        Key::Named(NamedKey::Home) => {
179            if direction == Direction::Vertical {
180                y = 0.0;
181            } else {
182                x = 0.0;
183            }
184        }
185        Key::Named(NamedKey::End) => {
186            if direction == Direction::Vertical {
187                y = -inner_height;
188            } else {
189                x = -inner_width;
190            }
191        }
192        _ => return None,
193    };
194    Some((x, y))
195}
196
197#[cfg(test)]
198mod tests {
199    use crate::scrollviews::shared::{
200        SCROLLBAR_MARGIN,
201        get_scroll_position_from_cursor,
202        get_scrollbar_pos_and_size,
203    };
204
205    #[test]
206    fn scrollbar_can_drag_when_the_track_is_smaller_than_the_minimum_thumb() {
207        for viewport_size in 51..=56 {
208            let viewport_size = viewport_size as f32;
209            let inner_size = viewport_size * 2.;
210            let track_size = viewport_size - SCROLLBAR_MARGIN * 2.;
211
212            let (_, thumb_size) = get_scrollbar_pos_and_size(inner_size, viewport_size, 0.);
213
214            assert!(thumb_size < track_size);
215            assert_eq!(
216                get_scroll_position_from_cursor(viewport_size, inner_size, viewport_size),
217                -viewport_size as i32
218            );
219        }
220    }
221}