1use super::{inspector_metadata, Modifier, Point, PointerEventKind};
27use crate::current_density;
28use crate::fling_animation::{
29 fling_rest_position, FlingAnimation, SettleAnimation, MIN_FLING_VELOCITY,
30};
31use crate::render_state::schedule_modifier_slices_repass;
32use crate::scroll::{
33 scroll_motion_context_for_key, ScrollElement, ScrollMotionContext, ScrollMotionContextKey,
34 ScrollSettlePolicy, ScrollState,
35};
36use cranpose_core::internal::FrameCallbackRegistration;
37use cranpose_core::{current_runtime_handle, NodeId};
38use cranpose_foundation::{
39 velocity_tracker::ASSUME_STOPPED_MS, DelegatableNode, ModifierNode, ModifierNodeElement,
40 NodeCapabilities, NodeState, PointerButton, PointerButtons, VelocityTracker1D, DRAG_THRESHOLD,
41 MAX_FLING_VELOCITY,
42};
43use std::cell::{Cell, RefCell};
44use std::rc::Rc;
45use web_time::Instant;
46
47#[cfg(feature = "test-helpers")]
48pub fn last_fling_velocity() -> f32 {
49 crate::render_state::debug_last_fling_velocity()
50}
51
52#[cfg(feature = "test-helpers")]
53pub fn reset_last_fling_velocity() {
54 crate::render_state::debug_reset_last_fling_velocity();
55}
56
57#[inline]
58fn set_last_fling_velocity(velocity: f32) {
59 crate::render_state::record_last_fling_velocity(velocity);
60}
61
62struct ScrollGestureState {
68 drag_down_position: Option<Point>,
71
72 last_position: Option<Point>,
75
76 is_dragging: bool,
80
81 axis_locked_out: bool,
87
88 velocity_tracker: VelocityTracker1D,
90
91 gesture_start_time: Option<Instant>,
93
94 gesture_start_event_time_ms: Option<i64>,
98
99 last_velocity_sample_ms: Option<i64>,
101
102 fling_animation: Option<FlingAnimation>,
104
105 settle_animation: Option<SettleAnimation>,
107
108 wheel_settle_watcher: Option<WheelSettleWatcher>,
111}
112
113impl Default for ScrollGestureState {
114 fn default() -> Self {
115 Self {
116 drag_down_position: None,
117 last_position: None,
118 is_dragging: false,
119 axis_locked_out: false,
120 velocity_tracker: VelocityTracker1D::new(),
121 gesture_start_time: None,
122 gesture_start_event_time_ms: None,
123 last_velocity_sample_ms: None,
124 fling_animation: None,
125 settle_animation: None,
126 wheel_settle_watcher: None,
127 }
128 }
129}
130
131#[inline]
141fn calculate_total_delta(from: Point, to: Point, is_vertical: bool) -> f32 {
142 if is_vertical {
143 to.y - from.y
144 } else {
145 to.x - from.x
146 }
147}
148
149#[inline]
154fn calculate_incremental_delta(from: Point, to: Point, is_vertical: bool) -> f32 {
155 if is_vertical {
156 to.y - from.y
157 } else {
158 to.x - from.x
159 }
160}
161
162trait ScrollTarget: Clone {
170 fn apply_delta(&self, delta: f32) -> f32;
172
173 fn apply_wheel_delta(&self, delta: f32) -> f32 {
175 self.apply_delta(delta)
176 }
177
178 fn apply_fling_delta(&self, delta: f32) -> f32;
180
181 fn invalidate(&self);
183
184 fn current_offset(&self) -> f32;
186
187 fn can_scroll(&self) -> bool {
195 true
196 }
197
198 fn can_consume(&self, gesture_delta: f32) -> bool {
204 let _ = gesture_delta;
205 self.can_scroll()
206 }
207
208 fn settle_policy(&self) -> Option<ScrollSettlePolicy> {
211 None
212 }
213}
214
215impl ScrollTarget for ScrollState {
216 fn apply_delta(&self, delta: f32) -> f32 {
217 self.dispatch_raw_delta(-delta)
219 }
220
221 fn apply_fling_delta(&self, delta: f32) -> f32 {
222 self.dispatch_raw_delta(delta)
223 }
224
225 fn invalidate(&self) {
226 }
228
229 fn current_offset(&self) -> f32 {
230 self.value()
231 }
232
233 fn can_scroll(&self) -> bool {
234 self.max_value() > 0.0
235 }
236
237 fn can_consume(&self, gesture_delta: f32) -> bool {
238 let raw = -gesture_delta;
241 if raw > 0.0 {
242 self.value_non_reactive() < self.max_value()
243 } else {
244 self.value_non_reactive() > 0.0
245 }
246 }
247
248 fn settle_policy(&self) -> Option<ScrollSettlePolicy> {
249 ScrollState::settle_policy(self)
250 }
251}
252
253impl ScrollTarget for LazyListState {
254 fn apply_delta(&self, delta: f32) -> f32 {
255 self.dispatch_scroll_delta(delta)
259 }
260
261 fn apply_wheel_delta(&self, delta: f32) -> f32 {
262 if delta.abs() <= 0.001 {
263 0.0
264 } else {
265 self.dispatch_scroll_delta(delta)
266 }
267 }
268
269 fn apply_fling_delta(&self, delta: f32) -> f32 {
270 -self.dispatch_scroll_delta(-delta)
271 }
272
273 fn invalidate(&self) {
274 }
277
278 fn current_offset(&self) -> f32 {
279 self.first_visible_item_scroll_offset()
281 }
282
283 fn can_scroll(&self) -> bool {
284 self.layout_info().total_items_count == 0
287 || self.can_scroll_forward_non_reactive()
288 || self.can_scroll_backward_non_reactive()
289 }
290
291 fn can_consume(&self, gesture_delta: f32) -> bool {
292 if self.layout_info().total_items_count == 0 {
296 return true;
297 }
298 if gesture_delta < 0.0 {
299 self.can_scroll_forward_non_reactive()
300 } else {
301 self.can_scroll_backward_non_reactive()
302 }
303 }
304}
305
306const WHEEL_SETTLE_IDLE_NANOS: u64 = 180_000_000;
314
315struct WheelSettleWatcher {
318 is_running: Rc<Cell<bool>>,
319 registration: Rc<RefCell<Option<FrameCallbackRegistration>>>,
320}
321
322impl WheelSettleWatcher {
323 fn cancel(&self) {
324 self.is_running.set(false);
325 self.registration.borrow_mut().take();
326 }
327}
328
329struct ScrollGestureDetector<S: ScrollTarget> {
330 gesture_state: Rc<RefCell<ScrollGestureState>>,
332
333 scroll_target: S,
335
336 is_vertical: bool,
338
339 reverse_scrolling: bool,
341
342 motion_context: ScrollMotionContext,
344}
345
346impl<S: ScrollTarget + 'static> ScrollGestureDetector<S> {
347 fn new(
349 gesture_state: Rc<RefCell<ScrollGestureState>>,
350 scroll_target: S,
351 is_vertical: bool,
352 reverse_scrolling: bool,
353 motion_context: ScrollMotionContext,
354 ) -> Self {
355 Self {
356 gesture_state,
357 scroll_target,
358 is_vertical,
359 reverse_scrolling,
360 motion_context,
361 }
362 }
363
364 fn on_down(&self, position: Point, time_ms: Option<i64>) -> bool {
373 let mut gs = self.gesture_state.borrow_mut();
374
375 if let Some(fling) = gs.fling_animation.take() {
377 fling.cancel();
378 }
379 if let Some(settle) = gs.settle_animation.take() {
380 settle.cancel();
381 }
382 if let Some(watcher) = gs.wheel_settle_watcher.take() {
383 watcher.cancel();
384 }
385 self.motion_context.set_active(false);
386
387 gs.drag_down_position = Some(position);
388 gs.last_position = Some(position);
389 gs.is_dragging = false;
390 gs.axis_locked_out = false;
391 gs.velocity_tracker.reset();
392 gs.gesture_start_time = Some(Instant::now());
393 gs.gesture_start_event_time_ms = time_ms;
394
395 let pos = if self.is_vertical {
397 position.y
398 } else {
399 position.x
400 };
401 gs.velocity_tracker.add_data_point(0, pos);
402 gs.last_velocity_sample_ms = Some(0);
403
404 false
406 }
407
408 fn on_move(&self, position: Point, buttons: PointerButtons, time_ms: Option<i64>) -> bool {
422 let mut gs = self.gesture_state.borrow_mut();
423
424 if !buttons.contains(PointerButton::Primary) && gs.drag_down_position.is_some() {
426 gs.drag_down_position = None;
427 gs.last_position = None;
428 gs.is_dragging = false;
429 gs.axis_locked_out = false;
430 gs.gesture_start_time = None;
431 gs.gesture_start_event_time_ms = None;
432 gs.last_velocity_sample_ms = None;
433 gs.velocity_tracker.reset();
434 self.motion_context.set_active(false);
435 return false;
436 }
437
438 let Some(down_pos) = gs.drag_down_position else {
439 return false;
440 };
441
442 let Some(last_pos) = gs.last_position else {
443 gs.last_position = Some(position);
444 return false;
445 };
446
447 let incremental_delta = calculate_incremental_delta(last_pos, position, self.is_vertical);
448
449 if !gs.is_dragging && !gs.axis_locked_out {
459 let signed_main_delta = calculate_total_delta(down_pos, position, self.is_vertical);
460 let main_delta = signed_main_delta.abs();
461 let cross_delta = calculate_total_delta(down_pos, position, !self.is_vertical).abs();
462 if main_delta > DRAG_THRESHOLD && main_delta >= cross_delta {
463 if self.scroll_target.can_consume(signed_main_delta) {
466 gs.is_dragging = true;
467 self.motion_context.set_active(true);
468 }
469 } else if cross_delta > DRAG_THRESHOLD && cross_delta > main_delta {
470 gs.axis_locked_out = true;
471 }
472 }
473
474 gs.last_position = Some(position);
475
476 let pos = if self.is_vertical {
478 position.y
479 } else {
480 position.x
481 };
482 let event_sample_ms = gs
483 .gesture_start_event_time_ms
484 .zip(time_ms)
485 .map(|(start_ms, now_ms)| now_ms - start_ms);
486 let sample_ms = if let Some(event_sample_ms) = event_sample_ms {
487 Some(match gs.last_velocity_sample_ms {
494 Some(last_sample_ms) => event_sample_ms.max(last_sample_ms),
495 None => event_sample_ms.max(0),
496 })
497 } else if let Some(start_time) = gs.gesture_start_time {
498 let elapsed_ms = start_time.elapsed().as_millis() as i64;
501 Some(match gs.last_velocity_sample_ms {
504 Some(last_sample_ms) => {
505 let mut sample_ms = if elapsed_ms <= last_sample_ms {
506 last_sample_ms + 1
507 } else {
508 elapsed_ms
509 };
510 if sample_ms - last_sample_ms > ASSUME_STOPPED_MS {
512 sample_ms = last_sample_ms + ASSUME_STOPPED_MS;
513 }
514 sample_ms
515 }
516 None => elapsed_ms,
517 })
518 } else {
519 None
520 };
521 if let Some(sample_ms) = sample_ms {
522 log::trace!(
523 target: "cranpose::velocity",
524 "sample t={sample_ms}ms pos={pos:.2} event_time={time_ms:?}"
525 );
526 gs.velocity_tracker.add_data_point(sample_ms, pos);
527 gs.last_velocity_sample_ms = Some(sample_ms);
528 }
529
530 if gs.is_dragging {
531 drop(gs); let delta = if self.reverse_scrolling {
533 -incremental_delta
534 } else {
535 incremental_delta
536 };
537 let _ = self.scroll_target.apply_delta(delta);
538 self.scroll_target.invalidate();
539 true } else {
541 false
542 }
543 }
544
545 fn finish_gesture(&self, allow_fling: bool, release_time_ms: Option<i64>) -> bool {
552 let (was_dragging, velocity, start_fling, existing_fling) = {
553 let mut gs = self.gesture_state.borrow_mut();
554 let was_dragging = gs.is_dragging;
555 let mut velocity = 0.0;
556
557 if allow_fling && was_dragging && gs.gesture_start_time.is_some() {
558 let release_sample_ms = release_time_ms
563 .zip(gs.gesture_start_event_time_ms)
564 .map(|(release_ms, start_ms)| release_ms - start_ms)
565 .or_else(|| {
566 gs.gesture_start_time
567 .map(|start| start.elapsed().as_millis() as i64)
568 });
569 let rested_before_release = release_sample_ms
570 .zip(gs.last_velocity_sample_ms)
571 .is_some_and(|(release_ms, last_sample_ms)| {
572 release_ms - last_sample_ms > ASSUME_STOPPED_MS
573 });
574 if !rested_before_release {
575 velocity = gs
576 .velocity_tracker
577 .calculate_velocity_with_max(MAX_FLING_VELOCITY);
578 }
579 }
580
581 let start_fling = allow_fling && was_dragging && velocity.abs() > MIN_FLING_VELOCITY;
582 let existing_fling = if start_fling {
583 gs.fling_animation.take()
584 } else {
585 None
586 };
587
588 gs.drag_down_position = None;
589 gs.last_position = None;
590 gs.is_dragging = false;
591 gs.axis_locked_out = false;
592 gs.gesture_start_time = None;
593 gs.gesture_start_event_time_ms = None;
594 gs.last_velocity_sample_ms = None;
595
596 (was_dragging, velocity, start_fling, existing_fling)
597 };
598
599 if allow_fling && was_dragging {
601 log::debug!(
602 target: "cranpose::velocity",
603 "gesture finished: fling velocity={velocity:.2} dp/s start_fling={start_fling}"
604 );
605 set_last_fling_velocity(velocity);
606 }
607
608 let adjusted_velocity = if self.reverse_scrolling {
610 -velocity
611 } else {
612 velocity
613 };
614 let fling_velocity = -adjusted_velocity;
615
616 let settle_target = if was_dragging {
621 self.scroll_target.settle_policy().and_then(|policy| {
622 let current = self.scroll_target.current_offset();
623 let proposed = if start_fling {
624 fling_rest_position(current, fling_velocity, current_density())
625 } else {
626 current
627 };
628 let target = policy(proposed, fling_velocity);
629 ((target - proposed).abs() > 0.5).then_some(target)
630 })
631 } else {
632 None
633 };
634
635 if let Some(target) = settle_target {
636 if let Some(old_fling) = existing_fling {
637 old_fling.cancel();
638 }
639 self.start_settle_animation(target, fling_velocity);
640 } else if start_fling {
641 if let Some(old_fling) = existing_fling {
642 old_fling.cancel();
643 }
644
645 if let Some(runtime) = current_runtime_handle() {
647 self.motion_context.set_active(true);
648 let scroll_target = self.scroll_target.clone();
649 let fling = FlingAnimation::new(runtime);
650 let motion_context = self.motion_context.clone();
651
652 let initial_value = scroll_target.current_offset();
654
655 let scroll_target_for_fling = scroll_target.clone();
656 let scroll_target_for_end = scroll_target.clone();
657
658 fling.start_fling(
659 initial_value,
660 fling_velocity,
661 current_density(),
662 move |delta| {
663 let consumed = scroll_target_for_fling.apply_fling_delta(delta);
665 scroll_target_for_fling.invalidate();
666 consumed
667 },
668 move || {
669 scroll_target_for_end.invalidate();
671 motion_context.set_active(false);
672 },
673 );
674
675 let mut gs = self.gesture_state.borrow_mut();
676 gs.fling_animation = Some(fling);
677 }
678 } else {
679 self.motion_context.set_active(false);
680 }
681
682 was_dragging
683 }
684
685 fn start_settle_animation(&self, target: f32, initial_velocity: f32) {
688 let Some(runtime) = current_runtime_handle() else {
689 self.motion_context.set_active(false);
690 return;
691 };
692 self.motion_context.set_active(true);
693 let settle = SettleAnimation::new(runtime);
694 let scroll_target_for_settle = self.scroll_target.clone();
695 let scroll_target_for_end = self.scroll_target.clone();
696 let motion_context = self.motion_context.clone();
697 settle.start_settle(
698 self.scroll_target.current_offset(),
699 initial_velocity,
700 target,
701 move |delta| {
702 let consumed = scroll_target_for_settle.apply_fling_delta(delta);
703 scroll_target_for_settle.invalidate();
704 consumed
705 },
706 move || {
707 scroll_target_for_end.invalidate();
708 motion_context.set_active(false);
709 },
710 );
711 let mut gs = self.gesture_state.borrow_mut();
712 gs.settle_animation = Some(settle);
713 }
714
715 fn on_up(&self, time_ms: Option<i64>) -> bool {
722 self.finish_gesture(true, time_ms)
723 }
724
725 fn on_cancel(&self) -> bool {
729 self.finish_gesture(false, None)
730 }
731
732 fn on_scroll(&self, axis_delta: f32) -> bool {
736 if axis_delta.abs() <= f32::EPSILON {
737 return false;
738 }
739
740 {
741 let mut gs = self.gesture_state.borrow_mut();
743 if let Some(fling) = gs.fling_animation.take() {
744 fling.cancel();
745 }
746 if let Some(settle) = gs.settle_animation.take() {
747 settle.cancel();
748 }
749 gs.drag_down_position = None;
750 gs.last_position = None;
751 gs.is_dragging = false;
752 gs.axis_locked_out = false;
753 gs.gesture_start_time = None;
754 gs.gesture_start_event_time_ms = None;
755 gs.last_velocity_sample_ms = None;
756 gs.velocity_tracker.reset();
757 }
758
759 self.motion_context.activate_for_current_frame();
760
761 let delta = if self.reverse_scrolling {
762 -axis_delta
763 } else {
764 axis_delta
765 };
766 let consumed = self.scroll_target.apply_wheel_delta(delta);
767 if consumed.abs() > 0.001 {
768 self.scroll_target.invalidate();
769 self.ensure_wheel_settle_watcher();
770 true
771 } else {
772 false
773 }
774 }
775
776 fn ensure_wheel_settle_watcher(&self) {
781 if self.scroll_target.settle_policy().is_none() {
782 return;
783 }
784 {
785 let gs = self.gesture_state.borrow();
786 if gs
787 .wheel_settle_watcher
788 .as_ref()
789 .is_some_and(|watcher| watcher.is_running.get())
790 {
791 return;
792 }
793 }
794 let Some(runtime) = current_runtime_handle() else {
795 return;
796 };
797
798 let is_running = Rc::new(Cell::new(true));
799 let registration = Rc::new(RefCell::new(None));
800
801 struct WheelSettleLoop<S: ScrollTarget> {
802 detector: ScrollGestureDetector<S>,
803 gesture_state: Rc<RefCell<ScrollGestureState>>,
804 frame_clock: cranpose_core::internal::FrameClock,
805 is_running: Rc<Cell<bool>>,
806 registration: Rc<RefCell<Option<FrameCallbackRegistration>>>,
807 last_offset: Rc<Cell<f32>>,
808 idle_nanos: Rc<Cell<u64>>,
809 last_frame: Rc<Cell<Option<u64>>>,
810 }
811
812 impl<S: ScrollTarget + 'static> WheelSettleLoop<S> {
813 fn next(&self) -> Self {
814 Self {
815 detector: self.detector.clone_for_watcher(),
816 gesture_state: Rc::clone(&self.gesture_state),
817 frame_clock: self.frame_clock.clone(),
818 is_running: Rc::clone(&self.is_running),
819 registration: Rc::clone(&self.registration),
820 last_offset: Rc::clone(&self.last_offset),
821 idle_nanos: Rc::clone(&self.idle_nanos),
822 last_frame: Rc::clone(&self.last_frame),
823 }
824 }
825
826 fn schedule(self) {
827 let continuation = self.next();
828 let registration_slot = Rc::clone(&self.registration);
829 let new_registration = self.frame_clock.with_frame_nanos(move |frame_time_nanos| {
830 let this = &continuation;
831 if !this.is_running.get() {
832 return;
833 }
834 {
836 let gs = this.gesture_state.borrow();
837 let animating = gs.is_dragging
838 || gs
839 .fling_animation
840 .as_ref()
841 .is_some_and(FlingAnimation::is_running)
842 || gs
843 .settle_animation
844 .as_ref()
845 .is_some_and(SettleAnimation::is_running);
846 if animating {
847 this.is_running.set(false);
848 return;
849 }
850 }
851
852 let offset = this.detector.scroll_target.current_offset();
853 let dt = this
854 .last_frame
855 .get()
856 .map_or(0, |last| frame_time_nanos.saturating_sub(last));
857 this.last_frame.set(Some(frame_time_nanos));
858 if (offset - this.last_offset.get()).abs() > 0.01 {
859 this.last_offset.set(offset);
860 this.idle_nanos.set(0);
861 } else {
862 this.idle_nanos.set(this.idle_nanos.get() + dt);
863 }
864
865 if this.idle_nanos.get() >= WHEEL_SETTLE_IDLE_NANOS {
866 this.is_running.set(false);
867 if let Some(policy) = this.detector.scroll_target.settle_policy() {
868 let target = policy(offset, 0.0);
869 if (target - offset).abs() > 0.5 {
870 this.detector.start_settle_animation(target, 0.0);
871 }
872 }
873 return;
874 }
875
876 continuation.next().schedule();
877 });
878 *registration_slot.borrow_mut() = Some(new_registration);
879 }
880 }
881
882 WheelSettleLoop {
883 detector: self.clone_for_watcher(),
884 gesture_state: Rc::clone(&self.gesture_state),
885 frame_clock: runtime.frame_clock(),
886 is_running: Rc::clone(&is_running),
887 registration: Rc::clone(®istration),
888 last_offset: Rc::new(Cell::new(self.scroll_target.current_offset())),
889 idle_nanos: Rc::new(Cell::new(0u64)),
890 last_frame: Rc::new(Cell::new(None::<u64>)),
891 }
892 .schedule();
893
894 self.gesture_state.borrow_mut().wheel_settle_watcher = Some(WheelSettleWatcher {
895 is_running,
896 registration,
897 });
898 }
899
900 fn clone_for_watcher(&self) -> ScrollGestureDetector<S> {
901 ScrollGestureDetector {
902 gesture_state: Rc::clone(&self.gesture_state),
903 scroll_target: self.scroll_target.clone(),
904 is_vertical: self.is_vertical,
905 reverse_scrolling: self.reverse_scrolling,
906 motion_context: self.motion_context.clone(),
907 }
908 }
909}
910
911pub(crate) struct MotionContextAnimatedNode {
912 state: NodeState,
913 motion_context: ScrollMotionContext,
914 invalidation_callback_id: Option<u64>,
915 node_id: Option<NodeId>,
916}
917
918impl MotionContextAnimatedNode {
919 fn new(motion_context: ScrollMotionContext) -> Self {
920 Self {
921 state: NodeState::new(),
922 motion_context,
923 invalidation_callback_id: None,
924 node_id: None,
925 }
926 }
927
928 pub(crate) fn is_active(&self) -> bool {
929 self.motion_context.is_active()
930 }
931}
932
933pub(crate) struct TranslatedContentContextNode {
934 state: NodeState,
935 identity: usize,
936 offset_source: TranslatedContentOffsetSource,
937}
938
939impl TranslatedContentContextNode {
940 fn new(identity: usize, offset_source: TranslatedContentOffsetSource) -> Self {
941 Self {
942 state: NodeState::new(),
943 identity,
944 offset_source,
945 }
946 }
947
948 pub(crate) fn is_active(&self) -> bool {
949 true
950 }
951
952 pub(crate) fn identity(&self) -> usize {
953 self.identity
954 }
955
956 pub(crate) fn content_offset_reader(&self) -> Option<Rc<dyn Fn() -> Point>> {
957 self.offset_source.content_offset_reader()
958 }
959}
960
961impl DelegatableNode for TranslatedContentContextNode {
962 fn node_state(&self) -> &NodeState {
963 &self.state
964 }
965}
966
967impl ModifierNode for TranslatedContentContextNode {}
968
969impl DelegatableNode for MotionContextAnimatedNode {
970 fn node_state(&self) -> &NodeState {
971 &self.state
972 }
973}
974
975impl ModifierNode for MotionContextAnimatedNode {
976 fn on_attach(&mut self, context: &mut dyn cranpose_foundation::ModifierNodeContext) {
977 let node_id = context.node_id();
978 self.node_id = node_id;
979 if let Some(node_id) = node_id {
980 let callback_id = self
981 .motion_context
982 .add_invalidate_callback(Box::new(move || {
983 schedule_modifier_slices_repass(node_id);
984 }));
985 self.invalidation_callback_id = Some(callback_id);
986 }
987 }
988
989 fn on_detach(&mut self) {
990 if let Some(id) = self.invalidation_callback_id.take() {
991 self.motion_context.remove_invalidate_callback(id);
992 }
993 self.node_id = None;
994 }
995}
996
997#[derive(Clone)]
998struct MotionContextAnimatedElement {
999 motion_context: ScrollMotionContext,
1000}
1001
1002impl MotionContextAnimatedElement {
1003 fn new(motion_context: ScrollMotionContext) -> Self {
1004 Self { motion_context }
1005 }
1006}
1007
1008impl std::fmt::Debug for MotionContextAnimatedElement {
1009 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1010 f.debug_struct("MotionContextAnimatedElement").finish()
1011 }
1012}
1013
1014impl PartialEq for MotionContextAnimatedElement {
1015 fn eq(&self, other: &Self) -> bool {
1016 self.motion_context.ptr_eq(&other.motion_context)
1017 }
1018}
1019
1020impl Eq for MotionContextAnimatedElement {}
1021
1022impl std::hash::Hash for MotionContextAnimatedElement {
1023 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1024 self.motion_context.stable_key().hash(state);
1025 }
1026}
1027
1028impl ModifierNodeElement for MotionContextAnimatedElement {
1029 type Node = MotionContextAnimatedNode;
1030
1031 fn create(&self) -> Self::Node {
1032 MotionContextAnimatedNode::new(self.motion_context.clone())
1033 }
1034
1035 fn update(&self, node: &mut Self::Node) {
1036 if node.motion_context.ptr_eq(&self.motion_context) {
1037 return;
1038 }
1039 if let Some(id) = node.invalidation_callback_id.take() {
1040 node.motion_context.remove_invalidate_callback(id);
1041 }
1042 node.motion_context = self.motion_context.clone();
1043 if let Some(node_id) = node.node_id {
1044 let callback_id = node
1045 .motion_context
1046 .add_invalidate_callback(Box::new(move || {
1047 schedule_modifier_slices_repass(node_id);
1048 }));
1049 node.invalidation_callback_id = Some(callback_id);
1050 }
1051 }
1052
1053 fn capabilities(&self) -> NodeCapabilities {
1054 NodeCapabilities::LAYOUT
1055 }
1056}
1057
1058#[derive(Clone)]
1059enum TranslatedContentOffsetSource {
1060 LayoutContentOffset,
1061 LazyList {
1062 state: LazyListState,
1063 is_vertical: bool,
1064 reverse_scrolling: bool,
1065 },
1066}
1067
1068impl TranslatedContentOffsetSource {
1069 fn content_offset_reader(&self) -> Option<Rc<dyn Fn() -> Point>> {
1070 match self {
1071 Self::LayoutContentOffset => None,
1072 Self::LazyList {
1073 state, is_vertical, ..
1074 } => Some(Rc::new(lazy_list_content_offset_reader(
1075 *state,
1076 *is_vertical,
1077 ))),
1078 }
1079 }
1080
1081 fn is_vertical(&self) -> Option<bool> {
1082 match self {
1083 Self::LayoutContentOffset => None,
1084 Self::LazyList { is_vertical, .. } => Some(*is_vertical),
1085 }
1086 }
1087
1088 fn reverse_scrolling(&self) -> Option<bool> {
1089 match self {
1090 Self::LayoutContentOffset => None,
1091 Self::LazyList {
1092 reverse_scrolling, ..
1093 } => Some(*reverse_scrolling),
1094 }
1095 }
1096}
1097
1098fn lazy_list_content_offset_reader(state: LazyListState, is_vertical: bool) -> impl Fn() -> Point {
1099 move || {
1100 let info = state.layout_info();
1101 if info.visible_items_info.is_empty() {
1102 return Point::default();
1103 };
1104 let main_offset = info.snap_anchor_offset;
1105 if is_vertical {
1106 Point::new(0.0, main_offset)
1107 } else {
1108 Point::new(main_offset, 0.0)
1109 }
1110 }
1111}
1112
1113#[derive(Clone)]
1114struct TranslatedContentContextElement {
1115 identity: usize,
1116 offset_source: TranslatedContentOffsetSource,
1117}
1118
1119impl TranslatedContentContextElement {
1120 fn new(identity: usize, offset_source: TranslatedContentOffsetSource) -> Self {
1121 Self {
1122 identity,
1123 offset_source,
1124 }
1125 }
1126}
1127
1128impl std::fmt::Debug for TranslatedContentContextElement {
1129 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1130 let offset_source = match &self.offset_source {
1131 TranslatedContentOffsetSource::LayoutContentOffset => "layout",
1132 TranslatedContentOffsetSource::LazyList { .. } => "lazy_list",
1133 };
1134 f.debug_struct("TranslatedContentContextElement")
1135 .field("identity", &self.identity)
1136 .field("offset_source", &offset_source)
1137 .finish()
1138 }
1139}
1140
1141impl PartialEq for TranslatedContentContextElement {
1142 fn eq(&self, other: &Self) -> bool {
1143 self.identity == other.identity
1144 && self.offset_source.is_vertical() == other.offset_source.is_vertical()
1145 && self.offset_source.reverse_scrolling() == other.offset_source.reverse_scrolling()
1146 }
1147}
1148
1149impl Eq for TranslatedContentContextElement {}
1150
1151impl std::hash::Hash for TranslatedContentContextElement {
1152 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1153 self.identity.hash(state);
1154 self.offset_source.is_vertical().hash(state);
1155 self.offset_source.reverse_scrolling().hash(state);
1156 }
1157}
1158
1159impl ModifierNodeElement for TranslatedContentContextElement {
1160 type Node = TranslatedContentContextNode;
1161
1162 fn create(&self) -> Self::Node {
1163 TranslatedContentContextNode::new(self.identity, self.offset_source.clone())
1164 }
1165
1166 fn update(&self, node: &mut Self::Node) {
1167 node.identity = self.identity;
1168 node.offset_source = self.offset_source.clone();
1169 }
1170
1171 fn capabilities(&self) -> NodeCapabilities {
1172 NodeCapabilities::LAYOUT
1173 }
1174}
1175
1176impl Modifier {
1181 pub fn horizontal_scroll(self, state: ScrollState, reverse_scrolling: bool) -> Self {
1199 self.then(scroll_impl(state, false, reverse_scrolling, None))
1200 }
1201
1202 pub fn vertical_scroll(self, state: ScrollState, reverse_scrolling: bool) -> Self {
1211 self.then(scroll_impl(state, true, reverse_scrolling, None))
1212 }
1213
1214 pub fn horizontal_scroll_guarded(
1216 self,
1217 state: ScrollState,
1218 reverse_scrolling: bool,
1219 guard: impl Fn() -> bool + 'static,
1220 ) -> Self {
1221 self.then(scroll_impl(
1222 state,
1223 false,
1224 reverse_scrolling,
1225 Some(Rc::new(guard)),
1226 ))
1227 }
1228
1229 pub fn vertical_scroll_guarded(
1231 self,
1232 state: ScrollState,
1233 reverse_scrolling: bool,
1234 guard: impl Fn() -> bool + 'static,
1235 ) -> Self {
1236 self.then(scroll_impl(
1237 state,
1238 true,
1239 reverse_scrolling,
1240 Some(Rc::new(guard)),
1241 ))
1242 }
1243}
1244
1245fn scroll_impl(
1254 state: ScrollState,
1255 is_vertical: bool,
1256 reverse_scrolling: bool,
1257 guard: Option<Rc<dyn Fn() -> bool>>,
1258) -> Modifier {
1259 let gesture_state = Rc::new(RefCell::new(ScrollGestureState::default()));
1261 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::ScrollState {
1262 state_id: state.id(),
1263 is_vertical,
1264 reverse_scrolling,
1265 });
1266
1267 let scroll_state = state.clone();
1269 let pointer_motion_context = motion_context.clone();
1270 let key = (state.id(), is_vertical);
1271 let pointer_input = Modifier::empty().pointer_input(key, move |scope| {
1272 let detector = ScrollGestureDetector::new(
1274 gesture_state.clone(),
1275 scroll_state.clone(),
1276 is_vertical,
1277 false, pointer_motion_context.clone(),
1279 );
1280 let guard = guard.clone();
1281
1282 async move {
1283 scope
1284 .await_pointer_event_scope(|await_scope| async move {
1285 loop {
1287 let event = await_scope.await_pointer_event().await;
1288
1289 if event.id != 0 {
1293 continue;
1294 }
1295
1296 if event.is_consumed() {
1297 if matches!(
1298 event.kind,
1299 PointerEventKind::Down
1300 | PointerEventKind::Move
1301 | PointerEventKind::Up
1302 | PointerEventKind::Cancel
1303 ) {
1304 detector.on_cancel();
1305 }
1306 continue;
1307 }
1308
1309 if let Some(ref guard) = guard {
1310 if !guard() {
1311 if matches!(
1312 event.kind,
1313 PointerEventKind::Up | PointerEventKind::Cancel
1314 ) {
1315 detector.on_cancel();
1316 }
1317 continue;
1318 }
1319 }
1320
1321 let should_consume = match event.kind {
1323 PointerEventKind::Down => {
1324 detector.on_down(event.position, event.time_ms)
1325 }
1326 PointerEventKind::Move => {
1327 detector.on_move(event.position, event.buttons, event.time_ms)
1328 }
1329 PointerEventKind::Up => detector.on_up(event.time_ms),
1330 PointerEventKind::Cancel => detector.on_cancel(),
1331 PointerEventKind::Scroll => detector.on_scroll(if is_vertical {
1332 event.scroll_delta.y
1333 } else {
1334 event.scroll_delta.x
1335 }),
1336 PointerEventKind::Zoom
1337 | PointerEventKind::Enter
1338 | PointerEventKind::Exit => false,
1339 };
1340
1341 if should_consume {
1342 event.consume();
1343 }
1344 }
1345 })
1346 .await;
1347 }
1348 });
1349
1350 let element = ScrollElement::new(state.clone(), is_vertical, reverse_scrolling);
1352 let layout_modifier =
1353 Modifier::with_element(element).with_inspector_metadata(inspector_metadata(
1354 if is_vertical {
1355 "verticalScroll"
1356 } else {
1357 "horizontalScroll"
1358 },
1359 move |info| {
1360 info.add_property("isVertical", is_vertical.to_string());
1361 info.add_property("reverseScrolling", reverse_scrolling.to_string());
1362 },
1363 ));
1364 let motion_modifier =
1365 Modifier::with_element(MotionContextAnimatedElement::new(motion_context.clone()));
1366 let translated_content_modifier = Modifier::with_element(TranslatedContentContextElement::new(
1367 state.id() as usize,
1368 TranslatedContentOffsetSource::LayoutContentOffset,
1369 ));
1370
1371 pointer_input
1373 .then(motion_modifier)
1374 .then(translated_content_modifier)
1375 .then(layout_modifier)
1376 .clip_to_bounds()
1377}
1378
1379use cranpose_foundation::lazy::LazyListState;
1384
1385impl Modifier {
1386 pub fn lazy_vertical_scroll(self, state: LazyListState, reverse_scrolling: bool) -> Self {
1397 self.then(lazy_scroll_impl(state, true, reverse_scrolling))
1398 }
1399
1400 pub fn lazy_horizontal_scroll(self, state: LazyListState, reverse_scrolling: bool) -> Self {
1402 self.then(lazy_scroll_impl(state, false, reverse_scrolling))
1403 }
1404}
1405
1406fn lazy_scroll_impl(state: LazyListState, is_vertical: bool, reverse_scrolling: bool) -> Modifier {
1408 let gesture_state = Rc::new(RefCell::new(ScrollGestureState::default()));
1409 let list_state = state;
1410 let state_id = state.inner_ptr() as usize;
1411 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::LazyList {
1412 state_identity: state_id,
1413 is_vertical,
1414 reverse_scrolling,
1415 });
1416 let key = (state_id, is_vertical, reverse_scrolling);
1417 let translated_content_modifier = Modifier::with_element(TranslatedContentContextElement::new(
1418 state_id,
1419 TranslatedContentOffsetSource::LazyList {
1420 state,
1421 is_vertical,
1422 reverse_scrolling,
1423 },
1424 ));
1425
1426 Modifier::with_element(MotionContextAnimatedElement::new(motion_context.clone()))
1427 .then(translated_content_modifier)
1428 .pointer_input(key, move |scope| {
1429 let detector = ScrollGestureDetector::new(
1431 gesture_state.clone(),
1432 list_state,
1433 is_vertical,
1434 reverse_scrolling,
1435 motion_context.clone(),
1436 );
1437
1438 async move {
1439 scope
1440 .await_pointer_event_scope(|await_scope| async move {
1441 loop {
1442 let event = await_scope.await_pointer_event().await;
1443
1444 if event.id != 0 {
1448 continue;
1449 }
1450
1451 if event.is_consumed() {
1452 if matches!(
1453 event.kind,
1454 PointerEventKind::Down
1455 | PointerEventKind::Move
1456 | PointerEventKind::Up
1457 | PointerEventKind::Cancel
1458 ) {
1459 detector.on_cancel();
1460 }
1461 continue;
1462 }
1463
1464 let should_consume = match event.kind {
1466 PointerEventKind::Down => {
1467 detector.on_down(event.position, event.time_ms)
1468 }
1469 PointerEventKind::Move => {
1470 detector.on_move(event.position, event.buttons, event.time_ms)
1471 }
1472 PointerEventKind::Up => detector.on_up(event.time_ms),
1473 PointerEventKind::Cancel => detector.on_cancel(),
1474 PointerEventKind::Scroll => detector.on_scroll(if is_vertical {
1475 event.scroll_delta.y
1476 } else {
1477 event.scroll_delta.x
1478 }),
1479 PointerEventKind::Zoom
1480 | PointerEventKind::Enter
1481 | PointerEventKind::Exit => false,
1482 };
1483
1484 if should_consume {
1485 event.consume();
1486 }
1487 }
1488 })
1489 .await;
1490 }
1491 })
1492}