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_fling_delta(&self, delta: f32) -> f32;
140
141 fn invalidate(&self);
143
144 fn current_offset(&self) -> f32;
146}
147
148impl ScrollTarget for ScrollState {
149 fn apply_delta(&self, delta: f32) -> f32 {
150 self.dispatch_raw_delta(-delta)
152 }
153
154 fn apply_fling_delta(&self, delta: f32) -> f32 {
155 self.dispatch_raw_delta(delta)
156 }
157
158 fn invalidate(&self) {
159 }
161
162 fn current_offset(&self) -> f32 {
163 self.value()
164 }
165}
166
167impl ScrollTarget for LazyListState {
168 fn apply_delta(&self, delta: f32) -> f32 {
169 self.dispatch_scroll_delta(delta)
173 }
174
175 fn apply_fling_delta(&self, delta: f32) -> f32 {
176 -self.dispatch_scroll_delta(-delta)
177 }
178
179 fn invalidate(&self) {
180 }
183
184 fn current_offset(&self) -> f32 {
185 self.first_visible_item_scroll_offset()
187 }
188}
189
190struct ScrollGestureDetector<S: ScrollTarget> {
196 gesture_state: Rc<RefCell<ScrollGestureState>>,
198
199 scroll_target: S,
201
202 is_vertical: bool,
204
205 reverse_scrolling: bool,
207
208 motion_context: ScrollMotionContext,
210}
211
212impl<S: ScrollTarget + 'static> ScrollGestureDetector<S> {
213 fn new(
215 gesture_state: Rc<RefCell<ScrollGestureState>>,
216 scroll_target: S,
217 is_vertical: bool,
218 reverse_scrolling: bool,
219 motion_context: ScrollMotionContext,
220 ) -> Self {
221 Self {
222 gesture_state,
223 scroll_target,
224 is_vertical,
225 reverse_scrolling,
226 motion_context,
227 }
228 }
229
230 fn on_down(&self, position: Point) -> bool {
239 let mut gs = self.gesture_state.borrow_mut();
240
241 if let Some(fling) = gs.fling_animation.take() {
243 fling.cancel();
244 }
245 self.motion_context.set_active(false);
246
247 gs.drag_down_position = Some(position);
248 gs.last_position = Some(position);
249 gs.is_dragging = false;
250 gs.velocity_tracker.reset();
251 gs.gesture_start_time = Some(Instant::now());
252
253 let pos = if self.is_vertical {
255 position.y
256 } else {
257 position.x
258 };
259 gs.velocity_tracker.add_data_point(0, pos);
260 gs.last_velocity_sample_ms = Some(0);
261
262 false
264 }
265
266 fn on_move(&self, position: Point, buttons: PointerButtons) -> bool {
277 let mut gs = self.gesture_state.borrow_mut();
278
279 if !buttons.contains(PointerButton::Primary) && gs.drag_down_position.is_some() {
281 gs.drag_down_position = None;
282 gs.last_position = None;
283 gs.is_dragging = false;
284 gs.gesture_start_time = None;
285 gs.last_velocity_sample_ms = None;
286 gs.velocity_tracker.reset();
287 self.motion_context.set_active(false);
288 return false;
289 }
290
291 let Some(down_pos) = gs.drag_down_position else {
292 return false;
293 };
294
295 let Some(last_pos) = gs.last_position else {
296 gs.last_position = Some(position);
297 return false;
298 };
299
300 let total_delta = calculate_total_delta(down_pos, position, self.is_vertical);
301 let incremental_delta = calculate_incremental_delta(last_pos, position, self.is_vertical);
302
303 if !gs.is_dragging && total_delta.abs() > DRAG_THRESHOLD {
305 gs.is_dragging = true;
306 self.motion_context.set_active(true);
307 }
308
309 gs.last_position = Some(position);
310
311 if let Some(start_time) = gs.gesture_start_time {
313 let elapsed_ms = start_time.elapsed().as_millis() as i64;
314 let pos = if self.is_vertical {
315 position.y
316 } else {
317 position.x
318 };
319 let sample_ms = match gs.last_velocity_sample_ms {
322 Some(last_sample_ms) => {
323 let mut sample_ms = if elapsed_ms <= last_sample_ms {
324 last_sample_ms + 1
325 } else {
326 elapsed_ms
327 };
328 if sample_ms - last_sample_ms > ASSUME_STOPPED_MS {
330 sample_ms = last_sample_ms + ASSUME_STOPPED_MS;
331 }
332 sample_ms
333 }
334 None => elapsed_ms,
335 };
336 gs.velocity_tracker.add_data_point(sample_ms, pos);
337 gs.last_velocity_sample_ms = Some(sample_ms);
338 }
339
340 if gs.is_dragging {
341 drop(gs); let delta = if self.reverse_scrolling {
343 -incremental_delta
344 } else {
345 incremental_delta
346 };
347 let _ = self.scroll_target.apply_delta(delta);
348 self.scroll_target.invalidate();
349 true } else {
351 false
352 }
353 }
354
355 fn finish_gesture(&self, allow_fling: bool) -> bool {
362 let (was_dragging, velocity, start_fling, existing_fling) = {
363 let mut gs = self.gesture_state.borrow_mut();
364 let was_dragging = gs.is_dragging;
365 let mut velocity = 0.0;
366
367 if allow_fling && was_dragging && gs.gesture_start_time.is_some() {
368 velocity = gs
369 .velocity_tracker
370 .calculate_velocity_with_max(MAX_FLING_VELOCITY);
371 }
372
373 let start_fling = allow_fling && was_dragging && velocity.abs() > MIN_FLING_VELOCITY;
374 let existing_fling = if start_fling {
375 gs.fling_animation.take()
376 } else {
377 None
378 };
379
380 gs.drag_down_position = None;
381 gs.last_position = None;
382 gs.is_dragging = false;
383 gs.gesture_start_time = None;
384 gs.last_velocity_sample_ms = None;
385
386 (was_dragging, velocity, start_fling, existing_fling)
387 };
388
389 if allow_fling && was_dragging {
391 set_last_fling_velocity(velocity);
392 }
393
394 if start_fling {
396 if let Some(old_fling) = existing_fling {
397 old_fling.cancel();
398 }
399
400 if let Some(runtime) = current_runtime_handle() {
402 self.motion_context.set_active(true);
403 let scroll_target = self.scroll_target.clone();
404 let reverse = self.reverse_scrolling;
405 let fling = FlingAnimation::new(runtime);
406 let motion_context = self.motion_context.clone();
407
408 let initial_value = scroll_target.current_offset();
410
411 let adjusted_velocity = if reverse { -velocity } else { velocity };
413 let fling_velocity = -adjusted_velocity;
414
415 let scroll_target_for_fling = scroll_target.clone();
416 let scroll_target_for_end = scroll_target.clone();
417
418 fling.start_fling(
419 initial_value,
420 fling_velocity,
421 current_density(),
422 move |delta| {
423 let consumed = scroll_target_for_fling.apply_fling_delta(delta);
425 scroll_target_for_fling.invalidate();
426 consumed
427 },
428 move || {
429 scroll_target_for_end.invalidate();
431 motion_context.set_active(false);
432 },
433 );
434
435 let mut gs = self.gesture_state.borrow_mut();
436 gs.fling_animation = Some(fling);
437 }
438 } else {
439 self.motion_context.set_active(false);
440 }
441
442 was_dragging
443 }
444
445 fn on_up(&self) -> bool {
452 self.finish_gesture(true)
453 }
454
455 fn on_cancel(&self) -> bool {
459 self.finish_gesture(false)
460 }
461
462 fn on_scroll(&self, axis_delta: f32) -> bool {
466 if axis_delta.abs() <= f32::EPSILON {
467 return false;
468 }
469
470 {
471 let mut gs = self.gesture_state.borrow_mut();
473 if let Some(fling) = gs.fling_animation.take() {
474 fling.cancel();
475 }
476 gs.drag_down_position = None;
477 gs.last_position = None;
478 gs.is_dragging = false;
479 gs.gesture_start_time = None;
480 gs.last_velocity_sample_ms = None;
481 gs.velocity_tracker.reset();
482 }
483
484 self.motion_context.activate_for_next_frame();
485
486 let delta = if self.reverse_scrolling {
487 -axis_delta
488 } else {
489 axis_delta
490 };
491 let consumed = self.scroll_target.apply_delta(delta);
492 if consumed.abs() > 0.001 {
493 self.scroll_target.invalidate();
494 true
495 } else {
496 false
497 }
498 }
499}
500
501pub(crate) struct MotionContextAnimatedNode {
502 state: NodeState,
503 motion_context: ScrollMotionContext,
504 invalidation_callback_id: Option<u64>,
505 node_id: Option<NodeId>,
506}
507
508impl MotionContextAnimatedNode {
509 fn new(motion_context: ScrollMotionContext) -> Self {
510 Self {
511 state: NodeState::new(),
512 motion_context,
513 invalidation_callback_id: None,
514 node_id: None,
515 }
516 }
517
518 pub(crate) fn is_active(&self) -> bool {
519 self.motion_context.is_active()
520 }
521}
522
523pub(crate) struct TranslatedContentContextNode {
524 state: NodeState,
525 identity: usize,
526 offset_source: TranslatedContentOffsetSource,
527}
528
529impl TranslatedContentContextNode {
530 fn new(identity: usize, offset_source: TranslatedContentOffsetSource) -> Self {
531 Self {
532 state: NodeState::new(),
533 identity,
534 offset_source,
535 }
536 }
537
538 pub(crate) fn is_active(&self) -> bool {
539 true
540 }
541
542 pub(crate) fn identity(&self) -> usize {
543 self.identity
544 }
545
546 pub(crate) fn content_offset_reader(&self) -> Option<Rc<dyn Fn() -> Point>> {
547 self.offset_source.content_offset_reader()
548 }
549}
550
551impl DelegatableNode for TranslatedContentContextNode {
552 fn node_state(&self) -> &NodeState {
553 &self.state
554 }
555}
556
557impl ModifierNode for TranslatedContentContextNode {}
558
559impl DelegatableNode for MotionContextAnimatedNode {
560 fn node_state(&self) -> &NodeState {
561 &self.state
562 }
563}
564
565impl ModifierNode for MotionContextAnimatedNode {
566 fn on_attach(&mut self, context: &mut dyn cranpose_foundation::ModifierNodeContext) {
567 let node_id = context.node_id();
568 self.node_id = node_id;
569 if let Some(node_id) = node_id {
570 let callback_id = self
571 .motion_context
572 .add_invalidate_callback(Box::new(move || {
573 schedule_modifier_slices_repass(node_id);
574 }));
575 self.invalidation_callback_id = Some(callback_id);
576 }
577 }
578
579 fn on_detach(&mut self) {
580 if let Some(id) = self.invalidation_callback_id.take() {
581 self.motion_context.remove_invalidate_callback(id);
582 }
583 self.node_id = None;
584 }
585}
586
587#[derive(Clone)]
588struct MotionContextAnimatedElement {
589 motion_context: ScrollMotionContext,
590}
591
592impl MotionContextAnimatedElement {
593 fn new(motion_context: ScrollMotionContext) -> Self {
594 Self { motion_context }
595 }
596}
597
598impl std::fmt::Debug for MotionContextAnimatedElement {
599 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
600 f.debug_struct("MotionContextAnimatedElement").finish()
601 }
602}
603
604impl PartialEq for MotionContextAnimatedElement {
605 fn eq(&self, other: &Self) -> bool {
606 self.motion_context.ptr_eq(&other.motion_context)
607 }
608}
609
610impl Eq for MotionContextAnimatedElement {}
611
612impl std::hash::Hash for MotionContextAnimatedElement {
613 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
614 self.motion_context.stable_key().hash(state);
615 }
616}
617
618impl ModifierNodeElement for MotionContextAnimatedElement {
619 type Node = MotionContextAnimatedNode;
620
621 fn create(&self) -> Self::Node {
622 MotionContextAnimatedNode::new(self.motion_context.clone())
623 }
624
625 fn update(&self, node: &mut Self::Node) {
626 if node.motion_context.ptr_eq(&self.motion_context) {
627 return;
628 }
629 if let Some(id) = node.invalidation_callback_id.take() {
630 node.motion_context.remove_invalidate_callback(id);
631 }
632 node.motion_context = self.motion_context.clone();
633 if let Some(node_id) = node.node_id {
634 let callback_id = node
635 .motion_context
636 .add_invalidate_callback(Box::new(move || {
637 schedule_modifier_slices_repass(node_id);
638 }));
639 node.invalidation_callback_id = Some(callback_id);
640 }
641 }
642
643 fn capabilities(&self) -> NodeCapabilities {
644 NodeCapabilities::LAYOUT
645 }
646}
647
648#[derive(Clone)]
649enum TranslatedContentOffsetSource {
650 LayoutContentOffset,
651 LazyList {
652 state: LazyListState,
653 is_vertical: bool,
654 reverse_scrolling: bool,
655 },
656}
657
658impl TranslatedContentOffsetSource {
659 fn content_offset_reader(&self) -> Option<Rc<dyn Fn() -> Point>> {
660 match self {
661 Self::LayoutContentOffset => None,
662 Self::LazyList {
663 state, is_vertical, ..
664 } => Some(Rc::new(lazy_list_content_offset_reader(
665 *state,
666 *is_vertical,
667 ))),
668 }
669 }
670
671 fn is_vertical(&self) -> Option<bool> {
672 match self {
673 Self::LayoutContentOffset => None,
674 Self::LazyList { is_vertical, .. } => Some(*is_vertical),
675 }
676 }
677
678 fn reverse_scrolling(&self) -> Option<bool> {
679 match self {
680 Self::LayoutContentOffset => None,
681 Self::LazyList {
682 reverse_scrolling, ..
683 } => Some(*reverse_scrolling),
684 }
685 }
686}
687
688fn lazy_list_content_offset_reader(state: LazyListState, is_vertical: bool) -> impl Fn() -> Point {
689 move || {
690 let info = state.layout_info();
691 if info.visible_items_info.is_empty() {
692 return Point::default();
693 };
694 let main_offset = info.snap_anchor_offset;
695 if is_vertical {
696 Point::new(0.0, main_offset)
697 } else {
698 Point::new(main_offset, 0.0)
699 }
700 }
701}
702
703#[derive(Clone)]
704struct TranslatedContentContextElement {
705 identity: usize,
706 offset_source: TranslatedContentOffsetSource,
707}
708
709impl TranslatedContentContextElement {
710 fn new(identity: usize, offset_source: TranslatedContentOffsetSource) -> Self {
711 Self {
712 identity,
713 offset_source,
714 }
715 }
716}
717
718impl std::fmt::Debug for TranslatedContentContextElement {
719 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
720 let offset_source = match &self.offset_source {
721 TranslatedContentOffsetSource::LayoutContentOffset => "layout",
722 TranslatedContentOffsetSource::LazyList { .. } => "lazy_list",
723 };
724 f.debug_struct("TranslatedContentContextElement")
725 .field("identity", &self.identity)
726 .field("offset_source", &offset_source)
727 .finish()
728 }
729}
730
731impl PartialEq for TranslatedContentContextElement {
732 fn eq(&self, other: &Self) -> bool {
733 self.identity == other.identity
734 && self.offset_source.is_vertical() == other.offset_source.is_vertical()
735 && self.offset_source.reverse_scrolling() == other.offset_source.reverse_scrolling()
736 }
737}
738
739impl Eq for TranslatedContentContextElement {}
740
741impl std::hash::Hash for TranslatedContentContextElement {
742 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
743 self.identity.hash(state);
744 self.offset_source.is_vertical().hash(state);
745 self.offset_source.reverse_scrolling().hash(state);
746 }
747}
748
749impl ModifierNodeElement for TranslatedContentContextElement {
750 type Node = TranslatedContentContextNode;
751
752 fn create(&self) -> Self::Node {
753 TranslatedContentContextNode::new(self.identity, self.offset_source.clone())
754 }
755
756 fn update(&self, node: &mut Self::Node) {
757 node.identity = self.identity;
758 node.offset_source = self.offset_source.clone();
759 }
760
761 fn capabilities(&self) -> NodeCapabilities {
762 NodeCapabilities::LAYOUT
763 }
764}
765
766impl Modifier {
771 pub fn horizontal_scroll(self, state: ScrollState, reverse_scrolling: bool) -> Self {
789 self.then(scroll_impl(state, false, reverse_scrolling, None))
790 }
791
792 pub fn vertical_scroll(self, state: ScrollState, reverse_scrolling: bool) -> Self {
801 self.then(scroll_impl(state, true, reverse_scrolling, None))
802 }
803
804 pub fn horizontal_scroll_guarded(
806 self,
807 state: ScrollState,
808 reverse_scrolling: bool,
809 guard: impl Fn() -> bool + 'static,
810 ) -> Self {
811 self.then(scroll_impl(
812 state,
813 false,
814 reverse_scrolling,
815 Some(Rc::new(guard)),
816 ))
817 }
818
819 pub fn vertical_scroll_guarded(
821 self,
822 state: ScrollState,
823 reverse_scrolling: bool,
824 guard: impl Fn() -> bool + 'static,
825 ) -> Self {
826 self.then(scroll_impl(
827 state,
828 true,
829 reverse_scrolling,
830 Some(Rc::new(guard)),
831 ))
832 }
833}
834
835fn scroll_impl(
844 state: ScrollState,
845 is_vertical: bool,
846 reverse_scrolling: bool,
847 guard: Option<Rc<dyn Fn() -> bool>>,
848) -> Modifier {
849 let gesture_state = Rc::new(RefCell::new(ScrollGestureState::default()));
851 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::ScrollState {
852 state_id: state.id(),
853 is_vertical,
854 reverse_scrolling,
855 });
856
857 let scroll_state = state.clone();
859 let pointer_motion_context = motion_context.clone();
860 let key = (state.id(), is_vertical);
861 let pointer_input = Modifier::empty().pointer_input(key, move |scope| {
862 let detector = ScrollGestureDetector::new(
864 gesture_state.clone(),
865 scroll_state.clone(),
866 is_vertical,
867 false, pointer_motion_context.clone(),
869 );
870 let guard = guard.clone();
871
872 async move {
873 scope
874 .await_pointer_event_scope(|await_scope| async move {
875 loop {
877 let event = await_scope.await_pointer_event().await;
878
879 if let Some(ref guard) = guard {
880 if !guard() {
881 if matches!(
882 event.kind,
883 PointerEventKind::Up | PointerEventKind::Cancel
884 ) {
885 detector.on_cancel();
886 }
887 continue;
888 }
889 }
890
891 let should_consume = match event.kind {
893 PointerEventKind::Down => detector.on_down(event.position),
894 PointerEventKind::Move => {
895 detector.on_move(event.position, event.buttons)
896 }
897 PointerEventKind::Up => detector.on_up(),
898 PointerEventKind::Cancel => detector.on_cancel(),
899 PointerEventKind::Scroll => detector.on_scroll(if is_vertical {
900 event.scroll_delta.y
901 } else {
902 event.scroll_delta.x
903 }),
904 PointerEventKind::Enter | PointerEventKind::Exit => false,
905 };
906
907 if should_consume {
908 event.consume();
909 }
910 }
911 })
912 .await;
913 }
914 });
915
916 let element = ScrollElement::new(state.clone(), is_vertical, reverse_scrolling);
918 let layout_modifier =
919 Modifier::with_element(element).with_inspector_metadata(inspector_metadata(
920 if is_vertical {
921 "verticalScroll"
922 } else {
923 "horizontalScroll"
924 },
925 move |info| {
926 info.add_property("isVertical", is_vertical.to_string());
927 info.add_property("reverseScrolling", reverse_scrolling.to_string());
928 },
929 ));
930 let motion_modifier =
931 Modifier::with_element(MotionContextAnimatedElement::new(motion_context.clone()));
932 let translated_content_modifier = Modifier::with_element(TranslatedContentContextElement::new(
933 state.id() as usize,
934 TranslatedContentOffsetSource::LayoutContentOffset,
935 ));
936
937 pointer_input
939 .then(motion_modifier)
940 .then(translated_content_modifier)
941 .then(layout_modifier)
942 .clip_to_bounds()
943}
944
945use cranpose_foundation::lazy::LazyListState;
950
951impl Modifier {
952 pub fn lazy_vertical_scroll(self, state: LazyListState, reverse_scrolling: bool) -> Self {
963 self.then(lazy_scroll_impl(state, true, reverse_scrolling))
964 }
965
966 pub fn lazy_horizontal_scroll(self, state: LazyListState, reverse_scrolling: bool) -> Self {
968 self.then(lazy_scroll_impl(state, false, reverse_scrolling))
969 }
970}
971
972fn lazy_scroll_impl(state: LazyListState, is_vertical: bool, reverse_scrolling: bool) -> Modifier {
974 let gesture_state = Rc::new(RefCell::new(ScrollGestureState::default()));
975 let list_state = state;
976 let state_id = state.inner_ptr() as usize;
977 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::LazyList {
978 state_identity: state_id,
979 is_vertical,
980 reverse_scrolling,
981 });
982 let key = (state_id, is_vertical, reverse_scrolling);
983 let translated_content_modifier = Modifier::with_element(TranslatedContentContextElement::new(
984 state_id,
985 TranslatedContentOffsetSource::LazyList {
986 state,
987 is_vertical,
988 reverse_scrolling,
989 },
990 ));
991
992 Modifier::with_element(MotionContextAnimatedElement::new(motion_context.clone()))
993 .then(translated_content_modifier)
994 .pointer_input(key, move |scope| {
995 let detector = ScrollGestureDetector::new(
997 gesture_state.clone(),
998 list_state,
999 is_vertical,
1000 reverse_scrolling,
1001 motion_context.clone(),
1002 );
1003
1004 async move {
1005 scope
1006 .await_pointer_event_scope(|await_scope| async move {
1007 loop {
1008 let event = await_scope.await_pointer_event().await;
1009
1010 let should_consume = match event.kind {
1012 PointerEventKind::Down => detector.on_down(event.position),
1013 PointerEventKind::Move => {
1014 detector.on_move(event.position, event.buttons)
1015 }
1016 PointerEventKind::Up => detector.on_up(),
1017 PointerEventKind::Cancel => detector.on_cancel(),
1018 PointerEventKind::Scroll => detector.on_scroll(if is_vertical {
1019 event.scroll_delta.y
1020 } else {
1021 event.scroll_delta.x
1022 }),
1023 PointerEventKind::Enter | PointerEventKind::Exit => false,
1024 };
1025
1026 if should_consume {
1027 event.consume();
1028 }
1029 }
1030 })
1031 .await;
1032 }
1033 })
1034}