1use super::{inspector_metadata, Modifier, Point, PointerEventKind};
18use crate::current_density;
19use crate::fling_animation::FlingAnimation;
20use crate::fling_animation::MIN_FLING_VELOCITY;
21use crate::render_state::schedule_modifier_slices_repass;
22use crate::scroll::{
23 scroll_motion_context_for_key, ScrollElement, ScrollMotionContext, ScrollMotionContextKey,
24 ScrollState,
25};
26use cranpose_core::{current_runtime_handle, NodeId};
27use cranpose_foundation::{
28 velocity_tracker::ASSUME_STOPPED_MS, DelegatableNode, ModifierNode, ModifierNodeElement,
29 NodeCapabilities, NodeState, PointerButton, PointerButtons, VelocityTracker1D, DRAG_THRESHOLD,
30 MAX_FLING_VELOCITY,
31};
32use std::cell::RefCell;
33use std::rc::Rc;
34use web_time::Instant;
35
36#[cfg(feature = "test-helpers")]
37pub fn last_fling_velocity() -> f32 {
38 crate::render_state::debug_last_fling_velocity()
39}
40
41#[cfg(feature = "test-helpers")]
42pub fn reset_last_fling_velocity() {
43 crate::render_state::debug_reset_last_fling_velocity();
44}
45
46#[inline]
47fn set_last_fling_velocity(velocity: f32) {
48 crate::render_state::record_last_fling_velocity(velocity);
49}
50
51struct ScrollGestureState {
57 drag_down_position: Option<Point>,
60
61 last_position: Option<Point>,
64
65 is_dragging: bool,
69
70 velocity_tracker: VelocityTracker1D,
72
73 gesture_start_time: Option<Instant>,
75
76 gesture_start_event_time_ms: Option<i64>,
80
81 last_velocity_sample_ms: Option<i64>,
83
84 fling_animation: Option<FlingAnimation>,
86}
87
88impl Default for ScrollGestureState {
89 fn default() -> Self {
90 Self {
91 drag_down_position: None,
92 last_position: None,
93 is_dragging: false,
94 velocity_tracker: VelocityTracker1D::new(),
95 gesture_start_time: None,
96 gesture_start_event_time_ms: None,
97 last_velocity_sample_ms: None,
98 fling_animation: None,
99 }
100 }
101}
102
103#[inline]
112fn calculate_total_delta(from: Point, to: Point, is_vertical: bool) -> f32 {
113 if is_vertical {
114 to.y - from.y
115 } else {
116 to.x - from.x
117 }
118}
119
120#[inline]
125fn calculate_incremental_delta(from: Point, to: Point, is_vertical: bool) -> f32 {
126 if is_vertical {
127 to.y - from.y
128 } else {
129 to.x - from.x
130 }
131}
132
133trait ScrollTarget: Clone {
141 fn apply_delta(&self, delta: f32) -> f32;
143
144 fn apply_wheel_delta(&self, delta: f32) -> f32 {
146 self.apply_delta(delta)
147 }
148
149 fn apply_fling_delta(&self, delta: f32) -> f32;
151
152 fn invalidate(&self);
154
155 fn current_offset(&self) -> f32;
157
158 fn can_scroll(&self) -> bool {
166 true
167 }
168}
169
170impl ScrollTarget for ScrollState {
171 fn apply_delta(&self, delta: f32) -> f32 {
172 self.dispatch_raw_delta(-delta)
174 }
175
176 fn apply_fling_delta(&self, delta: f32) -> f32 {
177 self.dispatch_raw_delta(delta)
178 }
179
180 fn invalidate(&self) {
181 }
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
193impl ScrollTarget for LazyListState {
194 fn apply_delta(&self, delta: f32) -> f32 {
195 self.dispatch_scroll_delta(delta)
199 }
200
201 fn apply_wheel_delta(&self, delta: f32) -> f32 {
202 if delta.abs() <= 0.001 {
203 0.0
204 } else {
205 self.dispatch_scroll_delta(delta)
206 }
207 }
208
209 fn apply_fling_delta(&self, delta: f32) -> f32 {
210 -self.dispatch_scroll_delta(-delta)
211 }
212
213 fn invalidate(&self) {
214 }
217
218 fn current_offset(&self) -> f32 {
219 self.first_visible_item_scroll_offset()
221 }
222
223 fn can_scroll(&self) -> bool {
224 self.layout_info().total_items_count == 0
227 || self.can_scroll_forward_non_reactive()
228 || self.can_scroll_backward_non_reactive()
229 }
230}
231
232struct ScrollGestureDetector<S: ScrollTarget> {
238 gesture_state: Rc<RefCell<ScrollGestureState>>,
240
241 scroll_target: S,
243
244 is_vertical: bool,
246
247 reverse_scrolling: bool,
249
250 motion_context: ScrollMotionContext,
252}
253
254impl<S: ScrollTarget + 'static> ScrollGestureDetector<S> {
255 fn new(
257 gesture_state: Rc<RefCell<ScrollGestureState>>,
258 scroll_target: S,
259 is_vertical: bool,
260 reverse_scrolling: bool,
261 motion_context: ScrollMotionContext,
262 ) -> Self {
263 Self {
264 gesture_state,
265 scroll_target,
266 is_vertical,
267 reverse_scrolling,
268 motion_context,
269 }
270 }
271
272 fn on_down(&self, position: Point, time_ms: Option<i64>) -> bool {
281 let mut gs = self.gesture_state.borrow_mut();
282
283 if let Some(fling) = gs.fling_animation.take() {
285 fling.cancel();
286 }
287 self.motion_context.set_active(false);
288
289 gs.drag_down_position = Some(position);
290 gs.last_position = Some(position);
291 gs.is_dragging = false;
292 gs.velocity_tracker.reset();
293 gs.gesture_start_time = Some(Instant::now());
294 gs.gesture_start_event_time_ms = time_ms;
295
296 let pos = if self.is_vertical {
298 position.y
299 } else {
300 position.x
301 };
302 gs.velocity_tracker.add_data_point(0, pos);
303 gs.last_velocity_sample_ms = Some(0);
304
305 false
307 }
308
309 fn on_move(&self, position: Point, buttons: PointerButtons, time_ms: Option<i64>) -> bool {
320 let mut gs = self.gesture_state.borrow_mut();
321
322 if !buttons.contains(PointerButton::Primary) && gs.drag_down_position.is_some() {
324 gs.drag_down_position = None;
325 gs.last_position = None;
326 gs.is_dragging = false;
327 gs.gesture_start_time = None;
328 gs.gesture_start_event_time_ms = None;
329 gs.last_velocity_sample_ms = None;
330 gs.velocity_tracker.reset();
331 self.motion_context.set_active(false);
332 return false;
333 }
334
335 let Some(down_pos) = gs.drag_down_position else {
336 return false;
337 };
338
339 let Some(last_pos) = gs.last_position else {
340 gs.last_position = Some(position);
341 return false;
342 };
343
344 let total_delta = calculate_total_delta(down_pos, position, self.is_vertical);
345 let incremental_delta = calculate_incremental_delta(last_pos, position, self.is_vertical);
346
347 if !gs.is_dragging && total_delta.abs() > DRAG_THRESHOLD && self.scroll_target.can_scroll()
351 {
352 gs.is_dragging = true;
353 self.motion_context.set_active(true);
354 }
355
356 gs.last_position = Some(position);
357
358 let pos = if self.is_vertical {
360 position.y
361 } else {
362 position.x
363 };
364 let event_sample_ms = gs
365 .gesture_start_event_time_ms
366 .zip(time_ms)
367 .map(|(start_ms, now_ms)| now_ms - start_ms);
368 let sample_ms = if let Some(event_sample_ms) = event_sample_ms {
369 Some(match gs.last_velocity_sample_ms {
376 Some(last_sample_ms) => event_sample_ms.max(last_sample_ms),
377 None => event_sample_ms.max(0),
378 })
379 } else if let Some(start_time) = gs.gesture_start_time {
380 let elapsed_ms = start_time.elapsed().as_millis() as i64;
383 Some(match gs.last_velocity_sample_ms {
386 Some(last_sample_ms) => {
387 let mut sample_ms = if elapsed_ms <= last_sample_ms {
388 last_sample_ms + 1
389 } else {
390 elapsed_ms
391 };
392 if sample_ms - last_sample_ms > ASSUME_STOPPED_MS {
394 sample_ms = last_sample_ms + ASSUME_STOPPED_MS;
395 }
396 sample_ms
397 }
398 None => elapsed_ms,
399 })
400 } else {
401 None
402 };
403 if let Some(sample_ms) = sample_ms {
404 log::trace!(
405 target: "cranpose::velocity",
406 "sample t={sample_ms}ms pos={pos:.2} event_time={time_ms:?}"
407 );
408 gs.velocity_tracker.add_data_point(sample_ms, pos);
409 gs.last_velocity_sample_ms = Some(sample_ms);
410 }
411
412 if gs.is_dragging {
413 drop(gs); let delta = if self.reverse_scrolling {
415 -incremental_delta
416 } else {
417 incremental_delta
418 };
419 let _ = self.scroll_target.apply_delta(delta);
420 self.scroll_target.invalidate();
421 true } else {
423 false
424 }
425 }
426
427 fn finish_gesture(&self, allow_fling: bool) -> bool {
434 let (was_dragging, velocity, start_fling, existing_fling) = {
435 let mut gs = self.gesture_state.borrow_mut();
436 let was_dragging = gs.is_dragging;
437 let mut velocity = 0.0;
438
439 if allow_fling && was_dragging && gs.gesture_start_time.is_some() {
440 velocity = gs
441 .velocity_tracker
442 .calculate_velocity_with_max(MAX_FLING_VELOCITY);
443 }
444
445 let start_fling = allow_fling && was_dragging && velocity.abs() > MIN_FLING_VELOCITY;
446 let existing_fling = if start_fling {
447 gs.fling_animation.take()
448 } else {
449 None
450 };
451
452 gs.drag_down_position = None;
453 gs.last_position = None;
454 gs.is_dragging = false;
455 gs.gesture_start_time = None;
456 gs.gesture_start_event_time_ms = None;
457 gs.last_velocity_sample_ms = None;
458
459 (was_dragging, velocity, start_fling, existing_fling)
460 };
461
462 if allow_fling && was_dragging {
464 log::debug!(
465 target: "cranpose::velocity",
466 "gesture finished: fling velocity={velocity:.2} dp/s start_fling={start_fling}"
467 );
468 set_last_fling_velocity(velocity);
469 }
470
471 if start_fling {
473 if let Some(old_fling) = existing_fling {
474 old_fling.cancel();
475 }
476
477 if let Some(runtime) = current_runtime_handle() {
479 self.motion_context.set_active(true);
480 let scroll_target = self.scroll_target.clone();
481 let reverse = self.reverse_scrolling;
482 let fling = FlingAnimation::new(runtime);
483 let motion_context = self.motion_context.clone();
484
485 let initial_value = scroll_target.current_offset();
487
488 let adjusted_velocity = if reverse { -velocity } else { velocity };
490 let fling_velocity = -adjusted_velocity;
491
492 let scroll_target_for_fling = scroll_target.clone();
493 let scroll_target_for_end = scroll_target.clone();
494
495 fling.start_fling(
496 initial_value,
497 fling_velocity,
498 current_density(),
499 move |delta| {
500 let consumed = scroll_target_for_fling.apply_fling_delta(delta);
502 scroll_target_for_fling.invalidate();
503 consumed
504 },
505 move || {
506 scroll_target_for_end.invalidate();
508 motion_context.set_active(false);
509 },
510 );
511
512 let mut gs = self.gesture_state.borrow_mut();
513 gs.fling_animation = Some(fling);
514 }
515 } else {
516 self.motion_context.set_active(false);
517 }
518
519 was_dragging
520 }
521
522 fn on_up(&self) -> bool {
529 self.finish_gesture(true)
530 }
531
532 fn on_cancel(&self) -> bool {
536 self.finish_gesture(false)
537 }
538
539 fn on_scroll(&self, axis_delta: f32) -> bool {
543 if axis_delta.abs() <= f32::EPSILON {
544 return false;
545 }
546
547 {
548 let mut gs = self.gesture_state.borrow_mut();
550 if let Some(fling) = gs.fling_animation.take() {
551 fling.cancel();
552 }
553 gs.drag_down_position = None;
554 gs.last_position = None;
555 gs.is_dragging = false;
556 gs.gesture_start_time = None;
557 gs.gesture_start_event_time_ms = None;
558 gs.last_velocity_sample_ms = None;
559 gs.velocity_tracker.reset();
560 }
561
562 self.motion_context.activate_for_current_frame();
563
564 let delta = if self.reverse_scrolling {
565 -axis_delta
566 } else {
567 axis_delta
568 };
569 let consumed = self.scroll_target.apply_wheel_delta(delta);
570 if consumed.abs() > 0.001 {
571 self.scroll_target.invalidate();
572 true
573 } else {
574 false
575 }
576 }
577}
578
579pub(crate) struct MotionContextAnimatedNode {
580 state: NodeState,
581 motion_context: ScrollMotionContext,
582 invalidation_callback_id: Option<u64>,
583 node_id: Option<NodeId>,
584}
585
586impl MotionContextAnimatedNode {
587 fn new(motion_context: ScrollMotionContext) -> Self {
588 Self {
589 state: NodeState::new(),
590 motion_context,
591 invalidation_callback_id: None,
592 node_id: None,
593 }
594 }
595
596 pub(crate) fn is_active(&self) -> bool {
597 self.motion_context.is_active()
598 }
599}
600
601pub(crate) struct TranslatedContentContextNode {
602 state: NodeState,
603 identity: usize,
604 offset_source: TranslatedContentOffsetSource,
605}
606
607impl TranslatedContentContextNode {
608 fn new(identity: usize, offset_source: TranslatedContentOffsetSource) -> Self {
609 Self {
610 state: NodeState::new(),
611 identity,
612 offset_source,
613 }
614 }
615
616 pub(crate) fn is_active(&self) -> bool {
617 true
618 }
619
620 pub(crate) fn identity(&self) -> usize {
621 self.identity
622 }
623
624 pub(crate) fn content_offset_reader(&self) -> Option<Rc<dyn Fn() -> Point>> {
625 self.offset_source.content_offset_reader()
626 }
627}
628
629impl DelegatableNode for TranslatedContentContextNode {
630 fn node_state(&self) -> &NodeState {
631 &self.state
632 }
633}
634
635impl ModifierNode for TranslatedContentContextNode {}
636
637impl DelegatableNode for MotionContextAnimatedNode {
638 fn node_state(&self) -> &NodeState {
639 &self.state
640 }
641}
642
643impl ModifierNode for MotionContextAnimatedNode {
644 fn on_attach(&mut self, context: &mut dyn cranpose_foundation::ModifierNodeContext) {
645 let node_id = context.node_id();
646 self.node_id = node_id;
647 if let Some(node_id) = node_id {
648 let callback_id = self
649 .motion_context
650 .add_invalidate_callback(Box::new(move || {
651 schedule_modifier_slices_repass(node_id);
652 }));
653 self.invalidation_callback_id = Some(callback_id);
654 }
655 }
656
657 fn on_detach(&mut self) {
658 if let Some(id) = self.invalidation_callback_id.take() {
659 self.motion_context.remove_invalidate_callback(id);
660 }
661 self.node_id = None;
662 }
663}
664
665#[derive(Clone)]
666struct MotionContextAnimatedElement {
667 motion_context: ScrollMotionContext,
668}
669
670impl MotionContextAnimatedElement {
671 fn new(motion_context: ScrollMotionContext) -> Self {
672 Self { motion_context }
673 }
674}
675
676impl std::fmt::Debug for MotionContextAnimatedElement {
677 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
678 f.debug_struct("MotionContextAnimatedElement").finish()
679 }
680}
681
682impl PartialEq for MotionContextAnimatedElement {
683 fn eq(&self, other: &Self) -> bool {
684 self.motion_context.ptr_eq(&other.motion_context)
685 }
686}
687
688impl Eq for MotionContextAnimatedElement {}
689
690impl std::hash::Hash for MotionContextAnimatedElement {
691 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
692 self.motion_context.stable_key().hash(state);
693 }
694}
695
696impl ModifierNodeElement for MotionContextAnimatedElement {
697 type Node = MotionContextAnimatedNode;
698
699 fn create(&self) -> Self::Node {
700 MotionContextAnimatedNode::new(self.motion_context.clone())
701 }
702
703 fn update(&self, node: &mut Self::Node) {
704 if node.motion_context.ptr_eq(&self.motion_context) {
705 return;
706 }
707 if let Some(id) = node.invalidation_callback_id.take() {
708 node.motion_context.remove_invalidate_callback(id);
709 }
710 node.motion_context = self.motion_context.clone();
711 if let Some(node_id) = node.node_id {
712 let callback_id = node
713 .motion_context
714 .add_invalidate_callback(Box::new(move || {
715 schedule_modifier_slices_repass(node_id);
716 }));
717 node.invalidation_callback_id = Some(callback_id);
718 }
719 }
720
721 fn capabilities(&self) -> NodeCapabilities {
722 NodeCapabilities::LAYOUT
723 }
724}
725
726#[derive(Clone)]
727enum TranslatedContentOffsetSource {
728 LayoutContentOffset,
729 LazyList {
730 state: LazyListState,
731 is_vertical: bool,
732 reverse_scrolling: bool,
733 },
734}
735
736impl TranslatedContentOffsetSource {
737 fn content_offset_reader(&self) -> Option<Rc<dyn Fn() -> Point>> {
738 match self {
739 Self::LayoutContentOffset => None,
740 Self::LazyList {
741 state, is_vertical, ..
742 } => Some(Rc::new(lazy_list_content_offset_reader(
743 *state,
744 *is_vertical,
745 ))),
746 }
747 }
748
749 fn is_vertical(&self) -> Option<bool> {
750 match self {
751 Self::LayoutContentOffset => None,
752 Self::LazyList { is_vertical, .. } => Some(*is_vertical),
753 }
754 }
755
756 fn reverse_scrolling(&self) -> Option<bool> {
757 match self {
758 Self::LayoutContentOffset => None,
759 Self::LazyList {
760 reverse_scrolling, ..
761 } => Some(*reverse_scrolling),
762 }
763 }
764}
765
766fn lazy_list_content_offset_reader(state: LazyListState, is_vertical: bool) -> impl Fn() -> Point {
767 move || {
768 let info = state.layout_info();
769 if info.visible_items_info.is_empty() {
770 return Point::default();
771 };
772 let main_offset = info.snap_anchor_offset;
773 if is_vertical {
774 Point::new(0.0, main_offset)
775 } else {
776 Point::new(main_offset, 0.0)
777 }
778 }
779}
780
781#[derive(Clone)]
782struct TranslatedContentContextElement {
783 identity: usize,
784 offset_source: TranslatedContentOffsetSource,
785}
786
787impl TranslatedContentContextElement {
788 fn new(identity: usize, offset_source: TranslatedContentOffsetSource) -> Self {
789 Self {
790 identity,
791 offset_source,
792 }
793 }
794}
795
796impl std::fmt::Debug for TranslatedContentContextElement {
797 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
798 let offset_source = match &self.offset_source {
799 TranslatedContentOffsetSource::LayoutContentOffset => "layout",
800 TranslatedContentOffsetSource::LazyList { .. } => "lazy_list",
801 };
802 f.debug_struct("TranslatedContentContextElement")
803 .field("identity", &self.identity)
804 .field("offset_source", &offset_source)
805 .finish()
806 }
807}
808
809impl PartialEq for TranslatedContentContextElement {
810 fn eq(&self, other: &Self) -> bool {
811 self.identity == other.identity
812 && self.offset_source.is_vertical() == other.offset_source.is_vertical()
813 && self.offset_source.reverse_scrolling() == other.offset_source.reverse_scrolling()
814 }
815}
816
817impl Eq for TranslatedContentContextElement {}
818
819impl std::hash::Hash for TranslatedContentContextElement {
820 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
821 self.identity.hash(state);
822 self.offset_source.is_vertical().hash(state);
823 self.offset_source.reverse_scrolling().hash(state);
824 }
825}
826
827impl ModifierNodeElement for TranslatedContentContextElement {
828 type Node = TranslatedContentContextNode;
829
830 fn create(&self) -> Self::Node {
831 TranslatedContentContextNode::new(self.identity, self.offset_source.clone())
832 }
833
834 fn update(&self, node: &mut Self::Node) {
835 node.identity = self.identity;
836 node.offset_source = self.offset_source.clone();
837 }
838
839 fn capabilities(&self) -> NodeCapabilities {
840 NodeCapabilities::LAYOUT
841 }
842}
843
844impl Modifier {
849 pub fn horizontal_scroll(self, state: ScrollState, reverse_scrolling: bool) -> Self {
867 self.then(scroll_impl(state, false, reverse_scrolling, None))
868 }
869
870 pub fn vertical_scroll(self, state: ScrollState, reverse_scrolling: bool) -> Self {
879 self.then(scroll_impl(state, true, reverse_scrolling, None))
880 }
881
882 pub fn horizontal_scroll_guarded(
884 self,
885 state: ScrollState,
886 reverse_scrolling: bool,
887 guard: impl Fn() -> bool + 'static,
888 ) -> Self {
889 self.then(scroll_impl(
890 state,
891 false,
892 reverse_scrolling,
893 Some(Rc::new(guard)),
894 ))
895 }
896
897 pub fn vertical_scroll_guarded(
899 self,
900 state: ScrollState,
901 reverse_scrolling: bool,
902 guard: impl Fn() -> bool + 'static,
903 ) -> Self {
904 self.then(scroll_impl(
905 state,
906 true,
907 reverse_scrolling,
908 Some(Rc::new(guard)),
909 ))
910 }
911}
912
913fn scroll_impl(
922 state: ScrollState,
923 is_vertical: bool,
924 reverse_scrolling: bool,
925 guard: Option<Rc<dyn Fn() -> bool>>,
926) -> Modifier {
927 let gesture_state = Rc::new(RefCell::new(ScrollGestureState::default()));
929 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::ScrollState {
930 state_id: state.id(),
931 is_vertical,
932 reverse_scrolling,
933 });
934
935 let scroll_state = state.clone();
937 let pointer_motion_context = motion_context.clone();
938 let key = (state.id(), is_vertical);
939 let pointer_input = Modifier::empty().pointer_input(key, move |scope| {
940 let detector = ScrollGestureDetector::new(
942 gesture_state.clone(),
943 scroll_state.clone(),
944 is_vertical,
945 false, pointer_motion_context.clone(),
947 );
948 let guard = guard.clone();
949
950 async move {
951 scope
952 .await_pointer_event_scope(|await_scope| async move {
953 loop {
955 let event = await_scope.await_pointer_event().await;
956
957 if event.id != 0 {
961 continue;
962 }
963
964 if event.is_consumed() {
965 if matches!(
966 event.kind,
967 PointerEventKind::Down
968 | PointerEventKind::Move
969 | PointerEventKind::Up
970 | PointerEventKind::Cancel
971 ) {
972 detector.on_cancel();
973 }
974 continue;
975 }
976
977 if let Some(ref guard) = guard {
978 if !guard() {
979 if matches!(
980 event.kind,
981 PointerEventKind::Up | PointerEventKind::Cancel
982 ) {
983 detector.on_cancel();
984 }
985 continue;
986 }
987 }
988
989 let should_consume = match event.kind {
991 PointerEventKind::Down => {
992 detector.on_down(event.position, event.time_ms)
993 }
994 PointerEventKind::Move => {
995 detector.on_move(event.position, event.buttons, event.time_ms)
996 }
997 PointerEventKind::Up => detector.on_up(),
998 PointerEventKind::Cancel => detector.on_cancel(),
999 PointerEventKind::Scroll => detector.on_scroll(if is_vertical {
1000 event.scroll_delta.y
1001 } else {
1002 event.scroll_delta.x
1003 }),
1004 PointerEventKind::Zoom
1005 | PointerEventKind::Enter
1006 | PointerEventKind::Exit => false,
1007 };
1008
1009 if should_consume {
1010 event.consume();
1011 }
1012 }
1013 })
1014 .await;
1015 }
1016 });
1017
1018 let element = ScrollElement::new(state.clone(), is_vertical, reverse_scrolling);
1020 let layout_modifier =
1021 Modifier::with_element(element).with_inspector_metadata(inspector_metadata(
1022 if is_vertical {
1023 "verticalScroll"
1024 } else {
1025 "horizontalScroll"
1026 },
1027 move |info| {
1028 info.add_property("isVertical", is_vertical.to_string());
1029 info.add_property("reverseScrolling", reverse_scrolling.to_string());
1030 },
1031 ));
1032 let motion_modifier =
1033 Modifier::with_element(MotionContextAnimatedElement::new(motion_context.clone()));
1034 let translated_content_modifier = Modifier::with_element(TranslatedContentContextElement::new(
1035 state.id() as usize,
1036 TranslatedContentOffsetSource::LayoutContentOffset,
1037 ));
1038
1039 pointer_input
1041 .then(motion_modifier)
1042 .then(translated_content_modifier)
1043 .then(layout_modifier)
1044 .clip_to_bounds()
1045}
1046
1047use cranpose_foundation::lazy::LazyListState;
1052
1053impl Modifier {
1054 pub fn lazy_vertical_scroll(self, state: LazyListState, reverse_scrolling: bool) -> Self {
1065 self.then(lazy_scroll_impl(state, true, reverse_scrolling))
1066 }
1067
1068 pub fn lazy_horizontal_scroll(self, state: LazyListState, reverse_scrolling: bool) -> Self {
1070 self.then(lazy_scroll_impl(state, false, reverse_scrolling))
1071 }
1072}
1073
1074fn lazy_scroll_impl(state: LazyListState, is_vertical: bool, reverse_scrolling: bool) -> Modifier {
1076 let gesture_state = Rc::new(RefCell::new(ScrollGestureState::default()));
1077 let list_state = state;
1078 let state_id = state.inner_ptr() as usize;
1079 let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::LazyList {
1080 state_identity: state_id,
1081 is_vertical,
1082 reverse_scrolling,
1083 });
1084 let key = (state_id, is_vertical, reverse_scrolling);
1085 let translated_content_modifier = Modifier::with_element(TranslatedContentContextElement::new(
1086 state_id,
1087 TranslatedContentOffsetSource::LazyList {
1088 state,
1089 is_vertical,
1090 reverse_scrolling,
1091 },
1092 ));
1093
1094 Modifier::with_element(MotionContextAnimatedElement::new(motion_context.clone()))
1095 .then(translated_content_modifier)
1096 .pointer_input(key, move |scope| {
1097 let detector = ScrollGestureDetector::new(
1099 gesture_state.clone(),
1100 list_state,
1101 is_vertical,
1102 reverse_scrolling,
1103 motion_context.clone(),
1104 );
1105
1106 async move {
1107 scope
1108 .await_pointer_event_scope(|await_scope| async move {
1109 loop {
1110 let event = await_scope.await_pointer_event().await;
1111
1112 if event.id != 0 {
1116 continue;
1117 }
1118
1119 if event.is_consumed() {
1120 if matches!(
1121 event.kind,
1122 PointerEventKind::Down
1123 | PointerEventKind::Move
1124 | PointerEventKind::Up
1125 | PointerEventKind::Cancel
1126 ) {
1127 detector.on_cancel();
1128 }
1129 continue;
1130 }
1131
1132 let should_consume = match event.kind {
1134 PointerEventKind::Down => {
1135 detector.on_down(event.position, event.time_ms)
1136 }
1137 PointerEventKind::Move => {
1138 detector.on_move(event.position, event.buttons, event.time_ms)
1139 }
1140 PointerEventKind::Up => detector.on_up(),
1141 PointerEventKind::Cancel => detector.on_cancel(),
1142 PointerEventKind::Scroll => detector.on_scroll(if is_vertical {
1143 event.scroll_delta.y
1144 } else {
1145 event.scroll_delta.x
1146 }),
1147 PointerEventKind::Zoom
1148 | PointerEventKind::Enter
1149 | PointerEventKind::Exit => false,
1150 };
1151
1152 if should_consume {
1153 event.consume();
1154 }
1155 }
1156 })
1157 .await;
1158 }
1159 })
1160}