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")]
37pub fn last_fling_velocity() -> f32 {
38 crate::render_state::debug_last_fling_velocity()
39}
40
41#[cfg(feature = "test-helpers")]
42pub fn reset_last_fling_velocity() {
43 crate::render_state::debug_reset_last_fling_velocity();
44}
45
46#[inline]
47fn set_last_fling_velocity(velocity: f32) {
48 crate::render_state::record_last_fling_velocity(velocity);
49}
50
51struct ScrollGestureState {
57 drag_down_position: Option<Point>,
60
61 last_position: Option<Point>,
64
65 is_dragging: bool,
69
70 velocity_tracker: VelocityTracker1D,
72
73 gesture_start_time: Option<Instant>,
75
76 last_velocity_sample_ms: Option<i64>,
78
79 fling_animation: Option<FlingAnimation>,
81}
82
83impl Default for ScrollGestureState {
84 fn default() -> Self {
85 Self {
86 drag_down_position: None,
87 last_position: None,
88 is_dragging: false,
89 velocity_tracker: VelocityTracker1D::new(),
90 gesture_start_time: None,
91 last_velocity_sample_ms: None,
92 fling_animation: None,
93 }
94 }
95}
96
97#[inline]
106fn calculate_total_delta(from: Point, to: Point, is_vertical: bool) -> f32 {
107 if is_vertical {
108 to.y - from.y
109 } else {
110 to.x - from.x
111 }
112}
113
114#[inline]
119fn calculate_incremental_delta(from: Point, to: Point, is_vertical: bool) -> f32 {
120 if is_vertical {
121 to.y - from.y
122 } else {
123 to.x - from.x
124 }
125}
126
127trait ScrollTarget: Clone {
135 fn apply_delta(&self, delta: f32) -> f32;
137
138 fn apply_wheel_delta(&self, delta: f32) -> f32 {
140 self.apply_delta(delta)
141 }
142
143 fn apply_fling_delta(&self, delta: f32) -> f32;
145
146 fn invalidate(&self);
148
149 fn current_offset(&self) -> f32;
151}
152
153impl ScrollTarget for ScrollState {
154 fn apply_delta(&self, delta: f32) -> f32 {
155 self.dispatch_raw_delta(-delta)
157 }
158
159 fn apply_fling_delta(&self, delta: f32) -> f32 {
160 self.dispatch_raw_delta(delta)
161 }
162
163 fn invalidate(&self) {
164 }
166
167 fn current_offset(&self) -> f32 {
168 self.value()
169 }
170}
171
172impl ScrollTarget for LazyListState {
173 fn apply_delta(&self, delta: f32) -> f32 {
174 self.dispatch_scroll_delta(delta)
178 }
179
180 fn apply_wheel_delta(&self, delta: f32) -> f32 {
181 if delta.abs() <= 0.001 {
182 0.0
183 } else {
184 self.dispatch_scroll_delta(delta)
185 }
186 }
187
188 fn apply_fling_delta(&self, delta: f32) -> f32 {
189 -self.dispatch_scroll_delta(-delta)
190 }
191
192 fn invalidate(&self) {
193 }
196
197 fn current_offset(&self) -> f32 {
198 self.first_visible_item_scroll_offset()
200 }
201}
202
203struct ScrollGestureDetector<S: ScrollTarget> {
209 gesture_state: Rc<RefCell<ScrollGestureState>>,
211
212 scroll_target: S,
214
215 is_vertical: bool,
217
218 reverse_scrolling: bool,
220
221 motion_context: ScrollMotionContext,
223}
224
225impl<S: ScrollTarget + 'static> ScrollGestureDetector<S> {
226 fn new(
228 gesture_state: Rc<RefCell<ScrollGestureState>>,
229 scroll_target: S,
230 is_vertical: bool,
231 reverse_scrolling: bool,
232 motion_context: ScrollMotionContext,
233 ) -> Self {
234 Self {
235 gesture_state,
236 scroll_target,
237 is_vertical,
238 reverse_scrolling,
239 motion_context,
240 }
241 }
242
243 fn on_down(&self, position: Point) -> bool {
252 let mut gs = self.gesture_state.borrow_mut();
253
254 if let Some(fling) = gs.fling_animation.take() {
256 fling.cancel();
257 }
258 self.motion_context.set_active(false);
259
260 gs.drag_down_position = Some(position);
261 gs.last_position = Some(position);
262 gs.is_dragging = false;
263 gs.velocity_tracker.reset();
264 gs.gesture_start_time = Some(Instant::now());
265
266 let pos = if self.is_vertical {
268 position.y
269 } else {
270 position.x
271 };
272 gs.velocity_tracker.add_data_point(0, pos);
273 gs.last_velocity_sample_ms = Some(0);
274
275 false
277 }
278
279 fn on_move(&self, position: Point, buttons: PointerButtons) -> bool {
290 let mut gs = self.gesture_state.borrow_mut();
291
292 if !buttons.contains(PointerButton::Primary) && gs.drag_down_position.is_some() {
294 gs.drag_down_position = None;
295 gs.last_position = None;
296 gs.is_dragging = false;
297 gs.gesture_start_time = None;
298 gs.last_velocity_sample_ms = None;
299 gs.velocity_tracker.reset();
300 self.motion_context.set_active(false);
301 return false;
302 }
303
304 let Some(down_pos) = gs.drag_down_position else {
305 return false;
306 };
307
308 let Some(last_pos) = gs.last_position else {
309 gs.last_position = Some(position);
310 return false;
311 };
312
313 let total_delta = calculate_total_delta(down_pos, position, self.is_vertical);
314 let incremental_delta = calculate_incremental_delta(last_pos, position, self.is_vertical);
315
316 if !gs.is_dragging && total_delta.abs() > DRAG_THRESHOLD {
318 gs.is_dragging = true;
319 self.motion_context.set_active(true);
320 }
321
322 gs.last_position = Some(position);
323
324 if let Some(start_time) = gs.gesture_start_time {
326 let elapsed_ms = start_time.elapsed().as_millis() as i64;
327 let pos = if self.is_vertical {
328 position.y
329 } else {
330 position.x
331 };
332 let sample_ms = match gs.last_velocity_sample_ms {
335 Some(last_sample_ms) => {
336 let mut sample_ms = if elapsed_ms <= last_sample_ms {
337 last_sample_ms + 1
338 } else {
339 elapsed_ms
340 };
341 if sample_ms - last_sample_ms > ASSUME_STOPPED_MS {
343 sample_ms = last_sample_ms + ASSUME_STOPPED_MS;
344 }
345 sample_ms
346 }
347 None => elapsed_ms,
348 };
349 gs.velocity_tracker.add_data_point(sample_ms, pos);
350 gs.last_velocity_sample_ms = Some(sample_ms);
351 }
352
353 if gs.is_dragging {
354 drop(gs); let delta = if self.reverse_scrolling {
356 -incremental_delta
357 } else {
358 incremental_delta
359 };
360 let _ = self.scroll_target.apply_delta(delta);
361 self.scroll_target.invalidate();
362 true } else {
364 false
365 }
366 }
367
368 fn finish_gesture(&self, allow_fling: bool) -> bool {
375 let (was_dragging, velocity, start_fling, existing_fling) = {
376 let mut gs = self.gesture_state.borrow_mut();
377 let was_dragging = gs.is_dragging;
378 let mut velocity = 0.0;
379
380 if allow_fling && was_dragging && gs.gesture_start_time.is_some() {
381 velocity = gs
382 .velocity_tracker
383 .calculate_velocity_with_max(MAX_FLING_VELOCITY);
384 }
385
386 let start_fling = allow_fling && was_dragging && velocity.abs() > MIN_FLING_VELOCITY;
387 let existing_fling = if start_fling {
388 gs.fling_animation.take()
389 } else {
390 None
391 };
392
393 gs.drag_down_position = None;
394 gs.last_position = None;
395 gs.is_dragging = false;
396 gs.gesture_start_time = None;
397 gs.last_velocity_sample_ms = None;
398
399 (was_dragging, velocity, start_fling, existing_fling)
400 };
401
402 if allow_fling && was_dragging {
404 set_last_fling_velocity(velocity);
405 }
406
407 if start_fling {
409 if let Some(old_fling) = existing_fling {
410 old_fling.cancel();
411 }
412
413 if let Some(runtime) = current_runtime_handle() {
415 self.motion_context.set_active(true);
416 let scroll_target = self.scroll_target.clone();
417 let reverse = self.reverse_scrolling;
418 let fling = FlingAnimation::new(runtime);
419 let motion_context = self.motion_context.clone();
420
421 let initial_value = scroll_target.current_offset();
423
424 let adjusted_velocity = if reverse { -velocity } else { velocity };
426 let fling_velocity = -adjusted_velocity;
427
428 let scroll_target_for_fling = scroll_target.clone();
429 let scroll_target_for_end = scroll_target.clone();
430
431 fling.start_fling(
432 initial_value,
433 fling_velocity,
434 current_density(),
435 move |delta| {
436 let consumed = scroll_target_for_fling.apply_fling_delta(delta);
438 scroll_target_for_fling.invalidate();
439 consumed
440 },
441 move || {
442 scroll_target_for_end.invalidate();
444 motion_context.set_active(false);
445 },
446 );
447
448 let mut gs = self.gesture_state.borrow_mut();
449 gs.fling_animation = Some(fling);
450 }
451 } else {
452 self.motion_context.set_active(false);
453 }
454
455 was_dragging
456 }
457
458 fn on_up(&self) -> bool {
465 self.finish_gesture(true)
466 }
467
468 fn on_cancel(&self) -> bool {
472 self.finish_gesture(false)
473 }
474
475 fn on_scroll(&self, axis_delta: f32) -> bool {
479 if axis_delta.abs() <= f32::EPSILON {
480 return false;
481 }
482
483 {
484 let mut gs = self.gesture_state.borrow_mut();
486 if let Some(fling) = gs.fling_animation.take() {
487 fling.cancel();
488 }
489 gs.drag_down_position = None;
490 gs.last_position = None;
491 gs.is_dragging = false;
492 gs.gesture_start_time = None;
493 gs.last_velocity_sample_ms = None;
494 gs.velocity_tracker.reset();
495 }
496
497 self.motion_context.activate_for_current_frame();
498
499 let delta = if self.reverse_scrolling {
500 -axis_delta
501 } else {
502 axis_delta
503 };
504 let consumed = self.scroll_target.apply_wheel_delta(delta);
505 if consumed.abs() > 0.001 {
506 self.scroll_target.invalidate();
507 true
508 } else {
509 false
510 }
511 }
512}
513
514pub(crate) struct MotionContextAnimatedNode {
515 state: NodeState,
516 motion_context: ScrollMotionContext,
517 invalidation_callback_id: Option<u64>,
518 node_id: Option<NodeId>,
519}
520
521impl MotionContextAnimatedNode {
522 fn new(motion_context: ScrollMotionContext) -> Self {
523 Self {
524 state: NodeState::new(),
525 motion_context,
526 invalidation_callback_id: None,
527 node_id: None,
528 }
529 }
530
531 pub(crate) fn is_active(&self) -> bool {
532 self.motion_context.is_active()
533 }
534}
535
536pub(crate) struct TranslatedContentContextNode {
537 state: NodeState,
538 identity: usize,
539 offset_source: TranslatedContentOffsetSource,
540}
541
542impl TranslatedContentContextNode {
543 fn new(identity: usize, offset_source: TranslatedContentOffsetSource) -> Self {
544 Self {
545 state: NodeState::new(),
546 identity,
547 offset_source,
548 }
549 }
550
551 pub(crate) fn is_active(&self) -> bool {
552 true
553 }
554
555 pub(crate) fn identity(&self) -> usize {
556 self.identity
557 }
558
559 pub(crate) fn content_offset_reader(&self) -> Option<Rc<dyn Fn() -> Point>> {
560 self.offset_source.content_offset_reader()
561 }
562}
563
564impl DelegatableNode for TranslatedContentContextNode {
565 fn node_state(&self) -> &NodeState {
566 &self.state
567 }
568}
569
570impl ModifierNode for TranslatedContentContextNode {}
571
572impl DelegatableNode for MotionContextAnimatedNode {
573 fn node_state(&self) -> &NodeState {
574 &self.state
575 }
576}
577
578impl ModifierNode for MotionContextAnimatedNode {
579 fn on_attach(&mut self, context: &mut dyn cranpose_foundation::ModifierNodeContext) {
580 let node_id = context.node_id();
581 self.node_id = node_id;
582 if let Some(node_id) = node_id {
583 let callback_id = self
584 .motion_context
585 .add_invalidate_callback(Box::new(move || {
586 schedule_modifier_slices_repass(node_id);
587 }));
588 self.invalidation_callback_id = Some(callback_id);
589 }
590 }
591
592 fn on_detach(&mut self) {
593 if let Some(id) = self.invalidation_callback_id.take() {
594 self.motion_context.remove_invalidate_callback(id);
595 }
596 self.node_id = None;
597 }
598}
599
600#[derive(Clone)]
601struct MotionContextAnimatedElement {
602 motion_context: ScrollMotionContext,
603}
604
605impl MotionContextAnimatedElement {
606 fn new(motion_context: ScrollMotionContext) -> Self {
607 Self { motion_context }
608 }
609}
610
611impl std::fmt::Debug for MotionContextAnimatedElement {
612 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
613 f.debug_struct("MotionContextAnimatedElement").finish()
614 }
615}
616
617impl PartialEq for MotionContextAnimatedElement {
618 fn eq(&self, other: &Self) -> bool {
619 self.motion_context.ptr_eq(&other.motion_context)
620 }
621}
622
623impl Eq for MotionContextAnimatedElement {}
624
625impl std::hash::Hash for MotionContextAnimatedElement {
626 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
627 self.motion_context.stable_key().hash(state);
628 }
629}
630
631impl ModifierNodeElement for MotionContextAnimatedElement {
632 type Node = MotionContextAnimatedNode;
633
634 fn create(&self) -> Self::Node {
635 MotionContextAnimatedNode::new(self.motion_context.clone())
636 }
637
638 fn update(&self, node: &mut Self::Node) {
639 if node.motion_context.ptr_eq(&self.motion_context) {
640 return;
641 }
642 if let Some(id) = node.invalidation_callback_id.take() {
643 node.motion_context.remove_invalidate_callback(id);
644 }
645 node.motion_context = self.motion_context.clone();
646 if let Some(node_id) = node.node_id {
647 let callback_id = node
648 .motion_context
649 .add_invalidate_callback(Box::new(move || {
650 schedule_modifier_slices_repass(node_id);
651 }));
652 node.invalidation_callback_id = Some(callback_id);
653 }
654 }
655
656 fn capabilities(&self) -> NodeCapabilities {
657 NodeCapabilities::LAYOUT
658 }
659}
660
661#[derive(Clone)]
662enum TranslatedContentOffsetSource {
663 LayoutContentOffset,
664 LazyList {
665 state: LazyListState,
666 is_vertical: bool,
667 reverse_scrolling: bool,
668 },
669}
670
671impl TranslatedContentOffsetSource {
672 fn content_offset_reader(&self) -> Option<Rc<dyn Fn() -> Point>> {
673 match self {
674 Self::LayoutContentOffset => None,
675 Self::LazyList {
676 state, is_vertical, ..
677 } => Some(Rc::new(lazy_list_content_offset_reader(
678 *state,
679 *is_vertical,
680 ))),
681 }
682 }
683
684 fn is_vertical(&self) -> Option<bool> {
685 match self {
686 Self::LayoutContentOffset => None,
687 Self::LazyList { is_vertical, .. } => Some(*is_vertical),
688 }
689 }
690
691 fn reverse_scrolling(&self) -> Option<bool> {
692 match self {
693 Self::LayoutContentOffset => None,
694 Self::LazyList {
695 reverse_scrolling, ..
696 } => Some(*reverse_scrolling),
697 }
698 }
699}
700
701fn lazy_list_content_offset_reader(state: LazyListState, is_vertical: bool) -> impl Fn() -> Point {
702 move || {
703 let info = state.layout_info();
704 if info.visible_items_info.is_empty() {
705 return Point::default();
706 };
707 let main_offset = info.snap_anchor_offset;
708 if is_vertical {
709 Point::new(0.0, main_offset)
710 } else {
711 Point::new(main_offset, 0.0)
712 }
713 }
714}
715
716#[derive(Clone)]
717struct TranslatedContentContextElement {
718 identity: usize,
719 offset_source: TranslatedContentOffsetSource,
720}
721
722impl TranslatedContentContextElement {
723 fn new(identity: usize, offset_source: TranslatedContentOffsetSource) -> Self {
724 Self {
725 identity,
726 offset_source,
727 }
728 }
729}
730
731impl std::fmt::Debug for TranslatedContentContextElement {
732 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
733 let offset_source = match &self.offset_source {
734 TranslatedContentOffsetSource::LayoutContentOffset => "layout",
735 TranslatedContentOffsetSource::LazyList { .. } => "lazy_list",
736 };
737 f.debug_struct("TranslatedContentContextElement")
738 .field("identity", &self.identity)
739 .field("offset_source", &offset_source)
740 .finish()
741 }
742}
743
744impl PartialEq for TranslatedContentContextElement {
745 fn eq(&self, other: &Self) -> bool {
746 self.identity == other.identity
747 && self.offset_source.is_vertical() == other.offset_source.is_vertical()
748 && self.offset_source.reverse_scrolling() == other.offset_source.reverse_scrolling()
749 }
750}
751
752impl Eq for TranslatedContentContextElement {}
753
754impl std::hash::Hash for TranslatedContentContextElement {
755 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
756 self.identity.hash(state);
757 self.offset_source.is_vertical().hash(state);
758 self.offset_source.reverse_scrolling().hash(state);
759 }
760}
761
762impl ModifierNodeElement for TranslatedContentContextElement {
763 type Node = TranslatedContentContextNode;
764
765 fn create(&self) -> Self::Node {
766 TranslatedContentContextNode::new(self.identity, self.offset_source.clone())
767 }
768
769 fn update(&self, node: &mut Self::Node) {
770 node.identity = self.identity;
771 node.offset_source = self.offset_source.clone();
772 }
773
774 fn capabilities(&self) -> NodeCapabilities {
775 NodeCapabilities::LAYOUT
776 }
777}
778
779impl Modifier {
784 pub fn horizontal_scroll(self, state: ScrollState, reverse_scrolling: bool) -> Self {
802 self.then(scroll_impl(state, false, reverse_scrolling, None))
803 }
804
805 pub fn vertical_scroll(self, state: ScrollState, reverse_scrolling: bool) -> Self {
814 self.then(scroll_impl(state, true, reverse_scrolling, None))
815 }
816
817 pub fn horizontal_scroll_guarded(
819 self,
820 state: ScrollState,
821 reverse_scrolling: bool,
822 guard: impl Fn() -> bool + 'static,
823 ) -> Self {
824 self.then(scroll_impl(
825 state,
826 false,
827 reverse_scrolling,
828 Some(Rc::new(guard)),
829 ))
830 }
831
832 pub fn vertical_scroll_guarded(
834 self,
835 state: ScrollState,
836 reverse_scrolling: bool,
837 guard: impl Fn() -> bool + 'static,
838 ) -> Self {
839 self.then(scroll_impl(
840 state,
841 true,
842 reverse_scrolling,
843 Some(Rc::new(guard)),
844 ))
845 }
846}
847
848fn scroll_impl(
857 state: ScrollState,
858 is_vertical: bool,
859 reverse_scrolling: bool,
860 guard: Option<Rc<dyn Fn() -> bool>>,
861) -> Modifier {
862 let gesture_state = Rc::new(RefCell::new(ScrollGestureState::default()));
864 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::ScrollState {
865 state_id: state.id(),
866 is_vertical,
867 reverse_scrolling,
868 });
869
870 let scroll_state = state.clone();
872 let pointer_motion_context = motion_context.clone();
873 let key = (state.id(), is_vertical);
874 let pointer_input = Modifier::empty().pointer_input(key, move |scope| {
875 let detector = ScrollGestureDetector::new(
877 gesture_state.clone(),
878 scroll_state.clone(),
879 is_vertical,
880 false, pointer_motion_context.clone(),
882 );
883 let guard = guard.clone();
884
885 async move {
886 scope
887 .await_pointer_event_scope(|await_scope| async move {
888 loop {
890 let event = await_scope.await_pointer_event().await;
891
892 if event.is_consumed() {
893 if matches!(
894 event.kind,
895 PointerEventKind::Down
896 | PointerEventKind::Move
897 | PointerEventKind::Up
898 | PointerEventKind::Cancel
899 ) {
900 detector.on_cancel();
901 }
902 continue;
903 }
904
905 if let Some(ref guard) = guard {
906 if !guard() {
907 if matches!(
908 event.kind,
909 PointerEventKind::Up | PointerEventKind::Cancel
910 ) {
911 detector.on_cancel();
912 }
913 continue;
914 }
915 }
916
917 let should_consume = match event.kind {
919 PointerEventKind::Down => detector.on_down(event.position),
920 PointerEventKind::Move => {
921 detector.on_move(event.position, event.buttons)
922 }
923 PointerEventKind::Up => detector.on_up(),
924 PointerEventKind::Cancel => detector.on_cancel(),
925 PointerEventKind::Scroll => detector.on_scroll(if is_vertical {
926 event.scroll_delta.y
927 } else {
928 event.scroll_delta.x
929 }),
930 PointerEventKind::Enter | PointerEventKind::Exit => false,
931 };
932
933 if should_consume {
934 event.consume();
935 }
936 }
937 })
938 .await;
939 }
940 });
941
942 let element = ScrollElement::new(state.clone(), is_vertical, reverse_scrolling);
944 let layout_modifier =
945 Modifier::with_element(element).with_inspector_metadata(inspector_metadata(
946 if is_vertical {
947 "verticalScroll"
948 } else {
949 "horizontalScroll"
950 },
951 move |info| {
952 info.add_property("isVertical", is_vertical.to_string());
953 info.add_property("reverseScrolling", reverse_scrolling.to_string());
954 },
955 ));
956 let motion_modifier =
957 Modifier::with_element(MotionContextAnimatedElement::new(motion_context.clone()));
958 let translated_content_modifier = Modifier::with_element(TranslatedContentContextElement::new(
959 state.id() as usize,
960 TranslatedContentOffsetSource::LayoutContentOffset,
961 ));
962
963 pointer_input
965 .then(motion_modifier)
966 .then(translated_content_modifier)
967 .then(layout_modifier)
968 .clip_to_bounds()
969}
970
971use cranpose_foundation::lazy::LazyListState;
976
977impl Modifier {
978 pub fn lazy_vertical_scroll(self, state: LazyListState, reverse_scrolling: bool) -> Self {
989 self.then(lazy_scroll_impl(state, true, reverse_scrolling))
990 }
991
992 pub fn lazy_horizontal_scroll(self, state: LazyListState, reverse_scrolling: bool) -> Self {
994 self.then(lazy_scroll_impl(state, false, reverse_scrolling))
995 }
996}
997
998fn lazy_scroll_impl(state: LazyListState, is_vertical: bool, reverse_scrolling: bool) -> Modifier {
1000 let gesture_state = Rc::new(RefCell::new(ScrollGestureState::default()));
1001 let list_state = state;
1002 let state_id = state.inner_ptr() as usize;
1003 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::LazyList {
1004 state_identity: state_id,
1005 is_vertical,
1006 reverse_scrolling,
1007 });
1008 let key = (state_id, is_vertical, reverse_scrolling);
1009 let translated_content_modifier = Modifier::with_element(TranslatedContentContextElement::new(
1010 state_id,
1011 TranslatedContentOffsetSource::LazyList {
1012 state,
1013 is_vertical,
1014 reverse_scrolling,
1015 },
1016 ));
1017
1018 Modifier::with_element(MotionContextAnimatedElement::new(motion_context.clone()))
1019 .then(translated_content_modifier)
1020 .pointer_input(key, move |scope| {
1021 let detector = ScrollGestureDetector::new(
1023 gesture_state.clone(),
1024 list_state,
1025 is_vertical,
1026 reverse_scrolling,
1027 motion_context.clone(),
1028 );
1029
1030 async move {
1031 scope
1032 .await_pointer_event_scope(|await_scope| async move {
1033 loop {
1034 let event = await_scope.await_pointer_event().await;
1035
1036 if event.is_consumed() {
1037 if matches!(
1038 event.kind,
1039 PointerEventKind::Down
1040 | PointerEventKind::Move
1041 | PointerEventKind::Up
1042 | PointerEventKind::Cancel
1043 ) {
1044 detector.on_cancel();
1045 }
1046 continue;
1047 }
1048
1049 let should_consume = match event.kind {
1051 PointerEventKind::Down => detector.on_down(event.position),
1052 PointerEventKind::Move => {
1053 detector.on_move(event.position, event.buttons)
1054 }
1055 PointerEventKind::Up => detector.on_up(),
1056 PointerEventKind::Cancel => detector.on_cancel(),
1057 PointerEventKind::Scroll => detector.on_scroll(if is_vertical {
1058 event.scroll_delta.y
1059 } else {
1060 event.scroll_delta.x
1061 }),
1062 PointerEventKind::Enter | PointerEventKind::Exit => false,
1063 };
1064
1065 if should_consume {
1066 event.consume();
1067 }
1068 }
1069 })
1070 .await;
1071 }
1072 })
1073}