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