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