Skip to main content

telar_ui_core/
drag.rs

1use std::cell::Cell;
2
3use geometry_core::Rect;
4use platform_core::{Event, ModifiersState, PointerButton};
5use ui_tree::EventResult;
6
7use crate::pointer::PointerButtons;
8
9/// What armed a drag: the button pressed, and what was held down at that moment.
10///
11/// Frozen at the press, and that is the whole of it. [`modifiers`](crate::modifiers) answers what is held
12/// *now*, so a mode read from it mid-stroke would change under a hand that let go of Shift — turning an orbit
13/// into a pan halfway through. A gesture chooses what it is once, when it starts, and is measured from there.
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub struct DragStart {
16    pub button: PointerButton,
17    pub modifiers: ModifiersState,
18}
19
20thread_local! {
21    /// What armed the drag whose callback is currently running, and how far it has travelled. Ambient rather
22    /// than a callback parameter for the reason [`PointerButtons`] gives: widening `on_drag` would make the
23    /// whole catalogue pay for a question two widgets ask.
24    static ACTIVE: Cell<Option<(DragStart, f32)>> = const { Cell::new(None) };
25}
26
27/// What armed the drag whose callback is running, or `None` outside one.
28///
29/// The button-and-modifier half of mode dispatch: a viewport reads this once and knows whether this stroke is
30/// an orbit, a pan or a dolly — without every drag callback in the catalogue growing a parameter for it.
31pub fn drag_start() -> Option<DragStart> {
32    ACTIVE.with(|a| a.get()).map(|(start, _)| start)
33}
34
35/// How far the drag whose callback is running has been from its press point, at its furthest.
36///
37/// The number a click-versus-drag decision is read against when the widget wants to make it itself rather
38/// than hand it to [`DragGesture`'s threshold](crate::StyledContainer::drag_threshold).
39pub fn drag_travel() -> f32 {
40    ACTIVE.with(|a| a.get()).map_or(0.0, |(_, travel)| travel)
41}
42
43/// A drag gesture any container can opt into: reports the pointer position on a press inside its bounds
44/// and on every move until release. Because pointer events are broadcast to every widget, a drag keeps
45/// receiving moves even after the pointer leaves the widget's bounds — no explicit pointer capture is
46/// needed. Coordinates are reported *local to the widget* (relative to its rect origin), so a slider maps
47/// `x / width` to a value regardless of where the widget sits; they can go negative or exceed the size
48/// once the pointer leaves the bounds.
49pub(crate) struct DragGesture {
50    on_drag: Option<Box<dyn Fn(f32, f32)>>,
51    on_drag_end: Option<Box<dyn Fn(f32, f32)>>,
52    /// Which buttons may start it. The primary one alone by default, which is every slider and splitter in
53    /// the catalogue; a viewport widens it, because a modeller pans with the button the OS calls secondary.
54    arms: PointerButtons,
55    /// Where the press landed (widget-local) and what armed it, for as long as the gesture is live.
56    origin: Option<((f32, f32), DragStart)>,
57    /// How far the pointer must travel before this counts as a drag at all.
58    ///
59    /// `0.0` (the default) reports from the press, which is what a slider wants: pressing the track *is*
60    /// setting the value, and waiting for movement would make the first click do nothing. A viewport wants the
61    /// other reading, where a press that never travelled was a click on whatever sits under it. Both are
62    /// legitimate, so it is the caller's to say — and the widget that says nothing keeps what it always had.
63    threshold: f32,
64    /// The furthest the pointer has been from the press.
65    travel: f32,
66    /// Whether the threshold has been cleared, so the callbacks are running.
67    started: bool,
68    /// The last position the drag reported, so an end with no event of its own still knows where it got to.
69    last: (f32, f32),
70}
71
72impl Default for DragGesture {
73    fn default() -> Self {
74        Self {
75            on_drag: None,
76            on_drag_end: None,
77            arms: PointerButtons {
78                primary: true,
79                ..PointerButtons::default()
80            },
81            origin: None,
82            threshold: 0.0,
83            travel: 0.0,
84            started: false,
85            last: (0.0, 0.0),
86        }
87    }
88}
89
90impl DragGesture {
91    pub(crate) fn set(&mut self, f: impl Fn(f32, f32) + 'static) {
92        self.on_drag = Some(Box::new(f));
93    }
94
95    pub(crate) fn set_end(&mut self, f: impl Fn(f32, f32) + 'static) {
96        self.on_drag_end = Some(Box::new(f));
97    }
98
99    pub(crate) fn arm_with(&mut self, button: &PointerButton) {
100        self.arms = self.arms.with(button);
101    }
102
103    /// Whether `button` may start this drag.
104    pub(crate) fn arms(&self, button: &PointerButton) -> bool {
105        self.arms.holds(button)
106    }
107
108    pub(crate) fn is_set(&self) -> bool {
109        self.on_drag.is_some() || self.on_drag_end.is_some()
110    }
111
112    pub(crate) fn set_threshold(&mut self, px: f32) {
113        self.threshold = px.max(0.0);
114    }
115
116    /// Whether the gesture has cleared its threshold and is reporting. A widget with both a tap and a
117    /// thresholded drag uses this to drop the tap once the stroke has committed to being a drag.
118    pub(crate) fn has_started(&self) -> bool {
119        self.started
120    }
121
122    pub(crate) fn has_threshold(&self) -> bool {
123        self.threshold > 0.0
124    }
125
126    /// A press inside `rect` with a button this gesture arms starts the drag and reports the press point.
127    /// Returns `Handled` when it starts, so the press is consumed (as with a tap).
128    ///
129    /// With a threshold set, the press *arms* the gesture without reporting: nothing has travelled yet, so
130    /// nothing has been dragged.
131    pub(crate) fn press(&mut self, event: &Event, rect: Rect) -> EventResult {
132        if let Event::PointerPressed { x, y, button, .. } = event
133            && self.arms(button)
134            && rect.contains(*x as f32, *y as f32)
135        {
136            let local = (*x as f32 - rect.x, *y as f32 - rect.y);
137            self.origin = Some((
138                local,
139                DragStart {
140                    button: *button,
141                    modifiers: crate::keyboard::modifiers(),
142                },
143            ));
144            self.travel = 0.0;
145            self.started = !self.has_threshold();
146            if self.started {
147                self.report(local.0, local.1);
148            }
149            return EventResult::Handled;
150        }
151        EventResult::Ignored
152    }
153
154    /// While a drag is active, reports each move (local to `rect`). Returns `Handled` so it is consumed —
155    /// and `Ignored` while the gesture is armed but has not travelled far enough to be a drag yet.
156    pub(crate) fn moved(&mut self, event: &Event, rect: Rect) -> EventResult {
157        let (Some((press, _)), Event::PointerMoved { x, y, .. }) = (self.origin, event) else {
158            return EventResult::Ignored;
159        };
160        let local = (*x as f32 - rect.x, *y as f32 - rect.y);
161        let (dx, dy) = (local.0 - press.0, local.1 - press.1);
162        self.travel = self.travel.max(dx.hypot(dy));
163        // The drag begins *here* and not back at the press: reporting the press point retroactively would jump
164        // whatever is being dragged by the slop distance the moment it started moving.
165        self.started |= self.travel > self.threshold;
166        if !self.started {
167            return EventResult::Ignored;
168        }
169        self.report(local.0, local.1);
170        EventResult::Handled
171    }
172
173    fn report(&mut self, x: f32, y: f32) {
174        self.last = (x, y);
175        let Some((_, start)) = self.origin else {
176            return;
177        };
178        if let Some(cb) = &self.on_drag {
179            in_drag(start, self.travel, || cb(x, y));
180        }
181    }
182
183    /// Ends the drag (on release, or when the pointer leaves the window) and fires `on_drag_end` with where
184    /// it finished. Returns whether one was active, so the caller can consume the release that ended it.
185    ///
186    /// `at` is the release position when the caller has one. The fallback matters: a drag also ends on
187    /// `CursorLeft`, and on a child consuming the release, neither of which carries a position — reporting the
188    /// last place the drag actually reached is the only answer that is true in all three cases.
189    /// A gesture that never cleared its threshold ends silently and answers `false`: nothing was dragged, so
190    /// the release belongs to whatever else the widget arms — which is how a click and a drag on one button
191    /// stop being ambiguous.
192    pub(crate) fn end(&mut self, at: Option<(f32, f32)>) -> bool {
193        let Some((_, start)) = self.origin.take() else {
194            return false;
195        };
196        let was_dragging = std::mem::take(&mut self.started);
197        if was_dragging {
198            let (x, y) = at.unwrap_or(self.last);
199            self.last = (x, y);
200            if let Some(cb) = &self.on_drag_end {
201                in_drag(start, self.travel, || cb(x, y));
202            }
203        }
204        was_dragging
205    }
206}
207
208/// Runs `f` with [`drag_start`] and [`drag_travel`] answering for this gesture. The previous value is put back
209/// rather than cleared, so a drag callback that builds a widget which drags in turn does not blank the outer.
210fn in_drag<R>(start: DragStart, travel: f32, f: impl FnOnce() -> R) -> R {
211    let outer = ACTIVE.with(|a| a.replace(Some((start, travel))));
212    let out = f();
213    ACTIVE.with(|a| a.set(outer));
214    out
215}
216
217#[cfg(test)]
218mod tests {
219    use std::cell::RefCell;
220    use std::rc::Rc;
221
222    use platform_core::PointerSource;
223
224    use super::*;
225
226    const RECT: Rect = Rect {
227        x: 0.0,
228        y: 0.0,
229        width: 100.0,
230        height: 100.0,
231    };
232
233    fn press_at(x: f32, y: f32) -> Event {
234        Event::PointerPressed {
235            x: x as f64,
236            y: y as f64,
237            button: PointerButton::Primary,
238            source: PointerSource::Mouse,
239        }
240    }
241
242    fn move_to(x: f32, y: f32) -> Event {
243        Event::PointerMoved {
244            x: x as f64,
245            y: y as f64,
246            source: PointerSource::Mouse,
247        }
248    }
249
250    /// Records every position the gesture reported, and what it said armed the stroke at the time.
251    type Log = Rc<RefCell<Vec<((f32, f32), Option<DragStart>)>>>;
252
253    fn logging(threshold: f32) -> (DragGesture, Log) {
254        let log: Log = Rc::new(RefCell::new(Vec::new()));
255        let mut drag = DragGesture::default();
256        drag.set_threshold(threshold);
257        let sink = log.clone();
258        drag.set(move |x, y| sink.borrow_mut().push(((x, y), drag_start())));
259        (drag, log)
260    }
261
262    /// The default, which a slider depends on: pressing the track *is* setting the value, so the press itself
263    /// reports and waiting for movement would make the first click do nothing.
264    #[test]
265    fn without_a_threshold_the_press_itself_reports() {
266        let (mut drag, log) = logging(0.0);
267        assert_eq!(
268            drag.press(&press_at(30.0, 40.0), RECT),
269            EventResult::Handled
270        );
271        assert_eq!(log.borrow().len(), 1, "the press reported straight away");
272        assert_eq!(log.borrow()[0].0, (30.0, 40.0));
273    }
274
275    /// And the reading a viewport needs: a stroke that never travelled was a click on whatever sits under it,
276    /// not a drag of nothing. `end` answering `false` is what leaves the release to the tap gesture.
277    #[test]
278    fn a_press_that_never_travels_is_not_a_drag() {
279        let (mut drag, log) = logging(4.0);
280        drag.press(&press_at(30.0, 40.0), RECT);
281        drag.moved(&move_to(32.0, 41.0), RECT);
282        assert!(log.borrow().is_empty(), "two pixels is not a drag");
283        assert!(!drag.end(None), "so nothing was dragged to end");
284    }
285
286    /// Crossing the threshold starts the drag *where it crossed*. Reporting the press point retroactively
287    /// would jump whatever is being dragged by the slop distance the instant it started moving.
288    #[test]
289    fn crossing_the_threshold_starts_the_drag_where_it_crossed() {
290        let (mut drag, log) = logging(4.0);
291        drag.press(&press_at(30.0, 40.0), RECT);
292        drag.moved(&move_to(32.0, 40.0), RECT);
293        drag.moved(&move_to(50.0, 40.0), RECT);
294
295        assert_eq!(log.borrow().len(), 1, "only the move that cleared it");
296        assert_eq!(log.borrow()[0].0, (50.0, 40.0), "and not back at the press");
297        assert!(drag.end(None), "this one really was a drag");
298    }
299
300    /// Mode dispatch, and the reason it is frozen: a hand that lets go of Shift halfway through would turn an
301    /// orbit into a pan mid-stroke if the gesture asked what is held *now*.
302    #[test]
303    fn a_drag_reports_what_armed_it_and_not_what_is_held_now() {
304        crate::keyboard::reset();
305        crate::keyboard::observe(&Event::ModifiersChanged {
306            modifiers: ModifiersState {
307                is_shift: true,
308                ..Default::default()
309            },
310        });
311        let (mut drag, log) = logging(0.0);
312        drag.press(&press_at(10.0, 10.0), RECT);
313
314        crate::keyboard::observe(&Event::ModifiersChanged {
315            modifiers: ModifiersState::default(),
316        });
317        drag.moved(&move_to(40.0, 10.0), RECT);
318        crate::keyboard::reset();
319
320        let entries = log.borrow();
321        assert!(
322            entries.iter().all(|(_, start)| start
323                .is_some_and(|s| s.modifiers.is_shift && s.button == PointerButton::Primary)),
324            "every report names the press, including the one after Shift was released: {entries:?}"
325        );
326        assert_eq!(
327            drag_start(),
328            None,
329            "and nothing leaks out of the callback it was scoped to"
330        );
331    }
332}