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