Skip to main content

blockworx_kernel/
pointer.rs

1//! The pointer, resolved: raw motions and button edges in, one
2//! [`Event`] a frame out.
3//!
4//! What a click is, when a press becomes a drag and how close two clicks
5//! have to be to be one double-click are decided here, once, with the
6//! thresholds a desktop toolkit uses — so a host reports what its pointer
7//! did and never what it meant.
8
9use core::time::Duration;
10
11use blockworx_geom::{Pos2, Vec2};
12use blockworx_paint::{Button, Event, Input, Interaction, Press, Raw, Vantage};
13
14/// How far a press may travel and still be a click, in screen points.
15const MAX_CLICK_DIST: f32 = 6.0;
16/// How long a press may be held and still be a click.
17const MAX_CLICK_DURATION: Duration = Duration::from_millis(800);
18/// How soon after one click a second is a double-click.
19const MAX_DOUBLE_CLICK_DELAY: Duration = Duration::from_millis(300);
20
21/// The primary button held: where it went down, when, and what has been
22/// made of it since.
23#[derive(Clone, Copy, Debug)]
24struct Pressed {
25    origin: Pos2,
26    since: Duration,
27    /// Travelled past the click distance — from here on it is a drag or
28    /// nothing, never a click.
29    strayed: bool,
30    phase: Phase,
31}
32
33/// What a held press has become.
34#[derive(Clone, Copy, PartialEq, Eq, Debug)]
35enum Phase {
36    /// Held, and not yet a drag.
37    Held,
38    Dragging,
39    /// The host abandoned the drag; the press outlives it but drags no more.
40    Cancelled,
41}
42
43impl Pressed {
44    fn could_click(&self, now: Duration) -> bool {
45        !self.strayed && now.saturating_sub(self.since) <= MAX_CLICK_DURATION
46    }
47}
48
49#[derive(Clone, Copy, Debug)]
50struct Click {
51    at: Duration,
52    pos: Pos2,
53}
54
55/// Whether the pointer is over the canvas: a hover is reported only while
56/// it is.
57#[derive(Clone, Copy, PartialEq, Eq, Debug)]
58enum Over {
59    Canvas,
60    Elsewhere,
61}
62
63/// The state one pointer carries between frames.
64#[derive(Debug)]
65pub struct Resolver {
66    /// Where the pointer was last seen, while it is seen at all.
67    latest: Option<Pos2>,
68    /// Where it last acted — a position a gesture is reported at, kept
69    /// after the pointer is gone so a drag can end off the canvas.
70    acted: Option<Pos2>,
71    over: Over,
72    press: Option<Pressed>,
73    last_click: Option<Click>,
74}
75
76impl Default for Resolver {
77    fn default() -> Self {
78        Self {
79            latest: None,
80            acted: None,
81            over: Over::Elsewhere,
82            press: None,
83            last_click: None,
84        }
85    }
86}
87
88impl Resolver {
89    /// Where the pointer is, in screen space, while it is over the canvas.
90    pub fn latest(&self) -> Option<Pos2> {
91        self.latest
92    }
93
94    /// One frame: take the raw events in order and answer with what the
95    /// tools see, positions mapped into world space through `vantage` at
96    /// `origin`. `now` is the frame's clock, which dates a press and a click.
97    pub fn resolve(
98        &mut self,
99        input: &Input,
100        now: Duration,
101        vantage: Vantage,
102        origin: Pos2,
103    ) -> Interaction {
104        let before = self.latest;
105        let mut resolved = None;
106        let mut panning = false;
107        for raw in &input.raw {
108            match *raw {
109                Raw::Moved(pos) => {
110                    self.latest = Some(pos);
111                    self.acted = Some(pos);
112                    self.over = Over::Canvas;
113                    if let Some(press) = &mut self.press
114                        && press.origin.distance(pos) > MAX_CLICK_DIST
115                    {
116                        press.strayed = true;
117                    }
118                }
119                Raw::Down { pos, button } => {
120                    self.latest = Some(pos);
121                    self.acted = Some(pos);
122                    if button == Button::Primary {
123                        self.press = Some(Pressed {
124                            origin: pos,
125                            since: now,
126                            strayed: false,
127                            phase: Phase::Held,
128                        });
129                    }
130                }
131                Raw::Up { pos, button } => {
132                    self.latest = Some(pos);
133                    self.acted = Some(pos);
134                    if button == Button::Primary
135                        && let Some(press) = self.press.take()
136                    {
137                        resolved = self.released(press, pos, now);
138                    }
139                }
140                Raw::Gone => {
141                    self.latest = None;
142                    self.over = Over::Elsewhere;
143                    // A host reports a release only to whoever it gave the
144                    // press: a press the pointer leaves behind will never be
145                    // released here, so it ends now, where it stands.
146                    if let Some(press) = self.press.take()
147                        && press.phase == Phase::Dragging
148                    {
149                        resolved = self.acted.map(|pos| Event::DragStopped { pos });
150                    }
151                }
152                Raw::Cancelled => {
153                    if let Some(press) = &mut self.press {
154                        if press.phase == Phase::Dragging {
155                            resolved = self.acted.map(|pos| Event::DragStopped { pos });
156                        }
157                        press.phase = Phase::Cancelled;
158                    }
159                }
160                Raw::Panning => panning = true,
161            }
162        }
163        let mut event = resolved.or_else(|| self.dragged(before, now));
164        if event.is_none()
165            && self.over == Over::Canvas
166            && let Some(pos) = self.latest
167        {
168            event = Some(Event::HoverAt(pos));
169        }
170        let mut press = self.press.map(|press| Press {
171            origin: press.origin,
172        });
173        if panning {
174            if matches!(
175                event,
176                Some(
177                    Event::DragStarted { .. } | Event::Dragging { .. } | Event::DragStopped { .. }
178                )
179            ) {
180                event = None;
181            }
182            // A key-pan holds the primary button down, which would otherwise
183            // arm the press-and-hold affordances (route starts, new-pin
184            // markers).
185            press = None;
186        }
187        let to_world = |screen: Pos2| vantage.screen_to_world(origin, screen);
188        let event = event.map(|event| match event {
189            Event::HoverAt(pos) => Event::HoverAt(to_world(pos)),
190            Event::DragStarted { pos } => Event::DragStarted { pos: to_world(pos) },
191            Event::Dragging { pos, delta } => Event::Dragging {
192                pos: to_world(pos),
193                delta: delta / vantage.zoom.get(),
194            },
195            Event::DragStopped { pos } => Event::DragStopped { pos: to_world(pos) },
196            Event::Clicked { pos } => Event::Clicked { pos: to_world(pos) },
197            Event::DoubleClicked { pos } => Event::DoubleClicked { pos: to_world(pos) },
198        });
199        let keys = input.keys;
200        Interaction {
201            event,
202            press: press.map(|press| Press {
203                origin: to_world(press.origin),
204            }),
205            text: None,
206            escape_pressed: keys.escape,
207            delete_pressed: keys.delete,
208            shift: keys.shift,
209        }
210    }
211
212    /// What letting go comes to: a click, a double-click, the end of a
213    /// drag — or nothing, for a press that strayed in the frame it ended.
214    fn released(&mut self, press: Pressed, pos: Pos2, now: Duration) -> Option<Event> {
215        if press.could_click(now) {
216            let double = self.last_click.is_some_and(|last| {
217                now.saturating_sub(last.at) < MAX_DOUBLE_CLICK_DELAY
218                    && last.pos.distance_sq(pos) < MAX_CLICK_DIST * MAX_CLICK_DIST
219            });
220            self.last_click = Some(Click { at: now, pos });
221            return Some(if double {
222                Event::DoubleClicked { pos }
223            } else {
224                Event::Clicked { pos }
225            });
226        }
227        (press.phase == Phase::Dragging).then_some(Event::DragStopped { pos })
228    }
229
230    /// The drag a held press is, this frame: the frame it stops being a
231    /// possible click it starts, at the press's own origin; every frame
232    /// after, it moves by what the pointer moved since the last frame.
233    fn dragged(&mut self, before: Option<Pos2>, now: Duration) -> Option<Event> {
234        let press = self.press.as_mut()?;
235        match press.phase {
236            Phase::Dragging => {
237                let pos = self.acted?;
238                let delta = match (before, self.latest) {
239                    (Some(was), Some(is)) => is - was,
240                    _ => Vec2::ZERO,
241                };
242                Some(Event::Dragging { pos, delta })
243            }
244            Phase::Cancelled => None,
245            Phase::Held => {
246                if press.could_click(now) {
247                    return None;
248                }
249                press.phase = Phase::Dragging;
250                Some(Event::DragStarted { pos: press.origin })
251            }
252        }
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259    use blockworx_geom::pos2;
260    use blockworx_paint::Keys;
261
262    fn at(millis: u64) -> Duration {
263        Duration::from_millis(millis)
264    }
265
266    fn input(raw: Vec<Raw>) -> Input {
267        Input {
268            raw,
269            keys: Keys::default(),
270        }
271    }
272
273    fn down(pos: Pos2) -> Raw {
274        Raw::Down {
275            pos,
276            button: Button::Primary,
277        }
278    }
279
280    fn up(pos: Pos2) -> Raw {
281        Raw::Up {
282            pos,
283            button: Button::Primary,
284        }
285    }
286
287    /// A frame at rest: the camera at unity over the origin, so screen and
288    /// world agree and the assertions read in one space.
289    fn frame(resolver: &mut Resolver, millis: u64, raw: Vec<Raw>) -> Interaction {
290        resolver.resolve(&input(raw), at(millis), Vantage::resting(), Pos2::ZERO)
291    }
292
293    #[test]
294    fn a_release_inside_the_click_distance_is_a_click() {
295        let mut resolver = Resolver::default();
296        let pressed = frame(
297            &mut resolver,
298            0,
299            vec![Raw::Moved(pos2(10.0, 10.0)), down(pos2(10.0, 10.0))],
300        );
301        assert_eq!(pressed.event, Some(Event::HoverAt(pos2(10.0, 10.0))));
302        assert_eq!(
303            pressed.press,
304            Some(Press {
305                origin: pos2(10.0, 10.0)
306            }),
307            "a held press is reported while it is held",
308        );
309        let released = frame(
310            &mut resolver,
311            100,
312            vec![Raw::Moved(pos2(13.0, 12.0)), up(pos2(13.0, 12.0))],
313        );
314        assert_eq!(
315            released.event,
316            Some(Event::Clicked {
317                pos: pos2(13.0, 12.0)
318            })
319        );
320        assert!(released.press.is_none());
321    }
322
323    #[test]
324    fn a_press_that_travels_past_the_threshold_drags_from_where_it_began() {
325        let mut resolver = Resolver::default();
326        let origin = pos2(10.0, 10.0);
327        frame(&mut resolver, 0, vec![Raw::Moved(origin), down(origin)]);
328        let started = frame(&mut resolver, 16, vec![Raw::Moved(pos2(20.0, 10.0))]);
329        assert_eq!(
330            started.event,
331            Some(Event::DragStarted { pos: origin }),
332            "the drag starts where the button went down, not where it was noticed",
333        );
334        let dragging = frame(&mut resolver, 32, vec![Raw::Moved(pos2(25.0, 12.0))]);
335        assert_eq!(
336            dragging.event,
337            Some(Event::Dragging {
338                pos: pos2(25.0, 12.0),
339                delta: Vec2::new(5.0, 2.0)
340            })
341        );
342        let stopped = frame(&mut resolver, 48, vec![up(pos2(25.0, 12.0))]);
343        assert_eq!(
344            stopped.event,
345            Some(Event::DragStopped {
346                pos: pos2(25.0, 12.0)
347            })
348        );
349    }
350
351    #[test]
352    fn a_press_held_past_the_click_duration_becomes_a_drag_without_moving() {
353        let mut resolver = Resolver::default();
354        let origin = pos2(10.0, 10.0);
355        frame(&mut resolver, 0, vec![Raw::Moved(origin), down(origin)]);
356        assert_eq!(
357            frame(&mut resolver, 700, vec![]).event,
358            Some(Event::HoverAt(origin)),
359            "still a possible click",
360        );
361        assert_eq!(
362            frame(&mut resolver, 900, vec![]).event,
363            Some(Event::DragStarted { pos: origin })
364        );
365        assert_eq!(
366            frame(&mut resolver, 1000, vec![up(origin)]).event,
367            Some(Event::DragStopped { pos: origin }),
368            "a long press let go is not a click",
369        );
370    }
371
372    #[test]
373    fn two_clicks_within_the_window_at_one_spot_are_a_double_click() {
374        let mut resolver = Resolver::default();
375        let spot = pos2(10.0, 10.0);
376        frame(
377            &mut resolver,
378            0,
379            vec![Raw::Moved(spot), down(spot), up(spot)],
380        );
381        let second = frame(&mut resolver, 200, vec![down(spot), up(spot)]);
382        assert_eq!(second.event, Some(Event::DoubleClicked { pos: spot }));
383
384        let mut resolver = Resolver::default();
385        frame(
386            &mut resolver,
387            0,
388            vec![Raw::Moved(spot), down(spot), up(spot)],
389        );
390        let late = frame(&mut resolver, 400, vec![down(spot), up(spot)]);
391        assert_eq!(
392            late.event,
393            Some(Event::Clicked { pos: spot }),
394            "past the window"
395        );
396
397        let mut resolver = Resolver::default();
398        frame(
399            &mut resolver,
400            0,
401            vec![Raw::Moved(spot), down(spot), up(spot)],
402        );
403        let far = pos2(30.0, 10.0);
404        let elsewhere = frame(&mut resolver, 200, vec![down(far), up(far)]);
405        assert_eq!(
406            elsewhere.event,
407            Some(Event::Clicked { pos: far }),
408            "too far apart"
409        );
410    }
411
412    /// A press the pointer leaves behind — one the host gave to a control
413    /// drawn over the canvas, whose release the canvas never hears — ends
414    /// when the pointer goes, rather than arming the tools until the next
415    /// press: a drag stops where it stood, a held press comes to nothing.
416    #[test]
417    fn a_press_ends_when_the_pointer_goes() {
418        let mut resolver = Resolver::default();
419        resolver.resolve(
420            &input(vec![down(pos2(10.0, 10.0))]),
421            at(0),
422            Vantage::resting(),
423            Pos2::ZERO,
424        );
425        let gone = resolver.resolve(
426            &input(vec![Raw::Gone]),
427            at(16),
428            Vantage::resting(),
429            Pos2::ZERO,
430        );
431        assert_eq!(gone.event, None);
432        assert_eq!(gone.press, None, "the press outlived the pointer");
433        let later = resolver.resolve(&input(vec![]), at(1000), Vantage::resting(), Pos2::ZERO);
434        assert_eq!(later.event, None, "a press that had gone started a drag");
435        assert_eq!(later.press, None);
436
437        let mut resolver = Resolver::default();
438        resolver.resolve(
439            &input(vec![down(pos2(10.0, 10.0))]),
440            at(0),
441            Vantage::resting(),
442            Pos2::ZERO,
443        );
444        let dragging = resolver.resolve(
445            &input(vec![Raw::Moved(pos2(40.0, 10.0))]),
446            at(16),
447            Vantage::resting(),
448            Pos2::ZERO,
449        );
450        assert!(matches!(dragging.event, Some(Event::DragStarted { .. })));
451        let gone = resolver.resolve(
452            &input(vec![Raw::Gone]),
453            at(32),
454            Vantage::resting(),
455            Pos2::ZERO,
456        );
457        assert_eq!(
458            gone.event,
459            Some(Event::DragStopped {
460                pos: pos2(40.0, 10.0)
461            }),
462            "a drag the pointer left did not stop",
463        );
464        assert_eq!(gone.press, None);
465    }
466
467    #[test]
468    fn a_hover_ends_when_the_pointer_leaves() {
469        let mut resolver = Resolver::default();
470        let hovering = frame(&mut resolver, 0, vec![Raw::Moved(pos2(10.0, 10.0))]);
471        assert_eq!(hovering.event, Some(Event::HoverAt(pos2(10.0, 10.0))));
472        let still = frame(&mut resolver, 16, vec![]);
473        assert_eq!(
474            still.event,
475            Some(Event::HoverAt(pos2(10.0, 10.0))),
476            "a still pointer still hovers"
477        );
478        let gone = frame(&mut resolver, 32, vec![Raw::Gone]);
479        assert_eq!(gone.event, None);
480        assert_eq!(resolver.latest(), None);
481    }
482
483    #[test]
484    fn a_panning_frame_keeps_the_drag_and_the_press_from_the_tools() {
485        let mut resolver = Resolver::default();
486        let origin = pos2(10.0, 10.0);
487        frame(&mut resolver, 0, vec![Raw::Moved(origin), down(origin)]);
488        let panned = frame(
489            &mut resolver,
490            16,
491            vec![Raw::Moved(pos2(40.0, 10.0)), Raw::Panning],
492        );
493        assert_eq!(panned.event, None);
494        assert_eq!(panned.press, None);
495        let still_panning = frame(&mut resolver, 32, vec![up(pos2(40.0, 10.0)), Raw::Panning]);
496        assert_eq!(still_panning.event, None, "the drag's end is the pan's too");
497    }
498
499    #[test]
500    fn positions_come_out_in_world_space() {
501        let mut resolver = Resolver::default();
502        let vantage = Vantage {
503            zoom: blockworx_paint::Zoom::new(2.0),
504            translation: Vec2::new(100.0, 50.0),
505        };
506        let origin = pos2(10.0, 10.0);
507        let hovered = resolver.resolve(
508            &input(vec![Raw::Moved(pos2(310.0, 260.0))]),
509            at(0),
510            vantage,
511            origin,
512        );
513        assert_eq!(hovered.event, Some(Event::HoverAt(pos2(100.0, 100.0))));
514        resolver.resolve(
515            &input(vec![down(pos2(310.0, 260.0))]),
516            at(16),
517            vantage,
518            origin,
519        );
520        resolver.resolve(
521            &input(vec![Raw::Moved(pos2(330.0, 260.0))]),
522            at(32),
523            vantage,
524            origin,
525        );
526        let dragging = resolver.resolve(
527            &input(vec![Raw::Moved(pos2(340.0, 270.0))]),
528            at(48),
529            vantage,
530            origin,
531        );
532        assert_eq!(
533            dragging.event,
534            Some(Event::Dragging {
535                pos: pos2(115.0, 105.0),
536                delta: Vec2::new(5.0, 5.0)
537            }),
538            "a screen delta is halved at twice the zoom",
539        );
540    }
541}