Skip to main content

blockworx_canvas2d/input/
reader.rs

1//! Samples in, kernel input out — and nothing of the browser in between.
2
3use std::collections::BTreeMap;
4
5use blockworx_geom::Pos2;
6use blockworx_paint::{Button, Camera, Factor, Input, Keys, Move, Raw};
7
8use super::{Edge, Named, Phase, PointerId, PointerSample, Sample, Shift};
9
10/// Who holds the keyboard. A text field owning it is typing, not editing the
11/// drawing: its Backspace must not delete the selected shape, and its space
12/// must not arm a pan.
13#[derive(Clone, Copy, PartialEq, Eq, Debug)]
14pub enum Focus {
15    Canvas,
16    Elsewhere,
17}
18
19/// Whether the key that turns a primary drag into a pan is held: **space**,
20/// the convention across drawing and diagram editors.
21#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
22pub enum PanKey {
23    Held,
24    #[default]
25    Free,
26}
27
28/// How many pointers the gesture in flight has.
29///
30/// One behaves as a mouse does; two pan and pinch the canvas together. A
31/// gesture that ever had a second finger belongs to the camera until the last
32/// one lifts, so the trailing finger never arrives as a drag.
33#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
34pub enum Fingers {
35    #[default]
36    One,
37    Several,
38}
39
40/// Where two fingers are, as the one thing a pan and a pinch are read from:
41/// the point between them, and how far apart they are.
42#[derive(Clone, Copy, PartialEq, Debug)]
43struct Span {
44    centre: Pos2,
45    apart: f32,
46}
47
48impl Span {
49    fn between(one: Pos2, other: Pos2) -> Self {
50        Self {
51            centre: one.lerp(other, 0.5),
52            apart: one.distance(other),
53        }
54    }
55
56    /// What the camera owes the gesture since it last stood at `was`: the
57    /// centre's motion, and the pinch about where the fingers are now.
58    fn since(self, was: Self) -> impl Iterator<Item = Move> {
59        let panned = (self.centre != was.centre).then(|| Move::Pan(self.centre - was.centre));
60        let pinched = (was.apart > 0.0 && self.apart != was.apart).then(|| Move::Zoom {
61            factor: Factor::new(self.apart / was.apart),
62            anchor: self.centre,
63        });
64        panned.into_iter().chain(pinched)
65    }
66}
67
68/// What a press in progress drives.
69#[derive(Clone, Copy, PartialEq, Eq, Debug)]
70enum Drives {
71    Camera,
72    Tool,
73}
74
75/// The button press in progress. Latched at the press, so releasing the pan
76/// key mid-drag cannot hand a half-finished pan to a tool.
77#[derive(Clone, Copy, PartialEq, Eq, Debug)]
78struct Press {
79    button: Button,
80    drives: Drives,
81}
82
83/// What one batch of samples came to: the input the kernel resolves, the
84/// camera moves to apply before it, and whether the camera is being worked.
85#[derive(Clone, PartialEq, Debug, Default)]
86pub struct Read {
87    pub input: Input,
88    pub moves: Vec<Move>,
89    pub camera: Camera,
90}
91
92/// The latches a stream of DOM events needs to be read through: which
93/// pointers are down and where, what the press in progress drives, where the
94/// two-finger gesture stood, whether the pan key is held, and where the
95/// pointer last was.
96#[derive(Default)]
97pub struct Reader {
98    down: BTreeMap<PointerId, Pos2>,
99    press: Option<Press>,
100    fingers: Fingers,
101    span: Option<Span>,
102    pan_key: PanKey,
103    shift: Shift,
104    at: Option<Pos2>,
105}
106
107impl Reader {
108    /// Whether the pan key is held, as of the last batch — what a host draws
109    /// the grab cursor from.
110    #[must_use]
111    pub fn pan_key(&self) -> PanKey {
112        self.pan_key
113    }
114
115    /// How many pointers the gesture in flight has, as of the last batch.
116    #[must_use]
117    pub fn fingers(&self) -> Fingers {
118        self.fingers
119    }
120
121    pub fn read(&mut self, samples: &[Sample], focus: Focus) -> Read {
122        let canvas_keys = focus == Focus::Canvas;
123        let mut raw = Vec::new();
124        let mut moves = Vec::new();
125        let mut keys = Keys::default();
126        let mut panning = false;
127        for sample in samples {
128            self.shift = sample.shift();
129            match sample {
130                Sample::Key(key) => match (key.key, key.edge) {
131                    (Named::Escape, Edge::Down) => {
132                        // A gesture is abandoned on Escape whoever holds the
133                        // keyboard; only the canvas's own reading of the key
134                        // is scoped by focus.
135                        raw.push(Raw::Cancelled);
136                        keys.escape |= canvas_keys;
137                    }
138                    (Named::Delete, Edge::Down) => keys.delete |= canvas_keys,
139                    (Named::Space, Edge::Down) if canvas_keys => self.pan_key = PanKey::Held,
140                    (Named::Space, Edge::Up) => self.pan_key = PanKey::Free,
141                    _ => {}
142                },
143                Sample::Wheel(wheel) => {
144                    // A bare wheel zooms — this canvas has no document flow
145                    // to scroll past — and so does the ctrl-held wheel a
146                    // trackpad pinch arrives as.
147                    let factor = Factor::of_scroll(wheel.scroll);
148                    if factor != Factor::IDENTITY {
149                        moves.push(Move::Zoom {
150                            factor,
151                            anchor: wheel.at,
152                        });
153                    }
154                }
155                Sample::Pointer(pointer) => {
156                    panning |= self.pointer(pointer, &mut raw, &mut moves);
157                }
158            }
159        }
160        keys.shift = self.shift == Shift::Held;
161        if panning {
162            raw.push(Raw::Panning);
163        }
164        Read {
165            input: Input { raw, keys },
166            moves,
167            camera: if panning {
168                Camera::Moving
169            } else {
170                Camera::Settled
171            },
172        }
173    }
174
175    /// One pointer sample, answering whether the camera owns the frame.
176    fn pointer(
177        &mut self,
178        sample: &PointerSample,
179        raw: &mut Vec<Raw>,
180        moves: &mut Vec<Move>,
181    ) -> bool {
182        let last = self.at.replace(sample.at);
183        match sample.phase {
184            Phase::Down => {
185                self.down.insert(sample.id, sample.at);
186                if self.down.len() > 1 && self.fingers == Fingers::One {
187                    self.fingers = Fingers::Several;
188                    // Whatever the first finger was drawing, the second says
189                    // the gesture was the camera's all along.
190                    raw.push(Raw::Cancelled);
191                    self.press = None;
192                }
193                if self.fingers == Fingers::Several {
194                    self.span = self.spanned();
195                    return true;
196                }
197                let Some(button) = sample.button else {
198                    return false;
199                };
200                let drives = self.drives(button);
201                self.press = Some(Press { button, drives });
202                raw.push(Raw::Down {
203                    pos: sample.at,
204                    button,
205                });
206                drives == Drives::Camera
207            }
208            Phase::Moved => {
209                raw.push(Raw::Moved(sample.at));
210                if let Some(at) = self.down.get_mut(&sample.id) {
211                    *at = sample.at;
212                }
213                if self.fingers == Fingers::Several {
214                    if let Some(now) = self.spanned()
215                        && let Some(was) = self.span.replace(now)
216                    {
217                        moves.extend(now.since(was));
218                    }
219                    return true;
220                }
221                // A press whose release was delivered elsewhere (outside the
222                // window, to another element) is let go here rather than held
223                // for the rest of the session.
224                if let Some(press) = self.press
225                    && !sample.held.holds(press.button)
226                {
227                    self.press = None;
228                }
229                if self
230                    .press
231                    .is_none_or(|press| press.drives != Drives::Camera)
232                {
233                    return false;
234                }
235                if let Some(last) = last {
236                    moves.push(Move::Pan(sample.at - last));
237                }
238                true
239            }
240            Phase::Up => {
241                self.lifted(sample.id);
242                if self.fingers == Fingers::Several {
243                    self.settle();
244                    return true;
245                }
246                if let Some(button) = sample.button {
247                    raw.push(Raw::Up {
248                        pos: sample.at,
249                        button,
250                    });
251                }
252                let closing = self
253                    .press
254                    .filter(|press| Some(press.button) == sample.button);
255                if closing.is_some() {
256                    self.press = None;
257                }
258                // The latch covers the release too, so the gesture's closing
259                // event is consumed by the pan rather than reaching a tool.
260                closing.is_some_and(|press| press.drives == Drives::Camera)
261            }
262            Phase::Left | Phase::Cancelled => {
263                self.lifted(sample.id);
264                raw.push(if sample.phase == Phase::Left {
265                    Raw::Gone
266                } else {
267                    Raw::Cancelled
268                });
269                let owned = self.fingers == Fingers::Several
270                    || self
271                        .press
272                        .is_some_and(|press| press.drives == Drives::Camera);
273                self.settle();
274                owned
275            }
276        }
277    }
278
279    /// A pointer that is no longer down, and the gesture it leaves behind:
280    /// one finger of a pair is no pinch, so the pair's span goes with it.
281    fn lifted(&mut self, id: PointerId) {
282        self.down.remove(&id);
283        self.span = self.spanned();
284    }
285
286    /// The pair the gesture is read from, or `None` for fewer than two
287    /// pointers down. A third finger joining does not take the gesture out
288    /// from under the two that are holding it.
289    fn spanned(&self) -> Option<Span> {
290        let mut down = self.down.values();
291        Some(Span::between(*down.next()?, *down.next()?))
292    }
293
294    /// Clear the gesture's latches once the last pointer has lifted.
295    fn settle(&mut self) {
296        if self.down.is_empty() {
297            self.fingers = Fingers::One;
298            self.press = None;
299        }
300    }
301
302    fn drives(&self, button: Button) -> Drives {
303        match button {
304            // A left-drag pans while the pan key is held, for pointing
305            // devices where the other buttons are awkward.
306            Button::Primary if self.pan_key == PanKey::Held => Drives::Camera,
307            // As the desktop pans a right-drag.
308            Button::Middle | Button::Secondary => Drives::Camera,
309            Button::Primary => Drives::Tool,
310        }
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317    use crate::input::{Held, KeySample, PointerId, PointerSample, WheelSample};
318    use blockworx_geom::{Vec2, pos2};
319    use blockworx_paint::{PointerKind, ScrollPx};
320
321    const MOUSE: PointerId = PointerId(1);
322    const FIRST: PointerId = PointerId(1);
323    const SECOND: PointerId = PointerId(2);
324    const SECOND_FINGER: PointerId = PointerId(2);
325
326    /// What a pixel of screen geometry is allowed to be out by, once a
327    /// centre and a separation have been through an `f32`.
328    const TOLERANCE: f32 = 1e-4;
329
330    fn pointing(id: PointerId, phase: Phase, at: Pos2, held: Held) -> PointerSample {
331        PointerSample {
332            id,
333            at,
334            phase,
335            button: matches!(phase, Phase::Down | Phase::Up).then_some(Button::Primary),
336            held,
337            kind: PointerKind::Mouse,
338            shift: Shift::Free,
339        }
340    }
341
342    fn pointer(id: PointerId, phase: Phase, at: Pos2, held: Held) -> Sample {
343        Sample::Pointer(pointing(id, phase, at, held))
344    }
345
346    /// One finger, as the DOM reports a touch: the primary button, and a
347    /// `buttons` mask that says it is down.
348    fn touch(id: PointerId, phase: Phase, at: Pos2) -> Sample {
349        Sample::Pointer(PointerSample {
350            kind: PointerKind::Touch,
351            ..pointing(id, phase, at, Held::of([Button::Primary]))
352        })
353    }
354
355    fn key(key: Named, edge: Edge) -> Sample {
356        Sample::Key(KeySample {
357            key,
358            edge,
359            shift: Shift::Free,
360        })
361    }
362
363    /// Whether a batch's raw input is a pan's — the core makes no tool
364    /// gesture of a panning frame — or a gesture the tools may read.
365    fn panning(read: &Read) -> bool {
366        read.input.raw.contains(&Raw::Panning)
367    }
368
369    fn moved(read: &Read) -> bool {
370        read.input
371            .raw
372            .iter()
373            .any(|raw| matches!(raw, Raw::Moved(_)))
374    }
375
376    /// Space+left-drag pans (the drawing-app convention), and the tool must
377    /// not also see the drag — the reader consumes it.
378    #[test]
379    fn space_plus_left_drag_pans_instead_of_reaching_the_tool() {
380        let mut reader = Reader::default();
381        let start = pos2(400.0, 300.0);
382        let step = Vec2::new(60.0, -25.0);
383        reader.read(&[key(Named::Space, Edge::Down)], Focus::Canvas);
384        reader.read(
385            &[pointer(MOUSE, Phase::Moved, start, Held::NONE)],
386            Focus::Canvas,
387        );
388        reader.read(
389            &[pointer(MOUSE, Phase::Down, start, Held::NONE)],
390            Focus::Canvas,
391        );
392        let read = reader.read(
393            &[pointer(
394                MOUSE,
395                Phase::Moved,
396                start + step,
397                Held::of([Button::Primary]),
398            )],
399            Focus::Canvas,
400        );
401        assert_eq!(read.moves, vec![Move::Pan(step)]);
402        assert_eq!(read.camera, Camera::Moving);
403        assert!(panning(&read), "the pan leaked to the tool as {read:?}");
404    }
405
406    /// The same drag without the key belongs to the tool: no pan, and the
407    /// tool sees the gesture.
408    #[test]
409    fn a_plain_left_drag_is_left_to_the_tool() {
410        let mut reader = Reader::default();
411        let start = pos2(400.0, 300.0);
412        reader.read(
413            &[pointer(MOUSE, Phase::Moved, start, Held::NONE)],
414            Focus::Canvas,
415        );
416        reader.read(
417            &[pointer(MOUSE, Phase::Down, start, Held::NONE)],
418            Focus::Canvas,
419        );
420        let read = reader.read(
421            &[pointer(
422                MOUSE,
423                Phase::Moved,
424                start + Vec2::new(60.0, -25.0),
425                Held::of([Button::Primary]),
426            )],
427            Focus::Canvas,
428        );
429        assert!(
430            read.moves.is_empty(),
431            "a plain drag must not pan: {:?}",
432            read.moves
433        );
434        assert!(
435            !panning(&read) && moved(&read),
436            "the tool never saw the drag ({read:?})"
437        );
438    }
439
440    /// Releasing the key mid-drag leaves the gesture a pan: it was latched
441    /// when the drag began, so no tool inherits a half-finished pan.
442    #[test]
443    fn releasing_the_key_mid_drag_keeps_panning() {
444        let mut reader = Reader::default();
445        let start = pos2(400.0, 300.0);
446        let held = Held::of([Button::Primary]);
447        reader.read(&[key(Named::Space, Edge::Down)], Focus::Canvas);
448        reader.read(
449            &[pointer(MOUSE, Phase::Moved, start, Held::NONE)],
450            Focus::Canvas,
451        );
452        reader.read(
453            &[pointer(MOUSE, Phase::Down, start, Held::NONE)],
454            Focus::Canvas,
455        );
456        let mut at = start + Vec2::new(30.0, 0.0);
457        reader.read(&[pointer(MOUSE, Phase::Moved, at, held)], Focus::Canvas);
458        at += Vec2::new(20.0, 10.0);
459        let read = reader.read(
460            &[
461                key(Named::Space, Edge::Up),
462                pointer(MOUSE, Phase::Moved, at, held),
463            ],
464            Focus::Canvas,
465        );
466        assert_eq!(reader.pan_key(), PanKey::Free, "the key did come up");
467        assert_eq!(read.moves, vec![Move::Pan(Vec2::new(20.0, 10.0))]);
468        assert!(panning(&read), "{read:?}");
469    }
470
471    /// Lifting one of two pointers must not hand the remaining one to a tool:
472    /// the gesture owns the canvas until the last one leaves.
473    #[test]
474    fn the_last_finger_of_a_pan_never_becomes_a_tool_drag() {
475        let mut reader = Reader::default();
476        let first = pos2(360.0, 300.0);
477        let second = pos2(440.0, 300.0);
478        let held = Held::of([Button::Primary]);
479        reader.read(
480            &[pointer(MOUSE, Phase::Down, first, Held::NONE)],
481            Focus::Canvas,
482        );
483        reader.read(
484            &[pointer(SECOND_FINGER, Phase::Down, second, held)],
485            Focus::Canvas,
486        );
487        assert_eq!(
488            reader.fingers(),
489            Fingers::Several,
490            "precondition: two pointers are down"
491        );
492        reader.read(
493            &[pointer(SECOND_FINGER, Phase::Up, second, held)],
494            Focus::Canvas,
495        );
496
497        let trailing = reader.read(
498            &[pointer(
499                MOUSE,
500                Phase::Moved,
501                first + Vec2::new(50.0, 0.0),
502                held,
503            )],
504            Focus::Canvas,
505        );
506        assert!(
507            panning(&trailing),
508            "the trailing finger reached the tool as a drag ({trailing:?})"
509        );
510
511        reader.read(
512            &[pointer(
513                MOUSE,
514                Phase::Up,
515                first + Vec2::new(50.0, 0.0),
516                Held::NONE,
517            )],
518            Focus::Canvas,
519        );
520        assert_eq!(
521            reader.fingers(),
522            Fingers::One,
523            "the gesture latch outlived the gesture"
524        );
525        let fresh = reader.read(
526            &[
527                pointer(MOUSE, Phase::Down, first, Held::NONE),
528                pointer(MOUSE, Phase::Moved, first + Vec2::new(10.0, 0.0), held),
529            ],
530            Focus::Canvas,
531        );
532        assert!(
533            !panning(&fresh) && moved(&fresh),
534            "the canvas is the tool's again once every pointer is up ({fresh:?})"
535        );
536    }
537
538    /// What the two-finger moves in a batch come to: the camera is moved by
539    /// each in turn, so the pans add up and the zooms multiply.
540    fn amounts_to(read: &Read) -> (Vec2, f32) {
541        read.moves
542            .iter()
543            .fold((Vec2::ZERO, 1.0), |(panned, zoomed), moved| match *moved {
544                Move::Pan(step) => (panned + step, zoomed),
545                Move::Zoom { factor, .. } => (panned, zoomed * factor.get()),
546            })
547    }
548
549    fn anchors(read: &Read) -> Vec<Pos2> {
550        read.moves
551            .iter()
552            .filter_map(|moved| match moved {
553                Move::Zoom { anchor, .. } => Some(*anchor),
554                Move::Pan(_) => None,
555            })
556            .collect()
557    }
558
559    /// One finger is the mouse: a drag is the tool's gesture, not a pan.
560    #[test]
561    fn one_finger_draws_as_the_mouse_does() {
562        let mut reader = Reader::default();
563        let start = pos2(300.0, 300.0);
564        reader.read(&[touch(FIRST, Phase::Down, start)], Focus::Canvas);
565        let read = reader.read(
566            &[touch(FIRST, Phase::Moved, start + Vec2::new(40.0, 20.0))],
567            Focus::Canvas,
568        );
569        assert!(
570            read.moves.is_empty(),
571            "a lone finger moved the camera: {:?}",
572            read.moves
573        );
574        assert!(!panning(&read) && moved(&read), "{read:?}");
575    }
576
577    /// Two fingers moving together pan by the step they share and leave the
578    /// zoom where it was — they never changed how far apart they are.
579    #[test]
580    fn two_fingers_pan_by_the_centre_they_carry() {
581        let mut reader = Reader::default();
582        let (first, second) = (pos2(300.0, 300.0), pos2(500.0, 300.0));
583        let step = Vec2::new(60.0, 40.0);
584        reader.read(&[touch(FIRST, Phase::Down, first)], Focus::Canvas);
585        reader.read(&[touch(SECOND, Phase::Down, second)], Focus::Canvas);
586        assert_eq!(
587            reader.fingers(),
588            Fingers::Several,
589            "precondition: both fingers are down"
590        );
591
592        let read = reader.read(
593            &[
594                touch(FIRST, Phase::Moved, first + step),
595                touch(SECOND, Phase::Moved, second + step),
596            ],
597            Focus::Canvas,
598        );
599        let (panned, zoomed) = amounts_to(&read);
600        assert!(
601            (panned - step).length() < TOLERANCE,
602            "the centre went {panned:?} rather than {step:?}"
603        );
604        assert!(
605            (zoomed - 1.0).abs() < TOLERANCE,
606            "fingers that kept their distance zoomed by {zoomed}"
607        );
608        assert!(panning(&read), "{read:?}");
609    }
610
611    /// Fingers drawn apart magnify by exactly how much further apart they
612    /// are, about the point between them — which they never moved off.
613    #[test]
614    fn two_fingers_pinch_about_the_point_between_them() {
615        let mut reader = Reader::default();
616        let (first, second) = (pos2(300.0, 300.0), pos2(500.0, 300.0));
617        let centre = pos2(400.0, 300.0);
618        reader.read(&[touch(FIRST, Phase::Down, first)], Focus::Canvas);
619        reader.read(&[touch(SECOND, Phase::Down, second)], Focus::Canvas);
620        assert_eq!(
621            first.distance(second),
622            200.0,
623            "precondition: the fingers start 200px apart"
624        );
625
626        // Apart to 400px, symmetrically, so the centre ends where it began.
627        let read = reader.read(
628            &[
629                touch(FIRST, Phase::Moved, pos2(200.0, 300.0)),
630                touch(SECOND, Phase::Moved, pos2(600.0, 300.0)),
631            ],
632            Focus::Canvas,
633        );
634        let (panned, zoomed) = amounts_to(&read);
635        assert!(
636            panned.length() < TOLERANCE,
637            "the centre moved by {panned:?}"
638        );
639        assert!(
640            (zoomed - 2.0).abs() < TOLERANCE,
641            "twice as far apart is not twice the zoom: {zoomed}"
642        );
643        assert_eq!(anchors(&read).last(), Some(&centre), "{:?}", read.moves);
644    }
645
646    /// The second finger takes the gesture away from the tool the first one
647    /// armed, rather than leaving half a drag behind it.
648    #[test]
649    fn a_second_finger_abandons_the_gesture_the_first_began() {
650        let mut reader = Reader::default();
651        let (first, second) = (pos2(300.0, 300.0), pos2(500.0, 300.0));
652        reader.read(&[touch(FIRST, Phase::Down, first)], Focus::Canvas);
653        let drawing = reader.read(
654            &[touch(FIRST, Phase::Moved, first + Vec2::new(20.0, 0.0))],
655            Focus::Canvas,
656        );
657        assert!(
658            !panning(&drawing) && moved(&drawing),
659            "precondition: the first finger is the tool's ({drawing:?})"
660        );
661
662        let joined = reader.read(&[touch(SECOND, Phase::Down, second)], Focus::Canvas);
663        assert!(
664            joined.input.raw.contains(&Raw::Cancelled),
665            "the tool was left holding the gesture: {joined:?}"
666        );
667        assert!(panning(&joined), "{joined:?}");
668    }
669
670    /// A finger lifted from a pinch leaves no pan behind it: one of a pair is
671    /// no gesture at all until the last one goes.
672    #[test]
673    fn the_finger_left_after_a_pinch_moves_nothing() {
674        let mut reader = Reader::default();
675        let (first, second) = (pos2(300.0, 300.0), pos2(500.0, 300.0));
676        reader.read(&[touch(FIRST, Phase::Down, first)], Focus::Canvas);
677        reader.read(&[touch(SECOND, Phase::Down, second)], Focus::Canvas);
678        reader.read(&[touch(SECOND, Phase::Up, second)], Focus::Canvas);
679
680        let trailing = reader.read(
681            &[touch(FIRST, Phase::Moved, first + Vec2::new(80.0, 80.0))],
682            Focus::Canvas,
683        );
684        assert!(
685            trailing.moves.is_empty(),
686            "the trailing finger moved the camera: {:?}",
687            trailing.moves
688        );
689        assert!(
690            panning(&trailing),
691            "and it reached the tool instead ({trailing:?})"
692        );
693    }
694
695    /// A middle-button drag is the pan a pointing device has without a
696    /// keyboard.
697    #[test]
698    fn a_middle_drag_pans() {
699        let mut reader = Reader::default();
700        let start = pos2(100.0, 100.0);
701        let step = Vec2::new(-12.0, 8.0);
702        reader.read(
703            &[Sample::Pointer(PointerSample {
704                id: MOUSE,
705                at: start,
706                phase: Phase::Down,
707                button: Some(Button::Middle),
708                held: Held::NONE,
709                kind: PointerKind::Mouse,
710                shift: Shift::Free,
711            })],
712            Focus::Canvas,
713        );
714        let read = reader.read(
715            &[pointer(
716                MOUSE,
717                Phase::Moved,
718                start + step,
719                Held::of([Button::Middle]),
720            )],
721            Focus::Canvas,
722        );
723        assert_eq!(read.moves, vec![Move::Pan(step)]);
724        assert!(panning(&read));
725    }
726
727    /// A right-button drag pans, as the desktop's does, rather than reaching a
728    /// tool.
729    #[test]
730    fn a_right_button_drag_pans() {
731        let mut reader = Reader::default();
732        let start = pos2(100.0, 100.0);
733        let step = Vec2::new(12.0, -5.0);
734        let down = reader.read(
735            &[Sample::Pointer(PointerSample {
736                button: Some(Button::Secondary),
737                ..pointing(MOUSE, Phase::Down, start, Held::NONE)
738            })],
739            Focus::Canvas,
740        );
741        assert!(panning(&down), "the press itself belongs to the camera");
742        let read = reader.read(
743            &[pointer(
744                MOUSE,
745                Phase::Moved,
746                start + step,
747                Held::of([Button::Secondary]),
748            )],
749            Focus::Canvas,
750        );
751        assert_eq!(read.moves, vec![Move::Pan(step)]);
752        assert!(panning(&read));
753    }
754
755    /// A bare wheel zooms about the cursor; nothing scrolls.
756    #[test]
757    fn a_wheel_zooms_about_the_cursor() {
758        let mut reader = Reader::default();
759        let at = pos2(320.0, 240.0);
760        let read = reader.read(
761            &[Sample::Wheel(WheelSample {
762                at,
763                scroll: ScrollPx::up(50.0),
764                shift: Shift::Free,
765            })],
766            Focus::Canvas,
767        );
768        let [Move::Zoom { factor, anchor }] = read.moves[..] else {
769            panic!("a wheel notch is one zoom: {:?}", read.moves);
770        };
771        assert!(factor.get() > 1.0, "scrolling up magnifies: {factor:?}");
772        assert_eq!(anchor, at);
773        assert!(
774            read.moves
775                .iter()
776                .all(|moved| !matches!(moved, Move::Pan(_)))
777        );
778    }
779
780    /// Escape abandons the gesture whoever holds the keyboard, but only the
781    /// canvas's own keys reach the tools.
782    #[test]
783    fn a_field_with_the_keyboard_keeps_its_own_keys() {
784        let mut reader = Reader::default();
785        let typing = reader.read(
786            &[
787                key(Named::Escape, Edge::Down),
788                key(Named::Delete, Edge::Down),
789            ],
790            Focus::Elsewhere,
791        );
792        assert_eq!(typing.input.keys, Keys::default(), "{typing:?}");
793        assert!(
794            typing.input.raw.contains(&Raw::Cancelled),
795            "a gesture in flight is still abandoned: {typing:?}"
796        );
797
798        let ours = reader.read(
799            &[
800                key(Named::Escape, Edge::Down),
801                key(Named::Delete, Edge::Down),
802            ],
803            Focus::Canvas,
804        );
805        assert!(ours.input.keys.escape && ours.input.keys.delete, "{ours:?}");
806    }
807
808    /// Space typed into a text field does not arm the canvas's pan.
809    #[test]
810    fn space_in_a_text_field_does_not_arm_the_pan() {
811        let mut reader = Reader::default();
812        reader.read(&[key(Named::Space, Edge::Down)], Focus::Elsewhere);
813        assert_eq!(reader.pan_key(), PanKey::Free);
814        reader.read(&[key(Named::Space, Edge::Down)], Focus::Canvas);
815        assert_eq!(reader.pan_key(), PanKey::Held);
816    }
817
818    /// Shift is level-triggered: a batch that carries no sample still reports
819    /// the state the last one left it in.
820    #[test]
821    fn shift_is_remembered_between_batches() {
822        let mut reader = Reader::default();
823        let shifted = reader.read(
824            &[Sample::Key(KeySample {
825                key: Named::Delete,
826                edge: Edge::Up,
827                shift: Shift::Held,
828            })],
829            Focus::Canvas,
830        );
831        assert!(shifted.input.keys.shift);
832        assert!(reader.read(&[], Focus::Canvas).input.keys.shift);
833    }
834
835    /// A pointer that leaves says so, and one the platform took away
836    /// abandons the gesture.
837    #[test]
838    fn a_pointer_that_goes_away_says_which_way_it_went() {
839        let mut reader = Reader::default();
840        let gone = reader.read(
841            &[pointer(MOUSE, Phase::Left, pos2(0.0, 0.0), Held::NONE)],
842            Focus::Canvas,
843        );
844        assert_eq!(gone.input.raw, vec![Raw::Gone]);
845        let taken = reader.read(
846            &[pointer(MOUSE, Phase::Cancelled, pos2(0.0, 0.0), Held::NONE)],
847            Focus::Canvas,
848        );
849        assert_eq!(taken.input.raw, vec![Raw::Cancelled]);
850    }
851
852    /// A release delivered somewhere else leaves the button held as far as
853    /// the DOM is concerned; the next motion says otherwise and the pan ends.
854    #[test]
855    fn a_press_whose_release_was_missed_is_let_go() {
856        let mut reader = Reader::default();
857        let start = pos2(10.0, 10.0);
858        reader.read(&[key(Named::Space, Edge::Down)], Focus::Canvas);
859        reader.read(
860            &[pointer(MOUSE, Phase::Down, start, Held::NONE)],
861            Focus::Canvas,
862        );
863        let held = reader.read(
864            &[pointer(
865                MOUSE,
866                Phase::Moved,
867                start + Vec2::new(5.0, 0.0),
868                Held::of([Button::Primary]),
869            )],
870            Focus::Canvas,
871        );
872        assert!(panning(&held), "precondition: the pan is in flight");
873        let let_go = reader.read(
874            &[pointer(
875                MOUSE,
876                Phase::Moved,
877                start + Vec2::new(10.0, 0.0),
878                Held::NONE,
879            )],
880            Focus::Canvas,
881        );
882        assert!(!panning(&let_go), "{let_go:?}");
883    }
884}