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 detector_for_end = self.clone_for_watcher();
763 let overscroll_for_fling = self.overscroll.clone();
764
765 fling.start_fling(
766 initial_value,
767 fling_velocity,
768 move |delta| {
769 let consumed = overscroll_for_fling.apply_to_fling(delta, |delta| {
770 scroll_target_for_fling.apply_fling_delta(delta)
771 });
772 scroll_target_for_fling.invalidate();
773 consumed
774 },
775 move || {
776 scroll_target.invalidate();
777 let settle_running = detector_for_end
778 .gesture_state
779 .borrow()
780 .settle_animation
781 .as_ref()
782 .is_some_and(SettleAnimation::is_running);
783 if detector_for_end.overscroll.offset().abs() > 0.001 && !settle_running {
784 detector_for_end.start_overscroll_settle(0.0);
785 }
786 detector_for_end.update_motion_active(&motion_context);
787 },
788 );
789
790 let mut gs = self.gesture_state.borrow_mut();
791 gs.fling_animation = Some(fling);
792 }
793
794 fn start_settle_animation(&self, target: f32, initial_velocity: f32) {
795 let Some(runtime) = current_runtime_handle() else {
796 self.motion_context.set_active(false);
797 return;
798 };
799 self.motion_context.set_active(true);
800 let settle = SettleAnimation::new(runtime, SpringParams::SETTLE_POLICY);
801 let scroll_target_for_settle = self.scroll_target.clone();
802 let scroll_target_for_end = self.scroll_target.clone();
803 let detector_for_end = self.clone_for_watcher();
804 let motion_context = self.motion_context.clone();
805 settle.start_settle(
806 self.scroll_target.current_offset(),
807 initial_velocity,
808 target,
809 move |delta| {
810 let consumed = scroll_target_for_settle.apply_fling_delta(delta);
811 scroll_target_for_settle.invalidate();
812 consumed
813 },
814 move |_| {
815 scroll_target_for_end.invalidate();
816 detector_for_end.update_motion_active(&motion_context);
817 },
818 );
819 let mut gs = self.gesture_state.borrow_mut();
820 gs.settle_animation = Some(settle);
821 }
822
823 fn start_overscroll_settle(&self, initial_velocity: f32) {
824 let Some(runtime) = current_runtime_handle() else {
825 let current = self.overscroll.offset();
826 self.overscroll.apply_settle_delta(-current);
827 self.motion_context.set_active(false);
828 return;
829 };
830 let settle = SettleAnimation::new(runtime, SpringParams::OVERSCROLL_BOUNCE);
831 let overscroll_for_settle = self.overscroll.clone();
832 let overscroll_for_end = self.overscroll.clone();
833 let detector_for_end = self.clone_for_watcher();
834 let motion_context = self.motion_context.clone();
835 let initial = self.overscroll.offset();
836 settle.start_settle(
837 initial,
838 initial_velocity,
839 0.0,
840 move |delta| overscroll_for_settle.apply_settle_delta(delta),
841 move |end| {
842 let current = overscroll_for_end.offset();
843 overscroll_for_end.apply_settle_delta(-current);
844 if end.hit_boundary && end.velocity.abs() > MIN_FLING_VELOCITY {
845 detector_for_end.start_fling_animation(-end.velocity);
846 } else {
847 detector_for_end.update_motion_active(&motion_context);
848 }
849 },
850 );
851 let mut gs = self.gesture_state.borrow_mut();
852 gs.is_overscrolling = false;
853 gs.settle_animation = Some(settle);
854 }
855
856 fn update_motion_active(&self, motion_context: &ScrollMotionContext) {
857 let running = self.gesture_state.borrow().is_active();
858 if !running {
859 motion_context.set_active(false);
860 }
861 }
862
863 fn on_up(&self, time_ms: Option<i64>) -> bool {
864 self.finish_gesture(self.scroll_target.allows_fling(), time_ms)
865 }
866
867 fn on_cancel(&self) -> bool {
868 self.finish_gesture(false, None)
869 }
870
871 fn on_scroll(&self, axis_delta: f32, event: &PointerEvent) -> bool {
872 if axis_delta.abs() <= f32::EPSILON {
873 return false;
874 }
875
876 let delta = if self.reverse_scrolling {
877 -axis_delta
878 } else {
879 axis_delta
880 };
881 if !self.scroll_target.can_consume(delta) && self.scroll_target.can_scroll() {
882 let detector = self.clone_for_watcher();
883 event.defer_post_dispatch_action(move || detector.apply_wheel_delta(delta));
884 return false;
885 }
886 self.apply_wheel_delta(delta)
887 }
888
889 fn apply_wheel_delta(&self, delta: f32) -> bool {
890 {
891 let mut gs = self.gesture_state.borrow_mut();
892 if gs.disposed {
893 return false;
894 }
895 if let Some(fling) = gs.fling_animation.take() {
896 fling.cancel();
897 }
898 if let Some(settle) = gs.settle_animation.take() {
899 settle.cancel();
900 }
901 gs.drag_down_position = None;
902 gs.last_position = None;
903 gs.is_dragging = false;
904 gs.axis_locked_out = false;
905 gs.gesture_start_time = None;
906 gs.gesture_start_event_time_ms = None;
907 gs.last_velocity_sample_ms = None;
908 gs.velocity_tracker.reset();
909 }
910
911 self.motion_context.activate_for_current_frame();
912 let overscroll = self.overscroll.clone();
913 let consumed =
914 overscroll.apply_to_scroll(delta, |delta| self.scroll_target.apply_wheel_delta(delta));
915 let overscroll_active = self.overscroll.offset().abs() > 0.001;
916 if consumed.abs() > 0.001 {
917 self.scroll_target.invalidate();
918 }
919 if consumed.abs() > 0.001 || overscroll_active {
920 self.ensure_wheel_settle_watcher();
921 true
922 } else {
923 false
924 }
925 }
926
927 fn ensure_wheel_settle_watcher(&self) {
928 if self.scroll_target.settle_policy().is_none() && self.overscroll.offset().abs() <= 0.001 {
929 return;
930 }
931 {
932 let gs = self.gesture_state.borrow();
933 if gs
934 .wheel_settle_watcher
935 .as_ref()
936 .is_some_and(|watcher| watcher.is_running.get())
937 {
938 return;
939 }
940 }
941 let Some(runtime) = current_runtime_handle() else {
942 return;
943 };
944
945 let is_running = Rc::new(Cell::new(true));
946 let registration = Rc::new(RefCell::new(None));
947
948 struct WheelSettleLoop<S: ScrollTarget> {
949 detector: ScrollGestureDetector<S>,
950 gesture_state: Rc<RefCell<ScrollGestureState>>,
951 frame_clock: cranpose_core::internal::FrameClock,
952 is_running: Rc<Cell<bool>>,
953 registration: Rc<RefCell<Option<FrameCallbackRegistration>>>,
954 last_offset: Rc<Cell<f32>>,
955 idle_nanos: Rc<Cell<u64>>,
956 last_frame: Rc<Cell<Option<u64>>>,
957 }
958
959 impl<S: ScrollTarget + 'static> WheelSettleLoop<S> {
960 fn next(&self) -> Self {
961 Self {
962 detector: self.detector.clone_for_watcher(),
963 gesture_state: Rc::clone(&self.gesture_state),
964 frame_clock: self.frame_clock.clone(),
965 is_running: Rc::clone(&self.is_running),
966 registration: Rc::clone(&self.registration),
967 last_offset: Rc::clone(&self.last_offset),
968 idle_nanos: Rc::clone(&self.idle_nanos),
969 last_frame: Rc::clone(&self.last_frame),
970 }
971 }
972
973 fn schedule(self) {
974 let continuation = self.next();
975 let registration_slot = Rc::clone(&self.registration);
976 let new_registration = self.frame_clock.with_frame_nanos(move |frame_time_nanos| {
977 let this = &continuation;
978 if !this.is_running.get() {
979 return;
980 }
981 {
982 let gs = this.gesture_state.borrow();
983 let animating = gs.is_dragging
984 || gs
985 .fling_animation
986 .as_ref()
987 .is_some_and(FlingAnimation::is_running)
988 || gs
989 .settle_animation
990 .as_ref()
991 .is_some_and(SettleAnimation::is_running);
992 if animating {
993 this.is_running.set(false);
994 return;
995 }
996 }
997
998 let offset = this.detector.scroll_target.current_offset();
999 let dt = this
1000 .last_frame
1001 .get()
1002 .map_or(0, |last| frame_time_nanos.saturating_sub(last));
1003 this.last_frame.set(Some(frame_time_nanos));
1004 if (offset - this.last_offset.get()).abs() > 0.01 {
1005 this.last_offset.set(offset);
1006 this.idle_nanos.set(0);
1007 } else {
1008 this.idle_nanos.set(this.idle_nanos.get() + dt);
1009 }
1010
1011 if this.idle_nanos.get() >= WHEEL_SETTLE_IDLE_NANOS {
1012 this.is_running.set(false);
1013 if this.detector.overscroll.offset().abs() > 0.001 {
1014 this.detector.start_overscroll_settle(0.0);
1015 return;
1016 }
1017 if let Some(policy) = this.detector.scroll_target.settle_policy() {
1018 let target = policy(offset, 0.0);
1019 if (target - offset).abs() > 0.5 {
1020 this.detector.start_settle_animation(target, 0.0);
1021 }
1022 }
1023 return;
1024 }
1025
1026 continuation.next().schedule();
1027 });
1028 *registration_slot.borrow_mut() = Some(new_registration);
1029 }
1030 }
1031
1032 WheelSettleLoop {
1033 detector: self.clone_for_watcher(),
1034 gesture_state: Rc::clone(&self.gesture_state),
1035 frame_clock: runtime.frame_clock(),
1036 is_running: Rc::clone(&is_running),
1037 registration: Rc::clone(®istration),
1038 last_offset: Rc::new(Cell::new(self.scroll_target.current_offset())),
1039 idle_nanos: Rc::new(Cell::new(0u64)),
1040 last_frame: Rc::new(Cell::new(None::<u64>)),
1041 }
1042 .schedule();
1043
1044 self.gesture_state.borrow_mut().wheel_settle_watcher = Some(WheelSettleWatcher {
1045 is_running,
1046 registration,
1047 });
1048 }
1049
1050 fn clone_for_watcher(&self) -> ScrollGestureDetector<S> {
1051 ScrollGestureDetector {
1052 gesture_state: Rc::clone(&self.gesture_state),
1053 scroll_target: self.scroll_target.clone(),
1054 is_vertical: self.is_vertical,
1055 reverse_scrolling: self.reverse_scrolling,
1056 overscroll: self.overscroll.clone(),
1057 motion_context: self.motion_context.clone(),
1058 }
1059 }
1060}
1061
1062pub(crate) struct MotionContextAnimatedNode {
1063 state: NodeState,
1064 motion_context: ScrollMotionContext,
1065 invalidation_callback_id: Option<u64>,
1066 overscroll_callback_id: Option<u64>,
1067 node_id: Option<NodeId>,
1068}
1069
1070impl MotionContextAnimatedNode {
1071 fn new(motion_context: ScrollMotionContext) -> Self {
1072 Self {
1073 state: NodeState::new(),
1074 motion_context,
1075 invalidation_callback_id: None,
1076 overscroll_callback_id: None,
1077 node_id: None,
1078 }
1079 }
1080
1081 pub(crate) fn is_active(&self) -> bool {
1082 self.motion_context.is_active()
1083 }
1084
1085 fn attach_callbacks(&mut self) {
1086 if let Some(node_id) = self.node_id {
1087 self.invalidation_callback_id = Some(self.motion_context.add_invalidate_callback(
1088 Box::new(move || schedule_modifier_slices_repass(node_id)),
1089 ));
1090 self.overscroll_callback_id = Some(
1091 self.motion_context
1092 .overscroll()
1093 .add_invalidate_callback(Box::new(move || {
1094 crate::schedule_measure_repass(node_id);
1095 schedule_modifier_slices_repass(node_id);
1096 })),
1097 );
1098 }
1099 }
1100
1101 fn detach_callbacks(&mut self) {
1102 if let Some(id) = self.invalidation_callback_id.take() {
1103 self.motion_context.remove_invalidate_callback(id);
1104 }
1105 if let Some(id) = self.overscroll_callback_id.take() {
1106 self.motion_context
1107 .overscroll()
1108 .remove_invalidate_callback(id);
1109 }
1110 }
1111}
1112
1113pub(crate) struct TranslatedContentContextNode {
1114 state: NodeState,
1115 identity: usize,
1116 offset_source: TranslatedContentOffsetSource,
1117 overscroll: crate::scroll::OverscrollEffect,
1118 overscroll_callback_id: Option<u64>,
1119 node_id: Option<NodeId>,
1120}
1121
1122impl TranslatedContentContextNode {
1123 fn new(
1124 identity: usize,
1125 offset_source: TranslatedContentOffsetSource,
1126 overscroll: crate::scroll::OverscrollEffect,
1127 ) -> Self {
1128 Self {
1129 state: NodeState::new(),
1130 identity,
1131 offset_source,
1132 overscroll,
1133 overscroll_callback_id: None,
1134 node_id: None,
1135 }
1136 }
1137
1138 pub(crate) fn is_active(&self) -> bool {
1139 true
1140 }
1141
1142 pub(crate) fn identity(&self) -> usize {
1143 self.identity
1144 }
1145
1146 pub(crate) fn content_offset_reader(&self) -> Option<Rc<dyn Fn() -> Point>> {
1147 self.offset_source
1148 .content_offset_reader(self.overscroll.clone())
1149 }
1150
1151 fn attach_callback(&mut self) {
1152 if let Some(node_id) = self.node_id {
1153 self.overscroll_callback_id =
1154 Some(self.overscroll.add_invalidate_callback(Box::new(move || {
1155 schedule_modifier_slices_repass(node_id);
1156 })));
1157 }
1158 }
1159
1160 fn detach_callback(&mut self) {
1161 if let Some(id) = self.overscroll_callback_id.take() {
1162 self.overscroll.remove_invalidate_callback(id);
1163 }
1164 }
1165}
1166
1167impl DelegatableNode for TranslatedContentContextNode {
1168 fn node_state(&self) -> &NodeState {
1169 &self.state
1170 }
1171}
1172
1173impl ModifierNode for TranslatedContentContextNode {
1174 fn on_attach(&mut self, context: &mut dyn cranpose_foundation::ModifierNodeContext) {
1175 self.node_id = context.node_id();
1176 self.attach_callback();
1177 }
1178
1179 fn on_detach(&mut self) {
1180 self.detach_callback();
1181 self.node_id = None;
1182 }
1183}
1184
1185impl DelegatableNode for MotionContextAnimatedNode {
1186 fn node_state(&self) -> &NodeState {
1187 &self.state
1188 }
1189}
1190
1191impl ModifierNode for MotionContextAnimatedNode {
1192 fn on_attach(&mut self, context: &mut dyn cranpose_foundation::ModifierNodeContext) {
1193 self.node_id = context.node_id();
1194 self.attach_callbacks();
1195 }
1196
1197 fn on_detach(&mut self) {
1198 self.detach_callbacks();
1199 self.node_id = None;
1200 }
1201}
1202
1203#[derive(Clone)]
1204struct MotionContextAnimatedElement {
1205 motion_context: ScrollMotionContext,
1206}
1207
1208impl MotionContextAnimatedElement {
1209 fn new(motion_context: ScrollMotionContext) -> Self {
1210 Self { motion_context }
1211 }
1212}
1213
1214impl std::fmt::Debug for MotionContextAnimatedElement {
1215 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1216 f.debug_struct("MotionContextAnimatedElement").finish()
1217 }
1218}
1219
1220impl PartialEq for MotionContextAnimatedElement {
1221 fn eq(&self, other: &Self) -> bool {
1222 self.motion_context.ptr_eq(&other.motion_context)
1223 }
1224}
1225
1226impl Eq for MotionContextAnimatedElement {}
1227
1228impl std::hash::Hash for MotionContextAnimatedElement {
1229 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1230 self.motion_context.stable_key().hash(state);
1231 }
1232}
1233
1234impl ModifierNodeElement for MotionContextAnimatedElement {
1235 type Node = MotionContextAnimatedNode;
1236
1237 fn create(&self) -> Self::Node {
1238 MotionContextAnimatedNode::new(self.motion_context.clone())
1239 }
1240
1241 fn update(&self, node: &mut Self::Node) {
1242 if node.motion_context.ptr_eq(&self.motion_context) {
1243 return;
1244 }
1245 node.detach_callbacks();
1246 node.motion_context = self.motion_context.clone();
1247 node.attach_callbacks();
1248 }
1249
1250 fn capabilities(&self) -> NodeCapabilities {
1251 NodeCapabilities::LAYOUT
1252 }
1253}
1254
1255#[derive(Clone)]
1256enum TranslatedContentOffsetSource {
1257 LayoutContentOffset,
1258 LazyList {
1259 state: LazyListState,
1260 is_vertical: bool,
1261 reverse_scrolling: bool,
1262 },
1263}
1264
1265impl TranslatedContentOffsetSource {
1266 fn content_offset_reader(
1267 &self,
1268 overscroll: crate::scroll::OverscrollEffect,
1269 ) -> Option<Rc<dyn Fn() -> Point>> {
1270 match self {
1271 Self::LayoutContentOffset => None,
1272 Self::LazyList {
1273 state, is_vertical, ..
1274 } => Some(Rc::new(lazy_list_content_offset_reader(
1275 *state,
1276 *is_vertical,
1277 overscroll,
1278 ))),
1279 }
1280 }
1281
1282 fn is_vertical(&self) -> Option<bool> {
1283 match self {
1284 Self::LayoutContentOffset => None,
1285 Self::LazyList { is_vertical, .. } => Some(*is_vertical),
1286 }
1287 }
1288
1289 fn reverse_scrolling(&self) -> Option<bool> {
1290 match self {
1291 Self::LayoutContentOffset => None,
1292 Self::LazyList {
1293 reverse_scrolling, ..
1294 } => Some(*reverse_scrolling),
1295 }
1296 }
1297}
1298
1299fn lazy_list_content_offset_reader(
1300 state: LazyListState,
1301 is_vertical: bool,
1302 overscroll: crate::scroll::OverscrollEffect,
1303) -> impl Fn() -> Point {
1304 move || {
1305 let info = state.layout_info();
1306 if info.visible_items_info.is_empty() {
1307 return Point::default();
1308 };
1309 overscroll.set_dimension(info.viewport_size);
1310 let main_offset = info.snap_anchor_offset;
1311 let bounce = overscroll.offset();
1312 if is_vertical {
1313 Point::new(0.0, main_offset + bounce)
1314 } else {
1315 Point::new(main_offset + bounce, 0.0)
1316 }
1317 }
1318}
1319
1320#[derive(Clone)]
1321struct TranslatedContentContextElement {
1322 identity: usize,
1323 offset_source: TranslatedContentOffsetSource,
1324 overscroll: crate::scroll::OverscrollEffect,
1325}
1326
1327impl TranslatedContentContextElement {
1328 fn new(
1329 identity: usize,
1330 offset_source: TranslatedContentOffsetSource,
1331 overscroll: crate::scroll::OverscrollEffect,
1332 ) -> Self {
1333 Self {
1334 identity,
1335 offset_source,
1336 overscroll,
1337 }
1338 }
1339}
1340
1341impl std::fmt::Debug for TranslatedContentContextElement {
1342 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1343 let offset_source = match &self.offset_source {
1344 TranslatedContentOffsetSource::LayoutContentOffset => "layout",
1345 TranslatedContentOffsetSource::LazyList { .. } => "lazy_list",
1346 };
1347 f.debug_struct("TranslatedContentContextElement")
1348 .field("identity", &self.identity)
1349 .field("offset_source", &offset_source)
1350 .finish()
1351 }
1352}
1353
1354impl PartialEq for TranslatedContentContextElement {
1355 fn eq(&self, other: &Self) -> bool {
1356 self.identity == other.identity
1357 && self.overscroll.ptr_eq(&other.overscroll)
1358 && self.offset_source.is_vertical() == other.offset_source.is_vertical()
1359 && self.offset_source.reverse_scrolling() == other.offset_source.reverse_scrolling()
1360 }
1361}
1362
1363impl Eq for TranslatedContentContextElement {}
1364
1365impl std::hash::Hash for TranslatedContentContextElement {
1366 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1367 self.identity.hash(state);
1368 self.offset_source.is_vertical().hash(state);
1369 self.offset_source.reverse_scrolling().hash(state);
1370 }
1371}
1372
1373impl ModifierNodeElement for TranslatedContentContextElement {
1374 type Node = TranslatedContentContextNode;
1375
1376 fn create(&self) -> Self::Node {
1377 TranslatedContentContextNode::new(
1378 self.identity,
1379 self.offset_source.clone(),
1380 self.overscroll.clone(),
1381 )
1382 }
1383
1384 fn update(&self, node: &mut Self::Node) {
1385 node.identity = self.identity;
1386 node.offset_source = self.offset_source.clone();
1387 if !node.overscroll.ptr_eq(&self.overscroll) {
1388 node.detach_callback();
1389 node.overscroll = self.overscroll.clone();
1390 node.attach_callback();
1391 }
1392 }
1393
1394 fn capabilities(&self) -> NodeCapabilities {
1395 NodeCapabilities::LAYOUT
1396 }
1397}
1398
1399impl Modifier {
1400 pub fn horizontal_scroll(self, state: ScrollState, reverse_scrolling: bool) -> Self {
1418 self.then(scroll_impl(state, false, reverse_scrolling, None))
1419 }
1420
1421 pub fn vertical_scroll(self, state: ScrollState, reverse_scrolling: bool) -> Self {
1430 self.then(scroll_impl(state, true, reverse_scrolling, None))
1431 }
1432
1433 pub fn horizontal_scroll_without_gestures(
1440 self,
1441 state: ScrollState,
1442 reverse_scrolling: bool,
1443 ) -> Self {
1444 self.then(scroll_impl(
1445 state,
1446 false,
1447 reverse_scrolling,
1448 Some(Rc::new(|| false)),
1449 ))
1450 }
1451
1452 pub fn vertical_scroll_without_gestures(
1456 self,
1457 state: ScrollState,
1458 reverse_scrolling: bool,
1459 ) -> Self {
1460 self.then(scroll_impl(
1461 state,
1462 true,
1463 reverse_scrolling,
1464 Some(Rc::new(|| false)),
1465 ))
1466 }
1467}
1468
1469fn scroll_impl(
1470 state: ScrollState,
1471 is_vertical: bool,
1472 reverse_scrolling: bool,
1473 guard: Option<Rc<dyn Fn() -> bool>>,
1474) -> Modifier {
1475 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::ScrollState {
1476 state_id: state.id(),
1477 is_vertical,
1478 reverse_scrolling,
1479 });
1480
1481 let pointer_input = drag_gesture_input(
1482 (state.id(), is_vertical),
1483 DragGesture {
1484 target: state,
1485 is_vertical,
1486 reverse_input: false,
1487 motion_context: motion_context.clone(),
1488 guard,
1489 },
1490 );
1491
1492 let overscroll = motion_context.overscroll();
1493 let element = ScrollElement::new(state, overscroll.clone(), is_vertical, reverse_scrolling);
1494 let layout_modifier =
1495 Modifier::with_element(element).with_inspector_metadata(inspector_metadata(
1496 if is_vertical {
1497 "verticalScroll"
1498 } else {
1499 "horizontalScroll"
1500 },
1501 move |info| {
1502 info.add_property("isVertical", is_vertical.to_string());
1503 info.add_property("reverseScrolling", reverse_scrolling.to_string());
1504 },
1505 ));
1506 let motion_modifier = Modifier::with_element(MotionContextAnimatedElement::new(motion_context));
1507 let translated_content_modifier = Modifier::with_element(TranslatedContentContextElement::new(
1508 state.id() as usize,
1509 TranslatedContentOffsetSource::LayoutContentOffset,
1510 overscroll,
1511 ));
1512
1513 pointer_input
1514 .then(motion_modifier)
1515 .then(translated_content_modifier)
1516 .then(layout_modifier)
1517 .clip_to_bounds()
1518 .semantics(scroll_semantics(state, is_vertical, reverse_scrolling))
1519}
1520
1521fn scroll_semantics(
1526 state: ScrollState,
1527 is_vertical: bool,
1528 reverse_scrolling: bool,
1529) -> impl Fn(&mut cranpose_foundation::SemanticsConfiguration) + 'static {
1530 move |config| {
1531 let range = cranpose_foundation::ScrollAxisRange::new(
1532 state.value(),
1533 state.max_value(),
1534 reverse_scrolling,
1535 );
1536 if is_vertical {
1537 config.vertical_scroll = Some(range);
1538 } else {
1539 config.horizontal_scroll = Some(range);
1540 }
1541 config.scroll_by = Some(cranpose_foundation::SemanticsScrollBy::new(
1542 move |dx, dy| {
1543 let delta = if is_vertical { dy } else { dx };
1544 state.dispatch_raw_delta(delta).abs() > f32::EPSILON
1545 },
1546 ));
1547 }
1548}
1549
1550use cranpose_foundation::lazy::LazyListState;
1551
1552impl Modifier {
1553 pub fn lazy_vertical_scroll(self, state: LazyListState, reverse_scrolling: bool) -> Self {
1564 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::LazyList {
1565 state_identity: state.inner_ptr() as usize,
1566 is_vertical: true,
1567 reverse_scrolling,
1568 });
1569 self.lazy_vertical_scroll_with_context(state, reverse_scrolling, (0.0, 0.0), motion_context)
1570 }
1571
1572 pub(crate) fn lazy_vertical_scroll_with_context(
1573 self,
1574 state: LazyListState,
1575 reverse_scrolling: bool,
1576 content_padding: (f32, f32),
1577 motion_context: ScrollMotionContext,
1578 ) -> Self {
1579 self.then(lazy_scroll_impl(
1580 state,
1581 true,
1582 reverse_scrolling,
1583 content_padding,
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 content_padding: (f32, f32),
1593 motion_context: ScrollMotionContext,
1594 ) -> Self {
1595 self.then(lazy_scroll_impl(
1596 state,
1597 false,
1598 reverse_scrolling,
1599 content_padding,
1600 motion_context,
1601 ))
1602 }
1603}
1604
1605fn lazy_scroll_impl(
1606 state: LazyListState,
1607 is_vertical: bool,
1608 reverse_scrolling: bool,
1609 content_padding: (f32, f32),
1610 motion_context: ScrollMotionContext,
1611) -> Modifier {
1612 let list_state = state;
1613 let state_id = state.inner_ptr() as usize;
1614 let key = (state_id, is_vertical, reverse_scrolling);
1615 let overscroll = motion_context.overscroll();
1616 let translated_content_modifier = Modifier::with_element(TranslatedContentContextElement::new(
1617 state_id,
1618 TranslatedContentOffsetSource::LazyList {
1619 state,
1620 is_vertical,
1621 reverse_scrolling,
1622 },
1623 overscroll,
1624 ));
1625
1626 Modifier::with_element(MotionContextAnimatedElement::new(motion_context.clone()))
1627 .then(translated_content_modifier)
1628 .then(drag_gesture_input(
1629 key,
1630 DragGesture {
1631 target: list_state,
1632 is_vertical,
1633 reverse_input: reverse_scrolling,
1634 motion_context,
1635 guard: None,
1636 },
1637 ))
1638 .semantics(lazy_scroll_semantics(
1639 list_state,
1640 is_vertical,
1641 reverse_scrolling,
1642 content_padding,
1643 ))
1644}
1645
1646pub(crate) fn lazy_scroll_semantics(
1652 state: LazyListState,
1653 is_vertical: bool,
1654 reverse_scrolling: bool,
1655 content_padding: (f32, f32),
1656) -> impl Fn(&mut cranpose_foundation::SemanticsConfiguration) + 'static {
1657 move |config| {
1658 let mut value = state.first_visible_item_index() as f32;
1659 if state.first_visible_item_scroll_offset() > 0.0 {
1660 value += 0.5;
1661 }
1662 let max_value = if state.can_scroll_forward() {
1663 value + 1.0
1664 } else {
1665 value
1666 };
1667 let range = cranpose_foundation::ScrollAxisRange::new(value, max_value, reverse_scrolling)
1668 .with_content_padding(content_padding.0, content_padding.1);
1669 let count = state.total_items_count();
1670 config.collection = Some(cranpose_foundation::CollectionInfo {
1671 rows: if is_vertical { count } else { 1 },
1672 columns: if is_vertical { 1 } else { count },
1673 });
1674 config
1675 .role
1676 .get_or_insert(cranpose_foundation::SemanticsWidgetRole::List);
1677 if is_vertical {
1678 config.vertical_scroll = Some(range);
1679 } else {
1680 config.horizontal_scroll = Some(range);
1681 }
1682 config.scroll_by = Some(cranpose_foundation::SemanticsScrollBy::new(
1683 move |dx, dy| {
1684 let delta = if is_vertical { dy } else { dx };
1685 state.dispatch_scroll_delta(-delta).abs() > f32::EPSILON
1686 },
1687 ));
1688 config.scroll_to_index = Some(cranpose_foundation::SemanticsScrollToIndex::new(
1689 move |index| jump_to_row(&state, index),
1690 ));
1691 }
1692}
1693
1694fn jump_to_row(state: &LazyListState, index: usize) -> bool {
1698 if index >= state.total_items_count() {
1699 return false;
1700 }
1701 state.scroll_to_item(index, 0.0);
1702 true
1703}
1704
1705impl Modifier {
1706 pub fn draggable(self, axis: Axis, state: DraggableState) -> Self {
1718 self.then(draggable_impl(axis, state, None))
1719 }
1720
1721 pub fn draggable_guarded(
1727 self,
1728 axis: Axis,
1729 state: DraggableState,
1730 guard: impl Fn() -> bool + 'static,
1731 ) -> Self {
1732 self.then(draggable_impl(axis, state, Some(Rc::new(guard))))
1733 }
1734}
1735
1736fn draggable_impl(
1737 axis: Axis,
1738 state: DraggableState,
1739 guard: Option<Rc<dyn Fn() -> bool>>,
1740) -> Modifier {
1741 let is_vertical = axis.is_vertical();
1742 let identity = state.identity();
1743 drag_gesture_input(
1744 (identity, is_vertical),
1745 DragGesture {
1746 target: state,
1747 is_vertical,
1748 reverse_input: false,
1749 motion_context: scroll_motion_context_for_key(ScrollMotionContextKey::Draggable {
1750 state_identity: identity,
1751 is_vertical,
1752 }),
1753 guard,
1754 },
1755 )
1756}