Skip to main content

gpui_ce_motion/
reorder_native.rs

1//! Measured spatial reorder, dedicated handles and elapsed-time edge autoscroll.
2use crate::{
3    AnimationError, AutoScroll, LayoutRect, MotionValue, ReorderAxis, Transition, move_item,
4};
5
6use crate::{Axis, MotionAnchor};
7
8use gpui::{
9    AnyElement, App, Bounds, DispatchPhase, Div, Element, ElementId, EntityId, FocusHandle,
10    GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId, IntoElement, KeyDownEvent,
11    LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, ScrollHandle,
12    Stateful, Window, point, prelude::*, px,
13};
14
15use std::{
16    cell::{Ref, RefCell},
17    collections::HashMap,
18    hash::Hash,
19    rc::Rc,
20    time::{Duration, Instant},
21};
22
23struct Metrics {
24    bounds: Bounds<Pixels>,
25    displayed: Bounds<Pixels>,
26    values: [MotionValue; 2],
27    focus: FocusHandle,
28    measured: bool,
29    // Logical slot represented by this geometry, independent of newer input edits.
30    slot: usize,
31}
32
33struct Grab<K> {
34    key: K,
35    offset: [f64; 2],
36    pointer: [f64; 2],
37    original: Vec<K>,
38    axis: ReorderAxis,
39}
40
41struct EdgeScroll {
42    handle: ScrollHandle,
43    viewport: MotionAnchor,
44    policy: AutoScroll,
45    last: Option<Instant>,
46}
47
48type Change<K> = Rc<dyn Fn(&[K], &mut Window, &mut App)>;
49
50struct Data<K> {
51    order: Vec<K>,
52    metrics: HashMap<K, Metrics>,
53    grab: Option<Grab<K>>,
54    mode: ReorderAxis,
55    epoch: Option<Instant>,
56    change: Option<Change<K>>,
57    rects: Vec<LayoutRect>,
58    indices: Vec<usize>,
59    scroll: Option<EdgeScroll>,
60    scroll_pending: bool,
61    owner: Option<EntityId>,
62    touch: bool,
63    layout_ready: bool,
64}
65
66fn rect(bounds: Bounds<Pixels>, offset: [f64; 2]) -> LayoutRect {
67    LayoutRect::new(
68        [
69            f64::from(bounds.origin.x) + offset[0],
70            f64::from(bounds.origin.y) + offset[1],
71        ],
72        [f64::from(bounds.size.width), f64::from(bounds.size.height)],
73    )
74    .expect("finite native bounds")
75}
76
77impl<K: Clone + Eq + Hash> Data<K> {
78    fn refresh(&mut self, offset: [f64; 2]) {
79        self.rects.clear();
80        self.indices.clear();
81
82        for key in &self.order {
83            if let Some(metric) = self.metrics.get(key).filter(|metric| metric.measured) {
84                self.rects.push(rect(metric.bounds, offset));
85                self.indices.push(metric.slot);
86            }
87        }
88    }
89
90    fn begin(&mut self, key: &K, pointer: [f64; 2], window: &mut Window, cx: &mut App) -> bool {
91        if self.grab.is_some() || !self.layout_ready || !window.is_window_active() {
92            return false;
93        }
94
95        let Some(metric) = self.metrics.get(key).filter(|metric| metric.measured) else {
96            return false;
97        };
98
99        let origin = rect(metric.displayed, [0.0; 2]).origin();
100        let focus = metric.focus.clone();
101
102        self.refresh([0.0; 2]);
103        self.grab = Some(Grab {
104            key: key.clone(),
105            pointer,
106            offset: std::array::from_fn(|i| pointer[i] - origin[i]),
107            original: self.order.clone(),
108            axis: self.mode.resolve(&self.rects),
109        });
110        self.touch = false;
111
112        if let Some(scroll) = &mut self.scroll {
113            scroll.last = Some(cx.background_executor().now());
114        }
115
116        focus.focus(window, cx);
117
118        true
119    }
120
121    fn cancel(&mut self) {
122        if let Some(grab) = self.grab.take() {
123            self.order = grab.original;
124        }
125
126        self.touch = false;
127
128        if let Some(scroll) = &mut self.scroll {
129            scroll.last = None;
130        }
131    }
132
133    fn reorder(&mut self, scroll_delta: [f64; 2]) -> bool {
134        let Some(grab) = &self.grab else {
135            return false;
136        };
137
138        let from = self
139            .order
140            .iter()
141            .position(|key| *key == grab.key)
142            .expect("retained key");
143
144        let axis = grab.axis;
145
146        let Some(metric) = self.metrics.get(&grab.key) else {
147            return false;
148        };
149
150        let size = rect(metric.bounds, [0.0; 2]).size();
151        let center = std::array::from_fn(|i| grab.pointer[i] - grab.offset[i] + size[i] * 0.5);
152
153        self.refresh(scroll_delta);
154
155        let Some(destination) = axis
156            .destination(center, &self.rects)
157            .map(|index| self.indices[index])
158        else {
159            return false;
160        };
161
162        if from == destination {
163            return false;
164        }
165
166        move_item(&mut self.order, from, destination).expect("measured indices");
167
168        true
169    }
170
171    fn keyboard(&mut self, key: &K, direction: &str) -> Option<(usize, usize)> {
172        if !self.layout_ready {
173            return None;
174        }
175
176        self.refresh([0.0; 2]);
177        let mode = self.mode.resolve(&self.rects);
178
179        let (axis, sign) = match direction {
180            "left" => (0, -1.0),
181            "right" => (0, 1.0),
182            "up" => (1, -1.0),
183            "down" => (1, 1.0),
184            _ => return None,
185        };
186
187        if (mode == ReorderAxis::X && axis == 1) || (mode == ReorderAxis::Y && axis == 0) {
188            return None;
189        }
190
191        let from = self.order.iter().position(|old| old == key)?;
192        let origin_slot = self.indices.iter().position(|slot| *slot == from)?;
193        let origin = self.rects[origin_slot].center();
194
195        let destination = self
196            .rects
197            .iter()
198            .enumerate()
199            .filter_map(|(i, rect)| {
200                let center = rect.center();
201                let forward = (center[axis] - origin[axis]) * sign;
202
203                (forward > 0.01).then_some((
204                    self.indices[i],
205                    forward + (center[1 - axis] - origin[1 - axis]).abs() * 2.0,
206                ))
207            })
208            .min_by(|a, b| a.1.total_cmp(&b.1))
209            .map(|value| value.0)?;
210
211        Some((from, destination))
212    }
213
214    fn scroll_velocity(&self) -> [f64; 2] {
215        let Some(grab) = &self.grab else {
216            return [0.0; 2];
217        };
218
219        let Some(scroll) = &self.scroll else {
220            return [0.0; 2];
221        };
222
223        let Some(viewport) = scroll.viewport.rect() else {
224            return [0.0; 2];
225        };
226
227        let offset = scroll.handle.offset();
228        let max = scroll.handle.max_offset();
229
230        std::array::from_fn(|i| {
231            if (grab.axis == ReorderAxis::X && i == 1) || (grab.axis == ReorderAxis::Y && i == 0) {
232                return 0.0;
233            }
234
235            let velocity =
236                scroll
237                    .policy
238                    .velocity(grab.pointer[i], viewport.origin()[i], viewport.size()[i]);
239
240            let current = if i == 0 {
241                -f64::from(offset.x)
242            } else {
243                -f64::from(offset.y)
244            };
245
246            let extent = if i == 0 {
247                f64::from(max.x).abs()
248            } else {
249                f64::from(max.y).abs()
250            };
251
252            if (velocity < 0.0 && current > 0.01) || (velocity > 0.0 && current < extent - 0.01) {
253                velocity
254            } else {
255                0.0
256            }
257        })
258    }
259}
260
261/// Retained logical order and measured spatial geometry. Mouse/keyboard input,
262/// cancellation and optional edge autoscroll share the portable engine. Automatic
263/// axis detection occurs on acquisition. Host owns model persistence and focus
264/// restoration after removal. Cross-list transfer and virtualization are separate
265/// application policies; keep this group's mounted geometry in one scroll source.
266#[derive(Clone)]
267pub struct ReorderGroup<K>(Rc<RefCell<Data<K>>>);
268
269impl<K: Clone + Eq + Hash + 'static> ReorderGroup<K> {
270    /// Creates a compatible horizontal/vertical group; duplicate keys fail without mutation.
271    pub fn new(order: Vec<K>, axis: Axis) -> Result<Self, AnimationError> {
272        Self::spatial(
273            order,
274            match axis {
275                Axis::X => ReorderAxis::X,
276                Axis::Y => ReorderAxis::Y,
277            },
278        )
279    }
280
281    /// Creates an automatically detected row/column/grid or explicit spatial mode.
282    pub fn spatial(order: Vec<K>, mode: ReorderAxis) -> Result<Self, AnimationError> {
283        if order
284            .iter()
285            .enumerate()
286            .any(|(i, key)| order[..i].contains(key))
287        {
288            return Err(AnimationError::Index);
289        }
290
291        let capacity = order.len();
292
293        Ok(Self(Rc::new(RefCell::new(Data {
294            order,
295            metrics: HashMap::new(),
296            grab: None,
297            mode,
298            epoch: None,
299            change: None,
300            rects: Vec::with_capacity(capacity),
301            indices: Vec::with_capacity(capacity),
302            scroll: None,
303            scroll_pending: false,
304            owner: None,
305            touch: false,
306            layout_ready: false,
307        }))))
308    }
309
310    /// Enables elapsed-time edge scrolling in a measured native viewport. Pointer
311    /// coordinates remain fixed while scrolled geometry participates in reorder.
312    /// After a stalled frame, one tick is capped to 50ms to avoid a scroll jump.
313    pub fn autoscroll(
314        self,
315        handle: ScrollHandle,
316        viewport: MotionAnchor,
317        policy: AutoScroll,
318    ) -> Self {
319        self.0.borrow_mut().scroll = Some(EdgeScroll {
320            handle,
321            viewport,
322            policy,
323            last: None,
324        });
325
326        self
327    }
328
329    /// Current order. Release the borrow before sending input or changing membership.
330    pub fn order(&self) -> Ref<'_, [K]> {
331        Ref::map(self.0.borrow(), |data| data.order.as_slice())
332    }
333
334    /// Invokes after pointer commit or keyboard reorder, outside all group borrows.
335    /// Capture application entities weakly to avoid a retained self-reference cycle.
336    pub fn on_change(self, callback: impl Fn(&[K], &mut Window, &mut App) + 'static) -> Self {
337        self.0.borrow_mut().change = Some(Rc::new(callback));
338
339        self
340    }
341
342    /// Changes external membership/order and cancels a held gesture. Duplicate keys
343    /// fail transactionally. Use on model changes, not on every render.
344    pub fn set_order(&self, order: Vec<K>) -> Result<(), AnimationError> {
345        if order
346            .iter()
347            .enumerate()
348            .any(|(i, key)| order[..i].contains(key))
349        {
350            return Err(AnimationError::Index);
351        }
352
353        let mut data = self.0.borrow_mut();
354
355        if data.grab.is_none() && data.order == order {
356            return Ok(());
357        }
358
359        data.cancel();
360        data.layout_ready = false;
361        data.metrics.retain(|key, _| order.contains(key));
362        let extra_rects = order.len().saturating_sub(data.rects.len());
363        let extra_indices = order.len().saturating_sub(data.indices.len());
364
365        data.rects.reserve(extra_rects);
366        data.indices.reserve(extra_indices);
367        data.order = order;
368
369        Ok(())
370    }
371
372    /// Starts dragging from an external handle after the item was measured.
373    pub fn start_drag(
374        &self,
375        key: &K,
376        pointer: [f64; 2],
377        window: &mut Window,
378        cx: &mut App,
379    ) -> bool {
380        assert!(
381            pointer.iter().all(|v| v.is_finite()),
382            "invalid reorder pointer"
383        );
384        let mut data = self.0.borrow_mut();
385        let started = data.begin(key, pointer, window, cx);
386
387        if started && let Some(owner) = data.owner {
388            cx.notify(owner);
389        }
390
391        started
392    }
393
394    /// Rolls back the current pointer gesture without firing a commit callback.
395    pub fn cancel(&self, cx: &mut App) {
396        let mut data = self.0.borrow_mut();
397
398        data.cancel();
399
400        if let Some(owner) = data.owner {
401            cx.notify(owner);
402        }
403    }
404
405    /// Whether a pointer currently owns the group.
406    pub fn is_dragging(&self) -> bool {
407        self.0.borrow().grab.is_some()
408    }
409
410    /// Wraps one stable-key item. Child semantics, styles and normal click handlers
411    /// remain native. Use `drag_listener(false)` when providing a separate handle.
412    pub fn item<E: IntoElement + StatefulInteractiveElement + 'static>(
413        &self,
414        id: impl Into<ElementId>,
415        key: K,
416        child: E,
417    ) -> ReorderElement<K, E> {
418        let slot = self
419            .0
420            .borrow()
421            .order
422            .iter()
423            .position(|old| old == &key)
424            .expect("unknown reorder key");
425
426        ReorderElement {
427            group: self.clone(),
428            id: id.into(),
429            key,
430            slot,
431            child: Some(child),
432            transition: Transition::default(),
433            listener: true,
434        }
435    }
436
437    fn finish(&self, key: &K, window: &mut Window, cx: &mut App) {
438        let mut data = self.0.borrow_mut();
439
440        if !data.grab.as_ref().is_some_and(|grab| grab.key == *key) {
441            return;
442        }
443
444        let changed = data.grab.take().expect("owned grab").original != data.order;
445
446        data.touch = false;
447
448        if let Some(scroll) = &mut data.scroll {
449            scroll.last = None;
450        }
451
452        let callback = if changed {
453            data.change
454                .clone()
455                .map(|callback| (callback, data.order.clone()))
456        } else {
457            None
458        };
459
460        let owner = data.owner;
461
462        drop(data);
463
464        if let Some((callback, order)) = callback {
465            callback(&order, window, cx);
466        }
467
468        if let Some(owner) = owner {
469            cx.notify(owner);
470        }
471    }
472}
473
474/// Native spatial reorder item.
475pub struct ReorderElement<K, E = Stateful<Div>> {
476    group: ReorderGroup<K>,
477    id: ElementId,
478    key: K,
479    slot: usize,
480    child: Option<E>,
481    transition: Transition,
482    listener: bool,
483}
484
485impl<K, E> ReorderElement<K, E> {
486    /// Changes sibling/release interpolation without resetting measured identity.
487    pub fn transition(mut self, transition: Transition) -> Self {
488        self.transition = transition;
489
490        self
491    }
492
493    /// Uses external drag acquisition while preserving keyboard reorder controls.
494    pub fn drag_listener(mut self, enabled: bool) -> Self {
495        self.listener = enabled;
496
497        self
498    }
499}
500
501impl<K: Clone + Eq + Hash + 'static, E: IntoElement + StatefulInteractiveElement + 'static>
502    IntoElement for ReorderElement<K, E>
503{
504    type Element = Self;
505
506    fn into_element(self) -> Self {
507        self
508    }
509}
510
511impl<K: Clone + Eq + Hash + 'static, E: IntoElement + StatefulInteractiveElement + 'static> Element
512    for ReorderElement<K, E>
513{
514    type RequestLayoutState = AnyElement;
515    type PrepaintState = Hitbox;
516
517    fn id(&self) -> Option<ElementId> {
518        Some(self.id.clone())
519    }
520
521    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
522        None
523    }
524
525    fn request_layout(
526        &mut self,
527        _: Option<&GlobalElementId>,
528        _: Option<&InspectorElementId>,
529        window: &mut Window,
530        cx: &mut App,
531    ) -> (LayoutId, AnyElement) {
532        let mut data = self.group.0.borrow_mut();
533
534        data.owner = Some(window.current_view());
535        data.epoch
536            .get_or_insert_with(|| cx.background_executor().now());
537
538        if !window.is_window_active()
539            || data.grab.as_ref().is_some_and(|grab| {
540                data.metrics
541                    .get(&grab.key)
542                    .is_some_and(|metric| !metric.focus.is_focused(window))
543            })
544        {
545            let was_dragging = data.grab.is_some();
546
547            data.cancel();
548
549            if was_dragging {
550                window.request_animation_frame();
551            }
552        }
553
554        let metric = data
555            .metrics
556            .entry(self.key.clone())
557            .or_insert_with(|| Metrics {
558                bounds: Bounds::default(),
559                displayed: Bounds::default(),
560                values: [MotionValue::new(0.0), MotionValue::new(0.0)],
561                focus: cx.focus_handle().tab_stop(true),
562                measured: false,
563                slot: 0,
564            });
565
566        let focus = metric.focus.clone();
567
568        drop(data);
569        let mut child = self
570            .child
571            .take()
572            .expect("one layout")
573            .track_focus(&focus)
574            .tab_index(0)
575            .into_any_element();
576
577        let layout = child.request_layout(window, cx);
578
579        (layout, child)
580    }
581
582    fn prepaint(
583        &mut self,
584        _: Option<&GlobalElementId>,
585        _: Option<&InspectorElementId>,
586        bounds: Bounds<Pixels>,
587        child: &mut AnyElement,
588        window: &mut Window,
589        cx: &mut App,
590    ) -> Hitbox {
591        let mut data = self.group.0.borrow_mut();
592
593        data.layout_ready = true;
594
595        let now = cx
596            .background_executor()
597            .now()
598            .saturating_duration_since(data.epoch.expect("layout epoch"));
599
600        let raw = bounds.origin - window.pixel_snap_point(window.element_offset());
601        let target = [f64::from(raw.x), f64::from(raw.y)];
602
603        let drag = data
604            .grab
605            .as_ref()
606            .filter(|grab| grab.key == self.key)
607            .map(|grab| {
608                (
609                    std::array::from_fn::<_, 2, _>(|i| grab.pointer[i] - grab.offset[i]),
610                    grab.axis,
611                )
612            });
613
614        let metric = data.metrics.get_mut(&self.key).expect("measured key");
615        let first = !metric.measured;
616
617        metric.measured = true;
618        metric.slot = self.slot;
619        metric.bounds = bounds;
620        let mut presented = target;
621
622        for i in 0..2 {
623            if first {
624                metric.values[i].jump(target[i], now);
625            }
626
627            if let Some((position, axis)) = drag
628                && (axis == ReorderAxis::XY
629                    || (axis == ReorderAxis::X && i == 0)
630                    || (axis == ReorderAxis::Y && i == 1))
631            {
632                let offset = if i == 0 {
633                    f64::from(window.element_offset().x)
634                } else {
635                    f64::from(window.element_offset().y)
636                };
637
638                metric.values[i].set(position[i] - offset, now);
639            } else {
640                metric.values[i].animate(target[i], self.transition, now);
641            }
642
643            let frame = metric.values[i].sample(now, cx.reduce_motion());
644
645            presented[i] = frame.value();
646
647            if frame.is_animating() {
648                window.request_animation_frame();
649            }
650        }
651
652        let offset = point(
653            px((presented[0] - target[0]) as f32),
654            px((presented[1] - target[1]) as f32),
655        );
656
657        let displayed = Bounds {
658            origin: bounds.origin + offset,
659            size: bounds.size,
660        };
661
662        metric.displayed = displayed;
663        drop(data);
664        let hitbox = window.insert_hitbox(displayed, HitboxBehavior::Normal);
665
666        window.with_element_offset(offset, |window| {
667            child.prepaint(window, cx);
668        });
669        schedule_scroll(&self.group, &self.key, window, cx);
670
671        hitbox
672    }
673
674    fn paint(
675        &mut self,
676        _: Option<&GlobalElementId>,
677        _: Option<&InspectorElementId>,
678        _: Bounds<Pixels>,
679        child: &mut AnyElement,
680        hitbox: &mut Hitbox,
681        window: &mut Window,
682        cx: &mut App,
683    ) {
684        let group = self.group.clone();
685        let key = self.key.clone();
686        let region = hitbox.clone();
687        let listener = self.listener;
688
689        window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
690            if listener
691                && phase == DispatchPhase::Bubble
692                && event.button == MouseButton::Left
693                && region.is_hovered(window)
694                && group.start_drag(
695                    &key,
696                    [f64::from(event.position.x), f64::from(event.position.y)],
697                    window,
698                    cx,
699                )
700            {
701                cx.stop_propagation();
702            }
703        });
704        let group = self.group.clone();
705        let key = self.key.clone();
706
707        window.on_mouse_event(move |event: &MouseMoveEvent, phase, _, cx| {
708            if phase != DispatchPhase::Capture {
709                return;
710            }
711
712            let mut data = group.0.borrow_mut();
713
714            if data.touch || !data.grab.as_ref().is_some_and(|grab| grab.key == key) {
715                return;
716            }
717
718            if event.pressed_button != Some(MouseButton::Left) {
719                data.cancel();
720            } else {
721                data.grab.as_mut().expect("owned grab").pointer =
722                    [f64::from(event.position.x), f64::from(event.position.y)];
723                data.reorder([0.0; 2]);
724            }
725
726            if let Some(owner) = data.owner {
727                cx.notify(owner);
728            }
729        });
730        let group = self.group.clone();
731        let key = self.key.clone();
732
733        window.on_mouse_event(move |event: &MouseUpEvent, phase, window, cx| {
734            if phase == DispatchPhase::Capture
735                && event.button == MouseButton::Left
736                && !group.0.borrow().touch
737            {
738                group.finish(&key, window, cx);
739            }
740        });
741        let group = self.group.clone();
742        let key = self.key.clone();
743
744        window.on_key_event(move |event: &KeyDownEvent, phase, window, cx| {
745            if phase != DispatchPhase::Capture {
746                return;
747            }
748
749            let mut data = group.0.borrow_mut();
750
751            if !data
752                .metrics
753                .get(&key)
754                .is_some_and(|metric| metric.focus.is_focused(window))
755            {
756                return;
757            }
758
759            if event.keystroke.key == "escape" {
760                data.cancel();
761
762                if let Some(owner) = data.owner {
763                    cx.notify(owner);
764                }
765
766                return;
767            }
768
769            if !event.keystroke.modifiers.alt {
770                return;
771            }
772
773            let Some((from, to)) = data.keyboard(&key, event.keystroke.key.as_str()) else {
774                return;
775            };
776
777            if from == to {
778                return;
779            }
780
781            move_item(&mut data.order, from, to).expect("measured order");
782            let callback = data
783                .change
784                .clone()
785                .map(|callback| (callback, data.order.clone()));
786
787            let owner = data.owner;
788
789            drop(data);
790
791            if let Some((callback, order)) = callback {
792                callback(&order, window, cx);
793            }
794
795            if let Some(owner) = owner {
796                cx.notify(owner);
797            }
798
799            cx.stop_propagation();
800        });
801        // Input listeners precede child paint so focused descendants retain correct dispatch ownership.
802        child.paint(window, cx);
803    }
804}
805
806fn schedule_scroll<K: Clone + Eq + Hash + 'static>(
807    group: &ReorderGroup<K>,
808    key: &K,
809    window: &mut Window,
810    cx: &App,
811) {
812    let mut data = group.0.borrow_mut();
813
814    if data.scroll_pending
815        || !data.grab.as_ref().is_some_and(|grab| grab.key == *key)
816        || data.scroll_velocity() == [0.0; 2]
817    {
818        return;
819    }
820
821    data.scroll_pending = true;
822
823    if let Some(scroll) = &mut data.scroll {
824        scroll
825            .last
826            .get_or_insert_with(|| cx.background_executor().now());
827    }
828
829    drop(data);
830    let weak = Rc::downgrade(&group.0);
831
832    window.on_next_frame(move |window, cx| {
833        let Some(group) = weak.upgrade() else {
834            return;
835        };
836
837        let mut data = group.borrow_mut();
838
839        data.scroll_pending = false;
840
841        if !window.is_window_active()
842            || data.grab.as_ref().is_some_and(|grab| {
843                data.metrics
844                    .get(&grab.key)
845                    .is_some_and(|metric| !metric.focus.is_focused(window))
846            })
847        {
848            data.cancel();
849
850            return;
851        }
852
853        let velocity = data.scroll_velocity();
854
855        if velocity == [0.0; 2] {
856            return;
857        }
858
859        let scroll = data.scroll.as_mut().expect("scroll policy");
860        let now = cx.background_executor().now();
861
862        let dt = now
863            .saturating_duration_since(scroll.last.replace(now).unwrap_or(now))
864            .min(Duration::from_millis(50))
865            .as_secs_f64();
866
867        let old = scroll.handle.offset();
868        let max = scroll.handle.max_offset();
869
870        let next = point(
871            px((f64::from(old.x) - velocity[0] * dt).clamp(-f64::from(max.x).abs(), 0.0) as f32),
872            px((f64::from(old.y) - velocity[1] * dt).clamp(-f64::from(max.y).abs(), 0.0) as f32),
873        );
874
875        scroll.handle.set_offset(next);
876        data.reorder([f64::from(next.x - old.x), f64::from(next.y - old.y)]);
877
878        if let Some(owner) = data.owner {
879            cx.notify(owner);
880        }
881    });
882}