1use std::{
2 cell::{Cell, RefCell},
3 rc::Rc,
4};
5
6use cranpose_core::{NodeId, current_runtime_handle, internal::FrameCallbackRegistration};
7use cranpose_foundation::{
8 DRAG_THRESHOLD, DelegatableNode, MAX_FLING_VELOCITY, ModifierNode, ModifierNodeElement,
9 NodeCapabilities, NodeState, PointerButton, PointerButtons, VelocityTracker1D,
10 velocity_tracker::ASSUME_STOPPED_MS,
11};
12use cranpose_ui_layout::Axis;
13use web_time::Instant;
14
15use super::{Modifier, Point, PointerEvent, PointerEventKind, inspector_metadata};
16use crate::{
17 draggable::DraggableState,
18 fling_animation::{
19 FlingAnimation, MIN_FLING_VELOCITY, SettleAnimation, SpringParams, fling_rest_position,
20 },
21 render_state::schedule_modifier_slices_repass,
22 scroll::{
23 ScrollElement, ScrollMotionContext, ScrollMotionContextKey, ScrollSettlePolicy,
24 ScrollState, scroll_motion_context_for_key,
25 },
26};
27
28#[cfg(feature = "test-helpers")]
29pub fn last_fling_velocity() -> f32 {
30 crate::render_state::debug_last_fling_velocity()
31}
32
33#[cfg(feature = "test-helpers")]
34pub fn reset_last_fling_velocity() {
35 crate::render_state::debug_reset_last_fling_velocity();
36}
37
38#[inline]
39fn set_last_fling_velocity(velocity: f32) {
40 crate::render_state::record_last_fling_velocity(velocity);
41}
42
43struct ScrollGestureState {
44 drag_down_position: Option<Point>,
45
46 last_position: Option<Point>,
47
48 is_dragging: bool,
49
50 axis_locked_out: bool,
51
52 velocity_tracker: VelocityTracker1D,
53
54 gesture_start_time: Option<Instant>,
55
56 gesture_start_event_time_ms: Option<i64>,
57
58 last_velocity_sample_ms: Option<i64>,
59
60 fling_animation: Option<FlingAnimation>,
61
62 is_overscrolling: bool,
63
64 settle_animation: Option<SettleAnimation>,
65
66 wheel_settle_watcher: Option<WheelSettleWatcher>,
67}
68
69impl Default for ScrollGestureState {
70 fn default() -> Self {
71 Self {
72 drag_down_position: None,
73 last_position: None,
74 is_dragging: false,
75 axis_locked_out: false,
76 velocity_tracker: VelocityTracker1D::new(),
77 gesture_start_time: None,
78 gesture_start_event_time_ms: None,
79 last_velocity_sample_ms: None,
80 fling_animation: None,
81 is_overscrolling: false,
82 settle_animation: None,
83 wheel_settle_watcher: None,
84 }
85 }
86}
87
88#[inline]
89fn calculate_total_delta(from: Point, to: Point, is_vertical: bool) -> f32 {
90 if is_vertical {
91 to.y - from.y
92 } else {
93 to.x - from.x
94 }
95}
96
97#[inline]
98fn calculate_incremental_delta(from: Point, to: Point, is_vertical: bool) -> f32 {
99 if is_vertical {
100 to.y - from.y
101 } else {
102 to.x - from.x
103 }
104}
105
106trait ScrollTarget: Clone {
107 fn apply_delta(&self, delta: f32) -> f32;
108
109 fn apply_wheel_delta(&self, delta: f32) -> f32 {
110 self.apply_delta(delta)
111 }
112
113 fn apply_fling_delta(&self, delta: f32) -> f32;
114
115 fn invalidate(&self);
116
117 fn current_offset(&self) -> f32;
118
119 fn can_scroll(&self) -> bool {
120 true
121 }
122
123 fn can_consume(&self, gesture_delta: f32) -> bool {
124 let _ = gesture_delta;
125 self.can_scroll()
126 }
127
128 fn settle_policy(&self) -> Option<ScrollSettlePolicy> {
129 None
130 }
131
132 fn allows_fling(&self) -> bool {
133 true
134 }
135
136 fn set_dragging(&self, dragging: bool) {
137 let _ = dragging;
138 }
139}
140
141impl ScrollTarget for ScrollState {
142 fn apply_delta(&self, delta: f32) -> f32 {
143 -self.dispatch_raw_delta(-delta)
144 }
145
146 fn apply_fling_delta(&self, delta: f32) -> f32 {
147 self.dispatch_raw_delta(delta)
148 }
149
150 fn invalidate(&self) {}
151
152 fn current_offset(&self) -> f32 {
153 self.value()
154 }
155
156 fn can_scroll(&self) -> bool {
157 self.max_value() > 0.0
158 }
159
160 fn can_consume(&self, gesture_delta: f32) -> bool {
161 let raw = -gesture_delta;
162 if raw > 0.0 {
163 self.value_non_reactive() < self.max_value()
164 } else {
165 self.value_non_reactive() > 0.0
166 }
167 }
168
169 fn settle_policy(&self) -> Option<ScrollSettlePolicy> {
170 ScrollState::settle_policy(self)
171 }
172}
173
174impl ScrollTarget for LazyListState {
175 fn apply_delta(&self, delta: f32) -> f32 {
176 self.dispatch_scroll_delta(delta)
177 }
178
179 fn apply_wheel_delta(&self, delta: f32) -> f32 {
180 if delta.abs() <= 0.001 {
181 0.0
182 } else {
183 self.dispatch_scroll_delta(delta)
184 }
185 }
186
187 fn apply_fling_delta(&self, delta: f32) -> f32 {
188 -self.dispatch_scroll_delta(-delta)
189 }
190
191 fn invalidate(&self) {}
192
193 fn current_offset(&self) -> f32 {
194 self.first_visible_item_scroll_offset()
195 }
196
197 fn can_scroll(&self) -> bool {
198 self.layout_info().total_items_count == 0
199 || self.can_scroll_forward_non_reactive()
200 || self.can_scroll_backward_non_reactive()
201 }
202
203 fn can_consume(&self, gesture_delta: f32) -> bool {
204 if self.layout_info().total_items_count == 0 {
205 return true;
206 }
207 if gesture_delta < 0.0 {
208 self.can_scroll_forward_non_reactive()
209 } else {
210 self.can_scroll_backward_non_reactive()
211 }
212 }
213}
214
215struct DragGesture<S: ScrollTarget> {
216 target: S,
217 gesture_state: Rc<RefCell<ScrollGestureState>>,
218 is_vertical: bool,
219 reverse_input: bool,
220 motion_context: ScrollMotionContext,
221 guard: Option<Rc<dyn Fn() -> bool>>,
222}
223
224fn drag_gesture_input<K, S>(key: K, gesture: DragGesture<S>) -> Modifier
225where
226 K: std::hash::Hash + 'static,
227 S: ScrollTarget + 'static,
228{
229 let DragGesture {
230 target,
231 gesture_state,
232 is_vertical,
233 reverse_input,
234 motion_context,
235 guard,
236 } = gesture;
237
238 Modifier::empty().pointer_input(key, move |scope| {
239 let detector = ScrollGestureDetector::new(
240 gesture_state.clone(),
241 target.clone(),
242 is_vertical,
243 reverse_input,
244 motion_context.overscroll(),
245 motion_context.clone(),
246 );
247 let guard = guard.clone();
248
249 async move {
250 scope
251 .await_pointer_event_scope(|await_scope| async move {
252 loop {
253 let event = await_scope.await_pointer_event().await;
254
255 if event.id != 0 {
256 continue;
257 }
258
259 if event.is_consumed() {
260 if matches!(
261 event.kind,
262 PointerEventKind::Down
263 | PointerEventKind::Move
264 | PointerEventKind::Up
265 | PointerEventKind::Cancel
266 ) {
267 detector.on_cancel();
268 }
269 continue;
270 }
271
272 if let Some(ref guard) = guard
273 && !guard()
274 {
275 if matches!(event.kind, PointerEventKind::Up | PointerEventKind::Cancel)
276 {
277 detector.on_cancel();
278 }
279 continue;
280 }
281
282 let should_consume = match event.kind {
283 PointerEventKind::Down => {
284 detector.on_down(event.position, event.time_ms)
285 }
286 PointerEventKind::Move => detector.on_move(
287 event.position,
288 event.buttons,
289 event.time_ms,
290 &event,
291 ),
292 PointerEventKind::Up => detector.on_up(event.time_ms),
293 PointerEventKind::Cancel => detector.on_cancel(),
294 PointerEventKind::Scroll => detector.on_scroll(
295 if is_vertical {
296 event.scroll_delta.y
297 } else {
298 event.scroll_delta.x
299 },
300 &event,
301 ),
302 PointerEventKind::Zoom
303 | PointerEventKind::RotaryScrollPre
304 | PointerEventKind::RotaryScroll
305 | PointerEventKind::Enter
306 | PointerEventKind::Exit => false,
307 };
308
309 if should_consume {
310 event.consume();
311 }
312 }
313 })
314 .await;
315 }
316 })
317}
318
319impl ScrollTarget for DraggableState {
320 fn apply_delta(&self, delta: f32) -> f32 {
321 self.drag_by(delta);
322 delta
323 }
324
325 fn apply_fling_delta(&self, delta: f32) -> f32 {
326 self.drag_by(delta);
327 delta
328 }
329
330 fn invalidate(&self) {}
331
332 fn current_offset(&self) -> f32 {
333 self.offset()
334 }
335
336 fn allows_fling(&self) -> bool {
337 false
338 }
339
340 fn set_dragging(&self, dragging: bool) {
341 DraggableState::set_dragging(self, dragging);
342 }
343}
344
345const WHEEL_SETTLE_IDLE_NANOS: u64 = 180_000_000;
346
347struct WheelSettleWatcher {
348 is_running: Rc<Cell<bool>>,
349 registration: Rc<RefCell<Option<FrameCallbackRegistration>>>,
350}
351
352impl WheelSettleWatcher {
353 fn cancel(&self) {
354 self.is_running.set(false);
355 self.registration.borrow_mut().take();
356 }
357}
358
359struct ScrollGestureDetector<S: ScrollTarget> {
360 gesture_state: Rc<RefCell<ScrollGestureState>>,
361
362 scroll_target: S,
363
364 is_vertical: bool,
365
366 reverse_scrolling: bool,
367
368 overscroll: crate::scroll::OverscrollEffect,
369
370 motion_context: ScrollMotionContext,
371}
372
373impl<S: ScrollTarget + 'static> ScrollGestureDetector<S> {
374 fn new(
375 gesture_state: Rc<RefCell<ScrollGestureState>>,
376 scroll_target: S,
377 is_vertical: bool,
378 reverse_scrolling: bool,
379 overscroll: crate::scroll::OverscrollEffect,
380 motion_context: ScrollMotionContext,
381 ) -> Self {
382 Self {
383 gesture_state,
384 scroll_target,
385 is_vertical,
386 reverse_scrolling,
387 overscroll,
388 motion_context,
389 }
390 }
391
392 fn on_down(&self, position: Point, time_ms: Option<i64>) -> bool {
393 let mut gs = self.gesture_state.borrow_mut();
394
395 if let Some(fling) = gs.fling_animation.take() {
396 fling.cancel();
397 }
398 if let Some(settle) = gs.settle_animation.take() {
399 settle.cancel();
400 }
401 if let Some(watcher) = gs.wheel_settle_watcher.take() {
402 watcher.cancel();
403 }
404 self.motion_context.set_active(false);
405
406 gs.drag_down_position = Some(position);
407 gs.last_position = Some(position);
408 gs.is_dragging = false;
409 gs.axis_locked_out = false;
410 gs.velocity_tracker.reset();
411 gs.gesture_start_time = Some(Instant::now());
412 gs.gesture_start_event_time_ms = time_ms;
413 gs.is_overscrolling = self.overscroll.offset().abs() > 0.001;
414
415 let pos = if self.is_vertical {
416 position.y
417 } else {
418 position.x
419 };
420 gs.velocity_tracker.add_data_point(0, pos);
421 gs.last_velocity_sample_ms = Some(0);
422
423 false
424 }
425
426 fn on_move(
427 &self,
428 position: Point,
429 buttons: PointerButtons,
430 time_ms: Option<i64>,
431 event: &PointerEvent,
432 ) -> bool {
433 let mut gs = self.gesture_state.borrow_mut();
434
435 if !buttons.contains(PointerButton::Primary) && gs.drag_down_position.is_some() {
436 if gs.is_dragging {
437 self.scroll_target.set_dragging(false);
438 }
439 gs.drag_down_position = None;
440 gs.last_position = None;
441 gs.is_dragging = false;
442 gs.axis_locked_out = false;
443 gs.gesture_start_time = None;
444 gs.gesture_start_event_time_ms = None;
445 gs.last_velocity_sample_ms = None;
446 gs.velocity_tracker.reset();
447 self.motion_context.set_active(false);
448 return false;
449 }
450
451 let Some(down_pos) = gs.drag_down_position else {
452 return false;
453 };
454
455 let Some(last_pos) = gs.last_position else {
456 gs.last_position = Some(position);
457 return false;
458 };
459
460 let incremental_delta = calculate_incremental_delta(last_pos, position, self.is_vertical);
461
462 let mut edge_candidate = false;
463 if !gs.is_dragging && !gs.axis_locked_out {
464 let signed_main_delta = calculate_total_delta(down_pos, position, self.is_vertical);
465 let main_delta = signed_main_delta.abs();
466 let cross_delta = calculate_total_delta(down_pos, position, !self.is_vertical).abs();
467 if main_delta > DRAG_THRESHOLD && main_delta >= cross_delta {
468 if self.scroll_target.can_consume(signed_main_delta) {
469 gs.is_dragging = true;
470 self.scroll_target.set_dragging(true);
471 self.motion_context.set_active(true);
472 } else if self.scroll_target.can_scroll() {
473 edge_candidate = true;
474 }
475 } else if cross_delta > DRAG_THRESHOLD && cross_delta > main_delta {
476 gs.axis_locked_out = true;
477 }
478 }
479
480 gs.last_position = Some(position);
481
482 let pos = if self.is_vertical {
483 position.y
484 } else {
485 position.x
486 };
487 let event_sample_ms = gs
488 .gesture_start_event_time_ms
489 .zip(time_ms)
490 .map(|(start_ms, now_ms)| now_ms - start_ms);
491 let sample_ms = if let Some(event_sample_ms) = event_sample_ms {
492 Some(match gs.last_velocity_sample_ms {
493 Some(last_sample_ms) => event_sample_ms.max(last_sample_ms),
494 None => event_sample_ms.max(0),
495 })
496 } else if let Some(start_time) = gs.gesture_start_time {
497 let elapsed_ms = start_time.elapsed().as_millis() as i64;
498 Some(match gs.last_velocity_sample_ms {
499 Some(last_sample_ms) => {
500 let mut sample_ms = if elapsed_ms <= last_sample_ms {
501 last_sample_ms + 1
502 } else {
503 elapsed_ms
504 };
505 if sample_ms - last_sample_ms > ASSUME_STOPPED_MS {
506 sample_ms = last_sample_ms + ASSUME_STOPPED_MS;
507 }
508 sample_ms
509 }
510 None => elapsed_ms,
511 })
512 } else {
513 None
514 };
515 if let Some(sample_ms) = sample_ms {
516 log::trace!(
517 target: "cranpose::velocity",
518 "sample t={sample_ms}ms pos={pos:.2} event_time={time_ms:?}"
519 );
520 gs.velocity_tracker.add_data_point(sample_ms, pos);
521 gs.last_velocity_sample_ms = Some(sample_ms);
522 }
523
524 if gs.is_dragging {
525 drop(gs);
526 let delta = if self.reverse_scrolling {
527 -incremental_delta
528 } else {
529 incremental_delta
530 };
531 let overscroll = self.overscroll.clone();
532 overscroll.apply_to_scroll(delta, |delta| self.scroll_target.apply_delta(delta));
533 self.scroll_target.invalidate();
534 true
535 } else if gs.is_overscrolling {
536 drop(gs);
537 let delta = if self.reverse_scrolling {
538 -incremental_delta
539 } else {
540 incremental_delta
541 };
542 self.apply_overscroll_delta(delta)
543 } else if edge_candidate {
544 drop(gs);
545 let delta = if self.reverse_scrolling {
546 -incremental_delta
547 } else {
548 incremental_delta
549 };
550 let detector = self.clone_for_watcher();
551 event.defer_post_dispatch_action(move || detector.apply_overscroll_candidate(delta));
552 false
553 } else {
554 false
555 }
556 }
557
558 fn apply_overscroll_delta(&self, delta: f32) -> bool {
559 self.motion_context.set_active(true);
560 let overscroll = self.overscroll.clone();
561 let target_consumed = Cell::new(0.0);
562 let before_overscroll = overscroll.offset();
563 overscroll.apply_to_scroll(delta, |delta| {
564 let consumed = self.scroll_target.apply_delta(delta);
565 target_consumed.set(consumed);
566 consumed
567 });
568 self.scroll_target.invalidate();
569 let consumed = target_consumed.get().abs() > 0.001
570 || (overscroll.offset() - before_overscroll).abs() > 0.001;
571 if !consumed {
572 self.motion_context.set_active(false);
573 }
574 consumed
575 }
576
577 fn apply_overscroll_candidate(&self, delta: f32) -> bool {
578 let consumed = self.apply_overscroll_delta(delta);
579 if consumed {
580 self.gesture_state.borrow_mut().is_overscrolling = true;
581 }
582 consumed
583 }
584
585 fn finish_gesture(&self, allow_fling: bool, release_time_ms: Option<i64>) -> bool {
586 let (was_dragging, gesture_owned, velocity, start_fling, existing_fling) = {
587 let mut gs = self.gesture_state.borrow_mut();
588 let was_dragging = gs.is_dragging;
589 let gesture_owned = was_dragging || gs.is_overscrolling;
590 let mut velocity = 0.0;
591
592 if allow_fling && gesture_owned && gs.gesture_start_time.is_some() {
593 let release_sample_ms = release_time_ms
594 .zip(gs.gesture_start_event_time_ms)
595 .map(|(release_ms, start_ms)| release_ms - start_ms)
596 .or_else(|| {
597 gs.gesture_start_time
598 .map(|start| start.elapsed().as_millis() as i64)
599 });
600 let rested_before_release = release_sample_ms
601 .zip(gs.last_velocity_sample_ms)
602 .is_some_and(|(release_ms, last_sample_ms)| {
603 release_ms - last_sample_ms > ASSUME_STOPPED_MS
604 });
605 if !rested_before_release {
606 velocity = gs
607 .velocity_tracker
608 .calculate_velocity_with_max(MAX_FLING_VELOCITY);
609 }
610 }
611
612 let start_fling = allow_fling && was_dragging && velocity.abs() > MIN_FLING_VELOCITY;
613 let existing_fling = if start_fling {
614 gs.fling_animation.take()
615 } else {
616 None
617 };
618
619 if was_dragging {
620 self.scroll_target.set_dragging(false);
621 }
622 gs.drag_down_position = None;
623 gs.last_position = None;
624 gs.is_dragging = false;
625 gs.is_overscrolling = false;
626 gs.axis_locked_out = false;
627 gs.gesture_start_time = None;
628 gs.gesture_start_event_time_ms = None;
629 gs.last_velocity_sample_ms = None;
630
631 (
632 was_dragging,
633 gesture_owned,
634 velocity,
635 start_fling,
636 existing_fling,
637 )
638 };
639
640 if allow_fling && gesture_owned {
641 log::debug!(
642 target: "cranpose::velocity",
643 "gesture finished: fling velocity={velocity:.2} dp/s start_fling={start_fling}"
644 );
645 set_last_fling_velocity(velocity);
646 }
647
648 let adjusted_velocity = if self.reverse_scrolling {
649 -velocity
650 } else {
651 velocity
652 };
653 let fling_velocity = -adjusted_velocity;
654 let has_overscroll = self.overscroll.offset().abs() > 0.001;
655
656 let settle_target = if was_dragging {
657 self.scroll_target.settle_policy().and_then(|policy| {
658 let current = self.scroll_target.current_offset();
659 let proposed = if start_fling {
660 fling_rest_position(current, fling_velocity)
661 } else {
662 current
663 };
664 let target = policy(proposed, fling_velocity);
665 ((target - proposed).abs() > 0.5).then_some(target)
666 })
667 } else {
668 None
669 };
670
671 if has_overscroll {
672 if let Some(old_fling) = existing_fling {
673 old_fling.cancel();
674 }
675 self.start_overscroll_settle(-fling_velocity);
676 } else if let Some(target) = settle_target {
677 if let Some(old_fling) = existing_fling {
678 old_fling.cancel();
679 }
680 self.start_settle_animation(target, fling_velocity);
681 } else if start_fling {
682 if let Some(old_fling) = existing_fling {
683 old_fling.cancel();
684 }
685 self.start_fling_animation(fling_velocity);
686 } else {
687 self.motion_context.set_active(false);
688 }
689
690 gesture_owned
691 }
692
693 fn start_fling_animation(&self, fling_velocity: f32) {
694 let Some(runtime) = current_runtime_handle() else {
695 self.motion_context.set_active(false);
696 return;
697 };
698 self.motion_context.set_active(true);
699 let scroll_target = self.scroll_target.clone();
700 let fling = FlingAnimation::new(runtime);
701 let motion_context = self.motion_context.clone();
702 let initial_value = scroll_target.current_offset();
703 let scroll_target_for_fling = scroll_target.clone();
704 let scroll_target_for_end = scroll_target.clone();
705 let detector_for_end = self.clone_for_watcher();
706 let overscroll_for_fling = self.overscroll.clone();
707
708 fling.start_fling(
709 initial_value,
710 fling_velocity,
711 move |delta| {
712 let consumed = overscroll_for_fling.apply_to_fling(delta, |delta| {
713 scroll_target_for_fling.apply_fling_delta(delta)
714 });
715 scroll_target_for_fling.invalidate();
716 consumed
717 },
718 move || {
719 scroll_target_for_end.invalidate();
720 let settle_running = detector_for_end
721 .gesture_state
722 .borrow()
723 .settle_animation
724 .as_ref()
725 .is_some_and(SettleAnimation::is_running);
726 if detector_for_end.overscroll.offset().abs() > 0.001 && !settle_running {
727 detector_for_end.start_overscroll_settle(0.0);
728 }
729 detector_for_end.update_motion_active(&motion_context);
730 },
731 );
732
733 let mut gs = self.gesture_state.borrow_mut();
734 gs.fling_animation = Some(fling);
735 }
736
737 fn start_settle_animation(&self, target: f32, initial_velocity: f32) {
738 let Some(runtime) = current_runtime_handle() else {
739 self.motion_context.set_active(false);
740 return;
741 };
742 self.motion_context.set_active(true);
743 let settle = SettleAnimation::new(runtime, SpringParams::SETTLE_POLICY);
744 let scroll_target_for_settle = self.scroll_target.clone();
745 let scroll_target_for_end = self.scroll_target.clone();
746 let detector_for_end = self.clone_for_watcher();
747 let motion_context = self.motion_context.clone();
748 settle.start_settle(
749 self.scroll_target.current_offset(),
750 initial_velocity,
751 target,
752 move |delta| {
753 let consumed = scroll_target_for_settle.apply_fling_delta(delta);
754 scroll_target_for_settle.invalidate();
755 consumed
756 },
757 move |_| {
758 scroll_target_for_end.invalidate();
759 detector_for_end.update_motion_active(&motion_context);
760 },
761 );
762 let mut gs = self.gesture_state.borrow_mut();
763 gs.settle_animation = Some(settle);
764 }
765
766 fn start_overscroll_settle(&self, initial_velocity: f32) {
767 let Some(runtime) = current_runtime_handle() else {
768 let current = self.overscroll.offset();
769 self.overscroll.apply_settle_delta(-current);
770 self.motion_context.set_active(false);
771 return;
772 };
773 let settle = SettleAnimation::new(runtime, SpringParams::OVERSCROLL_BOUNCE);
774 let overscroll_for_settle = self.overscroll.clone();
775 let overscroll_for_end = self.overscroll.clone();
776 let detector_for_end = self.clone_for_watcher();
777 let motion_context = self.motion_context.clone();
778 let initial = self.overscroll.offset();
779 settle.start_settle(
780 initial,
781 initial_velocity,
782 0.0,
783 move |delta| overscroll_for_settle.apply_settle_delta(delta),
784 move |end| {
785 let current = overscroll_for_end.offset();
786 overscroll_for_end.apply_settle_delta(-current);
787 if end.hit_boundary && end.velocity.abs() > MIN_FLING_VELOCITY {
788 detector_for_end.start_fling_animation(-end.velocity);
789 } else {
790 detector_for_end.update_motion_active(&motion_context);
791 }
792 },
793 );
794 let mut gs = self.gesture_state.borrow_mut();
795 gs.is_overscrolling = false;
796 gs.settle_animation = Some(settle);
797 }
798
799 fn update_motion_active(&self, motion_context: &ScrollMotionContext) {
800 let running = {
801 let gs = self.gesture_state.borrow();
802 gs.fling_animation
803 .as_ref()
804 .is_some_and(FlingAnimation::is_running)
805 || gs
806 .settle_animation
807 .as_ref()
808 .is_some_and(SettleAnimation::is_running)
809 };
810 if !running {
811 motion_context.set_active(false);
812 }
813 }
814
815 fn on_up(&self, time_ms: Option<i64>) -> bool {
816 self.finish_gesture(self.scroll_target.allows_fling(), time_ms)
817 }
818
819 fn on_cancel(&self) -> bool {
820 self.finish_gesture(false, None)
821 }
822
823 fn on_scroll(&self, axis_delta: f32, event: &PointerEvent) -> bool {
824 if axis_delta.abs() <= f32::EPSILON {
825 return false;
826 }
827
828 let delta = if self.reverse_scrolling {
829 -axis_delta
830 } else {
831 axis_delta
832 };
833 if !self.scroll_target.can_consume(delta) && self.scroll_target.can_scroll() {
834 let detector = self.clone_for_watcher();
835 event.defer_post_dispatch_action(move || detector.apply_wheel_delta(delta));
836 return false;
837 }
838 self.apply_wheel_delta(delta)
839 }
840
841 fn apply_wheel_delta(&self, delta: f32) -> bool {
842 {
843 let mut gs = self.gesture_state.borrow_mut();
844 if let Some(fling) = gs.fling_animation.take() {
845 fling.cancel();
846 }
847 if let Some(settle) = gs.settle_animation.take() {
848 settle.cancel();
849 }
850 gs.drag_down_position = None;
851 gs.last_position = None;
852 gs.is_dragging = false;
853 gs.axis_locked_out = false;
854 gs.gesture_start_time = None;
855 gs.gesture_start_event_time_ms = None;
856 gs.last_velocity_sample_ms = None;
857 gs.velocity_tracker.reset();
858 }
859
860 self.motion_context.activate_for_current_frame();
861 let overscroll = self.overscroll.clone();
862 let consumed =
863 overscroll.apply_to_scroll(delta, |delta| self.scroll_target.apply_wheel_delta(delta));
864 let overscroll_active = self.overscroll.offset().abs() > 0.001;
865 if consumed.abs() > 0.001 {
866 self.scroll_target.invalidate();
867 }
868 if consumed.abs() > 0.001 || overscroll_active {
869 self.ensure_wheel_settle_watcher();
870 true
871 } else {
872 false
873 }
874 }
875
876 fn ensure_wheel_settle_watcher(&self) {
877 if self.scroll_target.settle_policy().is_none() && self.overscroll.offset().abs() <= 0.001 {
878 return;
879 }
880 {
881 let gs = self.gesture_state.borrow();
882 if gs
883 .wheel_settle_watcher
884 .as_ref()
885 .is_some_and(|watcher| watcher.is_running.get())
886 {
887 return;
888 }
889 }
890 let Some(runtime) = current_runtime_handle() else {
891 return;
892 };
893
894 let is_running = Rc::new(Cell::new(true));
895 let registration = Rc::new(RefCell::new(None));
896
897 struct WheelSettleLoop<S: ScrollTarget> {
898 detector: ScrollGestureDetector<S>,
899 gesture_state: Rc<RefCell<ScrollGestureState>>,
900 frame_clock: cranpose_core::internal::FrameClock,
901 is_running: Rc<Cell<bool>>,
902 registration: Rc<RefCell<Option<FrameCallbackRegistration>>>,
903 last_offset: Rc<Cell<f32>>,
904 idle_nanos: Rc<Cell<u64>>,
905 last_frame: Rc<Cell<Option<u64>>>,
906 }
907
908 impl<S: ScrollTarget + 'static> WheelSettleLoop<S> {
909 fn next(&self) -> Self {
910 Self {
911 detector: self.detector.clone_for_watcher(),
912 gesture_state: Rc::clone(&self.gesture_state),
913 frame_clock: self.frame_clock.clone(),
914 is_running: Rc::clone(&self.is_running),
915 registration: Rc::clone(&self.registration),
916 last_offset: Rc::clone(&self.last_offset),
917 idle_nanos: Rc::clone(&self.idle_nanos),
918 last_frame: Rc::clone(&self.last_frame),
919 }
920 }
921
922 fn schedule(self) {
923 let continuation = self.next();
924 let registration_slot = Rc::clone(&self.registration);
925 let new_registration = self.frame_clock.with_frame_nanos(move |frame_time_nanos| {
926 let this = &continuation;
927 if !this.is_running.get() {
928 return;
929 }
930 {
931 let gs = this.gesture_state.borrow();
932 let animating = gs.is_dragging
933 || gs
934 .fling_animation
935 .as_ref()
936 .is_some_and(FlingAnimation::is_running)
937 || gs
938 .settle_animation
939 .as_ref()
940 .is_some_and(SettleAnimation::is_running);
941 if animating {
942 this.is_running.set(false);
943 return;
944 }
945 }
946
947 let offset = this.detector.scroll_target.current_offset();
948 let dt = this
949 .last_frame
950 .get()
951 .map_or(0, |last| frame_time_nanos.saturating_sub(last));
952 this.last_frame.set(Some(frame_time_nanos));
953 if (offset - this.last_offset.get()).abs() > 0.01 {
954 this.last_offset.set(offset);
955 this.idle_nanos.set(0);
956 } else {
957 this.idle_nanos.set(this.idle_nanos.get() + dt);
958 }
959
960 if this.idle_nanos.get() >= WHEEL_SETTLE_IDLE_NANOS {
961 this.is_running.set(false);
962 if this.detector.overscroll.offset().abs() > 0.001 {
963 this.detector.start_overscroll_settle(0.0);
964 return;
965 }
966 if let Some(policy) = this.detector.scroll_target.settle_policy() {
967 let target = policy(offset, 0.0);
968 if (target - offset).abs() > 0.5 {
969 this.detector.start_settle_animation(target, 0.0);
970 }
971 }
972 return;
973 }
974
975 continuation.next().schedule();
976 });
977 *registration_slot.borrow_mut() = Some(new_registration);
978 }
979 }
980
981 WheelSettleLoop {
982 detector: self.clone_for_watcher(),
983 gesture_state: Rc::clone(&self.gesture_state),
984 frame_clock: runtime.frame_clock(),
985 is_running: Rc::clone(&is_running),
986 registration: Rc::clone(®istration),
987 last_offset: Rc::new(Cell::new(self.scroll_target.current_offset())),
988 idle_nanos: Rc::new(Cell::new(0u64)),
989 last_frame: Rc::new(Cell::new(None::<u64>)),
990 }
991 .schedule();
992
993 self.gesture_state.borrow_mut().wheel_settle_watcher = Some(WheelSettleWatcher {
994 is_running,
995 registration,
996 });
997 }
998
999 fn clone_for_watcher(&self) -> ScrollGestureDetector<S> {
1000 ScrollGestureDetector {
1001 gesture_state: Rc::clone(&self.gesture_state),
1002 scroll_target: self.scroll_target.clone(),
1003 is_vertical: self.is_vertical,
1004 reverse_scrolling: self.reverse_scrolling,
1005 overscroll: self.overscroll.clone(),
1006 motion_context: self.motion_context.clone(),
1007 }
1008 }
1009}
1010
1011pub(crate) struct MotionContextAnimatedNode {
1012 state: NodeState,
1013 motion_context: ScrollMotionContext,
1014 invalidation_callback_id: Option<u64>,
1015 overscroll_callback_id: Option<u64>,
1016 node_id: Option<NodeId>,
1017}
1018
1019impl MotionContextAnimatedNode {
1020 fn new(motion_context: ScrollMotionContext) -> Self {
1021 Self {
1022 state: NodeState::new(),
1023 motion_context,
1024 invalidation_callback_id: None,
1025 overscroll_callback_id: None,
1026 node_id: None,
1027 }
1028 }
1029
1030 pub(crate) fn is_active(&self) -> bool {
1031 self.motion_context.is_active()
1032 }
1033}
1034
1035pub(crate) struct TranslatedContentContextNode {
1036 state: NodeState,
1037 identity: usize,
1038 offset_source: TranslatedContentOffsetSource,
1039 overscroll: crate::scroll::OverscrollEffect,
1040 overscroll_callback_id: Option<u64>,
1041}
1042
1043impl TranslatedContentContextNode {
1044 fn new(
1045 identity: usize,
1046 offset_source: TranslatedContentOffsetSource,
1047 overscroll: crate::scroll::OverscrollEffect,
1048 ) -> Self {
1049 Self {
1050 state: NodeState::new(),
1051 identity,
1052 offset_source,
1053 overscroll,
1054 overscroll_callback_id: None,
1055 }
1056 }
1057
1058 pub(crate) fn is_active(&self) -> bool {
1059 true
1060 }
1061
1062 pub(crate) fn identity(&self) -> usize {
1063 self.identity
1064 }
1065
1066 pub(crate) fn content_offset_reader(&self) -> Option<Rc<dyn Fn() -> Point>> {
1067 self.offset_source
1068 .content_offset_reader(self.overscroll.clone())
1069 }
1070}
1071
1072impl DelegatableNode for TranslatedContentContextNode {
1073 fn node_state(&self) -> &NodeState {
1074 &self.state
1075 }
1076}
1077
1078impl ModifierNode for TranslatedContentContextNode {
1079 fn on_attach(&mut self, context: &mut dyn cranpose_foundation::ModifierNodeContext) {
1080 if let Some(node_id) = context.node_id() {
1081 self.overscroll_callback_id =
1082 Some(self.overscroll.add_invalidate_callback(Box::new(move || {
1083 schedule_modifier_slices_repass(node_id)
1084 })));
1085 }
1086 }
1087
1088 fn on_detach(&mut self) {
1089 if let Some(id) = self.overscroll_callback_id.take() {
1090 self.overscroll.remove_invalidate_callback(id);
1091 }
1092 }
1093}
1094
1095impl DelegatableNode for MotionContextAnimatedNode {
1096 fn node_state(&self) -> &NodeState {
1097 &self.state
1098 }
1099}
1100
1101impl ModifierNode for MotionContextAnimatedNode {
1102 fn on_attach(&mut self, context: &mut dyn cranpose_foundation::ModifierNodeContext) {
1103 let node_id = context.node_id();
1104 self.node_id = node_id;
1105 if let Some(node_id) = node_id {
1106 let callback_id = self
1107 .motion_context
1108 .add_invalidate_callback(Box::new(move || {
1109 schedule_modifier_slices_repass(node_id);
1110 }));
1111 self.invalidation_callback_id = Some(callback_id);
1112 let callback_id = self
1113 .motion_context
1114 .overscroll()
1115 .add_invalidate_callback(Box::new(move || {
1116 crate::schedule_measure_repass(node_id);
1117 schedule_modifier_slices_repass(node_id);
1118 }));
1119 self.overscroll_callback_id = Some(callback_id);
1120 }
1121 }
1122
1123 fn on_detach(&mut self) {
1124 if let Some(id) = self.invalidation_callback_id.take() {
1125 self.motion_context.remove_invalidate_callback(id);
1126 }
1127 if let Some(id) = self.overscroll_callback_id.take() {
1128 self.motion_context
1129 .overscroll()
1130 .remove_invalidate_callback(id);
1131 }
1132 self.node_id = None;
1133 }
1134}
1135
1136#[derive(Clone)]
1137struct MotionContextAnimatedElement {
1138 motion_context: ScrollMotionContext,
1139}
1140
1141impl MotionContextAnimatedElement {
1142 fn new(motion_context: ScrollMotionContext) -> Self {
1143 Self { motion_context }
1144 }
1145}
1146
1147impl std::fmt::Debug for MotionContextAnimatedElement {
1148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1149 f.debug_struct("MotionContextAnimatedElement").finish()
1150 }
1151}
1152
1153impl PartialEq for MotionContextAnimatedElement {
1154 fn eq(&self, other: &Self) -> bool {
1155 self.motion_context.ptr_eq(&other.motion_context)
1156 }
1157}
1158
1159impl Eq for MotionContextAnimatedElement {}
1160
1161impl std::hash::Hash for MotionContextAnimatedElement {
1162 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1163 self.motion_context.stable_key().hash(state);
1164 }
1165}
1166
1167impl ModifierNodeElement for MotionContextAnimatedElement {
1168 type Node = MotionContextAnimatedNode;
1169
1170 fn create(&self) -> Self::Node {
1171 MotionContextAnimatedNode::new(self.motion_context.clone())
1172 }
1173
1174 fn update(&self, node: &mut Self::Node) {
1175 if node.motion_context.ptr_eq(&self.motion_context) {
1176 return;
1177 }
1178 if let Some(id) = node.invalidation_callback_id.take() {
1179 node.motion_context.remove_invalidate_callback(id);
1180 }
1181 node.motion_context = self.motion_context.clone();
1182 if let Some(node_id) = node.node_id {
1183 let callback_id = node
1184 .motion_context
1185 .add_invalidate_callback(Box::new(move || {
1186 schedule_modifier_slices_repass(node_id);
1187 }));
1188 node.invalidation_callback_id = Some(callback_id);
1189 }
1190 }
1191
1192 fn capabilities(&self) -> NodeCapabilities {
1193 NodeCapabilities::LAYOUT
1194 }
1195}
1196
1197#[derive(Clone)]
1198enum TranslatedContentOffsetSource {
1199 LayoutContentOffset,
1200 LazyList {
1201 state: LazyListState,
1202 is_vertical: bool,
1203 reverse_scrolling: bool,
1204 },
1205}
1206
1207impl TranslatedContentOffsetSource {
1208 fn content_offset_reader(
1209 &self,
1210 overscroll: crate::scroll::OverscrollEffect,
1211 ) -> Option<Rc<dyn Fn() -> Point>> {
1212 match self {
1213 Self::LayoutContentOffset => None,
1214 Self::LazyList {
1215 state, is_vertical, ..
1216 } => Some(Rc::new(lazy_list_content_offset_reader(
1217 *state,
1218 *is_vertical,
1219 overscroll,
1220 ))),
1221 }
1222 }
1223
1224 fn is_vertical(&self) -> Option<bool> {
1225 match self {
1226 Self::LayoutContentOffset => None,
1227 Self::LazyList { is_vertical, .. } => Some(*is_vertical),
1228 }
1229 }
1230
1231 fn reverse_scrolling(&self) -> Option<bool> {
1232 match self {
1233 Self::LayoutContentOffset => None,
1234 Self::LazyList {
1235 reverse_scrolling, ..
1236 } => Some(*reverse_scrolling),
1237 }
1238 }
1239}
1240
1241fn lazy_list_content_offset_reader(
1242 state: LazyListState,
1243 is_vertical: bool,
1244 overscroll: crate::scroll::OverscrollEffect,
1245) -> impl Fn() -> Point {
1246 move || {
1247 let info = state.layout_info();
1248 if info.visible_items_info.is_empty() {
1249 return Point::default();
1250 };
1251 overscroll.set_dimension(info.viewport_size);
1252 let main_offset = info.snap_anchor_offset;
1253 let bounce = overscroll.offset();
1254 if is_vertical {
1255 Point::new(0.0, main_offset + bounce)
1256 } else {
1257 Point::new(main_offset + bounce, 0.0)
1258 }
1259 }
1260}
1261
1262#[derive(Clone)]
1263struct TranslatedContentContextElement {
1264 identity: usize,
1265 offset_source: TranslatedContentOffsetSource,
1266 overscroll: crate::scroll::OverscrollEffect,
1267}
1268
1269impl TranslatedContentContextElement {
1270 fn new(
1271 identity: usize,
1272 offset_source: TranslatedContentOffsetSource,
1273 overscroll: crate::scroll::OverscrollEffect,
1274 ) -> Self {
1275 Self {
1276 identity,
1277 offset_source,
1278 overscroll,
1279 }
1280 }
1281}
1282
1283impl std::fmt::Debug for TranslatedContentContextElement {
1284 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1285 let offset_source = match &self.offset_source {
1286 TranslatedContentOffsetSource::LayoutContentOffset => "layout",
1287 TranslatedContentOffsetSource::LazyList { .. } => "lazy_list",
1288 };
1289 f.debug_struct("TranslatedContentContextElement")
1290 .field("identity", &self.identity)
1291 .field("offset_source", &offset_source)
1292 .finish()
1293 }
1294}
1295
1296impl PartialEq for TranslatedContentContextElement {
1297 fn eq(&self, other: &Self) -> bool {
1298 self.identity == other.identity
1299 && self.overscroll.ptr_eq(&other.overscroll)
1300 && self.offset_source.is_vertical() == other.offset_source.is_vertical()
1301 && self.offset_source.reverse_scrolling() == other.offset_source.reverse_scrolling()
1302 }
1303}
1304
1305impl Eq for TranslatedContentContextElement {}
1306
1307impl std::hash::Hash for TranslatedContentContextElement {
1308 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1309 self.identity.hash(state);
1310 self.offset_source.is_vertical().hash(state);
1311 self.offset_source.reverse_scrolling().hash(state);
1312 }
1313}
1314
1315impl ModifierNodeElement for TranslatedContentContextElement {
1316 type Node = TranslatedContentContextNode;
1317
1318 fn create(&self) -> Self::Node {
1319 TranslatedContentContextNode::new(
1320 self.identity,
1321 self.offset_source.clone(),
1322 self.overscroll.clone(),
1323 )
1324 }
1325
1326 fn update(&self, node: &mut Self::Node) {
1327 node.identity = self.identity;
1328 node.offset_source = self.offset_source.clone();
1329 node.overscroll = self.overscroll.clone();
1330 }
1331
1332 fn capabilities(&self) -> NodeCapabilities {
1333 NodeCapabilities::LAYOUT
1334 }
1335}
1336
1337impl Modifier {
1338 pub fn horizontal_scroll(self, state: ScrollState, reverse_scrolling: bool) -> Self {
1356 self.then(scroll_impl(state, false, reverse_scrolling, None))
1357 }
1358
1359 pub fn vertical_scroll(self, state: ScrollState, reverse_scrolling: bool) -> Self {
1368 self.then(scroll_impl(state, true, reverse_scrolling, None))
1369 }
1370}
1371
1372fn scroll_impl(
1373 state: ScrollState,
1374 is_vertical: bool,
1375 reverse_scrolling: bool,
1376 guard: Option<Rc<dyn Fn() -> bool>>,
1377) -> Modifier {
1378 let gesture_state = Rc::new(RefCell::new(ScrollGestureState::default()));
1379 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::ScrollState {
1380 state_id: state.id(),
1381 is_vertical,
1382 reverse_scrolling,
1383 });
1384
1385 let pointer_input = drag_gesture_input(
1386 (state.id(), is_vertical),
1387 DragGesture {
1388 target: state,
1389 gesture_state,
1390 is_vertical,
1391 reverse_input: false,
1392 motion_context: motion_context.clone(),
1393 guard,
1394 },
1395 );
1396
1397 let overscroll = motion_context.overscroll();
1398 let element = ScrollElement::new(state, overscroll.clone(), is_vertical, reverse_scrolling);
1399 let layout_modifier =
1400 Modifier::with_element(element).with_inspector_metadata(inspector_metadata(
1401 if is_vertical {
1402 "verticalScroll"
1403 } else {
1404 "horizontalScroll"
1405 },
1406 move |info| {
1407 info.add_property("isVertical", is_vertical.to_string());
1408 info.add_property("reverseScrolling", reverse_scrolling.to_string());
1409 },
1410 ));
1411 let motion_modifier =
1412 Modifier::with_element(MotionContextAnimatedElement::new(motion_context.clone()));
1413 let translated_content_modifier = Modifier::with_element(TranslatedContentContextElement::new(
1414 state.id() as usize,
1415 TranslatedContentOffsetSource::LayoutContentOffset,
1416 overscroll,
1417 ));
1418
1419 pointer_input
1420 .then(motion_modifier)
1421 .then(translated_content_modifier)
1422 .then(layout_modifier)
1423 .clip_to_bounds()
1424}
1425
1426use cranpose_foundation::lazy::LazyListState;
1427
1428impl Modifier {
1429 pub fn lazy_vertical_scroll(self, state: LazyListState, reverse_scrolling: bool) -> Self {
1440 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::LazyList {
1441 state_identity: state.inner_ptr() as usize,
1442 is_vertical: true,
1443 reverse_scrolling,
1444 });
1445 self.lazy_vertical_scroll_with_context(state, reverse_scrolling, motion_context)
1446 }
1447
1448 pub(crate) fn lazy_vertical_scroll_with_context(
1449 self,
1450 state: LazyListState,
1451 reverse_scrolling: bool,
1452 motion_context: ScrollMotionContext,
1453 ) -> Self {
1454 self.then(lazy_scroll_impl(
1455 state,
1456 true,
1457 reverse_scrolling,
1458 motion_context,
1459 ))
1460 }
1461
1462 pub(crate) fn lazy_horizontal_scroll_with_context(
1463 self,
1464 state: LazyListState,
1465 reverse_scrolling: bool,
1466 motion_context: ScrollMotionContext,
1467 ) -> Self {
1468 self.then(lazy_scroll_impl(
1469 state,
1470 false,
1471 reverse_scrolling,
1472 motion_context,
1473 ))
1474 }
1475}
1476
1477fn lazy_scroll_impl(
1478 state: LazyListState,
1479 is_vertical: bool,
1480 reverse_scrolling: bool,
1481 motion_context: ScrollMotionContext,
1482) -> Modifier {
1483 let gesture_state = Rc::new(RefCell::new(ScrollGestureState::default()));
1484 let list_state = state;
1485 let state_id = state.inner_ptr() as usize;
1486 let key = (state_id, is_vertical, reverse_scrolling);
1487 let overscroll = motion_context.overscroll();
1488 let translated_content_modifier = Modifier::with_element(TranslatedContentContextElement::new(
1489 state_id,
1490 TranslatedContentOffsetSource::LazyList {
1491 state,
1492 is_vertical,
1493 reverse_scrolling,
1494 },
1495 overscroll,
1496 ));
1497
1498 Modifier::with_element(MotionContextAnimatedElement::new(motion_context.clone()))
1499 .then(translated_content_modifier)
1500 .then(drag_gesture_input(
1501 key,
1502 DragGesture {
1503 target: list_state,
1504 gesture_state,
1505 is_vertical,
1506 reverse_input: reverse_scrolling,
1507 motion_context,
1508 guard: None,
1509 },
1510 ))
1511}
1512
1513impl Modifier {
1514 pub fn draggable(self, axis: Axis, state: DraggableState) -> Self {
1526 self.then(draggable_impl(axis, state, None))
1527 }
1528
1529 pub fn draggable_guarded(
1535 self,
1536 axis: Axis,
1537 state: DraggableState,
1538 guard: impl Fn() -> bool + 'static,
1539 ) -> Self {
1540 self.then(draggable_impl(axis, state, Some(Rc::new(guard))))
1541 }
1542}
1543
1544fn draggable_impl(
1545 axis: Axis,
1546 state: DraggableState,
1547 guard: Option<Rc<dyn Fn() -> bool>>,
1548) -> Modifier {
1549 let is_vertical = axis.is_vertical();
1550 let identity = state.identity();
1551 drag_gesture_input(
1552 (identity, is_vertical),
1553 DragGesture {
1554 target: state,
1555 gesture_state: Rc::new(RefCell::new(ScrollGestureState::default())),
1556 is_vertical,
1557 reverse_input: false,
1558 motion_context: scroll_motion_context_for_key(ScrollMotionContextKey::Draggable {
1559 state_identity: identity,
1560 is_vertical,
1561 }),
1562 guard,
1563 },
1564 )
1565}