1use 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#[derive(Clone, Copy, PartialEq, Eq, Debug)]
14pub enum Focus {
15 Canvas,
16 Elsewhere,
17}
18
19#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
22pub enum PanKey {
23 Held,
24 #[default]
25 Free,
26}
27
28#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
34pub enum Fingers {
35 #[default]
36 One,
37 Several,
38}
39
40#[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 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#[derive(Clone, Copy, PartialEq, Eq, Debug)]
70enum Drives {
71 Camera,
72 Tool,
73}
74
75#[derive(Clone, Copy, PartialEq, Eq, Debug)]
78struct Press {
79 button: Button,
80 drives: Drives,
81}
82
83#[derive(Clone, PartialEq, Debug, Default)]
86pub struct Read {
87 pub input: Input,
88 pub moves: Vec<Move>,
89 pub camera: Camera,
90}
91
92#[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 #[must_use]
111 pub fn pan_key(&self) -> PanKey {
112 self.pan_key
113 }
114
115 #[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 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 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 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 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 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 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 fn lifted(&mut self, id: PointerId) {
282 self.down.remove(&id);
283 self.span = self.spanned();
284 }
285
286 fn spanned(&self) -> Option<Span> {
290 let mut down = self.down.values();
291 Some(Span::between(*down.next()?, *down.next()?))
292 }
293
294 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 Button::Primary if self.pan_key == PanKey::Held => Drives::Camera,
307 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 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 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 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 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 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(¢re), "{:?}", read.moves);
644 }
645
646 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}