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 settle_policy(&self) -> Option<ScrollSettlePolicy> {
201 None
202 }
203}
204
205impl ScrollTarget for ScrollState {
206 fn apply_delta(&self, delta: f32) -> f32 {
207 self.dispatch_raw_delta(-delta)
209 }
210
211 fn apply_fling_delta(&self, delta: f32) -> f32 {
212 self.dispatch_raw_delta(delta)
213 }
214
215 fn invalidate(&self) {
216 }
218
219 fn current_offset(&self) -> f32 {
220 self.value()
221 }
222
223 fn can_scroll(&self) -> bool {
224 self.max_value() > 0.0
225 }
226
227 fn settle_policy(&self) -> Option<ScrollSettlePolicy> {
228 ScrollState::settle_policy(self)
229 }
230}
231
232impl ScrollTarget for LazyListState {
233 fn apply_delta(&self, delta: f32) -> f32 {
234 self.dispatch_scroll_delta(delta)
238 }
239
240 fn apply_wheel_delta(&self, delta: f32) -> f32 {
241 if delta.abs() <= 0.001 {
242 0.0
243 } else {
244 self.dispatch_scroll_delta(delta)
245 }
246 }
247
248 fn apply_fling_delta(&self, delta: f32) -> f32 {
249 -self.dispatch_scroll_delta(-delta)
250 }
251
252 fn invalidate(&self) {
253 }
256
257 fn current_offset(&self) -> f32 {
258 self.first_visible_item_scroll_offset()
260 }
261
262 fn can_scroll(&self) -> bool {
263 self.layout_info().total_items_count == 0
266 || self.can_scroll_forward_non_reactive()
267 || self.can_scroll_backward_non_reactive()
268 }
269}
270
271const WHEEL_SETTLE_IDLE_NANOS: u64 = 180_000_000;
279
280struct WheelSettleWatcher {
283 is_running: Rc<Cell<bool>>,
284 registration: Rc<RefCell<Option<FrameCallbackRegistration>>>,
285}
286
287impl WheelSettleWatcher {
288 fn cancel(&self) {
289 self.is_running.set(false);
290 self.registration.borrow_mut().take();
291 }
292}
293
294struct ScrollGestureDetector<S: ScrollTarget> {
295 gesture_state: Rc<RefCell<ScrollGestureState>>,
297
298 scroll_target: S,
300
301 is_vertical: bool,
303
304 reverse_scrolling: bool,
306
307 motion_context: ScrollMotionContext,
309}
310
311impl<S: ScrollTarget + 'static> ScrollGestureDetector<S> {
312 fn new(
314 gesture_state: Rc<RefCell<ScrollGestureState>>,
315 scroll_target: S,
316 is_vertical: bool,
317 reverse_scrolling: bool,
318 motion_context: ScrollMotionContext,
319 ) -> Self {
320 Self {
321 gesture_state,
322 scroll_target,
323 is_vertical,
324 reverse_scrolling,
325 motion_context,
326 }
327 }
328
329 fn on_down(&self, position: Point, time_ms: Option<i64>) -> bool {
338 let mut gs = self.gesture_state.borrow_mut();
339
340 if let Some(fling) = gs.fling_animation.take() {
342 fling.cancel();
343 }
344 if let Some(settle) = gs.settle_animation.take() {
345 settle.cancel();
346 }
347 if let Some(watcher) = gs.wheel_settle_watcher.take() {
348 watcher.cancel();
349 }
350 self.motion_context.set_active(false);
351
352 gs.drag_down_position = Some(position);
353 gs.last_position = Some(position);
354 gs.is_dragging = false;
355 gs.axis_locked_out = false;
356 gs.velocity_tracker.reset();
357 gs.gesture_start_time = Some(Instant::now());
358 gs.gesture_start_event_time_ms = time_ms;
359
360 let pos = if self.is_vertical {
362 position.y
363 } else {
364 position.x
365 };
366 gs.velocity_tracker.add_data_point(0, pos);
367 gs.last_velocity_sample_ms = Some(0);
368
369 false
371 }
372
373 fn on_move(&self, position: Point, buttons: PointerButtons, time_ms: Option<i64>) -> bool {
387 let mut gs = self.gesture_state.borrow_mut();
388
389 if !buttons.contains(PointerButton::Primary) && gs.drag_down_position.is_some() {
391 gs.drag_down_position = None;
392 gs.last_position = None;
393 gs.is_dragging = false;
394 gs.axis_locked_out = false;
395 gs.gesture_start_time = None;
396 gs.gesture_start_event_time_ms = None;
397 gs.last_velocity_sample_ms = None;
398 gs.velocity_tracker.reset();
399 self.motion_context.set_active(false);
400 return false;
401 }
402
403 let Some(down_pos) = gs.drag_down_position else {
404 return false;
405 };
406
407 let Some(last_pos) = gs.last_position else {
408 gs.last_position = Some(position);
409 return false;
410 };
411
412 let incremental_delta = calculate_incremental_delta(last_pos, position, self.is_vertical);
413
414 if !gs.is_dragging && !gs.axis_locked_out {
424 let main_delta = calculate_total_delta(down_pos, position, self.is_vertical).abs();
425 let cross_delta = calculate_total_delta(down_pos, position, !self.is_vertical).abs();
426 if main_delta > DRAG_THRESHOLD && main_delta >= cross_delta {
427 if self.scroll_target.can_scroll() {
428 gs.is_dragging = true;
429 self.motion_context.set_active(true);
430 }
431 } else if cross_delta > DRAG_THRESHOLD && cross_delta > main_delta {
432 gs.axis_locked_out = true;
433 }
434 }
435
436 gs.last_position = Some(position);
437
438 let pos = if self.is_vertical {
440 position.y
441 } else {
442 position.x
443 };
444 let event_sample_ms = gs
445 .gesture_start_event_time_ms
446 .zip(time_ms)
447 .map(|(start_ms, now_ms)| now_ms - start_ms);
448 let sample_ms = if let Some(event_sample_ms) = event_sample_ms {
449 Some(match gs.last_velocity_sample_ms {
456 Some(last_sample_ms) => event_sample_ms.max(last_sample_ms),
457 None => event_sample_ms.max(0),
458 })
459 } else if let Some(start_time) = gs.gesture_start_time {
460 let elapsed_ms = start_time.elapsed().as_millis() as i64;
463 Some(match gs.last_velocity_sample_ms {
466 Some(last_sample_ms) => {
467 let mut sample_ms = if elapsed_ms <= last_sample_ms {
468 last_sample_ms + 1
469 } else {
470 elapsed_ms
471 };
472 if sample_ms - last_sample_ms > ASSUME_STOPPED_MS {
474 sample_ms = last_sample_ms + ASSUME_STOPPED_MS;
475 }
476 sample_ms
477 }
478 None => elapsed_ms,
479 })
480 } else {
481 None
482 };
483 if let Some(sample_ms) = sample_ms {
484 log::trace!(
485 target: "cranpose::velocity",
486 "sample t={sample_ms}ms pos={pos:.2} event_time={time_ms:?}"
487 );
488 gs.velocity_tracker.add_data_point(sample_ms, pos);
489 gs.last_velocity_sample_ms = Some(sample_ms);
490 }
491
492 if gs.is_dragging {
493 drop(gs); let delta = if self.reverse_scrolling {
495 -incremental_delta
496 } else {
497 incremental_delta
498 };
499 let _ = self.scroll_target.apply_delta(delta);
500 self.scroll_target.invalidate();
501 true } else {
503 false
504 }
505 }
506
507 fn finish_gesture(&self, allow_fling: bool, release_time_ms: Option<i64>) -> bool {
514 let (was_dragging, velocity, start_fling, existing_fling) = {
515 let mut gs = self.gesture_state.borrow_mut();
516 let was_dragging = gs.is_dragging;
517 let mut velocity = 0.0;
518
519 if allow_fling && was_dragging && gs.gesture_start_time.is_some() {
520 let release_sample_ms = release_time_ms
525 .zip(gs.gesture_start_event_time_ms)
526 .map(|(release_ms, start_ms)| release_ms - start_ms)
527 .or_else(|| {
528 gs.gesture_start_time
529 .map(|start| start.elapsed().as_millis() as i64)
530 });
531 let rested_before_release = release_sample_ms
532 .zip(gs.last_velocity_sample_ms)
533 .is_some_and(|(release_ms, last_sample_ms)| {
534 release_ms - last_sample_ms > ASSUME_STOPPED_MS
535 });
536 if !rested_before_release {
537 velocity = gs
538 .velocity_tracker
539 .calculate_velocity_with_max(MAX_FLING_VELOCITY);
540 }
541 }
542
543 let start_fling = allow_fling && was_dragging && velocity.abs() > MIN_FLING_VELOCITY;
544 let existing_fling = if start_fling {
545 gs.fling_animation.take()
546 } else {
547 None
548 };
549
550 gs.drag_down_position = None;
551 gs.last_position = None;
552 gs.is_dragging = false;
553 gs.axis_locked_out = false;
554 gs.gesture_start_time = None;
555 gs.gesture_start_event_time_ms = None;
556 gs.last_velocity_sample_ms = None;
557
558 (was_dragging, velocity, start_fling, existing_fling)
559 };
560
561 if allow_fling && was_dragging {
563 log::debug!(
564 target: "cranpose::velocity",
565 "gesture finished: fling velocity={velocity:.2} dp/s start_fling={start_fling}"
566 );
567 set_last_fling_velocity(velocity);
568 }
569
570 let adjusted_velocity = if self.reverse_scrolling {
572 -velocity
573 } else {
574 velocity
575 };
576 let fling_velocity = -adjusted_velocity;
577
578 let settle_target = if was_dragging {
583 self.scroll_target.settle_policy().and_then(|policy| {
584 let current = self.scroll_target.current_offset();
585 let proposed = if start_fling {
586 fling_rest_position(current, fling_velocity, current_density())
587 } else {
588 current
589 };
590 let target = policy(proposed, fling_velocity);
591 ((target - proposed).abs() > 0.5).then_some(target)
592 })
593 } else {
594 None
595 };
596
597 if let Some(target) = settle_target {
598 if let Some(old_fling) = existing_fling {
599 old_fling.cancel();
600 }
601 self.start_settle_animation(target, fling_velocity);
602 } else if start_fling {
603 if let Some(old_fling) = existing_fling {
604 old_fling.cancel();
605 }
606
607 if let Some(runtime) = current_runtime_handle() {
609 self.motion_context.set_active(true);
610 let scroll_target = self.scroll_target.clone();
611 let fling = FlingAnimation::new(runtime);
612 let motion_context = self.motion_context.clone();
613
614 let initial_value = scroll_target.current_offset();
616
617 let scroll_target_for_fling = scroll_target.clone();
618 let scroll_target_for_end = scroll_target.clone();
619
620 fling.start_fling(
621 initial_value,
622 fling_velocity,
623 current_density(),
624 move |delta| {
625 let consumed = scroll_target_for_fling.apply_fling_delta(delta);
627 scroll_target_for_fling.invalidate();
628 consumed
629 },
630 move || {
631 scroll_target_for_end.invalidate();
633 motion_context.set_active(false);
634 },
635 );
636
637 let mut gs = self.gesture_state.borrow_mut();
638 gs.fling_animation = Some(fling);
639 }
640 } else {
641 self.motion_context.set_active(false);
642 }
643
644 was_dragging
645 }
646
647 fn start_settle_animation(&self, target: f32, initial_velocity: f32) {
650 let Some(runtime) = current_runtime_handle() else {
651 self.motion_context.set_active(false);
652 return;
653 };
654 self.motion_context.set_active(true);
655 let settle = SettleAnimation::new(runtime);
656 let scroll_target_for_settle = self.scroll_target.clone();
657 let scroll_target_for_end = self.scroll_target.clone();
658 let motion_context = self.motion_context.clone();
659 settle.start_settle(
660 self.scroll_target.current_offset(),
661 initial_velocity,
662 target,
663 move |delta| {
664 let consumed = scroll_target_for_settle.apply_fling_delta(delta);
665 scroll_target_for_settle.invalidate();
666 consumed
667 },
668 move || {
669 scroll_target_for_end.invalidate();
670 motion_context.set_active(false);
671 },
672 );
673 let mut gs = self.gesture_state.borrow_mut();
674 gs.settle_animation = Some(settle);
675 }
676
677 fn on_up(&self, time_ms: Option<i64>) -> bool {
684 self.finish_gesture(true, time_ms)
685 }
686
687 fn on_cancel(&self) -> bool {
691 self.finish_gesture(false, None)
692 }
693
694 fn on_scroll(&self, axis_delta: f32) -> bool {
698 if axis_delta.abs() <= f32::EPSILON {
699 return false;
700 }
701
702 {
703 let mut gs = self.gesture_state.borrow_mut();
705 if let Some(fling) = gs.fling_animation.take() {
706 fling.cancel();
707 }
708 if let Some(settle) = gs.settle_animation.take() {
709 settle.cancel();
710 }
711 gs.drag_down_position = None;
712 gs.last_position = None;
713 gs.is_dragging = false;
714 gs.axis_locked_out = false;
715 gs.gesture_start_time = None;
716 gs.gesture_start_event_time_ms = None;
717 gs.last_velocity_sample_ms = None;
718 gs.velocity_tracker.reset();
719 }
720
721 self.motion_context.activate_for_current_frame();
722
723 let delta = if self.reverse_scrolling {
724 -axis_delta
725 } else {
726 axis_delta
727 };
728 let consumed = self.scroll_target.apply_wheel_delta(delta);
729 if consumed.abs() > 0.001 {
730 self.scroll_target.invalidate();
731 self.ensure_wheel_settle_watcher();
732 true
733 } else {
734 false
735 }
736 }
737
738 fn ensure_wheel_settle_watcher(&self) {
743 if self.scroll_target.settle_policy().is_none() {
744 return;
745 }
746 {
747 let gs = self.gesture_state.borrow();
748 if gs
749 .wheel_settle_watcher
750 .as_ref()
751 .is_some_and(|watcher| watcher.is_running.get())
752 {
753 return;
754 }
755 }
756 let Some(runtime) = current_runtime_handle() else {
757 return;
758 };
759
760 let is_running = Rc::new(Cell::new(true));
761 let registration = Rc::new(RefCell::new(None));
762
763 struct WheelSettleLoop<S: ScrollTarget> {
764 detector: ScrollGestureDetector<S>,
765 gesture_state: Rc<RefCell<ScrollGestureState>>,
766 frame_clock: cranpose_core::internal::FrameClock,
767 is_running: Rc<Cell<bool>>,
768 registration: Rc<RefCell<Option<FrameCallbackRegistration>>>,
769 last_offset: Rc<Cell<f32>>,
770 idle_nanos: Rc<Cell<u64>>,
771 last_frame: Rc<Cell<Option<u64>>>,
772 }
773
774 impl<S: ScrollTarget + 'static> WheelSettleLoop<S> {
775 fn next(&self) -> Self {
776 Self {
777 detector: self.detector.clone_for_watcher(),
778 gesture_state: Rc::clone(&self.gesture_state),
779 frame_clock: self.frame_clock.clone(),
780 is_running: Rc::clone(&self.is_running),
781 registration: Rc::clone(&self.registration),
782 last_offset: Rc::clone(&self.last_offset),
783 idle_nanos: Rc::clone(&self.idle_nanos),
784 last_frame: Rc::clone(&self.last_frame),
785 }
786 }
787
788 fn schedule(self) {
789 let continuation = self.next();
790 let registration_slot = Rc::clone(&self.registration);
791 let new_registration = self.frame_clock.with_frame_nanos(move |frame_time_nanos| {
792 let this = &continuation;
793 if !this.is_running.get() {
794 return;
795 }
796 {
798 let gs = this.gesture_state.borrow();
799 let animating = gs.is_dragging
800 || gs
801 .fling_animation
802 .as_ref()
803 .is_some_and(FlingAnimation::is_running)
804 || gs
805 .settle_animation
806 .as_ref()
807 .is_some_and(SettleAnimation::is_running);
808 if animating {
809 this.is_running.set(false);
810 return;
811 }
812 }
813
814 let offset = this.detector.scroll_target.current_offset();
815 let dt = this
816 .last_frame
817 .get()
818 .map_or(0, |last| frame_time_nanos.saturating_sub(last));
819 this.last_frame.set(Some(frame_time_nanos));
820 if (offset - this.last_offset.get()).abs() > 0.01 {
821 this.last_offset.set(offset);
822 this.idle_nanos.set(0);
823 } else {
824 this.idle_nanos.set(this.idle_nanos.get() + dt);
825 }
826
827 if this.idle_nanos.get() >= WHEEL_SETTLE_IDLE_NANOS {
828 this.is_running.set(false);
829 if let Some(policy) = this.detector.scroll_target.settle_policy() {
830 let target = policy(offset, 0.0);
831 if (target - offset).abs() > 0.5 {
832 this.detector.start_settle_animation(target, 0.0);
833 }
834 }
835 return;
836 }
837
838 continuation.next().schedule();
839 });
840 *registration_slot.borrow_mut() = Some(new_registration);
841 }
842 }
843
844 WheelSettleLoop {
845 detector: self.clone_for_watcher(),
846 gesture_state: Rc::clone(&self.gesture_state),
847 frame_clock: runtime.frame_clock(),
848 is_running: Rc::clone(&is_running),
849 registration: Rc::clone(®istration),
850 last_offset: Rc::new(Cell::new(self.scroll_target.current_offset())),
851 idle_nanos: Rc::new(Cell::new(0u64)),
852 last_frame: Rc::new(Cell::new(None::<u64>)),
853 }
854 .schedule();
855
856 self.gesture_state.borrow_mut().wheel_settle_watcher = Some(WheelSettleWatcher {
857 is_running,
858 registration,
859 });
860 }
861
862 fn clone_for_watcher(&self) -> ScrollGestureDetector<S> {
863 ScrollGestureDetector {
864 gesture_state: Rc::clone(&self.gesture_state),
865 scroll_target: self.scroll_target.clone(),
866 is_vertical: self.is_vertical,
867 reverse_scrolling: self.reverse_scrolling,
868 motion_context: self.motion_context.clone(),
869 }
870 }
871}
872
873pub(crate) struct MotionContextAnimatedNode {
874 state: NodeState,
875 motion_context: ScrollMotionContext,
876 invalidation_callback_id: Option<u64>,
877 node_id: Option<NodeId>,
878}
879
880impl MotionContextAnimatedNode {
881 fn new(motion_context: ScrollMotionContext) -> Self {
882 Self {
883 state: NodeState::new(),
884 motion_context,
885 invalidation_callback_id: None,
886 node_id: None,
887 }
888 }
889
890 pub(crate) fn is_active(&self) -> bool {
891 self.motion_context.is_active()
892 }
893}
894
895pub(crate) struct TranslatedContentContextNode {
896 state: NodeState,
897 identity: usize,
898 offset_source: TranslatedContentOffsetSource,
899}
900
901impl TranslatedContentContextNode {
902 fn new(identity: usize, offset_source: TranslatedContentOffsetSource) -> Self {
903 Self {
904 state: NodeState::new(),
905 identity,
906 offset_source,
907 }
908 }
909
910 pub(crate) fn is_active(&self) -> bool {
911 true
912 }
913
914 pub(crate) fn identity(&self) -> usize {
915 self.identity
916 }
917
918 pub(crate) fn content_offset_reader(&self) -> Option<Rc<dyn Fn() -> Point>> {
919 self.offset_source.content_offset_reader()
920 }
921}
922
923impl DelegatableNode for TranslatedContentContextNode {
924 fn node_state(&self) -> &NodeState {
925 &self.state
926 }
927}
928
929impl ModifierNode for TranslatedContentContextNode {}
930
931impl DelegatableNode for MotionContextAnimatedNode {
932 fn node_state(&self) -> &NodeState {
933 &self.state
934 }
935}
936
937impl ModifierNode for MotionContextAnimatedNode {
938 fn on_attach(&mut self, context: &mut dyn cranpose_foundation::ModifierNodeContext) {
939 let node_id = context.node_id();
940 self.node_id = node_id;
941 if let Some(node_id) = node_id {
942 let callback_id = self
943 .motion_context
944 .add_invalidate_callback(Box::new(move || {
945 schedule_modifier_slices_repass(node_id);
946 }));
947 self.invalidation_callback_id = Some(callback_id);
948 }
949 }
950
951 fn on_detach(&mut self) {
952 if let Some(id) = self.invalidation_callback_id.take() {
953 self.motion_context.remove_invalidate_callback(id);
954 }
955 self.node_id = None;
956 }
957}
958
959#[derive(Clone)]
960struct MotionContextAnimatedElement {
961 motion_context: ScrollMotionContext,
962}
963
964impl MotionContextAnimatedElement {
965 fn new(motion_context: ScrollMotionContext) -> Self {
966 Self { motion_context }
967 }
968}
969
970impl std::fmt::Debug for MotionContextAnimatedElement {
971 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
972 f.debug_struct("MotionContextAnimatedElement").finish()
973 }
974}
975
976impl PartialEq for MotionContextAnimatedElement {
977 fn eq(&self, other: &Self) -> bool {
978 self.motion_context.ptr_eq(&other.motion_context)
979 }
980}
981
982impl Eq for MotionContextAnimatedElement {}
983
984impl std::hash::Hash for MotionContextAnimatedElement {
985 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
986 self.motion_context.stable_key().hash(state);
987 }
988}
989
990impl ModifierNodeElement for MotionContextAnimatedElement {
991 type Node = MotionContextAnimatedNode;
992
993 fn create(&self) -> Self::Node {
994 MotionContextAnimatedNode::new(self.motion_context.clone())
995 }
996
997 fn update(&self, node: &mut Self::Node) {
998 if node.motion_context.ptr_eq(&self.motion_context) {
999 return;
1000 }
1001 if let Some(id) = node.invalidation_callback_id.take() {
1002 node.motion_context.remove_invalidate_callback(id);
1003 }
1004 node.motion_context = self.motion_context.clone();
1005 if let Some(node_id) = node.node_id {
1006 let callback_id = node
1007 .motion_context
1008 .add_invalidate_callback(Box::new(move || {
1009 schedule_modifier_slices_repass(node_id);
1010 }));
1011 node.invalidation_callback_id = Some(callback_id);
1012 }
1013 }
1014
1015 fn capabilities(&self) -> NodeCapabilities {
1016 NodeCapabilities::LAYOUT
1017 }
1018}
1019
1020#[derive(Clone)]
1021enum TranslatedContentOffsetSource {
1022 LayoutContentOffset,
1023 LazyList {
1024 state: LazyListState,
1025 is_vertical: bool,
1026 reverse_scrolling: bool,
1027 },
1028}
1029
1030impl TranslatedContentOffsetSource {
1031 fn content_offset_reader(&self) -> Option<Rc<dyn Fn() -> Point>> {
1032 match self {
1033 Self::LayoutContentOffset => None,
1034 Self::LazyList {
1035 state, is_vertical, ..
1036 } => Some(Rc::new(lazy_list_content_offset_reader(
1037 *state,
1038 *is_vertical,
1039 ))),
1040 }
1041 }
1042
1043 fn is_vertical(&self) -> Option<bool> {
1044 match self {
1045 Self::LayoutContentOffset => None,
1046 Self::LazyList { is_vertical, .. } => Some(*is_vertical),
1047 }
1048 }
1049
1050 fn reverse_scrolling(&self) -> Option<bool> {
1051 match self {
1052 Self::LayoutContentOffset => None,
1053 Self::LazyList {
1054 reverse_scrolling, ..
1055 } => Some(*reverse_scrolling),
1056 }
1057 }
1058}
1059
1060fn lazy_list_content_offset_reader(state: LazyListState, is_vertical: bool) -> impl Fn() -> Point {
1061 move || {
1062 let info = state.layout_info();
1063 if info.visible_items_info.is_empty() {
1064 return Point::default();
1065 };
1066 let main_offset = info.snap_anchor_offset;
1067 if is_vertical {
1068 Point::new(0.0, main_offset)
1069 } else {
1070 Point::new(main_offset, 0.0)
1071 }
1072 }
1073}
1074
1075#[derive(Clone)]
1076struct TranslatedContentContextElement {
1077 identity: usize,
1078 offset_source: TranslatedContentOffsetSource,
1079}
1080
1081impl TranslatedContentContextElement {
1082 fn new(identity: usize, offset_source: TranslatedContentOffsetSource) -> Self {
1083 Self {
1084 identity,
1085 offset_source,
1086 }
1087 }
1088}
1089
1090impl std::fmt::Debug for TranslatedContentContextElement {
1091 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1092 let offset_source = match &self.offset_source {
1093 TranslatedContentOffsetSource::LayoutContentOffset => "layout",
1094 TranslatedContentOffsetSource::LazyList { .. } => "lazy_list",
1095 };
1096 f.debug_struct("TranslatedContentContextElement")
1097 .field("identity", &self.identity)
1098 .field("offset_source", &offset_source)
1099 .finish()
1100 }
1101}
1102
1103impl PartialEq for TranslatedContentContextElement {
1104 fn eq(&self, other: &Self) -> bool {
1105 self.identity == other.identity
1106 && self.offset_source.is_vertical() == other.offset_source.is_vertical()
1107 && self.offset_source.reverse_scrolling() == other.offset_source.reverse_scrolling()
1108 }
1109}
1110
1111impl Eq for TranslatedContentContextElement {}
1112
1113impl std::hash::Hash for TranslatedContentContextElement {
1114 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1115 self.identity.hash(state);
1116 self.offset_source.is_vertical().hash(state);
1117 self.offset_source.reverse_scrolling().hash(state);
1118 }
1119}
1120
1121impl ModifierNodeElement for TranslatedContentContextElement {
1122 type Node = TranslatedContentContextNode;
1123
1124 fn create(&self) -> Self::Node {
1125 TranslatedContentContextNode::new(self.identity, self.offset_source.clone())
1126 }
1127
1128 fn update(&self, node: &mut Self::Node) {
1129 node.identity = self.identity;
1130 node.offset_source = self.offset_source.clone();
1131 }
1132
1133 fn capabilities(&self) -> NodeCapabilities {
1134 NodeCapabilities::LAYOUT
1135 }
1136}
1137
1138impl Modifier {
1143 pub fn horizontal_scroll(self, state: ScrollState, reverse_scrolling: bool) -> Self {
1161 self.then(scroll_impl(state, false, reverse_scrolling, None))
1162 }
1163
1164 pub fn vertical_scroll(self, state: ScrollState, reverse_scrolling: bool) -> Self {
1173 self.then(scroll_impl(state, true, reverse_scrolling, None))
1174 }
1175
1176 pub fn horizontal_scroll_guarded(
1178 self,
1179 state: ScrollState,
1180 reverse_scrolling: bool,
1181 guard: impl Fn() -> bool + 'static,
1182 ) -> Self {
1183 self.then(scroll_impl(
1184 state,
1185 false,
1186 reverse_scrolling,
1187 Some(Rc::new(guard)),
1188 ))
1189 }
1190
1191 pub fn vertical_scroll_guarded(
1193 self,
1194 state: ScrollState,
1195 reverse_scrolling: bool,
1196 guard: impl Fn() -> bool + 'static,
1197 ) -> Self {
1198 self.then(scroll_impl(
1199 state,
1200 true,
1201 reverse_scrolling,
1202 Some(Rc::new(guard)),
1203 ))
1204 }
1205}
1206
1207fn scroll_impl(
1216 state: ScrollState,
1217 is_vertical: bool,
1218 reverse_scrolling: bool,
1219 guard: Option<Rc<dyn Fn() -> bool>>,
1220) -> Modifier {
1221 let gesture_state = Rc::new(RefCell::new(ScrollGestureState::default()));
1223 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::ScrollState {
1224 state_id: state.id(),
1225 is_vertical,
1226 reverse_scrolling,
1227 });
1228
1229 let scroll_state = state.clone();
1231 let pointer_motion_context = motion_context.clone();
1232 let key = (state.id(), is_vertical);
1233 let pointer_input = Modifier::empty().pointer_input(key, move |scope| {
1234 let detector = ScrollGestureDetector::new(
1236 gesture_state.clone(),
1237 scroll_state.clone(),
1238 is_vertical,
1239 false, pointer_motion_context.clone(),
1241 );
1242 let guard = guard.clone();
1243
1244 async move {
1245 scope
1246 .await_pointer_event_scope(|await_scope| async move {
1247 loop {
1249 let event = await_scope.await_pointer_event().await;
1250
1251 if event.id != 0 {
1255 continue;
1256 }
1257
1258 if event.is_consumed() {
1259 if matches!(
1260 event.kind,
1261 PointerEventKind::Down
1262 | PointerEventKind::Move
1263 | PointerEventKind::Up
1264 | PointerEventKind::Cancel
1265 ) {
1266 detector.on_cancel();
1267 }
1268 continue;
1269 }
1270
1271 if let Some(ref guard) = guard {
1272 if !guard() {
1273 if matches!(
1274 event.kind,
1275 PointerEventKind::Up | PointerEventKind::Cancel
1276 ) {
1277 detector.on_cancel();
1278 }
1279 continue;
1280 }
1281 }
1282
1283 let should_consume = match event.kind {
1285 PointerEventKind::Down => {
1286 detector.on_down(event.position, event.time_ms)
1287 }
1288 PointerEventKind::Move => {
1289 detector.on_move(event.position, event.buttons, event.time_ms)
1290 }
1291 PointerEventKind::Up => detector.on_up(event.time_ms),
1292 PointerEventKind::Cancel => detector.on_cancel(),
1293 PointerEventKind::Scroll => detector.on_scroll(if is_vertical {
1294 event.scroll_delta.y
1295 } else {
1296 event.scroll_delta.x
1297 }),
1298 PointerEventKind::Zoom
1299 | PointerEventKind::Enter
1300 | PointerEventKind::Exit => false,
1301 };
1302
1303 if should_consume {
1304 event.consume();
1305 }
1306 }
1307 })
1308 .await;
1309 }
1310 });
1311
1312 let element = ScrollElement::new(state.clone(), is_vertical, reverse_scrolling);
1314 let layout_modifier =
1315 Modifier::with_element(element).with_inspector_metadata(inspector_metadata(
1316 if is_vertical {
1317 "verticalScroll"
1318 } else {
1319 "horizontalScroll"
1320 },
1321 move |info| {
1322 info.add_property("isVertical", is_vertical.to_string());
1323 info.add_property("reverseScrolling", reverse_scrolling.to_string());
1324 },
1325 ));
1326 let motion_modifier =
1327 Modifier::with_element(MotionContextAnimatedElement::new(motion_context.clone()));
1328 let translated_content_modifier = Modifier::with_element(TranslatedContentContextElement::new(
1329 state.id() as usize,
1330 TranslatedContentOffsetSource::LayoutContentOffset,
1331 ));
1332
1333 pointer_input
1335 .then(motion_modifier)
1336 .then(translated_content_modifier)
1337 .then(layout_modifier)
1338 .clip_to_bounds()
1339}
1340
1341use cranpose_foundation::lazy::LazyListState;
1346
1347impl Modifier {
1348 pub fn lazy_vertical_scroll(self, state: LazyListState, reverse_scrolling: bool) -> Self {
1359 self.then(lazy_scroll_impl(state, true, reverse_scrolling))
1360 }
1361
1362 pub fn lazy_horizontal_scroll(self, state: LazyListState, reverse_scrolling: bool) -> Self {
1364 self.then(lazy_scroll_impl(state, false, reverse_scrolling))
1365 }
1366}
1367
1368fn lazy_scroll_impl(state: LazyListState, is_vertical: bool, reverse_scrolling: bool) -> Modifier {
1370 let gesture_state = Rc::new(RefCell::new(ScrollGestureState::default()));
1371 let list_state = state;
1372 let state_id = state.inner_ptr() as usize;
1373 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::LazyList {
1374 state_identity: state_id,
1375 is_vertical,
1376 reverse_scrolling,
1377 });
1378 let key = (state_id, is_vertical, reverse_scrolling);
1379 let translated_content_modifier = Modifier::with_element(TranslatedContentContextElement::new(
1380 state_id,
1381 TranslatedContentOffsetSource::LazyList {
1382 state,
1383 is_vertical,
1384 reverse_scrolling,
1385 },
1386 ));
1387
1388 Modifier::with_element(MotionContextAnimatedElement::new(motion_context.clone()))
1389 .then(translated_content_modifier)
1390 .pointer_input(key, move |scope| {
1391 let detector = ScrollGestureDetector::new(
1393 gesture_state.clone(),
1394 list_state,
1395 is_vertical,
1396 reverse_scrolling,
1397 motion_context.clone(),
1398 );
1399
1400 async move {
1401 scope
1402 .await_pointer_event_scope(|await_scope| async move {
1403 loop {
1404 let event = await_scope.await_pointer_event().await;
1405
1406 if event.id != 0 {
1410 continue;
1411 }
1412
1413 if event.is_consumed() {
1414 if matches!(
1415 event.kind,
1416 PointerEventKind::Down
1417 | PointerEventKind::Move
1418 | PointerEventKind::Up
1419 | PointerEventKind::Cancel
1420 ) {
1421 detector.on_cancel();
1422 }
1423 continue;
1424 }
1425
1426 let should_consume = match event.kind {
1428 PointerEventKind::Down => {
1429 detector.on_down(event.position, event.time_ms)
1430 }
1431 PointerEventKind::Move => {
1432 detector.on_move(event.position, event.buttons, event.time_ms)
1433 }
1434 PointerEventKind::Up => detector.on_up(event.time_ms),
1435 PointerEventKind::Cancel => detector.on_cancel(),
1436 PointerEventKind::Scroll => detector.on_scroll(if is_vertical {
1437 event.scroll_delta.y
1438 } else {
1439 event.scroll_delta.x
1440 }),
1441 PointerEventKind::Zoom
1442 | PointerEventKind::Enter
1443 | PointerEventKind::Exit => false,
1444 };
1445
1446 if should_consume {
1447 event.consume();
1448 }
1449 }
1450 })
1451 .await;
1452 }
1453 })
1454}