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, 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::{Cell, RefCell};
30use std::rc::Rc;
31use std::sync::atomic::{AtomicU64, Ordering};
32use web_time::Instant;
33
34#[cfg(feature = "test-helpers")]
39mod test_velocity_tracking {
40 use std::sync::atomic::{AtomicU32, Ordering};
41
42 static LAST_FLING_VELOCITY: AtomicU32 = AtomicU32::new(0);
52
53 pub fn last_fling_velocity() -> f32 {
58 f32::from_bits(LAST_FLING_VELOCITY.load(Ordering::SeqCst))
59 }
60
61 pub fn reset_last_fling_velocity() {
65 LAST_FLING_VELOCITY.store(0.0f32.to_bits(), Ordering::SeqCst);
66 }
67
68 pub(super) fn set_last_fling_velocity(velocity: f32) {
70 LAST_FLING_VELOCITY.store(velocity.to_bits(), Ordering::SeqCst);
71 }
72}
73
74#[cfg(feature = "test-helpers")]
75pub use test_velocity_tracking::{last_fling_velocity, reset_last_fling_velocity};
76
77#[inline]
80fn set_last_fling_velocity(velocity: f32) {
81 #[cfg(feature = "test-helpers")]
82 test_velocity_tracking::set_last_fling_velocity(velocity);
83 #[cfg(not(feature = "test-helpers"))]
84 let _ = velocity; }
86
87struct ScrollGestureState {
93 drag_down_position: Option<Point>,
96
97 last_position: Option<Point>,
100
101 is_dragging: bool,
105
106 velocity_tracker: VelocityTracker1D,
108
109 gesture_start_time: Option<Instant>,
111
112 last_velocity_sample_ms: Option<i64>,
114
115 fling_animation: Option<FlingAnimation>,
117}
118
119impl Default for ScrollGestureState {
120 fn default() -> Self {
121 Self {
122 drag_down_position: None,
123 last_position: None,
124 is_dragging: false,
125 velocity_tracker: VelocityTracker1D::new(),
126 gesture_start_time: None,
127 last_velocity_sample_ms: None,
128 fling_animation: None,
129 }
130 }
131}
132
133#[derive(Clone)]
134struct MotionContextState {
135 inner: Rc<MotionContextStateInner>,
136}
137
138struct MotionContextStateInner {
139 active: Cell<bool>,
140 generation: Cell<u64>,
141 invalidate_callbacks: RefCell<std::collections::HashMap<u64, Box<dyn Fn()>>>,
142 pending_invalidation: Cell<bool>,
143}
144
145impl MotionContextState {
146 fn new() -> Self {
147 Self {
148 inner: Rc::new(MotionContextStateInner {
149 active: Cell::new(false),
150 generation: Cell::new(0),
151 invalidate_callbacks: RefCell::new(std::collections::HashMap::new()),
152 pending_invalidation: Cell::new(false),
153 }),
154 }
155 }
156
157 fn is_active(&self) -> bool {
158 self.inner.active.get()
159 }
160
161 fn set_active(&self, active: bool) {
162 if self.inner.active.replace(active) != active {
163 self.bump_generation();
164 self.invalidate();
165 }
166 }
167
168 fn activate_for_next_frame(&self) {
169 let was_active = self.inner.active.replace(true);
170 let generation = self.bump_generation();
171 if !was_active {
172 self.invalidate();
173 }
174 if let Some(runtime) = current_runtime_handle() {
175 let state = self.clone();
176 let _ = runtime.register_frame_callback(move |_| {
177 state.clear_if_generation(generation);
178 });
179 runtime.schedule();
180 } else {
181 self.clear_if_generation(generation);
182 }
183 }
184
185 fn add_invalidate_callback(&self, callback: Box<dyn Fn()>) -> u64 {
186 static NEXT_CALLBACK_ID: AtomicU64 = AtomicU64::new(1);
187 let id = NEXT_CALLBACK_ID.fetch_add(1, Ordering::Relaxed);
188 self.inner
189 .invalidate_callbacks
190 .borrow_mut()
191 .insert(id, callback);
192 if self.inner.pending_invalidation.replace(false) {
193 if let Some(callback) = self.inner.invalidate_callbacks.borrow().get(&id) {
194 callback();
195 }
196 }
197 id
198 }
199
200 fn remove_invalidate_callback(&self, id: u64) {
201 self.inner.invalidate_callbacks.borrow_mut().remove(&id);
202 }
203
204 fn bump_generation(&self) -> u64 {
205 let next = self.inner.generation.get().wrapping_add(1);
206 self.inner.generation.set(next);
207 next
208 }
209
210 fn clear_if_generation(&self, generation: u64) {
211 if self.inner.generation.get() == generation {
212 self.set_active(false);
213 }
214 }
215
216 fn invalidate(&self) {
217 let callbacks = self.inner.invalidate_callbacks.borrow();
218 if callbacks.is_empty() {
219 self.inner.pending_invalidation.set(true);
220 return;
221 }
222 for callback in callbacks.values() {
223 callback();
224 }
225 }
226}
227
228#[inline]
237fn calculate_total_delta(from: Point, to: Point, is_vertical: bool) -> f32 {
238 if is_vertical {
239 to.y - from.y
240 } else {
241 to.x - from.x
242 }
243}
244
245#[inline]
250fn calculate_incremental_delta(from: Point, to: Point, is_vertical: bool) -> f32 {
251 if is_vertical {
252 to.y - from.y
253 } else {
254 to.x - from.x
255 }
256}
257
258trait ScrollTarget: Clone {
266 fn apply_delta(&self, delta: f32) -> f32;
268
269 fn apply_fling_delta(&self, delta: f32) -> f32;
271
272 fn invalidate(&self);
274
275 fn current_offset(&self) -> f32;
277}
278
279impl ScrollTarget for ScrollState {
280 fn apply_delta(&self, delta: f32) -> f32 {
281 self.dispatch_raw_delta(-delta)
283 }
284
285 fn apply_fling_delta(&self, delta: f32) -> f32 {
286 self.dispatch_raw_delta(delta)
287 }
288
289 fn invalidate(&self) {
290 }
292
293 fn current_offset(&self) -> f32 {
294 self.value()
295 }
296}
297
298impl ScrollTarget for LazyListState {
299 fn apply_delta(&self, delta: f32) -> f32 {
300 self.dispatch_scroll_delta(delta)
304 }
305
306 fn apply_fling_delta(&self, delta: f32) -> f32 {
307 -self.dispatch_scroll_delta(-delta)
308 }
309
310 fn invalidate(&self) {
311 }
316
317 fn current_offset(&self) -> f32 {
318 self.first_visible_item_scroll_offset()
320 }
321}
322
323struct ScrollGestureDetector<S: ScrollTarget> {
329 gesture_state: Rc<RefCell<ScrollGestureState>>,
331
332 scroll_target: S,
334
335 is_vertical: bool,
337
338 reverse_scrolling: bool,
340
341 motion_context: MotionContextState,
343}
344
345impl<S: ScrollTarget + 'static> ScrollGestureDetector<S> {
346 fn new(
348 gesture_state: Rc<RefCell<ScrollGestureState>>,
349 scroll_target: S,
350 is_vertical: bool,
351 reverse_scrolling: bool,
352 motion_context: MotionContextState,
353 ) -> Self {
354 Self {
355 gesture_state,
356 scroll_target,
357 is_vertical,
358 reverse_scrolling,
359 motion_context,
360 }
361 }
362
363 fn on_down(&self, position: Point) -> bool {
372 let mut gs = self.gesture_state.borrow_mut();
373
374 if let Some(fling) = gs.fling_animation.take() {
376 fling.cancel();
377 }
378 self.motion_context.set_active(false);
379
380 gs.drag_down_position = Some(position);
381 gs.last_position = Some(position);
382 gs.is_dragging = false;
383 gs.velocity_tracker.reset();
384 gs.gesture_start_time = Some(Instant::now());
385
386 let pos = if self.is_vertical {
388 position.y
389 } else {
390 position.x
391 };
392 gs.velocity_tracker.add_data_point(0, pos);
393 gs.last_velocity_sample_ms = Some(0);
394
395 false
397 }
398
399 fn on_move(&self, position: Point, buttons: PointerButtons) -> bool {
410 let mut gs = self.gesture_state.borrow_mut();
411
412 if !buttons.contains(PointerButton::Primary) && gs.drag_down_position.is_some() {
414 gs.drag_down_position = None;
415 gs.last_position = None;
416 gs.is_dragging = false;
417 gs.gesture_start_time = None;
418 gs.last_velocity_sample_ms = None;
419 gs.velocity_tracker.reset();
420 self.motion_context.set_active(false);
421 return false;
422 }
423
424 let Some(down_pos) = gs.drag_down_position else {
425 return false;
426 };
427
428 let Some(last_pos) = gs.last_position else {
429 gs.last_position = Some(position);
430 return false;
431 };
432
433 let total_delta = calculate_total_delta(down_pos, position, self.is_vertical);
434 let incremental_delta = calculate_incremental_delta(last_pos, position, self.is_vertical);
435
436 if !gs.is_dragging && total_delta.abs() > DRAG_THRESHOLD {
438 gs.is_dragging = true;
439 self.motion_context.set_active(true);
440 }
441
442 gs.last_position = Some(position);
443
444 if let Some(start_time) = gs.gesture_start_time {
446 let elapsed_ms = start_time.elapsed().as_millis() as i64;
447 let pos = if self.is_vertical {
448 position.y
449 } else {
450 position.x
451 };
452 let sample_ms = match gs.last_velocity_sample_ms {
455 Some(last_sample_ms) => {
456 let mut sample_ms = if elapsed_ms <= last_sample_ms {
457 last_sample_ms + 1
458 } else {
459 elapsed_ms
460 };
461 if sample_ms - last_sample_ms > ASSUME_STOPPED_MS {
463 sample_ms = last_sample_ms + ASSUME_STOPPED_MS;
464 }
465 sample_ms
466 }
467 None => elapsed_ms,
468 };
469 gs.velocity_tracker.add_data_point(sample_ms, pos);
470 gs.last_velocity_sample_ms = Some(sample_ms);
471 }
472
473 if gs.is_dragging {
474 drop(gs); let delta = if self.reverse_scrolling {
476 -incremental_delta
477 } else {
478 incremental_delta
479 };
480 let _ = self.scroll_target.apply_delta(delta);
481 self.scroll_target.invalidate();
482 true } else {
484 false
485 }
486 }
487
488 fn finish_gesture(&self, allow_fling: bool) -> bool {
495 let (was_dragging, velocity, start_fling, existing_fling) = {
496 let mut gs = self.gesture_state.borrow_mut();
497 let was_dragging = gs.is_dragging;
498 let mut velocity = 0.0;
499
500 if allow_fling && was_dragging && gs.gesture_start_time.is_some() {
501 velocity = gs
502 .velocity_tracker
503 .calculate_velocity_with_max(MAX_FLING_VELOCITY);
504 }
505
506 let start_fling = allow_fling && was_dragging && velocity.abs() > MIN_FLING_VELOCITY;
507 let existing_fling = if start_fling {
508 gs.fling_animation.take()
509 } else {
510 None
511 };
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
519 (was_dragging, velocity, start_fling, existing_fling)
520 };
521
522 if allow_fling && was_dragging {
524 set_last_fling_velocity(velocity);
525 }
526
527 if start_fling {
529 if let Some(old_fling) = existing_fling {
530 old_fling.cancel();
531 }
532
533 if let Some(runtime) = current_runtime_handle() {
535 self.motion_context.set_active(true);
536 let scroll_target = self.scroll_target.clone();
537 let reverse = self.reverse_scrolling;
538 let fling = FlingAnimation::new(runtime);
539 let motion_context = self.motion_context.clone();
540
541 let initial_value = scroll_target.current_offset();
543
544 let adjusted_velocity = if reverse { -velocity } else { velocity };
546 let fling_velocity = -adjusted_velocity;
547
548 let scroll_target_for_fling = scroll_target.clone();
549 let scroll_target_for_end = scroll_target.clone();
550
551 fling.start_fling(
552 initial_value,
553 fling_velocity,
554 current_density(),
555 move |delta| {
556 let consumed = scroll_target_for_fling.apply_fling_delta(delta);
558 scroll_target_for_fling.invalidate();
559 consumed
560 },
561 move || {
562 scroll_target_for_end.invalidate();
564 motion_context.set_active(false);
565 },
566 );
567
568 let mut gs = self.gesture_state.borrow_mut();
569 gs.fling_animation = Some(fling);
570 }
571 } else {
572 self.motion_context.set_active(false);
573 }
574
575 was_dragging
576 }
577
578 fn on_up(&self) -> bool {
585 self.finish_gesture(true)
586 }
587
588 fn on_cancel(&self) -> bool {
592 self.finish_gesture(false)
593 }
594
595 fn on_scroll(&self, axis_delta: f32) -> bool {
599 if axis_delta.abs() <= f32::EPSILON {
600 return false;
601 }
602
603 {
604 let mut gs = self.gesture_state.borrow_mut();
606 if let Some(fling) = gs.fling_animation.take() {
607 fling.cancel();
608 }
609 gs.drag_down_position = None;
610 gs.last_position = None;
611 gs.is_dragging = false;
612 gs.gesture_start_time = None;
613 gs.last_velocity_sample_ms = None;
614 gs.velocity_tracker.reset();
615 }
616
617 self.motion_context.activate_for_next_frame();
618
619 let delta = if self.reverse_scrolling {
620 -axis_delta
621 } else {
622 axis_delta
623 };
624 let consumed = self.scroll_target.apply_delta(delta);
625 if consumed.abs() > 0.001 {
626 self.scroll_target.invalidate();
627 true
628 } else {
629 false
630 }
631 }
632}
633
634pub(crate) struct MotionContextAnimatedNode {
635 state: NodeState,
636 motion_context: MotionContextState,
637 invalidation_callback_id: Option<u64>,
638 node_id: Option<NodeId>,
639}
640
641impl MotionContextAnimatedNode {
642 fn new(motion_context: MotionContextState) -> Self {
643 Self {
644 state: NodeState::new(),
645 motion_context,
646 invalidation_callback_id: None,
647 node_id: None,
648 }
649 }
650
651 pub(crate) fn is_active(&self) -> bool {
652 self.motion_context.is_active()
653 }
654}
655
656pub(crate) struct TranslatedContentContextNode {
657 state: NodeState,
658 identity: usize,
659}
660
661impl TranslatedContentContextNode {
662 fn new(identity: usize) -> Self {
663 Self {
664 state: NodeState::new(),
665 identity,
666 }
667 }
668
669 pub(crate) fn is_active(&self) -> bool {
670 true
671 }
672
673 pub(crate) fn identity(&self) -> usize {
674 self.identity
675 }
676}
677
678impl DelegatableNode for TranslatedContentContextNode {
679 fn node_state(&self) -> &NodeState {
680 &self.state
681 }
682}
683
684impl ModifierNode for TranslatedContentContextNode {}
685
686impl DelegatableNode for MotionContextAnimatedNode {
687 fn node_state(&self) -> &NodeState {
688 &self.state
689 }
690}
691
692impl ModifierNode for MotionContextAnimatedNode {
693 fn on_attach(&mut self, context: &mut dyn cranpose_foundation::ModifierNodeContext) {
694 let node_id = context.node_id();
695 self.node_id = node_id;
696 if let Some(node_id) = node_id {
697 let callback_id = self
698 .motion_context
699 .add_invalidate_callback(Box::new(move || {
700 schedule_draw_repass(node_id);
701 }));
702 self.invalidation_callback_id = Some(callback_id);
703 }
704 }
705
706 fn on_detach(&mut self) {
707 if let Some(id) = self.invalidation_callback_id.take() {
708 self.motion_context.remove_invalidate_callback(id);
709 }
710 self.node_id = None;
711 }
712}
713
714#[derive(Clone)]
715struct MotionContextAnimatedElement {
716 motion_context: MotionContextState,
717}
718
719impl MotionContextAnimatedElement {
720 fn new(motion_context: MotionContextState) -> Self {
721 Self { motion_context }
722 }
723}
724
725impl std::fmt::Debug for MotionContextAnimatedElement {
726 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
727 f.debug_struct("MotionContextAnimatedElement").finish()
728 }
729}
730
731impl PartialEq for MotionContextAnimatedElement {
732 fn eq(&self, other: &Self) -> bool {
733 Rc::ptr_eq(&self.motion_context.inner, &other.motion_context.inner)
734 }
735}
736
737impl Eq for MotionContextAnimatedElement {}
738
739impl std::hash::Hash for MotionContextAnimatedElement {
740 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
741 (Rc::as_ptr(&self.motion_context.inner) as usize).hash(state);
742 }
743}
744
745impl ModifierNodeElement for MotionContextAnimatedElement {
746 type Node = MotionContextAnimatedNode;
747
748 fn create(&self) -> Self::Node {
749 MotionContextAnimatedNode::new(self.motion_context.clone())
750 }
751
752 fn update(&self, node: &mut Self::Node) {
753 if Rc::ptr_eq(&node.motion_context.inner, &self.motion_context.inner) {
754 return;
755 }
756 if let Some(id) = node.invalidation_callback_id.take() {
757 node.motion_context.remove_invalidate_callback(id);
758 }
759 node.motion_context = self.motion_context.clone();
760 if let Some(node_id) = node.node_id {
761 let callback_id = node
762 .motion_context
763 .add_invalidate_callback(Box::new(move || {
764 schedule_draw_repass(node_id);
765 }));
766 node.invalidation_callback_id = Some(callback_id);
767 }
768 }
769
770 fn capabilities(&self) -> NodeCapabilities {
771 NodeCapabilities::LAYOUT
772 }
773}
774
775#[derive(Clone)]
776struct TranslatedContentContextElement {
777 identity: usize,
778}
779
780impl TranslatedContentContextElement {
781 fn new(identity: usize) -> Self {
782 Self { identity }
783 }
784}
785
786impl std::fmt::Debug for TranslatedContentContextElement {
787 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
788 f.debug_struct("TranslatedContentContextElement")
789 .field("identity", &self.identity)
790 .finish()
791 }
792}
793
794impl PartialEq for TranslatedContentContextElement {
795 fn eq(&self, other: &Self) -> bool {
796 self.identity == other.identity
797 }
798}
799
800impl Eq for TranslatedContentContextElement {}
801
802impl std::hash::Hash for TranslatedContentContextElement {
803 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
804 self.identity.hash(state);
805 }
806}
807
808impl ModifierNodeElement for TranslatedContentContextElement {
809 type Node = TranslatedContentContextNode;
810
811 fn create(&self) -> Self::Node {
812 TranslatedContentContextNode::new(self.identity)
813 }
814
815 fn update(&self, node: &mut Self::Node) {
816 node.identity = self.identity;
817 }
818
819 fn capabilities(&self) -> NodeCapabilities {
820 NodeCapabilities::LAYOUT
821 }
822}
823
824impl Modifier {
829 pub fn horizontal_scroll(self, state: ScrollState, reverse_scrolling: bool) -> Self {
847 self.then(scroll_impl(state, false, reverse_scrolling, None))
848 }
849
850 pub fn vertical_scroll(self, state: ScrollState, reverse_scrolling: bool) -> Self {
859 self.then(scroll_impl(state, true, reverse_scrolling, None))
860 }
861
862 pub fn horizontal_scroll_guarded(
864 self,
865 state: ScrollState,
866 reverse_scrolling: bool,
867 guard: impl Fn() -> bool + 'static,
868 ) -> Self {
869 self.then(scroll_impl(
870 state,
871 false,
872 reverse_scrolling,
873 Some(Rc::new(guard)),
874 ))
875 }
876
877 pub fn vertical_scroll_guarded(
879 self,
880 state: ScrollState,
881 reverse_scrolling: bool,
882 guard: impl Fn() -> bool + 'static,
883 ) -> Self {
884 self.then(scroll_impl(
885 state,
886 true,
887 reverse_scrolling,
888 Some(Rc::new(guard)),
889 ))
890 }
891}
892
893fn scroll_impl(
902 state: ScrollState,
903 is_vertical: bool,
904 reverse_scrolling: bool,
905 guard: Option<Rc<dyn Fn() -> bool>>,
906) -> Modifier {
907 let gesture_state = Rc::new(RefCell::new(ScrollGestureState::default()));
909 let motion_context = MotionContextState::new();
910
911 let scroll_state = state.clone();
913 let pointer_motion_context = motion_context.clone();
914 let key = (state.id(), is_vertical);
915 let pointer_input = Modifier::empty().pointer_input(key, move |scope| {
916 let detector = ScrollGestureDetector::new(
918 gesture_state.clone(),
919 scroll_state.clone(),
920 is_vertical,
921 false, pointer_motion_context.clone(),
923 );
924 let guard = guard.clone();
925
926 async move {
927 scope
928 .await_pointer_event_scope(|await_scope| async move {
929 loop {
931 let event = await_scope.await_pointer_event().await;
932
933 if let Some(ref guard) = guard {
934 if !guard() {
935 if matches!(
936 event.kind,
937 PointerEventKind::Up | PointerEventKind::Cancel
938 ) {
939 detector.on_cancel();
940 }
941 continue;
942 }
943 }
944
945 let should_consume = match event.kind {
947 PointerEventKind::Down => detector.on_down(event.position),
948 PointerEventKind::Move => {
949 detector.on_move(event.position, event.buttons)
950 }
951 PointerEventKind::Up => detector.on_up(),
952 PointerEventKind::Cancel => detector.on_cancel(),
953 PointerEventKind::Scroll => detector.on_scroll(if is_vertical {
954 event.scroll_delta.y
955 } else {
956 event.scroll_delta.x
957 }),
958 PointerEventKind::Enter | PointerEventKind::Exit => false,
959 };
960
961 if should_consume {
962 event.consume();
963 }
964 }
965 })
966 .await;
967 }
968 });
969
970 let element = ScrollElement::new(state.clone(), is_vertical, reverse_scrolling);
972 let layout_modifier =
973 Modifier::with_element(element).with_inspector_metadata(inspector_metadata(
974 if is_vertical {
975 "verticalScroll"
976 } else {
977 "horizontalScroll"
978 },
979 move |info| {
980 info.add_property("isVertical", is_vertical.to_string());
981 info.add_property("reverseScrolling", reverse_scrolling.to_string());
982 },
983 ));
984 let motion_modifier =
985 Modifier::with_element(MotionContextAnimatedElement::new(motion_context.clone()));
986 let translated_content_modifier =
987 Modifier::with_element(TranslatedContentContextElement::new(state.id() as usize));
988
989 pointer_input
991 .then(motion_modifier)
992 .then(translated_content_modifier)
993 .then(layout_modifier)
994 .clip_to_bounds()
995}
996
997use cranpose_foundation::lazy::LazyListState;
1002
1003impl Modifier {
1004 pub fn lazy_vertical_scroll(self, state: LazyListState, reverse_scrolling: bool) -> Self {
1015 self.then(lazy_scroll_impl(state, true, reverse_scrolling))
1016 }
1017
1018 pub fn lazy_horizontal_scroll(self, state: LazyListState, reverse_scrolling: bool) -> Self {
1020 self.then(lazy_scroll_impl(state, false, reverse_scrolling))
1021 }
1022}
1023
1024fn lazy_scroll_impl(state: LazyListState, is_vertical: bool, reverse_scrolling: bool) -> Modifier {
1026 let gesture_state = Rc::new(RefCell::new(ScrollGestureState::default()));
1027 let list_state = state;
1028 let motion_context = MotionContextState::new();
1029
1030 let state_id = std::ptr::addr_of!(*state.inner_ptr()) as usize;
1036 let key = (state_id, is_vertical, reverse_scrolling);
1037 let translated_content_modifier =
1038 Modifier::with_element(TranslatedContentContextElement::new(state_id));
1039
1040 Modifier::with_element(MotionContextAnimatedElement::new(motion_context.clone()))
1041 .then(translated_content_modifier)
1042 .pointer_input(key, move |scope| {
1043 let detector = ScrollGestureDetector::new(
1045 gesture_state.clone(),
1046 list_state,
1047 is_vertical,
1048 reverse_scrolling,
1049 motion_context.clone(),
1050 );
1051
1052 async move {
1053 scope
1054 .await_pointer_event_scope(|await_scope| async move {
1055 loop {
1056 let event = await_scope.await_pointer_event().await;
1057
1058 let should_consume = match event.kind {
1060 PointerEventKind::Down => detector.on_down(event.position),
1061 PointerEventKind::Move => {
1062 detector.on_move(event.position, event.buttons)
1063 }
1064 PointerEventKind::Up => detector.on_up(),
1065 PointerEventKind::Cancel => detector.on_cancel(),
1066 PointerEventKind::Scroll => detector.on_scroll(if is_vertical {
1067 event.scroll_delta.y
1068 } else {
1069 event.scroll_delta.x
1070 }),
1071 PointerEventKind::Enter | PointerEventKind::Exit => false,
1072 };
1073
1074 if should_consume {
1075 event.consume();
1076 }
1077 }
1078 })
1079 .await;
1080 }
1081 })
1082}