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