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}