1use crate::embla::drag_release::{
2 DragReleaseConfig, DragReleaseResult, PointerKind, compute_release,
3};
4use crate::embla::limit::Limit;
5use crate::embla::scroll_body::ScrollBody;
6use crate::embla::scroll_bounds::{ScrollBounds, ScrollBoundsConfig};
7use crate::embla::scroll_limit::scroll_limit;
8use crate::embla::scroll_target::{ScrollTarget, Target};
9use crate::embla::utils::{DIRECTION_NONE, Direction};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct SelectEvent {
13 pub target_snap: usize,
14 pub source_snap: usize,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq)]
18pub struct EngineConfig {
19 pub loop_enabled: bool,
20 pub drag_free: bool,
21 pub skip_snaps: bool,
22 pub duration: f32,
23 pub base_friction: f32,
24 pub view_size: f32,
25 pub start_snap: usize,
26}
27
28#[derive(Debug, Clone, PartialEq)]
42pub struct Engine {
43 pub scroll_body: ScrollBody,
44 pub scroll_target: ScrollTarget,
45 pub index_current: usize,
46 pub index_previous: usize,
47
48 pub scroll_bounds: ScrollBounds,
49
50 config: EngineConfig,
51 content_size: f32,
52 limit: Limit,
53}
54
55impl Engine {
56 pub fn new(scroll_snaps: Vec<f32>, content_size: f32, config: EngineConfig) -> Self {
57 let max_index = scroll_snaps.len().saturating_sub(1);
58 let start_snap = config.start_snap.min(max_index);
59 let start_location = scroll_snaps.get(start_snap).copied().unwrap_or_default();
60
61 let limit = scroll_limit(content_size, &scroll_snaps, config.loop_enabled);
62 let scroll_target = ScrollTarget::new(
63 config.loop_enabled,
64 scroll_snaps,
65 content_size,
66 limit,
67 start_location,
68 );
69 let mut scroll_body =
70 ScrollBody::new(start_location, config.duration, config.base_friction);
71 scroll_body.set_target(start_location);
72 let mut scroll_bounds = ScrollBounds::new(ScrollBoundsConfig {
73 view_size: config.view_size.max(0.0),
74 });
75 scroll_bounds.toggle_active(!config.loop_enabled);
76
77 Self {
78 scroll_body,
79 scroll_target,
80 index_current: start_snap,
81 index_previous: start_snap,
82 scroll_bounds,
83 config,
84 content_size,
85 limit,
86 }
87 }
88
89 pub fn set_options(
90 &mut self,
91 loop_enabled: bool,
92 drag_free: bool,
93 skip_snaps: bool,
94 duration: f32,
95 ) {
96 self.config.loop_enabled = loop_enabled;
97 self.config.drag_free = drag_free;
98 self.config.skip_snaps = skip_snaps;
99
100 let duration = duration.max(0.0);
101 if (self.config.duration - duration).abs() > 0.0001 {
102 self.config.duration = duration;
103 self.scroll_body.set_base_duration(duration);
104 }
105 }
106
107 #[inline]
108 pub fn loop_enabled(&self) -> bool {
109 self.config.loop_enabled
110 }
111
112 pub fn reinit(
113 &mut self,
114 scroll_snaps: Vec<f32>,
115 content_size: f32,
116 view_size: f32,
117 ) -> Option<SelectEvent> {
118 let content_size = content_size.max(0.0);
119 let view_size = view_size.max(0.0);
120
121 self.config.view_size = view_size;
122
123 let mut scroll_snaps = scroll_snaps;
124 if scroll_snaps.is_empty() {
125 scroll_snaps.push(0.0);
126 }
127
128 let limit = scroll_limit(content_size, &scroll_snaps, self.config.loop_enabled);
129
130 if self.config.loop_enabled {
134 if limit.length != 0.0 {
135 self.scroll_body
136 .set_location(limit.remove_offset(self.scroll_body.location()));
137 self.scroll_body
138 .set_target(limit.remove_offset(self.scroll_body.target()));
139 }
140 } else {
141 self.scroll_body
142 .set_location(limit.clamp(self.scroll_body.location()));
143 self.scroll_body
144 .set_target(limit.clamp(self.scroll_body.target()));
145 }
146
147 let scroll_target = ScrollTarget::new(
148 self.config.loop_enabled,
149 scroll_snaps,
150 content_size,
151 limit,
152 self.scroll_body.target(),
153 );
154
155 self.limit = limit;
156 self.content_size = content_size;
157 self.scroll_target = scroll_target;
158 self.scroll_bounds = ScrollBounds::new(ScrollBoundsConfig { view_size });
159 self.scroll_bounds.toggle_active(!self.config.loop_enabled);
160
161 self.sync_target_vector();
162
163 let next = self
164 .scroll_target
165 .by_distance(0.0, true)
166 .index
167 .min(self.scroll_target.max_index());
168 if next != self.index_current {
169 let source_snap = self.index_current;
170 self.index_previous = source_snap;
171 self.index_current = next;
172 Some(SelectEvent {
173 target_snap: next,
174 source_snap,
175 })
176 } else {
177 None
178 }
179 }
180
181 #[inline]
182 fn sync_target_vector(&mut self) {
183 self.scroll_target
184 .set_target_vector(self.scroll_body.target());
185 }
186
187 pub fn constrain_bounds(&mut self, pointer_down: bool) {
188 self.scroll_bounds
189 .constrain(self.limit, &mut self.scroll_body, pointer_down);
190 }
191
192 fn apply_target(&mut self, target: Target) -> Option<SelectEvent> {
193 let source_snap = self.index_current;
194 if target.distance != 0.0 {
195 self.scroll_body.add_target(target.distance);
196 }
197 self.sync_target_vector();
198
199 if target.index != source_snap {
200 self.index_previous = source_snap;
201 self.index_current = target.index;
202 Some(SelectEvent {
203 target_snap: target.index,
204 source_snap,
205 })
206 } else {
207 None
208 }
209 }
210
211 pub fn tick(&mut self, pointer_down: bool) {
213 self.scroll_body.seek();
214 self.constrain_bounds(pointer_down);
215 self.normalize_loop_entities();
216 self.sync_target_vector();
217 }
218
219 pub fn normalize_loop_entities(&mut self) {
224 if !self.config.loop_enabled {
225 return;
226 }
227 if self.limit.length == 0.0 {
228 return;
229 }
230
231 let location = self.scroll_body.location();
232 let wrapped = self.limit.remove_offset(location);
233 let delta = wrapped - location;
234 if delta == 0.0 {
235 return;
236 }
237
238 self.scroll_body.add_loop_distance(delta);
239 self.sync_target_vector();
240 }
241
242 pub fn scroll_to_distance(
243 &mut self,
244 distance: f32,
245 snap_to_closest: bool,
246 ) -> Option<SelectEvent> {
247 self.sync_target_vector();
248 let target = self.scroll_target.by_distance(distance, snap_to_closest);
249 self.apply_target(target)
250 }
251
252 pub fn scroll_to_index(&mut self, index: usize, direction: Direction) -> Option<SelectEvent> {
253 self.sync_target_vector();
254 let target = self.scroll_target.by_index(index, direction);
255 self.apply_target(target)
256 }
257
258 pub fn scroll_to_next(&mut self) -> Option<SelectEvent> {
259 let max = self.scroll_target.max_index();
260 let next = if self.config.loop_enabled {
261 (self.index_current + 1) % (max + 1).max(1)
262 } else {
263 (self.index_current + 1).min(max)
264 };
265 self.scroll_to_index(next, DIRECTION_NONE)
266 }
267
268 pub fn scroll_to_prev(&mut self) -> Option<SelectEvent> {
269 let max = self.scroll_target.max_index();
270 let prev = if self.config.loop_enabled {
271 if max == 0 {
272 0
273 } else if self.index_current == 0 {
274 max
275 } else {
276 self.index_current - 1
277 }
278 } else {
279 self.index_current.saturating_sub(1)
280 };
281 self.scroll_to_index(prev, DIRECTION_NONE)
282 }
283
284 pub fn on_drag_release(
289 &mut self,
290 pointer_kind: PointerKind,
291 pointer_delta: f32,
292 direction: impl Fn(f32) -> f32,
293 ) -> (DragReleaseResult, Option<SelectEvent>) {
294 self.sync_target_vector();
295 let cfg = DragReleaseConfig {
296 drag_free: self.config.drag_free,
297 skip_snaps: self.config.skip_snaps,
298 view_size: self.config.view_size,
299 base_friction: self.config.base_friction,
300 };
301
302 let out = compute_release(
303 cfg,
304 pointer_kind,
305 &self.scroll_target,
306 self.index_current,
307 pointer_delta,
308 direction,
309 );
310
311 self.scroll_body
312 .use_duration(out.duration)
313 .use_friction(out.friction);
314 let ev = self.scroll_to_distance(out.force, !self.config.drag_free);
315 (out, ev)
316 }
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322 use crate::embla::drag_release::PointerKind;
323
324 #[test]
325 fn scroll_to_distance_emits_select_when_index_changes() {
326 let snaps = vec![0.0, -100.0, -200.0, -300.0];
327 let mut engine = Engine::new(
328 snaps,
329 300.0,
330 EngineConfig {
331 loop_enabled: false,
332 drag_free: false,
333 skip_snaps: false,
334 duration: 25.0,
335 base_friction: 0.68,
336 view_size: 320.0,
337 start_snap: 0,
338 },
339 );
340
341 let ev = engine
342 .scroll_to_distance(-130.0, true)
343 .expect("select event");
344 assert_eq!(ev.source_snap, 0);
345 assert_eq!(ev.target_snap, 1);
346 assert_eq!(engine.index_current, 1);
347 }
348
349 #[test]
350 fn drag_release_shapes_duration_and_friction() {
351 let snaps = vec![0.0, -100.0, -200.0, -300.0];
352 let mut engine = Engine::new(
353 snaps,
354 300.0,
355 EngineConfig {
356 loop_enabled: false,
357 drag_free: false,
358 skip_snaps: false,
359 duration: 25.0,
360 base_friction: 0.68,
361 view_size: 320.0,
362 start_snap: 0,
363 },
364 );
365
366 let (release, _ev) = engine.on_drag_release(PointerKind::Mouse, -0.25, |v| v);
367 assert!(release.duration <= 25.0);
368 assert!(release.friction >= 0.68);
369 }
370
371 #[test]
372 fn reinit_updates_limit_and_keeps_location() {
373 let snaps = vec![0.0, -100.0, -200.0, -300.0];
374 let mut engine = Engine::new(
375 snaps,
376 300.0,
377 EngineConfig {
378 loop_enabled: false,
379 drag_free: false,
380 skip_snaps: false,
381 duration: 25.0,
382 base_friction: 0.68,
383 view_size: 320.0,
384 start_snap: 0,
385 },
386 );
387
388 engine.scroll_body.set_location(-250.0);
389 engine.scroll_body.set_target(-250.0);
390
391 let ev = engine.reinit(vec![0.0, -120.0, -240.0], 240.0, 320.0);
392 assert!(ev.is_some());
393 assert_eq!(engine.config.view_size, 320.0);
394 assert_eq!(engine.scroll_body.location(), -240.0);
395 assert_eq!(engine.scroll_body.target(), -240.0);
396 assert_eq!(engine.index_current, 2);
397 }
398
399 #[test]
400 fn loop_normalization_wraps_location_without_resetting_motion() {
401 let snaps = vec![0.0, -100.0, -200.0, -300.0, -400.0];
403 let mut engine = Engine::new(
404 snaps,
405 500.0,
406 EngineConfig {
407 loop_enabled: true,
408 drag_free: false,
409 skip_snaps: false,
410 duration: 25.0,
411 base_friction: 0.9,
412 view_size: 100.0,
413 start_snap: 0,
414 },
415 );
416
417 engine.scroll_body.set_location(20.0);
419 engine.scroll_body.set_target(20.0);
420 engine.scroll_target.set_target_vector(20.0);
421
422 engine.normalize_loop_entities();
423
424 assert!(
425 engine.scroll_body.location() <= engine.limit.max,
426 "expected wrapped location within max bound; loc={}",
427 engine.scroll_body.location()
428 );
429 assert!(
430 engine.scroll_body.location() >= engine.limit.min,
431 "expected wrapped location within min bound; loc={}",
432 engine.scroll_body.location()
433 );
434 }
435
436 #[test]
437 fn loop_scroll_to_next_wraps_selection_index() {
438 let snaps = vec![0.0, -100.0, -200.0, -300.0, -400.0];
439 let mut engine = Engine::new(
440 snaps,
441 500.0,
442 EngineConfig {
443 loop_enabled: true,
444 drag_free: false,
445 skip_snaps: false,
446 duration: 0.0,
447 base_friction: 0.9,
448 view_size: 100.0,
449 start_snap: 0,
450 },
451 );
452
453 for _ in 0..5 {
454 let _ = engine.scroll_to_next();
455 }
456
457 assert_eq!(engine.index_current, 0);
458 }
459
460 #[test]
461 fn loop_normalization_wraps_large_offsets_into_limit_range() {
462 let snaps = vec![0.0, -100.0, -200.0, -300.0, -400.0];
463 let mut engine = Engine::new(
464 snaps,
465 500.0,
466 EngineConfig {
467 loop_enabled: true,
468 drag_free: false,
469 skip_snaps: false,
470 duration: 25.0,
471 base_friction: 0.9,
472 view_size: 100.0,
473 start_snap: 0,
474 },
475 );
476
477 engine.scroll_body.set_location(1020.0);
479 engine.scroll_body.set_target(980.0);
480 engine.scroll_target.set_target_vector(980.0);
481 let displacement_before = engine.scroll_body.target() - engine.scroll_body.location();
482 engine.normalize_loop_entities();
483
484 let loc = engine.scroll_body.location();
485 assert!(loc <= engine.limit.max && loc >= engine.limit.min);
486 let displacement_after = engine.scroll_body.target() - engine.scroll_body.location();
490 assert!((displacement_before - displacement_after).abs() <= 0.0001);
491 }
492
493 #[test]
494 fn loop_normalization_is_idempotent() {
495 let snaps = vec![0.0, -100.0, -200.0, -300.0, -400.0];
496 let mut engine = Engine::new(
497 snaps,
498 500.0,
499 EngineConfig {
500 loop_enabled: true,
501 drag_free: false,
502 skip_snaps: false,
503 duration: 25.0,
504 base_friction: 0.9,
505 view_size: 100.0,
506 start_snap: 0,
507 },
508 );
509
510 engine.scroll_body.set_location(-980.0);
511 engine.scroll_body.set_target(-980.0);
512 engine.scroll_target.set_target_vector(-980.0);
513 engine.normalize_loop_entities();
514
515 let first = engine.scroll_body.snapshot();
516 engine.normalize_loop_entities();
517 let second = engine.scroll_body.snapshot();
518
519 assert!((first.location - second.location).abs() <= 0.0001);
520 assert!((first.target - second.target).abs() <= 0.0001);
521 assert!((first.previous_location - second.previous_location).abs() <= 0.0001);
522 }
523
524 #[test]
525 fn loop_normalization_preserves_scroll_velocity() {
526 let snaps = vec![0.0, -100.0, -200.0, -300.0, -400.0];
527 let mut engine = Engine::new(
528 snaps,
529 500.0,
530 EngineConfig {
531 loop_enabled: true,
532 drag_free: false,
533 skip_snaps: false,
534 duration: 25.0,
535 base_friction: 0.9,
536 view_size: 100.0,
537 start_snap: 0,
538 },
539 );
540
541 engine.scroll_body.set_target(200.0);
543 engine.scroll_body.seek();
544 engine
545 .scroll_target
546 .set_target_vector(engine.scroll_body.target());
547 let before = engine.scroll_body.snapshot();
548 assert!(before.velocity != 0.0);
549
550 engine.normalize_loop_entities();
551 let after = engine.scroll_body.snapshot();
552
553 assert!((before.velocity - after.velocity).abs() <= 0.0001);
554 assert!(after.location <= engine.limit.max && after.location >= engine.limit.min);
555 assert!(after.target <= engine.limit.max && after.target >= engine.limit.min);
556 }
557}