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 toward_target(&self, pointer_delta: f32) -> f32 {
627 if self.reverse_scrolling {
628 -pointer_delta
629 } else {
630 pointer_delta
631 }
632 }
633
634 fn along_axis(&self, point: Point) -> f32 {
635 if self.axis.is_vertical() {
636 point.y
637 } else {
638 point.x
639 }
640 }
641
642 fn on_down(&self, event: &PointerEvent) -> bool {
643 let (position, time_ms) = (event.position, event.time_ms);
644 let interrupted = self.scroll_target.stop_animation();
645 let mut gs = self.gesture_state.borrow_mut();
646 gs.interrupted_animation = interrupted;
647
648 if let Some(fling) = gs.fling_animation.take() {
649 fling.cancel();
650 }
651 if let Some(settle) = gs.settle_animation.take() {
652 settle.cancel();
653 }
654 if let Some(watcher) = gs.wheel_settle_watcher.take() {
655 watcher.cancel();
656 }
657 self.motion_context.set_active(false);
658
659 gs.drag_down_position = Some(position);
660 gs.last_position = Some(position);
661 gs.is_dragging = false;
662 gs.axis_locked_out = false;
663 gs.velocity_tracker.reset();
664 gs.gesture_start_time = Some(Instant::now());
665 gs.arrival_clock = ArrivalClock::default();
666 gs.gesture_start_event_time_ms = time_ms;
667 gs.is_overscrolling = self.overscroll.offset().abs() > 0.001;
668
669 gs.velocity_tracker
670 .add_data_point(0, self.along_axis(event.global_position));
671 gs.last_velocity_sample_ms = Some(0);
672
673 false
674 }
675
676 fn on_move(
677 &self,
678 position: Point,
679 buttons: PointerButtons,
680 time_ms: Option<i64>,
681 event: &PointerEvent,
682 ) -> bool {
683 let mut gs = self.gesture_state.borrow_mut();
684
685 if !buttons.contains(PointerButton::Primary) && gs.drag_down_position.is_some() {
686 if gs.is_dragging {
687 self.scroll_target.set_dragging(false);
688 }
689 gs.drag_down_position = None;
690 gs.last_position = None;
691 gs.is_dragging = false;
692 gs.axis_locked_out = false;
693 gs.gesture_start_time = None;
694 gs.gesture_start_event_time_ms = None;
695 gs.last_velocity_sample_ms = None;
696 gs.velocity_tracker.reset();
697 self.motion_context.set_active(false);
698 return false;
699 }
700
701 let Some(down_pos) = gs.drag_down_position else {
702 return false;
703 };
704
705 let Some(last_pos) = gs.last_position else {
706 gs.last_position = Some(position);
707 return false;
708 };
709
710 let incremental_delta = delta_along(last_pos, position, self.axis);
711
712 let mut edge_candidate = false;
713 if !gs.is_dragging && !gs.axis_locked_out {
714 let signed_main_delta = delta_along(down_pos, position, self.axis);
715 let main_delta = signed_main_delta.abs();
716 let cross_delta = delta_along(down_pos, position, self.axis.cross_axis()).abs();
717 if main_delta > DRAG_THRESHOLD && main_delta >= cross_delta {
718 if self
719 .scroll_target
720 .can_consume(self.toward_target(signed_main_delta))
721 {
722 gs.is_dragging = true;
723 self.scroll_target.set_dragging(true);
724 self.motion_context.set_active(true);
725 } else if self.scroll_target.can_scroll() {
726 edge_candidate = true;
727 }
728 } else if cross_delta > DRAG_THRESHOLD && cross_delta > main_delta {
729 gs.axis_locked_out = true;
730 }
731 }
732
733 gs.last_position = Some(position);
734
735 let pos = self.along_axis(event.global_position);
737 let event_sample_ms = gs
738 .gesture_start_event_time_ms
739 .zip(time_ms)
740 .map(|(start_ms, now_ms)| now_ms - start_ms);
741 let sample_ms = if let Some(event_sample_ms) = event_sample_ms {
742 Some(match gs.last_velocity_sample_ms {
743 Some(last_sample_ms) => event_sample_ms.max(last_sample_ms),
744 None => event_sample_ms.max(0),
745 })
746 } else if let Some(start_time) = gs.gesture_start_time {
747 let last_sample_ms = gs.last_velocity_sample_ms;
748 Some(
749 gs.arrival_clock
750 .sample_ms(Instant::now(), start_time, last_sample_ms),
751 )
752 } else {
753 None
754 };
755 if let Some(sample_ms) = sample_ms {
756 log::trace!(
757 target: "cranpose::velocity",
758 "sample t={sample_ms}ms pos={pos:.2} event_time={time_ms:?}"
759 );
760 gs.velocity_tracker.add_data_point(sample_ms, pos);
761 gs.last_velocity_sample_ms = Some(sample_ms);
762 }
763
764 if gs.is_dragging {
765 drop(gs);
766 let delta = self.toward_target(incremental_delta);
767 let overscroll = self.overscroll.clone();
768 overscroll.apply_to_scroll(delta, |delta| self.scroll_target.apply_delta(delta));
769 self.scroll_target.invalidate();
770 true
771 } else if gs.is_overscrolling {
772 drop(gs);
773 let delta = self.toward_target(incremental_delta);
774 self.apply_overscroll_delta(delta)
775 } else if edge_candidate {
776 drop(gs);
777 let delta = self.toward_target(incremental_delta);
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 = self.toward_target(velocity);
864 let adjusted_velocity = if gesture_owned {
867 adjusted_velocity - self.scroll_target.pre_fling(adjusted_velocity)
868 } else {
869 adjusted_velocity
870 };
871 let start_fling = start_fling && adjusted_velocity.abs() > MIN_FLING_VELOCITY;
872 let fling_velocity = -adjusted_velocity;
873 let has_overscroll = self.overscroll.offset().abs() > 0.001;
874
875 let settle_target = (was_dragging || interrupted)
876 .then(|| self.settle_target(start_fling, fling_velocity))
877 .flatten();
878
879 if has_overscroll {
880 self.start_overscroll_settle(-fling_velocity);
881 } else if let Some(target) = settle_target {
882 self.start_settle_animation(target, fling_velocity);
883 } else if start_fling {
884 self.start_fling_animation(fling_velocity);
885 } else {
886 self.motion_context.set_active(false);
887 }
888
889 gesture_owned
890 }
891
892 fn settle_target(&self, start_fling: bool, fling_velocity: f32) -> Option<f32> {
895 let policy = self.scroll_target.settle_policy()?;
896 let current = self.scroll_target.current_offset();
897 let proposed = if start_fling {
898 fling_rest_position(current, fling_velocity)
899 } else {
900 current
901 };
902 let target = policy(proposed, fling_velocity);
903 ((target - proposed).abs() > 0.5).then_some(target)
904 }
905
906 fn start_fling_animation(&self, fling_velocity: f32) {
907 let Some(runtime) = current_runtime_handle() else {
908 self.motion_context.set_active(false);
909 return;
910 };
911 self.motion_context.set_active(true);
912 let scroll_target = self.scroll_target.clone();
913 let fling = FlingAnimation::new(runtime);
914 let motion_context = self.motion_context.clone();
915 let initial_value = scroll_target.current_offset();
916 let scroll_target_for_fling = scroll_target.clone();
917 let detector_for_end = self.clone_for_watcher();
918 let overscroll_for_fling = self.overscroll.clone();
919
920 fling.start_fling(
921 initial_value,
922 fling_velocity,
923 move |delta| {
924 let consumed = overscroll_for_fling.apply_to_fling(delta, |delta| {
925 scroll_target_for_fling.apply_fling_delta(delta)
926 });
927 scroll_target_for_fling.invalidate();
928 consumed
929 },
930 move || {
931 scroll_target.invalidate();
932 let settle_running = detector_for_end
933 .gesture_state
934 .borrow()
935 .settle_animation
936 .as_ref()
937 .is_some_and(SettleAnimation::is_running);
938 if detector_for_end.overscroll.offset().abs() > 0.001 && !settle_running {
939 detector_for_end.start_overscroll_settle(0.0);
940 }
941 detector_for_end.update_motion_active(&motion_context);
942 },
943 );
944
945 let mut gs = self.gesture_state.borrow_mut();
946 gs.fling_animation = Some(fling);
947 }
948
949 fn start_settle_animation(&self, target: f32, initial_velocity: f32) {
950 let Some(runtime) = current_runtime_handle() else {
951 self.motion_context.set_active(false);
952 return;
953 };
954 self.motion_context.set_active(true);
955 let settle = SettleAnimation::new(runtime, SpringParams::SETTLE_POLICY);
956 let scroll_target_for_settle = self.scroll_target.clone();
957 let scroll_target_for_end = self.scroll_target.clone();
958 let detector_for_end = self.clone_for_watcher();
959 let motion_context = self.motion_context.clone();
960 settle.start_settle(
961 self.scroll_target.current_offset(),
962 initial_velocity,
963 target,
964 move |delta| {
965 let consumed = scroll_target_for_settle.apply_fling_delta(delta);
966 scroll_target_for_settle.invalidate();
967 consumed
968 },
969 move |_| {
970 scroll_target_for_end.invalidate();
971 detector_for_end.update_motion_active(&motion_context);
972 },
973 );
974 let mut gs = self.gesture_state.borrow_mut();
975 gs.settle_animation = Some(settle);
976 }
977
978 fn start_overscroll_settle(&self, initial_velocity: f32) {
979 let Some(runtime) = current_runtime_handle() else {
980 let current = self.overscroll.offset();
981 self.overscroll.apply_settle_delta(-current);
982 self.motion_context.set_active(false);
983 return;
984 };
985 let settle = SettleAnimation::new(runtime, SpringParams::OVERSCROLL_BOUNCE);
986 let overscroll_for_settle = self.overscroll.clone();
987 let overscroll_for_end = self.overscroll.clone();
988 let detector_for_end = self.clone_for_watcher();
989 let motion_context = self.motion_context.clone();
990 let initial = self.overscroll.offset();
991 settle.start_settle(
992 initial,
993 initial_velocity,
994 0.0,
995 move |delta| overscroll_for_settle.apply_settle_delta(delta),
996 move |end| {
997 let current = overscroll_for_end.offset();
998 overscroll_for_end.apply_settle_delta(-current);
999 if end.hit_boundary && end.velocity.abs() > MIN_FLING_VELOCITY {
1000 detector_for_end.start_fling_animation(-end.velocity);
1001 } else {
1002 detector_for_end.update_motion_active(&motion_context);
1003 }
1004 },
1005 );
1006 let mut gs = self.gesture_state.borrow_mut();
1007 gs.is_overscrolling = false;
1008 gs.settle_animation = Some(settle);
1009 }
1010
1011 fn update_motion_active(&self, motion_context: &ScrollMotionContext) {
1012 let running = self.gesture_state.borrow().is_active();
1013 if !running {
1014 motion_context.set_active(false);
1015 }
1016 }
1017
1018 fn on_up(&self, time_ms: Option<i64>) -> bool {
1019 self.finish_gesture(self.scroll_target.allows_fling(), time_ms)
1020 }
1021
1022 fn on_cancel(&self) -> bool {
1023 self.finish_gesture(false, None)
1024 }
1025
1026 fn on_scroll(&self, axis_delta: f32, event: &PointerEvent) -> bool {
1027 if axis_delta.abs() <= f32::EPSILON {
1028 return false;
1029 }
1030
1031 let delta = self.toward_target(axis_delta);
1032 if !self.scroll_target.can_consume(delta) && self.scroll_target.can_scroll() {
1033 let detector = self.clone_for_watcher();
1034 event.defer_post_dispatch_action(move || detector.apply_wheel_delta(delta));
1035 return false;
1036 }
1037 self.apply_wheel_delta(delta)
1038 }
1039
1040 fn apply_wheel_delta(&self, delta: f32) -> bool {
1041 {
1042 let mut gs = self.gesture_state.borrow_mut();
1043 if gs.disposed {
1044 return false;
1045 }
1046 if let Some(fling) = gs.fling_animation.take() {
1047 fling.cancel();
1048 }
1049 if let Some(settle) = gs.settle_animation.take() {
1050 settle.cancel();
1051 }
1052 gs.drag_down_position = None;
1053 gs.last_position = None;
1054 gs.is_dragging = false;
1055 gs.axis_locked_out = false;
1056 gs.gesture_start_time = None;
1057 gs.gesture_start_event_time_ms = None;
1058 gs.last_velocity_sample_ms = None;
1059 gs.velocity_tracker.reset();
1060 }
1061
1062 self.motion_context.activate_for_current_frame();
1063 let overscroll = self.overscroll.clone();
1064 let consumed =
1065 overscroll.apply_to_scroll(delta, |delta| self.scroll_target.apply_wheel_delta(delta));
1066 let overscroll_active = self.overscroll.offset().abs() > 0.001;
1067 if consumed.abs() > 0.001 {
1068 self.scroll_target.invalidate();
1069 }
1070 if consumed.abs() > 0.001 || overscroll_active {
1071 self.ensure_wheel_settle_watcher();
1072 true
1073 } else {
1074 false
1075 }
1076 }
1077
1078 fn ensure_wheel_settle_watcher(&self) {
1079 if self.scroll_target.settle_policy().is_none() && self.overscroll.offset().abs() <= 0.001 {
1080 return;
1081 }
1082 {
1083 let gs = self.gesture_state.borrow();
1084 if gs
1085 .wheel_settle_watcher
1086 .as_ref()
1087 .is_some_and(|watcher| watcher.is_running.get())
1088 {
1089 return;
1090 }
1091 }
1092 let Some(runtime) = current_runtime_handle() else {
1093 return;
1094 };
1095
1096 let is_running = Rc::new(Cell::new(true));
1097 let registration = Rc::new(RefCell::new(None));
1098
1099 struct WheelSettleLoop<S: ScrollTarget> {
1100 detector: ScrollGestureDetector<S>,
1101 gesture_state: Rc<RefCell<ScrollGestureState>>,
1102 frame_clock: cranpose_core::internal::FrameClock,
1103 is_running: Rc<Cell<bool>>,
1104 registration: Rc<RefCell<Option<FrameCallbackRegistration>>>,
1105 last_offset: Rc<Cell<f32>>,
1106 idle_nanos: Rc<Cell<u64>>,
1107 last_frame: Rc<Cell<Option<u64>>>,
1108 }
1109
1110 impl<S: ScrollTarget + 'static> WheelSettleLoop<S> {
1111 fn next(&self) -> Self {
1112 Self {
1113 detector: self.detector.clone_for_watcher(),
1114 gesture_state: Rc::clone(&self.gesture_state),
1115 frame_clock: self.frame_clock.clone(),
1116 is_running: Rc::clone(&self.is_running),
1117 registration: Rc::clone(&self.registration),
1118 last_offset: Rc::clone(&self.last_offset),
1119 idle_nanos: Rc::clone(&self.idle_nanos),
1120 last_frame: Rc::clone(&self.last_frame),
1121 }
1122 }
1123
1124 fn schedule(self) {
1125 let continuation = self.next();
1126 let registration_slot = Rc::clone(&self.registration);
1127 let new_registration = self.frame_clock.with_frame_nanos(move |frame_time_nanos| {
1128 let this = &continuation;
1129 if !this.is_running.get() {
1130 return;
1131 }
1132 {
1133 let gs = this.gesture_state.borrow();
1134 let animating = gs.is_dragging
1135 || gs
1136 .fling_animation
1137 .as_ref()
1138 .is_some_and(FlingAnimation::is_running)
1139 || gs
1140 .settle_animation
1141 .as_ref()
1142 .is_some_and(SettleAnimation::is_running);
1143 if animating {
1144 this.is_running.set(false);
1145 return;
1146 }
1147 }
1148
1149 let offset = this.detector.scroll_target.current_offset();
1150 let dt = this
1151 .last_frame
1152 .get()
1153 .map_or(0, |last| frame_time_nanos.saturating_sub(last));
1154 this.last_frame.set(Some(frame_time_nanos));
1155 if (offset - this.last_offset.get()).abs() > 0.01 {
1156 this.last_offset.set(offset);
1157 this.idle_nanos.set(0);
1158 } else {
1159 this.idle_nanos.set(this.idle_nanos.get() + dt);
1160 }
1161
1162 if this.idle_nanos.get() >= WHEEL_SETTLE_IDLE_NANOS {
1163 this.is_running.set(false);
1164 if this.detector.overscroll.offset().abs() > 0.001 {
1165 this.detector.start_overscroll_settle(0.0);
1166 return;
1167 }
1168 if let Some(policy) = this.detector.scroll_target.settle_policy() {
1169 let target = policy(offset, 0.0);
1170 if (target - offset).abs() > 0.5 {
1171 this.detector.start_settle_animation(target, 0.0);
1172 }
1173 }
1174 return;
1175 }
1176
1177 continuation.next().schedule();
1178 });
1179 *registration_slot.borrow_mut() = Some(new_registration);
1180 }
1181 }
1182
1183 WheelSettleLoop {
1184 detector: self.clone_for_watcher(),
1185 gesture_state: Rc::clone(&self.gesture_state),
1186 frame_clock: runtime.frame_clock(),
1187 is_running: Rc::clone(&is_running),
1188 registration: Rc::clone(®istration),
1189 last_offset: Rc::new(Cell::new(self.scroll_target.current_offset())),
1190 idle_nanos: Rc::new(Cell::new(0u64)),
1191 last_frame: Rc::new(Cell::new(None::<u64>)),
1192 }
1193 .schedule();
1194
1195 self.gesture_state.borrow_mut().wheel_settle_watcher = Some(WheelSettleWatcher {
1196 is_running,
1197 registration,
1198 });
1199 }
1200
1201 fn clone_for_watcher(&self) -> Self {
1202 Self {
1203 gesture_state: Rc::clone(&self.gesture_state),
1204 scroll_target: self.scroll_target.clone(),
1205 axis: self.axis,
1206 reverse_scrolling: self.reverse_scrolling,
1207 overscroll: self.overscroll.clone(),
1208 motion_context: self.motion_context.clone(),
1209 }
1210 }
1211}
1212
1213pub(crate) struct MotionContextAnimatedNode {
1214 state: NodeState,
1215 motion_context: ScrollMotionContext,
1216 invalidation_callback_id: Option<u64>,
1217 overscroll_callback_id: Option<u64>,
1218 node_id: Option<NodeId>,
1219}
1220
1221impl MotionContextAnimatedNode {
1222 fn new(motion_context: ScrollMotionContext) -> Self {
1223 Self {
1224 state: NodeState::new(),
1225 motion_context,
1226 invalidation_callback_id: None,
1227 overscroll_callback_id: None,
1228 node_id: None,
1229 }
1230 }
1231
1232 pub(crate) fn is_active(&self) -> bool {
1233 self.motion_context.is_active()
1234 }
1235
1236 fn attach_callbacks(&mut self) {
1237 if let Some(node_id) = self.node_id {
1238 self.invalidation_callback_id = Some(self.motion_context.add_invalidate_callback(
1239 Box::new(move || schedule_modifier_slices_repass(node_id)),
1240 ));
1241 self.overscroll_callback_id = Some(
1242 self.motion_context
1243 .overscroll()
1244 .add_invalidate_callback(Box::new(move || {
1245 crate::schedule_measure_repass(node_id);
1246 schedule_modifier_slices_repass(node_id);
1247 })),
1248 );
1249 }
1250 }
1251
1252 fn detach_callbacks(&mut self) {
1253 if let Some(id) = self.invalidation_callback_id.take() {
1254 self.motion_context.remove_invalidate_callback(id);
1255 }
1256 if let Some(id) = self.overscroll_callback_id.take() {
1257 self.motion_context
1258 .overscroll()
1259 .remove_invalidate_callback(id);
1260 }
1261 }
1262}
1263
1264pub(crate) struct TranslatedContentContextNode {
1265 state: NodeState,
1266 identity: usize,
1267 offset_source: TranslatedContentOffsetSource,
1268 overscroll: crate::scroll::OverscrollEffect,
1269 overscroll_callback_id: Option<u64>,
1270 node_id: Option<NodeId>,
1271}
1272
1273impl TranslatedContentContextNode {
1274 fn new(
1275 identity: usize,
1276 offset_source: TranslatedContentOffsetSource,
1277 overscroll: crate::scroll::OverscrollEffect,
1278 ) -> Self {
1279 Self {
1280 state: NodeState::new(),
1281 identity,
1282 offset_source,
1283 overscroll,
1284 overscroll_callback_id: None,
1285 node_id: None,
1286 }
1287 }
1288
1289 pub(crate) fn is_active(&self) -> bool {
1290 true
1291 }
1292
1293 pub(crate) fn identity(&self) -> usize {
1294 self.identity
1295 }
1296
1297 pub(crate) fn content_offset_reader(&self) -> Option<Rc<dyn Fn() -> Point>> {
1298 self.offset_source
1299 .content_offset_reader(self.overscroll.clone())
1300 }
1301
1302 fn attach_callback(&mut self) {
1303 if let Some(node_id) = self.node_id {
1304 self.overscroll_callback_id =
1305 Some(self.overscroll.add_invalidate_callback(Box::new(move || {
1306 schedule_modifier_slices_repass(node_id);
1307 })));
1308 }
1309 }
1310
1311 fn detach_callback(&mut self) {
1312 if let Some(id) = self.overscroll_callback_id.take() {
1313 self.overscroll.remove_invalidate_callback(id);
1314 }
1315 }
1316}
1317
1318impl DelegatableNode for TranslatedContentContextNode {
1319 fn node_state(&self) -> &NodeState {
1320 &self.state
1321 }
1322}
1323
1324impl ModifierNode for TranslatedContentContextNode {
1325 fn on_attach(&mut self, context: &mut dyn cranpose_foundation::ModifierNodeContext) {
1326 self.node_id = context.node_id();
1327 self.attach_callback();
1328 }
1329
1330 fn on_detach(&mut self) {
1331 self.detach_callback();
1332 self.node_id = None;
1333 }
1334}
1335
1336impl DelegatableNode for MotionContextAnimatedNode {
1337 fn node_state(&self) -> &NodeState {
1338 &self.state
1339 }
1340}
1341
1342impl ModifierNode for MotionContextAnimatedNode {
1343 fn on_attach(&mut self, context: &mut dyn cranpose_foundation::ModifierNodeContext) {
1344 self.node_id = context.node_id();
1345 self.attach_callbacks();
1346 }
1347
1348 fn on_detach(&mut self) {
1349 self.detach_callbacks();
1350 self.node_id = None;
1351 }
1352}
1353
1354#[derive(Clone)]
1355struct MotionContextAnimatedElement {
1356 motion_context: ScrollMotionContext,
1357}
1358
1359impl MotionContextAnimatedElement {
1360 fn new(motion_context: ScrollMotionContext) -> Self {
1361 Self { motion_context }
1362 }
1363}
1364
1365impl std::fmt::Debug for MotionContextAnimatedElement {
1366 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1367 f.debug_struct("MotionContextAnimatedElement").finish()
1368 }
1369}
1370
1371impl PartialEq for MotionContextAnimatedElement {
1372 fn eq(&self, other: &Self) -> bool {
1373 self.motion_context.ptr_eq(&other.motion_context)
1374 }
1375}
1376
1377impl Eq for MotionContextAnimatedElement {}
1378
1379impl std::hash::Hash for MotionContextAnimatedElement {
1380 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1381 self.motion_context.stable_key().hash(state);
1382 }
1383}
1384
1385impl ModifierNodeElement for MotionContextAnimatedElement {
1386 type Node = MotionContextAnimatedNode;
1387
1388 fn create(&self) -> Self::Node {
1389 MotionContextAnimatedNode::new(self.motion_context.clone())
1390 }
1391
1392 fn update(&self, node: &mut Self::Node) {
1393 if node.motion_context.ptr_eq(&self.motion_context) {
1394 return;
1395 }
1396 node.detach_callbacks();
1397 node.motion_context = self.motion_context.clone();
1398 node.attach_callbacks();
1399 }
1400
1401 fn capabilities(&self) -> NodeCapabilities {
1402 NodeCapabilities::LAYOUT
1403 }
1404}
1405
1406#[derive(Clone)]
1407enum TranslatedContentOffsetSource {
1408 LayoutContentOffset,
1409 LazyList {
1410 state: LazyListState,
1411 axis: Axis,
1412 reverse_scrolling: bool,
1413 },
1414}
1415
1416impl TranslatedContentOffsetSource {
1417 fn content_offset_reader(
1418 &self,
1419 overscroll: crate::scroll::OverscrollEffect,
1420 ) -> Option<Rc<dyn Fn() -> Point>> {
1421 match self {
1422 Self::LayoutContentOffset => None,
1423 Self::LazyList { state, axis, .. } => Some(Rc::new(lazy_list_content_offset_reader(
1424 *state, *axis, overscroll,
1425 ))),
1426 }
1427 }
1428
1429 fn axis(&self) -> Option<Axis> {
1430 match self {
1431 Self::LayoutContentOffset => None,
1432 Self::LazyList { axis, .. } => Some(*axis),
1433 }
1434 }
1435
1436 fn reverse_scrolling(&self) -> Option<bool> {
1437 match self {
1438 Self::LayoutContentOffset => None,
1439 Self::LazyList {
1440 reverse_scrolling, ..
1441 } => Some(*reverse_scrolling),
1442 }
1443 }
1444}
1445
1446fn lazy_list_content_offset_reader(
1447 state: LazyListState,
1448 axis: Axis,
1449 overscroll: crate::scroll::OverscrollEffect,
1450) -> impl Fn() -> Point {
1451 move || {
1452 let Some((viewport, main_offset)) = state
1453 .with_layout_info(|info| {
1454 (!info.visible_items_info.is_empty())
1455 .then_some((info.viewport_size, info.snap_anchor_offset))
1456 })
1457 .flatten()
1458 else {
1459 return Point::default();
1460 };
1461 overscroll.set_dimension(viewport);
1462 let bounce = overscroll.offset();
1463 if axis.is_vertical() {
1464 Point::new(0.0, main_offset + bounce)
1465 } else {
1466 Point::new(main_offset + bounce, 0.0)
1467 }
1468 }
1469}
1470
1471#[derive(Clone)]
1472struct TranslatedContentContextElement {
1473 identity: usize,
1474 offset_source: TranslatedContentOffsetSource,
1475 overscroll: crate::scroll::OverscrollEffect,
1476}
1477
1478impl TranslatedContentContextElement {
1479 fn new(
1480 identity: usize,
1481 offset_source: TranslatedContentOffsetSource,
1482 overscroll: crate::scroll::OverscrollEffect,
1483 ) -> Self {
1484 Self {
1485 identity,
1486 offset_source,
1487 overscroll,
1488 }
1489 }
1490}
1491
1492impl std::fmt::Debug for TranslatedContentContextElement {
1493 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1494 let offset_source = match &self.offset_source {
1495 TranslatedContentOffsetSource::LayoutContentOffset => "layout",
1496 TranslatedContentOffsetSource::LazyList { .. } => "lazy_list",
1497 };
1498 f.debug_struct("TranslatedContentContextElement")
1499 .field("identity", &self.identity)
1500 .field("offset_source", &offset_source)
1501 .finish()
1502 }
1503}
1504
1505impl PartialEq for TranslatedContentContextElement {
1506 fn eq(&self, other: &Self) -> bool {
1507 self.identity == other.identity
1508 && self.overscroll.ptr_eq(&other.overscroll)
1509 && self.offset_source.axis() == other.offset_source.axis()
1510 && self.offset_source.reverse_scrolling() == other.offset_source.reverse_scrolling()
1511 }
1512}
1513
1514impl Eq for TranslatedContentContextElement {}
1515
1516impl std::hash::Hash for TranslatedContentContextElement {
1517 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1518 self.identity.hash(state);
1519 self.offset_source.axis().hash(state);
1520 self.offset_source.reverse_scrolling().hash(state);
1521 }
1522}
1523
1524impl ModifierNodeElement for TranslatedContentContextElement {
1525 type Node = TranslatedContentContextNode;
1526
1527 fn create(&self) -> Self::Node {
1528 TranslatedContentContextNode::new(
1529 self.identity,
1530 self.offset_source.clone(),
1531 self.overscroll.clone(),
1532 )
1533 }
1534
1535 fn update(&self, node: &mut Self::Node) {
1536 node.identity = self.identity;
1537 node.offset_source = self.offset_source.clone();
1538 if !node.overscroll.ptr_eq(&self.overscroll) {
1539 node.detach_callback();
1540 node.overscroll = self.overscroll.clone();
1541 node.attach_callback();
1542 }
1543 }
1544
1545 fn capabilities(&self) -> NodeCapabilities {
1546 NodeCapabilities::LAYOUT
1547 }
1548}
1549
1550impl Modifier {
1551 pub fn horizontal_scroll(self, state: ScrollState, reverse_scrolling: bool) -> Self {
1569 self.then(scroll_impl(
1570 state,
1571 Axis::Horizontal,
1572 reverse_scrolling,
1573 None,
1574 (0.0, 0.0),
1575 ))
1576 }
1577
1578 pub fn vertical_scroll(self, state: ScrollState, reverse_scrolling: bool) -> Self {
1587 self.then(scroll_impl(
1588 state,
1589 Axis::Vertical,
1590 reverse_scrolling,
1591 None,
1592 (0.0, 0.0),
1593 ))
1594 }
1595
1596 pub fn vertical_scroll_with_content_padding(
1601 self,
1602 state: ScrollState,
1603 reverse_scrolling: bool,
1604 padding: cranpose_ui_graphics::EdgeInsets,
1605 ) -> Self {
1606 let density = crate::density();
1607 self.then(scroll_impl(
1608 state,
1609 Axis::Vertical,
1610 reverse_scrolling,
1611 None,
1612 (density.dp(padding.top), density.dp(padding.bottom)),
1613 ))
1614 .window_insets_padding(padding)
1615 }
1616
1617 pub fn horizontal_scroll_with_content_padding(
1622 self,
1623 state: ScrollState,
1624 reverse_scrolling: bool,
1625 padding: cranpose_ui_graphics::EdgeInsets,
1626 ) -> Self {
1627 let density = crate::density();
1628 self.then(scroll_impl(
1629 state,
1630 Axis::Horizontal,
1631 reverse_scrolling,
1632 None,
1633 (density.dp(padding.left), density.dp(padding.right)),
1634 ))
1635 .window_insets_padding(padding)
1636 }
1637
1638 pub fn horizontal_scroll_without_gestures(
1645 self,
1646 state: ScrollState,
1647 reverse_scrolling: bool,
1648 ) -> Self {
1649 self.then(scroll_impl(
1650 state,
1651 Axis::Horizontal,
1652 reverse_scrolling,
1653 Some(Rc::new(|| false)),
1654 (0.0, 0.0),
1655 ))
1656 }
1657
1658 pub fn vertical_scroll_without_gestures(
1662 self,
1663 state: ScrollState,
1664 reverse_scrolling: bool,
1665 ) -> Self {
1666 self.then(scroll_impl(
1667 state,
1668 Axis::Vertical,
1669 reverse_scrolling,
1670 Some(Rc::new(|| false)),
1671 (0.0, 0.0),
1672 ))
1673 }
1674
1675 pub fn lazy_vertical_scroll(self, state: LazyListState, reverse_scrolling: bool) -> Self {
1686 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::LazyList {
1687 state_identity: state.inner_ptr() as usize,
1688 axis: Axis::Vertical,
1689 reverse_scrolling,
1690 });
1691 self.lazy_vertical_scroll_with_context(state, reverse_scrolling, (0.0, 0.0), motion_context)
1692 }
1693
1694 pub(crate) fn lazy_vertical_scroll_with_context(
1695 self,
1696 state: LazyListState,
1697 reverse_scrolling: bool,
1698 content_padding: (f32, f32),
1699 motion_context: ScrollMotionContext,
1700 ) -> Self {
1701 self.then(lazy_scroll_impl(
1702 state,
1703 Axis::Vertical,
1704 reverse_scrolling,
1705 content_padding,
1706 motion_context,
1707 ))
1708 }
1709
1710 pub(crate) fn lazy_horizontal_scroll_with_context(
1711 self,
1712 state: LazyListState,
1713 reverse_scrolling: bool,
1714 content_padding: (f32, f32),
1715 motion_context: ScrollMotionContext,
1716 ) -> Self {
1717 self.then(lazy_scroll_impl(
1718 state,
1719 Axis::Horizontal,
1720 reverse_scrolling,
1721 content_padding,
1722 motion_context,
1723 ))
1724 }
1725
1726 pub fn draggable(self, axis: Axis, state: DraggableState) -> Self {
1738 self.then(draggable_impl(axis, state, None))
1739 }
1740
1741 pub fn scrollable(self, axis: Axis, state: ScrollableState) -> Self {
1747 let identity = state.identity();
1748 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::Draggable {
1749 state_identity: identity,
1750 axis,
1751 });
1752 self.then(Self::with_element(MotionContextAnimatedElement::new(
1753 motion_context.clone(),
1754 )))
1755 .then(nested_drag_gesture_input(
1756 (identity, axis),
1757 DragGesture {
1758 target: state,
1759 axis,
1760 reverse_input: false,
1761 motion_context,
1762 guard: None,
1763 },
1764 ))
1765 }
1766
1767 pub fn draggable_guarded(
1773 self,
1774 axis: Axis,
1775 state: DraggableState,
1776 guard: impl Fn() -> bool + 'static,
1777 ) -> Self {
1778 self.then(draggable_impl(axis, state, Some(Rc::new(guard))))
1779 }
1780}
1781
1782fn scroll_impl(
1783 state: ScrollState,
1784 axis: Axis,
1785 reverse_scrolling: bool,
1786 guard: Option<Rc<dyn Fn() -> bool>>,
1787 content_padding: (f32, f32),
1788) -> Modifier {
1789 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::ScrollState {
1790 state_id: state.id(),
1791 axis,
1792 reverse_scrolling,
1793 });
1794 motion_context
1795 .observe_activity(|| Box::new(move |active| state.set_scroll_in_progress(active)));
1796
1797 let pointer_input = nested_drag_gesture_input(
1798 (state.id(), axis),
1799 DragGesture {
1800 target: state,
1801 axis,
1802 reverse_input: false,
1803 motion_context: motion_context.clone(),
1804 guard,
1805 },
1806 );
1807
1808 let overscroll = motion_context.overscroll();
1809 let element = ScrollElement::new(state, overscroll.clone(), axis, reverse_scrolling);
1810 let layout_modifier =
1811 Modifier::with_element(element).with_inspector_metadata(inspector_metadata(
1812 if axis.is_vertical() {
1813 "verticalScroll"
1814 } else {
1815 "horizontalScroll"
1816 },
1817 move |info| {
1818 info.add_property("isVertical", axis.is_vertical().to_string());
1819 info.add_property("reverseScrolling", reverse_scrolling.to_string());
1820 },
1821 ));
1822 let motion_modifier = Modifier::with_element(MotionContextAnimatedElement::new(motion_context));
1823 let translated_content_modifier = Modifier::with_element(TranslatedContentContextElement::new(
1824 state.id() as usize,
1825 TranslatedContentOffsetSource::LayoutContentOffset,
1826 overscroll,
1827 ));
1828
1829 pointer_input
1833 .clip_to_bounds()
1834 .then(motion_modifier)
1835 .then(translated_content_modifier)
1836 .then(layout_modifier)
1837 .semantics(scroll_semantics(
1838 state,
1839 axis,
1840 reverse_scrolling,
1841 content_padding,
1842 ))
1843}
1844
1845fn scroll_semantics(
1850 state: ScrollState,
1851 axis: Axis,
1852 reverse_scrolling: bool,
1853 content_padding: (f32, f32),
1854) -> impl Fn(&mut cranpose_foundation::SemanticsConfiguration) + 'static {
1855 let spec = current_bring_into_view_spec();
1856 move |config| {
1857 let range = cranpose_foundation::ScrollAxisRange::new(
1858 state.value(),
1859 state.max_value(),
1860 reverse_scrolling,
1861 )
1862 .with_content_padding(content_padding.0, content_padding.1)
1863 .with_bring_into_view_spec(spec);
1864 if axis.is_vertical() {
1865 config.vertical_scroll = Some(range);
1866 } else {
1867 config.horizontal_scroll = Some(range);
1868 }
1869 config.scroll_by = Some(cranpose_foundation::SemanticsScrollBy::new(
1870 move |dx, dy| {
1871 let delta = if axis.is_vertical() { dy } else { dx };
1872 state.dispatch_raw_delta(delta).abs() > f32::EPSILON
1873 },
1874 ));
1875 }
1876}
1877
1878fn lazy_scroll_impl(
1879 state: LazyListState,
1880 axis: Axis,
1881 reverse_scrolling: bool,
1882 content_padding: (f32, f32),
1883 motion_context: ScrollMotionContext,
1884) -> Modifier {
1885 let list_state = state;
1886 let state_id = state.inner_ptr() as usize;
1887 let key = (state_id, axis, reverse_scrolling);
1888 motion_context
1889 .observe_activity(|| Box::new(move |active| state.set_gesture_scroll_active(active)));
1890 let overscroll = motion_context.overscroll();
1891 let translated_content_modifier = Modifier::with_element(TranslatedContentContextElement::new(
1892 state_id,
1893 TranslatedContentOffsetSource::LazyList {
1894 state,
1895 axis,
1896 reverse_scrolling,
1897 },
1898 overscroll,
1899 ));
1900
1901 Modifier::with_element(MotionContextAnimatedElement::new(motion_context.clone()))
1902 .then(translated_content_modifier)
1903 .then(nested_drag_gesture_input(
1904 key,
1905 DragGesture {
1906 target: list_state,
1907 axis,
1908 reverse_input: reverse_scrolling,
1909 motion_context,
1910 guard: None,
1911 },
1912 ))
1913 .semantics(lazy_scroll_semantics(
1914 list_state,
1915 axis,
1916 reverse_scrolling,
1917 content_padding,
1918 ))
1919}
1920
1921fn draggable_impl(
1922 axis: Axis,
1923 state: DraggableState,
1924 guard: Option<Rc<dyn Fn() -> bool>>,
1925) -> Modifier {
1926 let identity = state.identity();
1927 drag_gesture_input(
1928 (identity, axis),
1929 DragGesture {
1930 target: state,
1931 axis,
1932 reverse_input: false,
1933 motion_context: scroll_motion_context_for_key(ScrollMotionContextKey::Draggable {
1934 state_identity: identity,
1935 axis,
1936 }),
1937 guard,
1938 },
1939 )
1940}