Skip to main content

cranpose_ui/modifier/
scroll.rs

1use std::{
2    cell::{Cell, RefCell},
3    rc::Rc,
4};
5
6use cranpose_core::{NodeId, current_runtime_handle, internal::FrameCallbackRegistration};
7use cranpose_foundation::{
8    DRAG_THRESHOLD, DelegatableNode, MAX_FLING_VELOCITY, ModifierNode, ModifierNodeElement,
9    NodeCapabilities, NodeState, PointerButton, PointerButtons, VelocityTracker1D,
10    velocity_tracker::ASSUME_STOPPED_MS,
11};
12use cranpose_ui_layout::Axis;
13use web_time::Instant;
14
15use super::{Modifier, Point, PointerEvent, PointerEventKind, inspector_metadata};
16use crate::{
17    draggable::DraggableState,
18    fling_animation::{
19        FlingAnimation, MIN_FLING_VELOCITY, SettleAnimation, SpringParams, fling_rest_position,
20    },
21    render_state::schedule_modifier_slices_repass,
22    scroll::{
23        ScrollElement, ScrollMotionContext, ScrollMotionContextKey, ScrollSettlePolicy,
24        ScrollState, scroll_motion_context_for_key,
25    },
26};
27
28#[cfg(feature = "test-helpers")]
29pub fn last_fling_velocity() -> f32 {
30    crate::render_state::debug_last_fling_velocity()
31}
32
33#[cfg(feature = "test-helpers")]
34pub fn reset_last_fling_velocity() {
35    crate::render_state::debug_reset_last_fling_velocity();
36}
37
38#[inline]
39fn set_last_fling_velocity(velocity: f32) {
40    crate::render_state::record_last_fling_velocity(velocity);
41}
42
43struct ScrollGestureState {
44    drag_down_position: Option<Point>,
45
46    last_position: Option<Point>,
47
48    is_dragging: bool,
49
50    axis_locked_out: bool,
51
52    velocity_tracker: VelocityTracker1D,
53
54    gesture_start_time: Option<Instant>,
55
56    gesture_start_event_time_ms: Option<i64>,
57
58    last_velocity_sample_ms: Option<i64>,
59
60    fling_animation: Option<FlingAnimation>,
61
62    is_overscrolling: bool,
63
64    settle_animation: Option<SettleAnimation>,
65
66    wheel_settle_watcher: Option<WheelSettleWatcher>,
67}
68
69impl Default for ScrollGestureState {
70    fn default() -> Self {
71        Self {
72            drag_down_position: None,
73            last_position: None,
74            is_dragging: false,
75            axis_locked_out: false,
76            velocity_tracker: VelocityTracker1D::new(),
77            gesture_start_time: None,
78            gesture_start_event_time_ms: None,
79            last_velocity_sample_ms: None,
80            fling_animation: None,
81            is_overscrolling: false,
82            settle_animation: None,
83            wheel_settle_watcher: None,
84        }
85    }
86}
87
88#[inline]
89fn calculate_total_delta(from: Point, to: Point, is_vertical: bool) -> f32 {
90    if is_vertical {
91        to.y - from.y
92    } else {
93        to.x - from.x
94    }
95}
96
97#[inline]
98fn calculate_incremental_delta(from: Point, to: Point, is_vertical: bool) -> f32 {
99    if is_vertical {
100        to.y - from.y
101    } else {
102        to.x - from.x
103    }
104}
105
106trait ScrollTarget: Clone {
107    fn apply_delta(&self, delta: f32) -> f32;
108
109    fn apply_wheel_delta(&self, delta: f32) -> f32 {
110        self.apply_delta(delta)
111    }
112
113    fn apply_fling_delta(&self, delta: f32) -> f32;
114
115    fn invalidate(&self);
116
117    fn current_offset(&self) -> f32;
118
119    fn can_scroll(&self) -> bool {
120        true
121    }
122
123    fn can_consume(&self, gesture_delta: f32) -> bool {
124        let _ = gesture_delta;
125        self.can_scroll()
126    }
127
128    fn settle_policy(&self) -> Option<ScrollSettlePolicy> {
129        None
130    }
131
132    fn allows_fling(&self) -> bool {
133        true
134    }
135
136    fn set_dragging(&self, dragging: bool) {
137        let _ = dragging;
138    }
139}
140
141impl ScrollTarget for ScrollState {
142    fn apply_delta(&self, delta: f32) -> f32 {
143        -self.dispatch_raw_delta(-delta)
144    }
145
146    fn apply_fling_delta(&self, delta: f32) -> f32 {
147        self.dispatch_raw_delta(delta)
148    }
149
150    fn invalidate(&self) {}
151
152    fn current_offset(&self) -> f32 {
153        self.value()
154    }
155
156    fn can_scroll(&self) -> bool {
157        self.max_value() > 0.0
158    }
159
160    fn can_consume(&self, gesture_delta: f32) -> bool {
161        let raw = -gesture_delta;
162        if raw > 0.0 {
163            self.value_non_reactive() < self.max_value()
164        } else {
165            self.value_non_reactive() > 0.0
166        }
167    }
168
169    fn settle_policy(&self) -> Option<ScrollSettlePolicy> {
170        ScrollState::settle_policy(self)
171    }
172}
173
174impl ScrollTarget for LazyListState {
175    fn apply_delta(&self, delta: f32) -> f32 {
176        self.dispatch_scroll_delta(delta)
177    }
178
179    fn apply_wheel_delta(&self, delta: f32) -> f32 {
180        if delta.abs() <= 0.001 {
181            0.0
182        } else {
183            self.dispatch_scroll_delta(delta)
184        }
185    }
186
187    fn apply_fling_delta(&self, delta: f32) -> f32 {
188        -self.dispatch_scroll_delta(-delta)
189    }
190
191    fn invalidate(&self) {}
192
193    fn current_offset(&self) -> f32 {
194        self.first_visible_item_scroll_offset()
195    }
196
197    fn can_scroll(&self) -> bool {
198        self.layout_info().total_items_count == 0
199            || self.can_scroll_forward_non_reactive()
200            || self.can_scroll_backward_non_reactive()
201    }
202
203    fn can_consume(&self, gesture_delta: f32) -> bool {
204        if self.layout_info().total_items_count == 0 {
205            return true;
206        }
207        if gesture_delta < 0.0 {
208            self.can_scroll_forward_non_reactive()
209        } else {
210            self.can_scroll_backward_non_reactive()
211        }
212    }
213}
214
215struct DragGesture<S: ScrollTarget> {
216    target: S,
217    gesture_state: Rc<RefCell<ScrollGestureState>>,
218    is_vertical: bool,
219    reverse_input: bool,
220    motion_context: ScrollMotionContext,
221    guard: Option<Rc<dyn Fn() -> bool>>,
222}
223
224fn drag_gesture_input<K, S>(key: K, gesture: DragGesture<S>) -> Modifier
225where
226    K: std::hash::Hash + 'static,
227    S: ScrollTarget + 'static,
228{
229    let DragGesture {
230        target,
231        gesture_state,
232        is_vertical,
233        reverse_input,
234        motion_context,
235        guard,
236    } = gesture;
237
238    Modifier::empty().pointer_input(key, move |scope| {
239        let detector = ScrollGestureDetector::new(
240            gesture_state.clone(),
241            target.clone(),
242            is_vertical,
243            reverse_input,
244            motion_context.overscroll(),
245            motion_context.clone(),
246        );
247        let guard = guard.clone();
248
249        async move {
250            scope
251                .await_pointer_event_scope(|await_scope| async move {
252                    loop {
253                        let event = await_scope.await_pointer_event().await;
254
255                        if event.id != 0 {
256                            continue;
257                        }
258
259                        if event.is_consumed() {
260                            if matches!(
261                                event.kind,
262                                PointerEventKind::Down
263                                    | PointerEventKind::Move
264                                    | PointerEventKind::Up
265                                    | PointerEventKind::Cancel
266                            ) {
267                                detector.on_cancel();
268                            }
269                            continue;
270                        }
271
272                        if let Some(ref guard) = guard
273                            && !guard()
274                        {
275                            if matches!(event.kind, PointerEventKind::Up | PointerEventKind::Cancel)
276                            {
277                                detector.on_cancel();
278                            }
279                            continue;
280                        }
281
282                        let should_consume = match event.kind {
283                            PointerEventKind::Down => {
284                                detector.on_down(event.position, event.time_ms)
285                            }
286                            PointerEventKind::Move => detector.on_move(
287                                event.position,
288                                event.buttons,
289                                event.time_ms,
290                                &event,
291                            ),
292                            PointerEventKind::Up => detector.on_up(event.time_ms),
293                            PointerEventKind::Cancel => detector.on_cancel(),
294                            PointerEventKind::Scroll => detector.on_scroll(
295                                if is_vertical {
296                                    event.scroll_delta.y
297                                } else {
298                                    event.scroll_delta.x
299                                },
300                                &event,
301                            ),
302                            PointerEventKind::Zoom
303                            | PointerEventKind::RotaryScrollPre
304                            | PointerEventKind::RotaryScroll
305                            | PointerEventKind::Enter
306                            | PointerEventKind::Exit => false,
307                        };
308
309                        if should_consume {
310                            event.consume();
311                        }
312                    }
313                })
314                .await;
315        }
316    })
317}
318
319impl ScrollTarget for DraggableState {
320    fn apply_delta(&self, delta: f32) -> f32 {
321        self.drag_by(delta);
322        delta
323    }
324
325    fn apply_fling_delta(&self, delta: f32) -> f32 {
326        self.drag_by(delta);
327        delta
328    }
329
330    fn invalidate(&self) {}
331
332    fn current_offset(&self) -> f32 {
333        self.offset()
334    }
335
336    fn allows_fling(&self) -> bool {
337        false
338    }
339
340    fn set_dragging(&self, dragging: bool) {
341        DraggableState::set_dragging(self, dragging);
342    }
343}
344
345const WHEEL_SETTLE_IDLE_NANOS: u64 = 180_000_000;
346
347struct WheelSettleWatcher {
348    is_running: Rc<Cell<bool>>,
349    registration: Rc<RefCell<Option<FrameCallbackRegistration>>>,
350}
351
352impl WheelSettleWatcher {
353    fn cancel(&self) {
354        self.is_running.set(false);
355        self.registration.borrow_mut().take();
356    }
357}
358
359struct ScrollGestureDetector<S: ScrollTarget> {
360    gesture_state: Rc<RefCell<ScrollGestureState>>,
361
362    scroll_target: S,
363
364    is_vertical: bool,
365
366    reverse_scrolling: bool,
367
368    overscroll: crate::scroll::OverscrollEffect,
369
370    motion_context: ScrollMotionContext,
371}
372
373impl<S: ScrollTarget + 'static> ScrollGestureDetector<S> {
374    fn new(
375        gesture_state: Rc<RefCell<ScrollGestureState>>,
376        scroll_target: S,
377        is_vertical: bool,
378        reverse_scrolling: bool,
379        overscroll: crate::scroll::OverscrollEffect,
380        motion_context: ScrollMotionContext,
381    ) -> Self {
382        Self {
383            gesture_state,
384            scroll_target,
385            is_vertical,
386            reverse_scrolling,
387            overscroll,
388            motion_context,
389        }
390    }
391
392    fn on_down(&self, position: Point, time_ms: Option<i64>) -> bool {
393        let mut gs = self.gesture_state.borrow_mut();
394
395        if let Some(fling) = gs.fling_animation.take() {
396            fling.cancel();
397        }
398        if let Some(settle) = gs.settle_animation.take() {
399            settle.cancel();
400        }
401        if let Some(watcher) = gs.wheel_settle_watcher.take() {
402            watcher.cancel();
403        }
404        self.motion_context.set_active(false);
405
406        gs.drag_down_position = Some(position);
407        gs.last_position = Some(position);
408        gs.is_dragging = false;
409        gs.axis_locked_out = false;
410        gs.velocity_tracker.reset();
411        gs.gesture_start_time = Some(Instant::now());
412        gs.gesture_start_event_time_ms = time_ms;
413        gs.is_overscrolling = self.overscroll.offset().abs() > 0.001;
414
415        let pos = if self.is_vertical {
416            position.y
417        } else {
418            position.x
419        };
420        gs.velocity_tracker.add_data_point(0, pos);
421        gs.last_velocity_sample_ms = Some(0);
422
423        false
424    }
425
426    fn on_move(
427        &self,
428        position: Point,
429        buttons: PointerButtons,
430        time_ms: Option<i64>,
431        event: &PointerEvent,
432    ) -> bool {
433        let mut gs = self.gesture_state.borrow_mut();
434
435        if !buttons.contains(PointerButton::Primary) && gs.drag_down_position.is_some() {
436            if gs.is_dragging {
437                self.scroll_target.set_dragging(false);
438            }
439            gs.drag_down_position = None;
440            gs.last_position = None;
441            gs.is_dragging = false;
442            gs.axis_locked_out = false;
443            gs.gesture_start_time = None;
444            gs.gesture_start_event_time_ms = None;
445            gs.last_velocity_sample_ms = None;
446            gs.velocity_tracker.reset();
447            self.motion_context.set_active(false);
448            return false;
449        }
450
451        let Some(down_pos) = gs.drag_down_position else {
452            return false;
453        };
454
455        let Some(last_pos) = gs.last_position else {
456            gs.last_position = Some(position);
457            return false;
458        };
459
460        let incremental_delta = calculate_incremental_delta(last_pos, position, self.is_vertical);
461
462        let mut edge_candidate = false;
463        if !gs.is_dragging && !gs.axis_locked_out {
464            let signed_main_delta = calculate_total_delta(down_pos, position, self.is_vertical);
465            let main_delta = signed_main_delta.abs();
466            let cross_delta = calculate_total_delta(down_pos, position, !self.is_vertical).abs();
467            if main_delta > DRAG_THRESHOLD && main_delta >= cross_delta {
468                if self.scroll_target.can_consume(signed_main_delta) {
469                    gs.is_dragging = true;
470                    self.scroll_target.set_dragging(true);
471                    self.motion_context.set_active(true);
472                } else if self.scroll_target.can_scroll() {
473                    edge_candidate = true;
474                }
475            } else if cross_delta > DRAG_THRESHOLD && cross_delta > main_delta {
476                gs.axis_locked_out = true;
477            }
478        }
479
480        gs.last_position = Some(position);
481
482        let pos = if self.is_vertical {
483            position.y
484        } else {
485            position.x
486        };
487        let event_sample_ms = gs
488            .gesture_start_event_time_ms
489            .zip(time_ms)
490            .map(|(start_ms, now_ms)| now_ms - start_ms);
491        let sample_ms = if let Some(event_sample_ms) = event_sample_ms {
492            Some(match gs.last_velocity_sample_ms {
493                Some(last_sample_ms) => event_sample_ms.max(last_sample_ms),
494                None => event_sample_ms.max(0),
495            })
496        } else if let Some(start_time) = gs.gesture_start_time {
497            let elapsed_ms = start_time.elapsed().as_millis() as i64;
498            Some(match gs.last_velocity_sample_ms {
499                Some(last_sample_ms) => {
500                    let mut sample_ms = if elapsed_ms <= last_sample_ms {
501                        last_sample_ms + 1
502                    } else {
503                        elapsed_ms
504                    };
505                    if sample_ms - last_sample_ms > ASSUME_STOPPED_MS {
506                        sample_ms = last_sample_ms + ASSUME_STOPPED_MS;
507                    }
508                    sample_ms
509                }
510                None => elapsed_ms,
511            })
512        } else {
513            None
514        };
515        if let Some(sample_ms) = sample_ms {
516            log::trace!(
517                target: "cranpose::velocity",
518                "sample t={sample_ms}ms pos={pos:.2} event_time={time_ms:?}"
519            );
520            gs.velocity_tracker.add_data_point(sample_ms, pos);
521            gs.last_velocity_sample_ms = Some(sample_ms);
522        }
523
524        if gs.is_dragging {
525            drop(gs);
526            let delta = if self.reverse_scrolling {
527                -incremental_delta
528            } else {
529                incremental_delta
530            };
531            let overscroll = self.overscroll.clone();
532            overscroll.apply_to_scroll(delta, |delta| self.scroll_target.apply_delta(delta));
533            self.scroll_target.invalidate();
534            true
535        } else if gs.is_overscrolling {
536            drop(gs);
537            let delta = if self.reverse_scrolling {
538                -incremental_delta
539            } else {
540                incremental_delta
541            };
542            self.apply_overscroll_delta(delta)
543        } else if edge_candidate {
544            drop(gs);
545            let delta = if self.reverse_scrolling {
546                -incremental_delta
547            } else {
548                incremental_delta
549            };
550            let detector = self.clone_for_watcher();
551            event.defer_post_dispatch_action(move || detector.apply_overscroll_candidate(delta));
552            false
553        } else {
554            false
555        }
556    }
557
558    fn apply_overscroll_delta(&self, delta: f32) -> bool {
559        self.motion_context.set_active(true);
560        let overscroll = self.overscroll.clone();
561        let target_consumed = Cell::new(0.0);
562        let before_overscroll = overscroll.offset();
563        overscroll.apply_to_scroll(delta, |delta| {
564            let consumed = self.scroll_target.apply_delta(delta);
565            target_consumed.set(consumed);
566            consumed
567        });
568        self.scroll_target.invalidate();
569        let consumed = target_consumed.get().abs() > 0.001
570            || (overscroll.offset() - before_overscroll).abs() > 0.001;
571        if !consumed {
572            self.motion_context.set_active(false);
573        }
574        consumed
575    }
576
577    fn apply_overscroll_candidate(&self, delta: f32) -> bool {
578        let consumed = self.apply_overscroll_delta(delta);
579        if consumed {
580            self.gesture_state.borrow_mut().is_overscrolling = true;
581        }
582        consumed
583    }
584
585    fn finish_gesture(&self, allow_fling: bool, release_time_ms: Option<i64>) -> bool {
586        let (was_dragging, gesture_owned, velocity, start_fling, existing_fling) = {
587            let mut gs = self.gesture_state.borrow_mut();
588            let was_dragging = gs.is_dragging;
589            let gesture_owned = was_dragging || gs.is_overscrolling;
590            let mut velocity = 0.0;
591
592            if allow_fling && gesture_owned && gs.gesture_start_time.is_some() {
593                let release_sample_ms = release_time_ms
594                    .zip(gs.gesture_start_event_time_ms)
595                    .map(|(release_ms, start_ms)| release_ms - start_ms)
596                    .or_else(|| {
597                        gs.gesture_start_time
598                            .map(|start| start.elapsed().as_millis() as i64)
599                    });
600                let rested_before_release = release_sample_ms
601                    .zip(gs.last_velocity_sample_ms)
602                    .is_some_and(|(release_ms, last_sample_ms)| {
603                        release_ms - last_sample_ms > ASSUME_STOPPED_MS
604                    });
605                if !rested_before_release {
606                    velocity = gs
607                        .velocity_tracker
608                        .calculate_velocity_with_max(MAX_FLING_VELOCITY);
609                }
610            }
611
612            let start_fling = allow_fling && was_dragging && velocity.abs() > MIN_FLING_VELOCITY;
613            let existing_fling = if start_fling {
614                gs.fling_animation.take()
615            } else {
616                None
617            };
618
619            if was_dragging {
620                self.scroll_target.set_dragging(false);
621            }
622            gs.drag_down_position = None;
623            gs.last_position = None;
624            gs.is_dragging = false;
625            gs.is_overscrolling = false;
626            gs.axis_locked_out = false;
627            gs.gesture_start_time = None;
628            gs.gesture_start_event_time_ms = None;
629            gs.last_velocity_sample_ms = None;
630
631            (
632                was_dragging,
633                gesture_owned,
634                velocity,
635                start_fling,
636                existing_fling,
637            )
638        };
639
640        if allow_fling && gesture_owned {
641            log::debug!(
642                target: "cranpose::velocity",
643                "gesture finished: fling velocity={velocity:.2} dp/s start_fling={start_fling}"
644            );
645            set_last_fling_velocity(velocity);
646        }
647
648        let adjusted_velocity = if self.reverse_scrolling {
649            -velocity
650        } else {
651            velocity
652        };
653        let fling_velocity = -adjusted_velocity;
654        let has_overscroll = self.overscroll.offset().abs() > 0.001;
655
656        let settle_target = if was_dragging {
657            self.scroll_target.settle_policy().and_then(|policy| {
658                let current = self.scroll_target.current_offset();
659                let proposed = if start_fling {
660                    fling_rest_position(current, fling_velocity)
661                } else {
662                    current
663                };
664                let target = policy(proposed, fling_velocity);
665                ((target - proposed).abs() > 0.5).then_some(target)
666            })
667        } else {
668            None
669        };
670
671        if has_overscroll {
672            if let Some(old_fling) = existing_fling {
673                old_fling.cancel();
674            }
675            self.start_overscroll_settle(-fling_velocity);
676        } else if let Some(target) = settle_target {
677            if let Some(old_fling) = existing_fling {
678                old_fling.cancel();
679            }
680            self.start_settle_animation(target, fling_velocity);
681        } else if start_fling {
682            if let Some(old_fling) = existing_fling {
683                old_fling.cancel();
684            }
685            self.start_fling_animation(fling_velocity);
686        } else {
687            self.motion_context.set_active(false);
688        }
689
690        gesture_owned
691    }
692
693    fn start_fling_animation(&self, fling_velocity: f32) {
694        let Some(runtime) = current_runtime_handle() else {
695            self.motion_context.set_active(false);
696            return;
697        };
698        self.motion_context.set_active(true);
699        let scroll_target = self.scroll_target.clone();
700        let fling = FlingAnimation::new(runtime);
701        let motion_context = self.motion_context.clone();
702        let initial_value = scroll_target.current_offset();
703        let scroll_target_for_fling = scroll_target.clone();
704        let scroll_target_for_end = scroll_target.clone();
705        let detector_for_end = self.clone_for_watcher();
706        let overscroll_for_fling = self.overscroll.clone();
707
708        fling.start_fling(
709            initial_value,
710            fling_velocity,
711            move |delta| {
712                let consumed = overscroll_for_fling.apply_to_fling(delta, |delta| {
713                    scroll_target_for_fling.apply_fling_delta(delta)
714                });
715                scroll_target_for_fling.invalidate();
716                consumed
717            },
718            move || {
719                scroll_target_for_end.invalidate();
720                let settle_running = detector_for_end
721                    .gesture_state
722                    .borrow()
723                    .settle_animation
724                    .as_ref()
725                    .is_some_and(SettleAnimation::is_running);
726                if detector_for_end.overscroll.offset().abs() > 0.001 && !settle_running {
727                    detector_for_end.start_overscroll_settle(0.0);
728                }
729                detector_for_end.update_motion_active(&motion_context);
730            },
731        );
732
733        let mut gs = self.gesture_state.borrow_mut();
734        gs.fling_animation = Some(fling);
735    }
736
737    fn start_settle_animation(&self, target: f32, initial_velocity: f32) {
738        let Some(runtime) = current_runtime_handle() else {
739            self.motion_context.set_active(false);
740            return;
741        };
742        self.motion_context.set_active(true);
743        let settle = SettleAnimation::new(runtime, SpringParams::SETTLE_POLICY);
744        let scroll_target_for_settle = self.scroll_target.clone();
745        let scroll_target_for_end = self.scroll_target.clone();
746        let detector_for_end = self.clone_for_watcher();
747        let motion_context = self.motion_context.clone();
748        settle.start_settle(
749            self.scroll_target.current_offset(),
750            initial_velocity,
751            target,
752            move |delta| {
753                let consumed = scroll_target_for_settle.apply_fling_delta(delta);
754                scroll_target_for_settle.invalidate();
755                consumed
756            },
757            move |_| {
758                scroll_target_for_end.invalidate();
759                detector_for_end.update_motion_active(&motion_context);
760            },
761        );
762        let mut gs = self.gesture_state.borrow_mut();
763        gs.settle_animation = Some(settle);
764    }
765
766    fn start_overscroll_settle(&self, initial_velocity: f32) {
767        let Some(runtime) = current_runtime_handle() else {
768            let current = self.overscroll.offset();
769            self.overscroll.apply_settle_delta(-current);
770            self.motion_context.set_active(false);
771            return;
772        };
773        let settle = SettleAnimation::new(runtime, SpringParams::OVERSCROLL_BOUNCE);
774        let overscroll_for_settle = self.overscroll.clone();
775        let overscroll_for_end = self.overscroll.clone();
776        let detector_for_end = self.clone_for_watcher();
777        let motion_context = self.motion_context.clone();
778        let initial = self.overscroll.offset();
779        settle.start_settle(
780            initial,
781            initial_velocity,
782            0.0,
783            move |delta| overscroll_for_settle.apply_settle_delta(delta),
784            move |end| {
785                let current = overscroll_for_end.offset();
786                overscroll_for_end.apply_settle_delta(-current);
787                if end.hit_boundary && end.velocity.abs() > MIN_FLING_VELOCITY {
788                    detector_for_end.start_fling_animation(-end.velocity);
789                } else {
790                    detector_for_end.update_motion_active(&motion_context);
791                }
792            },
793        );
794        let mut gs = self.gesture_state.borrow_mut();
795        gs.is_overscrolling = false;
796        gs.settle_animation = Some(settle);
797    }
798
799    fn update_motion_active(&self, motion_context: &ScrollMotionContext) {
800        let running = {
801            let gs = self.gesture_state.borrow();
802            gs.fling_animation
803                .as_ref()
804                .is_some_and(FlingAnimation::is_running)
805                || gs
806                    .settle_animation
807                    .as_ref()
808                    .is_some_and(SettleAnimation::is_running)
809        };
810        if !running {
811            motion_context.set_active(false);
812        }
813    }
814
815    fn on_up(&self, time_ms: Option<i64>) -> bool {
816        self.finish_gesture(self.scroll_target.allows_fling(), time_ms)
817    }
818
819    fn on_cancel(&self) -> bool {
820        self.finish_gesture(false, None)
821    }
822
823    fn on_scroll(&self, axis_delta: f32, event: &PointerEvent) -> bool {
824        if axis_delta.abs() <= f32::EPSILON {
825            return false;
826        }
827
828        let delta = if self.reverse_scrolling {
829            -axis_delta
830        } else {
831            axis_delta
832        };
833        if !self.scroll_target.can_consume(delta) && self.scroll_target.can_scroll() {
834            let detector = self.clone_for_watcher();
835            event.defer_post_dispatch_action(move || detector.apply_wheel_delta(delta));
836            return false;
837        }
838        self.apply_wheel_delta(delta)
839    }
840
841    fn apply_wheel_delta(&self, delta: f32) -> bool {
842        {
843            let mut gs = self.gesture_state.borrow_mut();
844            if let Some(fling) = gs.fling_animation.take() {
845                fling.cancel();
846            }
847            if let Some(settle) = gs.settle_animation.take() {
848                settle.cancel();
849            }
850            gs.drag_down_position = None;
851            gs.last_position = None;
852            gs.is_dragging = false;
853            gs.axis_locked_out = false;
854            gs.gesture_start_time = None;
855            gs.gesture_start_event_time_ms = None;
856            gs.last_velocity_sample_ms = None;
857            gs.velocity_tracker.reset();
858        }
859
860        self.motion_context.activate_for_current_frame();
861        let overscroll = self.overscroll.clone();
862        let consumed =
863            overscroll.apply_to_scroll(delta, |delta| self.scroll_target.apply_wheel_delta(delta));
864        let overscroll_active = self.overscroll.offset().abs() > 0.001;
865        if consumed.abs() > 0.001 {
866            self.scroll_target.invalidate();
867        }
868        if consumed.abs() > 0.001 || overscroll_active {
869            self.ensure_wheel_settle_watcher();
870            true
871        } else {
872            false
873        }
874    }
875
876    fn ensure_wheel_settle_watcher(&self) {
877        if self.scroll_target.settle_policy().is_none() && self.overscroll.offset().abs() <= 0.001 {
878            return;
879        }
880        {
881            let gs = self.gesture_state.borrow();
882            if gs
883                .wheel_settle_watcher
884                .as_ref()
885                .is_some_and(|watcher| watcher.is_running.get())
886            {
887                return;
888            }
889        }
890        let Some(runtime) = current_runtime_handle() else {
891            return;
892        };
893
894        let is_running = Rc::new(Cell::new(true));
895        let registration = Rc::new(RefCell::new(None));
896
897        struct WheelSettleLoop<S: ScrollTarget> {
898            detector: ScrollGestureDetector<S>,
899            gesture_state: Rc<RefCell<ScrollGestureState>>,
900            frame_clock: cranpose_core::internal::FrameClock,
901            is_running: Rc<Cell<bool>>,
902            registration: Rc<RefCell<Option<FrameCallbackRegistration>>>,
903            last_offset: Rc<Cell<f32>>,
904            idle_nanos: Rc<Cell<u64>>,
905            last_frame: Rc<Cell<Option<u64>>>,
906        }
907
908        impl<S: ScrollTarget + 'static> WheelSettleLoop<S> {
909            fn next(&self) -> Self {
910                Self {
911                    detector: self.detector.clone_for_watcher(),
912                    gesture_state: Rc::clone(&self.gesture_state),
913                    frame_clock: self.frame_clock.clone(),
914                    is_running: Rc::clone(&self.is_running),
915                    registration: Rc::clone(&self.registration),
916                    last_offset: Rc::clone(&self.last_offset),
917                    idle_nanos: Rc::clone(&self.idle_nanos),
918                    last_frame: Rc::clone(&self.last_frame),
919                }
920            }
921
922            fn schedule(self) {
923                let continuation = self.next();
924                let registration_slot = Rc::clone(&self.registration);
925                let new_registration = self.frame_clock.with_frame_nanos(move |frame_time_nanos| {
926                    let this = &continuation;
927                    if !this.is_running.get() {
928                        return;
929                    }
930                    {
931                        let gs = this.gesture_state.borrow();
932                        let animating = gs.is_dragging
933                            || gs
934                                .fling_animation
935                                .as_ref()
936                                .is_some_and(FlingAnimation::is_running)
937                            || gs
938                                .settle_animation
939                                .as_ref()
940                                .is_some_and(SettleAnimation::is_running);
941                        if animating {
942                            this.is_running.set(false);
943                            return;
944                        }
945                    }
946
947                    let offset = this.detector.scroll_target.current_offset();
948                    let dt = this
949                        .last_frame
950                        .get()
951                        .map_or(0, |last| frame_time_nanos.saturating_sub(last));
952                    this.last_frame.set(Some(frame_time_nanos));
953                    if (offset - this.last_offset.get()).abs() > 0.01 {
954                        this.last_offset.set(offset);
955                        this.idle_nanos.set(0);
956                    } else {
957                        this.idle_nanos.set(this.idle_nanos.get() + dt);
958                    }
959
960                    if this.idle_nanos.get() >= WHEEL_SETTLE_IDLE_NANOS {
961                        this.is_running.set(false);
962                        if this.detector.overscroll.offset().abs() > 0.001 {
963                            this.detector.start_overscroll_settle(0.0);
964                            return;
965                        }
966                        if let Some(policy) = this.detector.scroll_target.settle_policy() {
967                            let target = policy(offset, 0.0);
968                            if (target - offset).abs() > 0.5 {
969                                this.detector.start_settle_animation(target, 0.0);
970                            }
971                        }
972                        return;
973                    }
974
975                    continuation.next().schedule();
976                });
977                *registration_slot.borrow_mut() = Some(new_registration);
978            }
979        }
980
981        WheelSettleLoop {
982            detector: self.clone_for_watcher(),
983            gesture_state: Rc::clone(&self.gesture_state),
984            frame_clock: runtime.frame_clock(),
985            is_running: Rc::clone(&is_running),
986            registration: Rc::clone(&registration),
987            last_offset: Rc::new(Cell::new(self.scroll_target.current_offset())),
988            idle_nanos: Rc::new(Cell::new(0u64)),
989            last_frame: Rc::new(Cell::new(None::<u64>)),
990        }
991        .schedule();
992
993        self.gesture_state.borrow_mut().wheel_settle_watcher = Some(WheelSettleWatcher {
994            is_running,
995            registration,
996        });
997    }
998
999    fn clone_for_watcher(&self) -> ScrollGestureDetector<S> {
1000        ScrollGestureDetector {
1001            gesture_state: Rc::clone(&self.gesture_state),
1002            scroll_target: self.scroll_target.clone(),
1003            is_vertical: self.is_vertical,
1004            reverse_scrolling: self.reverse_scrolling,
1005            overscroll: self.overscroll.clone(),
1006            motion_context: self.motion_context.clone(),
1007        }
1008    }
1009}
1010
1011pub(crate) struct MotionContextAnimatedNode {
1012    state: NodeState,
1013    motion_context: ScrollMotionContext,
1014    invalidation_callback_id: Option<u64>,
1015    overscroll_callback_id: Option<u64>,
1016    node_id: Option<NodeId>,
1017}
1018
1019impl MotionContextAnimatedNode {
1020    fn new(motion_context: ScrollMotionContext) -> Self {
1021        Self {
1022            state: NodeState::new(),
1023            motion_context,
1024            invalidation_callback_id: None,
1025            overscroll_callback_id: None,
1026            node_id: None,
1027        }
1028    }
1029
1030    pub(crate) fn is_active(&self) -> bool {
1031        self.motion_context.is_active()
1032    }
1033}
1034
1035pub(crate) struct TranslatedContentContextNode {
1036    state: NodeState,
1037    identity: usize,
1038    offset_source: TranslatedContentOffsetSource,
1039    overscroll: crate::scroll::OverscrollEffect,
1040    overscroll_callback_id: Option<u64>,
1041}
1042
1043impl TranslatedContentContextNode {
1044    fn new(
1045        identity: usize,
1046        offset_source: TranslatedContentOffsetSource,
1047        overscroll: crate::scroll::OverscrollEffect,
1048    ) -> Self {
1049        Self {
1050            state: NodeState::new(),
1051            identity,
1052            offset_source,
1053            overscroll,
1054            overscroll_callback_id: None,
1055        }
1056    }
1057
1058    pub(crate) fn is_active(&self) -> bool {
1059        true
1060    }
1061
1062    pub(crate) fn identity(&self) -> usize {
1063        self.identity
1064    }
1065
1066    pub(crate) fn content_offset_reader(&self) -> Option<Rc<dyn Fn() -> Point>> {
1067        self.offset_source
1068            .content_offset_reader(self.overscroll.clone())
1069    }
1070}
1071
1072impl DelegatableNode for TranslatedContentContextNode {
1073    fn node_state(&self) -> &NodeState {
1074        &self.state
1075    }
1076}
1077
1078impl ModifierNode for TranslatedContentContextNode {
1079    fn on_attach(&mut self, context: &mut dyn cranpose_foundation::ModifierNodeContext) {
1080        if let Some(node_id) = context.node_id() {
1081            self.overscroll_callback_id =
1082                Some(self.overscroll.add_invalidate_callback(Box::new(move || {
1083                    schedule_modifier_slices_repass(node_id)
1084                })));
1085        }
1086    }
1087
1088    fn on_detach(&mut self) {
1089        if let Some(id) = self.overscroll_callback_id.take() {
1090            self.overscroll.remove_invalidate_callback(id);
1091        }
1092    }
1093}
1094
1095impl DelegatableNode for MotionContextAnimatedNode {
1096    fn node_state(&self) -> &NodeState {
1097        &self.state
1098    }
1099}
1100
1101impl ModifierNode for MotionContextAnimatedNode {
1102    fn on_attach(&mut self, context: &mut dyn cranpose_foundation::ModifierNodeContext) {
1103        let node_id = context.node_id();
1104        self.node_id = node_id;
1105        if let Some(node_id) = node_id {
1106            let callback_id = self
1107                .motion_context
1108                .add_invalidate_callback(Box::new(move || {
1109                    schedule_modifier_slices_repass(node_id);
1110                }));
1111            self.invalidation_callback_id = Some(callback_id);
1112            let callback_id = self
1113                .motion_context
1114                .overscroll()
1115                .add_invalidate_callback(Box::new(move || {
1116                    crate::schedule_measure_repass(node_id);
1117                    schedule_modifier_slices_repass(node_id);
1118                }));
1119            self.overscroll_callback_id = Some(callback_id);
1120        }
1121    }
1122
1123    fn on_detach(&mut self) {
1124        if let Some(id) = self.invalidation_callback_id.take() {
1125            self.motion_context.remove_invalidate_callback(id);
1126        }
1127        if let Some(id) = self.overscroll_callback_id.take() {
1128            self.motion_context
1129                .overscroll()
1130                .remove_invalidate_callback(id);
1131        }
1132        self.node_id = None;
1133    }
1134}
1135
1136#[derive(Clone)]
1137struct MotionContextAnimatedElement {
1138    motion_context: ScrollMotionContext,
1139}
1140
1141impl MotionContextAnimatedElement {
1142    fn new(motion_context: ScrollMotionContext) -> Self {
1143        Self { motion_context }
1144    }
1145}
1146
1147impl std::fmt::Debug for MotionContextAnimatedElement {
1148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1149        f.debug_struct("MotionContextAnimatedElement").finish()
1150    }
1151}
1152
1153impl PartialEq for MotionContextAnimatedElement {
1154    fn eq(&self, other: &Self) -> bool {
1155        self.motion_context.ptr_eq(&other.motion_context)
1156    }
1157}
1158
1159impl Eq for MotionContextAnimatedElement {}
1160
1161impl std::hash::Hash for MotionContextAnimatedElement {
1162    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1163        self.motion_context.stable_key().hash(state);
1164    }
1165}
1166
1167impl ModifierNodeElement for MotionContextAnimatedElement {
1168    type Node = MotionContextAnimatedNode;
1169
1170    fn create(&self) -> Self::Node {
1171        MotionContextAnimatedNode::new(self.motion_context.clone())
1172    }
1173
1174    fn update(&self, node: &mut Self::Node) {
1175        if node.motion_context.ptr_eq(&self.motion_context) {
1176            return;
1177        }
1178        if let Some(id) = node.invalidation_callback_id.take() {
1179            node.motion_context.remove_invalidate_callback(id);
1180        }
1181        node.motion_context = self.motion_context.clone();
1182        if let Some(node_id) = node.node_id {
1183            let callback_id = node
1184                .motion_context
1185                .add_invalidate_callback(Box::new(move || {
1186                    schedule_modifier_slices_repass(node_id);
1187                }));
1188            node.invalidation_callback_id = Some(callback_id);
1189        }
1190    }
1191
1192    fn capabilities(&self) -> NodeCapabilities {
1193        NodeCapabilities::LAYOUT
1194    }
1195}
1196
1197#[derive(Clone)]
1198enum TranslatedContentOffsetSource {
1199    LayoutContentOffset,
1200    LazyList {
1201        state: LazyListState,
1202        is_vertical: bool,
1203        reverse_scrolling: bool,
1204    },
1205}
1206
1207impl TranslatedContentOffsetSource {
1208    fn content_offset_reader(
1209        &self,
1210        overscroll: crate::scroll::OverscrollEffect,
1211    ) -> Option<Rc<dyn Fn() -> Point>> {
1212        match self {
1213            Self::LayoutContentOffset => None,
1214            Self::LazyList {
1215                state, is_vertical, ..
1216            } => Some(Rc::new(lazy_list_content_offset_reader(
1217                *state,
1218                *is_vertical,
1219                overscroll,
1220            ))),
1221        }
1222    }
1223
1224    fn is_vertical(&self) -> Option<bool> {
1225        match self {
1226            Self::LayoutContentOffset => None,
1227            Self::LazyList { is_vertical, .. } => Some(*is_vertical),
1228        }
1229    }
1230
1231    fn reverse_scrolling(&self) -> Option<bool> {
1232        match self {
1233            Self::LayoutContentOffset => None,
1234            Self::LazyList {
1235                reverse_scrolling, ..
1236            } => Some(*reverse_scrolling),
1237        }
1238    }
1239}
1240
1241fn lazy_list_content_offset_reader(
1242    state: LazyListState,
1243    is_vertical: bool,
1244    overscroll: crate::scroll::OverscrollEffect,
1245) -> impl Fn() -> Point {
1246    move || {
1247        let info = state.layout_info();
1248        if info.visible_items_info.is_empty() {
1249            return Point::default();
1250        };
1251        overscroll.set_dimension(info.viewport_size);
1252        let main_offset = info.snap_anchor_offset;
1253        let bounce = overscroll.offset();
1254        if is_vertical {
1255            Point::new(0.0, main_offset + bounce)
1256        } else {
1257            Point::new(main_offset + bounce, 0.0)
1258        }
1259    }
1260}
1261
1262#[derive(Clone)]
1263struct TranslatedContentContextElement {
1264    identity: usize,
1265    offset_source: TranslatedContentOffsetSource,
1266    overscroll: crate::scroll::OverscrollEffect,
1267}
1268
1269impl TranslatedContentContextElement {
1270    fn new(
1271        identity: usize,
1272        offset_source: TranslatedContentOffsetSource,
1273        overscroll: crate::scroll::OverscrollEffect,
1274    ) -> Self {
1275        Self {
1276            identity,
1277            offset_source,
1278            overscroll,
1279        }
1280    }
1281}
1282
1283impl std::fmt::Debug for TranslatedContentContextElement {
1284    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1285        let offset_source = match &self.offset_source {
1286            TranslatedContentOffsetSource::LayoutContentOffset => "layout",
1287            TranslatedContentOffsetSource::LazyList { .. } => "lazy_list",
1288        };
1289        f.debug_struct("TranslatedContentContextElement")
1290            .field("identity", &self.identity)
1291            .field("offset_source", &offset_source)
1292            .finish()
1293    }
1294}
1295
1296impl PartialEq for TranslatedContentContextElement {
1297    fn eq(&self, other: &Self) -> bool {
1298        self.identity == other.identity
1299            && self.overscroll.ptr_eq(&other.overscroll)
1300            && self.offset_source.is_vertical() == other.offset_source.is_vertical()
1301            && self.offset_source.reverse_scrolling() == other.offset_source.reverse_scrolling()
1302    }
1303}
1304
1305impl Eq for TranslatedContentContextElement {}
1306
1307impl std::hash::Hash for TranslatedContentContextElement {
1308    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1309        self.identity.hash(state);
1310        self.offset_source.is_vertical().hash(state);
1311        self.offset_source.reverse_scrolling().hash(state);
1312    }
1313}
1314
1315impl ModifierNodeElement for TranslatedContentContextElement {
1316    type Node = TranslatedContentContextNode;
1317
1318    fn create(&self) -> Self::Node {
1319        TranslatedContentContextNode::new(
1320            self.identity,
1321            self.offset_source.clone(),
1322            self.overscroll.clone(),
1323        )
1324    }
1325
1326    fn update(&self, node: &mut Self::Node) {
1327        node.identity = self.identity;
1328        node.offset_source = self.offset_source.clone();
1329        node.overscroll = self.overscroll.clone();
1330    }
1331
1332    fn capabilities(&self) -> NodeCapabilities {
1333        NodeCapabilities::LAYOUT
1334    }
1335}
1336
1337impl Modifier {
1338    /// Creates a horizontally scrollable modifier.
1339    ///
1340    /// # Arguments
1341    /// * `state` - The ScrollState to control scroll position
1342    /// * `reverse_scrolling` - If true, reverses the scroll direction in layout.
1343    ///   Note: This affects how scroll offset is applied to content (via `ScrollNode`),
1344    ///   NOT the drag direction. Drag gestures always follow natural touch semantics:
1345    ///   drag right = scroll left (content moves right under finger).
1346    ///
1347    /// # Example
1348    /// ```text
1349    /// let scroll_state = ScrollState::new(0.0);
1350    /// Row(
1351    ///     Modifier::empty().horizontal_scroll(scroll_state, false),
1352    ///     // ... content
1353    /// );
1354    /// ```
1355    pub fn horizontal_scroll(self, state: ScrollState, reverse_scrolling: bool) -> Self {
1356        self.then(scroll_impl(state, false, reverse_scrolling, None))
1357    }
1358
1359    /// Creates a vertically scrollable modifier.
1360    ///
1361    /// # Arguments
1362    /// * `state` - The ScrollState to control scroll position
1363    /// * `reverse_scrolling` - If true, reverses the scroll direction in layout.
1364    ///   Note: This affects how scroll offset is applied to content (via `ScrollNode`),
1365    ///   NOT the drag direction. Drag gestures always follow natural touch semantics:
1366    ///   drag down = scroll up (content moves down under finger).
1367    pub fn vertical_scroll(self, state: ScrollState, reverse_scrolling: bool) -> Self {
1368        self.then(scroll_impl(state, true, reverse_scrolling, None))
1369    }
1370}
1371
1372fn scroll_impl(
1373    state: ScrollState,
1374    is_vertical: bool,
1375    reverse_scrolling: bool,
1376    guard: Option<Rc<dyn Fn() -> bool>>,
1377) -> Modifier {
1378    let gesture_state = Rc::new(RefCell::new(ScrollGestureState::default()));
1379    let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::ScrollState {
1380        state_id: state.id(),
1381        is_vertical,
1382        reverse_scrolling,
1383    });
1384
1385    let pointer_input = drag_gesture_input(
1386        (state.id(), is_vertical),
1387        DragGesture {
1388            target: state,
1389            gesture_state,
1390            is_vertical,
1391            reverse_input: false,
1392            motion_context: motion_context.clone(),
1393            guard,
1394        },
1395    );
1396
1397    let overscroll = motion_context.overscroll();
1398    let element = ScrollElement::new(state, overscroll.clone(), is_vertical, reverse_scrolling);
1399    let layout_modifier =
1400        Modifier::with_element(element).with_inspector_metadata(inspector_metadata(
1401            if is_vertical {
1402                "verticalScroll"
1403            } else {
1404                "horizontalScroll"
1405            },
1406            move |info| {
1407                info.add_property("isVertical", is_vertical.to_string());
1408                info.add_property("reverseScrolling", reverse_scrolling.to_string());
1409            },
1410        ));
1411    let motion_modifier =
1412        Modifier::with_element(MotionContextAnimatedElement::new(motion_context.clone()));
1413    let translated_content_modifier = Modifier::with_element(TranslatedContentContextElement::new(
1414        state.id() as usize,
1415        TranslatedContentOffsetSource::LayoutContentOffset,
1416        overscroll,
1417    ));
1418
1419    pointer_input
1420        .then(motion_modifier)
1421        .then(translated_content_modifier)
1422        .then(layout_modifier)
1423        .clip_to_bounds()
1424}
1425
1426use cranpose_foundation::lazy::LazyListState;
1427
1428impl Modifier {
1429    /// Creates a vertically scrollable modifier for lazy lists.
1430    ///
1431    /// This connects pointer gestures to LazyListState for scroll handling.
1432    /// Unlike regular vertical_scroll, no layout offset is applied here
1433    /// since LazyListState manages item positioning internally.
1434    /// Creates a vertically scrollable modifier for lazy lists.
1435    ///
1436    /// This connects pointer gestures to LazyListState for scroll handling.
1437    /// Unlike regular vertical_scroll, no layout offset is applied here
1438    /// since LazyListState manages item positioning internally.
1439    pub fn lazy_vertical_scroll(self, state: LazyListState, reverse_scrolling: bool) -> Self {
1440        let motion_context = scroll_motion_context_for_key(ScrollMotionContextKey::LazyList {
1441            state_identity: state.inner_ptr() as usize,
1442            is_vertical: true,
1443            reverse_scrolling,
1444        });
1445        self.lazy_vertical_scroll_with_context(state, reverse_scrolling, motion_context)
1446    }
1447
1448    pub(crate) fn lazy_vertical_scroll_with_context(
1449        self,
1450        state: LazyListState,
1451        reverse_scrolling: bool,
1452        motion_context: ScrollMotionContext,
1453    ) -> Self {
1454        self.then(lazy_scroll_impl(
1455            state,
1456            true,
1457            reverse_scrolling,
1458            motion_context,
1459        ))
1460    }
1461
1462    pub(crate) fn lazy_horizontal_scroll_with_context(
1463        self,
1464        state: LazyListState,
1465        reverse_scrolling: bool,
1466        motion_context: ScrollMotionContext,
1467    ) -> Self {
1468        self.then(lazy_scroll_impl(
1469            state,
1470            false,
1471            reverse_scrolling,
1472            motion_context,
1473        ))
1474    }
1475}
1476
1477fn lazy_scroll_impl(
1478    state: LazyListState,
1479    is_vertical: bool,
1480    reverse_scrolling: bool,
1481    motion_context: ScrollMotionContext,
1482) -> Modifier {
1483    let gesture_state = Rc::new(RefCell::new(ScrollGestureState::default()));
1484    let list_state = state;
1485    let state_id = state.inner_ptr() as usize;
1486    let key = (state_id, is_vertical, reverse_scrolling);
1487    let overscroll = motion_context.overscroll();
1488    let translated_content_modifier = Modifier::with_element(TranslatedContentContextElement::new(
1489        state_id,
1490        TranslatedContentOffsetSource::LazyList {
1491            state,
1492            is_vertical,
1493            reverse_scrolling,
1494        },
1495        overscroll,
1496    ));
1497
1498    Modifier::with_element(MotionContextAnimatedElement::new(motion_context.clone()))
1499        .then(translated_content_modifier)
1500        .then(drag_gesture_input(
1501            key,
1502            DragGesture {
1503                target: list_state,
1504                gesture_state,
1505                is_vertical,
1506                reverse_input: reverse_scrolling,
1507                motion_context,
1508                guard: None,
1509            },
1510        ))
1511}
1512
1513impl Modifier {
1514    /// Drags along `axis`, reporting each delta to `state`.
1515    ///
1516    /// The gesture is the one the scroll containers run — touch slop before a
1517    /// drag begins, axis locking so a mostly-vertical drag never steals a
1518    /// horizontal one, the primary pointer only, and yielding to whoever
1519    /// consumed the event first — so a dragged control and a scrolled list feel
1520    /// the same under a finger. Unlike a scroll, a release does not fling: a
1521    /// control the user placed stays where it was let go.
1522    ///
1523    /// Deltas are logical pixels, positive to the right for [`Axis::Horizontal`]
1524    /// and downwards for [`Axis::Vertical`].
1525    pub fn draggable(self, axis: Axis, state: DraggableState) -> Self {
1526        self.then(draggable_impl(axis, state, None))
1527    }
1528
1529    /// Drags along `axis` only while `guard` says so.
1530    ///
1531    /// The guard is asked per event, so a control can stop accepting a drag
1532    /// half way through one and the gesture ends rather than being applied to
1533    /// a control that has since been disabled.
1534    pub fn draggable_guarded(
1535        self,
1536        axis: Axis,
1537        state: DraggableState,
1538        guard: impl Fn() -> bool + 'static,
1539    ) -> Self {
1540        self.then(draggable_impl(axis, state, Some(Rc::new(guard))))
1541    }
1542}
1543
1544fn draggable_impl(
1545    axis: Axis,
1546    state: DraggableState,
1547    guard: Option<Rc<dyn Fn() -> bool>>,
1548) -> Modifier {
1549    let is_vertical = axis.is_vertical();
1550    let identity = state.identity();
1551    drag_gesture_input(
1552        (identity, is_vertical),
1553        DragGesture {
1554            target: state,
1555            gesture_state: Rc::new(RefCell::new(ScrollGestureState::default())),
1556            is_vertical,
1557            reverse_input: false,
1558            motion_context: scroll_motion_context_for_key(ScrollMotionContextKey::Draggable {
1559                state_identity: identity,
1560                is_vertical,
1561            }),
1562            guard,
1563        },
1564    )
1565}