1use super::{inspector_metadata, Modifier, Point, PointerEventKind};
18use crate::current_density;
19use crate::fling_animation::FlingAnimation;
20use crate::fling_animation::MIN_FLING_VELOCITY;
21use crate::render_state::schedule_modifier_slices_repass;
22use crate::scroll::{
23 scroll_motion_context_for_key, ScrollElement, ScrollMotionContext, ScrollMotionContextKey,
24 ScrollState,
25};
26use cranpose_core::{current_runtime_handle, NodeId};
27use cranpose_foundation::{
28 velocity_tracker::ASSUME_STOPPED_MS, DelegatableNode, ModifierNode, ModifierNodeElement,
29 NodeCapabilities, NodeState, PointerButton, PointerButtons, VelocityTracker1D, DRAG_THRESHOLD,
30 MAX_FLING_VELOCITY,
31};
32use std::cell::RefCell;
33use std::rc::Rc;
34use web_time::Instant;
35
36#[cfg(feature = "test-helpers")]
41mod test_velocity_tracking {
42 use std::sync::atomic::{AtomicU32, Ordering};
43
44 static LAST_FLING_VELOCITY: AtomicU32 = AtomicU32::new(0);
54
55 pub fn last_fling_velocity() -> f32 {
60 f32::from_bits(LAST_FLING_VELOCITY.load(Ordering::SeqCst))
61 }
62
63 pub fn reset_last_fling_velocity() {
67 LAST_FLING_VELOCITY.store(0.0f32.to_bits(), Ordering::SeqCst);
68 }
69
70 pub(super) fn set_last_fling_velocity(velocity: f32) {
72 LAST_FLING_VELOCITY.store(velocity.to_bits(), Ordering::SeqCst);
73 }
74}
75
76#[cfg(feature = "test-helpers")]
77pub use test_velocity_tracking::{last_fling_velocity, reset_last_fling_velocity};
78
79#[inline]
82fn set_last_fling_velocity(velocity: f32) {
83 #[cfg(feature = "test-helpers")]
84 test_velocity_tracking::set_last_fling_velocity(velocity);
85 #[cfg(not(feature = "test-helpers"))]
86 let _ = velocity; }
88
89struct ScrollGestureState {
95 drag_down_position: Option<Point>,
98
99 last_position: Option<Point>,
102
103 is_dragging: bool,
107
108 velocity_tracker: VelocityTracker1D,
110
111 gesture_start_time: Option<Instant>,
113
114 last_velocity_sample_ms: Option<i64>,
116
117 fling_animation: Option<FlingAnimation>,
119}
120
121impl Default for ScrollGestureState {
122 fn default() -> Self {
123 Self {
124 drag_down_position: None,
125 last_position: None,
126 is_dragging: false,
127 velocity_tracker: VelocityTracker1D::new(),
128 gesture_start_time: None,
129 last_velocity_sample_ms: None,
130 fling_animation: None,
131 }
132 }
133}
134
135#[inline]
144fn calculate_total_delta(from: Point, to: Point, is_vertical: bool) -> f32 {
145 if is_vertical {
146 to.y - from.y
147 } else {
148 to.x - from.x
149 }
150}
151
152#[inline]
157fn calculate_incremental_delta(from: Point, to: Point, is_vertical: bool) -> f32 {
158 if is_vertical {
159 to.y - from.y
160 } else {
161 to.x - from.x
162 }
163}
164
165trait ScrollTarget: Clone {
173 fn apply_delta(&self, delta: f32) -> f32;
175
176 fn apply_fling_delta(&self, delta: f32) -> f32;
178
179 fn invalidate(&self);
181
182 fn current_offset(&self) -> f32;
184}
185
186impl ScrollTarget for ScrollState {
187 fn apply_delta(&self, delta: f32) -> f32 {
188 self.dispatch_raw_delta(-delta)
190 }
191
192 fn apply_fling_delta(&self, delta: f32) -> f32 {
193 self.dispatch_raw_delta(delta)
194 }
195
196 fn invalidate(&self) {
197 }
199
200 fn current_offset(&self) -> f32 {
201 self.value()
202 }
203}
204
205impl ScrollTarget for LazyListState {
206 fn apply_delta(&self, delta: f32) -> f32 {
207 self.dispatch_scroll_delta(delta)
211 }
212
213 fn apply_fling_delta(&self, delta: f32) -> f32 {
214 -self.dispatch_scroll_delta(-delta)
215 }
216
217 fn invalidate(&self) {
218 }
223
224 fn current_offset(&self) -> f32 {
225 self.first_visible_item_scroll_offset()
227 }
228}
229
230struct ScrollGestureDetector<S: ScrollTarget> {
236 gesture_state: Rc<RefCell<ScrollGestureState>>,
238
239 scroll_target: S,
241
242 is_vertical: bool,
244
245 reverse_scrolling: bool,
247
248 motion_context: ScrollMotionContext,
250}
251
252impl<S: ScrollTarget + 'static> ScrollGestureDetector<S> {
253 fn new(
255 gesture_state: Rc<RefCell<ScrollGestureState>>,
256 scroll_target: S,
257 is_vertical: bool,
258 reverse_scrolling: bool,
259 motion_context: ScrollMotionContext,
260 ) -> Self {
261 Self {
262 gesture_state,
263 scroll_target,
264 is_vertical,
265 reverse_scrolling,
266 motion_context,
267 }
268 }
269
270 fn on_down(&self, position: Point) -> bool {
279 let mut gs = self.gesture_state.borrow_mut();
280
281 if let Some(fling) = gs.fling_animation.take() {
283 fling.cancel();
284 }
285 self.motion_context.set_active(false);
286
287 gs.drag_down_position = Some(position);
288 gs.last_position = Some(position);
289 gs.is_dragging = false;
290 gs.velocity_tracker.reset();
291 gs.gesture_start_time = Some(Instant::now());
292
293 let pos = if self.is_vertical {
295 position.y
296 } else {
297 position.x
298 };
299 gs.velocity_tracker.add_data_point(0, pos);
300 gs.last_velocity_sample_ms = Some(0);
301
302 false
304 }
305
306 fn on_move(&self, position: Point, buttons: PointerButtons) -> bool {
317 let mut gs = self.gesture_state.borrow_mut();
318
319 if !buttons.contains(PointerButton::Primary) && gs.drag_down_position.is_some() {
321 gs.drag_down_position = None;
322 gs.last_position = None;
323 gs.is_dragging = false;
324 gs.gesture_start_time = None;
325 gs.last_velocity_sample_ms = None;
326 gs.velocity_tracker.reset();
327 self.motion_context.set_active(false);
328 return false;
329 }
330
331 let Some(down_pos) = gs.drag_down_position else {
332 return false;
333 };
334
335 let Some(last_pos) = gs.last_position else {
336 gs.last_position = Some(position);
337 return false;
338 };
339
340 let total_delta = calculate_total_delta(down_pos, position, self.is_vertical);
341 let incremental_delta = calculate_incremental_delta(last_pos, position, self.is_vertical);
342
343 if !gs.is_dragging && total_delta.abs() > DRAG_THRESHOLD {
345 gs.is_dragging = true;
346 self.motion_context.set_active(true);
347 }
348
349 gs.last_position = Some(position);
350
351 if let Some(start_time) = gs.gesture_start_time {
353 let elapsed_ms = start_time.elapsed().as_millis() as i64;
354 let pos = if self.is_vertical {
355 position.y
356 } else {
357 position.x
358 };
359 let sample_ms = match gs.last_velocity_sample_ms {
362 Some(last_sample_ms) => {
363 let mut sample_ms = if elapsed_ms <= last_sample_ms {
364 last_sample_ms + 1
365 } else {
366 elapsed_ms
367 };
368 if sample_ms - last_sample_ms > ASSUME_STOPPED_MS {
370 sample_ms = last_sample_ms + ASSUME_STOPPED_MS;
371 }
372 sample_ms
373 }
374 None => elapsed_ms,
375 };
376 gs.velocity_tracker.add_data_point(sample_ms, pos);
377 gs.last_velocity_sample_ms = Some(sample_ms);
378 }
379
380 if gs.is_dragging {
381 drop(gs); let delta = if self.reverse_scrolling {
383 -incremental_delta
384 } else {
385 incremental_delta
386 };
387 let _ = self.scroll_target.apply_delta(delta);
388 self.scroll_target.invalidate();
389 true } else {
391 false
392 }
393 }
394
395 fn finish_gesture(&self, allow_fling: bool) -> bool {
402 let (was_dragging, velocity, start_fling, existing_fling) = {
403 let mut gs = self.gesture_state.borrow_mut();
404 let was_dragging = gs.is_dragging;
405 let mut velocity = 0.0;
406
407 if allow_fling && was_dragging && gs.gesture_start_time.is_some() {
408 velocity = gs
409 .velocity_tracker
410 .calculate_velocity_with_max(MAX_FLING_VELOCITY);
411 }
412
413 let start_fling = allow_fling && was_dragging && velocity.abs() > MIN_FLING_VELOCITY;
414 let existing_fling = if start_fling {
415 gs.fling_animation.take()
416 } else {
417 None
418 };
419
420 gs.drag_down_position = None;
421 gs.last_position = None;
422 gs.is_dragging = false;
423 gs.gesture_start_time = None;
424 gs.last_velocity_sample_ms = None;
425
426 (was_dragging, velocity, start_fling, existing_fling)
427 };
428
429 if allow_fling && was_dragging {
431 set_last_fling_velocity(velocity);
432 }
433
434 if start_fling {
436 if let Some(old_fling) = existing_fling {
437 old_fling.cancel();
438 }
439
440 if let Some(runtime) = current_runtime_handle() {
442 self.motion_context.set_active(true);
443 let scroll_target = self.scroll_target.clone();
444 let reverse = self.reverse_scrolling;
445 let fling = FlingAnimation::new(runtime);
446 let motion_context = self.motion_context.clone();
447
448 let initial_value = scroll_target.current_offset();
450
451 let adjusted_velocity = if reverse { -velocity } else { velocity };
453 let fling_velocity = -adjusted_velocity;
454
455 let scroll_target_for_fling = scroll_target.clone();
456 let scroll_target_for_end = scroll_target.clone();
457
458 fling.start_fling(
459 initial_value,
460 fling_velocity,
461 current_density(),
462 move |delta| {
463 let consumed = scroll_target_for_fling.apply_fling_delta(delta);
465 scroll_target_for_fling.invalidate();
466 consumed
467 },
468 move || {
469 scroll_target_for_end.invalidate();
471 motion_context.set_active(false);
472 },
473 );
474
475 let mut gs = self.gesture_state.borrow_mut();
476 gs.fling_animation = Some(fling);
477 }
478 } else {
479 self.motion_context.set_active(false);
480 }
481
482 was_dragging
483 }
484
485 fn on_up(&self) -> bool {
492 self.finish_gesture(true)
493 }
494
495 fn on_cancel(&self) -> bool {
499 self.finish_gesture(false)
500 }
501
502 fn on_scroll(&self, axis_delta: f32) -> bool {
506 if axis_delta.abs() <= f32::EPSILON {
507 return false;
508 }
509
510 {
511 let mut gs = self.gesture_state.borrow_mut();
513 if let Some(fling) = gs.fling_animation.take() {
514 fling.cancel();
515 }
516 gs.drag_down_position = None;
517 gs.last_position = None;
518 gs.is_dragging = false;
519 gs.gesture_start_time = None;
520 gs.last_velocity_sample_ms = None;
521 gs.velocity_tracker.reset();
522 }
523
524 self.motion_context.activate_for_next_frame();
525
526 let delta = if self.reverse_scrolling {
527 -axis_delta
528 } else {
529 axis_delta
530 };
531 let consumed = self.scroll_target.apply_delta(delta);
532 if consumed.abs() > 0.001 {
533 self.scroll_target.invalidate();
534 true
535 } else {
536 false
537 }
538 }
539}
540
541pub(crate) struct MotionContextAnimatedNode {
542 state: NodeState,
543 motion_context: ScrollMotionContext,
544 invalidation_callback_id: Option<u64>,
545 node_id: Option<NodeId>,
546}
547
548impl MotionContextAnimatedNode {
549 fn new(motion_context: ScrollMotionContext) -> Self {
550 Self {
551 state: NodeState::new(),
552 motion_context,
553 invalidation_callback_id: None,
554 node_id: None,
555 }
556 }
557
558 pub(crate) fn is_active(&self) -> bool {
559 self.motion_context.is_active()
560 }
561}
562
563pub(crate) struct TranslatedContentContextNode {
564 state: NodeState,
565 identity: usize,
566 offset_source: TranslatedContentOffsetSource,
567}
568
569impl TranslatedContentContextNode {
570 fn new(identity: usize, offset_source: TranslatedContentOffsetSource) -> Self {
571 Self {
572 state: NodeState::new(),
573 identity,
574 offset_source,
575 }
576 }
577
578 pub(crate) fn is_active(&self) -> bool {
579 true
580 }
581
582 pub(crate) fn identity(&self) -> usize {
583 self.identity
584 }
585
586 pub(crate) fn content_offset_reader(&self) -> Option<Rc<dyn Fn() -> Point>> {
587 self.offset_source.content_offset_reader()
588 }
589}
590
591impl DelegatableNode for TranslatedContentContextNode {
592 fn node_state(&self) -> &NodeState {
593 &self.state
594 }
595}
596
597impl ModifierNode for TranslatedContentContextNode {}
598
599impl DelegatableNode for MotionContextAnimatedNode {
600 fn node_state(&self) -> &NodeState {
601 &self.state
602 }
603}
604
605impl ModifierNode for MotionContextAnimatedNode {
606 fn on_attach(&mut self, context: &mut dyn cranpose_foundation::ModifierNodeContext) {
607 let node_id = context.node_id();
608 self.node_id = node_id;
609 if let Some(node_id) = node_id {
610 let callback_id = self
611 .motion_context
612 .add_invalidate_callback(Box::new(move || {
613 schedule_modifier_slices_repass(node_id);
614 }));
615 self.invalidation_callback_id = Some(callback_id);
616 }
617 }
618
619 fn on_detach(&mut self) {
620 if let Some(id) = self.invalidation_callback_id.take() {
621 self.motion_context.remove_invalidate_callback(id);
622 }
623 self.node_id = None;
624 }
625}
626
627#[derive(Clone)]
628struct MotionContextAnimatedElement {
629 motion_context: ScrollMotionContext,
630}
631
632impl MotionContextAnimatedElement {
633 fn new(motion_context: ScrollMotionContext) -> Self {
634 Self { motion_context }
635 }
636}
637
638impl std::fmt::Debug for MotionContextAnimatedElement {
639 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
640 f.debug_struct("MotionContextAnimatedElement").finish()
641 }
642}
643
644impl PartialEq for MotionContextAnimatedElement {
645 fn eq(&self, other: &Self) -> bool {
646 self.motion_context.ptr_eq(&other.motion_context)
647 }
648}
649
650impl Eq for MotionContextAnimatedElement {}
651
652impl std::hash::Hash for MotionContextAnimatedElement {
653 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
654 self.motion_context.stable_key().hash(state);
655 }
656}
657
658impl ModifierNodeElement for MotionContextAnimatedElement {
659 type Node = MotionContextAnimatedNode;
660
661 fn create(&self) -> Self::Node {
662 MotionContextAnimatedNode::new(self.motion_context.clone())
663 }
664
665 fn update(&self, node: &mut Self::Node) {
666 if node.motion_context.ptr_eq(&self.motion_context) {
667 return;
668 }
669 if let Some(id) = node.invalidation_callback_id.take() {
670 node.motion_context.remove_invalidate_callback(id);
671 }
672 node.motion_context = self.motion_context.clone();
673 if let Some(node_id) = node.node_id {
674 let callback_id = node
675 .motion_context
676 .add_invalidate_callback(Box::new(move || {
677 schedule_modifier_slices_repass(node_id);
678 }));
679 node.invalidation_callback_id = Some(callback_id);
680 }
681 }
682
683 fn capabilities(&self) -> NodeCapabilities {
684 NodeCapabilities::LAYOUT
685 }
686}
687
688#[derive(Clone)]
689enum TranslatedContentOffsetSource {
690 LayoutContentOffset,
691 LazyList {
692 state: LazyListState,
693 is_vertical: bool,
694 reverse_scrolling: bool,
695 },
696}
697
698impl TranslatedContentOffsetSource {
699 fn content_offset_reader(&self) -> Option<Rc<dyn Fn() -> Point>> {
700 match self {
701 Self::LayoutContentOffset => None,
702 Self::LazyList {
703 state, is_vertical, ..
704 } => Some(Rc::new(lazy_list_content_offset_reader(
705 *state,
706 *is_vertical,
707 ))),
708 }
709 }
710
711 fn is_vertical(&self) -> Option<bool> {
712 match self {
713 Self::LayoutContentOffset => None,
714 Self::LazyList { is_vertical, .. } => Some(*is_vertical),
715 }
716 }
717
718 fn reverse_scrolling(&self) -> Option<bool> {
719 match self {
720 Self::LayoutContentOffset => None,
721 Self::LazyList {
722 reverse_scrolling, ..
723 } => Some(*reverse_scrolling),
724 }
725 }
726}
727
728fn lazy_list_content_offset_reader(state: LazyListState, is_vertical: bool) -> impl Fn() -> Point {
729 move || {
730 let info = state.layout_info();
731 if info.visible_items_info.is_empty() {
732 return Point::default();
733 };
734 let main_offset = info.snap_anchor_offset;
735 if is_vertical {
736 Point::new(0.0, main_offset)
737 } else {
738 Point::new(main_offset, 0.0)
739 }
740 }
741}
742
743#[derive(Clone)]
744struct TranslatedContentContextElement {
745 identity: usize,
746 offset_source: TranslatedContentOffsetSource,
747}
748
749impl TranslatedContentContextElement {
750 fn new(identity: usize, offset_source: TranslatedContentOffsetSource) -> Self {
751 Self {
752 identity,
753 offset_source,
754 }
755 }
756}
757
758impl std::fmt::Debug for TranslatedContentContextElement {
759 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
760 let offset_source = match &self.offset_source {
761 TranslatedContentOffsetSource::LayoutContentOffset => "layout",
762 TranslatedContentOffsetSource::LazyList { .. } => "lazy_list",
763 };
764 f.debug_struct("TranslatedContentContextElement")
765 .field("identity", &self.identity)
766 .field("offset_source", &offset_source)
767 .finish()
768 }
769}
770
771impl PartialEq for TranslatedContentContextElement {
772 fn eq(&self, other: &Self) -> bool {
773 self.identity == other.identity
774 && self.offset_source.is_vertical() == other.offset_source.is_vertical()
775 && self.offset_source.reverse_scrolling() == other.offset_source.reverse_scrolling()
776 }
777}
778
779impl Eq for TranslatedContentContextElement {}
780
781impl std::hash::Hash for TranslatedContentContextElement {
782 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
783 self.identity.hash(state);
784 self.offset_source.is_vertical().hash(state);
785 self.offset_source.reverse_scrolling().hash(state);
786 }
787}
788
789impl ModifierNodeElement for TranslatedContentContextElement {
790 type Node = TranslatedContentContextNode;
791
792 fn create(&self) -> Self::Node {
793 TranslatedContentContextNode::new(self.identity, self.offset_source.clone())
794 }
795
796 fn update(&self, node: &mut Self::Node) {
797 node.identity = self.identity;
798 node.offset_source = self.offset_source.clone();
799 }
800
801 fn capabilities(&self) -> NodeCapabilities {
802 NodeCapabilities::LAYOUT
803 }
804}
805
806impl Modifier {
811 pub fn horizontal_scroll(self, state: ScrollState, reverse_scrolling: bool) -> Self {
829 self.then(scroll_impl(state, false, reverse_scrolling, None))
830 }
831
832 pub fn vertical_scroll(self, state: ScrollState, reverse_scrolling: bool) -> Self {
841 self.then(scroll_impl(state, true, reverse_scrolling, None))
842 }
843
844 pub fn horizontal_scroll_guarded(
846 self,
847 state: ScrollState,
848 reverse_scrolling: bool,
849 guard: impl Fn() -> bool + 'static,
850 ) -> Self {
851 self.then(scroll_impl(
852 state,
853 false,
854 reverse_scrolling,
855 Some(Rc::new(guard)),
856 ))
857 }
858
859 pub fn vertical_scroll_guarded(
861 self,
862 state: ScrollState,
863 reverse_scrolling: bool,
864 guard: impl Fn() -> bool + 'static,
865 ) -> Self {
866 self.then(scroll_impl(
867 state,
868 true,
869 reverse_scrolling,
870 Some(Rc::new(guard)),
871 ))
872 }
873}
874
875fn scroll_impl(
884 state: ScrollState,
885 is_vertical: bool,
886 reverse_scrolling: bool,
887 guard: Option<Rc<dyn Fn() -> bool>>,
888) -> Modifier {
889 let gesture_state = Rc::new(RefCell::new(ScrollGestureState::default()));
891 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::ScrollState {
892 state_id: state.id(),
893 is_vertical,
894 reverse_scrolling,
895 });
896
897 let scroll_state = state.clone();
899 let pointer_motion_context = motion_context.clone();
900 let key = (state.id(), is_vertical);
901 let pointer_input = Modifier::empty().pointer_input(key, move |scope| {
902 let detector = ScrollGestureDetector::new(
904 gesture_state.clone(),
905 scroll_state.clone(),
906 is_vertical,
907 false, pointer_motion_context.clone(),
909 );
910 let guard = guard.clone();
911
912 async move {
913 scope
914 .await_pointer_event_scope(|await_scope| async move {
915 loop {
917 let event = await_scope.await_pointer_event().await;
918
919 if let Some(ref guard) = guard {
920 if !guard() {
921 if matches!(
922 event.kind,
923 PointerEventKind::Up | PointerEventKind::Cancel
924 ) {
925 detector.on_cancel();
926 }
927 continue;
928 }
929 }
930
931 let should_consume = match event.kind {
933 PointerEventKind::Down => detector.on_down(event.position),
934 PointerEventKind::Move => {
935 detector.on_move(event.position, event.buttons)
936 }
937 PointerEventKind::Up => detector.on_up(),
938 PointerEventKind::Cancel => detector.on_cancel(),
939 PointerEventKind::Scroll => detector.on_scroll(if is_vertical {
940 event.scroll_delta.y
941 } else {
942 event.scroll_delta.x
943 }),
944 PointerEventKind::Enter | PointerEventKind::Exit => false,
945 };
946
947 if should_consume {
948 event.consume();
949 }
950 }
951 })
952 .await;
953 }
954 });
955
956 let element = ScrollElement::new(state.clone(), is_vertical, reverse_scrolling);
958 let layout_modifier =
959 Modifier::with_element(element).with_inspector_metadata(inspector_metadata(
960 if is_vertical {
961 "verticalScroll"
962 } else {
963 "horizontalScroll"
964 },
965 move |info| {
966 info.add_property("isVertical", is_vertical.to_string());
967 info.add_property("reverseScrolling", reverse_scrolling.to_string());
968 },
969 ));
970 let motion_modifier =
971 Modifier::with_element(MotionContextAnimatedElement::new(motion_context.clone()));
972 let translated_content_modifier = Modifier::with_element(TranslatedContentContextElement::new(
973 state.id() as usize,
974 TranslatedContentOffsetSource::LayoutContentOffset,
975 ));
976
977 pointer_input
979 .then(motion_modifier)
980 .then(translated_content_modifier)
981 .then(layout_modifier)
982 .clip_to_bounds()
983}
984
985use cranpose_foundation::lazy::LazyListState;
990
991impl Modifier {
992 pub fn lazy_vertical_scroll(self, state: LazyListState, reverse_scrolling: bool) -> Self {
1003 self.then(lazy_scroll_impl(state, true, reverse_scrolling))
1004 }
1005
1006 pub fn lazy_horizontal_scroll(self, state: LazyListState, reverse_scrolling: bool) -> Self {
1008 self.then(lazy_scroll_impl(state, false, reverse_scrolling))
1009 }
1010}
1011
1012fn lazy_scroll_impl(state: LazyListState, is_vertical: bool, reverse_scrolling: bool) -> Modifier {
1014 let gesture_state = Rc::new(RefCell::new(ScrollGestureState::default()));
1015 let list_state = state;
1016 let state_id = state.inner_ptr() as usize;
1017 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::LazyList {
1018 state_identity: state_id,
1019 is_vertical,
1020 reverse_scrolling,
1021 });
1022 let key = (state_id, is_vertical, reverse_scrolling);
1023 let translated_content_modifier = Modifier::with_element(TranslatedContentContextElement::new(
1024 state_id,
1025 TranslatedContentOffsetSource::LazyList {
1026 state,
1027 is_vertical,
1028 reverse_scrolling,
1029 },
1030 ));
1031
1032 Modifier::with_element(MotionContextAnimatedElement::new(motion_context.clone()))
1033 .then(translated_content_modifier)
1034 .pointer_input(key, move |scope| {
1035 let detector = ScrollGestureDetector::new(
1037 gesture_state.clone(),
1038 list_state,
1039 is_vertical,
1040 reverse_scrolling,
1041 motion_context.clone(),
1042 );
1043
1044 async move {
1045 scope
1046 .await_pointer_event_scope(|await_scope| async move {
1047 loop {
1048 let event = await_scope.await_pointer_event().await;
1049
1050 let should_consume = match event.kind {
1052 PointerEventKind::Down => detector.on_down(event.position),
1053 PointerEventKind::Move => {
1054 detector.on_move(event.position, event.buttons)
1055 }
1056 PointerEventKind::Up => detector.on_up(),
1057 PointerEventKind::Cancel => detector.on_cancel(),
1058 PointerEventKind::Scroll => detector.on_scroll(if is_vertical {
1059 event.scroll_delta.y
1060 } else {
1061 event.scroll_delta.x
1062 }),
1063 PointerEventKind::Enter | PointerEventKind::Exit => false,
1064 };
1065
1066 if should_consume {
1067 event.consume();
1068 }
1069 }
1070 })
1071 .await;
1072 }
1073 })
1074}