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,
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
400const WHEEL_SETTLE_IDLE_NANOS: u64 = 180_000_000;
401
402struct WheelSettleWatcher {
403 is_running: Rc<Cell<bool>>,
404 registration: Rc<RefCell<Option<FrameCallbackRegistration>>>,
405}
406
407impl WheelSettleWatcher {
408 fn cancel(&self) {
409 self.is_running.set(false);
410 self.registration.borrow_mut().take();
411 }
412}
413
414struct ScrollGestureDetector<S: ScrollTarget> {
415 gesture_state: Rc<RefCell<ScrollGestureState>>,
416
417 scroll_target: S,
418
419 is_vertical: bool,
420
421 reverse_scrolling: bool,
422
423 overscroll: crate::scroll::OverscrollEffect,
424
425 motion_context: ScrollMotionContext,
426}
427
428impl<S: ScrollTarget + 'static> ScrollGestureDetector<S> {
429 fn new(
430 gesture_state: Rc<RefCell<ScrollGestureState>>,
431 scroll_target: S,
432 is_vertical: bool,
433 reverse_scrolling: bool,
434 overscroll: crate::scroll::OverscrollEffect,
435 motion_context: ScrollMotionContext,
436 ) -> Self {
437 Self {
438 gesture_state,
439 scroll_target,
440 is_vertical,
441 reverse_scrolling,
442 overscroll,
443 motion_context,
444 }
445 }
446
447 fn on_down(&self, position: Point, time_ms: Option<i64>) -> bool {
448 let mut gs = self.gesture_state.borrow_mut();
449
450 if let Some(fling) = gs.fling_animation.take() {
451 fling.cancel();
452 }
453 if let Some(settle) = gs.settle_animation.take() {
454 settle.cancel();
455 }
456 if let Some(watcher) = gs.wheel_settle_watcher.take() {
457 watcher.cancel();
458 }
459 self.motion_context.set_active(false);
460
461 gs.drag_down_position = Some(position);
462 gs.last_position = Some(position);
463 gs.is_dragging = false;
464 gs.axis_locked_out = false;
465 gs.velocity_tracker.reset();
466 gs.gesture_start_time = Some(Instant::now());
467 gs.gesture_start_event_time_ms = time_ms;
468 gs.is_overscrolling = self.overscroll.offset().abs() > 0.001;
469
470 let pos = if self.is_vertical {
471 position.y
472 } else {
473 position.x
474 };
475 gs.velocity_tracker.add_data_point(0, pos);
476 gs.last_velocity_sample_ms = Some(0);
477
478 false
479 }
480
481 fn on_move(
482 &self,
483 position: Point,
484 buttons: PointerButtons,
485 time_ms: Option<i64>,
486 event: &PointerEvent,
487 ) -> bool {
488 let mut gs = self.gesture_state.borrow_mut();
489
490 if !buttons.contains(PointerButton::Primary) && gs.drag_down_position.is_some() {
491 if gs.is_dragging {
492 self.scroll_target.set_dragging(false);
493 }
494 gs.drag_down_position = None;
495 gs.last_position = None;
496 gs.is_dragging = false;
497 gs.axis_locked_out = false;
498 gs.gesture_start_time = None;
499 gs.gesture_start_event_time_ms = None;
500 gs.last_velocity_sample_ms = None;
501 gs.velocity_tracker.reset();
502 self.motion_context.set_active(false);
503 return false;
504 }
505
506 let Some(down_pos) = gs.drag_down_position else {
507 return false;
508 };
509
510 let Some(last_pos) = gs.last_position else {
511 gs.last_position = Some(position);
512 return false;
513 };
514
515 let incremental_delta = calculate_incremental_delta(last_pos, position, self.is_vertical);
516
517 let mut edge_candidate = false;
518 if !gs.is_dragging && !gs.axis_locked_out {
519 let signed_main_delta = calculate_total_delta(down_pos, position, self.is_vertical);
520 let main_delta = signed_main_delta.abs();
521 let cross_delta = calculate_total_delta(down_pos, position, !self.is_vertical).abs();
522 if main_delta > DRAG_THRESHOLD && main_delta >= cross_delta {
523 if self.scroll_target.can_consume(signed_main_delta) {
524 gs.is_dragging = true;
525 self.scroll_target.set_dragging(true);
526 self.motion_context.set_active(true);
527 } else if self.scroll_target.can_scroll() {
528 edge_candidate = true;
529 }
530 } else if cross_delta > DRAG_THRESHOLD && cross_delta > main_delta {
531 gs.axis_locked_out = true;
532 }
533 }
534
535 gs.last_position = Some(position);
536
537 let pos = if self.is_vertical {
538 position.y
539 } else {
540 position.x
541 };
542 let event_sample_ms = gs
543 .gesture_start_event_time_ms
544 .zip(time_ms)
545 .map(|(start_ms, now_ms)| now_ms - start_ms);
546 let sample_ms = if let Some(event_sample_ms) = event_sample_ms {
547 Some(match gs.last_velocity_sample_ms {
548 Some(last_sample_ms) => event_sample_ms.max(last_sample_ms),
549 None => event_sample_ms.max(0),
550 })
551 } else if let Some(start_time) = gs.gesture_start_time {
552 let elapsed_ms = start_time.elapsed().as_millis() as i64;
553 Some(match gs.last_velocity_sample_ms {
554 Some(last_sample_ms) => {
555 let mut sample_ms = if elapsed_ms <= last_sample_ms {
556 last_sample_ms + 1
557 } else {
558 elapsed_ms
559 };
560 if sample_ms - last_sample_ms > ASSUME_STOPPED_MS {
561 sample_ms = last_sample_ms + ASSUME_STOPPED_MS;
562 }
563 sample_ms
564 }
565 None => elapsed_ms,
566 })
567 } else {
568 None
569 };
570 if let Some(sample_ms) = sample_ms {
571 log::trace!(
572 target: "cranpose::velocity",
573 "sample t={sample_ms}ms pos={pos:.2} event_time={time_ms:?}"
574 );
575 gs.velocity_tracker.add_data_point(sample_ms, pos);
576 gs.last_velocity_sample_ms = Some(sample_ms);
577 }
578
579 if gs.is_dragging {
580 drop(gs);
581 let delta = if self.reverse_scrolling {
582 -incremental_delta
583 } else {
584 incremental_delta
585 };
586 let overscroll = self.overscroll.clone();
587 overscroll.apply_to_scroll(delta, |delta| self.scroll_target.apply_delta(delta));
588 self.scroll_target.invalidate();
589 true
590 } else if gs.is_overscrolling {
591 drop(gs);
592 let delta = if self.reverse_scrolling {
593 -incremental_delta
594 } else {
595 incremental_delta
596 };
597 self.apply_overscroll_delta(delta)
598 } else if edge_candidate {
599 drop(gs);
600 let delta = if self.reverse_scrolling {
601 -incremental_delta
602 } else {
603 incremental_delta
604 };
605 let detector = self.clone_for_watcher();
606 event.defer_post_dispatch_action(move || detector.apply_overscroll_candidate(delta));
607 false
608 } else {
609 false
610 }
611 }
612
613 fn apply_overscroll_delta(&self, delta: f32) -> bool {
614 self.motion_context.set_active(true);
615 let overscroll = self.overscroll.clone();
616 let target_consumed = Cell::new(0.0);
617 let before_overscroll = overscroll.offset();
618 overscroll.apply_to_scroll(delta, |delta| {
619 let consumed = self.scroll_target.apply_delta(delta);
620 target_consumed.set(consumed);
621 consumed
622 });
623 self.scroll_target.invalidate();
624 let consumed = target_consumed.get().abs() > 0.001
625 || (overscroll.offset() - before_overscroll).abs() > 0.001;
626 if !consumed {
627 self.motion_context.set_active(false);
628 }
629 consumed
630 }
631
632 fn apply_overscroll_candidate(&self, delta: f32) -> bool {
633 if self.gesture_state.borrow().disposed {
634 return false;
635 }
636 let consumed = self.apply_overscroll_delta(delta);
637 if consumed {
638 self.gesture_state.borrow_mut().is_overscrolling = true;
639 }
640 consumed
641 }
642
643 fn finish_gesture(&self, allow_fling: bool, release_time_ms: Option<i64>) -> bool {
644 let (was_dragging, gesture_owned, velocity, start_fling, existing_fling) = {
645 let mut gs = self.gesture_state.borrow_mut();
646 let was_dragging = gs.is_dragging;
647 let gesture_owned = was_dragging || gs.is_overscrolling;
648 let mut velocity = 0.0;
649
650 if allow_fling && gesture_owned && gs.gesture_start_time.is_some() {
651 let release_sample_ms = release_time_ms
652 .zip(gs.gesture_start_event_time_ms)
653 .map(|(release_ms, start_ms)| release_ms - start_ms)
654 .or_else(|| {
655 gs.gesture_start_time
656 .map(|start| start.elapsed().as_millis() as i64)
657 });
658 let rested_before_release = release_sample_ms
659 .zip(gs.last_velocity_sample_ms)
660 .is_some_and(|(release_ms, last_sample_ms)| {
661 release_ms - last_sample_ms > ASSUME_STOPPED_MS
662 });
663 if !rested_before_release {
664 velocity = gs
665 .velocity_tracker
666 .calculate_velocity_with_max(MAX_FLING_VELOCITY);
667 }
668 }
669
670 let start_fling = allow_fling && was_dragging && velocity.abs() > MIN_FLING_VELOCITY;
671 let existing_fling = if start_fling {
672 gs.fling_animation.take()
673 } else {
674 None
675 };
676
677 if was_dragging {
678 self.scroll_target.set_dragging(false);
679 }
680 gs.drag_down_position = None;
681 gs.last_position = None;
682 gs.is_dragging = false;
683 gs.is_overscrolling = false;
684 gs.axis_locked_out = false;
685 gs.gesture_start_time = None;
686 gs.gesture_start_event_time_ms = None;
687 gs.last_velocity_sample_ms = None;
688
689 (
690 was_dragging,
691 gesture_owned,
692 velocity,
693 start_fling,
694 existing_fling,
695 )
696 };
697
698 if allow_fling && gesture_owned {
699 log::debug!(
700 target: "cranpose::velocity",
701 "gesture finished: fling velocity={velocity:.2} dp/s start_fling={start_fling}"
702 );
703 set_last_fling_velocity(velocity);
704 }
705
706 let adjusted_velocity = if self.reverse_scrolling {
707 -velocity
708 } else {
709 velocity
710 };
711 let fling_velocity = -adjusted_velocity;
712 let has_overscroll = self.overscroll.offset().abs() > 0.001;
713
714 let settle_target = if was_dragging {
715 self.scroll_target.settle_policy().and_then(|policy| {
716 let current = self.scroll_target.current_offset();
717 let proposed = if start_fling {
718 fling_rest_position(current, fling_velocity)
719 } else {
720 current
721 };
722 let target = policy(proposed, fling_velocity);
723 ((target - proposed).abs() > 0.5).then_some(target)
724 })
725 } else {
726 None
727 };
728
729 if has_overscroll {
730 if let Some(old_fling) = existing_fling {
731 old_fling.cancel();
732 }
733 self.start_overscroll_settle(-fling_velocity);
734 } else if let Some(target) = settle_target {
735 if let Some(old_fling) = existing_fling {
736 old_fling.cancel();
737 }
738 self.start_settle_animation(target, fling_velocity);
739 } else if start_fling {
740 if let Some(old_fling) = existing_fling {
741 old_fling.cancel();
742 }
743 self.start_fling_animation(fling_velocity);
744 } else {
745 self.motion_context.set_active(false);
746 }
747
748 gesture_owned
749 }
750
751 fn start_fling_animation(&self, fling_velocity: f32) {
752 let Some(runtime) = current_runtime_handle() else {
753 self.motion_context.set_active(false);
754 return;
755 };
756 self.motion_context.set_active(true);
757 let scroll_target = self.scroll_target.clone();
758 let fling = FlingAnimation::new(runtime);
759 let motion_context = self.motion_context.clone();
760 let initial_value = scroll_target.current_offset();
761 let scroll_target_for_fling = scroll_target.clone();
762 let scroll_target_for_end = scroll_target.clone();
763 let detector_for_end = self.clone_for_watcher();
764 let overscroll_for_fling = self.overscroll.clone();
765
766 fling.start_fling(
767 initial_value,
768 fling_velocity,
769 move |delta| {
770 let consumed = overscroll_for_fling.apply_to_fling(delta, |delta| {
771 scroll_target_for_fling.apply_fling_delta(delta)
772 });
773 scroll_target_for_fling.invalidate();
774 consumed
775 },
776 move || {
777 scroll_target_for_end.invalidate();
778 let settle_running = detector_for_end
779 .gesture_state
780 .borrow()
781 .settle_animation
782 .as_ref()
783 .is_some_and(SettleAnimation::is_running);
784 if detector_for_end.overscroll.offset().abs() > 0.001 && !settle_running {
785 detector_for_end.start_overscroll_settle(0.0);
786 }
787 detector_for_end.update_motion_active(&motion_context);
788 },
789 );
790
791 let mut gs = self.gesture_state.borrow_mut();
792 gs.fling_animation = Some(fling);
793 }
794
795 fn start_settle_animation(&self, target: f32, initial_velocity: f32) {
796 let Some(runtime) = current_runtime_handle() else {
797 self.motion_context.set_active(false);
798 return;
799 };
800 self.motion_context.set_active(true);
801 let settle = SettleAnimation::new(runtime, SpringParams::SETTLE_POLICY);
802 let scroll_target_for_settle = self.scroll_target.clone();
803 let scroll_target_for_end = self.scroll_target.clone();
804 let detector_for_end = self.clone_for_watcher();
805 let motion_context = self.motion_context.clone();
806 settle.start_settle(
807 self.scroll_target.current_offset(),
808 initial_velocity,
809 target,
810 move |delta| {
811 let consumed = scroll_target_for_settle.apply_fling_delta(delta);
812 scroll_target_for_settle.invalidate();
813 consumed
814 },
815 move |_| {
816 scroll_target_for_end.invalidate();
817 detector_for_end.update_motion_active(&motion_context);
818 },
819 );
820 let mut gs = self.gesture_state.borrow_mut();
821 gs.settle_animation = Some(settle);
822 }
823
824 fn start_overscroll_settle(&self, initial_velocity: f32) {
825 let Some(runtime) = current_runtime_handle() else {
826 let current = self.overscroll.offset();
827 self.overscroll.apply_settle_delta(-current);
828 self.motion_context.set_active(false);
829 return;
830 };
831 let settle = SettleAnimation::new(runtime, SpringParams::OVERSCROLL_BOUNCE);
832 let overscroll_for_settle = self.overscroll.clone();
833 let overscroll_for_end = self.overscroll.clone();
834 let detector_for_end = self.clone_for_watcher();
835 let motion_context = self.motion_context.clone();
836 let initial = self.overscroll.offset();
837 settle.start_settle(
838 initial,
839 initial_velocity,
840 0.0,
841 move |delta| overscroll_for_settle.apply_settle_delta(delta),
842 move |end| {
843 let current = overscroll_for_end.offset();
844 overscroll_for_end.apply_settle_delta(-current);
845 if end.hit_boundary && end.velocity.abs() > MIN_FLING_VELOCITY {
846 detector_for_end.start_fling_animation(-end.velocity);
847 } else {
848 detector_for_end.update_motion_active(&motion_context);
849 }
850 },
851 );
852 let mut gs = self.gesture_state.borrow_mut();
853 gs.is_overscrolling = false;
854 gs.settle_animation = Some(settle);
855 }
856
857 fn update_motion_active(&self, motion_context: &ScrollMotionContext) {
858 let running = self.gesture_state.borrow().is_active();
859 if !running {
860 motion_context.set_active(false);
861 }
862 }
863
864 fn on_up(&self, time_ms: Option<i64>) -> bool {
865 self.finish_gesture(self.scroll_target.allows_fling(), time_ms)
866 }
867
868 fn on_cancel(&self) -> bool {
869 self.finish_gesture(false, None)
870 }
871
872 fn on_scroll(&self, axis_delta: f32, event: &PointerEvent) -> bool {
873 if axis_delta.abs() <= f32::EPSILON {
874 return false;
875 }
876
877 let delta = if self.reverse_scrolling {
878 -axis_delta
879 } else {
880 axis_delta
881 };
882 if !self.scroll_target.can_consume(delta) && self.scroll_target.can_scroll() {
883 let detector = self.clone_for_watcher();
884 event.defer_post_dispatch_action(move || detector.apply_wheel_delta(delta));
885 return false;
886 }
887 self.apply_wheel_delta(delta)
888 }
889
890 fn apply_wheel_delta(&self, delta: f32) -> bool {
891 {
892 let mut gs = self.gesture_state.borrow_mut();
893 if gs.disposed {
894 return false;
895 }
896 if let Some(fling) = gs.fling_animation.take() {
897 fling.cancel();
898 }
899 if let Some(settle) = gs.settle_animation.take() {
900 settle.cancel();
901 }
902 gs.drag_down_position = None;
903 gs.last_position = None;
904 gs.is_dragging = false;
905 gs.axis_locked_out = false;
906 gs.gesture_start_time = None;
907 gs.gesture_start_event_time_ms = None;
908 gs.last_velocity_sample_ms = None;
909 gs.velocity_tracker.reset();
910 }
911
912 self.motion_context.activate_for_current_frame();
913 let overscroll = self.overscroll.clone();
914 let consumed =
915 overscroll.apply_to_scroll(delta, |delta| self.scroll_target.apply_wheel_delta(delta));
916 let overscroll_active = self.overscroll.offset().abs() > 0.001;
917 if consumed.abs() > 0.001 {
918 self.scroll_target.invalidate();
919 }
920 if consumed.abs() > 0.001 || overscroll_active {
921 self.ensure_wheel_settle_watcher();
922 true
923 } else {
924 false
925 }
926 }
927
928 fn ensure_wheel_settle_watcher(&self) {
929 if self.scroll_target.settle_policy().is_none() && self.overscroll.offset().abs() <= 0.001 {
930 return;
931 }
932 {
933 let gs = self.gesture_state.borrow();
934 if gs
935 .wheel_settle_watcher
936 .as_ref()
937 .is_some_and(|watcher| watcher.is_running.get())
938 {
939 return;
940 }
941 }
942 let Some(runtime) = current_runtime_handle() else {
943 return;
944 };
945
946 let is_running = Rc::new(Cell::new(true));
947 let registration = Rc::new(RefCell::new(None));
948
949 struct WheelSettleLoop<S: ScrollTarget> {
950 detector: ScrollGestureDetector<S>,
951 gesture_state: Rc<RefCell<ScrollGestureState>>,
952 frame_clock: cranpose_core::internal::FrameClock,
953 is_running: Rc<Cell<bool>>,
954 registration: Rc<RefCell<Option<FrameCallbackRegistration>>>,
955 last_offset: Rc<Cell<f32>>,
956 idle_nanos: Rc<Cell<u64>>,
957 last_frame: Rc<Cell<Option<u64>>>,
958 }
959
960 impl<S: ScrollTarget + 'static> WheelSettleLoop<S> {
961 fn next(&self) -> Self {
962 Self {
963 detector: self.detector.clone_for_watcher(),
964 gesture_state: Rc::clone(&self.gesture_state),
965 frame_clock: self.frame_clock.clone(),
966 is_running: Rc::clone(&self.is_running),
967 registration: Rc::clone(&self.registration),
968 last_offset: Rc::clone(&self.last_offset),
969 idle_nanos: Rc::clone(&self.idle_nanos),
970 last_frame: Rc::clone(&self.last_frame),
971 }
972 }
973
974 fn schedule(self) {
975 let continuation = self.next();
976 let registration_slot = Rc::clone(&self.registration);
977 let new_registration = self.frame_clock.with_frame_nanos(move |frame_time_nanos| {
978 let this = &continuation;
979 if !this.is_running.get() {
980 return;
981 }
982 {
983 let gs = this.gesture_state.borrow();
984 let animating = gs.is_dragging
985 || gs
986 .fling_animation
987 .as_ref()
988 .is_some_and(FlingAnimation::is_running)
989 || gs
990 .settle_animation
991 .as_ref()
992 .is_some_and(SettleAnimation::is_running);
993 if animating {
994 this.is_running.set(false);
995 return;
996 }
997 }
998
999 let offset = this.detector.scroll_target.current_offset();
1000 let dt = this
1001 .last_frame
1002 .get()
1003 .map_or(0, |last| frame_time_nanos.saturating_sub(last));
1004 this.last_frame.set(Some(frame_time_nanos));
1005 if (offset - this.last_offset.get()).abs() > 0.01 {
1006 this.last_offset.set(offset);
1007 this.idle_nanos.set(0);
1008 } else {
1009 this.idle_nanos.set(this.idle_nanos.get() + dt);
1010 }
1011
1012 if this.idle_nanos.get() >= WHEEL_SETTLE_IDLE_NANOS {
1013 this.is_running.set(false);
1014 if this.detector.overscroll.offset().abs() > 0.001 {
1015 this.detector.start_overscroll_settle(0.0);
1016 return;
1017 }
1018 if let Some(policy) = this.detector.scroll_target.settle_policy() {
1019 let target = policy(offset, 0.0);
1020 if (target - offset).abs() > 0.5 {
1021 this.detector.start_settle_animation(target, 0.0);
1022 }
1023 }
1024 return;
1025 }
1026
1027 continuation.next().schedule();
1028 });
1029 *registration_slot.borrow_mut() = Some(new_registration);
1030 }
1031 }
1032
1033 WheelSettleLoop {
1034 detector: self.clone_for_watcher(),
1035 gesture_state: Rc::clone(&self.gesture_state),
1036 frame_clock: runtime.frame_clock(),
1037 is_running: Rc::clone(&is_running),
1038 registration: Rc::clone(®istration),
1039 last_offset: Rc::new(Cell::new(self.scroll_target.current_offset())),
1040 idle_nanos: Rc::new(Cell::new(0u64)),
1041 last_frame: Rc::new(Cell::new(None::<u64>)),
1042 }
1043 .schedule();
1044
1045 self.gesture_state.borrow_mut().wheel_settle_watcher = Some(WheelSettleWatcher {
1046 is_running,
1047 registration,
1048 });
1049 }
1050
1051 fn clone_for_watcher(&self) -> ScrollGestureDetector<S> {
1052 ScrollGestureDetector {
1053 gesture_state: Rc::clone(&self.gesture_state),
1054 scroll_target: self.scroll_target.clone(),
1055 is_vertical: self.is_vertical,
1056 reverse_scrolling: self.reverse_scrolling,
1057 overscroll: self.overscroll.clone(),
1058 motion_context: self.motion_context.clone(),
1059 }
1060 }
1061}
1062
1063pub(crate) struct MotionContextAnimatedNode {
1064 state: NodeState,
1065 motion_context: ScrollMotionContext,
1066 invalidation_callback_id: Option<u64>,
1067 overscroll_callback_id: Option<u64>,
1068 node_id: Option<NodeId>,
1069}
1070
1071impl MotionContextAnimatedNode {
1072 fn new(motion_context: ScrollMotionContext) -> Self {
1073 Self {
1074 state: NodeState::new(),
1075 motion_context,
1076 invalidation_callback_id: None,
1077 overscroll_callback_id: None,
1078 node_id: None,
1079 }
1080 }
1081
1082 pub(crate) fn is_active(&self) -> bool {
1083 self.motion_context.is_active()
1084 }
1085
1086 fn attach_callbacks(&mut self) {
1087 if let Some(node_id) = self.node_id {
1088 self.invalidation_callback_id = Some(self.motion_context.add_invalidate_callback(
1089 Box::new(move || schedule_modifier_slices_repass(node_id)),
1090 ));
1091 self.overscroll_callback_id = Some(
1092 self.motion_context
1093 .overscroll()
1094 .add_invalidate_callback(Box::new(move || {
1095 crate::schedule_measure_repass(node_id);
1096 schedule_modifier_slices_repass(node_id);
1097 })),
1098 );
1099 }
1100 }
1101
1102 fn detach_callbacks(&mut self) {
1103 if let Some(id) = self.invalidation_callback_id.take() {
1104 self.motion_context.remove_invalidate_callback(id);
1105 }
1106 if let Some(id) = self.overscroll_callback_id.take() {
1107 self.motion_context
1108 .overscroll()
1109 .remove_invalidate_callback(id);
1110 }
1111 }
1112}
1113
1114pub(crate) struct TranslatedContentContextNode {
1115 state: NodeState,
1116 identity: usize,
1117 offset_source: TranslatedContentOffsetSource,
1118 overscroll: crate::scroll::OverscrollEffect,
1119 overscroll_callback_id: Option<u64>,
1120 node_id: Option<NodeId>,
1121}
1122
1123impl TranslatedContentContextNode {
1124 fn new(
1125 identity: usize,
1126 offset_source: TranslatedContentOffsetSource,
1127 overscroll: crate::scroll::OverscrollEffect,
1128 ) -> Self {
1129 Self {
1130 state: NodeState::new(),
1131 identity,
1132 offset_source,
1133 overscroll,
1134 overscroll_callback_id: None,
1135 node_id: None,
1136 }
1137 }
1138
1139 pub(crate) fn is_active(&self) -> bool {
1140 true
1141 }
1142
1143 pub(crate) fn identity(&self) -> usize {
1144 self.identity
1145 }
1146
1147 pub(crate) fn content_offset_reader(&self) -> Option<Rc<dyn Fn() -> Point>> {
1148 self.offset_source
1149 .content_offset_reader(self.overscroll.clone())
1150 }
1151
1152 fn attach_callback(&mut self) {
1153 if let Some(node_id) = self.node_id {
1154 self.overscroll_callback_id =
1155 Some(self.overscroll.add_invalidate_callback(Box::new(move || {
1156 schedule_modifier_slices_repass(node_id)
1157 })));
1158 }
1159 }
1160
1161 fn detach_callback(&mut self) {
1162 if let Some(id) = self.overscroll_callback_id.take() {
1163 self.overscroll.remove_invalidate_callback(id);
1164 }
1165 }
1166}
1167
1168impl DelegatableNode for TranslatedContentContextNode {
1169 fn node_state(&self) -> &NodeState {
1170 &self.state
1171 }
1172}
1173
1174impl ModifierNode for TranslatedContentContextNode {
1175 fn on_attach(&mut self, context: &mut dyn cranpose_foundation::ModifierNodeContext) {
1176 self.node_id = context.node_id();
1177 self.attach_callback();
1178 }
1179
1180 fn on_detach(&mut self) {
1181 self.detach_callback();
1182 self.node_id = None;
1183 }
1184}
1185
1186impl DelegatableNode for MotionContextAnimatedNode {
1187 fn node_state(&self) -> &NodeState {
1188 &self.state
1189 }
1190}
1191
1192impl ModifierNode for MotionContextAnimatedNode {
1193 fn on_attach(&mut self, context: &mut dyn cranpose_foundation::ModifierNodeContext) {
1194 self.node_id = context.node_id();
1195 self.attach_callbacks();
1196 }
1197
1198 fn on_detach(&mut self) {
1199 self.detach_callbacks();
1200 self.node_id = None;
1201 }
1202}
1203
1204#[derive(Clone)]
1205struct MotionContextAnimatedElement {
1206 motion_context: ScrollMotionContext,
1207}
1208
1209impl MotionContextAnimatedElement {
1210 fn new(motion_context: ScrollMotionContext) -> Self {
1211 Self { motion_context }
1212 }
1213}
1214
1215impl std::fmt::Debug for MotionContextAnimatedElement {
1216 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1217 f.debug_struct("MotionContextAnimatedElement").finish()
1218 }
1219}
1220
1221impl PartialEq for MotionContextAnimatedElement {
1222 fn eq(&self, other: &Self) -> bool {
1223 self.motion_context.ptr_eq(&other.motion_context)
1224 }
1225}
1226
1227impl Eq for MotionContextAnimatedElement {}
1228
1229impl std::hash::Hash for MotionContextAnimatedElement {
1230 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1231 self.motion_context.stable_key().hash(state);
1232 }
1233}
1234
1235impl ModifierNodeElement for MotionContextAnimatedElement {
1236 type Node = MotionContextAnimatedNode;
1237
1238 fn create(&self) -> Self::Node {
1239 MotionContextAnimatedNode::new(self.motion_context.clone())
1240 }
1241
1242 fn update(&self, node: &mut Self::Node) {
1243 if node.motion_context.ptr_eq(&self.motion_context) {
1244 return;
1245 }
1246 node.detach_callbacks();
1247 node.motion_context = self.motion_context.clone();
1248 node.attach_callbacks();
1249 }
1250
1251 fn capabilities(&self) -> NodeCapabilities {
1252 NodeCapabilities::LAYOUT
1253 }
1254}
1255
1256#[derive(Clone)]
1257enum TranslatedContentOffsetSource {
1258 LayoutContentOffset,
1259 LazyList {
1260 state: LazyListState,
1261 is_vertical: bool,
1262 reverse_scrolling: bool,
1263 },
1264}
1265
1266impl TranslatedContentOffsetSource {
1267 fn content_offset_reader(
1268 &self,
1269 overscroll: crate::scroll::OverscrollEffect,
1270 ) -> Option<Rc<dyn Fn() -> Point>> {
1271 match self {
1272 Self::LayoutContentOffset => None,
1273 Self::LazyList {
1274 state, is_vertical, ..
1275 } => Some(Rc::new(lazy_list_content_offset_reader(
1276 *state,
1277 *is_vertical,
1278 overscroll,
1279 ))),
1280 }
1281 }
1282
1283 fn is_vertical(&self) -> Option<bool> {
1284 match self {
1285 Self::LayoutContentOffset => None,
1286 Self::LazyList { is_vertical, .. } => Some(*is_vertical),
1287 }
1288 }
1289
1290 fn reverse_scrolling(&self) -> Option<bool> {
1291 match self {
1292 Self::LayoutContentOffset => None,
1293 Self::LazyList {
1294 reverse_scrolling, ..
1295 } => Some(*reverse_scrolling),
1296 }
1297 }
1298}
1299
1300fn lazy_list_content_offset_reader(
1301 state: LazyListState,
1302 is_vertical: bool,
1303 overscroll: crate::scroll::OverscrollEffect,
1304) -> impl Fn() -> Point {
1305 move || {
1306 let info = state.layout_info();
1307 if info.visible_items_info.is_empty() {
1308 return Point::default();
1309 };
1310 overscroll.set_dimension(info.viewport_size);
1311 let main_offset = info.snap_anchor_offset;
1312 let bounce = overscroll.offset();
1313 if is_vertical {
1314 Point::new(0.0, main_offset + bounce)
1315 } else {
1316 Point::new(main_offset + bounce, 0.0)
1317 }
1318 }
1319}
1320
1321#[derive(Clone)]
1322struct TranslatedContentContextElement {
1323 identity: usize,
1324 offset_source: TranslatedContentOffsetSource,
1325 overscroll: crate::scroll::OverscrollEffect,
1326}
1327
1328impl TranslatedContentContextElement {
1329 fn new(
1330 identity: usize,
1331 offset_source: TranslatedContentOffsetSource,
1332 overscroll: crate::scroll::OverscrollEffect,
1333 ) -> Self {
1334 Self {
1335 identity,
1336 offset_source,
1337 overscroll,
1338 }
1339 }
1340}
1341
1342impl std::fmt::Debug for TranslatedContentContextElement {
1343 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1344 let offset_source = match &self.offset_source {
1345 TranslatedContentOffsetSource::LayoutContentOffset => "layout",
1346 TranslatedContentOffsetSource::LazyList { .. } => "lazy_list",
1347 };
1348 f.debug_struct("TranslatedContentContextElement")
1349 .field("identity", &self.identity)
1350 .field("offset_source", &offset_source)
1351 .finish()
1352 }
1353}
1354
1355impl PartialEq for TranslatedContentContextElement {
1356 fn eq(&self, other: &Self) -> bool {
1357 self.identity == other.identity
1358 && self.overscroll.ptr_eq(&other.overscroll)
1359 && self.offset_source.is_vertical() == other.offset_source.is_vertical()
1360 && self.offset_source.reverse_scrolling() == other.offset_source.reverse_scrolling()
1361 }
1362}
1363
1364impl Eq for TranslatedContentContextElement {}
1365
1366impl std::hash::Hash for TranslatedContentContextElement {
1367 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1368 self.identity.hash(state);
1369 self.offset_source.is_vertical().hash(state);
1370 self.offset_source.reverse_scrolling().hash(state);
1371 }
1372}
1373
1374impl ModifierNodeElement for TranslatedContentContextElement {
1375 type Node = TranslatedContentContextNode;
1376
1377 fn create(&self) -> Self::Node {
1378 TranslatedContentContextNode::new(
1379 self.identity,
1380 self.offset_source.clone(),
1381 self.overscroll.clone(),
1382 )
1383 }
1384
1385 fn update(&self, node: &mut Self::Node) {
1386 node.identity = self.identity;
1387 node.offset_source = self.offset_source.clone();
1388 if !node.overscroll.ptr_eq(&self.overscroll) {
1389 node.detach_callback();
1390 node.overscroll = self.overscroll.clone();
1391 node.attach_callback();
1392 }
1393 }
1394
1395 fn capabilities(&self) -> NodeCapabilities {
1396 NodeCapabilities::LAYOUT
1397 }
1398}
1399
1400impl Modifier {
1401 pub fn horizontal_scroll(self, state: ScrollState, reverse_scrolling: bool) -> Self {
1419 self.then(scroll_impl(state, false, reverse_scrolling, None))
1420 }
1421
1422 pub fn vertical_scroll(self, state: ScrollState, reverse_scrolling: bool) -> Self {
1431 self.then(scroll_impl(state, true, reverse_scrolling, None))
1432 }
1433
1434 pub fn horizontal_scroll_without_gestures(
1441 self,
1442 state: ScrollState,
1443 reverse_scrolling: bool,
1444 ) -> Self {
1445 self.then(scroll_impl(
1446 state,
1447 false,
1448 reverse_scrolling,
1449 Some(Rc::new(|| false)),
1450 ))
1451 }
1452
1453 pub fn vertical_scroll_without_gestures(
1457 self,
1458 state: ScrollState,
1459 reverse_scrolling: bool,
1460 ) -> Self {
1461 self.then(scroll_impl(
1462 state,
1463 true,
1464 reverse_scrolling,
1465 Some(Rc::new(|| false)),
1466 ))
1467 }
1468}
1469
1470fn scroll_impl(
1471 state: ScrollState,
1472 is_vertical: bool,
1473 reverse_scrolling: bool,
1474 guard: Option<Rc<dyn Fn() -> bool>>,
1475) -> Modifier {
1476 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::ScrollState {
1477 state_id: state.id(),
1478 is_vertical,
1479 reverse_scrolling,
1480 });
1481
1482 let pointer_input = drag_gesture_input(
1483 (state.id(), is_vertical),
1484 DragGesture {
1485 target: state,
1486 is_vertical,
1487 reverse_input: false,
1488 motion_context: motion_context.clone(),
1489 guard,
1490 },
1491 );
1492
1493 let overscroll = motion_context.overscroll();
1494 let element = ScrollElement::new(state, overscroll.clone(), is_vertical, reverse_scrolling);
1495 let layout_modifier =
1496 Modifier::with_element(element).with_inspector_metadata(inspector_metadata(
1497 if is_vertical {
1498 "verticalScroll"
1499 } else {
1500 "horizontalScroll"
1501 },
1502 move |info| {
1503 info.add_property("isVertical", is_vertical.to_string());
1504 info.add_property("reverseScrolling", reverse_scrolling.to_string());
1505 },
1506 ));
1507 let motion_modifier =
1508 Modifier::with_element(MotionContextAnimatedElement::new(motion_context.clone()));
1509 let translated_content_modifier = Modifier::with_element(TranslatedContentContextElement::new(
1510 state.id() as usize,
1511 TranslatedContentOffsetSource::LayoutContentOffset,
1512 overscroll,
1513 ));
1514
1515 pointer_input
1516 .then(motion_modifier)
1517 .then(translated_content_modifier)
1518 .then(layout_modifier)
1519 .clip_to_bounds()
1520 .semantics(scroll_semantics(state, is_vertical, reverse_scrolling))
1521}
1522
1523fn scroll_semantics(
1528 state: ScrollState,
1529 is_vertical: bool,
1530 reverse_scrolling: bool,
1531) -> impl Fn(&mut cranpose_foundation::SemanticsConfiguration) + 'static {
1532 move |config| {
1533 let range = cranpose_foundation::ScrollAxisRange::new(
1534 state.value(),
1535 state.max_value(),
1536 reverse_scrolling,
1537 );
1538 if is_vertical {
1539 config.vertical_scroll = Some(range);
1540 } else {
1541 config.horizontal_scroll = Some(range);
1542 }
1543 config.scroll_by = Some(cranpose_foundation::SemanticsScrollBy::new(
1544 move |dx, dy| {
1545 let delta = if is_vertical { dy } else { dx };
1546 state.dispatch_raw_delta(delta).abs() > f32::EPSILON
1547 },
1548 ));
1549 }
1550}
1551
1552use cranpose_foundation::lazy::LazyListState;
1553
1554impl Modifier {
1555 pub fn lazy_vertical_scroll(self, state: LazyListState, reverse_scrolling: bool) -> Self {
1566 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::LazyList {
1567 state_identity: state.inner_ptr() as usize,
1568 is_vertical: true,
1569 reverse_scrolling,
1570 });
1571 self.lazy_vertical_scroll_with_context(state, reverse_scrolling, motion_context)
1572 }
1573
1574 pub(crate) fn lazy_vertical_scroll_with_context(
1575 self,
1576 state: LazyListState,
1577 reverse_scrolling: bool,
1578 motion_context: ScrollMotionContext,
1579 ) -> Self {
1580 self.then(lazy_scroll_impl(
1581 state,
1582 true,
1583 reverse_scrolling,
1584 motion_context,
1585 ))
1586 }
1587
1588 pub(crate) fn lazy_horizontal_scroll_with_context(
1589 self,
1590 state: LazyListState,
1591 reverse_scrolling: bool,
1592 motion_context: ScrollMotionContext,
1593 ) -> Self {
1594 self.then(lazy_scroll_impl(
1595 state,
1596 false,
1597 reverse_scrolling,
1598 motion_context,
1599 ))
1600 }
1601}
1602
1603fn lazy_scroll_impl(
1604 state: LazyListState,
1605 is_vertical: bool,
1606 reverse_scrolling: bool,
1607 motion_context: ScrollMotionContext,
1608) -> Modifier {
1609 let list_state = state;
1610 let state_id = state.inner_ptr() as usize;
1611 let key = (state_id, is_vertical, reverse_scrolling);
1612 let overscroll = motion_context.overscroll();
1613 let translated_content_modifier = Modifier::with_element(TranslatedContentContextElement::new(
1614 state_id,
1615 TranslatedContentOffsetSource::LazyList {
1616 state,
1617 is_vertical,
1618 reverse_scrolling,
1619 },
1620 overscroll,
1621 ));
1622
1623 Modifier::with_element(MotionContextAnimatedElement::new(motion_context.clone()))
1624 .then(translated_content_modifier)
1625 .then(drag_gesture_input(
1626 key,
1627 DragGesture {
1628 target: list_state,
1629 is_vertical,
1630 reverse_input: reverse_scrolling,
1631 motion_context,
1632 guard: None,
1633 },
1634 ))
1635 .semantics(lazy_scroll_semantics(
1636 list_state,
1637 is_vertical,
1638 reverse_scrolling,
1639 ))
1640}
1641
1642pub(crate) fn lazy_scroll_semantics(
1648 state: LazyListState,
1649 is_vertical: bool,
1650 reverse_scrolling: bool,
1651) -> impl Fn(&mut cranpose_foundation::SemanticsConfiguration) + 'static {
1652 move |config| {
1653 let mut value = state.first_visible_item_index() as f32;
1654 if state.first_visible_item_scroll_offset() > 0.0 {
1655 value += 0.5;
1656 }
1657 let max_value = if state.can_scroll_forward() {
1658 value + 1.0
1659 } else {
1660 value
1661 };
1662 let range = cranpose_foundation::ScrollAxisRange::new(value, max_value, reverse_scrolling);
1663 let count = state.total_items_count();
1664 config.collection = Some(cranpose_foundation::CollectionInfo {
1665 rows: if is_vertical { count } else { 1 },
1666 columns: if is_vertical { 1 } else { count },
1667 });
1668 config
1669 .role
1670 .get_or_insert(cranpose_foundation::SemanticsWidgetRole::List);
1671 if is_vertical {
1672 config.vertical_scroll = Some(range);
1673 } else {
1674 config.horizontal_scroll = Some(range);
1675 }
1676 config.scroll_by = Some(cranpose_foundation::SemanticsScrollBy::new(
1677 move |dx, dy| {
1678 let delta = if is_vertical { dy } else { dx };
1679 state.dispatch_scroll_delta(-delta).abs() > f32::EPSILON
1680 },
1681 ));
1682 config.scroll_to_index = Some(cranpose_foundation::SemanticsScrollToIndex::new(
1683 move |index| jump_to_row(&state, index),
1684 ));
1685 }
1686}
1687
1688fn jump_to_row(state: &LazyListState, index: usize) -> bool {
1692 if index >= state.total_items_count() {
1693 return false;
1694 }
1695 state.scroll_to_item(index, 0.0);
1696 true
1697}
1698
1699impl Modifier {
1700 pub fn draggable(self, axis: Axis, state: DraggableState) -> Self {
1712 self.then(draggable_impl(axis, state, None))
1713 }
1714
1715 pub fn draggable_guarded(
1721 self,
1722 axis: Axis,
1723 state: DraggableState,
1724 guard: impl Fn() -> bool + 'static,
1725 ) -> Self {
1726 self.then(draggable_impl(axis, state, Some(Rc::new(guard))))
1727 }
1728}
1729
1730fn draggable_impl(
1731 axis: Axis,
1732 state: DraggableState,
1733 guard: Option<Rc<dyn Fn() -> bool>>,
1734) -> Modifier {
1735 let is_vertical = axis.is_vertical();
1736 let identity = state.identity();
1737 drag_gesture_input(
1738 (identity, is_vertical),
1739 DragGesture {
1740 target: state,
1741 is_vertical,
1742 reverse_input: false,
1743 motion_context: scroll_motion_context_for_key(ScrollMotionContextKey::Draggable {
1744 state_identity: identity,
1745 is_vertical,
1746 }),
1747 guard,
1748 },
1749 )
1750}