1use super::{inspector_metadata, Modifier, Point, PointerEvent, PointerEventKind};
27use crate::current_density;
28use crate::fling_animation::{
29 fling_rest_position, FlingAnimation, SettleAnimation, MIN_FLING_VELOCITY,
30};
31use crate::render_state::schedule_modifier_slices_repass;
32use crate::scroll::{
33 scroll_motion_context_for_key, ScrollElement, ScrollMotionContext, ScrollMotionContextKey,
34 ScrollSettlePolicy, ScrollState,
35};
36use cranpose_core::internal::FrameCallbackRegistration;
37use cranpose_core::{current_runtime_handle, NodeId};
38use cranpose_foundation::{
39 velocity_tracker::ASSUME_STOPPED_MS, DelegatableNode, ModifierNode, ModifierNodeElement,
40 NodeCapabilities, NodeState, PointerButton, PointerButtons, VelocityTracker1D, DRAG_THRESHOLD,
41 MAX_FLING_VELOCITY,
42};
43use std::cell::{Cell, RefCell};
44use std::rc::Rc;
45use web_time::Instant;
46
47#[cfg(feature = "test-helpers")]
48pub fn last_fling_velocity() -> f32 {
49 crate::render_state::debug_last_fling_velocity()
50}
51
52#[cfg(feature = "test-helpers")]
53pub fn reset_last_fling_velocity() {
54 crate::render_state::debug_reset_last_fling_velocity();
55}
56
57#[inline]
58fn set_last_fling_velocity(velocity: f32) {
59 crate::render_state::record_last_fling_velocity(velocity);
60}
61
62struct ScrollGestureState {
68 drag_down_position: Option<Point>,
71
72 last_position: Option<Point>,
75
76 is_dragging: bool,
80
81 axis_locked_out: bool,
87
88 velocity_tracker: VelocityTracker1D,
90
91 gesture_start_time: Option<Instant>,
93
94 gesture_start_event_time_ms: Option<i64>,
98
99 last_velocity_sample_ms: Option<i64>,
101
102 fling_animation: Option<FlingAnimation>,
104
105 is_overscrolling: bool,
106
107 settle_animation: Option<SettleAnimation>,
109
110 wheel_settle_watcher: Option<WheelSettleWatcher>,
113}
114
115impl Default for ScrollGestureState {
116 fn default() -> Self {
117 Self {
118 drag_down_position: None,
119 last_position: None,
120 is_dragging: false,
121 axis_locked_out: false,
122 velocity_tracker: VelocityTracker1D::new(),
123 gesture_start_time: None,
124 gesture_start_event_time_ms: None,
125 last_velocity_sample_ms: None,
126 fling_animation: None,
127 is_overscrolling: false,
128 settle_animation: None,
129 wheel_settle_watcher: None,
130 }
131 }
132}
133
134#[inline]
144fn calculate_total_delta(from: Point, to: Point, is_vertical: bool) -> f32 {
145 if is_vertical {
146 to.y - from.y
147 } else {
148 to.x - from.x
149 }
150}
151
152#[inline]
157fn calculate_incremental_delta(from: Point, to: Point, is_vertical: bool) -> f32 {
158 if is_vertical {
159 to.y - from.y
160 } else {
161 to.x - from.x
162 }
163}
164
165trait ScrollTarget: Clone {
173 fn apply_delta(&self, delta: f32) -> f32;
175
176 fn apply_wheel_delta(&self, delta: f32) -> f32 {
178 self.apply_delta(delta)
179 }
180
181 fn apply_fling_delta(&self, delta: f32) -> f32;
183
184 fn invalidate(&self);
186
187 fn current_offset(&self) -> f32;
189
190 fn can_scroll(&self) -> bool {
198 true
199 }
200
201 fn can_consume(&self, gesture_delta: f32) -> bool {
207 let _ = gesture_delta;
208 self.can_scroll()
209 }
210
211 fn settle_policy(&self) -> Option<ScrollSettlePolicy> {
214 None
215 }
216}
217
218impl ScrollTarget for ScrollState {
219 fn apply_delta(&self, delta: f32) -> f32 {
220 -self.dispatch_raw_delta(-delta)
221 }
222
223 fn apply_fling_delta(&self, delta: f32) -> f32 {
224 self.dispatch_raw_delta(delta)
225 }
226
227 fn invalidate(&self) {
228 }
230
231 fn current_offset(&self) -> f32 {
232 self.value()
233 }
234
235 fn can_scroll(&self) -> bool {
236 self.max_value() > 0.0
237 }
238
239 fn can_consume(&self, gesture_delta: f32) -> bool {
240 let raw = -gesture_delta;
243 if raw > 0.0 {
244 self.value_non_reactive() < self.max_value()
245 } else {
246 self.value_non_reactive() > 0.0
247 }
248 }
249
250 fn settle_policy(&self) -> Option<ScrollSettlePolicy> {
251 ScrollState::settle_policy(self)
252 }
253}
254
255impl ScrollTarget for LazyListState {
256 fn apply_delta(&self, delta: f32) -> f32 {
257 self.dispatch_scroll_delta(delta)
261 }
262
263 fn apply_wheel_delta(&self, delta: f32) -> f32 {
264 if delta.abs() <= 0.001 {
265 0.0
266 } else {
267 self.dispatch_scroll_delta(delta)
268 }
269 }
270
271 fn apply_fling_delta(&self, delta: f32) -> f32 {
272 -self.dispatch_scroll_delta(-delta)
273 }
274
275 fn invalidate(&self) {
276 }
279
280 fn current_offset(&self) -> f32 {
281 self.first_visible_item_scroll_offset()
283 }
284
285 fn can_scroll(&self) -> bool {
286 self.layout_info().total_items_count == 0
289 || self.can_scroll_forward_non_reactive()
290 || self.can_scroll_backward_non_reactive()
291 }
292
293 fn can_consume(&self, gesture_delta: f32) -> bool {
294 if self.layout_info().total_items_count == 0 {
298 return true;
299 }
300 if gesture_delta < 0.0 {
301 self.can_scroll_forward_non_reactive()
302 } else {
303 self.can_scroll_backward_non_reactive()
304 }
305 }
306}
307
308const WHEEL_SETTLE_IDLE_NANOS: u64 = 180_000_000;
316
317struct WheelSettleWatcher {
320 is_running: Rc<Cell<bool>>,
321 registration: Rc<RefCell<Option<FrameCallbackRegistration>>>,
322}
323
324impl WheelSettleWatcher {
325 fn cancel(&self) {
326 self.is_running.set(false);
327 self.registration.borrow_mut().take();
328 }
329}
330
331struct ScrollGestureDetector<S: ScrollTarget> {
332 gesture_state: Rc<RefCell<ScrollGestureState>>,
334
335 scroll_target: S,
337
338 is_vertical: bool,
340
341 reverse_scrolling: bool,
343
344 overscroll: crate::scroll::OverscrollEffect,
345
346 motion_context: ScrollMotionContext,
348}
349
350impl<S: ScrollTarget + 'static> ScrollGestureDetector<S> {
351 fn new(
353 gesture_state: Rc<RefCell<ScrollGestureState>>,
354 scroll_target: S,
355 is_vertical: bool,
356 reverse_scrolling: bool,
357 overscroll: crate::scroll::OverscrollEffect,
358 motion_context: ScrollMotionContext,
359 ) -> Self {
360 Self {
361 gesture_state,
362 scroll_target,
363 is_vertical,
364 reverse_scrolling,
365 overscroll,
366 motion_context,
367 }
368 }
369
370 fn on_down(&self, position: Point, time_ms: Option<i64>) -> bool {
379 let mut gs = self.gesture_state.borrow_mut();
380
381 if let Some(fling) = gs.fling_animation.take() {
383 fling.cancel();
384 }
385 if let Some(settle) = gs.settle_animation.take() {
386 settle.cancel();
387 }
388 if let Some(watcher) = gs.wheel_settle_watcher.take() {
389 watcher.cancel();
390 }
391 self.motion_context.set_active(false);
392
393 gs.drag_down_position = Some(position);
394 gs.last_position = Some(position);
395 gs.is_dragging = false;
396 gs.axis_locked_out = false;
397 gs.velocity_tracker.reset();
398 gs.gesture_start_time = Some(Instant::now());
399 gs.gesture_start_event_time_ms = time_ms;
400 gs.is_overscrolling = self.overscroll.offset().abs() > 0.001;
401
402 let pos = if self.is_vertical {
404 position.y
405 } else {
406 position.x
407 };
408 gs.velocity_tracker.add_data_point(0, pos);
409 gs.last_velocity_sample_ms = Some(0);
410
411 false
413 }
414
415 fn on_move(
429 &self,
430 position: Point,
431 buttons: PointerButtons,
432 time_ms: Option<i64>,
433 event: &PointerEvent,
434 ) -> bool {
435 let mut gs = self.gesture_state.borrow_mut();
436
437 if !buttons.contains(PointerButton::Primary) && gs.drag_down_position.is_some() {
439 gs.drag_down_position = None;
440 gs.last_position = None;
441 gs.is_dragging = false;
442 gs.axis_locked_out = false;
443 gs.gesture_start_time = None;
444 gs.gesture_start_event_time_ms = None;
445 gs.last_velocity_sample_ms = None;
446 gs.velocity_tracker.reset();
447 self.motion_context.set_active(false);
448 return false;
449 }
450
451 let Some(down_pos) = gs.drag_down_position else {
452 return false;
453 };
454
455 let Some(last_pos) = gs.last_position else {
456 gs.last_position = Some(position);
457 return false;
458 };
459
460 let incremental_delta = calculate_incremental_delta(last_pos, position, self.is_vertical);
461
462 let mut edge_candidate = false;
472 if !gs.is_dragging && !gs.axis_locked_out {
473 let signed_main_delta = calculate_total_delta(down_pos, position, self.is_vertical);
474 let main_delta = signed_main_delta.abs();
475 let cross_delta = calculate_total_delta(down_pos, position, !self.is_vertical).abs();
476 if main_delta > DRAG_THRESHOLD && main_delta >= cross_delta {
477 if self.scroll_target.can_consume(signed_main_delta) {
480 gs.is_dragging = true;
481 self.motion_context.set_active(true);
482 } else if self.scroll_target.can_scroll() {
483 edge_candidate = true;
484 }
485 } else if cross_delta > DRAG_THRESHOLD && cross_delta > main_delta {
486 gs.axis_locked_out = true;
487 }
488 }
489
490 gs.last_position = Some(position);
491
492 let pos = if self.is_vertical {
494 position.y
495 } else {
496 position.x
497 };
498 let event_sample_ms = gs
499 .gesture_start_event_time_ms
500 .zip(time_ms)
501 .map(|(start_ms, now_ms)| now_ms - start_ms);
502 let sample_ms = if let Some(event_sample_ms) = event_sample_ms {
503 Some(match gs.last_velocity_sample_ms {
510 Some(last_sample_ms) => event_sample_ms.max(last_sample_ms),
511 None => event_sample_ms.max(0),
512 })
513 } else if let Some(start_time) = gs.gesture_start_time {
514 let elapsed_ms = start_time.elapsed().as_millis() as i64;
517 Some(match gs.last_velocity_sample_ms {
520 Some(last_sample_ms) => {
521 let mut sample_ms = if elapsed_ms <= last_sample_ms {
522 last_sample_ms + 1
523 } else {
524 elapsed_ms
525 };
526 if sample_ms - last_sample_ms > ASSUME_STOPPED_MS {
528 sample_ms = last_sample_ms + ASSUME_STOPPED_MS;
529 }
530 sample_ms
531 }
532 None => elapsed_ms,
533 })
534 } else {
535 None
536 };
537 if let Some(sample_ms) = sample_ms {
538 log::trace!(
539 target: "cranpose::velocity",
540 "sample t={sample_ms}ms pos={pos:.2} event_time={time_ms:?}"
541 );
542 gs.velocity_tracker.add_data_point(sample_ms, pos);
543 gs.last_velocity_sample_ms = Some(sample_ms);
544 }
545
546 if gs.is_dragging {
547 drop(gs); let delta = if self.reverse_scrolling {
549 -incremental_delta
550 } else {
551 incremental_delta
552 };
553 let overscroll = self.overscroll.clone();
554 overscroll.apply_to_scroll(delta, |delta| self.scroll_target.apply_delta(delta));
555 self.scroll_target.invalidate();
556 true } else if gs.is_overscrolling {
558 drop(gs);
559 let delta = if self.reverse_scrolling {
560 -incremental_delta
561 } else {
562 incremental_delta
563 };
564 self.apply_overscroll_delta(delta)
565 } else if edge_candidate {
566 drop(gs);
567 let delta = if self.reverse_scrolling {
568 -incremental_delta
569 } else {
570 incremental_delta
571 };
572 let detector = self.clone_for_watcher();
573 event.defer_post_dispatch_action(move || detector.apply_overscroll_candidate(delta));
574 false
575 } else {
576 false
577 }
578 }
579
580 fn apply_overscroll_delta(&self, delta: f32) -> bool {
581 self.motion_context.set_active(true);
582 let overscroll = self.overscroll.clone();
583 let target_consumed = Cell::new(0.0);
584 let before_overscroll = overscroll.offset();
585 overscroll.apply_to_scroll(delta, |delta| {
586 let consumed = self.scroll_target.apply_delta(delta);
587 target_consumed.set(consumed);
588 consumed
589 });
590 self.scroll_target.invalidate();
591 let consumed = target_consumed.get().abs() > 0.001
592 || (overscroll.offset() - before_overscroll).abs() > 0.001;
593 if !consumed {
594 self.motion_context.set_active(false);
595 }
596 consumed
597 }
598
599 fn apply_overscroll_candidate(&self, delta: f32) -> bool {
600 let consumed = self.apply_overscroll_delta(delta);
601 if consumed {
602 self.gesture_state.borrow_mut().is_overscrolling = true;
603 }
604 consumed
605 }
606
607 fn finish_gesture(&self, allow_fling: bool, release_time_ms: Option<i64>) -> bool {
614 let (was_dragging, gesture_owned, velocity, start_fling, existing_fling) = {
615 let mut gs = self.gesture_state.borrow_mut();
616 let was_dragging = gs.is_dragging;
617 let gesture_owned = was_dragging || gs.is_overscrolling;
618 let mut velocity = 0.0;
619
620 if allow_fling && gesture_owned && gs.gesture_start_time.is_some() {
621 let release_sample_ms = release_time_ms
626 .zip(gs.gesture_start_event_time_ms)
627 .map(|(release_ms, start_ms)| release_ms - start_ms)
628 .or_else(|| {
629 gs.gesture_start_time
630 .map(|start| start.elapsed().as_millis() as i64)
631 });
632 let rested_before_release = release_sample_ms
633 .zip(gs.last_velocity_sample_ms)
634 .is_some_and(|(release_ms, last_sample_ms)| {
635 release_ms - last_sample_ms > ASSUME_STOPPED_MS
636 });
637 if !rested_before_release {
638 velocity = gs
639 .velocity_tracker
640 .calculate_velocity_with_max(MAX_FLING_VELOCITY);
641 }
642 }
643
644 let start_fling = allow_fling && was_dragging && velocity.abs() > MIN_FLING_VELOCITY;
645 let existing_fling = if start_fling {
646 gs.fling_animation.take()
647 } else {
648 None
649 };
650
651 gs.drag_down_position = None;
652 gs.last_position = None;
653 gs.is_dragging = false;
654 gs.is_overscrolling = false;
655 gs.axis_locked_out = false;
656 gs.gesture_start_time = None;
657 gs.gesture_start_event_time_ms = None;
658 gs.last_velocity_sample_ms = None;
659
660 (
661 was_dragging,
662 gesture_owned,
663 velocity,
664 start_fling,
665 existing_fling,
666 )
667 };
668
669 if allow_fling && gesture_owned {
671 log::debug!(
672 target: "cranpose::velocity",
673 "gesture finished: fling velocity={velocity:.2} dp/s start_fling={start_fling}"
674 );
675 set_last_fling_velocity(velocity);
676 }
677
678 let adjusted_velocity = if self.reverse_scrolling {
680 -velocity
681 } else {
682 velocity
683 };
684 let fling_velocity = -adjusted_velocity;
685 let has_overscroll = self.overscroll.offset().abs() > 0.001;
686
687 let settle_target = if was_dragging {
692 self.scroll_target.settle_policy().and_then(|policy| {
693 let current = self.scroll_target.current_offset();
694 let proposed = if start_fling {
695 fling_rest_position(current, fling_velocity, current_density())
696 } else {
697 current
698 };
699 let target = policy(proposed, fling_velocity);
700 ((target - proposed).abs() > 0.5).then_some(target)
701 })
702 } else {
703 None
704 };
705
706 if has_overscroll {
707 if let Some(old_fling) = existing_fling {
708 old_fling.cancel();
709 }
710 self.start_overscroll_settle(-fling_velocity);
711 } else if let Some(target) = settle_target {
712 if let Some(old_fling) = existing_fling {
713 old_fling.cancel();
714 }
715 self.start_settle_animation(target, fling_velocity);
716 } else if start_fling {
717 if let Some(old_fling) = existing_fling {
718 old_fling.cancel();
719 }
720 self.start_fling_animation(fling_velocity);
721 } else {
722 self.motion_context.set_active(false);
723 }
724
725 gesture_owned
726 }
727
728 fn start_fling_animation(&self, fling_velocity: f32) {
729 let Some(runtime) = current_runtime_handle() else {
730 self.motion_context.set_active(false);
731 return;
732 };
733 self.motion_context.set_active(true);
734 let scroll_target = self.scroll_target.clone();
735 let fling = FlingAnimation::new(runtime);
736 let motion_context = self.motion_context.clone();
737 let initial_value = scroll_target.current_offset();
738 let scroll_target_for_fling = scroll_target.clone();
739 let scroll_target_for_end = scroll_target.clone();
740 let detector_for_end = self.clone_for_watcher();
741 let overscroll_for_fling = self.overscroll.clone();
742
743 fling.start_fling(
744 initial_value,
745 fling_velocity,
746 current_density(),
747 move |delta| {
748 let consumed = overscroll_for_fling.apply_to_fling(delta, |delta| {
749 scroll_target_for_fling.apply_fling_delta(delta)
750 });
751 scroll_target_for_fling.invalidate();
752 consumed
753 },
754 move || {
755 scroll_target_for_end.invalidate();
756 let settle_running = detector_for_end
757 .gesture_state
758 .borrow()
759 .settle_animation
760 .as_ref()
761 .is_some_and(SettleAnimation::is_running);
762 if detector_for_end.overscroll.offset().abs() > 0.001 && !settle_running {
763 detector_for_end.start_overscroll_settle(0.0);
764 }
765 detector_for_end.update_motion_active(&motion_context);
766 },
767 );
768
769 let mut gs = self.gesture_state.borrow_mut();
770 gs.fling_animation = Some(fling);
771 }
772
773 fn start_settle_animation(&self, target: f32, initial_velocity: f32) {
776 let Some(runtime) = current_runtime_handle() else {
777 self.motion_context.set_active(false);
778 return;
779 };
780 self.motion_context.set_active(true);
781 let settle = SettleAnimation::new(runtime);
782 let scroll_target_for_settle = self.scroll_target.clone();
783 let scroll_target_for_end = self.scroll_target.clone();
784 let detector_for_end = self.clone_for_watcher();
785 let motion_context = self.motion_context.clone();
786 settle.start_settle(
787 self.scroll_target.current_offset(),
788 initial_velocity,
789 target,
790 move |delta| {
791 let consumed = scroll_target_for_settle.apply_fling_delta(delta);
792 scroll_target_for_settle.invalidate();
793 consumed
794 },
795 move |_| {
796 scroll_target_for_end.invalidate();
797 detector_for_end.update_motion_active(&motion_context);
798 },
799 );
800 let mut gs = self.gesture_state.borrow_mut();
801 gs.settle_animation = Some(settle);
802 }
803
804 fn start_overscroll_settle(&self, initial_velocity: f32) {
805 let Some(runtime) = current_runtime_handle() else {
806 let current = self.overscroll.offset();
807 self.overscroll.apply_settle_delta(-current);
808 self.motion_context.set_active(false);
809 return;
810 };
811 let settle = SettleAnimation::new(runtime);
812 let overscroll_for_settle = self.overscroll.clone();
813 let overscroll_for_end = self.overscroll.clone();
814 let detector_for_end = self.clone_for_watcher();
815 let motion_context = self.motion_context.clone();
816 let initial = self.overscroll.offset();
817 settle.start_settle(
818 initial,
819 initial_velocity,
820 0.0,
821 move |delta| overscroll_for_settle.apply_settle_delta(delta),
822 move |end| {
823 let current = overscroll_for_end.offset();
824 overscroll_for_end.apply_settle_delta(-current);
825 if end.hit_boundary && end.velocity.abs() > MIN_FLING_VELOCITY {
826 detector_for_end.start_fling_animation(-end.velocity);
827 } else {
828 detector_for_end.update_motion_active(&motion_context);
829 }
830 },
831 );
832 let mut gs = self.gesture_state.borrow_mut();
833 gs.is_overscrolling = false;
834 gs.settle_animation = Some(settle);
835 }
836
837 fn update_motion_active(&self, motion_context: &ScrollMotionContext) {
838 let running = {
839 let gs = self.gesture_state.borrow();
840 gs.fling_animation
841 .as_ref()
842 .is_some_and(FlingAnimation::is_running)
843 || gs
844 .settle_animation
845 .as_ref()
846 .is_some_and(SettleAnimation::is_running)
847 };
848 if !running {
849 motion_context.set_active(false);
850 }
851 }
852
853 fn on_up(&self, time_ms: Option<i64>) -> bool {
860 self.finish_gesture(true, time_ms)
861 }
862
863 fn on_cancel(&self) -> bool {
867 self.finish_gesture(false, None)
868 }
869
870 fn on_scroll(&self, axis_delta: f32, event: &PointerEvent) -> bool {
874 if axis_delta.abs() <= f32::EPSILON {
875 return false;
876 }
877
878 let delta = if self.reverse_scrolling {
879 -axis_delta
880 } else {
881 axis_delta
882 };
883 if !self.scroll_target.can_consume(delta) && self.scroll_target.can_scroll() {
884 let detector = self.clone_for_watcher();
885 event.defer_post_dispatch_action(move || detector.apply_wheel_delta(delta));
886 return false;
887 }
888 self.apply_wheel_delta(delta)
889 }
890
891 fn apply_wheel_delta(&self, delta: f32) -> bool {
892 {
893 let mut gs = self.gesture_state.borrow_mut();
894 if let Some(fling) = gs.fling_animation.take() {
895 fling.cancel();
896 }
897 if let Some(settle) = gs.settle_animation.take() {
898 settle.cancel();
899 }
900 gs.drag_down_position = None;
901 gs.last_position = None;
902 gs.is_dragging = false;
903 gs.axis_locked_out = false;
904 gs.gesture_start_time = None;
905 gs.gesture_start_event_time_ms = None;
906 gs.last_velocity_sample_ms = None;
907 gs.velocity_tracker.reset();
908 }
909
910 self.motion_context.activate_for_current_frame();
911 let overscroll = self.overscroll.clone();
912 let consumed =
913 overscroll.apply_to_scroll(delta, |delta| self.scroll_target.apply_wheel_delta(delta));
914 let overscroll_active = self.overscroll.offset().abs() > 0.001;
915 if consumed.abs() > 0.001 {
916 self.scroll_target.invalidate();
917 }
918 if consumed.abs() > 0.001 || overscroll_active {
919 self.ensure_wheel_settle_watcher();
920 true
921 } else {
922 false
923 }
924 }
925
926 fn ensure_wheel_settle_watcher(&self) {
931 if self.scroll_target.settle_policy().is_none() && self.overscroll.offset().abs() <= 0.001 {
932 return;
933 }
934 {
935 let gs = self.gesture_state.borrow();
936 if gs
937 .wheel_settle_watcher
938 .as_ref()
939 .is_some_and(|watcher| watcher.is_running.get())
940 {
941 return;
942 }
943 }
944 let Some(runtime) = current_runtime_handle() else {
945 return;
946 };
947
948 let is_running = Rc::new(Cell::new(true));
949 let registration = Rc::new(RefCell::new(None));
950
951 struct WheelSettleLoop<S: ScrollTarget> {
952 detector: ScrollGestureDetector<S>,
953 gesture_state: Rc<RefCell<ScrollGestureState>>,
954 frame_clock: cranpose_core::internal::FrameClock,
955 is_running: Rc<Cell<bool>>,
956 registration: Rc<RefCell<Option<FrameCallbackRegistration>>>,
957 last_offset: Rc<Cell<f32>>,
958 idle_nanos: Rc<Cell<u64>>,
959 last_frame: Rc<Cell<Option<u64>>>,
960 }
961
962 impl<S: ScrollTarget + 'static> WheelSettleLoop<S> {
963 fn next(&self) -> Self {
964 Self {
965 detector: self.detector.clone_for_watcher(),
966 gesture_state: Rc::clone(&self.gesture_state),
967 frame_clock: self.frame_clock.clone(),
968 is_running: Rc::clone(&self.is_running),
969 registration: Rc::clone(&self.registration),
970 last_offset: Rc::clone(&self.last_offset),
971 idle_nanos: Rc::clone(&self.idle_nanos),
972 last_frame: Rc::clone(&self.last_frame),
973 }
974 }
975
976 fn schedule(self) {
977 let continuation = self.next();
978 let registration_slot = Rc::clone(&self.registration);
979 let new_registration = self.frame_clock.with_frame_nanos(move |frame_time_nanos| {
980 let this = &continuation;
981 if !this.is_running.get() {
982 return;
983 }
984 {
986 let gs = this.gesture_state.borrow();
987 let animating = gs.is_dragging
988 || gs
989 .fling_animation
990 .as_ref()
991 .is_some_and(FlingAnimation::is_running)
992 || gs
993 .settle_animation
994 .as_ref()
995 .is_some_and(SettleAnimation::is_running);
996 if animating {
997 this.is_running.set(false);
998 return;
999 }
1000 }
1001
1002 let offset = this.detector.scroll_target.current_offset();
1003 let dt = this
1004 .last_frame
1005 .get()
1006 .map_or(0, |last| frame_time_nanos.saturating_sub(last));
1007 this.last_frame.set(Some(frame_time_nanos));
1008 if (offset - this.last_offset.get()).abs() > 0.01 {
1009 this.last_offset.set(offset);
1010 this.idle_nanos.set(0);
1011 } else {
1012 this.idle_nanos.set(this.idle_nanos.get() + dt);
1013 }
1014
1015 if this.idle_nanos.get() >= WHEEL_SETTLE_IDLE_NANOS {
1016 this.is_running.set(false);
1017 if this.detector.overscroll.offset().abs() > 0.001 {
1018 this.detector.start_overscroll_settle(0.0);
1019 return;
1020 }
1021 if let Some(policy) = this.detector.scroll_target.settle_policy() {
1022 let target = policy(offset, 0.0);
1023 if (target - offset).abs() > 0.5 {
1024 this.detector.start_settle_animation(target, 0.0);
1025 }
1026 }
1027 return;
1028 }
1029
1030 continuation.next().schedule();
1031 });
1032 *registration_slot.borrow_mut() = Some(new_registration);
1033 }
1034 }
1035
1036 WheelSettleLoop {
1037 detector: self.clone_for_watcher(),
1038 gesture_state: Rc::clone(&self.gesture_state),
1039 frame_clock: runtime.frame_clock(),
1040 is_running: Rc::clone(&is_running),
1041 registration: Rc::clone(®istration),
1042 last_offset: Rc::new(Cell::new(self.scroll_target.current_offset())),
1043 idle_nanos: Rc::new(Cell::new(0u64)),
1044 last_frame: Rc::new(Cell::new(None::<u64>)),
1045 }
1046 .schedule();
1047
1048 self.gesture_state.borrow_mut().wheel_settle_watcher = Some(WheelSettleWatcher {
1049 is_running,
1050 registration,
1051 });
1052 }
1053
1054 fn clone_for_watcher(&self) -> ScrollGestureDetector<S> {
1055 ScrollGestureDetector {
1056 gesture_state: Rc::clone(&self.gesture_state),
1057 scroll_target: self.scroll_target.clone(),
1058 is_vertical: self.is_vertical,
1059 reverse_scrolling: self.reverse_scrolling,
1060 overscroll: self.overscroll.clone(),
1061 motion_context: self.motion_context.clone(),
1062 }
1063 }
1064}
1065
1066pub(crate) struct MotionContextAnimatedNode {
1067 state: NodeState,
1068 motion_context: ScrollMotionContext,
1069 invalidation_callback_id: Option<u64>,
1070 overscroll_callback_id: Option<u64>,
1071 node_id: Option<NodeId>,
1072}
1073
1074impl MotionContextAnimatedNode {
1075 fn new(motion_context: ScrollMotionContext) -> Self {
1076 Self {
1077 state: NodeState::new(),
1078 motion_context,
1079 invalidation_callback_id: None,
1080 overscroll_callback_id: None,
1081 node_id: None,
1082 }
1083 }
1084
1085 pub(crate) fn is_active(&self) -> bool {
1086 self.motion_context.is_active()
1087 }
1088}
1089
1090pub(crate) struct TranslatedContentContextNode {
1091 state: NodeState,
1092 identity: usize,
1093 offset_source: TranslatedContentOffsetSource,
1094 overscroll: crate::scroll::OverscrollEffect,
1095 overscroll_callback_id: Option<u64>,
1096}
1097
1098impl TranslatedContentContextNode {
1099 fn new(
1100 identity: usize,
1101 offset_source: TranslatedContentOffsetSource,
1102 overscroll: crate::scroll::OverscrollEffect,
1103 ) -> Self {
1104 Self {
1105 state: NodeState::new(),
1106 identity,
1107 offset_source,
1108 overscroll,
1109 overscroll_callback_id: None,
1110 }
1111 }
1112
1113 pub(crate) fn is_active(&self) -> bool {
1114 true
1115 }
1116
1117 pub(crate) fn identity(&self) -> usize {
1118 self.identity
1119 }
1120
1121 pub(crate) fn content_offset_reader(&self) -> Option<Rc<dyn Fn() -> Point>> {
1122 self.offset_source
1123 .content_offset_reader(self.overscroll.clone())
1124 }
1125}
1126
1127impl DelegatableNode for TranslatedContentContextNode {
1128 fn node_state(&self) -> &NodeState {
1129 &self.state
1130 }
1131}
1132
1133impl ModifierNode for TranslatedContentContextNode {
1134 fn on_attach(&mut self, context: &mut dyn cranpose_foundation::ModifierNodeContext) {
1135 if let Some(node_id) = context.node_id() {
1136 self.overscroll_callback_id =
1137 Some(self.overscroll.add_invalidate_callback(Box::new(move || {
1138 schedule_modifier_slices_repass(node_id)
1139 })));
1140 }
1141 }
1142
1143 fn on_detach(&mut self) {
1144 if let Some(id) = self.overscroll_callback_id.take() {
1145 self.overscroll.remove_invalidate_callback(id);
1146 }
1147 }
1148}
1149
1150impl DelegatableNode for MotionContextAnimatedNode {
1151 fn node_state(&self) -> &NodeState {
1152 &self.state
1153 }
1154}
1155
1156impl ModifierNode for MotionContextAnimatedNode {
1157 fn on_attach(&mut self, context: &mut dyn cranpose_foundation::ModifierNodeContext) {
1158 let node_id = context.node_id();
1159 self.node_id = node_id;
1160 if let Some(node_id) = node_id {
1161 let callback_id = self
1162 .motion_context
1163 .add_invalidate_callback(Box::new(move || {
1164 schedule_modifier_slices_repass(node_id);
1165 }));
1166 self.invalidation_callback_id = Some(callback_id);
1167 let callback_id = self
1168 .motion_context
1169 .overscroll()
1170 .add_invalidate_callback(Box::new(move || {
1171 crate::schedule_measure_repass(node_id);
1172 schedule_modifier_slices_repass(node_id);
1173 }));
1174 self.overscroll_callback_id = Some(callback_id);
1175 }
1176 }
1177
1178 fn on_detach(&mut self) {
1179 if let Some(id) = self.invalidation_callback_id.take() {
1180 self.motion_context.remove_invalidate_callback(id);
1181 }
1182 if let Some(id) = self.overscroll_callback_id.take() {
1183 self.motion_context
1184 .overscroll()
1185 .remove_invalidate_callback(id);
1186 }
1187 self.node_id = None;
1188 }
1189}
1190
1191#[derive(Clone)]
1192struct MotionContextAnimatedElement {
1193 motion_context: ScrollMotionContext,
1194}
1195
1196impl MotionContextAnimatedElement {
1197 fn new(motion_context: ScrollMotionContext) -> Self {
1198 Self { motion_context }
1199 }
1200}
1201
1202impl std::fmt::Debug for MotionContextAnimatedElement {
1203 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1204 f.debug_struct("MotionContextAnimatedElement").finish()
1205 }
1206}
1207
1208impl PartialEq for MotionContextAnimatedElement {
1209 fn eq(&self, other: &Self) -> bool {
1210 self.motion_context.ptr_eq(&other.motion_context)
1211 }
1212}
1213
1214impl Eq for MotionContextAnimatedElement {}
1215
1216impl std::hash::Hash for MotionContextAnimatedElement {
1217 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1218 self.motion_context.stable_key().hash(state);
1219 }
1220}
1221
1222impl ModifierNodeElement for MotionContextAnimatedElement {
1223 type Node = MotionContextAnimatedNode;
1224
1225 fn create(&self) -> Self::Node {
1226 MotionContextAnimatedNode::new(self.motion_context.clone())
1227 }
1228
1229 fn update(&self, node: &mut Self::Node) {
1230 if node.motion_context.ptr_eq(&self.motion_context) {
1231 return;
1232 }
1233 if let Some(id) = node.invalidation_callback_id.take() {
1234 node.motion_context.remove_invalidate_callback(id);
1235 }
1236 node.motion_context = self.motion_context.clone();
1237 if let Some(node_id) = node.node_id {
1238 let callback_id = node
1239 .motion_context
1240 .add_invalidate_callback(Box::new(move || {
1241 schedule_modifier_slices_repass(node_id);
1242 }));
1243 node.invalidation_callback_id = Some(callback_id);
1244 }
1245 }
1246
1247 fn capabilities(&self) -> NodeCapabilities {
1248 NodeCapabilities::LAYOUT
1249 }
1250}
1251
1252#[derive(Clone)]
1253enum TranslatedContentOffsetSource {
1254 LayoutContentOffset,
1255 LazyList {
1256 state: LazyListState,
1257 is_vertical: bool,
1258 reverse_scrolling: bool,
1259 },
1260}
1261
1262impl TranslatedContentOffsetSource {
1263 fn content_offset_reader(
1264 &self,
1265 overscroll: crate::scroll::OverscrollEffect,
1266 ) -> Option<Rc<dyn Fn() -> Point>> {
1267 match self {
1268 Self::LayoutContentOffset => None,
1269 Self::LazyList {
1270 state, is_vertical, ..
1271 } => Some(Rc::new(lazy_list_content_offset_reader(
1272 *state,
1273 *is_vertical,
1274 overscroll,
1275 ))),
1276 }
1277 }
1278
1279 fn is_vertical(&self) -> Option<bool> {
1280 match self {
1281 Self::LayoutContentOffset => None,
1282 Self::LazyList { is_vertical, .. } => Some(*is_vertical),
1283 }
1284 }
1285
1286 fn reverse_scrolling(&self) -> Option<bool> {
1287 match self {
1288 Self::LayoutContentOffset => None,
1289 Self::LazyList {
1290 reverse_scrolling, ..
1291 } => Some(*reverse_scrolling),
1292 }
1293 }
1294}
1295
1296fn lazy_list_content_offset_reader(
1297 state: LazyListState,
1298 is_vertical: bool,
1299 overscroll: crate::scroll::OverscrollEffect,
1300) -> impl Fn() -> Point {
1301 move || {
1302 let info = state.layout_info();
1303 if info.visible_items_info.is_empty() {
1304 return Point::default();
1305 };
1306 overscroll.set_limit(info.viewport_size * 0.5);
1307 let main_offset = info.snap_anchor_offset;
1308 let bounce = overscroll.offset();
1309 if is_vertical {
1310 Point::new(0.0, main_offset + bounce)
1311 } else {
1312 Point::new(main_offset + bounce, 0.0)
1313 }
1314 }
1315}
1316
1317#[derive(Clone)]
1318struct TranslatedContentContextElement {
1319 identity: usize,
1320 offset_source: TranslatedContentOffsetSource,
1321 overscroll: crate::scroll::OverscrollEffect,
1322}
1323
1324impl TranslatedContentContextElement {
1325 fn new(
1326 identity: usize,
1327 offset_source: TranslatedContentOffsetSource,
1328 overscroll: crate::scroll::OverscrollEffect,
1329 ) -> Self {
1330 Self {
1331 identity,
1332 offset_source,
1333 overscroll,
1334 }
1335 }
1336}
1337
1338impl std::fmt::Debug for TranslatedContentContextElement {
1339 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1340 let offset_source = match &self.offset_source {
1341 TranslatedContentOffsetSource::LayoutContentOffset => "layout",
1342 TranslatedContentOffsetSource::LazyList { .. } => "lazy_list",
1343 };
1344 f.debug_struct("TranslatedContentContextElement")
1345 .field("identity", &self.identity)
1346 .field("offset_source", &offset_source)
1347 .finish()
1348 }
1349}
1350
1351impl PartialEq for TranslatedContentContextElement {
1352 fn eq(&self, other: &Self) -> bool {
1353 self.identity == other.identity
1354 && self.overscroll.ptr_eq(&other.overscroll)
1355 && self.offset_source.is_vertical() == other.offset_source.is_vertical()
1356 && self.offset_source.reverse_scrolling() == other.offset_source.reverse_scrolling()
1357 }
1358}
1359
1360impl Eq for TranslatedContentContextElement {}
1361
1362impl std::hash::Hash for TranslatedContentContextElement {
1363 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1364 self.identity.hash(state);
1365 self.offset_source.is_vertical().hash(state);
1366 self.offset_source.reverse_scrolling().hash(state);
1367 }
1368}
1369
1370impl ModifierNodeElement for TranslatedContentContextElement {
1371 type Node = TranslatedContentContextNode;
1372
1373 fn create(&self) -> Self::Node {
1374 TranslatedContentContextNode::new(
1375 self.identity,
1376 self.offset_source.clone(),
1377 self.overscroll.clone(),
1378 )
1379 }
1380
1381 fn update(&self, node: &mut Self::Node) {
1382 node.identity = self.identity;
1383 node.offset_source = self.offset_source.clone();
1384 node.overscroll = self.overscroll.clone();
1385 }
1386
1387 fn capabilities(&self) -> NodeCapabilities {
1388 NodeCapabilities::LAYOUT
1389 }
1390}
1391
1392impl Modifier {
1397 pub fn horizontal_scroll(self, state: ScrollState, reverse_scrolling: bool) -> Self {
1415 self.then(scroll_impl(state, false, reverse_scrolling, None))
1416 }
1417
1418 pub fn vertical_scroll(self, state: ScrollState, reverse_scrolling: bool) -> Self {
1427 self.then(scroll_impl(state, true, reverse_scrolling, None))
1428 }
1429
1430 pub fn horizontal_scroll_guarded(
1432 self,
1433 state: ScrollState,
1434 reverse_scrolling: bool,
1435 guard: impl Fn() -> bool + 'static,
1436 ) -> Self {
1437 self.then(scroll_impl(
1438 state,
1439 false,
1440 reverse_scrolling,
1441 Some(Rc::new(guard)),
1442 ))
1443 }
1444
1445 pub fn vertical_scroll_guarded(
1447 self,
1448 state: ScrollState,
1449 reverse_scrolling: bool,
1450 guard: impl Fn() -> bool + 'static,
1451 ) -> Self {
1452 self.then(scroll_impl(
1453 state,
1454 true,
1455 reverse_scrolling,
1456 Some(Rc::new(guard)),
1457 ))
1458 }
1459}
1460
1461fn scroll_impl(
1470 state: ScrollState,
1471 is_vertical: bool,
1472 reverse_scrolling: bool,
1473 guard: Option<Rc<dyn Fn() -> bool>>,
1474) -> Modifier {
1475 let gesture_state = Rc::new(RefCell::new(ScrollGestureState::default()));
1477 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::ScrollState {
1478 state_id: state.id(),
1479 is_vertical,
1480 reverse_scrolling,
1481 });
1482
1483 let scroll_state = state;
1485 let pointer_motion_context = motion_context.clone();
1486 let key = (state.id(), is_vertical);
1487 let pointer_input = Modifier::empty().pointer_input(key, move |scope| {
1488 let detector = ScrollGestureDetector::new(
1490 gesture_state.clone(),
1491 scroll_state,
1492 is_vertical,
1493 false, pointer_motion_context.overscroll(),
1495 pointer_motion_context.clone(),
1496 );
1497 let guard = guard.clone();
1498
1499 async move {
1500 scope
1501 .await_pointer_event_scope(|await_scope| async move {
1502 loop {
1504 let event = await_scope.await_pointer_event().await;
1505
1506 if event.id != 0 {
1510 continue;
1511 }
1512
1513 if event.is_consumed() {
1514 if matches!(
1515 event.kind,
1516 PointerEventKind::Down
1517 | PointerEventKind::Move
1518 | PointerEventKind::Up
1519 | PointerEventKind::Cancel
1520 ) {
1521 detector.on_cancel();
1522 }
1523 continue;
1524 }
1525
1526 if let Some(ref guard) = guard {
1527 if !guard() {
1528 if matches!(
1529 event.kind,
1530 PointerEventKind::Up | PointerEventKind::Cancel
1531 ) {
1532 detector.on_cancel();
1533 }
1534 continue;
1535 }
1536 }
1537
1538 let should_consume = match event.kind {
1540 PointerEventKind::Down => {
1541 detector.on_down(event.position, event.time_ms)
1542 }
1543 PointerEventKind::Move => detector.on_move(
1544 event.position,
1545 event.buttons,
1546 event.time_ms,
1547 &event,
1548 ),
1549 PointerEventKind::Up => detector.on_up(event.time_ms),
1550 PointerEventKind::Cancel => detector.on_cancel(),
1551 PointerEventKind::Scroll => detector.on_scroll(
1552 if is_vertical {
1553 event.scroll_delta.y
1554 } else {
1555 event.scroll_delta.x
1556 },
1557 &event,
1558 ),
1559 PointerEventKind::Zoom
1562 | PointerEventKind::RotaryScrollPre
1563 | PointerEventKind::RotaryScroll
1564 | PointerEventKind::Enter
1565 | PointerEventKind::Exit => false,
1566 };
1567
1568 if should_consume {
1569 event.consume();
1570 }
1571 }
1572 })
1573 .await;
1574 }
1575 });
1576
1577 let overscroll = motion_context.overscroll();
1579 let element = ScrollElement::new(state, overscroll.clone(), is_vertical, reverse_scrolling);
1580 let layout_modifier =
1581 Modifier::with_element(element).with_inspector_metadata(inspector_metadata(
1582 if is_vertical {
1583 "verticalScroll"
1584 } else {
1585 "horizontalScroll"
1586 },
1587 move |info| {
1588 info.add_property("isVertical", is_vertical.to_string());
1589 info.add_property("reverseScrolling", reverse_scrolling.to_string());
1590 },
1591 ));
1592 let motion_modifier =
1593 Modifier::with_element(MotionContextAnimatedElement::new(motion_context.clone()));
1594 let translated_content_modifier = Modifier::with_element(TranslatedContentContextElement::new(
1595 state.id() as usize,
1596 TranslatedContentOffsetSource::LayoutContentOffset,
1597 overscroll,
1598 ));
1599
1600 pointer_input
1602 .then(motion_modifier)
1603 .then(translated_content_modifier)
1604 .then(layout_modifier)
1605 .clip_to_bounds()
1606}
1607
1608use cranpose_foundation::lazy::LazyListState;
1613
1614impl Modifier {
1615 pub fn lazy_vertical_scroll(self, state: LazyListState, reverse_scrolling: bool) -> Self {
1626 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::LazyList {
1627 state_identity: state.inner_ptr() as usize,
1628 is_vertical: true,
1629 reverse_scrolling,
1630 });
1631 self.lazy_vertical_scroll_with_context(state, reverse_scrolling, motion_context)
1632 }
1633
1634 pub(crate) fn lazy_vertical_scroll_with_context(
1635 self,
1636 state: LazyListState,
1637 reverse_scrolling: bool,
1638 motion_context: ScrollMotionContext,
1639 ) -> Self {
1640 self.then(lazy_scroll_impl(
1641 state,
1642 true,
1643 reverse_scrolling,
1644 motion_context,
1645 ))
1646 }
1647
1648 pub fn lazy_horizontal_scroll(self, state: LazyListState, reverse_scrolling: bool) -> Self {
1650 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::LazyList {
1651 state_identity: state.inner_ptr() as usize,
1652 is_vertical: false,
1653 reverse_scrolling,
1654 });
1655 self.lazy_horizontal_scroll_with_context(state, reverse_scrolling, motion_context)
1656 }
1657
1658 pub(crate) fn lazy_horizontal_scroll_with_context(
1659 self,
1660 state: LazyListState,
1661 reverse_scrolling: bool,
1662 motion_context: ScrollMotionContext,
1663 ) -> Self {
1664 self.then(lazy_scroll_impl(
1665 state,
1666 false,
1667 reverse_scrolling,
1668 motion_context,
1669 ))
1670 }
1671}
1672
1673fn lazy_scroll_impl(
1675 state: LazyListState,
1676 is_vertical: bool,
1677 reverse_scrolling: bool,
1678 motion_context: ScrollMotionContext,
1679) -> Modifier {
1680 let gesture_state = Rc::new(RefCell::new(ScrollGestureState::default()));
1681 let list_state = state;
1682 let state_id = state.inner_ptr() as usize;
1683 let key = (state_id, is_vertical, reverse_scrolling);
1684 let overscroll = motion_context.overscroll();
1685 let translated_content_modifier = Modifier::with_element(TranslatedContentContextElement::new(
1686 state_id,
1687 TranslatedContentOffsetSource::LazyList {
1688 state,
1689 is_vertical,
1690 reverse_scrolling,
1691 },
1692 overscroll,
1693 ));
1694
1695 Modifier::with_element(MotionContextAnimatedElement::new(motion_context.clone()))
1696 .then(translated_content_modifier)
1697 .pointer_input(key, move |scope| {
1698 let detector = ScrollGestureDetector::new(
1700 gesture_state.clone(),
1701 list_state,
1702 is_vertical,
1703 reverse_scrolling,
1704 motion_context.overscroll(),
1705 motion_context.clone(),
1706 );
1707
1708 async move {
1709 scope
1710 .await_pointer_event_scope(|await_scope| async move {
1711 loop {
1712 let event = await_scope.await_pointer_event().await;
1713
1714 if event.id != 0 {
1718 continue;
1719 }
1720
1721 if event.is_consumed() {
1722 if matches!(
1723 event.kind,
1724 PointerEventKind::Down
1725 | PointerEventKind::Move
1726 | PointerEventKind::Up
1727 | PointerEventKind::Cancel
1728 ) {
1729 detector.on_cancel();
1730 }
1731 continue;
1732 }
1733
1734 let should_consume = match event.kind {
1736 PointerEventKind::Down => {
1737 detector.on_down(event.position, event.time_ms)
1738 }
1739 PointerEventKind::Move => detector.on_move(
1740 event.position,
1741 event.buttons,
1742 event.time_ms,
1743 &event,
1744 ),
1745 PointerEventKind::Up => detector.on_up(event.time_ms),
1746 PointerEventKind::Cancel => detector.on_cancel(),
1747 PointerEventKind::Scroll => detector.on_scroll(
1748 if is_vertical {
1749 event.scroll_delta.y
1750 } else {
1751 event.scroll_delta.x
1752 },
1753 &event,
1754 ),
1755 PointerEventKind::Zoom
1758 | PointerEventKind::RotaryScrollPre
1759 | PointerEventKind::RotaryScroll
1760 | PointerEventKind::Enter
1761 | PointerEventKind::Exit => false,
1762 };
1763
1764 if should_consume {
1765 event.consume();
1766 }
1767 }
1768 })
1769 .await;
1770 }
1771 })
1772}