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