1use super::{inspector_metadata, Modifier, Point, PointerEventKind};
27use crate::current_density;
28use crate::fling_animation::FlingAnimation;
29use crate::fling_animation::MIN_FLING_VELOCITY;
30use crate::render_state::schedule_modifier_slices_repass;
31use crate::scroll::{
32 scroll_motion_context_for_key, ScrollElement, ScrollMotionContext, ScrollMotionContextKey,
33 ScrollState,
34};
35use cranpose_core::{current_runtime_handle, NodeId};
36use cranpose_foundation::{
37 velocity_tracker::ASSUME_STOPPED_MS, DelegatableNode, ModifierNode, ModifierNodeElement,
38 NodeCapabilities, NodeState, PointerButton, PointerButtons, VelocityTracker1D, DRAG_THRESHOLD,
39 MAX_FLING_VELOCITY,
40};
41use std::cell::RefCell;
42use std::rc::Rc;
43use web_time::Instant;
44
45#[cfg(feature = "test-helpers")]
46pub fn last_fling_velocity() -> f32 {
47 crate::render_state::debug_last_fling_velocity()
48}
49
50#[cfg(feature = "test-helpers")]
51pub fn reset_last_fling_velocity() {
52 crate::render_state::debug_reset_last_fling_velocity();
53}
54
55#[inline]
56fn set_last_fling_velocity(velocity: f32) {
57 crate::render_state::record_last_fling_velocity(velocity);
58}
59
60struct ScrollGestureState {
66 drag_down_position: Option<Point>,
69
70 last_position: Option<Point>,
73
74 is_dragging: bool,
78
79 axis_locked_out: bool,
85
86 velocity_tracker: VelocityTracker1D,
88
89 gesture_start_time: Option<Instant>,
91
92 gesture_start_event_time_ms: Option<i64>,
96
97 last_velocity_sample_ms: Option<i64>,
99
100 fling_animation: Option<FlingAnimation>,
102}
103
104impl Default for ScrollGestureState {
105 fn default() -> Self {
106 Self {
107 drag_down_position: None,
108 last_position: None,
109 is_dragging: false,
110 axis_locked_out: false,
111 velocity_tracker: VelocityTracker1D::new(),
112 gesture_start_time: None,
113 gesture_start_event_time_ms: None,
114 last_velocity_sample_ms: None,
115 fling_animation: None,
116 }
117 }
118}
119
120#[inline]
130fn calculate_total_delta(from: Point, to: Point, is_vertical: bool) -> f32 {
131 if is_vertical {
132 to.y - from.y
133 } else {
134 to.x - from.x
135 }
136}
137
138#[inline]
143fn calculate_incremental_delta(from: Point, to: Point, is_vertical: bool) -> f32 {
144 if is_vertical {
145 to.y - from.y
146 } else {
147 to.x - from.x
148 }
149}
150
151trait ScrollTarget: Clone {
159 fn apply_delta(&self, delta: f32) -> f32;
161
162 fn apply_wheel_delta(&self, delta: f32) -> f32 {
164 self.apply_delta(delta)
165 }
166
167 fn apply_fling_delta(&self, delta: f32) -> f32;
169
170 fn invalidate(&self);
172
173 fn current_offset(&self) -> f32;
175
176 fn can_scroll(&self) -> bool {
184 true
185 }
186}
187
188impl ScrollTarget for ScrollState {
189 fn apply_delta(&self, delta: f32) -> f32 {
190 self.dispatch_raw_delta(-delta)
192 }
193
194 fn apply_fling_delta(&self, delta: f32) -> f32 {
195 self.dispatch_raw_delta(delta)
196 }
197
198 fn invalidate(&self) {
199 }
201
202 fn current_offset(&self) -> f32 {
203 self.value()
204 }
205
206 fn can_scroll(&self) -> bool {
207 self.max_value() > 0.0
208 }
209}
210
211impl ScrollTarget for LazyListState {
212 fn apply_delta(&self, delta: f32) -> f32 {
213 self.dispatch_scroll_delta(delta)
217 }
218
219 fn apply_wheel_delta(&self, delta: f32) -> f32 {
220 if delta.abs() <= 0.001 {
221 0.0
222 } else {
223 self.dispatch_scroll_delta(delta)
224 }
225 }
226
227 fn apply_fling_delta(&self, delta: f32) -> f32 {
228 -self.dispatch_scroll_delta(-delta)
229 }
230
231 fn invalidate(&self) {
232 }
235
236 fn current_offset(&self) -> f32 {
237 self.first_visible_item_scroll_offset()
239 }
240
241 fn can_scroll(&self) -> bool {
242 self.layout_info().total_items_count == 0
245 || self.can_scroll_forward_non_reactive()
246 || self.can_scroll_backward_non_reactive()
247 }
248}
249
250struct ScrollGestureDetector<S: ScrollTarget> {
256 gesture_state: Rc<RefCell<ScrollGestureState>>,
258
259 scroll_target: S,
261
262 is_vertical: bool,
264
265 reverse_scrolling: bool,
267
268 motion_context: ScrollMotionContext,
270}
271
272impl<S: ScrollTarget + 'static> ScrollGestureDetector<S> {
273 fn new(
275 gesture_state: Rc<RefCell<ScrollGestureState>>,
276 scroll_target: S,
277 is_vertical: bool,
278 reverse_scrolling: bool,
279 motion_context: ScrollMotionContext,
280 ) -> Self {
281 Self {
282 gesture_state,
283 scroll_target,
284 is_vertical,
285 reverse_scrolling,
286 motion_context,
287 }
288 }
289
290 fn on_down(&self, position: Point, time_ms: Option<i64>) -> bool {
299 let mut gs = self.gesture_state.borrow_mut();
300
301 if let Some(fling) = gs.fling_animation.take() {
303 fling.cancel();
304 }
305 self.motion_context.set_active(false);
306
307 gs.drag_down_position = Some(position);
308 gs.last_position = Some(position);
309 gs.is_dragging = false;
310 gs.axis_locked_out = false;
311 gs.velocity_tracker.reset();
312 gs.gesture_start_time = Some(Instant::now());
313 gs.gesture_start_event_time_ms = time_ms;
314
315 let pos = if self.is_vertical {
317 position.y
318 } else {
319 position.x
320 };
321 gs.velocity_tracker.add_data_point(0, pos);
322 gs.last_velocity_sample_ms = Some(0);
323
324 false
326 }
327
328 fn on_move(&self, position: Point, buttons: PointerButtons, time_ms: Option<i64>) -> bool {
342 let mut gs = self.gesture_state.borrow_mut();
343
344 if !buttons.contains(PointerButton::Primary) && gs.drag_down_position.is_some() {
346 gs.drag_down_position = None;
347 gs.last_position = None;
348 gs.is_dragging = false;
349 gs.axis_locked_out = false;
350 gs.gesture_start_time = None;
351 gs.gesture_start_event_time_ms = None;
352 gs.last_velocity_sample_ms = None;
353 gs.velocity_tracker.reset();
354 self.motion_context.set_active(false);
355 return false;
356 }
357
358 let Some(down_pos) = gs.drag_down_position else {
359 return false;
360 };
361
362 let Some(last_pos) = gs.last_position else {
363 gs.last_position = Some(position);
364 return false;
365 };
366
367 let incremental_delta = calculate_incremental_delta(last_pos, position, self.is_vertical);
368
369 if !gs.is_dragging && !gs.axis_locked_out {
379 let main_delta = calculate_total_delta(down_pos, position, self.is_vertical).abs();
380 let cross_delta = calculate_total_delta(down_pos, position, !self.is_vertical).abs();
381 if main_delta > DRAG_THRESHOLD && main_delta >= cross_delta {
382 if self.scroll_target.can_scroll() {
383 gs.is_dragging = true;
384 self.motion_context.set_active(true);
385 }
386 } else if cross_delta > DRAG_THRESHOLD && cross_delta > main_delta {
387 gs.axis_locked_out = true;
388 }
389 }
390
391 gs.last_position = Some(position);
392
393 let pos = if self.is_vertical {
395 position.y
396 } else {
397 position.x
398 };
399 let event_sample_ms = gs
400 .gesture_start_event_time_ms
401 .zip(time_ms)
402 .map(|(start_ms, now_ms)| now_ms - start_ms);
403 let sample_ms = if let Some(event_sample_ms) = event_sample_ms {
404 Some(match gs.last_velocity_sample_ms {
411 Some(last_sample_ms) => event_sample_ms.max(last_sample_ms),
412 None => event_sample_ms.max(0),
413 })
414 } else if let Some(start_time) = gs.gesture_start_time {
415 let elapsed_ms = start_time.elapsed().as_millis() as i64;
418 Some(match gs.last_velocity_sample_ms {
421 Some(last_sample_ms) => {
422 let mut sample_ms = if elapsed_ms <= last_sample_ms {
423 last_sample_ms + 1
424 } else {
425 elapsed_ms
426 };
427 if sample_ms - last_sample_ms > ASSUME_STOPPED_MS {
429 sample_ms = last_sample_ms + ASSUME_STOPPED_MS;
430 }
431 sample_ms
432 }
433 None => elapsed_ms,
434 })
435 } else {
436 None
437 };
438 if let Some(sample_ms) = sample_ms {
439 log::trace!(
440 target: "cranpose::velocity",
441 "sample t={sample_ms}ms pos={pos:.2} event_time={time_ms:?}"
442 );
443 gs.velocity_tracker.add_data_point(sample_ms, pos);
444 gs.last_velocity_sample_ms = Some(sample_ms);
445 }
446
447 if gs.is_dragging {
448 drop(gs); let delta = if self.reverse_scrolling {
450 -incremental_delta
451 } else {
452 incremental_delta
453 };
454 let _ = self.scroll_target.apply_delta(delta);
455 self.scroll_target.invalidate();
456 true } else {
458 false
459 }
460 }
461
462 fn finish_gesture(&self, allow_fling: bool) -> bool {
469 let (was_dragging, velocity, start_fling, existing_fling) = {
470 let mut gs = self.gesture_state.borrow_mut();
471 let was_dragging = gs.is_dragging;
472 let mut velocity = 0.0;
473
474 if allow_fling && was_dragging && gs.gesture_start_time.is_some() {
475 velocity = gs
476 .velocity_tracker
477 .calculate_velocity_with_max(MAX_FLING_VELOCITY);
478 }
479
480 let start_fling = allow_fling && was_dragging && velocity.abs() > MIN_FLING_VELOCITY;
481 let existing_fling = if start_fling {
482 gs.fling_animation.take()
483 } else {
484 None
485 };
486
487 gs.drag_down_position = None;
488 gs.last_position = None;
489 gs.is_dragging = false;
490 gs.axis_locked_out = false;
491 gs.gesture_start_time = None;
492 gs.gesture_start_event_time_ms = None;
493 gs.last_velocity_sample_ms = None;
494
495 (was_dragging, velocity, start_fling, existing_fling)
496 };
497
498 if allow_fling && was_dragging {
500 log::debug!(
501 target: "cranpose::velocity",
502 "gesture finished: fling velocity={velocity:.2} dp/s start_fling={start_fling}"
503 );
504 set_last_fling_velocity(velocity);
505 }
506
507 if start_fling {
509 if let Some(old_fling) = existing_fling {
510 old_fling.cancel();
511 }
512
513 if let Some(runtime) = current_runtime_handle() {
515 self.motion_context.set_active(true);
516 let scroll_target = self.scroll_target.clone();
517 let reverse = self.reverse_scrolling;
518 let fling = FlingAnimation::new(runtime);
519 let motion_context = self.motion_context.clone();
520
521 let initial_value = scroll_target.current_offset();
523
524 let adjusted_velocity = if reverse { -velocity } else { velocity };
526 let fling_velocity = -adjusted_velocity;
527
528 let scroll_target_for_fling = scroll_target.clone();
529 let scroll_target_for_end = scroll_target.clone();
530
531 fling.start_fling(
532 initial_value,
533 fling_velocity,
534 current_density(),
535 move |delta| {
536 let consumed = scroll_target_for_fling.apply_fling_delta(delta);
538 scroll_target_for_fling.invalidate();
539 consumed
540 },
541 move || {
542 scroll_target_for_end.invalidate();
544 motion_context.set_active(false);
545 },
546 );
547
548 let mut gs = self.gesture_state.borrow_mut();
549 gs.fling_animation = Some(fling);
550 }
551 } else {
552 self.motion_context.set_active(false);
553 }
554
555 was_dragging
556 }
557
558 fn on_up(&self) -> bool {
565 self.finish_gesture(true)
566 }
567
568 fn on_cancel(&self) -> bool {
572 self.finish_gesture(false)
573 }
574
575 fn on_scroll(&self, axis_delta: f32) -> bool {
579 if axis_delta.abs() <= f32::EPSILON {
580 return false;
581 }
582
583 {
584 let mut gs = self.gesture_state.borrow_mut();
586 if let Some(fling) = gs.fling_animation.take() {
587 fling.cancel();
588 }
589 gs.drag_down_position = None;
590 gs.last_position = None;
591 gs.is_dragging = false;
592 gs.axis_locked_out = false;
593 gs.gesture_start_time = None;
594 gs.gesture_start_event_time_ms = None;
595 gs.last_velocity_sample_ms = None;
596 gs.velocity_tracker.reset();
597 }
598
599 self.motion_context.activate_for_current_frame();
600
601 let delta = if self.reverse_scrolling {
602 -axis_delta
603 } else {
604 axis_delta
605 };
606 let consumed = self.scroll_target.apply_wheel_delta(delta);
607 if consumed.abs() > 0.001 {
608 self.scroll_target.invalidate();
609 true
610 } else {
611 false
612 }
613 }
614}
615
616pub(crate) struct MotionContextAnimatedNode {
617 state: NodeState,
618 motion_context: ScrollMotionContext,
619 invalidation_callback_id: Option<u64>,
620 node_id: Option<NodeId>,
621}
622
623impl MotionContextAnimatedNode {
624 fn new(motion_context: ScrollMotionContext) -> Self {
625 Self {
626 state: NodeState::new(),
627 motion_context,
628 invalidation_callback_id: None,
629 node_id: None,
630 }
631 }
632
633 pub(crate) fn is_active(&self) -> bool {
634 self.motion_context.is_active()
635 }
636}
637
638pub(crate) struct TranslatedContentContextNode {
639 state: NodeState,
640 identity: usize,
641 offset_source: TranslatedContentOffsetSource,
642}
643
644impl TranslatedContentContextNode {
645 fn new(identity: usize, offset_source: TranslatedContentOffsetSource) -> Self {
646 Self {
647 state: NodeState::new(),
648 identity,
649 offset_source,
650 }
651 }
652
653 pub(crate) fn is_active(&self) -> bool {
654 true
655 }
656
657 pub(crate) fn identity(&self) -> usize {
658 self.identity
659 }
660
661 pub(crate) fn content_offset_reader(&self) -> Option<Rc<dyn Fn() -> Point>> {
662 self.offset_source.content_offset_reader()
663 }
664}
665
666impl DelegatableNode for TranslatedContentContextNode {
667 fn node_state(&self) -> &NodeState {
668 &self.state
669 }
670}
671
672impl ModifierNode for TranslatedContentContextNode {}
673
674impl DelegatableNode for MotionContextAnimatedNode {
675 fn node_state(&self) -> &NodeState {
676 &self.state
677 }
678}
679
680impl ModifierNode for MotionContextAnimatedNode {
681 fn on_attach(&mut self, context: &mut dyn cranpose_foundation::ModifierNodeContext) {
682 let node_id = context.node_id();
683 self.node_id = node_id;
684 if let Some(node_id) = node_id {
685 let callback_id = self
686 .motion_context
687 .add_invalidate_callback(Box::new(move || {
688 schedule_modifier_slices_repass(node_id);
689 }));
690 self.invalidation_callback_id = Some(callback_id);
691 }
692 }
693
694 fn on_detach(&mut self) {
695 if let Some(id) = self.invalidation_callback_id.take() {
696 self.motion_context.remove_invalidate_callback(id);
697 }
698 self.node_id = None;
699 }
700}
701
702#[derive(Clone)]
703struct MotionContextAnimatedElement {
704 motion_context: ScrollMotionContext,
705}
706
707impl MotionContextAnimatedElement {
708 fn new(motion_context: ScrollMotionContext) -> Self {
709 Self { motion_context }
710 }
711}
712
713impl std::fmt::Debug for MotionContextAnimatedElement {
714 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
715 f.debug_struct("MotionContextAnimatedElement").finish()
716 }
717}
718
719impl PartialEq for MotionContextAnimatedElement {
720 fn eq(&self, other: &Self) -> bool {
721 self.motion_context.ptr_eq(&other.motion_context)
722 }
723}
724
725impl Eq for MotionContextAnimatedElement {}
726
727impl std::hash::Hash for MotionContextAnimatedElement {
728 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
729 self.motion_context.stable_key().hash(state);
730 }
731}
732
733impl ModifierNodeElement for MotionContextAnimatedElement {
734 type Node = MotionContextAnimatedNode;
735
736 fn create(&self) -> Self::Node {
737 MotionContextAnimatedNode::new(self.motion_context.clone())
738 }
739
740 fn update(&self, node: &mut Self::Node) {
741 if node.motion_context.ptr_eq(&self.motion_context) {
742 return;
743 }
744 if let Some(id) = node.invalidation_callback_id.take() {
745 node.motion_context.remove_invalidate_callback(id);
746 }
747 node.motion_context = self.motion_context.clone();
748 if let Some(node_id) = node.node_id {
749 let callback_id = node
750 .motion_context
751 .add_invalidate_callback(Box::new(move || {
752 schedule_modifier_slices_repass(node_id);
753 }));
754 node.invalidation_callback_id = Some(callback_id);
755 }
756 }
757
758 fn capabilities(&self) -> NodeCapabilities {
759 NodeCapabilities::LAYOUT
760 }
761}
762
763#[derive(Clone)]
764enum TranslatedContentOffsetSource {
765 LayoutContentOffset,
766 LazyList {
767 state: LazyListState,
768 is_vertical: bool,
769 reverse_scrolling: bool,
770 },
771}
772
773impl TranslatedContentOffsetSource {
774 fn content_offset_reader(&self) -> Option<Rc<dyn Fn() -> Point>> {
775 match self {
776 Self::LayoutContentOffset => None,
777 Self::LazyList {
778 state, is_vertical, ..
779 } => Some(Rc::new(lazy_list_content_offset_reader(
780 *state,
781 *is_vertical,
782 ))),
783 }
784 }
785
786 fn is_vertical(&self) -> Option<bool> {
787 match self {
788 Self::LayoutContentOffset => None,
789 Self::LazyList { is_vertical, .. } => Some(*is_vertical),
790 }
791 }
792
793 fn reverse_scrolling(&self) -> Option<bool> {
794 match self {
795 Self::LayoutContentOffset => None,
796 Self::LazyList {
797 reverse_scrolling, ..
798 } => Some(*reverse_scrolling),
799 }
800 }
801}
802
803fn lazy_list_content_offset_reader(state: LazyListState, is_vertical: bool) -> impl Fn() -> Point {
804 move || {
805 let info = state.layout_info();
806 if info.visible_items_info.is_empty() {
807 return Point::default();
808 };
809 let main_offset = info.snap_anchor_offset;
810 if is_vertical {
811 Point::new(0.0, main_offset)
812 } else {
813 Point::new(main_offset, 0.0)
814 }
815 }
816}
817
818#[derive(Clone)]
819struct TranslatedContentContextElement {
820 identity: usize,
821 offset_source: TranslatedContentOffsetSource,
822}
823
824impl TranslatedContentContextElement {
825 fn new(identity: usize, offset_source: TranslatedContentOffsetSource) -> Self {
826 Self {
827 identity,
828 offset_source,
829 }
830 }
831}
832
833impl std::fmt::Debug for TranslatedContentContextElement {
834 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
835 let offset_source = match &self.offset_source {
836 TranslatedContentOffsetSource::LayoutContentOffset => "layout",
837 TranslatedContentOffsetSource::LazyList { .. } => "lazy_list",
838 };
839 f.debug_struct("TranslatedContentContextElement")
840 .field("identity", &self.identity)
841 .field("offset_source", &offset_source)
842 .finish()
843 }
844}
845
846impl PartialEq for TranslatedContentContextElement {
847 fn eq(&self, other: &Self) -> bool {
848 self.identity == other.identity
849 && self.offset_source.is_vertical() == other.offset_source.is_vertical()
850 && self.offset_source.reverse_scrolling() == other.offset_source.reverse_scrolling()
851 }
852}
853
854impl Eq for TranslatedContentContextElement {}
855
856impl std::hash::Hash for TranslatedContentContextElement {
857 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
858 self.identity.hash(state);
859 self.offset_source.is_vertical().hash(state);
860 self.offset_source.reverse_scrolling().hash(state);
861 }
862}
863
864impl ModifierNodeElement for TranslatedContentContextElement {
865 type Node = TranslatedContentContextNode;
866
867 fn create(&self) -> Self::Node {
868 TranslatedContentContextNode::new(self.identity, self.offset_source.clone())
869 }
870
871 fn update(&self, node: &mut Self::Node) {
872 node.identity = self.identity;
873 node.offset_source = self.offset_source.clone();
874 }
875
876 fn capabilities(&self) -> NodeCapabilities {
877 NodeCapabilities::LAYOUT
878 }
879}
880
881impl Modifier {
886 pub fn horizontal_scroll(self, state: ScrollState, reverse_scrolling: bool) -> Self {
904 self.then(scroll_impl(state, false, reverse_scrolling, None))
905 }
906
907 pub fn vertical_scroll(self, state: ScrollState, reverse_scrolling: bool) -> Self {
916 self.then(scroll_impl(state, true, reverse_scrolling, None))
917 }
918
919 pub fn horizontal_scroll_guarded(
921 self,
922 state: ScrollState,
923 reverse_scrolling: bool,
924 guard: impl Fn() -> bool + 'static,
925 ) -> Self {
926 self.then(scroll_impl(
927 state,
928 false,
929 reverse_scrolling,
930 Some(Rc::new(guard)),
931 ))
932 }
933
934 pub fn vertical_scroll_guarded(
936 self,
937 state: ScrollState,
938 reverse_scrolling: bool,
939 guard: impl Fn() -> bool + 'static,
940 ) -> Self {
941 self.then(scroll_impl(
942 state,
943 true,
944 reverse_scrolling,
945 Some(Rc::new(guard)),
946 ))
947 }
948}
949
950fn scroll_impl(
959 state: ScrollState,
960 is_vertical: bool,
961 reverse_scrolling: bool,
962 guard: Option<Rc<dyn Fn() -> bool>>,
963) -> Modifier {
964 let gesture_state = Rc::new(RefCell::new(ScrollGestureState::default()));
966 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::ScrollState {
967 state_id: state.id(),
968 is_vertical,
969 reverse_scrolling,
970 });
971
972 let scroll_state = state.clone();
974 let pointer_motion_context = motion_context.clone();
975 let key = (state.id(), is_vertical);
976 let pointer_input = Modifier::empty().pointer_input(key, move |scope| {
977 let detector = ScrollGestureDetector::new(
979 gesture_state.clone(),
980 scroll_state.clone(),
981 is_vertical,
982 false, pointer_motion_context.clone(),
984 );
985 let guard = guard.clone();
986
987 async move {
988 scope
989 .await_pointer_event_scope(|await_scope| async move {
990 loop {
992 let event = await_scope.await_pointer_event().await;
993
994 if event.id != 0 {
998 continue;
999 }
1000
1001 if event.is_consumed() {
1002 if matches!(
1003 event.kind,
1004 PointerEventKind::Down
1005 | PointerEventKind::Move
1006 | PointerEventKind::Up
1007 | PointerEventKind::Cancel
1008 ) {
1009 detector.on_cancel();
1010 }
1011 continue;
1012 }
1013
1014 if let Some(ref guard) = guard {
1015 if !guard() {
1016 if matches!(
1017 event.kind,
1018 PointerEventKind::Up | PointerEventKind::Cancel
1019 ) {
1020 detector.on_cancel();
1021 }
1022 continue;
1023 }
1024 }
1025
1026 let should_consume = match event.kind {
1028 PointerEventKind::Down => {
1029 detector.on_down(event.position, event.time_ms)
1030 }
1031 PointerEventKind::Move => {
1032 detector.on_move(event.position, event.buttons, event.time_ms)
1033 }
1034 PointerEventKind::Up => detector.on_up(),
1035 PointerEventKind::Cancel => detector.on_cancel(),
1036 PointerEventKind::Scroll => detector.on_scroll(if is_vertical {
1037 event.scroll_delta.y
1038 } else {
1039 event.scroll_delta.x
1040 }),
1041 PointerEventKind::Zoom
1042 | PointerEventKind::Enter
1043 | PointerEventKind::Exit => false,
1044 };
1045
1046 if should_consume {
1047 event.consume();
1048 }
1049 }
1050 })
1051 .await;
1052 }
1053 });
1054
1055 let element = ScrollElement::new(state.clone(), is_vertical, reverse_scrolling);
1057 let layout_modifier =
1058 Modifier::with_element(element).with_inspector_metadata(inspector_metadata(
1059 if is_vertical {
1060 "verticalScroll"
1061 } else {
1062 "horizontalScroll"
1063 },
1064 move |info| {
1065 info.add_property("isVertical", is_vertical.to_string());
1066 info.add_property("reverseScrolling", reverse_scrolling.to_string());
1067 },
1068 ));
1069 let motion_modifier =
1070 Modifier::with_element(MotionContextAnimatedElement::new(motion_context.clone()));
1071 let translated_content_modifier = Modifier::with_element(TranslatedContentContextElement::new(
1072 state.id() as usize,
1073 TranslatedContentOffsetSource::LayoutContentOffset,
1074 ));
1075
1076 pointer_input
1078 .then(motion_modifier)
1079 .then(translated_content_modifier)
1080 .then(layout_modifier)
1081 .clip_to_bounds()
1082}
1083
1084use cranpose_foundation::lazy::LazyListState;
1089
1090impl Modifier {
1091 pub fn lazy_vertical_scroll(self, state: LazyListState, reverse_scrolling: bool) -> Self {
1102 self.then(lazy_scroll_impl(state, true, reverse_scrolling))
1103 }
1104
1105 pub fn lazy_horizontal_scroll(self, state: LazyListState, reverse_scrolling: bool) -> Self {
1107 self.then(lazy_scroll_impl(state, false, reverse_scrolling))
1108 }
1109}
1110
1111fn lazy_scroll_impl(state: LazyListState, is_vertical: bool, reverse_scrolling: bool) -> Modifier {
1113 let gesture_state = Rc::new(RefCell::new(ScrollGestureState::default()));
1114 let list_state = state;
1115 let state_id = state.inner_ptr() as usize;
1116 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::LazyList {
1117 state_identity: state_id,
1118 is_vertical,
1119 reverse_scrolling,
1120 });
1121 let key = (state_id, is_vertical, reverse_scrolling);
1122 let translated_content_modifier = Modifier::with_element(TranslatedContentContextElement::new(
1123 state_id,
1124 TranslatedContentOffsetSource::LazyList {
1125 state,
1126 is_vertical,
1127 reverse_scrolling,
1128 },
1129 ));
1130
1131 Modifier::with_element(MotionContextAnimatedElement::new(motion_context.clone()))
1132 .then(translated_content_modifier)
1133 .pointer_input(key, move |scope| {
1134 let detector = ScrollGestureDetector::new(
1136 gesture_state.clone(),
1137 list_state,
1138 is_vertical,
1139 reverse_scrolling,
1140 motion_context.clone(),
1141 );
1142
1143 async move {
1144 scope
1145 .await_pointer_event_scope(|await_scope| async move {
1146 loop {
1147 let event = await_scope.await_pointer_event().await;
1148
1149 if event.id != 0 {
1153 continue;
1154 }
1155
1156 if event.is_consumed() {
1157 if matches!(
1158 event.kind,
1159 PointerEventKind::Down
1160 | PointerEventKind::Move
1161 | PointerEventKind::Up
1162 | PointerEventKind::Cancel
1163 ) {
1164 detector.on_cancel();
1165 }
1166 continue;
1167 }
1168
1169 let should_consume = match event.kind {
1171 PointerEventKind::Down => {
1172 detector.on_down(event.position, event.time_ms)
1173 }
1174 PointerEventKind::Move => {
1175 detector.on_move(event.position, event.buttons, event.time_ms)
1176 }
1177 PointerEventKind::Up => detector.on_up(),
1178 PointerEventKind::Cancel => detector.on_cancel(),
1179 PointerEventKind::Scroll => detector.on_scroll(if is_vertical {
1180 event.scroll_delta.y
1181 } else {
1182 event.scroll_delta.x
1183 }),
1184 PointerEventKind::Zoom
1185 | PointerEventKind::Enter
1186 | PointerEventKind::Exit => false,
1187 };
1188
1189 if should_consume {
1190 event.consume();
1191 }
1192 }
1193 })
1194 .await;
1195 }
1196 })
1197}