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 draggable::DraggableState,
43 fling_animation::{
44 FlingAnimation, MIN_FLING_VELOCITY, SettleAnimation, SpringParams, fling_rest_position,
45 },
46 render_state::schedule_modifier_slices_repass,
47 scroll::{
48 ScrollElement, ScrollMotionContext, ScrollMotionContextKey, ScrollSettlePolicy,
49 ScrollState, scroll_motion_context_for_key,
50 },
51};
52
53#[cfg(feature = "test-helpers")]
54pub fn last_fling_velocity() -> f32 {
55 crate::render_state::debug_last_fling_velocity()
56}
57
58#[cfg(feature = "test-helpers")]
59pub fn reset_last_fling_velocity() {
60 crate::render_state::debug_reset_last_fling_velocity();
61}
62
63#[inline]
64fn set_last_fling_velocity(velocity: f32) {
65 crate::render_state::record_last_fling_velocity(velocity);
66}
67
68struct ScrollGestureState {
74 drag_down_position: Option<Point>,
77
78 last_position: Option<Point>,
81
82 is_dragging: bool,
86
87 axis_locked_out: bool,
93
94 velocity_tracker: VelocityTracker1D,
96
97 gesture_start_time: Option<Instant>,
99
100 gesture_start_event_time_ms: Option<i64>,
104
105 last_velocity_sample_ms: Option<i64>,
107
108 fling_animation: Option<FlingAnimation>,
110
111 is_overscrolling: bool,
112
113 settle_animation: Option<SettleAnimation>,
115
116 wheel_settle_watcher: Option<WheelSettleWatcher>,
119}
120
121impl Default for ScrollGestureState {
122 fn default() -> Self {
123 Self {
124 drag_down_position: None,
125 last_position: None,
126 is_dragging: false,
127 axis_locked_out: false,
128 velocity_tracker: VelocityTracker1D::new(),
129 gesture_start_time: None,
130 gesture_start_event_time_ms: None,
131 last_velocity_sample_ms: None,
132 fling_animation: None,
133 is_overscrolling: false,
134 settle_animation: None,
135 wheel_settle_watcher: None,
136 }
137 }
138}
139
140#[inline]
150fn calculate_total_delta(from: Point, to: Point, is_vertical: bool) -> f32 {
151 if is_vertical {
152 to.y - from.y
153 } else {
154 to.x - from.x
155 }
156}
157
158#[inline]
163fn calculate_incremental_delta(from: Point, to: Point, is_vertical: bool) -> f32 {
164 if is_vertical {
165 to.y - from.y
166 } else {
167 to.x - from.x
168 }
169}
170
171trait ScrollTarget: Clone {
179 fn apply_delta(&self, delta: f32) -> f32;
181
182 fn apply_wheel_delta(&self, delta: f32) -> f32 {
184 self.apply_delta(delta)
185 }
186
187 fn apply_fling_delta(&self, delta: f32) -> f32;
189
190 fn invalidate(&self);
192
193 fn current_offset(&self) -> f32;
195
196 fn can_scroll(&self) -> bool {
204 true
205 }
206
207 fn can_consume(&self, gesture_delta: f32) -> bool {
213 let _ = gesture_delta;
214 self.can_scroll()
215 }
216
217 fn settle_policy(&self) -> Option<ScrollSettlePolicy> {
220 None
221 }
222
223 fn allows_fling(&self) -> bool {
230 true
231 }
232
233 fn set_dragging(&self, dragging: bool) {
236 let _ = dragging;
237 }
238}
239
240impl ScrollTarget for ScrollState {
241 fn apply_delta(&self, delta: f32) -> f32 {
242 -self.dispatch_raw_delta(-delta)
243 }
244
245 fn apply_fling_delta(&self, delta: f32) -> f32 {
246 self.dispatch_raw_delta(delta)
247 }
248
249 fn invalidate(&self) {
250 }
252
253 fn current_offset(&self) -> f32 {
254 self.value()
255 }
256
257 fn can_scroll(&self) -> bool {
258 self.max_value() > 0.0
259 }
260
261 fn can_consume(&self, gesture_delta: f32) -> bool {
262 let raw = -gesture_delta;
265 if raw > 0.0 {
266 self.value_non_reactive() < self.max_value()
267 } else {
268 self.value_non_reactive() > 0.0
269 }
270 }
271
272 fn settle_policy(&self) -> Option<ScrollSettlePolicy> {
273 ScrollState::settle_policy(self)
274 }
275}
276
277impl ScrollTarget for LazyListState {
278 fn apply_delta(&self, delta: f32) -> f32 {
279 self.dispatch_scroll_delta(delta)
283 }
284
285 fn apply_wheel_delta(&self, delta: f32) -> f32 {
286 if delta.abs() <= 0.001 {
287 0.0
288 } else {
289 self.dispatch_scroll_delta(delta)
290 }
291 }
292
293 fn apply_fling_delta(&self, delta: f32) -> f32 {
294 -self.dispatch_scroll_delta(-delta)
295 }
296
297 fn invalidate(&self) {
298 }
301
302 fn current_offset(&self) -> f32 {
303 self.first_visible_item_scroll_offset()
305 }
306
307 fn can_scroll(&self) -> bool {
308 self.layout_info().total_items_count == 0
311 || self.can_scroll_forward_non_reactive()
312 || self.can_scroll_backward_non_reactive()
313 }
314
315 fn can_consume(&self, gesture_delta: f32) -> bool {
316 if self.layout_info().total_items_count == 0 {
320 return true;
321 }
322 if gesture_delta < 0.0 {
323 self.can_scroll_forward_non_reactive()
324 } else {
325 self.can_scroll_backward_non_reactive()
326 }
327 }
328}
329
330struct DragGesture<S: ScrollTarget> {
338 target: S,
339 gesture_state: Rc<RefCell<ScrollGestureState>>,
340 is_vertical: bool,
341 reverse_input: bool,
342 motion_context: ScrollMotionContext,
343 guard: Option<Rc<dyn Fn() -> bool>>,
344}
345
346fn drag_gesture_input<K, S>(key: K, gesture: DragGesture<S>) -> Modifier
353where
354 K: std::hash::Hash + 'static,
355 S: ScrollTarget + 'static,
356{
357 let DragGesture {
358 target,
359 gesture_state,
360 is_vertical,
361 reverse_input,
362 motion_context,
363 guard,
364 } = gesture;
365
366 Modifier::empty().pointer_input(key, move |scope| {
367 let detector = ScrollGestureDetector::new(
368 gesture_state.clone(),
369 target.clone(),
370 is_vertical,
371 reverse_input,
372 motion_context.overscroll(),
373 motion_context.clone(),
374 );
375 let guard = guard.clone();
376
377 async move {
378 scope
379 .await_pointer_event_scope(|await_scope| async move {
380 loop {
381 let event = await_scope.await_pointer_event().await;
382
383 if event.id != 0 {
387 continue;
388 }
389
390 if event.is_consumed() {
393 if matches!(
394 event.kind,
395 PointerEventKind::Down
396 | PointerEventKind::Move
397 | PointerEventKind::Up
398 | PointerEventKind::Cancel
399 ) {
400 detector.on_cancel();
401 }
402 continue;
403 }
404
405 if let Some(ref guard) = guard
406 && !guard()
407 {
408 if matches!(event.kind, PointerEventKind::Up | PointerEventKind::Cancel)
409 {
410 detector.on_cancel();
411 }
412 continue;
413 }
414
415 let should_consume = match event.kind {
416 PointerEventKind::Down => {
417 detector.on_down(event.position, event.time_ms)
418 }
419 PointerEventKind::Move => detector.on_move(
420 event.position,
421 event.buttons,
422 event.time_ms,
423 &event,
424 ),
425 PointerEventKind::Up => detector.on_up(event.time_ms),
426 PointerEventKind::Cancel => detector.on_cancel(),
427 PointerEventKind::Scroll => detector.on_scroll(
428 if is_vertical {
429 event.scroll_delta.y
430 } else {
431 event.scroll_delta.x
432 },
433 &event,
434 ),
435 PointerEventKind::Zoom
439 | PointerEventKind::RotaryScrollPre
440 | PointerEventKind::RotaryScroll
441 | PointerEventKind::Enter
442 | PointerEventKind::Exit => false,
443 };
444
445 if should_consume {
446 event.consume();
447 }
448 }
449 })
450 .await;
451 }
452 })
453}
454
455impl ScrollTarget for DraggableState {
456 fn apply_delta(&self, delta: f32) -> f32 {
459 self.drag_by(delta);
460 delta
461 }
462
463 fn apply_fling_delta(&self, delta: f32) -> f32 {
464 self.drag_by(delta);
465 delta
466 }
467
468 fn invalidate(&self) {
469 }
471
472 fn current_offset(&self) -> f32 {
473 self.offset()
474 }
475
476 fn allows_fling(&self) -> bool {
478 false
479 }
480
481 fn set_dragging(&self, dragging: bool) {
482 DraggableState::set_dragging(self, dragging);
483 }
484}
485
486const WHEEL_SETTLE_IDLE_NANOS: u64 = 180_000_000;
494
495struct WheelSettleWatcher {
498 is_running: Rc<Cell<bool>>,
499 registration: Rc<RefCell<Option<FrameCallbackRegistration>>>,
500}
501
502impl WheelSettleWatcher {
503 fn cancel(&self) {
504 self.is_running.set(false);
505 self.registration.borrow_mut().take();
506 }
507}
508
509struct ScrollGestureDetector<S: ScrollTarget> {
510 gesture_state: Rc<RefCell<ScrollGestureState>>,
512
513 scroll_target: S,
515
516 is_vertical: bool,
518
519 reverse_scrolling: bool,
521
522 overscroll: crate::scroll::OverscrollEffect,
523
524 motion_context: ScrollMotionContext,
526}
527
528impl<S: ScrollTarget + 'static> ScrollGestureDetector<S> {
529 fn new(
531 gesture_state: Rc<RefCell<ScrollGestureState>>,
532 scroll_target: S,
533 is_vertical: bool,
534 reverse_scrolling: bool,
535 overscroll: crate::scroll::OverscrollEffect,
536 motion_context: ScrollMotionContext,
537 ) -> Self {
538 Self {
539 gesture_state,
540 scroll_target,
541 is_vertical,
542 reverse_scrolling,
543 overscroll,
544 motion_context,
545 }
546 }
547
548 fn on_down(&self, position: Point, time_ms: Option<i64>) -> bool {
557 let mut gs = self.gesture_state.borrow_mut();
558
559 if let Some(fling) = gs.fling_animation.take() {
561 fling.cancel();
562 }
563 if let Some(settle) = gs.settle_animation.take() {
564 settle.cancel();
565 }
566 if let Some(watcher) = gs.wheel_settle_watcher.take() {
567 watcher.cancel();
568 }
569 self.motion_context.set_active(false);
570
571 gs.drag_down_position = Some(position);
572 gs.last_position = Some(position);
573 gs.is_dragging = false;
574 gs.axis_locked_out = false;
575 gs.velocity_tracker.reset();
576 gs.gesture_start_time = Some(Instant::now());
577 gs.gesture_start_event_time_ms = time_ms;
578 gs.is_overscrolling = self.overscroll.offset().abs() > 0.001;
579
580 let pos = if self.is_vertical {
582 position.y
583 } else {
584 position.x
585 };
586 gs.velocity_tracker.add_data_point(0, pos);
587 gs.last_velocity_sample_ms = Some(0);
588
589 false
591 }
592
593 fn on_move(
607 &self,
608 position: Point,
609 buttons: PointerButtons,
610 time_ms: Option<i64>,
611 event: &PointerEvent,
612 ) -> bool {
613 let mut gs = self.gesture_state.borrow_mut();
614
615 if !buttons.contains(PointerButton::Primary) && gs.drag_down_position.is_some() {
617 if gs.is_dragging {
618 self.scroll_target.set_dragging(false);
619 }
620 gs.drag_down_position = None;
621 gs.last_position = None;
622 gs.is_dragging = false;
623 gs.axis_locked_out = false;
624 gs.gesture_start_time = None;
625 gs.gesture_start_event_time_ms = None;
626 gs.last_velocity_sample_ms = None;
627 gs.velocity_tracker.reset();
628 self.motion_context.set_active(false);
629 return false;
630 }
631
632 let Some(down_pos) = gs.drag_down_position else {
633 return false;
634 };
635
636 let Some(last_pos) = gs.last_position else {
637 gs.last_position = Some(position);
638 return false;
639 };
640
641 let incremental_delta = calculate_incremental_delta(last_pos, position, self.is_vertical);
642
643 let mut edge_candidate = false;
653 if !gs.is_dragging && !gs.axis_locked_out {
654 let signed_main_delta = calculate_total_delta(down_pos, position, self.is_vertical);
655 let main_delta = signed_main_delta.abs();
656 let cross_delta = calculate_total_delta(down_pos, position, !self.is_vertical).abs();
657 if main_delta > DRAG_THRESHOLD && main_delta >= cross_delta {
658 if self.scroll_target.can_consume(signed_main_delta) {
661 gs.is_dragging = true;
662 self.scroll_target.set_dragging(true);
663 self.motion_context.set_active(true);
664 } else if self.scroll_target.can_scroll() {
665 edge_candidate = true;
666 }
667 } else if cross_delta > DRAG_THRESHOLD && cross_delta > main_delta {
668 gs.axis_locked_out = true;
669 }
670 }
671
672 gs.last_position = Some(position);
673
674 let pos = if self.is_vertical {
676 position.y
677 } else {
678 position.x
679 };
680 let event_sample_ms = gs
681 .gesture_start_event_time_ms
682 .zip(time_ms)
683 .map(|(start_ms, now_ms)| now_ms - start_ms);
684 let sample_ms = if let Some(event_sample_ms) = event_sample_ms {
685 Some(match gs.last_velocity_sample_ms {
692 Some(last_sample_ms) => event_sample_ms.max(last_sample_ms),
693 None => event_sample_ms.max(0),
694 })
695 } else if let Some(start_time) = gs.gesture_start_time {
696 let elapsed_ms = start_time.elapsed().as_millis() as i64;
699 Some(match gs.last_velocity_sample_ms {
702 Some(last_sample_ms) => {
703 let mut sample_ms = if elapsed_ms <= last_sample_ms {
704 last_sample_ms + 1
705 } else {
706 elapsed_ms
707 };
708 if sample_ms - last_sample_ms > ASSUME_STOPPED_MS {
710 sample_ms = last_sample_ms + ASSUME_STOPPED_MS;
711 }
712 sample_ms
713 }
714 None => elapsed_ms,
715 })
716 } else {
717 None
718 };
719 if let Some(sample_ms) = sample_ms {
720 log::trace!(
721 target: "cranpose::velocity",
722 "sample t={sample_ms}ms pos={pos:.2} event_time={time_ms:?}"
723 );
724 gs.velocity_tracker.add_data_point(sample_ms, pos);
725 gs.last_velocity_sample_ms = Some(sample_ms);
726 }
727
728 if gs.is_dragging {
729 drop(gs); let delta = if self.reverse_scrolling {
731 -incremental_delta
732 } else {
733 incremental_delta
734 };
735 let overscroll = self.overscroll.clone();
736 overscroll.apply_to_scroll(delta, |delta| self.scroll_target.apply_delta(delta));
737 self.scroll_target.invalidate();
738 true } else if gs.is_overscrolling {
740 drop(gs);
741 let delta = if self.reverse_scrolling {
742 -incremental_delta
743 } else {
744 incremental_delta
745 };
746 self.apply_overscroll_delta(delta)
747 } else if edge_candidate {
748 drop(gs);
749 let delta = if self.reverse_scrolling {
750 -incremental_delta
751 } else {
752 incremental_delta
753 };
754 let detector = self.clone_for_watcher();
755 event.defer_post_dispatch_action(move || detector.apply_overscroll_candidate(delta));
756 false
757 } else {
758 false
759 }
760 }
761
762 fn apply_overscroll_delta(&self, delta: f32) -> bool {
763 self.motion_context.set_active(true);
764 let overscroll = self.overscroll.clone();
765 let target_consumed = Cell::new(0.0);
766 let before_overscroll = overscroll.offset();
767 overscroll.apply_to_scroll(delta, |delta| {
768 let consumed = self.scroll_target.apply_delta(delta);
769 target_consumed.set(consumed);
770 consumed
771 });
772 self.scroll_target.invalidate();
773 let consumed = target_consumed.get().abs() > 0.001
774 || (overscroll.offset() - before_overscroll).abs() > 0.001;
775 if !consumed {
776 self.motion_context.set_active(false);
777 }
778 consumed
779 }
780
781 fn apply_overscroll_candidate(&self, delta: f32) -> bool {
782 let consumed = self.apply_overscroll_delta(delta);
783 if consumed {
784 self.gesture_state.borrow_mut().is_overscrolling = true;
785 }
786 consumed
787 }
788
789 fn finish_gesture(&self, allow_fling: bool, release_time_ms: Option<i64>) -> bool {
796 let (was_dragging, gesture_owned, velocity, start_fling, existing_fling) = {
797 let mut gs = self.gesture_state.borrow_mut();
798 let was_dragging = gs.is_dragging;
799 let gesture_owned = was_dragging || gs.is_overscrolling;
800 let mut velocity = 0.0;
801
802 if allow_fling && gesture_owned && gs.gesture_start_time.is_some() {
803 let release_sample_ms = release_time_ms
808 .zip(gs.gesture_start_event_time_ms)
809 .map(|(release_ms, start_ms)| release_ms - start_ms)
810 .or_else(|| {
811 gs.gesture_start_time
812 .map(|start| start.elapsed().as_millis() as i64)
813 });
814 let rested_before_release = release_sample_ms
815 .zip(gs.last_velocity_sample_ms)
816 .is_some_and(|(release_ms, last_sample_ms)| {
817 release_ms - last_sample_ms > ASSUME_STOPPED_MS
818 });
819 if !rested_before_release {
820 velocity = gs
821 .velocity_tracker
822 .calculate_velocity_with_max(MAX_FLING_VELOCITY);
823 }
824 }
825
826 let start_fling = allow_fling && was_dragging && velocity.abs() > MIN_FLING_VELOCITY;
827 let existing_fling = if start_fling {
828 gs.fling_animation.take()
829 } else {
830 None
831 };
832
833 if was_dragging {
834 self.scroll_target.set_dragging(false);
835 }
836 gs.drag_down_position = None;
837 gs.last_position = None;
838 gs.is_dragging = false;
839 gs.is_overscrolling = false;
840 gs.axis_locked_out = false;
841 gs.gesture_start_time = None;
842 gs.gesture_start_event_time_ms = None;
843 gs.last_velocity_sample_ms = None;
844
845 (
846 was_dragging,
847 gesture_owned,
848 velocity,
849 start_fling,
850 existing_fling,
851 )
852 };
853
854 if allow_fling && gesture_owned {
856 log::debug!(
857 target: "cranpose::velocity",
858 "gesture finished: fling velocity={velocity:.2} dp/s start_fling={start_fling}"
859 );
860 set_last_fling_velocity(velocity);
861 }
862
863 let adjusted_velocity = if self.reverse_scrolling {
865 -velocity
866 } else {
867 velocity
868 };
869 let fling_velocity = -adjusted_velocity;
870 let has_overscroll = self.overscroll.offset().abs() > 0.001;
871
872 let settle_target = if was_dragging {
877 self.scroll_target.settle_policy().and_then(|policy| {
878 let current = self.scroll_target.current_offset();
879 let proposed = if start_fling {
880 fling_rest_position(current, fling_velocity)
881 } else {
882 current
883 };
884 let target = policy(proposed, fling_velocity);
885 ((target - proposed).abs() > 0.5).then_some(target)
886 })
887 } else {
888 None
889 };
890
891 if has_overscroll {
892 if let Some(old_fling) = existing_fling {
893 old_fling.cancel();
894 }
895 self.start_overscroll_settle(-fling_velocity);
896 } else if let Some(target) = settle_target {
897 if let Some(old_fling) = existing_fling {
898 old_fling.cancel();
899 }
900 self.start_settle_animation(target, fling_velocity);
901 } else if start_fling {
902 if let Some(old_fling) = existing_fling {
903 old_fling.cancel();
904 }
905 self.start_fling_animation(fling_velocity);
906 } else {
907 self.motion_context.set_active(false);
908 }
909
910 gesture_owned
911 }
912
913 fn start_fling_animation(&self, fling_velocity: f32) {
914 let Some(runtime) = current_runtime_handle() else {
915 self.motion_context.set_active(false);
916 return;
917 };
918 self.motion_context.set_active(true);
919 let scroll_target = self.scroll_target.clone();
920 let fling = FlingAnimation::new(runtime);
921 let motion_context = self.motion_context.clone();
922 let initial_value = scroll_target.current_offset();
923 let scroll_target_for_fling = scroll_target.clone();
924 let scroll_target_for_end = scroll_target.clone();
925 let detector_for_end = self.clone_for_watcher();
926 let overscroll_for_fling = self.overscroll.clone();
927
928 fling.start_fling(
929 initial_value,
930 fling_velocity,
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, SpringParams::SETTLE_POLICY);
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, SpringParams::OVERSCROLL_BOUNCE);
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_dimension(info.viewport_size);
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}