1use fret_core::{Axis, LayoutDirection, Point, Px};
7
8use crate::snap_points as headless_snap_points;
9
10pub const DEFAULT_DRAG_THRESHOLD_PX: f32 = 10.0;
11pub const DEFAULT_SNAP_THRESHOLD_FRACTION: f32 = 0.25;
12pub const DEFAULT_TOUCH_SCROLL_LOCK_THRESHOLD_PX: f32 = 2.0;
13pub const DEFAULT_SCROLL_CONTAIN_PIXEL_TOLERANCE_PX: f32 = 2.0;
14
15#[derive(Debug, Clone, Copy, PartialEq)]
16pub struct CarouselDragConfig {
17 pub drag_threshold_px: f32,
18 pub snap_threshold_fraction: f32,
19 pub touch_prevent_scroll: bool,
20 pub touch_scroll_lock_threshold_px: f32,
21}
22
23impl Default for CarouselDragConfig {
24 fn default() -> Self {
25 Self {
26 drag_threshold_px: DEFAULT_DRAG_THRESHOLD_PX,
27 snap_threshold_fraction: DEFAULT_SNAP_THRESHOLD_FRACTION,
28 touch_prevent_scroll: true,
29 touch_scroll_lock_threshold_px: DEFAULT_TOUCH_SCROLL_LOCK_THRESHOLD_PX,
30 }
31 }
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum CarouselDragInputKind {
36 Mouse,
37 Touch,
38}
39
40#[derive(Debug, Clone, Copy, Default, PartialEq)]
41pub struct CarouselDragState {
42 pub armed: bool,
43 pub dragging: bool,
44 pub start: Point,
45 pub start_offset: Px,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq)]
49pub struct CarouselDragMoveOutput {
50 pub steal_capture: bool,
51 pub consumed: bool,
52 pub next_offset: Option<Px>,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq)]
56pub struct CarouselDragReleaseOutput {
57 pub next_index: usize,
58 pub target_offset: Px,
59}
60
61#[inline]
62fn axis_delta(axis: Axis, from: Point, to: Point) -> f32 {
63 match axis {
64 Axis::Horizontal => to.x.0 - from.x.0,
65 Axis::Vertical => to.y.0 - from.y.0,
66 }
67}
68
69#[inline]
70fn axis_direction_sign(axis: Axis, direction: LayoutDirection) -> f32 {
71 match (axis, direction) {
72 (Axis::Horizontal, LayoutDirection::Rtl) => -1.0,
73 _ => 1.0,
74 }
75}
76
77#[inline]
78fn axis_delta_with_direction(
79 axis: Axis,
80 direction: LayoutDirection,
81 from: Point,
82 to: Point,
83) -> f32 {
84 axis_delta(axis, from, to) * axis_direction_sign(axis, direction)
85}
86
87#[inline]
88fn cross_axis(axis: Axis) -> Axis {
89 match axis {
90 Axis::Horizontal => Axis::Vertical,
91 Axis::Vertical => Axis::Horizontal,
92 }
93}
94
95pub fn on_pointer_down(
96 state: &mut CarouselDragState,
97 button_left: bool,
98 position: Point,
99 start_offset: Px,
100) {
101 if !button_left {
102 return;
103 }
104
105 state.armed = true;
106 state.dragging = false;
107 state.start = position;
108 state.start_offset = start_offset;
109}
110
111pub fn on_pointer_move(
112 config: CarouselDragConfig,
113 state: &mut CarouselDragState,
114 axis: Axis,
115 direction: LayoutDirection,
116 position: Point,
117 buttons_left: bool,
118 input_kind: CarouselDragInputKind,
119 max_offset: Px,
120) -> CarouselDragMoveOutput {
121 if !state.armed && !state.dragging {
122 return CarouselDragMoveOutput {
123 steal_capture: false,
124 consumed: false,
125 next_offset: None,
126 };
127 }
128
129 if !buttons_left {
130 *state = CarouselDragState::default();
131 return CarouselDragMoveOutput {
132 steal_capture: false,
133 consumed: false,
134 next_offset: None,
135 };
136 }
137
138 if config.touch_prevent_scroll
139 && input_kind == CarouselDragInputKind::Touch
140 && state.armed
141 && !state.dragging
142 {
143 let primary_abs = axis_delta_with_direction(axis, direction, state.start, position).abs();
144 let cross_abs = axis_delta(cross_axis(axis), state.start, position).abs();
145 if primary_abs.max(cross_abs) >= config.touch_scroll_lock_threshold_px
146 && primary_abs <= cross_abs
147 {
148 *state = CarouselDragState::default();
149 return CarouselDragMoveOutput {
150 steal_capture: false,
151 consumed: false,
152 next_offset: None,
153 };
154 }
155 }
156
157 let delta = axis_delta_with_direction(axis, direction, state.start, position);
158 if !state.dragging && state.armed && delta.abs() <= config.drag_threshold_px {
161 return CarouselDragMoveOutput {
162 steal_capture: false,
163 consumed: false,
164 next_offset: None,
165 };
166 }
167
168 let mut steal_capture = false;
169 if !state.dragging && state.armed {
170 steal_capture = true;
171 state.armed = false;
172 state.dragging = true;
173 }
174
175 let next = Px((state.start_offset.0 - delta).clamp(0.0, max_offset.0));
176 CarouselDragMoveOutput {
177 steal_capture,
178 consumed: true,
179 next_offset: Some(next),
180 }
181}
182
183pub fn on_pointer_cancel(state: &mut CarouselDragState) -> bool {
184 if !state.armed && !state.dragging {
185 return false;
186 }
187
188 *state = CarouselDragState::default();
189 true
190}
191
192pub fn on_pointer_up(
193 config: CarouselDragConfig,
194 state: &mut CarouselDragState,
195 axis: Axis,
196 direction: LayoutDirection,
197 position: Point,
198 extent: Px,
199 items_len: usize,
200) -> Option<CarouselDragReleaseOutput> {
201 if !state.dragging {
202 state.armed = false;
203 state.dragging = false;
204 return None;
205 }
206
207 let max_index = items_len.saturating_sub(1);
208 let start_index = if extent.0 > 0.0 {
209 (state.start_offset.0 / extent.0)
210 .round()
211 .clamp(0.0, max_index as f32) as usize
212 } else {
213 0
214 };
215
216 let delta = axis_delta_with_direction(axis, direction, state.start, position);
217 let mut next_index = start_index;
218 if extent.0 > 0.0 {
219 let threshold = extent.0 * config.snap_threshold_fraction;
220 if delta.abs() > threshold {
221 if delta > 0.0 {
222 next_index = start_index.saturating_sub(1);
223 } else {
224 next_index = (start_index + 1).min(max_index);
225 }
226 }
227 }
228
229 let target_offset = if extent.0 > 0.0 {
230 Px((next_index as f32) * extent.0)
231 } else {
232 Px(0.0)
233 };
234
235 *state = CarouselDragState::default();
236 Some(CarouselDragReleaseOutput {
237 next_index,
238 target_offset,
239 })
240}
241
242pub fn on_pointer_up_with_snaps(
243 config: CarouselDragConfig,
244 state: &mut CarouselDragState,
245 axis: Axis,
246 direction: LayoutDirection,
247 position: Point,
248 snaps: &[Px],
249) -> Option<CarouselDragReleaseOutput> {
250 if !state.dragging {
251 state.armed = false;
252 state.dragging = false;
253 return None;
254 }
255
256 if snaps.is_empty() {
257 *state = CarouselDragState::default();
258 return Some(CarouselDragReleaseOutput {
259 next_index: 0,
260 target_offset: Px(0.0),
261 });
262 }
263
264 let start_offset = state.start_offset;
265 let (start_index, start_snap) = snaps
266 .iter()
267 .copied()
268 .enumerate()
269 .min_by(|(_, a), (_, b)| {
270 (a.0 - start_offset.0)
271 .abs()
272 .total_cmp(&(b.0 - start_offset.0).abs())
273 })
274 .expect("non-empty snaps");
275
276 let delta = axis_delta_with_direction(axis, direction, state.start, position);
277 let mut next_index = start_index;
278
279 if snaps.len() > 1 {
280 let neighbor = if delta > 0.0 {
281 start_index.checked_sub(1)
282 } else if delta < 0.0 {
283 Some((start_index + 1).min(snaps.len().saturating_sub(1)))
284 } else {
285 None
286 };
287
288 if let Some(neighbor_index) = neighbor {
289 let neighbor_snap = snaps[neighbor_index];
290 let distance = (neighbor_snap.0 - start_snap.0).abs();
291 let threshold = distance * config.snap_threshold_fraction;
292 if distance > 0.0 && delta.abs() > threshold {
293 next_index = neighbor_index;
294 }
295 }
296 }
297
298 let target_offset = snaps[next_index];
299
300 *state = CarouselDragState::default();
301 Some(CarouselDragReleaseOutput {
302 next_index,
303 target_offset,
304 })
305}
306
307pub fn on_pointer_up_with_snaps_options(
308 config: CarouselDragConfig,
309 state: &mut CarouselDragState,
310 axis: Axis,
311 direction: LayoutDirection,
312 position: Point,
313 snaps: &[Px],
314 max_offset: Px,
315 loop_enabled: bool,
316 skip_snaps: bool,
317 drag_free: bool,
318) -> Option<CarouselDragReleaseOutput> {
319 if !state.dragging {
320 state.armed = false;
321 state.dragging = false;
322 return None;
323 }
324
325 if snaps.is_empty() {
326 *state = CarouselDragState::default();
327 return Some(CarouselDragReleaseOutput {
328 next_index: 0,
329 target_offset: Px(0.0),
330 });
331 }
332
333 let start_offset = state.start_offset;
334 let start_index = headless_snap_points::closest_index_px(snaps, start_offset).unwrap_or(0);
335 let start_index = start_index.min(snaps.len().saturating_sub(1));
336 let start_snap = snaps[start_index];
337
338 let delta = axis_delta_with_direction(axis, direction, state.start, position);
339 let projected_offset = Px(start_offset.0 - delta);
340 let projected_offset = clamp_px(projected_offset, Px(0.0), max_offset);
341 let projected_offset = round_3(projected_offset);
342
343 let (next_index, target_offset) = if drag_free {
344 let ix =
345 headless_snap_points::closest_index_px(snaps, projected_offset).unwrap_or(start_index);
346 (ix.min(snaps.len().saturating_sub(1)), projected_offset)
347 } else if skip_snaps {
348 let ix =
349 headless_snap_points::closest_index_px(snaps, projected_offset).unwrap_or(start_index);
350 let ix = ix.min(snaps.len().saturating_sub(1));
351 (ix, snaps[ix])
352 } else {
353 let mut next_index = start_index;
354 if snaps.len() > 1 {
355 let neighbor = if delta > 0.0 {
356 if loop_enabled {
357 headless_snap_points::step_index_wrapped(snaps.len(), start_index, -1)
358 } else {
359 start_index.checked_sub(1)
360 }
361 } else if delta < 0.0 {
362 if loop_enabled {
363 headless_snap_points::step_index_wrapped(snaps.len(), start_index, 1)
364 } else {
365 Some((start_index + 1).min(snaps.len().saturating_sub(1)))
366 }
367 } else {
368 None
369 };
370
371 if let Some(neighbor_index) = neighbor {
372 let neighbor_snap = snaps[neighbor_index];
373 let distance = (neighbor_snap.0 - start_snap.0).abs();
374 let threshold = distance * config.snap_threshold_fraction;
375 if distance > 0.0 && delta.abs() > threshold {
376 next_index = neighbor_index;
377 }
378 }
379 }
380
381 (next_index, snaps[next_index])
382 };
383
384 *state = CarouselDragState::default();
385 Some(CarouselDragReleaseOutput {
386 next_index,
387 target_offset,
388 })
389}
390
391#[derive(Debug, Clone, Copy, PartialEq, Eq)]
396pub enum CarouselSlidesToScrollOption {
397 Auto,
398 Fixed(usize),
399}
400
401#[derive(Debug, Clone, Copy)]
402pub enum CarouselSnapAlign {
403 Start,
404 Center,
405 End,
406 Custom(fn(view_size: Px, snap_size: Px, index: usize) -> Px),
407}
408
409impl CarouselSnapAlign {
410 fn measure(self, view_size: Px, snap_size: Px, index: usize) -> Px {
411 match self {
412 CarouselSnapAlign::Start => Px(0.0),
413 CarouselSnapAlign::Center => Px((view_size.0 - snap_size.0) / 2.0),
414 CarouselSnapAlign::End => Px(view_size.0 - snap_size.0),
415 CarouselSnapAlign::Custom(f) => f(view_size, snap_size, index),
416 }
417 }
418}
419
420#[derive(Debug, Clone, Copy, PartialEq, Eq)]
421pub enum CarouselContainScroll {
422 KeepSnaps,
423 TrimSnaps,
424}
425
426#[derive(Debug, Clone, Copy, PartialEq, Eq)]
427pub enum CarouselContainScrollOption {
428 None,
429 KeepSnaps,
430 TrimSnaps,
431}
432
433#[derive(Debug, Clone, Copy, PartialEq)]
434pub struct CarouselContainScrollConfig {
435 pub contain_scroll: CarouselContainScroll,
436 pub pixel_tolerance_px: f32,
437}
438
439impl Default for CarouselContainScrollConfig {
440 fn default() -> Self {
441 Self {
442 contain_scroll: CarouselContainScroll::TrimSnaps,
443 pixel_tolerance_px: DEFAULT_SCROLL_CONTAIN_PIXEL_TOLERANCE_PX,
444 }
445 }
446}
447
448#[derive(Debug, Clone, Copy, PartialEq)]
449pub struct CarouselSlide1D {
450 pub start: Px,
451 pub size: Px,
452}
453
454impl CarouselSlide1D {
455 #[inline]
456 pub fn end(self) -> Px {
457 Px(self.start.0 + self.size.0)
458 }
459}
460
461#[inline]
462fn round_3(px: Px) -> Px {
463 Px((px.0 * 1000.0).round() / 1000.0)
464}
465
466#[inline]
467fn clamp_px(v: Px, min: Px, max: Px) -> Px {
468 Px(v.0.clamp(min.0, max.0))
469}
470
471#[derive(Debug, Clone, PartialEq)]
472pub struct CarouselSnapModel1D {
473 pub snaps_px: Vec<Px>,
474 pub slides_by_snap: Vec<Vec<usize>>,
475 pub snap_by_slide: Vec<usize>,
476 pub max_offset_px: Px,
477}
478
479fn group_by_number<T: Clone>(items: &[T], group_size: usize) -> Vec<Vec<T>> {
480 let group_size = group_size.max(1);
481 let mut groups = Vec::new();
482 let mut i = 0usize;
483 while i < items.len() {
484 groups.push(items[i..items.len().min(i + group_size)].to_vec());
485 i += group_size;
486 }
487 groups
488}
489
490fn group_slide_indexes_auto(
491 view_size: Px,
492 slides: &[CarouselSlide1D],
493 start_gap: Px,
494 end_gap: Px,
495 loop_enabled: bool,
496 pixel_tolerance_px: f32,
497) -> Vec<Vec<usize>> {
498 if slides.is_empty() {
499 return Vec::new();
500 }
501
502 let mut boundaries: Vec<usize> = Vec::new();
503 for (index, rect_b) in (0..slides.len()).enumerate() {
504 let rect_a = boundaries.last().copied().unwrap_or(0);
505 let is_first = rect_a == 0;
506 let is_last = rect_b + 1 == slides.len();
507
508 let a = slides[rect_a];
509 let b = slides[rect_b];
510 let gap_a = if !loop_enabled && is_first {
511 start_gap
512 } else {
513 Px(0.0)
514 };
515 let gap_b = if !loop_enabled && is_last {
516 end_gap
517 } else {
518 Px(0.0)
519 };
520
521 let chunk_size = Px((b.end().0 + gap_b.0) - (a.start.0 + gap_a.0)).0.abs();
522
523 if index != 0 && chunk_size > view_size.0 + pixel_tolerance_px {
524 boundaries.push(rect_b);
525 }
526 if is_last {
527 boundaries.push(slides.len());
528 }
529 }
530
531 let mut groups = Vec::new();
532 for (index, &end) in boundaries.iter().enumerate() {
533 let start = boundaries.get(index.wrapping_sub(1)).copied().unwrap_or(0);
534 let start = start.min(end);
535 groups.push((start..end).collect::<Vec<_>>());
536 }
537 groups
538}
539
540fn group_slide_indexes(
541 view_size: Px,
542 slides: &[CarouselSlide1D],
543 slides_to_scroll: CarouselSlidesToScrollOption,
544 start_gap: Px,
545 end_gap: Px,
546 loop_enabled: bool,
547 pixel_tolerance_px: f32,
548) -> Vec<Vec<usize>> {
549 match slides_to_scroll {
550 CarouselSlidesToScrollOption::Fixed(n) => {
551 group_by_number(&(0..slides.len()).collect::<Vec<_>>(), n)
552 }
553 CarouselSlidesToScrollOption::Auto => group_slide_indexes_auto(
554 view_size,
555 slides,
556 start_gap,
557 end_gap,
558 loop_enabled,
559 pixel_tolerance_px,
560 ),
561 }
562}
563
564fn array_from_range(end: usize, start: usize) -> Vec<usize> {
565 (start..=end).collect()
566}
567
568pub fn snap_model_1d(
605 view_size: Px,
606 slides: &[CarouselSlide1D],
607 start_gap: Px,
608 end_gap: Px,
609 slides_to_scroll: CarouselSlidesToScrollOption,
610 loop_enabled: bool,
611 align: CarouselSnapAlign,
612 contain_scroll: CarouselContainScrollOption,
613 pixel_tolerance_px: f32,
614) -> CarouselSnapModel1D {
615 let slide_count = slides.len();
616 if slide_count == 0 {
617 return CarouselSnapModel1D {
618 snaps_px: vec![Px(0.0)],
619 slides_by_snap: vec![Vec::new()],
620 snap_by_slide: Vec::new(),
621 max_offset_px: Px(0.0),
622 };
623 }
624
625 let content_size = slides
626 .iter()
627 .map(|s| s.end())
628 .fold(Px(0.0), |a, b| Px(a.0.max(b.0)));
629 let content_size = Px(content_size.0 + end_gap.0.max(0.0));
630 let max_offset_px = Px((content_size.0 - view_size.0).max(0.0));
631
632 if content_size.0 <= view_size.0 + pixel_tolerance_px {
633 let all = (0..slide_count).collect::<Vec<_>>();
634 return CarouselSnapModel1D {
635 snaps_px: vec![Px(0.0)],
636 slides_by_snap: vec![all.clone()],
637 snap_by_slide: vec![0; slide_count],
638 max_offset_px,
639 };
640 }
641
642 let contain_snaps =
643 !loop_enabled && !matches!(contain_scroll, CarouselContainScrollOption::None);
644
645 let slide_groups = group_slide_indexes(
646 view_size,
647 slides,
648 slides_to_scroll,
649 start_gap,
650 end_gap,
651 loop_enabled,
652 pixel_tolerance_px,
653 );
654
655 let mut group_sizes = Vec::with_capacity(slide_groups.len());
656 for group in &slide_groups {
657 if group.is_empty() {
658 group_sizes.push(Px(0.0));
659 continue;
660 }
661 let first = slides[group[0]];
662 let last = slides[*group.last().expect("group last")];
663 group_sizes.push(Px((last.end().0 - first.start.0).abs()));
664 }
665
666 let mut alignments = Vec::with_capacity(group_sizes.len());
667 for (i, size) in group_sizes.iter().copied().enumerate() {
668 alignments.push(align.measure(view_size, size, i));
669 }
670
671 let snaps_unaligned = slides.iter().map(|s| s.start).collect::<Vec<_>>();
672 let mut snaps_aligned = Vec::with_capacity(slide_groups.len());
673 for (group_index, group) in slide_groups.iter().enumerate() {
674 let first_slide_ix = group.first().copied().unwrap_or(0);
675 let snap = snaps_unaligned[first_slide_ix];
676 snaps_aligned.push(Px(snap.0 - alignments[group_index].0));
677 }
678
679 let (snaps_px, contain_limit_min, contain_limit_max) = if contain_snaps {
680 let mut snaps_bounded = Vec::with_capacity(snaps_aligned.len());
681 for (i, snap) in snaps_aligned.iter().copied().enumerate() {
682 let is_first = i == 0;
683 let is_last = i + 1 == snaps_aligned.len();
684 if is_first {
685 snaps_bounded.push(Px(0.0));
686 continue;
687 }
688 if is_last {
689 snaps_bounded.push(max_offset_px);
690 continue;
691 }
692
693 let mut clamped = clamp_px(snap, Px(0.0), max_offset_px);
694 if pixel_tolerance_px > 0.0 {
695 if (clamped.0 - 0.0).abs() <= 1.0 {
696 clamped = Px(0.0);
697 } else if (clamped.0 - max_offset_px.0).abs() <= 1.0 {
698 clamped = max_offset_px;
699 }
700 }
701 snaps_bounded.push(round_3(clamped));
702 }
703
704 let start_snap = snaps_bounded[0];
705 let end_snap = *snaps_bounded.last().expect("snaps_bounded non-empty");
706 let mut min_ix = 0usize;
707 for (i, snap) in snaps_bounded.iter().copied().enumerate() {
708 if snap == start_snap {
709 min_ix = i;
710 }
711 }
712 let mut max_ix = snaps_bounded.len();
713 for (i, snap) in snaps_bounded.iter().copied().enumerate() {
714 if snap == end_snap {
715 max_ix = i + 1;
716 break;
717 }
718 }
719
720 let snaps_contained = match contain_scroll {
721 CarouselContainScrollOption::KeepSnaps => snaps_bounded.clone(),
722 CarouselContainScrollOption::TrimSnaps => snaps_bounded[min_ix..max_ix].to_vec(),
723 CarouselContainScrollOption::None => snaps_bounded.clone(),
724 };
725 (snaps_contained, min_ix, max_ix)
726 } else {
727 (snaps_aligned, 0usize, slide_groups.len())
728 };
729
730 let mut slides_by_snap = if snaps_px.len() == 1 {
731 vec![(0..slide_count).collect::<Vec<_>>()]
732 } else if !contain_snaps || matches!(contain_scroll, CarouselContainScrollOption::KeepSnaps) {
733 slide_groups.clone()
734 } else {
735 let groups = slide_groups[contain_limit_min..contain_limit_max].to_vec();
736 groups
737 .iter()
738 .enumerate()
739 .map(|(index, group)| {
740 let is_first = index == 0;
741 let is_last = index + 1 == groups.len();
742 if is_first {
743 let range_end = *group.last().expect("first group last");
744 return array_from_range(range_end, 0);
745 }
746 if is_last {
747 let range_end = slide_count - 1;
748 return array_from_range(range_end, group[0]);
749 }
750 group.clone()
751 })
752 .collect::<Vec<_>>()
753 };
754
755 if slides_by_snap.is_empty() {
756 slides_by_snap.push((0..slide_count).collect::<Vec<_>>());
757 }
758
759 let mut snap_by_slide = vec![0usize; slide_count];
760 for (snap_index, group) in slides_by_snap.iter().enumerate() {
761 for &slide_index in group {
762 if slide_index < snap_by_slide.len() {
763 snap_by_slide[slide_index] = snap_index;
764 }
765 }
766 }
767
768 CarouselSnapModel1D {
769 snaps_px,
770 slides_by_snap,
771 snap_by_slide,
772 max_offset_px,
773 }
774}
775
776pub fn contained_scroll_snaps_1d(
782 view_size: Px,
783 slides: &[CarouselSlide1D],
784 align: CarouselSnapAlign,
785 config: CarouselContainScrollConfig,
786) -> Vec<Px> {
787 contained_scroll_snaps_1d_with_end_gap(view_size, slides, Px(0.0), align, config)
788}
789
790pub fn contained_scroll_snaps_1d_with_end_gap(
792 view_size: Px,
793 slides: &[CarouselSlide1D],
794 end_gap: Px,
795 align: CarouselSnapAlign,
796 config: CarouselContainScrollConfig,
797) -> Vec<Px> {
798 let model = snap_model_1d(
799 view_size,
800 slides,
801 Px(0.0),
802 end_gap,
803 CarouselSlidesToScrollOption::Fixed(1),
804 false,
805 align,
806 match config.contain_scroll {
807 CarouselContainScroll::KeepSnaps => CarouselContainScrollOption::KeepSnaps,
808 CarouselContainScroll::TrimSnaps => CarouselContainScrollOption::TrimSnaps,
809 },
810 config.pixel_tolerance_px,
811 );
812 model.snaps_px
813}
814
815#[cfg(test)]
816mod tests {
817 use super::*;
818
819 #[test]
820 fn drag_threshold_arms_then_starts_drag() {
821 let mut state = CarouselDragState::default();
822 on_pointer_down(&mut state, true, Point::new(Px(0.0), Px(0.0)), Px(0.0));
823 assert!(state.armed);
824 assert!(!state.dragging);
825
826 let out = on_pointer_move(
827 CarouselDragConfig::default(),
828 &mut state,
829 Axis::Horizontal,
830 LayoutDirection::Ltr,
831 Point::new(Px(9.0), Px(0.0)),
832 true,
833 CarouselDragInputKind::Mouse,
834 Px(100.0),
835 );
836 assert_eq!(
837 out,
838 CarouselDragMoveOutput {
839 steal_capture: false,
840 consumed: false,
841 next_offset: None
842 }
843 );
844
845 let out = on_pointer_move(
846 CarouselDragConfig::default(),
847 &mut state,
848 Axis::Horizontal,
849 LayoutDirection::Ltr,
850 Point::new(Px(10.0), Px(0.0)),
851 true,
852 CarouselDragInputKind::Mouse,
853 Px(100.0),
854 );
855 assert_eq!(
856 out,
857 CarouselDragMoveOutput {
858 steal_capture: false,
859 consumed: false,
860 next_offset: None
861 }
862 );
863
864 let out = on_pointer_move(
865 CarouselDragConfig::default(),
866 &mut state,
867 Axis::Horizontal,
868 LayoutDirection::Ltr,
869 Point::new(Px(11.0), Px(0.0)),
870 true,
871 CarouselDragInputKind::Mouse,
872 Px(100.0),
873 );
874 assert!(out.steal_capture);
875 assert!(out.consumed);
876 assert!(out.next_offset.is_some());
877 assert!(!state.armed);
878 assert!(state.dragging);
879 }
880
881 #[test]
882 fn move_clamps_offset_to_bounds() {
883 let mut state = CarouselDragState::default();
884 on_pointer_down(&mut state, true, Point::new(Px(0.0), Px(0.0)), Px(50.0));
885 let out = on_pointer_move(
886 CarouselDragConfig::default(),
887 &mut state,
888 Axis::Horizontal,
889 LayoutDirection::Ltr,
890 Point::new(Px(-50.0), Px(0.0)),
891 true,
892 CarouselDragInputKind::Mouse,
893 Px(60.0),
894 );
895 assert_eq!(out.next_offset, Some(Px(60.0)));
896 }
897
898 #[test]
899 fn move_mirrors_horizontal_delta_in_rtl() {
900 let mut state = CarouselDragState::default();
901 on_pointer_down(&mut state, true, Point::new(Px(0.0), Px(0.0)), Px(50.0));
902
903 let out = on_pointer_move(
905 CarouselDragConfig {
906 drag_threshold_px: 0.0,
907 ..Default::default()
908 },
909 &mut state,
910 Axis::Horizontal,
911 LayoutDirection::Rtl,
912 Point::new(Px(20.0), Px(0.0)),
913 true,
914 CarouselDragInputKind::Mouse,
915 Px(100.0),
916 );
917 assert_eq!(out.next_offset, Some(Px(70.0)));
918 }
919
920 #[test]
921 fn release_snaps_by_fractional_threshold() {
922 let mut state = CarouselDragState::default();
923 on_pointer_down(&mut state, true, Point::new(Px(0.0), Px(0.0)), Px(100.0));
924 let _ = on_pointer_move(
925 CarouselDragConfig::default(),
926 &mut state,
927 Axis::Horizontal,
928 LayoutDirection::Ltr,
929 Point::new(Px(-20.0), Px(0.0)),
930 true,
931 CarouselDragInputKind::Mouse,
932 Px(400.0),
933 );
934
935 let release = on_pointer_up(
936 CarouselDragConfig::default(),
937 &mut state,
938 Axis::Horizontal,
939 LayoutDirection::Ltr,
940 Point::new(Px(-30.0), Px(0.0)),
941 Px(100.0),
942 5,
943 )
944 .expect("release");
945 assert_eq!(release.next_index, 2usize);
946 assert_eq!(release.target_offset, Px(200.0));
947 }
948
949 #[test]
950 fn release_snaps_by_fractional_threshold_with_snaps() {
951 let mut state = CarouselDragState::default();
952 on_pointer_down(&mut state, true, Point::new(Px(0.0), Px(0.0)), Px(100.0));
953
954 let _ = on_pointer_move(
955 CarouselDragConfig {
956 drag_threshold_px: 0.0,
957 ..Default::default()
958 },
959 &mut state,
960 Axis::Horizontal,
961 LayoutDirection::Ltr,
962 Point::new(Px(-40.0), Px(0.0)),
963 true,
964 CarouselDragInputKind::Mouse,
965 Px(200.0),
966 );
967
968 let snaps = [Px(0.0), Px(100.0), Px(180.0)];
969 let release = on_pointer_up_with_snaps(
970 CarouselDragConfig {
971 drag_threshold_px: 0.0,
972 snap_threshold_fraction: 0.3,
973 ..Default::default()
974 },
975 &mut state,
976 Axis::Horizontal,
977 LayoutDirection::Ltr,
978 Point::new(Px(-40.0), Px(0.0)),
979 &snaps,
980 )
981 .expect("release");
982
983 assert_eq!(release.next_index, 2usize);
984 assert_eq!(release.target_offset, Px(180.0));
985 }
986
987 #[test]
988 fn release_with_skip_snaps_allows_skipping_multiple_snaps() {
989 let mut state = CarouselDragState::default();
990 on_pointer_down(&mut state, true, Point::new(Px(0.0), Px(0.0)), Px(100.0));
991
992 let _ = on_pointer_move(
993 CarouselDragConfig {
994 drag_threshold_px: 0.0,
995 ..Default::default()
996 },
997 &mut state,
998 Axis::Horizontal,
999 LayoutDirection::Ltr,
1000 Point::new(Px(-12.0), Px(0.0)),
1001 true,
1002 CarouselDragInputKind::Mouse,
1003 Px(300.0),
1004 );
1005
1006 let snaps = [Px(0.0), Px(100.0), Px(200.0), Px(300.0)];
1007
1008 let release_neighbor_only = on_pointer_up_with_snaps_options(
1009 CarouselDragConfig {
1010 drag_threshold_px: 0.0,
1011 ..Default::default()
1012 },
1013 &mut state,
1014 Axis::Horizontal,
1015 LayoutDirection::Ltr,
1016 Point::new(Px(-240.0), Px(0.0)),
1017 &snaps,
1018 Px(300.0),
1019 false,
1020 false,
1021 false,
1022 )
1023 .expect("release");
1024
1025 assert_eq!(release_neighbor_only.next_index, 2);
1026 assert_eq!(release_neighbor_only.target_offset, Px(200.0));
1027
1028 on_pointer_down(&mut state, true, Point::new(Px(0.0), Px(0.0)), Px(100.0));
1030 let _ = on_pointer_move(
1031 CarouselDragConfig {
1032 drag_threshold_px: 0.0,
1033 ..Default::default()
1034 },
1035 &mut state,
1036 Axis::Horizontal,
1037 LayoutDirection::Ltr,
1038 Point::new(Px(-12.0), Px(0.0)),
1039 true,
1040 CarouselDragInputKind::Mouse,
1041 Px(300.0),
1042 );
1043
1044 let release_skipping = on_pointer_up_with_snaps_options(
1045 CarouselDragConfig {
1046 drag_threshold_px: 0.0,
1047 ..Default::default()
1048 },
1049 &mut state,
1050 Axis::Horizontal,
1051 LayoutDirection::Ltr,
1052 Point::new(Px(-240.0), Px(0.0)),
1053 &snaps,
1054 Px(300.0),
1055 false,
1056 true,
1057 false,
1058 )
1059 .expect("release");
1060
1061 assert_eq!(release_skipping.next_index, 3);
1062 assert_eq!(release_skipping.target_offset, Px(300.0));
1063 }
1064
1065 #[test]
1066 fn release_with_drag_free_settles_to_projected_offset() {
1067 let mut state = CarouselDragState::default();
1068 on_pointer_down(&mut state, true, Point::new(Px(0.0), Px(0.0)), Px(100.0));
1069
1070 let _ = on_pointer_move(
1071 CarouselDragConfig {
1072 drag_threshold_px: 0.0,
1073 ..Default::default()
1074 },
1075 &mut state,
1076 Axis::Horizontal,
1077 LayoutDirection::Ltr,
1078 Point::new(Px(-12.0), Px(0.0)),
1079 true,
1080 CarouselDragInputKind::Mouse,
1081 Px(300.0),
1082 );
1083
1084 let snaps = [Px(0.0), Px(100.0), Px(200.0), Px(300.0)];
1085
1086 let release = on_pointer_up_with_snaps_options(
1087 CarouselDragConfig {
1088 drag_threshold_px: 0.0,
1089 ..Default::default()
1090 },
1091 &mut state,
1092 Axis::Horizontal,
1093 LayoutDirection::Ltr,
1094 Point::new(Px(-160.0), Px(0.0)),
1095 &snaps,
1096 Px(300.0),
1097 false,
1098 false,
1099 true,
1100 )
1101 .expect("release");
1102
1103 assert_eq!(release.next_index, 3);
1104 assert_eq!(release.target_offset, Px(260.0));
1105 }
1106
1107 #[test]
1108 fn touch_cross_axis_movement_cancels_armed_drag() {
1109 let mut state = CarouselDragState::default();
1110 on_pointer_down(&mut state, true, Point::new(Px(0.0), Px(0.0)), Px(0.0));
1111
1112 let out = on_pointer_move(
1113 CarouselDragConfig::default(),
1114 &mut state,
1115 Axis::Horizontal,
1116 LayoutDirection::Ltr,
1117 Point::new(Px(1.0), Px(5.0)),
1118 true,
1119 CarouselDragInputKind::Touch,
1120 Px(100.0),
1121 );
1122 assert_eq!(
1123 out,
1124 CarouselDragMoveOutput {
1125 steal_capture: false,
1126 consumed: false,
1127 next_offset: None
1128 }
1129 );
1130 assert_eq!(state, CarouselDragState::default());
1131 }
1132
1133 fn fixture_contain_scroll_ltr_1() -> (Px, Vec<CarouselSlide1D>) {
1134 let view = Px(1000.0);
1135 let slides = vec![
1136 CarouselSlide1D {
1137 start: Px(0.0),
1138 size: Px(100.0),
1139 },
1140 CarouselSlide1D {
1141 start: Px(100.0),
1142 size: Px(200.0),
1143 },
1144 CarouselSlide1D {
1145 start: Px(300.0),
1146 size: Px(150.0),
1147 },
1148 CarouselSlide1D {
1149 start: Px(450.0),
1150 size: Px(250.0),
1151 },
1152 CarouselSlide1D {
1153 start: Px(700.0),
1154 size: Px(130.0),
1155 },
1156 CarouselSlide1D {
1157 start: Px(830.0),
1158 size: Px(100.0),
1159 },
1160 CarouselSlide1D {
1161 start: Px(930.0),
1162 size: Px(200.0),
1163 },
1164 CarouselSlide1D {
1165 start: Px(1130.0),
1166 size: Px(150.0),
1167 },
1168 CarouselSlide1D {
1169 start: Px(1280.0),
1170 size: Px(250.0),
1171 },
1172 CarouselSlide1D {
1173 start: Px(1530.0),
1174 size: Px(130.0),
1175 },
1176 ];
1177 (view, slides)
1178 }
1179
1180 fn align_10pct(view_size: Px, _snap_size: Px, _index: usize) -> Px {
1181 Px(view_size.0 * 0.1)
1182 }
1183
1184 #[test]
1185 fn snap_model_short_circuits_when_content_fits_view_with_tolerance() {
1186 let view = Px(100.0);
1187 let slides = vec![
1188 CarouselSlide1D {
1189 start: Px(0.0),
1190 size: Px(50.0),
1191 },
1192 CarouselSlide1D {
1193 start: Px(50.0),
1194 size: Px(50.0),
1195 },
1196 ];
1197
1198 let model = snap_model_1d(
1200 view,
1201 &slides,
1202 Px(0.0),
1203 Px(0.0),
1204 CarouselSlidesToScrollOption::Fixed(1),
1205 false,
1206 CarouselSnapAlign::Start,
1207 CarouselContainScrollOption::TrimSnaps,
1208 0.0,
1209 );
1210 assert_eq!(model.snaps_px, vec![Px(0.0)]);
1211 assert_eq!(model.slides_by_snap, vec![vec![0, 1]]);
1212 assert_eq!(model.snap_by_slide, vec![0, 0]);
1213 assert_eq!(model.max_offset_px, Px(0.0));
1214
1215 let model = snap_model_1d(
1217 view,
1218 &slides,
1219 Px(0.0),
1220 Px(0.5),
1221 CarouselSlidesToScrollOption::Fixed(1),
1222 false,
1223 CarouselSnapAlign::Start,
1224 CarouselContainScrollOption::TrimSnaps,
1225 1.0,
1226 );
1227 assert_eq!(model.snaps_px, vec![Px(0.0)]);
1228 assert_eq!(model.slides_by_snap, vec![vec![0, 1]]);
1229 assert_eq!(model.snap_by_slide, vec![0, 0]);
1230 assert_eq!(model.max_offset_px, Px(0.5));
1231 }
1232
1233 #[test]
1234 fn snap_model_fixed_slides_to_scroll_groups_slides_by_n() {
1235 let view = Px(150.0);
1236 let slides = (0..5)
1237 .map(|i| CarouselSlide1D {
1238 start: Px((i as f32) * 100.0),
1239 size: Px(100.0),
1240 })
1241 .collect::<Vec<_>>();
1242
1243 let model = snap_model_1d(
1244 view,
1245 &slides,
1246 Px(0.0),
1247 Px(0.0),
1248 CarouselSlidesToScrollOption::Fixed(2),
1249 false,
1250 CarouselSnapAlign::Start,
1251 CarouselContainScrollOption::None,
1252 0.0,
1253 );
1254
1255 assert_eq!(model.snaps_px, vec![Px(0.0), Px(200.0), Px(400.0)]);
1256 assert_eq!(model.slides_by_snap, vec![vec![0, 1], vec![2, 3], vec![4]]);
1257 assert_eq!(model.snap_by_slide, vec![0, 0, 1, 1, 2]);
1258 assert_eq!(model.max_offset_px, Px(350.0));
1259 }
1260
1261 #[test]
1262 fn snap_model_auto_slides_to_scroll_groups_by_view_size() {
1263 let view = Px(100.0);
1265 let slides = (0..3)
1266 .map(|i| CarouselSlide1D {
1267 start: Px((i as f32) * 40.0),
1268 size: Px(40.0),
1269 })
1270 .collect::<Vec<_>>();
1271
1272 let model = snap_model_1d(
1273 view,
1274 &slides,
1275 Px(0.0),
1276 Px(0.0),
1277 CarouselSlidesToScrollOption::Auto,
1278 false,
1279 CarouselSnapAlign::Start,
1280 CarouselContainScrollOption::None,
1281 0.0,
1282 );
1283
1284 assert_eq!(model.snaps_px, vec![Px(0.0), Px(80.0)]);
1285 assert_eq!(model.slides_by_snap, vec![vec![0, 1], vec![2]]);
1286 assert_eq!(model.snap_by_slide, vec![0, 0, 1]);
1287 assert_eq!(model.max_offset_px, Px(20.0));
1288 }
1289
1290 fn fixture_keep_trim_none_1() -> (Px, Vec<CarouselSlide1D>) {
1291 let view = Px(250.0);
1292 let slides = vec![
1293 CarouselSlide1D {
1294 start: Px(0.0),
1295 size: Px(100.0),
1296 },
1297 CarouselSlide1D {
1298 start: Px(100.0),
1299 size: Px(100.0),
1300 },
1301 CarouselSlide1D {
1302 start: Px(200.0),
1303 size: Px(100.0),
1304 },
1305 CarouselSlide1D {
1306 start: Px(300.0),
1307 size: Px(100.0),
1308 },
1309 ];
1310 (view, slides)
1311 }
1312
1313 #[test]
1314 fn snap_model_contain_scroll_keep_snaps_preserves_count_and_duplicates() {
1315 let (view, slides) = fixture_keep_trim_none_1();
1316 let model = snap_model_1d(
1317 view,
1318 &slides,
1319 Px(0.0),
1320 Px(0.0),
1321 CarouselSlidesToScrollOption::Fixed(1),
1322 false,
1323 CarouselSnapAlign::Start,
1324 CarouselContainScrollOption::KeepSnaps,
1325 0.0,
1326 );
1327
1328 assert_eq!(
1330 model.snaps_px,
1331 vec![Px(0.0), Px(100.0), Px(150.0), Px(150.0)]
1332 );
1333 assert_eq!(
1334 model.slides_by_snap,
1335 vec![vec![0], vec![1], vec![2], vec![3]]
1336 );
1337 assert_eq!(model.snap_by_slide, vec![0, 1, 2, 3]);
1338 assert_eq!(model.max_offset_px, Px(150.0));
1339 }
1340
1341 #[test]
1342 fn snap_model_contain_scroll_trim_snaps_trims_and_expands_edge_groups() {
1343 let (view, slides) = fixture_keep_trim_none_1();
1344 let model = snap_model_1d(
1345 view,
1346 &slides,
1347 Px(0.0),
1348 Px(0.0),
1349 CarouselSlidesToScrollOption::Fixed(1),
1350 false,
1351 CarouselSnapAlign::Start,
1352 CarouselContainScrollOption::TrimSnaps,
1353 0.0,
1354 );
1355
1356 assert_eq!(model.snaps_px, vec![Px(0.0), Px(100.0), Px(150.0)]);
1357 assert_eq!(model.slides_by_snap, vec![vec![0], vec![1], vec![2, 3]]);
1358 assert_eq!(model.snap_by_slide, vec![0, 1, 2, 2]);
1359 assert_eq!(model.max_offset_px, Px(150.0));
1360 }
1361
1362 #[test]
1363 fn snap_model_contain_scroll_none_does_not_clamp_snaps() {
1364 let (view, slides) = fixture_keep_trim_none_1();
1365 let model = snap_model_1d(
1366 view,
1367 &slides,
1368 Px(0.0),
1369 Px(0.0),
1370 CarouselSlidesToScrollOption::Fixed(1),
1371 false,
1372 CarouselSnapAlign::Start,
1373 CarouselContainScrollOption::None,
1374 0.0,
1375 );
1376
1377 assert_eq!(
1378 model.snaps_px,
1379 vec![Px(0.0), Px(100.0), Px(200.0), Px(300.0)]
1380 );
1381 assert_eq!(
1382 model.slides_by_snap,
1383 vec![vec![0], vec![1], vec![2], vec![3]]
1384 );
1385 assert_eq!(model.snap_by_slide, vec![0, 1, 2, 3]);
1386 assert_eq!(model.max_offset_px, Px(150.0));
1387 }
1388
1389 #[test]
1390 fn embla_fixture_ltr_1_trim_snaps_align_start_matches() {
1391 let (view, slides) = fixture_contain_scroll_ltr_1();
1392 let snaps = contained_scroll_snaps_1d(
1393 view,
1394 &slides,
1395 CarouselSnapAlign::Start,
1396 CarouselContainScrollConfig::default(),
1397 );
1398 assert_eq!(
1399 snaps,
1400 vec![Px(0.0), Px(100.0), Px(300.0), Px(450.0), Px(660.0)]
1401 );
1402 }
1403
1404 #[test]
1405 fn embla_fixture_ltr_1_trim_snaps_align_center_matches() {
1406 let (view, slides) = fixture_contain_scroll_ltr_1();
1407 let snaps = contained_scroll_snaps_1d(
1408 view,
1409 &slides,
1410 CarouselSnapAlign::Center,
1411 CarouselContainScrollConfig::default(),
1412 );
1413 assert_eq!(
1414 snaps,
1415 vec![
1416 Px(0.0),
1417 Px(75.0),
1418 Px(265.0),
1419 Px(380.0),
1420 Px(530.0),
1421 Px(660.0)
1422 ]
1423 );
1424 }
1425
1426 #[test]
1427 fn embla_fixture_ltr_1_trim_snaps_align_end_matches() {
1428 let (view, slides) = fixture_contain_scroll_ltr_1();
1429 let snaps = contained_scroll_snaps_1d(
1430 view,
1431 &slides,
1432 CarouselSnapAlign::End,
1433 CarouselContainScrollConfig::default(),
1434 );
1435 assert_eq!(
1436 snaps,
1437 vec![Px(0.0), Px(130.0), Px(280.0), Px(530.0), Px(660.0)]
1438 );
1439 }
1440
1441 #[test]
1442 fn embla_fixture_ltr_1_trim_snaps_align_custom_matches() {
1443 let (view, slides) = fixture_contain_scroll_ltr_1();
1444 let snaps = contained_scroll_snaps_1d(
1445 view,
1446 &slides,
1447 CarouselSnapAlign::Custom(align_10pct),
1448 CarouselContainScrollConfig::default(),
1449 );
1450 assert_eq!(
1451 snaps,
1452 vec![Px(0.0), Px(200.0), Px(350.0), Px(600.0), Px(660.0)]
1453 );
1454 }
1455
1456 #[test]
1457 fn embla_fixture_content_size_within_pixel_tolerance_collapses_to_zero() {
1458 let view = Px(1000.0);
1459 let slides = vec![
1460 CarouselSlide1D {
1461 start: Px(0.0),
1462 size: Px(501.0),
1463 },
1464 CarouselSlide1D {
1465 start: Px(501.0),
1466 size: Px(501.0),
1467 },
1468 ];
1469
1470 let snaps = contained_scroll_snaps_1d(
1471 view,
1472 &slides,
1473 CarouselSnapAlign::Center,
1474 CarouselContainScrollConfig::default(),
1475 );
1476 assert_eq!(snaps, vec![Px(0.0)]);
1477 }
1478
1479 #[test]
1480 fn embla_fixture_content_size_just_outside_pixel_tolerance_keeps_small_edge_snap() {
1481 let view = Px(1000.0);
1482 let slides = vec![
1483 CarouselSlide1D {
1484 start: Px(0.0),
1485 size: Px(502.0),
1486 },
1487 CarouselSlide1D {
1488 start: Px(502.0),
1489 size: Px(501.0),
1490 },
1491 ];
1492
1493 let snaps = contained_scroll_snaps_1d(
1494 view,
1495 &slides,
1496 CarouselSnapAlign::Center,
1497 CarouselContainScrollConfig::default(),
1498 );
1499 assert_eq!(snaps, vec![Px(0.0), Px(3.0)]);
1500 }
1501
1502 fn fixture_contain_scroll_ltr_2() -> (Px, Px, Vec<CarouselSlide1D>) {
1503 let view = Px(1000.0);
1504 let end_gap = Px(10.0);
1505 let slides = vec![
1506 CarouselSlide1D {
1507 start: Px(10.0),
1508 size: Px(100.0),
1509 },
1510 CarouselSlide1D {
1511 start: Px(130.0),
1512 size: Px(200.0),
1513 },
1514 CarouselSlide1D {
1515 start: Px(350.0),
1516 size: Px(150.0),
1517 },
1518 CarouselSlide1D {
1519 start: Px(520.0),
1520 size: Px(250.0),
1521 },
1522 CarouselSlide1D {
1523 start: Px(790.0),
1524 size: Px(130.0),
1525 },
1526 CarouselSlide1D {
1527 start: Px(940.0),
1528 size: Px(100.0),
1529 },
1530 CarouselSlide1D {
1531 start: Px(1060.0),
1532 size: Px(200.0),
1533 },
1534 CarouselSlide1D {
1535 start: Px(1280.0),
1536 size: Px(150.0),
1537 },
1538 CarouselSlide1D {
1539 start: Px(1450.0),
1540 size: Px(250.0),
1541 },
1542 CarouselSlide1D {
1543 start: Px(1720.0),
1544 size: Px(130.0),
1545 },
1546 ];
1547 (view, end_gap, slides)
1548 }
1549
1550 #[test]
1551 fn embla_fixture_ltr_2_trim_snaps_align_start_matches() {
1552 let (view, end_gap, slides) = fixture_contain_scroll_ltr_2();
1553 let snaps = contained_scroll_snaps_1d_with_end_gap(
1554 view,
1555 &slides,
1556 end_gap,
1557 CarouselSnapAlign::Start,
1558 CarouselContainScrollConfig::default(),
1559 );
1560 assert_eq!(
1561 snaps,
1562 vec![
1563 Px(0.0),
1564 Px(130.0),
1565 Px(350.0),
1566 Px(520.0),
1567 Px(790.0),
1568 Px(860.0)
1569 ]
1570 );
1571 }
1572
1573 #[test]
1574 fn embla_fixture_ltr_2_trim_snaps_align_center_matches() {
1575 let (view, end_gap, slides) = fixture_contain_scroll_ltr_2();
1576 let snaps = contained_scroll_snaps_1d_with_end_gap(
1577 view,
1578 &slides,
1579 end_gap,
1580 CarouselSnapAlign::Center,
1581 CarouselContainScrollConfig::default(),
1582 );
1583 assert_eq!(
1584 snaps,
1585 vec![
1586 Px(0.0),
1587 Px(145.0),
1588 Px(355.0),
1589 Px(490.0),
1590 Px(660.0),
1591 Px(855.0),
1592 Px(860.0)
1593 ]
1594 );
1595 }
1596
1597 #[test]
1598 fn embla_fixture_ltr_2_trim_snaps_align_end_matches() {
1599 let (view, end_gap, slides) = fixture_contain_scroll_ltr_2();
1600 let snaps = contained_scroll_snaps_1d_with_end_gap(
1601 view,
1602 &slides,
1603 end_gap,
1604 CarouselSnapAlign::End,
1605 CarouselContainScrollConfig::default(),
1606 );
1607 assert_eq!(
1608 snaps,
1609 vec![
1610 Px(0.0),
1611 Px(40.0),
1612 Px(260.0),
1613 Px(430.0),
1614 Px(700.0),
1615 Px(860.0)
1616 ]
1617 );
1618 }
1619
1620 #[test]
1621 fn embla_fixture_ltr_2_trim_snaps_align_custom_matches() {
1622 let (view, end_gap, slides) = fixture_contain_scroll_ltr_2();
1623 let snaps = contained_scroll_snaps_1d_with_end_gap(
1624 view,
1625 &slides,
1626 end_gap,
1627 CarouselSnapAlign::Custom(align_10pct),
1628 CarouselContainScrollConfig::default(),
1629 );
1630 assert_eq!(
1631 snaps,
1632 vec![
1633 Px(0.0),
1634 Px(30.0),
1635 Px(250.0),
1636 Px(420.0),
1637 Px(690.0),
1638 Px(840.0),
1639 Px(860.0)
1640 ]
1641 );
1642 }
1643}