fret_ui_headless/embla/
scroll_target.rs1use crate::embla::limit::Limit;
2use crate::embla::utils::{DIRECTION_NONE, Direction, math_abs, math_sign};
3
4#[derive(Debug, Clone, Copy, PartialEq)]
5pub struct Target {
6 pub distance: f32,
7 pub index: usize,
8}
9
10#[derive(Debug, Clone, PartialEq)]
14pub struct ScrollTarget {
15 loop_enabled: bool,
16 scroll_snaps: Vec<f32>,
17 content_size: f32,
18 limit: Limit,
19 target_vector: f32,
20}
21
22impl ScrollTarget {
23 pub fn new(
24 loop_enabled: bool,
25 scroll_snaps: Vec<f32>,
26 content_size: f32,
27 limit: Limit,
28 target_vector: f32,
29 ) -> Self {
30 Self {
31 loop_enabled,
32 scroll_snaps,
33 content_size,
34 limit,
35 target_vector,
36 }
37 }
38
39 pub fn set_target_vector(&mut self, target_vector: f32) {
40 self.target_vector = target_vector;
41 }
42
43 pub fn target_vector(&self) -> f32 {
44 self.target_vector
45 }
46
47 pub fn loop_enabled(&self) -> bool {
48 self.loop_enabled
49 }
50
51 pub fn max_index(&self) -> usize {
52 self.scroll_snaps.len().saturating_sub(1)
53 }
54
55 fn min_distance(distances: &mut [f32]) -> f32 {
56 distances.sort_by(|a, b| math_abs(*a).total_cmp(&math_abs(*b)));
57 distances[0]
58 }
59
60 pub fn shortcut(&self, target: f32, direction: Direction) -> f32 {
61 if !self.loop_enabled {
62 return target;
63 }
64
65 let targets = [
66 target,
67 target + self.content_size,
68 target - self.content_size,
69 ];
70 if direction == DIRECTION_NONE {
71 let mut tmp = targets;
72 return Self::min_distance(&mut tmp);
73 }
74
75 let mut valid = targets
76 .into_iter()
77 .filter(|t| math_sign(*t) == direction as f32)
78 .collect::<Vec<_>>();
79 if !valid.is_empty() {
80 return Self::min_distance(&mut valid);
81 }
82 targets[2] - self.content_size
83 }
84
85 fn get_closest_snap(&self, target: f32) -> Target {
86 let distance = if self.loop_enabled {
87 self.limit.remove_offset(target)
88 } else {
89 self.limit.clamp(target)
90 };
91
92 let mut smallest = f32::INFINITY;
93 let mut index = 0usize;
94 for (snap_index, snap) in self.scroll_snaps.iter().copied().enumerate() {
95 let displacement_abs = math_abs(self.shortcut(snap - distance, DIRECTION_NONE));
96 if displacement_abs >= smallest {
97 continue;
98 }
99 smallest = displacement_abs;
100 index = snap_index;
101 }
102
103 Target { index, distance }
104 }
105
106 pub fn by_index(&self, index: usize, direction: Direction) -> Target {
107 let index = index.min(self.scroll_snaps.len().saturating_sub(1));
108 let diff_to_snap = self.scroll_snaps[index] - self.target_vector;
109 let distance = self.shortcut(diff_to_snap, direction);
110 Target { index, distance }
111 }
112
113 pub fn by_distance(&self, distance: f32, snap_to_closest: bool) -> Target {
114 let target = self.target_vector + distance;
115 let Target {
116 index,
117 distance: target_snap_distance,
118 } = self.get_closest_snap(target);
119 let is_past_any_bound = !self.loop_enabled && self.limit.past_any_bound(target);
120
121 if !snap_to_closest || is_past_any_bound {
122 return Target { index, distance };
123 }
124
125 let diff_to_snap = self.scroll_snaps[index] - target_snap_distance;
126 let snap_distance = distance + self.shortcut(diff_to_snap, DIRECTION_NONE);
127 Target {
128 index,
129 distance: snap_distance,
130 }
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use crate::embla::scroll_limit::scroll_limit;
138
139 #[test]
140 fn by_distance_snaps_to_closest_when_enabled() {
141 let snaps = vec![0.0, -100.0, -200.0, -300.0];
143 let content_size = 300.0;
144 let limit = scroll_limit(content_size, &snaps, false);
145 let target = ScrollTarget::new(false, snaps, content_size, limit, 0.0);
146
147 let out = target.by_distance(-130.0, true);
148 assert_eq!(out.index, 1);
149 assert!((out.distance - -100.0).abs() < 0.001);
150 }
151
152 #[test]
153 fn by_distance_does_not_snap_when_flag_is_false() {
154 let snaps = vec![0.0, -100.0, -200.0, -300.0];
155 let content_size = 300.0;
156 let limit = scroll_limit(content_size, &snaps, false);
157 let target = ScrollTarget::new(false, snaps, content_size, limit, 0.0);
158
159 let out = target.by_distance(-130.0, false);
160 assert_eq!(out.index, 1);
161 assert!((out.distance - -130.0).abs() < 0.001);
162 }
163}