fret_ui_headless/embla/
scroll_contain.rs1use crate::embla::limit::Limit;
2use crate::embla::utils::array_last;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum ScrollContainOption {
6 None,
7 TrimSnaps,
8 KeepSnaps,
9}
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct ScrollContainLimit {
13 pub min: usize,
14 pub max: usize,
15}
16
17#[derive(Debug, Clone, PartialEq)]
18pub struct ScrollContainOutput {
19 pub snaps_contained: Vec<f32>,
20 pub scroll_contain_limit: ScrollContainLimit,
21}
22
23pub fn scroll_contain(
27 view_size: f32,
28 content_size: f32,
29 snaps_aligned: &[f32],
30 contain_scroll: ScrollContainOption,
31 pixel_tolerance: f32,
32) -> ScrollContainOutput {
33 let view_size = view_size.max(0.0);
34 let content_size = content_size.max(0.0);
35
36 let scroll_bounds = Limit::new(-content_size + view_size, 0.0);
37 let snaps_bounded = snaps_bounded(scroll_bounds, snaps_aligned, pixel_tolerance.max(0.0));
38 let scroll_contain_limit = scroll_contain_limit(&snaps_bounded);
39 let snaps_contained = snaps_contained(
40 scroll_bounds,
41 &snaps_bounded,
42 scroll_contain_limit,
43 view_size,
44 content_size,
45 contain_scroll,
46 pixel_tolerance.max(0.0),
47 );
48
49 ScrollContainOutput {
50 snaps_contained,
51 scroll_contain_limit,
52 }
53}
54
55fn snaps_bounded(scroll_bounds: Limit, snaps_aligned: &[f32], pixel_tolerance: f32) -> Vec<f32> {
56 let len = snaps_aligned.len();
57 if len == 0 {
58 return Vec::new();
59 }
60
61 let use_pixel_tolerance = |bound: f32, snap: f32| -> bool {
62 if pixel_tolerance <= 0.0 {
63 return false;
64 }
65 (bound - snap).abs() <= 1.0
67 };
68
69 let mut out = Vec::with_capacity(len);
70 for (index, snap_aligned) in snaps_aligned.iter().copied().enumerate() {
71 let snap = scroll_bounds.clamp(snap_aligned);
72 let is_first = index == 0;
73 let is_last = index + 1 == len;
74
75 let bounded = if is_first {
76 scroll_bounds.max
77 } else if is_last || use_pixel_tolerance(scroll_bounds.min, snap) {
78 scroll_bounds.min
79 } else if use_pixel_tolerance(scroll_bounds.max, snap) {
80 scroll_bounds.max
81 } else {
82 snap
83 };
84
85 out.push((bounded * 1000.0).round() / 1000.0);
87 }
88 out
89}
90
91fn scroll_contain_limit(snaps_bounded: &[f32]) -> ScrollContainLimit {
92 if snaps_bounded.is_empty() {
93 return ScrollContainLimit { min: 0, max: 0 };
94 }
95
96 let start_snap = snaps_bounded[0];
97 let end_snap = array_last(snaps_bounded);
98
99 let mut min = 0usize;
100 for (idx, snap) in snaps_bounded.iter().copied().enumerate() {
101 if snap == start_snap {
102 min = idx;
103 }
104 }
105
106 let mut max = snaps_bounded.len();
107 for (idx, snap) in snaps_bounded.iter().copied().enumerate() {
108 if snap == end_snap {
109 max = idx + 1;
110 break;
111 }
112 }
113
114 ScrollContainLimit { min, max }
115}
116
117fn snaps_contained(
118 scroll_bounds: Limit,
119 snaps_bounded: &[f32],
120 contain_limit: ScrollContainLimit,
121 view_size: f32,
122 content_size: f32,
123 contain_scroll: ScrollContainOption,
124 pixel_tolerance: f32,
125) -> Vec<f32> {
126 if content_size <= view_size + pixel_tolerance {
127 return vec![scroll_bounds.max];
128 }
129
130 if contain_scroll == ScrollContainOption::KeepSnaps {
131 return snaps_bounded.to_vec();
132 }
133
134 snaps_bounded[contain_limit.min..contain_limit.max].to_vec()
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn scroll_contain_short_circuits_when_content_fits_view_with_tolerance() {
143 let out = scroll_contain(
144 320.0,
145 321.0,
146 &[0.0, -100.0, -200.0],
147 ScrollContainOption::TrimSnaps,
148 2.0,
149 );
150 assert_eq!(out.snaps_contained, vec![0.0]);
151 }
152
153 #[test]
154 fn scroll_contain_keep_snaps_preserves_bounded_list() {
155 let out = scroll_contain(
156 320.0,
157 520.0,
158 &[0.0, -10.0, -200.0],
159 ScrollContainOption::KeepSnaps,
160 2.0,
161 );
162
163 assert_eq!(out.snaps_contained.len(), 3);
164 assert_eq!(out.snaps_contained[0], 0.0);
165 assert_eq!(out.snaps_contained[2], -200.0);
166 }
167
168 #[test]
169 fn scroll_contain_trim_snaps_slices_between_edge_duplicates() {
170 let out = scroll_contain(
173 320.0,
174 820.0,
175 &[0.0, 0.1, -0.4, -400.0, -500.0],
176 ScrollContainOption::TrimSnaps,
177 2.0,
178 );
179
180 assert_eq!(out.snaps_contained[0], 0.0);
182 assert_eq!(array_last(&out.snaps_contained), -500.0);
183 assert!(out.scroll_contain_limit.min <= out.scroll_contain_limit.max);
184 }
185}