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 lazy::LazyListState, 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 bring_into_view_spec::current_bring_into_view_spec,
18 draggable::{DraggableState, ScrollableState},
19 fling_animation::{
20 FlingAnimation, MIN_FLING_VELOCITY, SettleAnimation, SpringParams, fling_rest_position,
21 },
22 nested_scroll::NestedScrollSource,
23 render_state::schedule_modifier_slices_repass,
24 scroll::{
25 ScrollElement, ScrollMotionContext, ScrollMotionContextKey, ScrollSettlePolicy,
26 ScrollState, scroll_motion_context_for_key,
27 },
28 scroll_semantics::lazy_scroll_semantics,
29};
30
31#[cfg(feature = "test-helpers")]
34pub fn last_fling_velocity() -> f32 {
35 crate::render_state::debug_last_fling_velocity()
36}
37
38#[cfg(feature = "test-helpers")]
40pub fn reset_last_fling_velocity() {
41 crate::render_state::debug_reset_last_fling_velocity();
42}
43
44#[inline]
45fn set_last_fling_velocity(velocity: f32) {
46 crate::render_state::record_last_fling_velocity(velocity);
47}
48
49struct ScrollGestureState {
50 disposed: bool,
51 drag_down_position: Option<Point>,
52
53 last_position: Option<Point>,
54
55 is_dragging: bool,
56
57 interrupted_animation: bool,
60
61 axis_locked_out: bool,
62
63 velocity_tracker: VelocityTracker1D,
64
65 gesture_start_time: Option<Instant>,
66
67 gesture_start_event_time_ms: Option<i64>,
68
69 last_velocity_sample_ms: Option<i64>,
70
71 fling_animation: Option<FlingAnimation>,
72
73 is_overscrolling: bool,
74
75 settle_animation: Option<SettleAnimation>,
76
77 wheel_settle_watcher: Option<WheelSettleWatcher>,
78}
79
80impl Default for ScrollGestureState {
81 fn default() -> Self {
82 Self {
83 disposed: false,
84 drag_down_position: None,
85 last_position: None,
86 is_dragging: false,
87 interrupted_animation: false,
88 axis_locked_out: false,
89 velocity_tracker: VelocityTracker1D::new(),
90 gesture_start_time: None,
91 gesture_start_event_time_ms: None,
92 last_velocity_sample_ms: None,
93 fling_animation: None,
94 is_overscrolling: false,
95 settle_animation: None,
96 wheel_settle_watcher: None,
97 }
98 }
99}
100
101impl ScrollGestureState {
102 fn is_active(&self) -> bool {
103 self.is_dragging
104 || self.is_overscrolling
105 || self
106 .fling_animation
107 .as_ref()
108 .is_some_and(FlingAnimation::is_running)
109 || self
110 .settle_animation
111 .as_ref()
112 .is_some_and(SettleAnimation::is_running)
113 || self
114 .wheel_settle_watcher
115 .as_ref()
116 .is_some_and(|watcher| watcher.is_running.get())
117 }
118
119 fn release_velocity(&mut self, release_time_ms: Option<i64>) -> f32 {
122 let Some(start) = self.gesture_start_time else {
123 return 0.0;
124 };
125 let release_sample_ms = release_time_ms
126 .zip(self.gesture_start_event_time_ms)
127 .map_or_else(
128 || start.elapsed().as_millis() as i64,
129 |(release_ms, start_ms)| release_ms - start_ms,
130 );
131 let rested_before_release = self
132 .last_velocity_sample_ms
133 .is_some_and(|last_sample_ms| release_sample_ms - last_sample_ms > ASSUME_STOPPED_MS);
134 if rested_before_release {
135 return 0.0;
136 }
137 self.velocity_tracker
138 .calculate_velocity_with_max(MAX_FLING_VELOCITY)
139 }
140}
141
142#[inline]
143fn delta_along(from: Point, to: Point, axis: Axis) -> f32 {
144 match axis {
145 Axis::Horizontal => to.x - from.x,
146 Axis::Vertical => to.y - from.y,
147 }
148}
149
150trait ScrollTarget: Clone {
151 fn apply_delta(&self, delta: f32) -> f32;
152
153 fn apply_wheel_delta(&self, delta: f32) -> f32 {
154 self.apply_delta(delta)
155 }
156
157 fn apply_fling_delta(&self, delta: f32) -> f32;
158
159 fn invalidate(&self);
160
161 fn current_offset(&self) -> f32;
162
163 fn can_scroll(&self) -> bool {
164 true
165 }
166
167 fn can_consume(&self, gesture_delta: f32) -> bool {
168 let _ = gesture_delta;
169 self.can_scroll()
170 }
171
172 fn settle_policy(&self) -> Option<ScrollSettlePolicy> {
173 None
174 }
175
176 fn allows_fling(&self) -> bool {
177 true
178 }
179
180 fn set_dragging(&self, dragging: bool) {
181 let _ = dragging;
182 }
183
184 fn stop_animation(&self) -> bool {
187 false
188 }
189
190 fn pre_fling(&self, velocity: f32) -> f32 {
193 let _ = velocity;
194 0.0
195 }
196}
197
198#[derive(Clone)]
201struct NestedTarget<S> {
202 target: S,
203 parent: crate::nested_scroll::NestedScrollParent,
204 axis: Axis,
205}
206
207impl<S: ScrollTarget> ScrollTarget for NestedTarget<S> {
208 fn apply_delta(&self, delta: f32) -> f32 {
209 self.parent
210 .dispatch(self.axis, delta, NestedScrollSource::Drag, |left| {
211 self.target.apply_delta(left)
212 })
213 }
214
215 fn apply_wheel_delta(&self, delta: f32) -> f32 {
216 self.parent
217 .dispatch(self.axis, delta, NestedScrollSource::Wheel, |left| {
218 self.target.apply_wheel_delta(left)
219 })
220 }
221
222 fn apply_fling_delta(&self, delta: f32) -> f32 {
224 -self
225 .parent
226 .dispatch(self.axis, -delta, NestedScrollSource::Fling, |left| {
227 -self.target.apply_fling_delta(-left)
228 })
229 }
230
231 fn invalidate(&self) {
232 self.target.invalidate();
233 }
234
235 fn current_offset(&self) -> f32 {
236 self.target.current_offset()
237 }
238
239 fn can_scroll(&self) -> bool {
240 self.target.can_scroll()
241 }
242
243 fn can_consume(&self, gesture_delta: f32) -> bool {
244 self.target.can_consume(gesture_delta)
245 }
246
247 fn settle_policy(&self) -> Option<ScrollSettlePolicy> {
248 self.target.settle_policy()
249 }
250
251 fn allows_fling(&self) -> bool {
252 self.target.allows_fling()
253 }
254
255 fn set_dragging(&self, dragging: bool) {
256 self.target.set_dragging(dragging);
257 }
258
259 fn stop_animation(&self) -> bool {
260 self.target.stop_animation()
261 }
262
263 fn pre_fling(&self, velocity: f32) -> f32 {
264 self.parent.pre_fling(self.axis, velocity)
265 }
266}
267
268impl ScrollTarget for ScrollState {
269 fn apply_delta(&self, delta: f32) -> f32 {
270 -self.dispatch_raw_delta(-delta)
271 }
272
273 fn apply_fling_delta(&self, delta: f32) -> f32 {
274 self.dispatch_raw_delta(delta)
275 }
276
277 fn invalidate(&self) {}
278
279 fn current_offset(&self) -> f32 {
280 self.value()
281 }
282
283 fn can_scroll(&self) -> bool {
284 self.max_value() > 0.0
285 }
286
287 fn can_consume(&self, gesture_delta: f32) -> bool {
288 let raw = -gesture_delta;
289 if raw > 0.0 {
290 self.value_non_reactive() < self.max_value()
291 } else {
292 self.value_non_reactive() > 0.0
293 }
294 }
295
296 fn settle_policy(&self) -> Option<ScrollSettlePolicy> {
297 Self::settle_policy(self)
298 }
299}
300
301impl ScrollTarget for LazyListState {
302 fn apply_delta(&self, delta: f32) -> f32 {
303 self.dispatch_scroll_delta(delta)
304 }
305
306 fn apply_wheel_delta(&self, delta: f32) -> f32 {
307 if delta.abs() <= 0.001 {
308 0.0
309 } else {
310 self.dispatch_scroll_delta(delta)
311 }
312 }
313
314 fn apply_fling_delta(&self, delta: f32) -> f32 {
315 -self.dispatch_scroll_delta(-delta)
316 }
317
318 fn invalidate(&self) {}
319
320 fn current_offset(&self) -> f32 {
321 self.first_visible_item_scroll_offset()
322 }
323
324 fn can_scroll(&self) -> bool {
325 self.total_items_count() == 0
326 || self.can_scroll_forward_non_reactive()
327 || self.can_scroll_backward_non_reactive()
328 }
329
330 fn can_consume(&self, gesture_delta: f32) -> bool {
331 if self.total_items_count() == 0 {
332 return true;
333 }
334 if gesture_delta < 0.0 {
335 self.can_scroll_forward_non_reactive()
336 } else {
337 self.can_scroll_backward_non_reactive()
338 }
339 }
340
341 fn settle_policy(&self) -> Option<ScrollSettlePolicy> {
342 Self::settle_policy(self)
343 }
344
345 fn set_dragging(&self, dragging: bool) {
346 self.set_dragged(dragging);
347 }
348
349 fn stop_animation(&self) -> bool {
350 self.stop_scroll_animation()
351 }
352}
353
354struct DragGesture<S: ScrollTarget> {
355 target: S,
356 axis: Axis,
357 reverse_input: bool,
358 motion_context: ScrollMotionContext,
359 guard: Option<Rc<dyn Fn() -> bool>>,
360}
361
362struct ScrollGestureLifetime {
363 gesture_state: Rc<RefCell<ScrollGestureState>>,
364 motion_context: ScrollMotionContext,
365}
366
367impl Drop for ScrollGestureLifetime {
368 fn drop(&mut self) {
369 let state = std::mem::replace(
370 &mut *self.gesture_state.borrow_mut(),
371 ScrollGestureState {
372 disposed: true,
373 ..ScrollGestureState::default()
374 },
375 );
376 let was_active = state.is_active();
377 if let Some(fling) = state.fling_animation {
378 fling.cancel();
379 }
380 if let Some(settle) = state.settle_animation {
381 settle.cancel();
382 }
383 if let Some(watcher) = state.wheel_settle_watcher {
384 watcher.cancel();
385 }
386 if was_active {
387 self.motion_context.set_active(false);
388 }
389 }
390}
391
392fn nested_drag_gesture_input<K, S>(key: K, gesture: DragGesture<S>) -> Modifier
395where
396 K: std::hash::Hash + 'static,
397 S: ScrollTarget + 'static,
398{
399 let Some(parent) = crate::nested_scroll::current_nested_scroll_parent() else {
400 return drag_gesture_input(key, gesture);
401 };
402 let DragGesture {
403 target,
404 axis,
405 reverse_input,
406 motion_context,
407 guard,
408 } = gesture;
409 let identity = parent.identity();
410 drag_gesture_input(
411 (key, identity),
412 DragGesture {
413 target: NestedTarget {
414 target,
415 parent,
416 axis,
417 },
418 axis,
419 reverse_input,
420 motion_context,
421 guard,
422 },
423 )
424}
425
426fn drag_gesture_input<K, S>(key: K, gesture: DragGesture<S>) -> Modifier
427where
428 K: std::hash::Hash + 'static,
429 S: ScrollTarget + 'static,
430{
431 let DragGesture {
432 target,
433 axis,
434 reverse_input,
435 motion_context,
436 guard,
437 } = gesture;
438
439 Modifier::empty().pointer_input(key, move |scope| {
440 let gesture_state = Rc::new(RefCell::new(ScrollGestureState::default()));
441 let detector = ScrollGestureDetector::new(
442 Rc::clone(&gesture_state),
443 target.clone(),
444 axis,
445 reverse_input,
446 motion_context.overscroll(),
447 motion_context.clone(),
448 );
449 let guard = guard.clone();
450 let lifetime = ScrollGestureLifetime {
451 gesture_state: Rc::clone(&gesture_state),
452 motion_context: motion_context.clone(),
453 };
454
455 async move {
456 let _lifetime = lifetime;
457 scope
458 .await_pointer_event_scope(|await_scope| async move {
459 loop {
460 let event = await_scope.await_pointer_event().await;
461
462 if !event.id.is_primary() {
463 continue;
464 }
465
466 if event.is_consumed() {
467 if matches!(
468 event.kind,
469 PointerEventKind::Down
470 | PointerEventKind::Move
471 | PointerEventKind::Up
472 | PointerEventKind::Cancel
473 ) {
474 detector.on_cancel();
475 }
476 continue;
477 }
478
479 if let Some(ref guard) = guard
480 && !guard()
481 {
482 if matches!(event.kind, PointerEventKind::Up | PointerEventKind::Cancel)
483 {
484 detector.on_cancel();
485 }
486 continue;
487 }
488
489 let should_consume = match event.kind {
490 PointerEventKind::Down => {
491 detector.on_down(event.position, event.time_ms)
492 }
493 PointerEventKind::Move => detector.on_move(
494 event.position,
495 event.buttons,
496 event.time_ms,
497 &event,
498 ),
499 PointerEventKind::Up => detector.on_up(event.time_ms),
500 PointerEventKind::Cancel => detector.on_cancel(),
501 PointerEventKind::Scroll => detector.on_scroll(
502 if axis.is_vertical() {
503 event.scroll_delta.y
504 } else {
505 event.scroll_delta.x
506 },
507 &event,
508 ),
509 PointerEventKind::Zoom
510 | PointerEventKind::RotaryScrollPre
511 | PointerEventKind::RotaryScroll
512 | PointerEventKind::Enter
513 | PointerEventKind::Exit
514 | PointerEventKind::ButtonDown(_)
515 | PointerEventKind::ButtonUp(_) => false,
516 };
517
518 if should_consume {
519 event.consume();
520 }
521 }
522 })
523 .await;
524 }
525 })
526}
527
528impl ScrollTarget for DraggableState {
529 fn apply_delta(&self, delta: f32) -> f32 {
530 self.drag_by(delta);
531 delta
532 }
533
534 fn apply_fling_delta(&self, delta: f32) -> f32 {
535 self.drag_by(delta);
536 delta
537 }
538
539 fn invalidate(&self) {}
540
541 fn current_offset(&self) -> f32 {
542 self.offset()
543 }
544
545 fn allows_fling(&self) -> bool {
546 false
547 }
548
549 fn set_dragging(&self, dragging: bool) {
550 Self::set_dragging(self, dragging);
551 }
552}
553
554impl ScrollTarget for ScrollableState {
555 fn apply_delta(&self, delta: f32) -> f32 {
556 self.dispatch_raw_delta(delta)
557 }
558
559 fn apply_fling_delta(&self, delta: f32) -> f32 {
560 -self.dispatch_raw_delta(-delta)
561 }
562
563 fn invalidate(&self) {}
564
565 fn current_offset(&self) -> f32 {
566 -self.offset()
567 }
568
569 fn set_dragging(&self, dragging: bool) {
570 Self::set_dragging(self, dragging);
571 }
572}
573
574const WHEEL_SETTLE_IDLE_NANOS: u64 = 180_000_000;
575
576struct WheelSettleWatcher {
577 is_running: Rc<Cell<bool>>,
578 registration: Rc<RefCell<Option<FrameCallbackRegistration>>>,
579}
580
581impl WheelSettleWatcher {
582 fn cancel(&self) {
583 self.is_running.set(false);
584 self.registration.borrow_mut().take();
585 }
586}
587
588struct ScrollGestureDetector<S: ScrollTarget> {
589 gesture_state: Rc<RefCell<ScrollGestureState>>,
590
591 scroll_target: S,
592
593 axis: Axis,
594
595 reverse_scrolling: bool,
596
597 overscroll: crate::scroll::OverscrollEffect,
598
599 motion_context: ScrollMotionContext,
600}
601
602impl<S: ScrollTarget + 'static> ScrollGestureDetector<S> {
603 fn new(
604 gesture_state: Rc<RefCell<ScrollGestureState>>,
605 scroll_target: S,
606 axis: Axis,
607 reverse_scrolling: bool,
608 overscroll: crate::scroll::OverscrollEffect,
609 motion_context: ScrollMotionContext,
610 ) -> Self {
611 Self {
612 gesture_state,
613 scroll_target,
614 axis,
615 reverse_scrolling,
616 overscroll,
617 motion_context,
618 }
619 }
620
621 fn on_down(&self, position: Point, time_ms: Option<i64>) -> bool {
622 let interrupted = self.scroll_target.stop_animation();
623 let mut gs = self.gesture_state.borrow_mut();
624 gs.interrupted_animation = interrupted;
625
626 if let Some(fling) = gs.fling_animation.take() {
627 fling.cancel();
628 }
629 if let Some(settle) = gs.settle_animation.take() {
630 settle.cancel();
631 }
632 if let Some(watcher) = gs.wheel_settle_watcher.take() {
633 watcher.cancel();
634 }
635 self.motion_context.set_active(false);
636
637 gs.drag_down_position = Some(position);
638 gs.last_position = Some(position);
639 gs.is_dragging = false;
640 gs.axis_locked_out = false;
641 gs.velocity_tracker.reset();
642 gs.gesture_start_time = Some(Instant::now());
643 gs.gesture_start_event_time_ms = time_ms;
644 gs.is_overscrolling = self.overscroll.offset().abs() > 0.001;
645
646 let pos = if self.axis.is_vertical() {
647 position.y
648 } else {
649 position.x
650 };
651 gs.velocity_tracker.add_data_point(0, pos);
652 gs.last_velocity_sample_ms = Some(0);
653
654 false
655 }
656
657 fn on_move(
658 &self,
659 position: Point,
660 buttons: PointerButtons,
661 time_ms: Option<i64>,
662 event: &PointerEvent,
663 ) -> bool {
664 let mut gs = self.gesture_state.borrow_mut();
665
666 if !buttons.contains(PointerButton::Primary) && gs.drag_down_position.is_some() {
667 if gs.is_dragging {
668 self.scroll_target.set_dragging(false);
669 }
670 gs.drag_down_position = None;
671 gs.last_position = None;
672 gs.is_dragging = false;
673 gs.axis_locked_out = false;
674 gs.gesture_start_time = None;
675 gs.gesture_start_event_time_ms = None;
676 gs.last_velocity_sample_ms = None;
677 gs.velocity_tracker.reset();
678 self.motion_context.set_active(false);
679 return false;
680 }
681
682 let Some(down_pos) = gs.drag_down_position else {
683 return false;
684 };
685
686 let Some(last_pos) = gs.last_position else {
687 gs.last_position = Some(position);
688 return false;
689 };
690
691 let incremental_delta = delta_along(last_pos, position, self.axis);
692
693 let mut edge_candidate = false;
694 if !gs.is_dragging && !gs.axis_locked_out {
695 let signed_main_delta = delta_along(down_pos, position, self.axis);
696 let main_delta = signed_main_delta.abs();
697 let cross_delta = delta_along(down_pos, position, self.axis.cross_axis()).abs();
698 if main_delta > DRAG_THRESHOLD && main_delta >= cross_delta {
699 if self.scroll_target.can_consume(signed_main_delta) {
700 gs.is_dragging = true;
701 self.scroll_target.set_dragging(true);
702 self.motion_context.set_active(true);
703 } else if self.scroll_target.can_scroll() {
704 edge_candidate = true;
705 }
706 } else if cross_delta > DRAG_THRESHOLD && cross_delta > main_delta {
707 gs.axis_locked_out = true;
708 }
709 }
710
711 gs.last_position = Some(position);
712
713 let pos = if self.axis.is_vertical() {
714 position.y
715 } else {
716 position.x
717 };
718 let event_sample_ms = gs
719 .gesture_start_event_time_ms
720 .zip(time_ms)
721 .map(|(start_ms, now_ms)| now_ms - start_ms);
722 let sample_ms = if let Some(event_sample_ms) = event_sample_ms {
723 Some(match gs.last_velocity_sample_ms {
724 Some(last_sample_ms) => event_sample_ms.max(last_sample_ms),
725 None => event_sample_ms.max(0),
726 })
727 } else if let Some(start_time) = gs.gesture_start_time {
728 let elapsed_ms = start_time.elapsed().as_millis() as i64;
729 Some(match gs.last_velocity_sample_ms {
730 Some(last_sample_ms) => {
731 let mut sample_ms = if elapsed_ms <= last_sample_ms {
732 last_sample_ms + 1
733 } else {
734 elapsed_ms
735 };
736 if sample_ms - last_sample_ms > ASSUME_STOPPED_MS {
737 sample_ms = last_sample_ms + ASSUME_STOPPED_MS;
738 }
739 sample_ms
740 }
741 None => elapsed_ms,
742 })
743 } else {
744 None
745 };
746 if let Some(sample_ms) = sample_ms {
747 log::trace!(
748 target: "cranpose::velocity",
749 "sample t={sample_ms}ms pos={pos:.2} event_time={time_ms:?}"
750 );
751 gs.velocity_tracker.add_data_point(sample_ms, pos);
752 gs.last_velocity_sample_ms = Some(sample_ms);
753 }
754
755 if gs.is_dragging {
756 drop(gs);
757 let delta = if self.reverse_scrolling {
758 -incremental_delta
759 } else {
760 incremental_delta
761 };
762 let overscroll = self.overscroll.clone();
763 overscroll.apply_to_scroll(delta, |delta| self.scroll_target.apply_delta(delta));
764 self.scroll_target.invalidate();
765 true
766 } else if gs.is_overscrolling {
767 drop(gs);
768 let delta = if self.reverse_scrolling {
769 -incremental_delta
770 } else {
771 incremental_delta
772 };
773 self.apply_overscroll_delta(delta)
774 } else if edge_candidate {
775 drop(gs);
776 let delta = if self.reverse_scrolling {
777 -incremental_delta
778 } else {
779 incremental_delta
780 };
781 let detector = self.clone_for_watcher();
782 event.defer_post_dispatch_action(move || detector.apply_overscroll_candidate(delta));
783 false
784 } else {
785 false
786 }
787 }
788
789 fn apply_overscroll_delta(&self, delta: f32) -> bool {
790 self.motion_context.set_active(true);
791 let overscroll = self.overscroll.clone();
792 let target_consumed = Cell::new(0.0);
793 let before_overscroll = overscroll.offset();
794 overscroll.apply_to_scroll(delta, |delta| {
795 let consumed = self.scroll_target.apply_delta(delta);
796 target_consumed.set(consumed);
797 consumed
798 });
799 self.scroll_target.invalidate();
800 let consumed = target_consumed.get().abs() > 0.001
801 || (overscroll.offset() - before_overscroll).abs() > 0.001;
802 if !consumed {
803 self.motion_context.set_active(false);
804 }
805 consumed
806 }
807
808 fn apply_overscroll_candidate(&self, delta: f32) -> bool {
809 if self.gesture_state.borrow().disposed {
810 return false;
811 }
812 let consumed = self.apply_overscroll_delta(delta);
813 if consumed {
814 self.gesture_state.borrow_mut().is_overscrolling = true;
815 }
816 consumed
817 }
818
819 fn finish_gesture(&self, allow_fling: bool, release_time_ms: Option<i64>) -> bool {
820 let (was_dragging, interrupted, gesture_owned, velocity, start_fling) = {
821 let mut gs = self.gesture_state.borrow_mut();
822 let was_dragging = gs.is_dragging;
823 let interrupted = std::mem::take(&mut gs.interrupted_animation);
824 let gesture_owned = was_dragging || gs.is_overscrolling;
825 let velocity = if allow_fling && gesture_owned {
826 gs.release_velocity(release_time_ms)
827 } else {
828 0.0
829 };
830
831 let start_fling = allow_fling && was_dragging && velocity.abs() > MIN_FLING_VELOCITY;
832 if start_fling && let Some(fling) = gs.fling_animation.take() {
834 fling.cancel();
835 }
836
837 if was_dragging {
838 self.scroll_target.set_dragging(false);
839 }
840 gs.drag_down_position = None;
841 gs.last_position = None;
842 gs.is_dragging = false;
843 gs.is_overscrolling = false;
844 gs.axis_locked_out = false;
845 gs.gesture_start_time = None;
846 gs.gesture_start_event_time_ms = None;
847 gs.last_velocity_sample_ms = None;
848
849 (
850 was_dragging,
851 interrupted,
852 gesture_owned,
853 velocity,
854 start_fling,
855 )
856 };
857
858 if allow_fling && gesture_owned {
859 log::debug!(
860 target: "cranpose::velocity",
861 "gesture finished: fling velocity={velocity:.2} dp/s start_fling={start_fling}"
862 );
863 set_last_fling_velocity(velocity);
864 }
865
866 let adjusted_velocity = if self.reverse_scrolling {
867 -velocity
868 } else {
869 velocity
870 };
871 let adjusted_velocity = if gesture_owned {
874 adjusted_velocity - self.scroll_target.pre_fling(adjusted_velocity)
875 } else {
876 adjusted_velocity
877 };
878 let start_fling = start_fling && adjusted_velocity.abs() > MIN_FLING_VELOCITY;
879 let fling_velocity = -adjusted_velocity;
880 let has_overscroll = self.overscroll.offset().abs() > 0.001;
881
882 let settle_target = (was_dragging || interrupted)
883 .then(|| self.settle_target(start_fling, fling_velocity))
884 .flatten();
885
886 if has_overscroll {
887 self.start_overscroll_settle(-fling_velocity);
888 } else if let Some(target) = settle_target {
889 self.start_settle_animation(target, fling_velocity);
890 } else if start_fling {
891 self.start_fling_animation(fling_velocity);
892 } else {
893 self.motion_context.set_active(false);
894 }
895
896 gesture_owned
897 }
898
899 fn settle_target(&self, start_fling: bool, fling_velocity: f32) -> Option<f32> {
902 let policy = self.scroll_target.settle_policy()?;
903 let current = self.scroll_target.current_offset();
904 let proposed = if start_fling {
905 fling_rest_position(current, fling_velocity)
906 } else {
907 current
908 };
909 let target = policy(proposed, fling_velocity);
910 ((target - proposed).abs() > 0.5).then_some(target)
911 }
912
913 fn start_fling_animation(&self, fling_velocity: f32) {
914 let Some(runtime) = current_runtime_handle() else {
915 self.motion_context.set_active(false);
916 return;
917 };
918 self.motion_context.set_active(true);
919 let scroll_target = self.scroll_target.clone();
920 let fling = FlingAnimation::new(runtime);
921 let motion_context = self.motion_context.clone();
922 let initial_value = scroll_target.current_offset();
923 let scroll_target_for_fling = scroll_target.clone();
924 let detector_for_end = self.clone_for_watcher();
925 let overscroll_for_fling = self.overscroll.clone();
926
927 fling.start_fling(
928 initial_value,
929 fling_velocity,
930 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.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) {
957 let Some(runtime) = current_runtime_handle() else {
958 self.motion_context.set_active(false);
959 return;
960 };
961 self.motion_context.set_active(true);
962 let settle = SettleAnimation::new(runtime, SpringParams::SETTLE_POLICY);
963 let scroll_target_for_settle = self.scroll_target.clone();
964 let scroll_target_for_end = self.scroll_target.clone();
965 let detector_for_end = self.clone_for_watcher();
966 let motion_context = self.motion_context.clone();
967 settle.start_settle(
968 self.scroll_target.current_offset(),
969 initial_velocity,
970 target,
971 move |delta| {
972 let consumed = scroll_target_for_settle.apply_fling_delta(delta);
973 scroll_target_for_settle.invalidate();
974 consumed
975 },
976 move |_| {
977 scroll_target_for_end.invalidate();
978 detector_for_end.update_motion_active(&motion_context);
979 },
980 );
981 let mut gs = self.gesture_state.borrow_mut();
982 gs.settle_animation = Some(settle);
983 }
984
985 fn start_overscroll_settle(&self, initial_velocity: f32) {
986 let Some(runtime) = current_runtime_handle() else {
987 let current = self.overscroll.offset();
988 self.overscroll.apply_settle_delta(-current);
989 self.motion_context.set_active(false);
990 return;
991 };
992 let settle = SettleAnimation::new(runtime, SpringParams::OVERSCROLL_BOUNCE);
993 let overscroll_for_settle = self.overscroll.clone();
994 let overscroll_for_end = self.overscroll.clone();
995 let detector_for_end = self.clone_for_watcher();
996 let motion_context = self.motion_context.clone();
997 let initial = self.overscroll.offset();
998 settle.start_settle(
999 initial,
1000 initial_velocity,
1001 0.0,
1002 move |delta| overscroll_for_settle.apply_settle_delta(delta),
1003 move |end| {
1004 let current = overscroll_for_end.offset();
1005 overscroll_for_end.apply_settle_delta(-current);
1006 if end.hit_boundary && end.velocity.abs() > MIN_FLING_VELOCITY {
1007 detector_for_end.start_fling_animation(-end.velocity);
1008 } else {
1009 detector_for_end.update_motion_active(&motion_context);
1010 }
1011 },
1012 );
1013 let mut gs = self.gesture_state.borrow_mut();
1014 gs.is_overscrolling = false;
1015 gs.settle_animation = Some(settle);
1016 }
1017
1018 fn update_motion_active(&self, motion_context: &ScrollMotionContext) {
1019 let running = self.gesture_state.borrow().is_active();
1020 if !running {
1021 motion_context.set_active(false);
1022 }
1023 }
1024
1025 fn on_up(&self, time_ms: Option<i64>) -> bool {
1026 self.finish_gesture(self.scroll_target.allows_fling(), time_ms)
1027 }
1028
1029 fn on_cancel(&self) -> bool {
1030 self.finish_gesture(false, None)
1031 }
1032
1033 fn on_scroll(&self, axis_delta: f32, event: &PointerEvent) -> bool {
1034 if axis_delta.abs() <= f32::EPSILON {
1035 return false;
1036 }
1037
1038 let delta = if self.reverse_scrolling {
1039 -axis_delta
1040 } else {
1041 axis_delta
1042 };
1043 if !self.scroll_target.can_consume(delta) && self.scroll_target.can_scroll() {
1044 let detector = self.clone_for_watcher();
1045 event.defer_post_dispatch_action(move || detector.apply_wheel_delta(delta));
1046 return false;
1047 }
1048 self.apply_wheel_delta(delta)
1049 }
1050
1051 fn apply_wheel_delta(&self, delta: f32) -> bool {
1052 {
1053 let mut gs = self.gesture_state.borrow_mut();
1054 if gs.disposed {
1055 return false;
1056 }
1057 if let Some(fling) = gs.fling_animation.take() {
1058 fling.cancel();
1059 }
1060 if let Some(settle) = gs.settle_animation.take() {
1061 settle.cancel();
1062 }
1063 gs.drag_down_position = None;
1064 gs.last_position = None;
1065 gs.is_dragging = false;
1066 gs.axis_locked_out = false;
1067 gs.gesture_start_time = None;
1068 gs.gesture_start_event_time_ms = None;
1069 gs.last_velocity_sample_ms = None;
1070 gs.velocity_tracker.reset();
1071 }
1072
1073 self.motion_context.activate_for_current_frame();
1074 let overscroll = self.overscroll.clone();
1075 let consumed =
1076 overscroll.apply_to_scroll(delta, |delta| self.scroll_target.apply_wheel_delta(delta));
1077 let overscroll_active = self.overscroll.offset().abs() > 0.001;
1078 if consumed.abs() > 0.001 {
1079 self.scroll_target.invalidate();
1080 }
1081 if consumed.abs() > 0.001 || overscroll_active {
1082 self.ensure_wheel_settle_watcher();
1083 true
1084 } else {
1085 false
1086 }
1087 }
1088
1089 fn ensure_wheel_settle_watcher(&self) {
1090 if self.scroll_target.settle_policy().is_none() && self.overscroll.offset().abs() <= 0.001 {
1091 return;
1092 }
1093 {
1094 let gs = self.gesture_state.borrow();
1095 if gs
1096 .wheel_settle_watcher
1097 .as_ref()
1098 .is_some_and(|watcher| watcher.is_running.get())
1099 {
1100 return;
1101 }
1102 }
1103 let Some(runtime) = current_runtime_handle() else {
1104 return;
1105 };
1106
1107 let is_running = Rc::new(Cell::new(true));
1108 let registration = Rc::new(RefCell::new(None));
1109
1110 struct WheelSettleLoop<S: ScrollTarget> {
1111 detector: ScrollGestureDetector<S>,
1112 gesture_state: Rc<RefCell<ScrollGestureState>>,
1113 frame_clock: cranpose_core::internal::FrameClock,
1114 is_running: Rc<Cell<bool>>,
1115 registration: Rc<RefCell<Option<FrameCallbackRegistration>>>,
1116 last_offset: Rc<Cell<f32>>,
1117 idle_nanos: Rc<Cell<u64>>,
1118 last_frame: Rc<Cell<Option<u64>>>,
1119 }
1120
1121 impl<S: ScrollTarget + 'static> WheelSettleLoop<S> {
1122 fn next(&self) -> Self {
1123 Self {
1124 detector: self.detector.clone_for_watcher(),
1125 gesture_state: Rc::clone(&self.gesture_state),
1126 frame_clock: self.frame_clock.clone(),
1127 is_running: Rc::clone(&self.is_running),
1128 registration: Rc::clone(&self.registration),
1129 last_offset: Rc::clone(&self.last_offset),
1130 idle_nanos: Rc::clone(&self.idle_nanos),
1131 last_frame: Rc::clone(&self.last_frame),
1132 }
1133 }
1134
1135 fn schedule(self) {
1136 let continuation = self.next();
1137 let registration_slot = Rc::clone(&self.registration);
1138 let new_registration = self.frame_clock.with_frame_nanos(move |frame_time_nanos| {
1139 let this = &continuation;
1140 if !this.is_running.get() {
1141 return;
1142 }
1143 {
1144 let gs = this.gesture_state.borrow();
1145 let animating = gs.is_dragging
1146 || gs
1147 .fling_animation
1148 .as_ref()
1149 .is_some_and(FlingAnimation::is_running)
1150 || gs
1151 .settle_animation
1152 .as_ref()
1153 .is_some_and(SettleAnimation::is_running);
1154 if animating {
1155 this.is_running.set(false);
1156 return;
1157 }
1158 }
1159
1160 let offset = this.detector.scroll_target.current_offset();
1161 let dt = this
1162 .last_frame
1163 .get()
1164 .map_or(0, |last| frame_time_nanos.saturating_sub(last));
1165 this.last_frame.set(Some(frame_time_nanos));
1166 if (offset - this.last_offset.get()).abs() > 0.01 {
1167 this.last_offset.set(offset);
1168 this.idle_nanos.set(0);
1169 } else {
1170 this.idle_nanos.set(this.idle_nanos.get() + dt);
1171 }
1172
1173 if this.idle_nanos.get() >= WHEEL_SETTLE_IDLE_NANOS {
1174 this.is_running.set(false);
1175 if this.detector.overscroll.offset().abs() > 0.001 {
1176 this.detector.start_overscroll_settle(0.0);
1177 return;
1178 }
1179 if let Some(policy) = this.detector.scroll_target.settle_policy() {
1180 let target = policy(offset, 0.0);
1181 if (target - offset).abs() > 0.5 {
1182 this.detector.start_settle_animation(target, 0.0);
1183 }
1184 }
1185 return;
1186 }
1187
1188 continuation.next().schedule();
1189 });
1190 *registration_slot.borrow_mut() = Some(new_registration);
1191 }
1192 }
1193
1194 WheelSettleLoop {
1195 detector: self.clone_for_watcher(),
1196 gesture_state: Rc::clone(&self.gesture_state),
1197 frame_clock: runtime.frame_clock(),
1198 is_running: Rc::clone(&is_running),
1199 registration: Rc::clone(®istration),
1200 last_offset: Rc::new(Cell::new(self.scroll_target.current_offset())),
1201 idle_nanos: Rc::new(Cell::new(0u64)),
1202 last_frame: Rc::new(Cell::new(None::<u64>)),
1203 }
1204 .schedule();
1205
1206 self.gesture_state.borrow_mut().wheel_settle_watcher = Some(WheelSettleWatcher {
1207 is_running,
1208 registration,
1209 });
1210 }
1211
1212 fn clone_for_watcher(&self) -> Self {
1213 Self {
1214 gesture_state: Rc::clone(&self.gesture_state),
1215 scroll_target: self.scroll_target.clone(),
1216 axis: self.axis,
1217 reverse_scrolling: self.reverse_scrolling,
1218 overscroll: self.overscroll.clone(),
1219 motion_context: self.motion_context.clone(),
1220 }
1221 }
1222}
1223
1224pub(crate) struct MotionContextAnimatedNode {
1225 state: NodeState,
1226 motion_context: ScrollMotionContext,
1227 invalidation_callback_id: Option<u64>,
1228 overscroll_callback_id: Option<u64>,
1229 node_id: Option<NodeId>,
1230}
1231
1232impl MotionContextAnimatedNode {
1233 fn new(motion_context: ScrollMotionContext) -> Self {
1234 Self {
1235 state: NodeState::new(),
1236 motion_context,
1237 invalidation_callback_id: None,
1238 overscroll_callback_id: None,
1239 node_id: None,
1240 }
1241 }
1242
1243 pub(crate) fn is_active(&self) -> bool {
1244 self.motion_context.is_active()
1245 }
1246
1247 fn attach_callbacks(&mut self) {
1248 if let Some(node_id) = self.node_id {
1249 self.invalidation_callback_id = Some(self.motion_context.add_invalidate_callback(
1250 Box::new(move || schedule_modifier_slices_repass(node_id)),
1251 ));
1252 self.overscroll_callback_id = Some(
1253 self.motion_context
1254 .overscroll()
1255 .add_invalidate_callback(Box::new(move || {
1256 crate::schedule_measure_repass(node_id);
1257 schedule_modifier_slices_repass(node_id);
1258 })),
1259 );
1260 }
1261 }
1262
1263 fn detach_callbacks(&mut self) {
1264 if let Some(id) = self.invalidation_callback_id.take() {
1265 self.motion_context.remove_invalidate_callback(id);
1266 }
1267 if let Some(id) = self.overscroll_callback_id.take() {
1268 self.motion_context
1269 .overscroll()
1270 .remove_invalidate_callback(id);
1271 }
1272 }
1273}
1274
1275pub(crate) struct TranslatedContentContextNode {
1276 state: NodeState,
1277 identity: usize,
1278 offset_source: TranslatedContentOffsetSource,
1279 overscroll: crate::scroll::OverscrollEffect,
1280 overscroll_callback_id: Option<u64>,
1281 node_id: Option<NodeId>,
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 node_id: None,
1297 }
1298 }
1299
1300 pub(crate) fn is_active(&self) -> bool {
1301 true
1302 }
1303
1304 pub(crate) fn identity(&self) -> usize {
1305 self.identity
1306 }
1307
1308 pub(crate) fn content_offset_reader(&self) -> Option<Rc<dyn Fn() -> Point>> {
1309 self.offset_source
1310 .content_offset_reader(self.overscroll.clone())
1311 }
1312
1313 fn attach_callback(&mut self) {
1314 if let Some(node_id) = self.node_id {
1315 self.overscroll_callback_id =
1316 Some(self.overscroll.add_invalidate_callback(Box::new(move || {
1317 schedule_modifier_slices_repass(node_id);
1318 })));
1319 }
1320 }
1321
1322 fn detach_callback(&mut self) {
1323 if let Some(id) = self.overscroll_callback_id.take() {
1324 self.overscroll.remove_invalidate_callback(id);
1325 }
1326 }
1327}
1328
1329impl DelegatableNode for TranslatedContentContextNode {
1330 fn node_state(&self) -> &NodeState {
1331 &self.state
1332 }
1333}
1334
1335impl ModifierNode for TranslatedContentContextNode {
1336 fn on_attach(&mut self, context: &mut dyn cranpose_foundation::ModifierNodeContext) {
1337 self.node_id = context.node_id();
1338 self.attach_callback();
1339 }
1340
1341 fn on_detach(&mut self) {
1342 self.detach_callback();
1343 self.node_id = None;
1344 }
1345}
1346
1347impl DelegatableNode for MotionContextAnimatedNode {
1348 fn node_state(&self) -> &NodeState {
1349 &self.state
1350 }
1351}
1352
1353impl ModifierNode for MotionContextAnimatedNode {
1354 fn on_attach(&mut self, context: &mut dyn cranpose_foundation::ModifierNodeContext) {
1355 self.node_id = context.node_id();
1356 self.attach_callbacks();
1357 }
1358
1359 fn on_detach(&mut self) {
1360 self.detach_callbacks();
1361 self.node_id = None;
1362 }
1363}
1364
1365#[derive(Clone)]
1366struct MotionContextAnimatedElement {
1367 motion_context: ScrollMotionContext,
1368}
1369
1370impl MotionContextAnimatedElement {
1371 fn new(motion_context: ScrollMotionContext) -> Self {
1372 Self { motion_context }
1373 }
1374}
1375
1376impl std::fmt::Debug for MotionContextAnimatedElement {
1377 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1378 f.debug_struct("MotionContextAnimatedElement").finish()
1379 }
1380}
1381
1382impl PartialEq for MotionContextAnimatedElement {
1383 fn eq(&self, other: &Self) -> bool {
1384 self.motion_context.ptr_eq(&other.motion_context)
1385 }
1386}
1387
1388impl Eq for MotionContextAnimatedElement {}
1389
1390impl std::hash::Hash for MotionContextAnimatedElement {
1391 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1392 self.motion_context.stable_key().hash(state);
1393 }
1394}
1395
1396impl ModifierNodeElement for MotionContextAnimatedElement {
1397 type Node = MotionContextAnimatedNode;
1398
1399 fn create(&self) -> Self::Node {
1400 MotionContextAnimatedNode::new(self.motion_context.clone())
1401 }
1402
1403 fn update(&self, node: &mut Self::Node) {
1404 if node.motion_context.ptr_eq(&self.motion_context) {
1405 return;
1406 }
1407 node.detach_callbacks();
1408 node.motion_context = self.motion_context.clone();
1409 node.attach_callbacks();
1410 }
1411
1412 fn capabilities(&self) -> NodeCapabilities {
1413 NodeCapabilities::LAYOUT
1414 }
1415}
1416
1417#[derive(Clone)]
1418enum TranslatedContentOffsetSource {
1419 LayoutContentOffset,
1420 LazyList {
1421 state: LazyListState,
1422 axis: Axis,
1423 reverse_scrolling: bool,
1424 },
1425}
1426
1427impl TranslatedContentOffsetSource {
1428 fn content_offset_reader(
1429 &self,
1430 overscroll: crate::scroll::OverscrollEffect,
1431 ) -> Option<Rc<dyn Fn() -> Point>> {
1432 match self {
1433 Self::LayoutContentOffset => None,
1434 Self::LazyList { state, axis, .. } => Some(Rc::new(lazy_list_content_offset_reader(
1435 *state, *axis, overscroll,
1436 ))),
1437 }
1438 }
1439
1440 fn axis(&self) -> Option<Axis> {
1441 match self {
1442 Self::LayoutContentOffset => None,
1443 Self::LazyList { axis, .. } => Some(*axis),
1444 }
1445 }
1446
1447 fn reverse_scrolling(&self) -> Option<bool> {
1448 match self {
1449 Self::LayoutContentOffset => None,
1450 Self::LazyList {
1451 reverse_scrolling, ..
1452 } => Some(*reverse_scrolling),
1453 }
1454 }
1455}
1456
1457fn lazy_list_content_offset_reader(
1458 state: LazyListState,
1459 axis: Axis,
1460 overscroll: crate::scroll::OverscrollEffect,
1461) -> impl Fn() -> Point {
1462 move || {
1463 let Some((viewport, main_offset)) = state
1464 .with_layout_info(|info| {
1465 (!info.visible_items_info.is_empty())
1466 .then_some((info.viewport_size, info.snap_anchor_offset))
1467 })
1468 .flatten()
1469 else {
1470 return Point::default();
1471 };
1472 overscroll.set_dimension(viewport);
1473 let bounce = overscroll.offset();
1474 if axis.is_vertical() {
1475 Point::new(0.0, main_offset + bounce)
1476 } else {
1477 Point::new(main_offset + bounce, 0.0)
1478 }
1479 }
1480}
1481
1482#[derive(Clone)]
1483struct TranslatedContentContextElement {
1484 identity: usize,
1485 offset_source: TranslatedContentOffsetSource,
1486 overscroll: crate::scroll::OverscrollEffect,
1487}
1488
1489impl TranslatedContentContextElement {
1490 fn new(
1491 identity: usize,
1492 offset_source: TranslatedContentOffsetSource,
1493 overscroll: crate::scroll::OverscrollEffect,
1494 ) -> Self {
1495 Self {
1496 identity,
1497 offset_source,
1498 overscroll,
1499 }
1500 }
1501}
1502
1503impl std::fmt::Debug for TranslatedContentContextElement {
1504 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1505 let offset_source = match &self.offset_source {
1506 TranslatedContentOffsetSource::LayoutContentOffset => "layout",
1507 TranslatedContentOffsetSource::LazyList { .. } => "lazy_list",
1508 };
1509 f.debug_struct("TranslatedContentContextElement")
1510 .field("identity", &self.identity)
1511 .field("offset_source", &offset_source)
1512 .finish()
1513 }
1514}
1515
1516impl PartialEq for TranslatedContentContextElement {
1517 fn eq(&self, other: &Self) -> bool {
1518 self.identity == other.identity
1519 && self.overscroll.ptr_eq(&other.overscroll)
1520 && self.offset_source.axis() == other.offset_source.axis()
1521 && self.offset_source.reverse_scrolling() == other.offset_source.reverse_scrolling()
1522 }
1523}
1524
1525impl Eq for TranslatedContentContextElement {}
1526
1527impl std::hash::Hash for TranslatedContentContextElement {
1528 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1529 self.identity.hash(state);
1530 self.offset_source.axis().hash(state);
1531 self.offset_source.reverse_scrolling().hash(state);
1532 }
1533}
1534
1535impl ModifierNodeElement for TranslatedContentContextElement {
1536 type Node = TranslatedContentContextNode;
1537
1538 fn create(&self) -> Self::Node {
1539 TranslatedContentContextNode::new(
1540 self.identity,
1541 self.offset_source.clone(),
1542 self.overscroll.clone(),
1543 )
1544 }
1545
1546 fn update(&self, node: &mut Self::Node) {
1547 node.identity = self.identity;
1548 node.offset_source = self.offset_source.clone();
1549 if !node.overscroll.ptr_eq(&self.overscroll) {
1550 node.detach_callback();
1551 node.overscroll = self.overscroll.clone();
1552 node.attach_callback();
1553 }
1554 }
1555
1556 fn capabilities(&self) -> NodeCapabilities {
1557 NodeCapabilities::LAYOUT
1558 }
1559}
1560
1561impl Modifier {
1562 pub fn horizontal_scroll(self, state: ScrollState, reverse_scrolling: bool) -> Self {
1580 self.then(scroll_impl(
1581 state,
1582 Axis::Horizontal,
1583 reverse_scrolling,
1584 None,
1585 (0.0, 0.0),
1586 ))
1587 }
1588
1589 pub fn vertical_scroll(self, state: ScrollState, reverse_scrolling: bool) -> Self {
1598 self.then(scroll_impl(
1599 state,
1600 Axis::Vertical,
1601 reverse_scrolling,
1602 None,
1603 (0.0, 0.0),
1604 ))
1605 }
1606
1607 pub fn vertical_scroll_with_content_padding(
1612 self,
1613 state: ScrollState,
1614 reverse_scrolling: bool,
1615 padding: cranpose_ui_graphics::EdgeInsets,
1616 ) -> Self {
1617 let density = crate::density();
1618 self.then(scroll_impl(
1619 state,
1620 Axis::Vertical,
1621 reverse_scrolling,
1622 None,
1623 (density.dp(padding.top), density.dp(padding.bottom)),
1624 ))
1625 .window_insets_padding(padding)
1626 }
1627
1628 pub fn horizontal_scroll_with_content_padding(
1633 self,
1634 state: ScrollState,
1635 reverse_scrolling: bool,
1636 padding: cranpose_ui_graphics::EdgeInsets,
1637 ) -> Self {
1638 let density = crate::density();
1639 self.then(scroll_impl(
1640 state,
1641 Axis::Horizontal,
1642 reverse_scrolling,
1643 None,
1644 (density.dp(padding.left), density.dp(padding.right)),
1645 ))
1646 .window_insets_padding(padding)
1647 }
1648
1649 pub fn horizontal_scroll_without_gestures(
1656 self,
1657 state: ScrollState,
1658 reverse_scrolling: bool,
1659 ) -> Self {
1660 self.then(scroll_impl(
1661 state,
1662 Axis::Horizontal,
1663 reverse_scrolling,
1664 Some(Rc::new(|| false)),
1665 (0.0, 0.0),
1666 ))
1667 }
1668
1669 pub fn vertical_scroll_without_gestures(
1673 self,
1674 state: ScrollState,
1675 reverse_scrolling: bool,
1676 ) -> Self {
1677 self.then(scroll_impl(
1678 state,
1679 Axis::Vertical,
1680 reverse_scrolling,
1681 Some(Rc::new(|| false)),
1682 (0.0, 0.0),
1683 ))
1684 }
1685
1686 pub fn lazy_vertical_scroll(self, state: LazyListState, reverse_scrolling: bool) -> Self {
1697 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::LazyList {
1698 state_identity: state.inner_ptr() as usize,
1699 axis: Axis::Vertical,
1700 reverse_scrolling,
1701 });
1702 self.lazy_vertical_scroll_with_context(state, reverse_scrolling, (0.0, 0.0), motion_context)
1703 }
1704
1705 pub(crate) fn lazy_vertical_scroll_with_context(
1706 self,
1707 state: LazyListState,
1708 reverse_scrolling: bool,
1709 content_padding: (f32, f32),
1710 motion_context: ScrollMotionContext,
1711 ) -> Self {
1712 self.then(lazy_scroll_impl(
1713 state,
1714 Axis::Vertical,
1715 reverse_scrolling,
1716 content_padding,
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 content_padding: (f32, f32),
1726 motion_context: ScrollMotionContext,
1727 ) -> Self {
1728 self.then(lazy_scroll_impl(
1729 state,
1730 Axis::Horizontal,
1731 reverse_scrolling,
1732 content_padding,
1733 motion_context,
1734 ))
1735 }
1736
1737 pub fn draggable(self, axis: Axis, state: DraggableState) -> Self {
1749 self.then(draggable_impl(axis, state, None))
1750 }
1751
1752 pub fn scrollable(self, axis: Axis, state: ScrollableState) -> Self {
1758 let identity = state.identity();
1759 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::Draggable {
1760 state_identity: identity,
1761 axis,
1762 });
1763 self.then(Self::with_element(MotionContextAnimatedElement::new(
1764 motion_context.clone(),
1765 )))
1766 .then(nested_drag_gesture_input(
1767 (identity, axis),
1768 DragGesture {
1769 target: state,
1770 axis,
1771 reverse_input: false,
1772 motion_context,
1773 guard: None,
1774 },
1775 ))
1776 }
1777
1778 pub fn draggable_guarded(
1784 self,
1785 axis: Axis,
1786 state: DraggableState,
1787 guard: impl Fn() -> bool + 'static,
1788 ) -> Self {
1789 self.then(draggable_impl(axis, state, Some(Rc::new(guard))))
1790 }
1791}
1792
1793fn scroll_impl(
1794 state: ScrollState,
1795 axis: Axis,
1796 reverse_scrolling: bool,
1797 guard: Option<Rc<dyn Fn() -> bool>>,
1798 content_padding: (f32, f32),
1799) -> Modifier {
1800 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::ScrollState {
1801 state_id: state.id(),
1802 axis,
1803 reverse_scrolling,
1804 });
1805 motion_context
1806 .observe_activity(|| Box::new(move |active| state.set_scroll_in_progress(active)));
1807
1808 let pointer_input = nested_drag_gesture_input(
1809 (state.id(), axis),
1810 DragGesture {
1811 target: state,
1812 axis,
1813 reverse_input: false,
1814 motion_context: motion_context.clone(),
1815 guard,
1816 },
1817 );
1818
1819 let overscroll = motion_context.overscroll();
1820 let element = ScrollElement::new(state, overscroll.clone(), axis, reverse_scrolling);
1821 let layout_modifier =
1822 Modifier::with_element(element).with_inspector_metadata(inspector_metadata(
1823 if axis.is_vertical() {
1824 "verticalScroll"
1825 } else {
1826 "horizontalScroll"
1827 },
1828 move |info| {
1829 info.add_property("isVertical", axis.is_vertical().to_string());
1830 info.add_property("reverseScrolling", reverse_scrolling.to_string());
1831 },
1832 ));
1833 let motion_modifier = Modifier::with_element(MotionContextAnimatedElement::new(motion_context));
1834 let translated_content_modifier = Modifier::with_element(TranslatedContentContextElement::new(
1835 state.id() as usize,
1836 TranslatedContentOffsetSource::LayoutContentOffset,
1837 overscroll,
1838 ));
1839
1840 pointer_input
1844 .clip_to_bounds()
1845 .then(motion_modifier)
1846 .then(translated_content_modifier)
1847 .then(layout_modifier)
1848 .semantics(scroll_semantics(
1849 state,
1850 axis,
1851 reverse_scrolling,
1852 content_padding,
1853 ))
1854}
1855
1856fn scroll_semantics(
1861 state: ScrollState,
1862 axis: Axis,
1863 reverse_scrolling: bool,
1864 content_padding: (f32, f32),
1865) -> impl Fn(&mut cranpose_foundation::SemanticsConfiguration) + 'static {
1866 let spec = current_bring_into_view_spec();
1867 move |config| {
1868 let range = cranpose_foundation::ScrollAxisRange::new(
1869 state.value(),
1870 state.max_value(),
1871 reverse_scrolling,
1872 )
1873 .with_content_padding(content_padding.0, content_padding.1)
1874 .with_bring_into_view_spec(spec);
1875 if axis.is_vertical() {
1876 config.vertical_scroll = Some(range);
1877 } else {
1878 config.horizontal_scroll = Some(range);
1879 }
1880 config.scroll_by = Some(cranpose_foundation::SemanticsScrollBy::new(
1881 move |dx, dy| {
1882 let delta = if axis.is_vertical() { dy } else { dx };
1883 state.dispatch_raw_delta(delta).abs() > f32::EPSILON
1884 },
1885 ));
1886 }
1887}
1888
1889fn lazy_scroll_impl(
1890 state: LazyListState,
1891 axis: Axis,
1892 reverse_scrolling: bool,
1893 content_padding: (f32, f32),
1894 motion_context: ScrollMotionContext,
1895) -> Modifier {
1896 let list_state = state;
1897 let state_id = state.inner_ptr() as usize;
1898 let key = (state_id, axis, reverse_scrolling);
1899 motion_context
1900 .observe_activity(|| Box::new(move |active| state.set_gesture_scroll_active(active)));
1901 let overscroll = motion_context.overscroll();
1902 let translated_content_modifier = Modifier::with_element(TranslatedContentContextElement::new(
1903 state_id,
1904 TranslatedContentOffsetSource::LazyList {
1905 state,
1906 axis,
1907 reverse_scrolling,
1908 },
1909 overscroll,
1910 ));
1911
1912 Modifier::with_element(MotionContextAnimatedElement::new(motion_context.clone()))
1913 .then(translated_content_modifier)
1914 .then(nested_drag_gesture_input(
1915 key,
1916 DragGesture {
1917 target: list_state,
1918 axis,
1919 reverse_input: reverse_scrolling,
1920 motion_context,
1921 guard: None,
1922 },
1923 ))
1924 .semantics(lazy_scroll_semantics(
1925 list_state,
1926 axis,
1927 reverse_scrolling,
1928 content_padding,
1929 ))
1930}
1931
1932fn draggable_impl(
1933 axis: Axis,
1934 state: DraggableState,
1935 guard: Option<Rc<dyn Fn() -> bool>>,
1936) -> Modifier {
1937 let identity = state.identity();
1938 drag_gesture_input(
1939 (identity, axis),
1940 DragGesture {
1941 target: state,
1942 axis,
1943 reverse_input: false,
1944 motion_context: scroll_motion_context_for_key(ScrollMotionContextKey::Draggable {
1945 state_identity: identity,
1946 axis,
1947 }),
1948 guard,
1949 },
1950 )
1951}