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 set_dragging(&self, dragging: bool) {
354 self.set_dragged(dragging);
355 }
356
357 fn stop_animation(&self) -> bool {
358 self.stop_scroll_animation()
359 }
360}
361
362struct DragGesture<S: ScrollTarget> {
363 target: S,
364 is_vertical: bool,
365 reverse_input: bool,
366 motion_context: ScrollMotionContext,
367 guard: Option<Rc<dyn Fn() -> bool>>,
368}
369
370struct ScrollGestureLifetime {
371 gesture_state: Rc<RefCell<ScrollGestureState>>,
372 motion_context: ScrollMotionContext,
373}
374
375impl Drop for ScrollGestureLifetime {
376 fn drop(&mut self) {
377 let state = std::mem::replace(
378 &mut *self.gesture_state.borrow_mut(),
379 ScrollGestureState {
380 disposed: true,
381 ..ScrollGestureState::default()
382 },
383 );
384 let was_active = state.is_active();
385 if let Some(fling) = state.fling_animation {
386 fling.cancel();
387 }
388 if let Some(settle) = state.settle_animation {
389 settle.cancel();
390 }
391 if let Some(watcher) = state.wheel_settle_watcher {
392 watcher.cancel();
393 }
394 if was_active {
395 self.motion_context.set_active(false);
396 }
397 }
398}
399
400fn nested_drag_gesture_input<K, S>(key: K, gesture: DragGesture<S>) -> Modifier
403where
404 K: std::hash::Hash + 'static,
405 S: ScrollTarget + 'static,
406{
407 let Some(parent) = crate::nested_scroll::current_nested_scroll_parent() else {
408 return drag_gesture_input(key, gesture);
409 };
410 let DragGesture {
411 target,
412 is_vertical,
413 reverse_input,
414 motion_context,
415 guard,
416 } = gesture;
417 let identity = parent.identity();
418 let axis = if is_vertical {
419 ScrollAxis::Vertical
420 } else {
421 ScrollAxis::Horizontal
422 };
423 drag_gesture_input(
424 (key, identity),
425 DragGesture {
426 target: NestedTarget {
427 target,
428 parent,
429 axis,
430 },
431 is_vertical,
432 reverse_input,
433 motion_context,
434 guard,
435 },
436 )
437}
438
439fn drag_gesture_input<K, S>(key: K, gesture: DragGesture<S>) -> Modifier
440where
441 K: std::hash::Hash + 'static,
442 S: ScrollTarget + 'static,
443{
444 let DragGesture {
445 target,
446 is_vertical,
447 reverse_input,
448 motion_context,
449 guard,
450 } = gesture;
451
452 Modifier::empty().pointer_input(key, move |scope| {
453 let gesture_state = Rc::new(RefCell::new(ScrollGestureState::default()));
454 let detector = ScrollGestureDetector::new(
455 gesture_state.clone(),
456 target.clone(),
457 is_vertical,
458 reverse_input,
459 motion_context.overscroll(),
460 motion_context.clone(),
461 );
462 let guard = guard.clone();
463 let lifetime = ScrollGestureLifetime {
464 gesture_state: Rc::clone(&gesture_state),
465 motion_context: motion_context.clone(),
466 };
467
468 async move {
469 let _lifetime = lifetime;
470 scope
471 .await_pointer_event_scope(|await_scope| async move {
472 loop {
473 let event = await_scope.await_pointer_event().await;
474
475 if event.id != 0 {
476 continue;
477 }
478
479 if event.is_consumed() {
480 if matches!(
481 event.kind,
482 PointerEventKind::Down
483 | PointerEventKind::Move
484 | PointerEventKind::Up
485 | PointerEventKind::Cancel
486 ) {
487 detector.on_cancel();
488 }
489 continue;
490 }
491
492 if let Some(ref guard) = guard
493 && !guard()
494 {
495 if matches!(event.kind, PointerEventKind::Up | PointerEventKind::Cancel)
496 {
497 detector.on_cancel();
498 }
499 continue;
500 }
501
502 let should_consume = match event.kind {
503 PointerEventKind::Down => {
504 detector.on_down(event.position, event.time_ms)
505 }
506 PointerEventKind::Move => detector.on_move(
507 event.position,
508 event.buttons,
509 event.time_ms,
510 &event,
511 ),
512 PointerEventKind::Up => detector.on_up(event.time_ms),
513 PointerEventKind::Cancel => detector.on_cancel(),
514 PointerEventKind::Scroll => detector.on_scroll(
515 if is_vertical {
516 event.scroll_delta.y
517 } else {
518 event.scroll_delta.x
519 },
520 &event,
521 ),
522 PointerEventKind::Zoom
523 | PointerEventKind::RotaryScrollPre
524 | PointerEventKind::RotaryScroll
525 | PointerEventKind::Enter
526 | PointerEventKind::Exit => false,
527 };
528
529 if should_consume {
530 event.consume();
531 }
532 }
533 })
534 .await;
535 }
536 })
537}
538
539impl ScrollTarget for DraggableState {
540 fn apply_delta(&self, delta: f32) -> f32 {
541 self.drag_by(delta);
542 delta
543 }
544
545 fn apply_fling_delta(&self, delta: f32) -> f32 {
546 self.drag_by(delta);
547 delta
548 }
549
550 fn invalidate(&self) {}
551
552 fn current_offset(&self) -> f32 {
553 self.offset()
554 }
555
556 fn allows_fling(&self) -> bool {
557 false
558 }
559
560 fn set_dragging(&self, dragging: bool) {
561 DraggableState::set_dragging(self, dragging);
562 }
563}
564
565impl ScrollTarget for ScrollableState {
566 fn apply_delta(&self, delta: f32) -> f32 {
567 self.dispatch_raw_delta(delta)
568 }
569
570 fn apply_fling_delta(&self, delta: f32) -> f32 {
571 -self.dispatch_raw_delta(-delta)
572 }
573
574 fn invalidate(&self) {}
575
576 fn current_offset(&self) -> f32 {
577 -self.offset()
578 }
579
580 fn set_dragging(&self, dragging: bool) {
581 ScrollableState::set_dragging(self, dragging);
582 }
583}
584
585const WHEEL_SETTLE_IDLE_NANOS: u64 = 180_000_000;
586
587struct WheelSettleWatcher {
588 is_running: Rc<Cell<bool>>,
589 registration: Rc<RefCell<Option<FrameCallbackRegistration>>>,
590}
591
592impl WheelSettleWatcher {
593 fn cancel(&self) {
594 self.is_running.set(false);
595 self.registration.borrow_mut().take();
596 }
597}
598
599struct ScrollGestureDetector<S: ScrollTarget> {
600 gesture_state: Rc<RefCell<ScrollGestureState>>,
601
602 scroll_target: S,
603
604 is_vertical: bool,
605
606 reverse_scrolling: bool,
607
608 overscroll: crate::scroll::OverscrollEffect,
609
610 motion_context: ScrollMotionContext,
611}
612
613impl<S: ScrollTarget + 'static> ScrollGestureDetector<S> {
614 fn new(
615 gesture_state: Rc<RefCell<ScrollGestureState>>,
616 scroll_target: S,
617 is_vertical: bool,
618 reverse_scrolling: bool,
619 overscroll: crate::scroll::OverscrollEffect,
620 motion_context: ScrollMotionContext,
621 ) -> Self {
622 Self {
623 gesture_state,
624 scroll_target,
625 is_vertical,
626 reverse_scrolling,
627 overscroll,
628 motion_context,
629 }
630 }
631
632 fn on_down(&self, position: Point, time_ms: Option<i64>) -> bool {
633 let interrupted = self.scroll_target.stop_animation();
634 let mut gs = self.gesture_state.borrow_mut();
635 gs.interrupted_animation = interrupted;
636
637 if let Some(fling) = gs.fling_animation.take() {
638 fling.cancel();
639 }
640 if let Some(settle) = gs.settle_animation.take() {
641 settle.cancel();
642 }
643 if let Some(watcher) = gs.wheel_settle_watcher.take() {
644 watcher.cancel();
645 }
646 self.motion_context.set_active(false);
647
648 gs.drag_down_position = Some(position);
649 gs.last_position = Some(position);
650 gs.is_dragging = false;
651 gs.axis_locked_out = false;
652 gs.velocity_tracker.reset();
653 gs.gesture_start_time = Some(Instant::now());
654 gs.gesture_start_event_time_ms = time_ms;
655 gs.is_overscrolling = self.overscroll.offset().abs() > 0.001;
656
657 let pos = if self.is_vertical {
658 position.y
659 } else {
660 position.x
661 };
662 gs.velocity_tracker.add_data_point(0, pos);
663 gs.last_velocity_sample_ms = Some(0);
664
665 false
666 }
667
668 fn on_move(
669 &self,
670 position: Point,
671 buttons: PointerButtons,
672 time_ms: Option<i64>,
673 event: &PointerEvent,
674 ) -> bool {
675 let mut gs = self.gesture_state.borrow_mut();
676
677 if !buttons.contains(PointerButton::Primary) && gs.drag_down_position.is_some() {
678 if gs.is_dragging {
679 self.scroll_target.set_dragging(false);
680 }
681 gs.drag_down_position = None;
682 gs.last_position = None;
683 gs.is_dragging = false;
684 gs.axis_locked_out = false;
685 gs.gesture_start_time = None;
686 gs.gesture_start_event_time_ms = None;
687 gs.last_velocity_sample_ms = None;
688 gs.velocity_tracker.reset();
689 self.motion_context.set_active(false);
690 return false;
691 }
692
693 let Some(down_pos) = gs.drag_down_position else {
694 return false;
695 };
696
697 let Some(last_pos) = gs.last_position else {
698 gs.last_position = Some(position);
699 return false;
700 };
701
702 let incremental_delta = calculate_incremental_delta(last_pos, position, self.is_vertical);
703
704 let mut edge_candidate = false;
705 if !gs.is_dragging && !gs.axis_locked_out {
706 let signed_main_delta = calculate_total_delta(down_pos, position, self.is_vertical);
707 let main_delta = signed_main_delta.abs();
708 let cross_delta = calculate_total_delta(down_pos, position, !self.is_vertical).abs();
709 if main_delta > DRAG_THRESHOLD && main_delta >= cross_delta {
710 if self.scroll_target.can_consume(signed_main_delta) {
711 gs.is_dragging = true;
712 self.scroll_target.set_dragging(true);
713 self.motion_context.set_active(true);
714 } else if self.scroll_target.can_scroll() {
715 edge_candidate = true;
716 }
717 } else if cross_delta > DRAG_THRESHOLD && cross_delta > main_delta {
718 gs.axis_locked_out = true;
719 }
720 }
721
722 gs.last_position = Some(position);
723
724 let pos = if self.is_vertical {
725 position.y
726 } else {
727 position.x
728 };
729 let event_sample_ms = gs
730 .gesture_start_event_time_ms
731 .zip(time_ms)
732 .map(|(start_ms, now_ms)| now_ms - start_ms);
733 let sample_ms = if let Some(event_sample_ms) = event_sample_ms {
734 Some(match gs.last_velocity_sample_ms {
735 Some(last_sample_ms) => event_sample_ms.max(last_sample_ms),
736 None => event_sample_ms.max(0),
737 })
738 } else if let Some(start_time) = gs.gesture_start_time {
739 let elapsed_ms = start_time.elapsed().as_millis() as i64;
740 Some(match gs.last_velocity_sample_ms {
741 Some(last_sample_ms) => {
742 let mut sample_ms = if elapsed_ms <= last_sample_ms {
743 last_sample_ms + 1
744 } else {
745 elapsed_ms
746 };
747 if sample_ms - last_sample_ms > ASSUME_STOPPED_MS {
748 sample_ms = last_sample_ms + ASSUME_STOPPED_MS;
749 }
750 sample_ms
751 }
752 None => elapsed_ms,
753 })
754 } else {
755 None
756 };
757 if let Some(sample_ms) = sample_ms {
758 log::trace!(
759 target: "cranpose::velocity",
760 "sample t={sample_ms}ms pos={pos:.2} event_time={time_ms:?}"
761 );
762 gs.velocity_tracker.add_data_point(sample_ms, pos);
763 gs.last_velocity_sample_ms = Some(sample_ms);
764 }
765
766 if gs.is_dragging {
767 drop(gs);
768 let delta = if self.reverse_scrolling {
769 -incremental_delta
770 } else {
771 incremental_delta
772 };
773 let overscroll = self.overscroll.clone();
774 overscroll.apply_to_scroll(delta, |delta| self.scroll_target.apply_delta(delta));
775 self.scroll_target.invalidate();
776 true
777 } else if gs.is_overscrolling {
778 drop(gs);
779 let delta = if self.reverse_scrolling {
780 -incremental_delta
781 } else {
782 incremental_delta
783 };
784 self.apply_overscroll_delta(delta)
785 } else if edge_candidate {
786 drop(gs);
787 let delta = if self.reverse_scrolling {
788 -incremental_delta
789 } else {
790 incremental_delta
791 };
792 let detector = self.clone_for_watcher();
793 event.defer_post_dispatch_action(move || detector.apply_overscroll_candidate(delta));
794 false
795 } else {
796 false
797 }
798 }
799
800 fn apply_overscroll_delta(&self, delta: f32) -> bool {
801 self.motion_context.set_active(true);
802 let overscroll = self.overscroll.clone();
803 let target_consumed = Cell::new(0.0);
804 let before_overscroll = overscroll.offset();
805 overscroll.apply_to_scroll(delta, |delta| {
806 let consumed = self.scroll_target.apply_delta(delta);
807 target_consumed.set(consumed);
808 consumed
809 });
810 self.scroll_target.invalidate();
811 let consumed = target_consumed.get().abs() > 0.001
812 || (overscroll.offset() - before_overscroll).abs() > 0.001;
813 if !consumed {
814 self.motion_context.set_active(false);
815 }
816 consumed
817 }
818
819 fn apply_overscroll_candidate(&self, delta: f32) -> bool {
820 if self.gesture_state.borrow().disposed {
821 return false;
822 }
823 let consumed = self.apply_overscroll_delta(delta);
824 if consumed {
825 self.gesture_state.borrow_mut().is_overscrolling = true;
826 }
827 consumed
828 }
829
830 fn finish_gesture(&self, allow_fling: bool, release_time_ms: Option<i64>) -> bool {
831 let (was_dragging, interrupted, gesture_owned, velocity, start_fling) = {
832 let mut gs = self.gesture_state.borrow_mut();
833 let was_dragging = gs.is_dragging;
834 let interrupted = std::mem::take(&mut gs.interrupted_animation);
835 let gesture_owned = was_dragging || gs.is_overscrolling;
836 let velocity = if allow_fling && gesture_owned {
837 gs.release_velocity(release_time_ms)
838 } else {
839 0.0
840 };
841
842 let start_fling = allow_fling && was_dragging && velocity.abs() > MIN_FLING_VELOCITY;
843 if start_fling && let Some(fling) = gs.fling_animation.take() {
845 fling.cancel();
846 }
847
848 if was_dragging {
849 self.scroll_target.set_dragging(false);
850 }
851 gs.drag_down_position = None;
852 gs.last_position = None;
853 gs.is_dragging = false;
854 gs.is_overscrolling = false;
855 gs.axis_locked_out = false;
856 gs.gesture_start_time = None;
857 gs.gesture_start_event_time_ms = None;
858 gs.last_velocity_sample_ms = None;
859
860 (
861 was_dragging,
862 interrupted,
863 gesture_owned,
864 velocity,
865 start_fling,
866 )
867 };
868
869 if allow_fling && gesture_owned {
870 log::debug!(
871 target: "cranpose::velocity",
872 "gesture finished: fling velocity={velocity:.2} dp/s start_fling={start_fling}"
873 );
874 set_last_fling_velocity(velocity);
875 }
876
877 let adjusted_velocity = if self.reverse_scrolling {
878 -velocity
879 } else {
880 velocity
881 };
882 let adjusted_velocity = if gesture_owned {
885 adjusted_velocity - self.scroll_target.pre_fling(adjusted_velocity)
886 } else {
887 adjusted_velocity
888 };
889 let start_fling = start_fling && adjusted_velocity.abs() > MIN_FLING_VELOCITY;
890 let fling_velocity = -adjusted_velocity;
891 let has_overscroll = self.overscroll.offset().abs() > 0.001;
892
893 let settle_target = (was_dragging || interrupted)
894 .then(|| self.settle_target(start_fling, fling_velocity))
895 .flatten();
896
897 if has_overscroll {
898 self.start_overscroll_settle(-fling_velocity);
899 } else if let Some(target) = settle_target {
900 self.start_settle_animation(target, fling_velocity);
901 } else if start_fling {
902 self.start_fling_animation(fling_velocity);
903 } else {
904 self.motion_context.set_active(false);
905 }
906
907 gesture_owned
908 }
909
910 fn settle_target(&self, start_fling: bool, fling_velocity: f32) -> Option<f32> {
913 let policy = self.scroll_target.settle_policy()?;
914 let current = self.scroll_target.current_offset();
915 let proposed = if start_fling {
916 fling_rest_position(current, fling_velocity)
917 } else {
918 current
919 };
920 let target = policy(proposed, fling_velocity);
921 ((target - proposed).abs() > 0.5).then_some(target)
922 }
923
924 fn start_fling_animation(&self, fling_velocity: f32) {
925 let Some(runtime) = current_runtime_handle() else {
926 self.motion_context.set_active(false);
927 return;
928 };
929 self.motion_context.set_active(true);
930 let scroll_target = self.scroll_target.clone();
931 let fling = FlingAnimation::new(runtime);
932 let motion_context = self.motion_context.clone();
933 let initial_value = scroll_target.current_offset();
934 let scroll_target_for_fling = scroll_target.clone();
935 let detector_for_end = self.clone_for_watcher();
936 let overscroll_for_fling = self.overscroll.clone();
937
938 fling.start_fling(
939 initial_value,
940 fling_velocity,
941 move |delta| {
942 let consumed = overscroll_for_fling.apply_to_fling(delta, |delta| {
943 scroll_target_for_fling.apply_fling_delta(delta)
944 });
945 scroll_target_for_fling.invalidate();
946 consumed
947 },
948 move || {
949 scroll_target.invalidate();
950 let settle_running = detector_for_end
951 .gesture_state
952 .borrow()
953 .settle_animation
954 .as_ref()
955 .is_some_and(SettleAnimation::is_running);
956 if detector_for_end.overscroll.offset().abs() > 0.001 && !settle_running {
957 detector_for_end.start_overscroll_settle(0.0);
958 }
959 detector_for_end.update_motion_active(&motion_context);
960 },
961 );
962
963 let mut gs = self.gesture_state.borrow_mut();
964 gs.fling_animation = Some(fling);
965 }
966
967 fn start_settle_animation(&self, target: f32, initial_velocity: f32) {
968 let Some(runtime) = current_runtime_handle() else {
969 self.motion_context.set_active(false);
970 return;
971 };
972 self.motion_context.set_active(true);
973 let settle = SettleAnimation::new(runtime, SpringParams::SETTLE_POLICY);
974 let scroll_target_for_settle = self.scroll_target.clone();
975 let scroll_target_for_end = self.scroll_target.clone();
976 let detector_for_end = self.clone_for_watcher();
977 let motion_context = self.motion_context.clone();
978 settle.start_settle(
979 self.scroll_target.current_offset(),
980 initial_velocity,
981 target,
982 move |delta| {
983 let consumed = scroll_target_for_settle.apply_fling_delta(delta);
984 scroll_target_for_settle.invalidate();
985 consumed
986 },
987 move |_| {
988 scroll_target_for_end.invalidate();
989 detector_for_end.update_motion_active(&motion_context);
990 },
991 );
992 let mut gs = self.gesture_state.borrow_mut();
993 gs.settle_animation = Some(settle);
994 }
995
996 fn start_overscroll_settle(&self, initial_velocity: f32) {
997 let Some(runtime) = current_runtime_handle() else {
998 let current = self.overscroll.offset();
999 self.overscroll.apply_settle_delta(-current);
1000 self.motion_context.set_active(false);
1001 return;
1002 };
1003 let settle = SettleAnimation::new(runtime, SpringParams::OVERSCROLL_BOUNCE);
1004 let overscroll_for_settle = self.overscroll.clone();
1005 let overscroll_for_end = self.overscroll.clone();
1006 let detector_for_end = self.clone_for_watcher();
1007 let motion_context = self.motion_context.clone();
1008 let initial = self.overscroll.offset();
1009 settle.start_settle(
1010 initial,
1011 initial_velocity,
1012 0.0,
1013 move |delta| overscroll_for_settle.apply_settle_delta(delta),
1014 move |end| {
1015 let current = overscroll_for_end.offset();
1016 overscroll_for_end.apply_settle_delta(-current);
1017 if end.hit_boundary && end.velocity.abs() > MIN_FLING_VELOCITY {
1018 detector_for_end.start_fling_animation(-end.velocity);
1019 } else {
1020 detector_for_end.update_motion_active(&motion_context);
1021 }
1022 },
1023 );
1024 let mut gs = self.gesture_state.borrow_mut();
1025 gs.is_overscrolling = false;
1026 gs.settle_animation = Some(settle);
1027 }
1028
1029 fn update_motion_active(&self, motion_context: &ScrollMotionContext) {
1030 let running = self.gesture_state.borrow().is_active();
1031 if !running {
1032 motion_context.set_active(false);
1033 }
1034 }
1035
1036 fn on_up(&self, time_ms: Option<i64>) -> bool {
1037 self.finish_gesture(self.scroll_target.allows_fling(), time_ms)
1038 }
1039
1040 fn on_cancel(&self) -> bool {
1041 self.finish_gesture(false, None)
1042 }
1043
1044 fn on_scroll(&self, axis_delta: f32, event: &PointerEvent) -> bool {
1045 if axis_delta.abs() <= f32::EPSILON {
1046 return false;
1047 }
1048
1049 let delta = if self.reverse_scrolling {
1050 -axis_delta
1051 } else {
1052 axis_delta
1053 };
1054 if !self.scroll_target.can_consume(delta) && self.scroll_target.can_scroll() {
1055 let detector = self.clone_for_watcher();
1056 event.defer_post_dispatch_action(move || detector.apply_wheel_delta(delta));
1057 return false;
1058 }
1059 self.apply_wheel_delta(delta)
1060 }
1061
1062 fn apply_wheel_delta(&self, delta: f32) -> bool {
1063 {
1064 let mut gs = self.gesture_state.borrow_mut();
1065 if gs.disposed {
1066 return false;
1067 }
1068 if let Some(fling) = gs.fling_animation.take() {
1069 fling.cancel();
1070 }
1071 if let Some(settle) = gs.settle_animation.take() {
1072 settle.cancel();
1073 }
1074 gs.drag_down_position = None;
1075 gs.last_position = None;
1076 gs.is_dragging = false;
1077 gs.axis_locked_out = false;
1078 gs.gesture_start_time = None;
1079 gs.gesture_start_event_time_ms = None;
1080 gs.last_velocity_sample_ms = None;
1081 gs.velocity_tracker.reset();
1082 }
1083
1084 self.motion_context.activate_for_current_frame();
1085 let overscroll = self.overscroll.clone();
1086 let consumed =
1087 overscroll.apply_to_scroll(delta, |delta| self.scroll_target.apply_wheel_delta(delta));
1088 let overscroll_active = self.overscroll.offset().abs() > 0.001;
1089 if consumed.abs() > 0.001 {
1090 self.scroll_target.invalidate();
1091 }
1092 if consumed.abs() > 0.001 || overscroll_active {
1093 self.ensure_wheel_settle_watcher();
1094 true
1095 } else {
1096 false
1097 }
1098 }
1099
1100 fn ensure_wheel_settle_watcher(&self) {
1101 if self.scroll_target.settle_policy().is_none() && self.overscroll.offset().abs() <= 0.001 {
1102 return;
1103 }
1104 {
1105 let gs = self.gesture_state.borrow();
1106 if gs
1107 .wheel_settle_watcher
1108 .as_ref()
1109 .is_some_and(|watcher| watcher.is_running.get())
1110 {
1111 return;
1112 }
1113 }
1114 let Some(runtime) = current_runtime_handle() else {
1115 return;
1116 };
1117
1118 let is_running = Rc::new(Cell::new(true));
1119 let registration = Rc::new(RefCell::new(None));
1120
1121 struct WheelSettleLoop<S: ScrollTarget> {
1122 detector: ScrollGestureDetector<S>,
1123 gesture_state: Rc<RefCell<ScrollGestureState>>,
1124 frame_clock: cranpose_core::internal::FrameClock,
1125 is_running: Rc<Cell<bool>>,
1126 registration: Rc<RefCell<Option<FrameCallbackRegistration>>>,
1127 last_offset: Rc<Cell<f32>>,
1128 idle_nanos: Rc<Cell<u64>>,
1129 last_frame: Rc<Cell<Option<u64>>>,
1130 }
1131
1132 impl<S: ScrollTarget + 'static> WheelSettleLoop<S> {
1133 fn next(&self) -> Self {
1134 Self {
1135 detector: self.detector.clone_for_watcher(),
1136 gesture_state: Rc::clone(&self.gesture_state),
1137 frame_clock: self.frame_clock.clone(),
1138 is_running: Rc::clone(&self.is_running),
1139 registration: Rc::clone(&self.registration),
1140 last_offset: Rc::clone(&self.last_offset),
1141 idle_nanos: Rc::clone(&self.idle_nanos),
1142 last_frame: Rc::clone(&self.last_frame),
1143 }
1144 }
1145
1146 fn schedule(self) {
1147 let continuation = self.next();
1148 let registration_slot = Rc::clone(&self.registration);
1149 let new_registration = self.frame_clock.with_frame_nanos(move |frame_time_nanos| {
1150 let this = &continuation;
1151 if !this.is_running.get() {
1152 return;
1153 }
1154 {
1155 let gs = this.gesture_state.borrow();
1156 let animating = gs.is_dragging
1157 || gs
1158 .fling_animation
1159 .as_ref()
1160 .is_some_and(FlingAnimation::is_running)
1161 || gs
1162 .settle_animation
1163 .as_ref()
1164 .is_some_and(SettleAnimation::is_running);
1165 if animating {
1166 this.is_running.set(false);
1167 return;
1168 }
1169 }
1170
1171 let offset = this.detector.scroll_target.current_offset();
1172 let dt = this
1173 .last_frame
1174 .get()
1175 .map_or(0, |last| frame_time_nanos.saturating_sub(last));
1176 this.last_frame.set(Some(frame_time_nanos));
1177 if (offset - this.last_offset.get()).abs() > 0.01 {
1178 this.last_offset.set(offset);
1179 this.idle_nanos.set(0);
1180 } else {
1181 this.idle_nanos.set(this.idle_nanos.get() + dt);
1182 }
1183
1184 if this.idle_nanos.get() >= WHEEL_SETTLE_IDLE_NANOS {
1185 this.is_running.set(false);
1186 if this.detector.overscroll.offset().abs() > 0.001 {
1187 this.detector.start_overscroll_settle(0.0);
1188 return;
1189 }
1190 if let Some(policy) = this.detector.scroll_target.settle_policy() {
1191 let target = policy(offset, 0.0);
1192 if (target - offset).abs() > 0.5 {
1193 this.detector.start_settle_animation(target, 0.0);
1194 }
1195 }
1196 return;
1197 }
1198
1199 continuation.next().schedule();
1200 });
1201 *registration_slot.borrow_mut() = Some(new_registration);
1202 }
1203 }
1204
1205 WheelSettleLoop {
1206 detector: self.clone_for_watcher(),
1207 gesture_state: Rc::clone(&self.gesture_state),
1208 frame_clock: runtime.frame_clock(),
1209 is_running: Rc::clone(&is_running),
1210 registration: Rc::clone(®istration),
1211 last_offset: Rc::new(Cell::new(self.scroll_target.current_offset())),
1212 idle_nanos: Rc::new(Cell::new(0u64)),
1213 last_frame: Rc::new(Cell::new(None::<u64>)),
1214 }
1215 .schedule();
1216
1217 self.gesture_state.borrow_mut().wheel_settle_watcher = Some(WheelSettleWatcher {
1218 is_running,
1219 registration,
1220 });
1221 }
1222
1223 fn clone_for_watcher(&self) -> ScrollGestureDetector<S> {
1224 ScrollGestureDetector {
1225 gesture_state: Rc::clone(&self.gesture_state),
1226 scroll_target: self.scroll_target.clone(),
1227 is_vertical: self.is_vertical,
1228 reverse_scrolling: self.reverse_scrolling,
1229 overscroll: self.overscroll.clone(),
1230 motion_context: self.motion_context.clone(),
1231 }
1232 }
1233}
1234
1235pub(crate) struct MotionContextAnimatedNode {
1236 state: NodeState,
1237 motion_context: ScrollMotionContext,
1238 invalidation_callback_id: Option<u64>,
1239 overscroll_callback_id: Option<u64>,
1240 node_id: Option<NodeId>,
1241}
1242
1243impl MotionContextAnimatedNode {
1244 fn new(motion_context: ScrollMotionContext) -> Self {
1245 Self {
1246 state: NodeState::new(),
1247 motion_context,
1248 invalidation_callback_id: None,
1249 overscroll_callback_id: None,
1250 node_id: None,
1251 }
1252 }
1253
1254 pub(crate) fn is_active(&self) -> bool {
1255 self.motion_context.is_active()
1256 }
1257
1258 fn attach_callbacks(&mut self) {
1259 if let Some(node_id) = self.node_id {
1260 self.invalidation_callback_id = Some(self.motion_context.add_invalidate_callback(
1261 Box::new(move || schedule_modifier_slices_repass(node_id)),
1262 ));
1263 self.overscroll_callback_id = Some(
1264 self.motion_context
1265 .overscroll()
1266 .add_invalidate_callback(Box::new(move || {
1267 crate::schedule_measure_repass(node_id);
1268 schedule_modifier_slices_repass(node_id);
1269 })),
1270 );
1271 }
1272 }
1273
1274 fn detach_callbacks(&mut self) {
1275 if let Some(id) = self.invalidation_callback_id.take() {
1276 self.motion_context.remove_invalidate_callback(id);
1277 }
1278 if let Some(id) = self.overscroll_callback_id.take() {
1279 self.motion_context
1280 .overscroll()
1281 .remove_invalidate_callback(id);
1282 }
1283 }
1284}
1285
1286pub(crate) struct TranslatedContentContextNode {
1287 state: NodeState,
1288 identity: usize,
1289 offset_source: TranslatedContentOffsetSource,
1290 overscroll: crate::scroll::OverscrollEffect,
1291 overscroll_callback_id: Option<u64>,
1292 node_id: Option<NodeId>,
1293}
1294
1295impl TranslatedContentContextNode {
1296 fn new(
1297 identity: usize,
1298 offset_source: TranslatedContentOffsetSource,
1299 overscroll: crate::scroll::OverscrollEffect,
1300 ) -> Self {
1301 Self {
1302 state: NodeState::new(),
1303 identity,
1304 offset_source,
1305 overscroll,
1306 overscroll_callback_id: None,
1307 node_id: None,
1308 }
1309 }
1310
1311 pub(crate) fn is_active(&self) -> bool {
1312 true
1313 }
1314
1315 pub(crate) fn identity(&self) -> usize {
1316 self.identity
1317 }
1318
1319 pub(crate) fn content_offset_reader(&self) -> Option<Rc<dyn Fn() -> Point>> {
1320 self.offset_source
1321 .content_offset_reader(self.overscroll.clone())
1322 }
1323
1324 fn attach_callback(&mut self) {
1325 if let Some(node_id) = self.node_id {
1326 self.overscroll_callback_id =
1327 Some(self.overscroll.add_invalidate_callback(Box::new(move || {
1328 schedule_modifier_slices_repass(node_id);
1329 })));
1330 }
1331 }
1332
1333 fn detach_callback(&mut self) {
1334 if let Some(id) = self.overscroll_callback_id.take() {
1335 self.overscroll.remove_invalidate_callback(id);
1336 }
1337 }
1338}
1339
1340impl DelegatableNode for TranslatedContentContextNode {
1341 fn node_state(&self) -> &NodeState {
1342 &self.state
1343 }
1344}
1345
1346impl ModifierNode for TranslatedContentContextNode {
1347 fn on_attach(&mut self, context: &mut dyn cranpose_foundation::ModifierNodeContext) {
1348 self.node_id = context.node_id();
1349 self.attach_callback();
1350 }
1351
1352 fn on_detach(&mut self) {
1353 self.detach_callback();
1354 self.node_id = None;
1355 }
1356}
1357
1358impl DelegatableNode for MotionContextAnimatedNode {
1359 fn node_state(&self) -> &NodeState {
1360 &self.state
1361 }
1362}
1363
1364impl ModifierNode for MotionContextAnimatedNode {
1365 fn on_attach(&mut self, context: &mut dyn cranpose_foundation::ModifierNodeContext) {
1366 self.node_id = context.node_id();
1367 self.attach_callbacks();
1368 }
1369
1370 fn on_detach(&mut self) {
1371 self.detach_callbacks();
1372 self.node_id = None;
1373 }
1374}
1375
1376#[derive(Clone)]
1377struct MotionContextAnimatedElement {
1378 motion_context: ScrollMotionContext,
1379}
1380
1381impl MotionContextAnimatedElement {
1382 fn new(motion_context: ScrollMotionContext) -> Self {
1383 Self { motion_context }
1384 }
1385}
1386
1387impl std::fmt::Debug for MotionContextAnimatedElement {
1388 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1389 f.debug_struct("MotionContextAnimatedElement").finish()
1390 }
1391}
1392
1393impl PartialEq for MotionContextAnimatedElement {
1394 fn eq(&self, other: &Self) -> bool {
1395 self.motion_context.ptr_eq(&other.motion_context)
1396 }
1397}
1398
1399impl Eq for MotionContextAnimatedElement {}
1400
1401impl std::hash::Hash for MotionContextAnimatedElement {
1402 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1403 self.motion_context.stable_key().hash(state);
1404 }
1405}
1406
1407impl ModifierNodeElement for MotionContextAnimatedElement {
1408 type Node = MotionContextAnimatedNode;
1409
1410 fn create(&self) -> Self::Node {
1411 MotionContextAnimatedNode::new(self.motion_context.clone())
1412 }
1413
1414 fn update(&self, node: &mut Self::Node) {
1415 if node.motion_context.ptr_eq(&self.motion_context) {
1416 return;
1417 }
1418 node.detach_callbacks();
1419 node.motion_context = self.motion_context.clone();
1420 node.attach_callbacks();
1421 }
1422
1423 fn capabilities(&self) -> NodeCapabilities {
1424 NodeCapabilities::LAYOUT
1425 }
1426}
1427
1428#[derive(Clone)]
1429enum TranslatedContentOffsetSource {
1430 LayoutContentOffset,
1431 LazyList {
1432 state: LazyListState,
1433 is_vertical: bool,
1434 reverse_scrolling: bool,
1435 },
1436}
1437
1438impl TranslatedContentOffsetSource {
1439 fn content_offset_reader(
1440 &self,
1441 overscroll: crate::scroll::OverscrollEffect,
1442 ) -> Option<Rc<dyn Fn() -> Point>> {
1443 match self {
1444 Self::LayoutContentOffset => None,
1445 Self::LazyList {
1446 state, is_vertical, ..
1447 } => Some(Rc::new(lazy_list_content_offset_reader(
1448 *state,
1449 *is_vertical,
1450 overscroll,
1451 ))),
1452 }
1453 }
1454
1455 fn is_vertical(&self) -> Option<bool> {
1456 match self {
1457 Self::LayoutContentOffset => None,
1458 Self::LazyList { is_vertical, .. } => Some(*is_vertical),
1459 }
1460 }
1461
1462 fn reverse_scrolling(&self) -> Option<bool> {
1463 match self {
1464 Self::LayoutContentOffset => None,
1465 Self::LazyList {
1466 reverse_scrolling, ..
1467 } => Some(*reverse_scrolling),
1468 }
1469 }
1470}
1471
1472fn lazy_list_content_offset_reader(
1473 state: LazyListState,
1474 is_vertical: bool,
1475 overscroll: crate::scroll::OverscrollEffect,
1476) -> impl Fn() -> Point {
1477 move || {
1478 let Some((viewport, main_offset)) = state
1479 .with_layout_info(|info| {
1480 (!info.visible_items_info.is_empty())
1481 .then_some((info.viewport_size, info.snap_anchor_offset))
1482 })
1483 .flatten()
1484 else {
1485 return Point::default();
1486 };
1487 overscroll.set_dimension(viewport);
1488 let bounce = overscroll.offset();
1489 if is_vertical {
1490 Point::new(0.0, main_offset + bounce)
1491 } else {
1492 Point::new(main_offset + bounce, 0.0)
1493 }
1494 }
1495}
1496
1497#[derive(Clone)]
1498struct TranslatedContentContextElement {
1499 identity: usize,
1500 offset_source: TranslatedContentOffsetSource,
1501 overscroll: crate::scroll::OverscrollEffect,
1502}
1503
1504impl TranslatedContentContextElement {
1505 fn new(
1506 identity: usize,
1507 offset_source: TranslatedContentOffsetSource,
1508 overscroll: crate::scroll::OverscrollEffect,
1509 ) -> Self {
1510 Self {
1511 identity,
1512 offset_source,
1513 overscroll,
1514 }
1515 }
1516}
1517
1518impl std::fmt::Debug for TranslatedContentContextElement {
1519 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1520 let offset_source = match &self.offset_source {
1521 TranslatedContentOffsetSource::LayoutContentOffset => "layout",
1522 TranslatedContentOffsetSource::LazyList { .. } => "lazy_list",
1523 };
1524 f.debug_struct("TranslatedContentContextElement")
1525 .field("identity", &self.identity)
1526 .field("offset_source", &offset_source)
1527 .finish()
1528 }
1529}
1530
1531impl PartialEq for TranslatedContentContextElement {
1532 fn eq(&self, other: &Self) -> bool {
1533 self.identity == other.identity
1534 && self.overscroll.ptr_eq(&other.overscroll)
1535 && self.offset_source.is_vertical() == other.offset_source.is_vertical()
1536 && self.offset_source.reverse_scrolling() == other.offset_source.reverse_scrolling()
1537 }
1538}
1539
1540impl Eq for TranslatedContentContextElement {}
1541
1542impl std::hash::Hash for TranslatedContentContextElement {
1543 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1544 self.identity.hash(state);
1545 self.offset_source.is_vertical().hash(state);
1546 self.offset_source.reverse_scrolling().hash(state);
1547 }
1548}
1549
1550impl ModifierNodeElement for TranslatedContentContextElement {
1551 type Node = TranslatedContentContextNode;
1552
1553 fn create(&self) -> Self::Node {
1554 TranslatedContentContextNode::new(
1555 self.identity,
1556 self.offset_source.clone(),
1557 self.overscroll.clone(),
1558 )
1559 }
1560
1561 fn update(&self, node: &mut Self::Node) {
1562 node.identity = self.identity;
1563 node.offset_source = self.offset_source.clone();
1564 if !node.overscroll.ptr_eq(&self.overscroll) {
1565 node.detach_callback();
1566 node.overscroll = self.overscroll.clone();
1567 node.attach_callback();
1568 }
1569 }
1570
1571 fn capabilities(&self) -> NodeCapabilities {
1572 NodeCapabilities::LAYOUT
1573 }
1574}
1575
1576impl Modifier {
1577 pub fn horizontal_scroll(self, state: ScrollState, reverse_scrolling: bool) -> Self {
1595 self.then(scroll_impl(
1596 state,
1597 false,
1598 reverse_scrolling,
1599 None,
1600 (0.0, 0.0),
1601 ))
1602 }
1603
1604 pub fn vertical_scroll(self, state: ScrollState, reverse_scrolling: bool) -> Self {
1613 self.then(scroll_impl(
1614 state,
1615 true,
1616 reverse_scrolling,
1617 None,
1618 (0.0, 0.0),
1619 ))
1620 }
1621
1622 pub fn vertical_scroll_with_content_padding(
1627 self,
1628 state: ScrollState,
1629 reverse_scrolling: bool,
1630 padding: cranpose_ui_graphics::EdgeInsets,
1631 ) -> Self {
1632 let density = crate::density();
1633 self.then(scroll_impl(
1634 state,
1635 true,
1636 reverse_scrolling,
1637 None,
1638 (density.dp(padding.top), density.dp(padding.bottom)),
1639 ))
1640 .window_insets_padding(padding)
1641 }
1642
1643 pub fn horizontal_scroll_with_content_padding(
1648 self,
1649 state: ScrollState,
1650 reverse_scrolling: bool,
1651 padding: cranpose_ui_graphics::EdgeInsets,
1652 ) -> Self {
1653 let density = crate::density();
1654 self.then(scroll_impl(
1655 state,
1656 false,
1657 reverse_scrolling,
1658 None,
1659 (density.dp(padding.left), density.dp(padding.right)),
1660 ))
1661 .window_insets_padding(padding)
1662 }
1663
1664 pub fn horizontal_scroll_without_gestures(
1671 self,
1672 state: ScrollState,
1673 reverse_scrolling: bool,
1674 ) -> Self {
1675 self.then(scroll_impl(
1676 state,
1677 false,
1678 reverse_scrolling,
1679 Some(Rc::new(|| false)),
1680 (0.0, 0.0),
1681 ))
1682 }
1683
1684 pub fn vertical_scroll_without_gestures(
1688 self,
1689 state: ScrollState,
1690 reverse_scrolling: bool,
1691 ) -> Self {
1692 self.then(scroll_impl(
1693 state,
1694 true,
1695 reverse_scrolling,
1696 Some(Rc::new(|| false)),
1697 (0.0, 0.0),
1698 ))
1699 }
1700}
1701
1702fn scroll_impl(
1703 state: ScrollState,
1704 is_vertical: bool,
1705 reverse_scrolling: bool,
1706 guard: Option<Rc<dyn Fn() -> bool>>,
1707 content_padding: (f32, f32),
1708) -> Modifier {
1709 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::ScrollState {
1710 state_id: state.id(),
1711 is_vertical,
1712 reverse_scrolling,
1713 });
1714 motion_context
1715 .observe_activity(|| Box::new(move |active| state.set_scroll_in_progress(active)));
1716
1717 let pointer_input = nested_drag_gesture_input(
1718 (state.id(), is_vertical),
1719 DragGesture {
1720 target: state,
1721 is_vertical,
1722 reverse_input: false,
1723 motion_context: motion_context.clone(),
1724 guard,
1725 },
1726 );
1727
1728 let overscroll = motion_context.overscroll();
1729 let element = ScrollElement::new(state, overscroll.clone(), is_vertical, reverse_scrolling);
1730 let layout_modifier =
1731 Modifier::with_element(element).with_inspector_metadata(inspector_metadata(
1732 if is_vertical {
1733 "verticalScroll"
1734 } else {
1735 "horizontalScroll"
1736 },
1737 move |info| {
1738 info.add_property("isVertical", is_vertical.to_string());
1739 info.add_property("reverseScrolling", reverse_scrolling.to_string());
1740 },
1741 ));
1742 let motion_modifier = Modifier::with_element(MotionContextAnimatedElement::new(motion_context));
1743 let translated_content_modifier = Modifier::with_element(TranslatedContentContextElement::new(
1744 state.id() as usize,
1745 TranslatedContentOffsetSource::LayoutContentOffset,
1746 overscroll,
1747 ));
1748
1749 pointer_input
1753 .clip_to_bounds()
1754 .then(motion_modifier)
1755 .then(translated_content_modifier)
1756 .then(layout_modifier)
1757 .semantics(scroll_semantics(
1758 state,
1759 is_vertical,
1760 reverse_scrolling,
1761 content_padding,
1762 ))
1763}
1764
1765fn scroll_semantics(
1770 state: ScrollState,
1771 is_vertical: bool,
1772 reverse_scrolling: bool,
1773 content_padding: (f32, f32),
1774) -> impl Fn(&mut cranpose_foundation::SemanticsConfiguration) + 'static {
1775 move |config| {
1776 let range = cranpose_foundation::ScrollAxisRange::new(
1777 state.value(),
1778 state.max_value(),
1779 reverse_scrolling,
1780 )
1781 .with_content_padding(content_padding.0, content_padding.1);
1782 if is_vertical {
1783 config.vertical_scroll = Some(range);
1784 } else {
1785 config.horizontal_scroll = Some(range);
1786 }
1787 config.scroll_by = Some(cranpose_foundation::SemanticsScrollBy::new(
1788 move |dx, dy| {
1789 let delta = if is_vertical { dy } else { dx };
1790 state.dispatch_raw_delta(delta).abs() > f32::EPSILON
1791 },
1792 ));
1793 }
1794}
1795
1796use cranpose_foundation::lazy::{LazyListState, LazySettlePolicy};
1797
1798impl Modifier {
1799 pub fn lazy_vertical_scroll(self, state: LazyListState, reverse_scrolling: bool) -> Self {
1810 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::LazyList {
1811 state_identity: state.inner_ptr() as usize,
1812 is_vertical: true,
1813 reverse_scrolling,
1814 });
1815 self.lazy_vertical_scroll_with_context(state, reverse_scrolling, (0.0, 0.0), motion_context)
1816 }
1817
1818 pub(crate) fn lazy_vertical_scroll_with_context(
1819 self,
1820 state: LazyListState,
1821 reverse_scrolling: bool,
1822 content_padding: (f32, f32),
1823 motion_context: ScrollMotionContext,
1824 ) -> Self {
1825 self.then(lazy_scroll_impl(
1826 state,
1827 true,
1828 reverse_scrolling,
1829 content_padding,
1830 motion_context,
1831 ))
1832 }
1833
1834 pub(crate) fn lazy_horizontal_scroll_with_context(
1835 self,
1836 state: LazyListState,
1837 reverse_scrolling: bool,
1838 content_padding: (f32, f32),
1839 motion_context: ScrollMotionContext,
1840 ) -> Self {
1841 self.then(lazy_scroll_impl(
1842 state,
1843 false,
1844 reverse_scrolling,
1845 content_padding,
1846 motion_context,
1847 ))
1848 }
1849}
1850
1851fn lazy_scroll_impl(
1852 state: LazyListState,
1853 is_vertical: bool,
1854 reverse_scrolling: bool,
1855 content_padding: (f32, f32),
1856 motion_context: ScrollMotionContext,
1857) -> Modifier {
1858 let list_state = state;
1859 let state_id = state.inner_ptr() as usize;
1860 let key = (state_id, is_vertical, reverse_scrolling);
1861 motion_context
1862 .observe_activity(|| Box::new(move |active| state.set_gesture_scroll_active(active)));
1863 let overscroll = motion_context.overscroll();
1864 let translated_content_modifier = Modifier::with_element(TranslatedContentContextElement::new(
1865 state_id,
1866 TranslatedContentOffsetSource::LazyList {
1867 state,
1868 is_vertical,
1869 reverse_scrolling,
1870 },
1871 overscroll,
1872 ));
1873
1874 Modifier::with_element(MotionContextAnimatedElement::new(motion_context.clone()))
1875 .then(translated_content_modifier)
1876 .then(nested_drag_gesture_input(
1877 key,
1878 DragGesture {
1879 target: list_state,
1880 is_vertical,
1881 reverse_input: reverse_scrolling,
1882 motion_context,
1883 guard: None,
1884 },
1885 ))
1886 .semantics(lazy_scroll_semantics(
1887 list_state,
1888 is_vertical,
1889 reverse_scrolling,
1890 content_padding,
1891 ))
1892}
1893
1894pub(crate) fn lazy_scroll_semantics(
1900 state: LazyListState,
1901 is_vertical: bool,
1902 reverse_scrolling: bool,
1903 content_padding: (f32, f32),
1904) -> impl Fn(&mut cranpose_foundation::SemanticsConfiguration) + 'static {
1905 move |config| {
1906 let mut value = state.first_visible_item_index() as f32;
1907 if state.first_visible_item_scroll_offset() > 0.0 {
1908 value += 0.5;
1909 }
1910 let max_value = if state.can_scroll_forward() {
1911 value + 1.0
1912 } else {
1913 value
1914 };
1915 let range = cranpose_foundation::ScrollAxisRange::new(value, max_value, reverse_scrolling)
1916 .with_content_padding(content_padding.0, content_padding.1);
1917 let count = state.total_items_count();
1918 config.collection = Some(cranpose_foundation::CollectionInfo {
1919 rows: if is_vertical { count } else { 1 },
1920 columns: if is_vertical { 1 } else { count },
1921 });
1922 config
1923 .role
1924 .get_or_insert(cranpose_foundation::SemanticsWidgetRole::List);
1925 if is_vertical {
1926 config.vertical_scroll = Some(range);
1927 } else {
1928 config.horizontal_scroll = Some(range);
1929 }
1930 config.scroll_by = Some(cranpose_foundation::SemanticsScrollBy::new(
1931 move |dx, dy| {
1932 let delta = if is_vertical { dy } else { dx };
1933 match state.settle_policy() {
1934 Some(policy) => settle_reader_scroll(&state, &policy, delta),
1935 None => state.dispatch_scroll_delta(-delta).abs() > f32::EPSILON,
1936 }
1937 },
1938 ));
1939 config.scroll_to_index = Some(cranpose_foundation::SemanticsScrollToIndex::new(
1940 move |index| jump_to_row(&state, index),
1941 ));
1942 }
1943}
1944
1945fn settle_reader_scroll(state: &LazyListState, policy: &LazySettlePolicy, delta: f32) -> bool {
1950 let can_move = if delta > 0.0 {
1951 state.can_scroll_forward_non_reactive()
1952 } else {
1953 state.can_scroll_backward_non_reactive()
1954 };
1955 let size = state.average_item_size();
1956 if !can_move || size <= 0.0 {
1957 return false;
1958 }
1959 let first = state.first_visible_item_index_non_reactive() as f32;
1960 let reached =
1961 (first * size + state.first_visible_item_scroll_offset_non_reactive() + delta).max(0.0);
1962 let last = state.total_items_count().saturating_sub(1) as f32;
1963 let index = (reached / size).floor().min(last);
1964 let target = policy(reached - index * size, 0.0);
1965 crate::AnimateScroll::animate_scroll_to_item(state, index as usize, target);
1966 true
1967}
1968
1969fn jump_to_row(state: &LazyListState, index: usize) -> bool {
1973 if index >= state.total_items_count() {
1974 return false;
1975 }
1976 state.scroll_to_item(index, 0.0);
1977 true
1978}
1979
1980impl Modifier {
1981 pub fn draggable(self, axis: Axis, state: DraggableState) -> Self {
1993 self.then(draggable_impl(axis, state, None))
1994 }
1995
1996 pub fn scrollable(self, axis: Axis, state: ScrollableState) -> Self {
2002 let is_vertical = axis.is_vertical();
2003 let identity = state.identity();
2004 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::Draggable {
2005 state_identity: identity,
2006 is_vertical,
2007 });
2008 self.then(Modifier::with_element(MotionContextAnimatedElement::new(
2009 motion_context.clone(),
2010 )))
2011 .then(nested_drag_gesture_input(
2012 (identity, is_vertical),
2013 DragGesture {
2014 target: state,
2015 is_vertical,
2016 reverse_input: false,
2017 motion_context,
2018 guard: None,
2019 },
2020 ))
2021 }
2022
2023 pub fn draggable_guarded(
2029 self,
2030 axis: Axis,
2031 state: DraggableState,
2032 guard: impl Fn() -> bool + 'static,
2033 ) -> Self {
2034 self.then(draggable_impl(axis, state, Some(Rc::new(guard))))
2035 }
2036}
2037
2038fn draggable_impl(
2039 axis: Axis,
2040 state: DraggableState,
2041 guard: Option<Rc<dyn Fn() -> bool>>,
2042) -> Modifier {
2043 let is_vertical = axis.is_vertical();
2044 let identity = state.identity();
2045 drag_gesture_input(
2046 (identity, is_vertical),
2047 DragGesture {
2048 target: state,
2049 is_vertical,
2050 reverse_input: false,
2051 motion_context: scroll_motion_context_for_key(ScrollMotionContextKey::Draggable {
2052 state_identity: identity,
2053 is_vertical,
2054 }),
2055 guard,
2056 },
2057 )
2058}