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