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