1use std::{
27 cell::{Cell, RefCell},
28 rc::Rc,
29};
30
31use cranpose_core::{current_runtime_handle, internal::FrameCallbackRegistration, NodeId};
32use cranpose_foundation::{
33 velocity_tracker::ASSUME_STOPPED_MS, DelegatableNode, ModifierNode, ModifierNodeElement,
34 NodeCapabilities, NodeState, PointerButton, PointerButtons, VelocityTracker1D, DRAG_THRESHOLD,
35 MAX_FLING_VELOCITY,
36};
37use cranpose_ui_layout::Axis;
38use web_time::Instant;
39
40use super::{inspector_metadata, Modifier, Point, PointerEvent, PointerEventKind};
41use crate::{
42 current_density,
43 draggable::DraggableState,
44 fling_animation::{fling_rest_position, FlingAnimation, SettleAnimation, MIN_FLING_VELOCITY},
45 render_state::schedule_modifier_slices_repass,
46 scroll::{
47 scroll_motion_context_for_key, ScrollElement, ScrollMotionContext, ScrollMotionContextKey,
48 ScrollSettlePolicy, ScrollState,
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 if !guard() {
406 if matches!(
407 event.kind,
408 PointerEventKind::Up | PointerEventKind::Cancel
409 ) {
410 detector.on_cancel();
411 }
412 continue;
413 }
414 }
415
416 let should_consume = match event.kind {
417 PointerEventKind::Down => {
418 detector.on_down(event.position, event.time_ms)
419 }
420 PointerEventKind::Move => detector.on_move(
421 event.position,
422 event.buttons,
423 event.time_ms,
424 &event,
425 ),
426 PointerEventKind::Up => detector.on_up(event.time_ms),
427 PointerEventKind::Cancel => detector.on_cancel(),
428 PointerEventKind::Scroll => detector.on_scroll(
429 if is_vertical {
430 event.scroll_delta.y
431 } else {
432 event.scroll_delta.x
433 },
434 &event,
435 ),
436 PointerEventKind::Zoom
440 | PointerEventKind::RotaryScrollPre
441 | PointerEventKind::RotaryScroll
442 | PointerEventKind::Enter
443 | PointerEventKind::Exit => false,
444 };
445
446 if should_consume {
447 event.consume();
448 }
449 }
450 })
451 .await;
452 }
453 })
454}
455
456impl ScrollTarget for DraggableState {
457 fn apply_delta(&self, delta: f32) -> f32 {
460 self.drag_by(delta);
461 delta
462 }
463
464 fn apply_fling_delta(&self, delta: f32) -> f32 {
465 self.drag_by(delta);
466 delta
467 }
468
469 fn invalidate(&self) {
470 }
472
473 fn current_offset(&self) -> f32 {
474 self.offset()
475 }
476
477 fn allows_fling(&self) -> bool {
479 false
480 }
481
482 fn set_dragging(&self, dragging: bool) {
483 DraggableState::set_dragging(self, dragging);
484 }
485}
486
487const WHEEL_SETTLE_IDLE_NANOS: u64 = 180_000_000;
495
496struct WheelSettleWatcher {
499 is_running: Rc<Cell<bool>>,
500 registration: Rc<RefCell<Option<FrameCallbackRegistration>>>,
501}
502
503impl WheelSettleWatcher {
504 fn cancel(&self) {
505 self.is_running.set(false);
506 self.registration.borrow_mut().take();
507 }
508}
509
510struct ScrollGestureDetector<S: ScrollTarget> {
511 gesture_state: Rc<RefCell<ScrollGestureState>>,
513
514 scroll_target: S,
516
517 is_vertical: bool,
519
520 reverse_scrolling: bool,
522
523 overscroll: crate::scroll::OverscrollEffect,
524
525 motion_context: ScrollMotionContext,
527}
528
529impl<S: ScrollTarget + 'static> ScrollGestureDetector<S> {
530 fn new(
532 gesture_state: Rc<RefCell<ScrollGestureState>>,
533 scroll_target: S,
534 is_vertical: bool,
535 reverse_scrolling: bool,
536 overscroll: crate::scroll::OverscrollEffect,
537 motion_context: ScrollMotionContext,
538 ) -> Self {
539 Self {
540 gesture_state,
541 scroll_target,
542 is_vertical,
543 reverse_scrolling,
544 overscroll,
545 motion_context,
546 }
547 }
548
549 fn on_down(&self, position: Point, time_ms: Option<i64>) -> bool {
558 let mut gs = self.gesture_state.borrow_mut();
559
560 if let Some(fling) = gs.fling_animation.take() {
562 fling.cancel();
563 }
564 if let Some(settle) = gs.settle_animation.take() {
565 settle.cancel();
566 }
567 if let Some(watcher) = gs.wheel_settle_watcher.take() {
568 watcher.cancel();
569 }
570 self.motion_context.set_active(false);
571
572 gs.drag_down_position = Some(position);
573 gs.last_position = Some(position);
574 gs.is_dragging = false;
575 gs.axis_locked_out = false;
576 gs.velocity_tracker.reset();
577 gs.gesture_start_time = Some(Instant::now());
578 gs.gesture_start_event_time_ms = time_ms;
579 gs.is_overscrolling = self.overscroll.offset().abs() > 0.001;
580
581 let pos = if self.is_vertical {
583 position.y
584 } else {
585 position.x
586 };
587 gs.velocity_tracker.add_data_point(0, pos);
588 gs.last_velocity_sample_ms = Some(0);
589
590 false
592 }
593
594 fn on_move(
608 &self,
609 position: Point,
610 buttons: PointerButtons,
611 time_ms: Option<i64>,
612 event: &PointerEvent,
613 ) -> bool {
614 let mut gs = self.gesture_state.borrow_mut();
615
616 if !buttons.contains(PointerButton::Primary) && gs.drag_down_position.is_some() {
618 if gs.is_dragging {
619 self.scroll_target.set_dragging(false);
620 }
621 gs.drag_down_position = None;
622 gs.last_position = None;
623 gs.is_dragging = false;
624 gs.axis_locked_out = false;
625 gs.gesture_start_time = None;
626 gs.gesture_start_event_time_ms = None;
627 gs.last_velocity_sample_ms = None;
628 gs.velocity_tracker.reset();
629 self.motion_context.set_active(false);
630 return false;
631 }
632
633 let Some(down_pos) = gs.drag_down_position else {
634 return false;
635 };
636
637 let Some(last_pos) = gs.last_position else {
638 gs.last_position = Some(position);
639 return false;
640 };
641
642 let incremental_delta = calculate_incremental_delta(last_pos, position, self.is_vertical);
643
644 let mut edge_candidate = false;
654 if !gs.is_dragging && !gs.axis_locked_out {
655 let signed_main_delta = calculate_total_delta(down_pos, position, self.is_vertical);
656 let main_delta = signed_main_delta.abs();
657 let cross_delta = calculate_total_delta(down_pos, position, !self.is_vertical).abs();
658 if main_delta > DRAG_THRESHOLD && main_delta >= cross_delta {
659 if self.scroll_target.can_consume(signed_main_delta) {
662 gs.is_dragging = true;
663 self.scroll_target.set_dragging(true);
664 self.motion_context.set_active(true);
665 } else if self.scroll_target.can_scroll() {
666 edge_candidate = true;
667 }
668 } else if cross_delta > DRAG_THRESHOLD && cross_delta > main_delta {
669 gs.axis_locked_out = true;
670 }
671 }
672
673 gs.last_position = Some(position);
674
675 let pos = if self.is_vertical {
677 position.y
678 } else {
679 position.x
680 };
681 let event_sample_ms = gs
682 .gesture_start_event_time_ms
683 .zip(time_ms)
684 .map(|(start_ms, now_ms)| now_ms - start_ms);
685 let sample_ms = if let Some(event_sample_ms) = event_sample_ms {
686 Some(match gs.last_velocity_sample_ms {
693 Some(last_sample_ms) => event_sample_ms.max(last_sample_ms),
694 None => event_sample_ms.max(0),
695 })
696 } else if let Some(start_time) = gs.gesture_start_time {
697 let elapsed_ms = start_time.elapsed().as_millis() as i64;
700 Some(match gs.last_velocity_sample_ms {
703 Some(last_sample_ms) => {
704 let mut sample_ms = if elapsed_ms <= last_sample_ms {
705 last_sample_ms + 1
706 } else {
707 elapsed_ms
708 };
709 if sample_ms - last_sample_ms > ASSUME_STOPPED_MS {
711 sample_ms = last_sample_ms + ASSUME_STOPPED_MS;
712 }
713 sample_ms
714 }
715 None => elapsed_ms,
716 })
717 } else {
718 None
719 };
720 if let Some(sample_ms) = sample_ms {
721 log::trace!(
722 target: "cranpose::velocity",
723 "sample t={sample_ms}ms pos={pos:.2} event_time={time_ms:?}"
724 );
725 gs.velocity_tracker.add_data_point(sample_ms, pos);
726 gs.last_velocity_sample_ms = Some(sample_ms);
727 }
728
729 if gs.is_dragging {
730 drop(gs); let delta = if self.reverse_scrolling {
732 -incremental_delta
733 } else {
734 incremental_delta
735 };
736 let overscroll = self.overscroll.clone();
737 overscroll.apply_to_scroll(delta, |delta| self.scroll_target.apply_delta(delta));
738 self.scroll_target.invalidate();
739 true } else if gs.is_overscrolling {
741 drop(gs);
742 let delta = if self.reverse_scrolling {
743 -incremental_delta
744 } else {
745 incremental_delta
746 };
747 self.apply_overscroll_delta(delta)
748 } else if edge_candidate {
749 drop(gs);
750 let delta = if self.reverse_scrolling {
751 -incremental_delta
752 } else {
753 incremental_delta
754 };
755 let detector = self.clone_for_watcher();
756 event.defer_post_dispatch_action(move || detector.apply_overscroll_candidate(delta));
757 false
758 } else {
759 false
760 }
761 }
762
763 fn apply_overscroll_delta(&self, delta: f32) -> bool {
764 self.motion_context.set_active(true);
765 let overscroll = self.overscroll.clone();
766 let target_consumed = Cell::new(0.0);
767 let before_overscroll = overscroll.offset();
768 overscroll.apply_to_scroll(delta, |delta| {
769 let consumed = self.scroll_target.apply_delta(delta);
770 target_consumed.set(consumed);
771 consumed
772 });
773 self.scroll_target.invalidate();
774 let consumed = target_consumed.get().abs() > 0.001
775 || (overscroll.offset() - before_overscroll).abs() > 0.001;
776 if !consumed {
777 self.motion_context.set_active(false);
778 }
779 consumed
780 }
781
782 fn apply_overscroll_candidate(&self, delta: f32) -> bool {
783 let consumed = self.apply_overscroll_delta(delta);
784 if consumed {
785 self.gesture_state.borrow_mut().is_overscrolling = true;
786 }
787 consumed
788 }
789
790 fn finish_gesture(&self, allow_fling: bool, release_time_ms: Option<i64>) -> bool {
797 let (was_dragging, gesture_owned, velocity, start_fling, existing_fling) = {
798 let mut gs = self.gesture_state.borrow_mut();
799 let was_dragging = gs.is_dragging;
800 let gesture_owned = was_dragging || gs.is_overscrolling;
801 let mut velocity = 0.0;
802
803 if allow_fling && gesture_owned && gs.gesture_start_time.is_some() {
804 let release_sample_ms = release_time_ms
809 .zip(gs.gesture_start_event_time_ms)
810 .map(|(release_ms, start_ms)| release_ms - start_ms)
811 .or_else(|| {
812 gs.gesture_start_time
813 .map(|start| start.elapsed().as_millis() as i64)
814 });
815 let rested_before_release = release_sample_ms
816 .zip(gs.last_velocity_sample_ms)
817 .is_some_and(|(release_ms, last_sample_ms)| {
818 release_ms - last_sample_ms > ASSUME_STOPPED_MS
819 });
820 if !rested_before_release {
821 velocity = gs
822 .velocity_tracker
823 .calculate_velocity_with_max(MAX_FLING_VELOCITY);
824 }
825 }
826
827 let start_fling = allow_fling && was_dragging && velocity.abs() > MIN_FLING_VELOCITY;
828 let existing_fling = if start_fling {
829 gs.fling_animation.take()
830 } else {
831 None
832 };
833
834 if was_dragging {
835 self.scroll_target.set_dragging(false);
836 }
837 gs.drag_down_position = None;
838 gs.last_position = None;
839 gs.is_dragging = false;
840 gs.is_overscrolling = false;
841 gs.axis_locked_out = false;
842 gs.gesture_start_time = None;
843 gs.gesture_start_event_time_ms = None;
844 gs.last_velocity_sample_ms = None;
845
846 (
847 was_dragging,
848 gesture_owned,
849 velocity,
850 start_fling,
851 existing_fling,
852 )
853 };
854
855 if allow_fling && gesture_owned {
857 log::debug!(
858 target: "cranpose::velocity",
859 "gesture finished: fling velocity={velocity:.2} dp/s start_fling={start_fling}"
860 );
861 set_last_fling_velocity(velocity);
862 }
863
864 let adjusted_velocity = if self.reverse_scrolling {
866 -velocity
867 } else {
868 velocity
869 };
870 let fling_velocity = -adjusted_velocity;
871 let has_overscroll = self.overscroll.offset().abs() > 0.001;
872
873 let settle_target = if was_dragging {
878 self.scroll_target.settle_policy().and_then(|policy| {
879 let current = self.scroll_target.current_offset();
880 let proposed = if start_fling {
881 fling_rest_position(current, fling_velocity, current_density())
882 } else {
883 current
884 };
885 let target = policy(proposed, fling_velocity);
886 ((target - proposed).abs() > 0.5).then_some(target)
887 })
888 } else {
889 None
890 };
891
892 if has_overscroll {
893 if let Some(old_fling) = existing_fling {
894 old_fling.cancel();
895 }
896 self.start_overscroll_settle(-fling_velocity);
897 } else if let Some(target) = settle_target {
898 if let Some(old_fling) = existing_fling {
899 old_fling.cancel();
900 }
901 self.start_settle_animation(target, fling_velocity);
902 } else if start_fling {
903 if let Some(old_fling) = existing_fling {
904 old_fling.cancel();
905 }
906 self.start_fling_animation(fling_velocity);
907 } else {
908 self.motion_context.set_active(false);
909 }
910
911 gesture_owned
912 }
913
914 fn start_fling_animation(&self, fling_velocity: f32) {
915 let Some(runtime) = current_runtime_handle() else {
916 self.motion_context.set_active(false);
917 return;
918 };
919 self.motion_context.set_active(true);
920 let scroll_target = self.scroll_target.clone();
921 let fling = FlingAnimation::new(runtime);
922 let motion_context = self.motion_context.clone();
923 let initial_value = scroll_target.current_offset();
924 let scroll_target_for_fling = scroll_target.clone();
925 let scroll_target_for_end = scroll_target.clone();
926 let detector_for_end = self.clone_for_watcher();
927 let overscroll_for_fling = self.overscroll.clone();
928
929 fling.start_fling(
930 initial_value,
931 fling_velocity,
932 current_density(),
933 move |delta| {
934 let consumed = overscroll_for_fling.apply_to_fling(delta, |delta| {
935 scroll_target_for_fling.apply_fling_delta(delta)
936 });
937 scroll_target_for_fling.invalidate();
938 consumed
939 },
940 move || {
941 scroll_target_for_end.invalidate();
942 let settle_running = detector_for_end
943 .gesture_state
944 .borrow()
945 .settle_animation
946 .as_ref()
947 .is_some_and(SettleAnimation::is_running);
948 if detector_for_end.overscroll.offset().abs() > 0.001 && !settle_running {
949 detector_for_end.start_overscroll_settle(0.0);
950 }
951 detector_for_end.update_motion_active(&motion_context);
952 },
953 );
954
955 let mut gs = self.gesture_state.borrow_mut();
956 gs.fling_animation = Some(fling);
957 }
958
959 fn start_settle_animation(&self, target: f32, initial_velocity: f32) {
962 let Some(runtime) = current_runtime_handle() else {
963 self.motion_context.set_active(false);
964 return;
965 };
966 self.motion_context.set_active(true);
967 let settle = SettleAnimation::new(runtime);
968 let scroll_target_for_settle = self.scroll_target.clone();
969 let scroll_target_for_end = self.scroll_target.clone();
970 let detector_for_end = self.clone_for_watcher();
971 let motion_context = self.motion_context.clone();
972 settle.start_settle(
973 self.scroll_target.current_offset(),
974 initial_velocity,
975 target,
976 move |delta| {
977 let consumed = scroll_target_for_settle.apply_fling_delta(delta);
978 scroll_target_for_settle.invalidate();
979 consumed
980 },
981 move |_| {
982 scroll_target_for_end.invalidate();
983 detector_for_end.update_motion_active(&motion_context);
984 },
985 );
986 let mut gs = self.gesture_state.borrow_mut();
987 gs.settle_animation = Some(settle);
988 }
989
990 fn start_overscroll_settle(&self, initial_velocity: f32) {
991 let Some(runtime) = current_runtime_handle() else {
992 let current = self.overscroll.offset();
993 self.overscroll.apply_settle_delta(-current);
994 self.motion_context.set_active(false);
995 return;
996 };
997 let settle = SettleAnimation::new(runtime);
998 let overscroll_for_settle = self.overscroll.clone();
999 let overscroll_for_end = self.overscroll.clone();
1000 let detector_for_end = self.clone_for_watcher();
1001 let motion_context = self.motion_context.clone();
1002 let initial = self.overscroll.offset();
1003 settle.start_settle(
1004 initial,
1005 initial_velocity,
1006 0.0,
1007 move |delta| overscroll_for_settle.apply_settle_delta(delta),
1008 move |end| {
1009 let current = overscroll_for_end.offset();
1010 overscroll_for_end.apply_settle_delta(-current);
1011 if end.hit_boundary && end.velocity.abs() > MIN_FLING_VELOCITY {
1012 detector_for_end.start_fling_animation(-end.velocity);
1013 } else {
1014 detector_for_end.update_motion_active(&motion_context);
1015 }
1016 },
1017 );
1018 let mut gs = self.gesture_state.borrow_mut();
1019 gs.is_overscrolling = false;
1020 gs.settle_animation = Some(settle);
1021 }
1022
1023 fn update_motion_active(&self, motion_context: &ScrollMotionContext) {
1024 let running = {
1025 let gs = self.gesture_state.borrow();
1026 gs.fling_animation
1027 .as_ref()
1028 .is_some_and(FlingAnimation::is_running)
1029 || gs
1030 .settle_animation
1031 .as_ref()
1032 .is_some_and(SettleAnimation::is_running)
1033 };
1034 if !running {
1035 motion_context.set_active(false);
1036 }
1037 }
1038
1039 fn on_up(&self, time_ms: Option<i64>) -> bool {
1046 self.finish_gesture(self.scroll_target.allows_fling(), time_ms)
1047 }
1048
1049 fn on_cancel(&self) -> bool {
1053 self.finish_gesture(false, None)
1054 }
1055
1056 fn on_scroll(&self, axis_delta: f32, event: &PointerEvent) -> bool {
1060 if axis_delta.abs() <= f32::EPSILON {
1061 return false;
1062 }
1063
1064 let delta = if self.reverse_scrolling {
1065 -axis_delta
1066 } else {
1067 axis_delta
1068 };
1069 if !self.scroll_target.can_consume(delta) && self.scroll_target.can_scroll() {
1070 let detector = self.clone_for_watcher();
1071 event.defer_post_dispatch_action(move || detector.apply_wheel_delta(delta));
1072 return false;
1073 }
1074 self.apply_wheel_delta(delta)
1075 }
1076
1077 fn apply_wheel_delta(&self, delta: f32) -> bool {
1078 {
1079 let mut gs = self.gesture_state.borrow_mut();
1080 if let Some(fling) = gs.fling_animation.take() {
1081 fling.cancel();
1082 }
1083 if let Some(settle) = gs.settle_animation.take() {
1084 settle.cancel();
1085 }
1086 gs.drag_down_position = None;
1087 gs.last_position = None;
1088 gs.is_dragging = false;
1089 gs.axis_locked_out = false;
1090 gs.gesture_start_time = None;
1091 gs.gesture_start_event_time_ms = None;
1092 gs.last_velocity_sample_ms = None;
1093 gs.velocity_tracker.reset();
1094 }
1095
1096 self.motion_context.activate_for_current_frame();
1097 let overscroll = self.overscroll.clone();
1098 let consumed =
1099 overscroll.apply_to_scroll(delta, |delta| self.scroll_target.apply_wheel_delta(delta));
1100 let overscroll_active = self.overscroll.offset().abs() > 0.001;
1101 if consumed.abs() > 0.001 {
1102 self.scroll_target.invalidate();
1103 }
1104 if consumed.abs() > 0.001 || overscroll_active {
1105 self.ensure_wheel_settle_watcher();
1106 true
1107 } else {
1108 false
1109 }
1110 }
1111
1112 fn ensure_wheel_settle_watcher(&self) {
1117 if self.scroll_target.settle_policy().is_none() && self.overscroll.offset().abs() <= 0.001 {
1118 return;
1119 }
1120 {
1121 let gs = self.gesture_state.borrow();
1122 if gs
1123 .wheel_settle_watcher
1124 .as_ref()
1125 .is_some_and(|watcher| watcher.is_running.get())
1126 {
1127 return;
1128 }
1129 }
1130 let Some(runtime) = current_runtime_handle() else {
1131 return;
1132 };
1133
1134 let is_running = Rc::new(Cell::new(true));
1135 let registration = Rc::new(RefCell::new(None));
1136
1137 struct WheelSettleLoop<S: ScrollTarget> {
1138 detector: ScrollGestureDetector<S>,
1139 gesture_state: Rc<RefCell<ScrollGestureState>>,
1140 frame_clock: cranpose_core::internal::FrameClock,
1141 is_running: Rc<Cell<bool>>,
1142 registration: Rc<RefCell<Option<FrameCallbackRegistration>>>,
1143 last_offset: Rc<Cell<f32>>,
1144 idle_nanos: Rc<Cell<u64>>,
1145 last_frame: Rc<Cell<Option<u64>>>,
1146 }
1147
1148 impl<S: ScrollTarget + 'static> WheelSettleLoop<S> {
1149 fn next(&self) -> Self {
1150 Self {
1151 detector: self.detector.clone_for_watcher(),
1152 gesture_state: Rc::clone(&self.gesture_state),
1153 frame_clock: self.frame_clock.clone(),
1154 is_running: Rc::clone(&self.is_running),
1155 registration: Rc::clone(&self.registration),
1156 last_offset: Rc::clone(&self.last_offset),
1157 idle_nanos: Rc::clone(&self.idle_nanos),
1158 last_frame: Rc::clone(&self.last_frame),
1159 }
1160 }
1161
1162 fn schedule(self) {
1163 let continuation = self.next();
1164 let registration_slot = Rc::clone(&self.registration);
1165 let new_registration = self.frame_clock.with_frame_nanos(move |frame_time_nanos| {
1166 let this = &continuation;
1167 if !this.is_running.get() {
1168 return;
1169 }
1170 {
1172 let gs = this.gesture_state.borrow();
1173 let animating = gs.is_dragging
1174 || gs
1175 .fling_animation
1176 .as_ref()
1177 .is_some_and(FlingAnimation::is_running)
1178 || gs
1179 .settle_animation
1180 .as_ref()
1181 .is_some_and(SettleAnimation::is_running);
1182 if animating {
1183 this.is_running.set(false);
1184 return;
1185 }
1186 }
1187
1188 let offset = this.detector.scroll_target.current_offset();
1189 let dt = this
1190 .last_frame
1191 .get()
1192 .map_or(0, |last| frame_time_nanos.saturating_sub(last));
1193 this.last_frame.set(Some(frame_time_nanos));
1194 if (offset - this.last_offset.get()).abs() > 0.01 {
1195 this.last_offset.set(offset);
1196 this.idle_nanos.set(0);
1197 } else {
1198 this.idle_nanos.set(this.idle_nanos.get() + dt);
1199 }
1200
1201 if this.idle_nanos.get() >= WHEEL_SETTLE_IDLE_NANOS {
1202 this.is_running.set(false);
1203 if this.detector.overscroll.offset().abs() > 0.001 {
1204 this.detector.start_overscroll_settle(0.0);
1205 return;
1206 }
1207 if let Some(policy) = this.detector.scroll_target.settle_policy() {
1208 let target = policy(offset, 0.0);
1209 if (target - offset).abs() > 0.5 {
1210 this.detector.start_settle_animation(target, 0.0);
1211 }
1212 }
1213 return;
1214 }
1215
1216 continuation.next().schedule();
1217 });
1218 *registration_slot.borrow_mut() = Some(new_registration);
1219 }
1220 }
1221
1222 WheelSettleLoop {
1223 detector: self.clone_for_watcher(),
1224 gesture_state: Rc::clone(&self.gesture_state),
1225 frame_clock: runtime.frame_clock(),
1226 is_running: Rc::clone(&is_running),
1227 registration: Rc::clone(®istration),
1228 last_offset: Rc::new(Cell::new(self.scroll_target.current_offset())),
1229 idle_nanos: Rc::new(Cell::new(0u64)),
1230 last_frame: Rc::new(Cell::new(None::<u64>)),
1231 }
1232 .schedule();
1233
1234 self.gesture_state.borrow_mut().wheel_settle_watcher = Some(WheelSettleWatcher {
1235 is_running,
1236 registration,
1237 });
1238 }
1239
1240 fn clone_for_watcher(&self) -> ScrollGestureDetector<S> {
1241 ScrollGestureDetector {
1242 gesture_state: Rc::clone(&self.gesture_state),
1243 scroll_target: self.scroll_target.clone(),
1244 is_vertical: self.is_vertical,
1245 reverse_scrolling: self.reverse_scrolling,
1246 overscroll: self.overscroll.clone(),
1247 motion_context: self.motion_context.clone(),
1248 }
1249 }
1250}
1251
1252pub(crate) struct MotionContextAnimatedNode {
1253 state: NodeState,
1254 motion_context: ScrollMotionContext,
1255 invalidation_callback_id: Option<u64>,
1256 overscroll_callback_id: Option<u64>,
1257 node_id: Option<NodeId>,
1258}
1259
1260impl MotionContextAnimatedNode {
1261 fn new(motion_context: ScrollMotionContext) -> Self {
1262 Self {
1263 state: NodeState::new(),
1264 motion_context,
1265 invalidation_callback_id: None,
1266 overscroll_callback_id: None,
1267 node_id: None,
1268 }
1269 }
1270
1271 pub(crate) fn is_active(&self) -> bool {
1272 self.motion_context.is_active()
1273 }
1274}
1275
1276pub(crate) struct TranslatedContentContextNode {
1277 state: NodeState,
1278 identity: usize,
1279 offset_source: TranslatedContentOffsetSource,
1280 overscroll: crate::scroll::OverscrollEffect,
1281 overscroll_callback_id: Option<u64>,
1282}
1283
1284impl TranslatedContentContextNode {
1285 fn new(
1286 identity: usize,
1287 offset_source: TranslatedContentOffsetSource,
1288 overscroll: crate::scroll::OverscrollEffect,
1289 ) -> Self {
1290 Self {
1291 state: NodeState::new(),
1292 identity,
1293 offset_source,
1294 overscroll,
1295 overscroll_callback_id: None,
1296 }
1297 }
1298
1299 pub(crate) fn is_active(&self) -> bool {
1300 true
1301 }
1302
1303 pub(crate) fn identity(&self) -> usize {
1304 self.identity
1305 }
1306
1307 pub(crate) fn content_offset_reader(&self) -> Option<Rc<dyn Fn() -> Point>> {
1308 self.offset_source
1309 .content_offset_reader(self.overscroll.clone())
1310 }
1311}
1312
1313impl DelegatableNode for TranslatedContentContextNode {
1314 fn node_state(&self) -> &NodeState {
1315 &self.state
1316 }
1317}
1318
1319impl ModifierNode for TranslatedContentContextNode {
1320 fn on_attach(&mut self, context: &mut dyn cranpose_foundation::ModifierNodeContext) {
1321 if let Some(node_id) = context.node_id() {
1322 self.overscroll_callback_id =
1323 Some(self.overscroll.add_invalidate_callback(Box::new(move || {
1324 schedule_modifier_slices_repass(node_id)
1325 })));
1326 }
1327 }
1328
1329 fn on_detach(&mut self) {
1330 if let Some(id) = self.overscroll_callback_id.take() {
1331 self.overscroll.remove_invalidate_callback(id);
1332 }
1333 }
1334}
1335
1336impl DelegatableNode for MotionContextAnimatedNode {
1337 fn node_state(&self) -> &NodeState {
1338 &self.state
1339 }
1340}
1341
1342impl ModifierNode for MotionContextAnimatedNode {
1343 fn on_attach(&mut self, context: &mut dyn cranpose_foundation::ModifierNodeContext) {
1344 let node_id = context.node_id();
1345 self.node_id = node_id;
1346 if let Some(node_id) = node_id {
1347 let callback_id = self
1348 .motion_context
1349 .add_invalidate_callback(Box::new(move || {
1350 schedule_modifier_slices_repass(node_id);
1351 }));
1352 self.invalidation_callback_id = Some(callback_id);
1353 let callback_id = self
1354 .motion_context
1355 .overscroll()
1356 .add_invalidate_callback(Box::new(move || {
1357 crate::schedule_measure_repass(node_id);
1358 schedule_modifier_slices_repass(node_id);
1359 }));
1360 self.overscroll_callback_id = Some(callback_id);
1361 }
1362 }
1363
1364 fn on_detach(&mut self) {
1365 if let Some(id) = self.invalidation_callback_id.take() {
1366 self.motion_context.remove_invalidate_callback(id);
1367 }
1368 if let Some(id) = self.overscroll_callback_id.take() {
1369 self.motion_context
1370 .overscroll()
1371 .remove_invalidate_callback(id);
1372 }
1373 self.node_id = None;
1374 }
1375}
1376
1377#[derive(Clone)]
1378struct MotionContextAnimatedElement {
1379 motion_context: ScrollMotionContext,
1380}
1381
1382impl MotionContextAnimatedElement {
1383 fn new(motion_context: ScrollMotionContext) -> Self {
1384 Self { motion_context }
1385 }
1386}
1387
1388impl std::fmt::Debug for MotionContextAnimatedElement {
1389 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1390 f.debug_struct("MotionContextAnimatedElement").finish()
1391 }
1392}
1393
1394impl PartialEq for MotionContextAnimatedElement {
1395 fn eq(&self, other: &Self) -> bool {
1396 self.motion_context.ptr_eq(&other.motion_context)
1397 }
1398}
1399
1400impl Eq for MotionContextAnimatedElement {}
1401
1402impl std::hash::Hash for MotionContextAnimatedElement {
1403 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1404 self.motion_context.stable_key().hash(state);
1405 }
1406}
1407
1408impl ModifierNodeElement for MotionContextAnimatedElement {
1409 type Node = MotionContextAnimatedNode;
1410
1411 fn create(&self) -> Self::Node {
1412 MotionContextAnimatedNode::new(self.motion_context.clone())
1413 }
1414
1415 fn update(&self, node: &mut Self::Node) {
1416 if node.motion_context.ptr_eq(&self.motion_context) {
1417 return;
1418 }
1419 if let Some(id) = node.invalidation_callback_id.take() {
1420 node.motion_context.remove_invalidate_callback(id);
1421 }
1422 node.motion_context = self.motion_context.clone();
1423 if let Some(node_id) = node.node_id {
1424 let callback_id = node
1425 .motion_context
1426 .add_invalidate_callback(Box::new(move || {
1427 schedule_modifier_slices_repass(node_id);
1428 }));
1429 node.invalidation_callback_id = Some(callback_id);
1430 }
1431 }
1432
1433 fn capabilities(&self) -> NodeCapabilities {
1434 NodeCapabilities::LAYOUT
1435 }
1436}
1437
1438#[derive(Clone)]
1439enum TranslatedContentOffsetSource {
1440 LayoutContentOffset,
1441 LazyList {
1442 state: LazyListState,
1443 is_vertical: bool,
1444 reverse_scrolling: bool,
1445 },
1446}
1447
1448impl TranslatedContentOffsetSource {
1449 fn content_offset_reader(
1450 &self,
1451 overscroll: crate::scroll::OverscrollEffect,
1452 ) -> Option<Rc<dyn Fn() -> Point>> {
1453 match self {
1454 Self::LayoutContentOffset => None,
1455 Self::LazyList {
1456 state, is_vertical, ..
1457 } => Some(Rc::new(lazy_list_content_offset_reader(
1458 *state,
1459 *is_vertical,
1460 overscroll,
1461 ))),
1462 }
1463 }
1464
1465 fn is_vertical(&self) -> Option<bool> {
1466 match self {
1467 Self::LayoutContentOffset => None,
1468 Self::LazyList { is_vertical, .. } => Some(*is_vertical),
1469 }
1470 }
1471
1472 fn reverse_scrolling(&self) -> Option<bool> {
1473 match self {
1474 Self::LayoutContentOffset => None,
1475 Self::LazyList {
1476 reverse_scrolling, ..
1477 } => Some(*reverse_scrolling),
1478 }
1479 }
1480}
1481
1482fn lazy_list_content_offset_reader(
1483 state: LazyListState,
1484 is_vertical: bool,
1485 overscroll: crate::scroll::OverscrollEffect,
1486) -> impl Fn() -> Point {
1487 move || {
1488 let info = state.layout_info();
1489 if info.visible_items_info.is_empty() {
1490 return Point::default();
1491 };
1492 overscroll.set_limit(info.viewport_size * 0.5);
1493 let main_offset = info.snap_anchor_offset;
1494 let bounce = overscroll.offset();
1495 if is_vertical {
1496 Point::new(0.0, main_offset + bounce)
1497 } else {
1498 Point::new(main_offset + bounce, 0.0)
1499 }
1500 }
1501}
1502
1503#[derive(Clone)]
1504struct TranslatedContentContextElement {
1505 identity: usize,
1506 offset_source: TranslatedContentOffsetSource,
1507 overscroll: crate::scroll::OverscrollEffect,
1508}
1509
1510impl TranslatedContentContextElement {
1511 fn new(
1512 identity: usize,
1513 offset_source: TranslatedContentOffsetSource,
1514 overscroll: crate::scroll::OverscrollEffect,
1515 ) -> Self {
1516 Self {
1517 identity,
1518 offset_source,
1519 overscroll,
1520 }
1521 }
1522}
1523
1524impl std::fmt::Debug for TranslatedContentContextElement {
1525 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1526 let offset_source = match &self.offset_source {
1527 TranslatedContentOffsetSource::LayoutContentOffset => "layout",
1528 TranslatedContentOffsetSource::LazyList { .. } => "lazy_list",
1529 };
1530 f.debug_struct("TranslatedContentContextElement")
1531 .field("identity", &self.identity)
1532 .field("offset_source", &offset_source)
1533 .finish()
1534 }
1535}
1536
1537impl PartialEq for TranslatedContentContextElement {
1538 fn eq(&self, other: &Self) -> bool {
1539 self.identity == other.identity
1540 && self.overscroll.ptr_eq(&other.overscroll)
1541 && self.offset_source.is_vertical() == other.offset_source.is_vertical()
1542 && self.offset_source.reverse_scrolling() == other.offset_source.reverse_scrolling()
1543 }
1544}
1545
1546impl Eq for TranslatedContentContextElement {}
1547
1548impl std::hash::Hash for TranslatedContentContextElement {
1549 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1550 self.identity.hash(state);
1551 self.offset_source.is_vertical().hash(state);
1552 self.offset_source.reverse_scrolling().hash(state);
1553 }
1554}
1555
1556impl ModifierNodeElement for TranslatedContentContextElement {
1557 type Node = TranslatedContentContextNode;
1558
1559 fn create(&self) -> Self::Node {
1560 TranslatedContentContextNode::new(
1561 self.identity,
1562 self.offset_source.clone(),
1563 self.overscroll.clone(),
1564 )
1565 }
1566
1567 fn update(&self, node: &mut Self::Node) {
1568 node.identity = self.identity;
1569 node.offset_source = self.offset_source.clone();
1570 node.overscroll = self.overscroll.clone();
1571 }
1572
1573 fn capabilities(&self) -> NodeCapabilities {
1574 NodeCapabilities::LAYOUT
1575 }
1576}
1577
1578impl Modifier {
1583 pub fn horizontal_scroll(self, state: ScrollState, reverse_scrolling: bool) -> Self {
1601 self.then(scroll_impl(state, false, reverse_scrolling, None))
1602 }
1603
1604 pub fn vertical_scroll(self, state: ScrollState, reverse_scrolling: bool) -> Self {
1613 self.then(scroll_impl(state, true, reverse_scrolling, None))
1614 }
1615}
1616
1617fn scroll_impl(
1626 state: ScrollState,
1627 is_vertical: bool,
1628 reverse_scrolling: bool,
1629 guard: Option<Rc<dyn Fn() -> bool>>,
1630) -> Modifier {
1631 let gesture_state = Rc::new(RefCell::new(ScrollGestureState::default()));
1633 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::ScrollState {
1634 state_id: state.id(),
1635 is_vertical,
1636 reverse_scrolling,
1637 });
1638
1639 let pointer_input = drag_gesture_input(
1641 (state.id(), is_vertical),
1642 DragGesture {
1643 target: state,
1644 gesture_state,
1645 is_vertical,
1646 reverse_input: false,
1648 motion_context: motion_context.clone(),
1649 guard,
1650 },
1651 );
1652
1653 let overscroll = motion_context.overscroll();
1655 let element = ScrollElement::new(state, overscroll.clone(), is_vertical, reverse_scrolling);
1656 let layout_modifier =
1657 Modifier::with_element(element).with_inspector_metadata(inspector_metadata(
1658 if is_vertical {
1659 "verticalScroll"
1660 } else {
1661 "horizontalScroll"
1662 },
1663 move |info| {
1664 info.add_property("isVertical", is_vertical.to_string());
1665 info.add_property("reverseScrolling", reverse_scrolling.to_string());
1666 },
1667 ));
1668 let motion_modifier =
1669 Modifier::with_element(MotionContextAnimatedElement::new(motion_context.clone()));
1670 let translated_content_modifier = Modifier::with_element(TranslatedContentContextElement::new(
1671 state.id() as usize,
1672 TranslatedContentOffsetSource::LayoutContentOffset,
1673 overscroll,
1674 ));
1675
1676 pointer_input
1678 .then(motion_modifier)
1679 .then(translated_content_modifier)
1680 .then(layout_modifier)
1681 .clip_to_bounds()
1682}
1683
1684use cranpose_foundation::lazy::LazyListState;
1689
1690impl Modifier {
1691 pub fn lazy_vertical_scroll(self, state: LazyListState, reverse_scrolling: bool) -> Self {
1702 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::LazyList {
1703 state_identity: state.inner_ptr() as usize,
1704 is_vertical: true,
1705 reverse_scrolling,
1706 });
1707 self.lazy_vertical_scroll_with_context(state, reverse_scrolling, motion_context)
1708 }
1709
1710 pub(crate) fn lazy_vertical_scroll_with_context(
1711 self,
1712 state: LazyListState,
1713 reverse_scrolling: bool,
1714 motion_context: ScrollMotionContext,
1715 ) -> Self {
1716 self.then(lazy_scroll_impl(
1717 state,
1718 true,
1719 reverse_scrolling,
1720 motion_context,
1721 ))
1722 }
1723
1724 pub(crate) fn lazy_horizontal_scroll_with_context(
1725 self,
1726 state: LazyListState,
1727 reverse_scrolling: bool,
1728 motion_context: ScrollMotionContext,
1729 ) -> Self {
1730 self.then(lazy_scroll_impl(
1731 state,
1732 false,
1733 reverse_scrolling,
1734 motion_context,
1735 ))
1736 }
1737}
1738
1739fn lazy_scroll_impl(
1741 state: LazyListState,
1742 is_vertical: bool,
1743 reverse_scrolling: bool,
1744 motion_context: ScrollMotionContext,
1745) -> Modifier {
1746 let gesture_state = Rc::new(RefCell::new(ScrollGestureState::default()));
1747 let list_state = state;
1748 let state_id = state.inner_ptr() as usize;
1749 let key = (state_id, is_vertical, reverse_scrolling);
1750 let overscroll = motion_context.overscroll();
1751 let translated_content_modifier = Modifier::with_element(TranslatedContentContextElement::new(
1752 state_id,
1753 TranslatedContentOffsetSource::LazyList {
1754 state,
1755 is_vertical,
1756 reverse_scrolling,
1757 },
1758 overscroll,
1759 ));
1760
1761 Modifier::with_element(MotionContextAnimatedElement::new(motion_context.clone()))
1762 .then(translated_content_modifier)
1763 .then(drag_gesture_input(
1764 key,
1765 DragGesture {
1766 target: list_state,
1767 gesture_state,
1768 is_vertical,
1769 reverse_input: reverse_scrolling,
1770 motion_context,
1771 guard: None,
1772 },
1773 ))
1774}
1775
1776impl Modifier {
1781 pub fn draggable(self, axis: Axis, state: DraggableState) -> Self {
1793 self.then(draggable_impl(axis, state, None))
1794 }
1795
1796 pub fn draggable_guarded(
1802 self,
1803 axis: Axis,
1804 state: DraggableState,
1805 guard: impl Fn() -> bool + 'static,
1806 ) -> Self {
1807 self.then(draggable_impl(axis, state, Some(Rc::new(guard))))
1808 }
1809}
1810
1811fn draggable_impl(
1812 axis: Axis,
1813 state: DraggableState,
1814 guard: Option<Rc<dyn Fn() -> bool>>,
1815) -> Modifier {
1816 let is_vertical = axis.is_vertical();
1817 let identity = state.identity();
1818 drag_gesture_input(
1819 (identity, is_vertical),
1820 DragGesture {
1821 target: state,
1822 gesture_state: Rc::new(RefCell::new(ScrollGestureState::default())),
1823 is_vertical,
1824 reverse_input: false,
1825 motion_context: scroll_motion_context_for_key(ScrollMotionContextKey::Draggable {
1828 state_identity: identity,
1829 is_vertical,
1830 }),
1831 guard,
1832 },
1833 )
1834}