1use std::{
27 cell::{Cell, RefCell},
28 rc::Rc,
29};
30
31use cranpose_core::{NodeId, current_runtime_handle, internal::FrameCallbackRegistration};
32use cranpose_foundation::{
33 DRAG_THRESHOLD, DelegatableNode, MAX_FLING_VELOCITY, ModifierNode, ModifierNodeElement,
34 NodeCapabilities, NodeState, PointerButton, PointerButtons, VelocityTracker1D,
35 velocity_tracker::ASSUME_STOPPED_MS,
36};
37use cranpose_ui_layout::Axis;
38use web_time::Instant;
39
40use super::{Modifier, Point, PointerEvent, PointerEventKind, inspector_metadata};
41use crate::{
42 current_density,
43 draggable::DraggableState,
44 fling_animation::{FlingAnimation, MIN_FLING_VELOCITY, SettleAnimation, fling_rest_position},
45 render_state::schedule_modifier_slices_repass,
46 scroll::{
47 ScrollElement, ScrollMotionContext, ScrollMotionContextKey, ScrollSettlePolicy,
48 ScrollState, scroll_motion_context_for_key,
49 },
50};
51
52#[cfg(feature = "test-helpers")]
53pub fn last_fling_velocity() -> f32 {
54 crate::render_state::debug_last_fling_velocity()
55}
56
57#[cfg(feature = "test-helpers")]
58pub fn reset_last_fling_velocity() {
59 crate::render_state::debug_reset_last_fling_velocity();
60}
61
62#[inline]
63fn set_last_fling_velocity(velocity: f32) {
64 crate::render_state::record_last_fling_velocity(velocity);
65}
66
67struct ScrollGestureState {
73 drag_down_position: Option<Point>,
76
77 last_position: Option<Point>,
80
81 is_dragging: bool,
85
86 axis_locked_out: bool,
92
93 velocity_tracker: VelocityTracker1D,
95
96 gesture_start_time: Option<Instant>,
98
99 gesture_start_event_time_ms: Option<i64>,
103
104 last_velocity_sample_ms: Option<i64>,
106
107 fling_animation: Option<FlingAnimation>,
109
110 is_overscrolling: bool,
111
112 settle_animation: Option<SettleAnimation>,
114
115 wheel_settle_watcher: Option<WheelSettleWatcher>,
118}
119
120impl Default for ScrollGestureState {
121 fn default() -> Self {
122 Self {
123 drag_down_position: None,
124 last_position: None,
125 is_dragging: false,
126 axis_locked_out: false,
127 velocity_tracker: VelocityTracker1D::new(),
128 gesture_start_time: None,
129 gesture_start_event_time_ms: None,
130 last_velocity_sample_ms: None,
131 fling_animation: None,
132 is_overscrolling: false,
133 settle_animation: None,
134 wheel_settle_watcher: None,
135 }
136 }
137}
138
139#[inline]
149fn calculate_total_delta(from: Point, to: Point, is_vertical: bool) -> f32 {
150 if is_vertical {
151 to.y - from.y
152 } else {
153 to.x - from.x
154 }
155}
156
157#[inline]
162fn calculate_incremental_delta(from: Point, to: Point, is_vertical: bool) -> f32 {
163 if is_vertical {
164 to.y - from.y
165 } else {
166 to.x - from.x
167 }
168}
169
170trait ScrollTarget: Clone {
178 fn apply_delta(&self, delta: f32) -> f32;
180
181 fn apply_wheel_delta(&self, delta: f32) -> f32 {
183 self.apply_delta(delta)
184 }
185
186 fn apply_fling_delta(&self, delta: f32) -> f32;
188
189 fn invalidate(&self);
191
192 fn current_offset(&self) -> f32;
194
195 fn can_scroll(&self) -> bool {
203 true
204 }
205
206 fn can_consume(&self, gesture_delta: f32) -> bool {
212 let _ = gesture_delta;
213 self.can_scroll()
214 }
215
216 fn settle_policy(&self) -> Option<ScrollSettlePolicy> {
219 None
220 }
221
222 fn allows_fling(&self) -> bool {
229 true
230 }
231
232 fn set_dragging(&self, dragging: bool) {
235 let _ = dragging;
236 }
237}
238
239impl ScrollTarget for ScrollState {
240 fn apply_delta(&self, delta: f32) -> f32 {
241 -self.dispatch_raw_delta(-delta)
242 }
243
244 fn apply_fling_delta(&self, delta: f32) -> f32 {
245 self.dispatch_raw_delta(delta)
246 }
247
248 fn invalidate(&self) {
249 }
251
252 fn current_offset(&self) -> f32 {
253 self.value()
254 }
255
256 fn can_scroll(&self) -> bool {
257 self.max_value() > 0.0
258 }
259
260 fn can_consume(&self, gesture_delta: f32) -> bool {
261 let raw = -gesture_delta;
264 if raw > 0.0 {
265 self.value_non_reactive() < self.max_value()
266 } else {
267 self.value_non_reactive() > 0.0
268 }
269 }
270
271 fn settle_policy(&self) -> Option<ScrollSettlePolicy> {
272 ScrollState::settle_policy(self)
273 }
274}
275
276impl ScrollTarget for LazyListState {
277 fn apply_delta(&self, delta: f32) -> f32 {
278 self.dispatch_scroll_delta(delta)
282 }
283
284 fn apply_wheel_delta(&self, delta: f32) -> f32 {
285 if delta.abs() <= 0.001 {
286 0.0
287 } else {
288 self.dispatch_scroll_delta(delta)
289 }
290 }
291
292 fn apply_fling_delta(&self, delta: f32) -> f32 {
293 -self.dispatch_scroll_delta(-delta)
294 }
295
296 fn invalidate(&self) {
297 }
300
301 fn current_offset(&self) -> f32 {
302 self.first_visible_item_scroll_offset()
304 }
305
306 fn can_scroll(&self) -> bool {
307 self.layout_info().total_items_count == 0
310 || self.can_scroll_forward_non_reactive()
311 || self.can_scroll_backward_non_reactive()
312 }
313
314 fn can_consume(&self, gesture_delta: f32) -> bool {
315 if self.layout_info().total_items_count == 0 {
319 return true;
320 }
321 if gesture_delta < 0.0 {
322 self.can_scroll_forward_non_reactive()
323 } else {
324 self.can_scroll_backward_non_reactive()
325 }
326 }
327}
328
329struct DragGesture<S: ScrollTarget> {
337 target: S,
338 gesture_state: Rc<RefCell<ScrollGestureState>>,
339 is_vertical: bool,
340 reverse_input: bool,
341 motion_context: ScrollMotionContext,
342 guard: Option<Rc<dyn Fn() -> bool>>,
343}
344
345fn drag_gesture_input<K, S>(key: K, gesture: DragGesture<S>) -> Modifier
352where
353 K: std::hash::Hash + 'static,
354 S: ScrollTarget + 'static,
355{
356 let DragGesture {
357 target,
358 gesture_state,
359 is_vertical,
360 reverse_input,
361 motion_context,
362 guard,
363 } = gesture;
364
365 Modifier::empty().pointer_input(key, move |scope| {
366 let detector = ScrollGestureDetector::new(
367 gesture_state.clone(),
368 target.clone(),
369 is_vertical,
370 reverse_input,
371 motion_context.overscroll(),
372 motion_context.clone(),
373 );
374 let guard = guard.clone();
375
376 async move {
377 scope
378 .await_pointer_event_scope(|await_scope| async move {
379 loop {
380 let event = await_scope.await_pointer_event().await;
381
382 if event.id != 0 {
386 continue;
387 }
388
389 if event.is_consumed() {
392 if matches!(
393 event.kind,
394 PointerEventKind::Down
395 | PointerEventKind::Move
396 | PointerEventKind::Up
397 | PointerEventKind::Cancel
398 ) {
399 detector.on_cancel();
400 }
401 continue;
402 }
403
404 if let Some(ref guard) = guard
405 && !guard()
406 {
407 if matches!(event.kind, PointerEventKind::Up | PointerEventKind::Cancel)
408 {
409 detector.on_cancel();
410 }
411 continue;
412 }
413
414 let should_consume = match event.kind {
415 PointerEventKind::Down => {
416 detector.on_down(event.position, event.time_ms)
417 }
418 PointerEventKind::Move => detector.on_move(
419 event.position,
420 event.buttons,
421 event.time_ms,
422 &event,
423 ),
424 PointerEventKind::Up => detector.on_up(event.time_ms),
425 PointerEventKind::Cancel => detector.on_cancel(),
426 PointerEventKind::Scroll => detector.on_scroll(
427 if is_vertical {
428 event.scroll_delta.y
429 } else {
430 event.scroll_delta.x
431 },
432 &event,
433 ),
434 PointerEventKind::Zoom
438 | PointerEventKind::RotaryScrollPre
439 | PointerEventKind::RotaryScroll
440 | PointerEventKind::Enter
441 | PointerEventKind::Exit => false,
442 };
443
444 if should_consume {
445 event.consume();
446 }
447 }
448 })
449 .await;
450 }
451 })
452}
453
454impl ScrollTarget for DraggableState {
455 fn apply_delta(&self, delta: f32) -> f32 {
458 self.drag_by(delta);
459 delta
460 }
461
462 fn apply_fling_delta(&self, delta: f32) -> f32 {
463 self.drag_by(delta);
464 delta
465 }
466
467 fn invalidate(&self) {
468 }
470
471 fn current_offset(&self) -> f32 {
472 self.offset()
473 }
474
475 fn allows_fling(&self) -> bool {
477 false
478 }
479
480 fn set_dragging(&self, dragging: bool) {
481 DraggableState::set_dragging(self, dragging);
482 }
483}
484
485const WHEEL_SETTLE_IDLE_NANOS: u64 = 180_000_000;
493
494struct WheelSettleWatcher {
497 is_running: Rc<Cell<bool>>,
498 registration: Rc<RefCell<Option<FrameCallbackRegistration>>>,
499}
500
501impl WheelSettleWatcher {
502 fn cancel(&self) {
503 self.is_running.set(false);
504 self.registration.borrow_mut().take();
505 }
506}
507
508struct ScrollGestureDetector<S: ScrollTarget> {
509 gesture_state: Rc<RefCell<ScrollGestureState>>,
511
512 scroll_target: S,
514
515 is_vertical: bool,
517
518 reverse_scrolling: bool,
520
521 overscroll: crate::scroll::OverscrollEffect,
522
523 motion_context: ScrollMotionContext,
525}
526
527impl<S: ScrollTarget + 'static> ScrollGestureDetector<S> {
528 fn new(
530 gesture_state: Rc<RefCell<ScrollGestureState>>,
531 scroll_target: S,
532 is_vertical: bool,
533 reverse_scrolling: bool,
534 overscroll: crate::scroll::OverscrollEffect,
535 motion_context: ScrollMotionContext,
536 ) -> Self {
537 Self {
538 gesture_state,
539 scroll_target,
540 is_vertical,
541 reverse_scrolling,
542 overscroll,
543 motion_context,
544 }
545 }
546
547 fn on_down(&self, position: Point, time_ms: Option<i64>) -> bool {
556 let mut gs = self.gesture_state.borrow_mut();
557
558 if let Some(fling) = gs.fling_animation.take() {
560 fling.cancel();
561 }
562 if let Some(settle) = gs.settle_animation.take() {
563 settle.cancel();
564 }
565 if let Some(watcher) = gs.wheel_settle_watcher.take() {
566 watcher.cancel();
567 }
568 self.motion_context.set_active(false);
569
570 gs.drag_down_position = Some(position);
571 gs.last_position = Some(position);
572 gs.is_dragging = false;
573 gs.axis_locked_out = false;
574 gs.velocity_tracker.reset();
575 gs.gesture_start_time = Some(Instant::now());
576 gs.gesture_start_event_time_ms = time_ms;
577 gs.is_overscrolling = self.overscroll.offset().abs() > 0.001;
578
579 let pos = if self.is_vertical {
581 position.y
582 } else {
583 position.x
584 };
585 gs.velocity_tracker.add_data_point(0, pos);
586 gs.last_velocity_sample_ms = Some(0);
587
588 false
590 }
591
592 fn on_move(
606 &self,
607 position: Point,
608 buttons: PointerButtons,
609 time_ms: Option<i64>,
610 event: &PointerEvent,
611 ) -> bool {
612 let mut gs = self.gesture_state.borrow_mut();
613
614 if !buttons.contains(PointerButton::Primary) && gs.drag_down_position.is_some() {
616 if gs.is_dragging {
617 self.scroll_target.set_dragging(false);
618 }
619 gs.drag_down_position = None;
620 gs.last_position = None;
621 gs.is_dragging = false;
622 gs.axis_locked_out = false;
623 gs.gesture_start_time = None;
624 gs.gesture_start_event_time_ms = None;
625 gs.last_velocity_sample_ms = None;
626 gs.velocity_tracker.reset();
627 self.motion_context.set_active(false);
628 return false;
629 }
630
631 let Some(down_pos) = gs.drag_down_position else {
632 return false;
633 };
634
635 let Some(last_pos) = gs.last_position else {
636 gs.last_position = Some(position);
637 return false;
638 };
639
640 let incremental_delta = calculate_incremental_delta(last_pos, position, self.is_vertical);
641
642 let mut edge_candidate = false;
652 if !gs.is_dragging && !gs.axis_locked_out {
653 let signed_main_delta = calculate_total_delta(down_pos, position, self.is_vertical);
654 let main_delta = signed_main_delta.abs();
655 let cross_delta = calculate_total_delta(down_pos, position, !self.is_vertical).abs();
656 if main_delta > DRAG_THRESHOLD && main_delta >= cross_delta {
657 if self.scroll_target.can_consume(signed_main_delta) {
660 gs.is_dragging = true;
661 self.scroll_target.set_dragging(true);
662 self.motion_context.set_active(true);
663 } else if self.scroll_target.can_scroll() {
664 edge_candidate = true;
665 }
666 } else if cross_delta > DRAG_THRESHOLD && cross_delta > main_delta {
667 gs.axis_locked_out = true;
668 }
669 }
670
671 gs.last_position = Some(position);
672
673 let pos = if self.is_vertical {
675 position.y
676 } else {
677 position.x
678 };
679 let event_sample_ms = gs
680 .gesture_start_event_time_ms
681 .zip(time_ms)
682 .map(|(start_ms, now_ms)| now_ms - start_ms);
683 let sample_ms = if let Some(event_sample_ms) = event_sample_ms {
684 Some(match gs.last_velocity_sample_ms {
691 Some(last_sample_ms) => event_sample_ms.max(last_sample_ms),
692 None => event_sample_ms.max(0),
693 })
694 } else if let Some(start_time) = gs.gesture_start_time {
695 let elapsed_ms = start_time.elapsed().as_millis() as i64;
698 Some(match gs.last_velocity_sample_ms {
701 Some(last_sample_ms) => {
702 let mut sample_ms = if elapsed_ms <= last_sample_ms {
703 last_sample_ms + 1
704 } else {
705 elapsed_ms
706 };
707 if sample_ms - last_sample_ms > ASSUME_STOPPED_MS {
709 sample_ms = last_sample_ms + ASSUME_STOPPED_MS;
710 }
711 sample_ms
712 }
713 None => elapsed_ms,
714 })
715 } else {
716 None
717 };
718 if let Some(sample_ms) = sample_ms {
719 log::trace!(
720 target: "cranpose::velocity",
721 "sample t={sample_ms}ms pos={pos:.2} event_time={time_ms:?}"
722 );
723 gs.velocity_tracker.add_data_point(sample_ms, pos);
724 gs.last_velocity_sample_ms = Some(sample_ms);
725 }
726
727 if gs.is_dragging {
728 drop(gs); let delta = if self.reverse_scrolling {
730 -incremental_delta
731 } else {
732 incremental_delta
733 };
734 let overscroll = self.overscroll.clone();
735 overscroll.apply_to_scroll(delta, |delta| self.scroll_target.apply_delta(delta));
736 self.scroll_target.invalidate();
737 true } else if gs.is_overscrolling {
739 drop(gs);
740 let delta = if self.reverse_scrolling {
741 -incremental_delta
742 } else {
743 incremental_delta
744 };
745 self.apply_overscroll_delta(delta)
746 } else if edge_candidate {
747 drop(gs);
748 let delta = if self.reverse_scrolling {
749 -incremental_delta
750 } else {
751 incremental_delta
752 };
753 let detector = self.clone_for_watcher();
754 event.defer_post_dispatch_action(move || detector.apply_overscroll_candidate(delta));
755 false
756 } else {
757 false
758 }
759 }
760
761 fn apply_overscroll_delta(&self, delta: f32) -> bool {
762 self.motion_context.set_active(true);
763 let overscroll = self.overscroll.clone();
764 let target_consumed = Cell::new(0.0);
765 let before_overscroll = overscroll.offset();
766 overscroll.apply_to_scroll(delta, |delta| {
767 let consumed = self.scroll_target.apply_delta(delta);
768 target_consumed.set(consumed);
769 consumed
770 });
771 self.scroll_target.invalidate();
772 let consumed = target_consumed.get().abs() > 0.001
773 || (overscroll.offset() - before_overscroll).abs() > 0.001;
774 if !consumed {
775 self.motion_context.set_active(false);
776 }
777 consumed
778 }
779
780 fn apply_overscroll_candidate(&self, delta: f32) -> bool {
781 let consumed = self.apply_overscroll_delta(delta);
782 if consumed {
783 self.gesture_state.borrow_mut().is_overscrolling = true;
784 }
785 consumed
786 }
787
788 fn finish_gesture(&self, allow_fling: bool, release_time_ms: Option<i64>) -> bool {
795 let (was_dragging, gesture_owned, velocity, start_fling, existing_fling) = {
796 let mut gs = self.gesture_state.borrow_mut();
797 let was_dragging = gs.is_dragging;
798 let gesture_owned = was_dragging || gs.is_overscrolling;
799 let mut velocity = 0.0;
800
801 if allow_fling && gesture_owned && gs.gesture_start_time.is_some() {
802 let release_sample_ms = release_time_ms
807 .zip(gs.gesture_start_event_time_ms)
808 .map(|(release_ms, start_ms)| release_ms - start_ms)
809 .or_else(|| {
810 gs.gesture_start_time
811 .map(|start| start.elapsed().as_millis() as i64)
812 });
813 let rested_before_release = release_sample_ms
814 .zip(gs.last_velocity_sample_ms)
815 .is_some_and(|(release_ms, last_sample_ms)| {
816 release_ms - last_sample_ms > ASSUME_STOPPED_MS
817 });
818 if !rested_before_release {
819 velocity = gs
820 .velocity_tracker
821 .calculate_velocity_with_max(MAX_FLING_VELOCITY);
822 }
823 }
824
825 let start_fling = allow_fling && was_dragging && velocity.abs() > MIN_FLING_VELOCITY;
826 let existing_fling = if start_fling {
827 gs.fling_animation.take()
828 } else {
829 None
830 };
831
832 if was_dragging {
833 self.scroll_target.set_dragging(false);
834 }
835 gs.drag_down_position = None;
836 gs.last_position = None;
837 gs.is_dragging = false;
838 gs.is_overscrolling = false;
839 gs.axis_locked_out = false;
840 gs.gesture_start_time = None;
841 gs.gesture_start_event_time_ms = None;
842 gs.last_velocity_sample_ms = None;
843
844 (
845 was_dragging,
846 gesture_owned,
847 velocity,
848 start_fling,
849 existing_fling,
850 )
851 };
852
853 if allow_fling && gesture_owned {
855 log::debug!(
856 target: "cranpose::velocity",
857 "gesture finished: fling velocity={velocity:.2} dp/s start_fling={start_fling}"
858 );
859 set_last_fling_velocity(velocity);
860 }
861
862 let adjusted_velocity = if self.reverse_scrolling {
864 -velocity
865 } else {
866 velocity
867 };
868 let fling_velocity = -adjusted_velocity;
869 let has_overscroll = self.overscroll.offset().abs() > 0.001;
870
871 let settle_target = if was_dragging {
876 self.scroll_target.settle_policy().and_then(|policy| {
877 let current = self.scroll_target.current_offset();
878 let proposed = if start_fling {
879 fling_rest_position(current, fling_velocity, current_density())
880 } else {
881 current
882 };
883 let target = policy(proposed, fling_velocity);
884 ((target - proposed).abs() > 0.5).then_some(target)
885 })
886 } else {
887 None
888 };
889
890 if has_overscroll {
891 if let Some(old_fling) = existing_fling {
892 old_fling.cancel();
893 }
894 self.start_overscroll_settle(-fling_velocity);
895 } else if let Some(target) = settle_target {
896 if let Some(old_fling) = existing_fling {
897 old_fling.cancel();
898 }
899 self.start_settle_animation(target, fling_velocity);
900 } else if start_fling {
901 if let Some(old_fling) = existing_fling {
902 old_fling.cancel();
903 }
904 self.start_fling_animation(fling_velocity);
905 } else {
906 self.motion_context.set_active(false);
907 }
908
909 gesture_owned
910 }
911
912 fn start_fling_animation(&self, fling_velocity: f32) {
913 let Some(runtime) = current_runtime_handle() else {
914 self.motion_context.set_active(false);
915 return;
916 };
917 self.motion_context.set_active(true);
918 let scroll_target = self.scroll_target.clone();
919 let fling = FlingAnimation::new(runtime);
920 let motion_context = self.motion_context.clone();
921 let initial_value = scroll_target.current_offset();
922 let scroll_target_for_fling = scroll_target.clone();
923 let scroll_target_for_end = scroll_target.clone();
924 let detector_for_end = self.clone_for_watcher();
925 let overscroll_for_fling = self.overscroll.clone();
926
927 fling.start_fling(
928 initial_value,
929 fling_velocity,
930 current_density(),
931 move |delta| {
932 let consumed = overscroll_for_fling.apply_to_fling(delta, |delta| {
933 scroll_target_for_fling.apply_fling_delta(delta)
934 });
935 scroll_target_for_fling.invalidate();
936 consumed
937 },
938 move || {
939 scroll_target_for_end.invalidate();
940 let settle_running = detector_for_end
941 .gesture_state
942 .borrow()
943 .settle_animation
944 .as_ref()
945 .is_some_and(SettleAnimation::is_running);
946 if detector_for_end.overscroll.offset().abs() > 0.001 && !settle_running {
947 detector_for_end.start_overscroll_settle(0.0);
948 }
949 detector_for_end.update_motion_active(&motion_context);
950 },
951 );
952
953 let mut gs = self.gesture_state.borrow_mut();
954 gs.fling_animation = Some(fling);
955 }
956
957 fn start_settle_animation(&self, target: f32, initial_velocity: f32) {
960 let Some(runtime) = current_runtime_handle() else {
961 self.motion_context.set_active(false);
962 return;
963 };
964 self.motion_context.set_active(true);
965 let settle = SettleAnimation::new(runtime);
966 let scroll_target_for_settle = self.scroll_target.clone();
967 let scroll_target_for_end = self.scroll_target.clone();
968 let detector_for_end = self.clone_for_watcher();
969 let motion_context = self.motion_context.clone();
970 settle.start_settle(
971 self.scroll_target.current_offset(),
972 initial_velocity,
973 target,
974 move |delta| {
975 let consumed = scroll_target_for_settle.apply_fling_delta(delta);
976 scroll_target_for_settle.invalidate();
977 consumed
978 },
979 move |_| {
980 scroll_target_for_end.invalidate();
981 detector_for_end.update_motion_active(&motion_context);
982 },
983 );
984 let mut gs = self.gesture_state.borrow_mut();
985 gs.settle_animation = Some(settle);
986 }
987
988 fn start_overscroll_settle(&self, initial_velocity: f32) {
989 let Some(runtime) = current_runtime_handle() else {
990 let current = self.overscroll.offset();
991 self.overscroll.apply_settle_delta(-current);
992 self.motion_context.set_active(false);
993 return;
994 };
995 let settle = SettleAnimation::new(runtime);
996 let overscroll_for_settle = self.overscroll.clone();
997 let overscroll_for_end = self.overscroll.clone();
998 let detector_for_end = self.clone_for_watcher();
999 let motion_context = self.motion_context.clone();
1000 let initial = self.overscroll.offset();
1001 settle.start_settle(
1002 initial,
1003 initial_velocity,
1004 0.0,
1005 move |delta| overscroll_for_settle.apply_settle_delta(delta),
1006 move |end| {
1007 let current = overscroll_for_end.offset();
1008 overscroll_for_end.apply_settle_delta(-current);
1009 if end.hit_boundary && end.velocity.abs() > MIN_FLING_VELOCITY {
1010 detector_for_end.start_fling_animation(-end.velocity);
1011 } else {
1012 detector_for_end.update_motion_active(&motion_context);
1013 }
1014 },
1015 );
1016 let mut gs = self.gesture_state.borrow_mut();
1017 gs.is_overscrolling = false;
1018 gs.settle_animation = Some(settle);
1019 }
1020
1021 fn update_motion_active(&self, motion_context: &ScrollMotionContext) {
1022 let running = {
1023 let gs = self.gesture_state.borrow();
1024 gs.fling_animation
1025 .as_ref()
1026 .is_some_and(FlingAnimation::is_running)
1027 || gs
1028 .settle_animation
1029 .as_ref()
1030 .is_some_and(SettleAnimation::is_running)
1031 };
1032 if !running {
1033 motion_context.set_active(false);
1034 }
1035 }
1036
1037 fn on_up(&self, time_ms: Option<i64>) -> bool {
1044 self.finish_gesture(self.scroll_target.allows_fling(), time_ms)
1045 }
1046
1047 fn on_cancel(&self) -> bool {
1051 self.finish_gesture(false, None)
1052 }
1053
1054 fn on_scroll(&self, axis_delta: f32, event: &PointerEvent) -> bool {
1058 if axis_delta.abs() <= f32::EPSILON {
1059 return false;
1060 }
1061
1062 let delta = if self.reverse_scrolling {
1063 -axis_delta
1064 } else {
1065 axis_delta
1066 };
1067 if !self.scroll_target.can_consume(delta) && self.scroll_target.can_scroll() {
1068 let detector = self.clone_for_watcher();
1069 event.defer_post_dispatch_action(move || detector.apply_wheel_delta(delta));
1070 return false;
1071 }
1072 self.apply_wheel_delta(delta)
1073 }
1074
1075 fn apply_wheel_delta(&self, delta: f32) -> bool {
1076 {
1077 let mut gs = self.gesture_state.borrow_mut();
1078 if let Some(fling) = gs.fling_animation.take() {
1079 fling.cancel();
1080 }
1081 if let Some(settle) = gs.settle_animation.take() {
1082 settle.cancel();
1083 }
1084 gs.drag_down_position = None;
1085 gs.last_position = None;
1086 gs.is_dragging = false;
1087 gs.axis_locked_out = false;
1088 gs.gesture_start_time = None;
1089 gs.gesture_start_event_time_ms = None;
1090 gs.last_velocity_sample_ms = None;
1091 gs.velocity_tracker.reset();
1092 }
1093
1094 self.motion_context.activate_for_current_frame();
1095 let overscroll = self.overscroll.clone();
1096 let consumed =
1097 overscroll.apply_to_scroll(delta, |delta| self.scroll_target.apply_wheel_delta(delta));
1098 let overscroll_active = self.overscroll.offset().abs() > 0.001;
1099 if consumed.abs() > 0.001 {
1100 self.scroll_target.invalidate();
1101 }
1102 if consumed.abs() > 0.001 || overscroll_active {
1103 self.ensure_wheel_settle_watcher();
1104 true
1105 } else {
1106 false
1107 }
1108 }
1109
1110 fn ensure_wheel_settle_watcher(&self) {
1115 if self.scroll_target.settle_policy().is_none() && self.overscroll.offset().abs() <= 0.001 {
1116 return;
1117 }
1118 {
1119 let gs = self.gesture_state.borrow();
1120 if gs
1121 .wheel_settle_watcher
1122 .as_ref()
1123 .is_some_and(|watcher| watcher.is_running.get())
1124 {
1125 return;
1126 }
1127 }
1128 let Some(runtime) = current_runtime_handle() else {
1129 return;
1130 };
1131
1132 let is_running = Rc::new(Cell::new(true));
1133 let registration = Rc::new(RefCell::new(None));
1134
1135 struct WheelSettleLoop<S: ScrollTarget> {
1136 detector: ScrollGestureDetector<S>,
1137 gesture_state: Rc<RefCell<ScrollGestureState>>,
1138 frame_clock: cranpose_core::internal::FrameClock,
1139 is_running: Rc<Cell<bool>>,
1140 registration: Rc<RefCell<Option<FrameCallbackRegistration>>>,
1141 last_offset: Rc<Cell<f32>>,
1142 idle_nanos: Rc<Cell<u64>>,
1143 last_frame: Rc<Cell<Option<u64>>>,
1144 }
1145
1146 impl<S: ScrollTarget + 'static> WheelSettleLoop<S> {
1147 fn next(&self) -> Self {
1148 Self {
1149 detector: self.detector.clone_for_watcher(),
1150 gesture_state: Rc::clone(&self.gesture_state),
1151 frame_clock: self.frame_clock.clone(),
1152 is_running: Rc::clone(&self.is_running),
1153 registration: Rc::clone(&self.registration),
1154 last_offset: Rc::clone(&self.last_offset),
1155 idle_nanos: Rc::clone(&self.idle_nanos),
1156 last_frame: Rc::clone(&self.last_frame),
1157 }
1158 }
1159
1160 fn schedule(self) {
1161 let continuation = self.next();
1162 let registration_slot = Rc::clone(&self.registration);
1163 let new_registration = self.frame_clock.with_frame_nanos(move |frame_time_nanos| {
1164 let this = &continuation;
1165 if !this.is_running.get() {
1166 return;
1167 }
1168 {
1170 let gs = this.gesture_state.borrow();
1171 let animating = gs.is_dragging
1172 || gs
1173 .fling_animation
1174 .as_ref()
1175 .is_some_and(FlingAnimation::is_running)
1176 || gs
1177 .settle_animation
1178 .as_ref()
1179 .is_some_and(SettleAnimation::is_running);
1180 if animating {
1181 this.is_running.set(false);
1182 return;
1183 }
1184 }
1185
1186 let offset = this.detector.scroll_target.current_offset();
1187 let dt = this
1188 .last_frame
1189 .get()
1190 .map_or(0, |last| frame_time_nanos.saturating_sub(last));
1191 this.last_frame.set(Some(frame_time_nanos));
1192 if (offset - this.last_offset.get()).abs() > 0.01 {
1193 this.last_offset.set(offset);
1194 this.idle_nanos.set(0);
1195 } else {
1196 this.idle_nanos.set(this.idle_nanos.get() + dt);
1197 }
1198
1199 if this.idle_nanos.get() >= WHEEL_SETTLE_IDLE_NANOS {
1200 this.is_running.set(false);
1201 if this.detector.overscroll.offset().abs() > 0.001 {
1202 this.detector.start_overscroll_settle(0.0);
1203 return;
1204 }
1205 if let Some(policy) = this.detector.scroll_target.settle_policy() {
1206 let target = policy(offset, 0.0);
1207 if (target - offset).abs() > 0.5 {
1208 this.detector.start_settle_animation(target, 0.0);
1209 }
1210 }
1211 return;
1212 }
1213
1214 continuation.next().schedule();
1215 });
1216 *registration_slot.borrow_mut() = Some(new_registration);
1217 }
1218 }
1219
1220 WheelSettleLoop {
1221 detector: self.clone_for_watcher(),
1222 gesture_state: Rc::clone(&self.gesture_state),
1223 frame_clock: runtime.frame_clock(),
1224 is_running: Rc::clone(&is_running),
1225 registration: Rc::clone(®istration),
1226 last_offset: Rc::new(Cell::new(self.scroll_target.current_offset())),
1227 idle_nanos: Rc::new(Cell::new(0u64)),
1228 last_frame: Rc::new(Cell::new(None::<u64>)),
1229 }
1230 .schedule();
1231
1232 self.gesture_state.borrow_mut().wheel_settle_watcher = Some(WheelSettleWatcher {
1233 is_running,
1234 registration,
1235 });
1236 }
1237
1238 fn clone_for_watcher(&self) -> ScrollGestureDetector<S> {
1239 ScrollGestureDetector {
1240 gesture_state: Rc::clone(&self.gesture_state),
1241 scroll_target: self.scroll_target.clone(),
1242 is_vertical: self.is_vertical,
1243 reverse_scrolling: self.reverse_scrolling,
1244 overscroll: self.overscroll.clone(),
1245 motion_context: self.motion_context.clone(),
1246 }
1247 }
1248}
1249
1250pub(crate) struct MotionContextAnimatedNode {
1251 state: NodeState,
1252 motion_context: ScrollMotionContext,
1253 invalidation_callback_id: Option<u64>,
1254 overscroll_callback_id: Option<u64>,
1255 node_id: Option<NodeId>,
1256}
1257
1258impl MotionContextAnimatedNode {
1259 fn new(motion_context: ScrollMotionContext) -> Self {
1260 Self {
1261 state: NodeState::new(),
1262 motion_context,
1263 invalidation_callback_id: None,
1264 overscroll_callback_id: None,
1265 node_id: None,
1266 }
1267 }
1268
1269 pub(crate) fn is_active(&self) -> bool {
1270 self.motion_context.is_active()
1271 }
1272}
1273
1274pub(crate) struct TranslatedContentContextNode {
1275 state: NodeState,
1276 identity: usize,
1277 offset_source: TranslatedContentOffsetSource,
1278 overscroll: crate::scroll::OverscrollEffect,
1279 overscroll_callback_id: Option<u64>,
1280}
1281
1282impl TranslatedContentContextNode {
1283 fn new(
1284 identity: usize,
1285 offset_source: TranslatedContentOffsetSource,
1286 overscroll: crate::scroll::OverscrollEffect,
1287 ) -> Self {
1288 Self {
1289 state: NodeState::new(),
1290 identity,
1291 offset_source,
1292 overscroll,
1293 overscroll_callback_id: None,
1294 }
1295 }
1296
1297 pub(crate) fn is_active(&self) -> bool {
1298 true
1299 }
1300
1301 pub(crate) fn identity(&self) -> usize {
1302 self.identity
1303 }
1304
1305 pub(crate) fn content_offset_reader(&self) -> Option<Rc<dyn Fn() -> Point>> {
1306 self.offset_source
1307 .content_offset_reader(self.overscroll.clone())
1308 }
1309}
1310
1311impl DelegatableNode for TranslatedContentContextNode {
1312 fn node_state(&self) -> &NodeState {
1313 &self.state
1314 }
1315}
1316
1317impl ModifierNode for TranslatedContentContextNode {
1318 fn on_attach(&mut self, context: &mut dyn cranpose_foundation::ModifierNodeContext) {
1319 if let Some(node_id) = context.node_id() {
1320 self.overscroll_callback_id =
1321 Some(self.overscroll.add_invalidate_callback(Box::new(move || {
1322 schedule_modifier_slices_repass(node_id)
1323 })));
1324 }
1325 }
1326
1327 fn on_detach(&mut self) {
1328 if let Some(id) = self.overscroll_callback_id.take() {
1329 self.overscroll.remove_invalidate_callback(id);
1330 }
1331 }
1332}
1333
1334impl DelegatableNode for MotionContextAnimatedNode {
1335 fn node_state(&self) -> &NodeState {
1336 &self.state
1337 }
1338}
1339
1340impl ModifierNode for MotionContextAnimatedNode {
1341 fn on_attach(&mut self, context: &mut dyn cranpose_foundation::ModifierNodeContext) {
1342 let node_id = context.node_id();
1343 self.node_id = node_id;
1344 if let Some(node_id) = node_id {
1345 let callback_id = self
1346 .motion_context
1347 .add_invalidate_callback(Box::new(move || {
1348 schedule_modifier_slices_repass(node_id);
1349 }));
1350 self.invalidation_callback_id = Some(callback_id);
1351 let callback_id = self
1352 .motion_context
1353 .overscroll()
1354 .add_invalidate_callback(Box::new(move || {
1355 crate::schedule_measure_repass(node_id);
1356 schedule_modifier_slices_repass(node_id);
1357 }));
1358 self.overscroll_callback_id = Some(callback_id);
1359 }
1360 }
1361
1362 fn on_detach(&mut self) {
1363 if let Some(id) = self.invalidation_callback_id.take() {
1364 self.motion_context.remove_invalidate_callback(id);
1365 }
1366 if let Some(id) = self.overscroll_callback_id.take() {
1367 self.motion_context
1368 .overscroll()
1369 .remove_invalidate_callback(id);
1370 }
1371 self.node_id = None;
1372 }
1373}
1374
1375#[derive(Clone)]
1376struct MotionContextAnimatedElement {
1377 motion_context: ScrollMotionContext,
1378}
1379
1380impl MotionContextAnimatedElement {
1381 fn new(motion_context: ScrollMotionContext) -> Self {
1382 Self { motion_context }
1383 }
1384}
1385
1386impl std::fmt::Debug for MotionContextAnimatedElement {
1387 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1388 f.debug_struct("MotionContextAnimatedElement").finish()
1389 }
1390}
1391
1392impl PartialEq for MotionContextAnimatedElement {
1393 fn eq(&self, other: &Self) -> bool {
1394 self.motion_context.ptr_eq(&other.motion_context)
1395 }
1396}
1397
1398impl Eq for MotionContextAnimatedElement {}
1399
1400impl std::hash::Hash for MotionContextAnimatedElement {
1401 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1402 self.motion_context.stable_key().hash(state);
1403 }
1404}
1405
1406impl ModifierNodeElement for MotionContextAnimatedElement {
1407 type Node = MotionContextAnimatedNode;
1408
1409 fn create(&self) -> Self::Node {
1410 MotionContextAnimatedNode::new(self.motion_context.clone())
1411 }
1412
1413 fn update(&self, node: &mut Self::Node) {
1414 if node.motion_context.ptr_eq(&self.motion_context) {
1415 return;
1416 }
1417 if let Some(id) = node.invalidation_callback_id.take() {
1418 node.motion_context.remove_invalidate_callback(id);
1419 }
1420 node.motion_context = self.motion_context.clone();
1421 if let Some(node_id) = node.node_id {
1422 let callback_id = node
1423 .motion_context
1424 .add_invalidate_callback(Box::new(move || {
1425 schedule_modifier_slices_repass(node_id);
1426 }));
1427 node.invalidation_callback_id = Some(callback_id);
1428 }
1429 }
1430
1431 fn capabilities(&self) -> NodeCapabilities {
1432 NodeCapabilities::LAYOUT
1433 }
1434}
1435
1436#[derive(Clone)]
1437enum TranslatedContentOffsetSource {
1438 LayoutContentOffset,
1439 LazyList {
1440 state: LazyListState,
1441 is_vertical: bool,
1442 reverse_scrolling: bool,
1443 },
1444}
1445
1446impl TranslatedContentOffsetSource {
1447 fn content_offset_reader(
1448 &self,
1449 overscroll: crate::scroll::OverscrollEffect,
1450 ) -> Option<Rc<dyn Fn() -> Point>> {
1451 match self {
1452 Self::LayoutContentOffset => None,
1453 Self::LazyList {
1454 state, is_vertical, ..
1455 } => Some(Rc::new(lazy_list_content_offset_reader(
1456 *state,
1457 *is_vertical,
1458 overscroll,
1459 ))),
1460 }
1461 }
1462
1463 fn is_vertical(&self) -> Option<bool> {
1464 match self {
1465 Self::LayoutContentOffset => None,
1466 Self::LazyList { is_vertical, .. } => Some(*is_vertical),
1467 }
1468 }
1469
1470 fn reverse_scrolling(&self) -> Option<bool> {
1471 match self {
1472 Self::LayoutContentOffset => None,
1473 Self::LazyList {
1474 reverse_scrolling, ..
1475 } => Some(*reverse_scrolling),
1476 }
1477 }
1478}
1479
1480fn lazy_list_content_offset_reader(
1481 state: LazyListState,
1482 is_vertical: bool,
1483 overscroll: crate::scroll::OverscrollEffect,
1484) -> impl Fn() -> Point {
1485 move || {
1486 let info = state.layout_info();
1487 if info.visible_items_info.is_empty() {
1488 return Point::default();
1489 };
1490 overscroll.set_limit(info.viewport_size * 0.5);
1491 let main_offset = info.snap_anchor_offset;
1492 let bounce = overscroll.offset();
1493 if is_vertical {
1494 Point::new(0.0, main_offset + bounce)
1495 } else {
1496 Point::new(main_offset + bounce, 0.0)
1497 }
1498 }
1499}
1500
1501#[derive(Clone)]
1502struct TranslatedContentContextElement {
1503 identity: usize,
1504 offset_source: TranslatedContentOffsetSource,
1505 overscroll: crate::scroll::OverscrollEffect,
1506}
1507
1508impl TranslatedContentContextElement {
1509 fn new(
1510 identity: usize,
1511 offset_source: TranslatedContentOffsetSource,
1512 overscroll: crate::scroll::OverscrollEffect,
1513 ) -> Self {
1514 Self {
1515 identity,
1516 offset_source,
1517 overscroll,
1518 }
1519 }
1520}
1521
1522impl std::fmt::Debug for TranslatedContentContextElement {
1523 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1524 let offset_source = match &self.offset_source {
1525 TranslatedContentOffsetSource::LayoutContentOffset => "layout",
1526 TranslatedContentOffsetSource::LazyList { .. } => "lazy_list",
1527 };
1528 f.debug_struct("TranslatedContentContextElement")
1529 .field("identity", &self.identity)
1530 .field("offset_source", &offset_source)
1531 .finish()
1532 }
1533}
1534
1535impl PartialEq for TranslatedContentContextElement {
1536 fn eq(&self, other: &Self) -> bool {
1537 self.identity == other.identity
1538 && self.overscroll.ptr_eq(&other.overscroll)
1539 && self.offset_source.is_vertical() == other.offset_source.is_vertical()
1540 && self.offset_source.reverse_scrolling() == other.offset_source.reverse_scrolling()
1541 }
1542}
1543
1544impl Eq for TranslatedContentContextElement {}
1545
1546impl std::hash::Hash for TranslatedContentContextElement {
1547 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1548 self.identity.hash(state);
1549 self.offset_source.is_vertical().hash(state);
1550 self.offset_source.reverse_scrolling().hash(state);
1551 }
1552}
1553
1554impl ModifierNodeElement for TranslatedContentContextElement {
1555 type Node = TranslatedContentContextNode;
1556
1557 fn create(&self) -> Self::Node {
1558 TranslatedContentContextNode::new(
1559 self.identity,
1560 self.offset_source.clone(),
1561 self.overscroll.clone(),
1562 )
1563 }
1564
1565 fn update(&self, node: &mut Self::Node) {
1566 node.identity = self.identity;
1567 node.offset_source = self.offset_source.clone();
1568 node.overscroll = self.overscroll.clone();
1569 }
1570
1571 fn capabilities(&self) -> NodeCapabilities {
1572 NodeCapabilities::LAYOUT
1573 }
1574}
1575
1576impl Modifier {
1581 pub fn horizontal_scroll(self, state: ScrollState, reverse_scrolling: bool) -> Self {
1599 self.then(scroll_impl(state, false, reverse_scrolling, None))
1600 }
1601
1602 pub fn vertical_scroll(self, state: ScrollState, reverse_scrolling: bool) -> Self {
1611 self.then(scroll_impl(state, true, reverse_scrolling, None))
1612 }
1613}
1614
1615fn scroll_impl(
1624 state: ScrollState,
1625 is_vertical: bool,
1626 reverse_scrolling: bool,
1627 guard: Option<Rc<dyn Fn() -> bool>>,
1628) -> Modifier {
1629 let gesture_state = Rc::new(RefCell::new(ScrollGestureState::default()));
1631 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::ScrollState {
1632 state_id: state.id(),
1633 is_vertical,
1634 reverse_scrolling,
1635 });
1636
1637 let pointer_input = drag_gesture_input(
1639 (state.id(), is_vertical),
1640 DragGesture {
1641 target: state,
1642 gesture_state,
1643 is_vertical,
1644 reverse_input: false,
1646 motion_context: motion_context.clone(),
1647 guard,
1648 },
1649 );
1650
1651 let overscroll = motion_context.overscroll();
1653 let element = ScrollElement::new(state, overscroll.clone(), is_vertical, reverse_scrolling);
1654 let layout_modifier =
1655 Modifier::with_element(element).with_inspector_metadata(inspector_metadata(
1656 if is_vertical {
1657 "verticalScroll"
1658 } else {
1659 "horizontalScroll"
1660 },
1661 move |info| {
1662 info.add_property("isVertical", is_vertical.to_string());
1663 info.add_property("reverseScrolling", reverse_scrolling.to_string());
1664 },
1665 ));
1666 let motion_modifier =
1667 Modifier::with_element(MotionContextAnimatedElement::new(motion_context.clone()));
1668 let translated_content_modifier = Modifier::with_element(TranslatedContentContextElement::new(
1669 state.id() as usize,
1670 TranslatedContentOffsetSource::LayoutContentOffset,
1671 overscroll,
1672 ));
1673
1674 pointer_input
1676 .then(motion_modifier)
1677 .then(translated_content_modifier)
1678 .then(layout_modifier)
1679 .clip_to_bounds()
1680}
1681
1682use cranpose_foundation::lazy::LazyListState;
1687
1688impl Modifier {
1689 pub fn lazy_vertical_scroll(self, state: LazyListState, reverse_scrolling: bool) -> Self {
1700 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::LazyList {
1701 state_identity: state.inner_ptr() as usize,
1702 is_vertical: true,
1703 reverse_scrolling,
1704 });
1705 self.lazy_vertical_scroll_with_context(state, reverse_scrolling, motion_context)
1706 }
1707
1708 pub(crate) fn lazy_vertical_scroll_with_context(
1709 self,
1710 state: LazyListState,
1711 reverse_scrolling: bool,
1712 motion_context: ScrollMotionContext,
1713 ) -> Self {
1714 self.then(lazy_scroll_impl(
1715 state,
1716 true,
1717 reverse_scrolling,
1718 motion_context,
1719 ))
1720 }
1721
1722 pub(crate) fn lazy_horizontal_scroll_with_context(
1723 self,
1724 state: LazyListState,
1725 reverse_scrolling: bool,
1726 motion_context: ScrollMotionContext,
1727 ) -> Self {
1728 self.then(lazy_scroll_impl(
1729 state,
1730 false,
1731 reverse_scrolling,
1732 motion_context,
1733 ))
1734 }
1735}
1736
1737fn lazy_scroll_impl(
1739 state: LazyListState,
1740 is_vertical: bool,
1741 reverse_scrolling: bool,
1742 motion_context: ScrollMotionContext,
1743) -> Modifier {
1744 let gesture_state = Rc::new(RefCell::new(ScrollGestureState::default()));
1745 let list_state = state;
1746 let state_id = state.inner_ptr() as usize;
1747 let key = (state_id, is_vertical, reverse_scrolling);
1748 let overscroll = motion_context.overscroll();
1749 let translated_content_modifier = Modifier::with_element(TranslatedContentContextElement::new(
1750 state_id,
1751 TranslatedContentOffsetSource::LazyList {
1752 state,
1753 is_vertical,
1754 reverse_scrolling,
1755 },
1756 overscroll,
1757 ));
1758
1759 Modifier::with_element(MotionContextAnimatedElement::new(motion_context.clone()))
1760 .then(translated_content_modifier)
1761 .then(drag_gesture_input(
1762 key,
1763 DragGesture {
1764 target: list_state,
1765 gesture_state,
1766 is_vertical,
1767 reverse_input: reverse_scrolling,
1768 motion_context,
1769 guard: None,
1770 },
1771 ))
1772}
1773
1774impl Modifier {
1779 pub fn draggable(self, axis: Axis, state: DraggableState) -> Self {
1791 self.then(draggable_impl(axis, state, None))
1792 }
1793
1794 pub fn draggable_guarded(
1800 self,
1801 axis: Axis,
1802 state: DraggableState,
1803 guard: impl Fn() -> bool + 'static,
1804 ) -> Self {
1805 self.then(draggable_impl(axis, state, Some(Rc::new(guard))))
1806 }
1807}
1808
1809fn draggable_impl(
1810 axis: Axis,
1811 state: DraggableState,
1812 guard: Option<Rc<dyn Fn() -> bool>>,
1813) -> Modifier {
1814 let is_vertical = axis.is_vertical();
1815 let identity = state.identity();
1816 drag_gesture_input(
1817 (identity, is_vertical),
1818 DragGesture {
1819 target: state,
1820 gesture_state: Rc::new(RefCell::new(ScrollGestureState::default())),
1821 is_vertical,
1822 reverse_input: false,
1823 motion_context: scroll_motion_context_for_key(ScrollMotionContextKey::Draggable {
1826 state_identity: identity,
1827 is_vertical,
1828 }),
1829 guard,
1830 },
1831 )
1832}