1use frust_core::event::{PointerId, ScaleEvent, ScalePhase};
84use frust_core::{
85 AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent,
86 LayoutCtx, PaintCtx, PaintScene, PointerEvent, PointerPhase, SemanticsCtx, TOUCH_SLOP,
87 VELOCITY_WINDOW_MS, View, Widget, any,
88};
89use kurbo::{Point, Size};
90use std::cell::Cell;
91use std::rc::Rc;
92
93use crate::authoring::presses;
94use crate::scroll::ambient_scroll_veto;
95
96pub const PINCH_SLOP: f64 = TOUCH_SLOP;
105
106pub const PINCH_VELOCITY_SAMPLES: f64 = 4.0;
113
114pub const MAX_PINCH_CONTACTS: usize = 10;
117
118const MIN_SPAN: f64 = 1.0;
123
124#[derive(Clone, Copy, Debug, PartialEq)]
126enum Phase {
127 Idle,
129 Armed { start_distance: f64 },
131 Active { last_distance: f64 },
133}
134
135#[derive(Debug)]
141pub struct PinchRecognizer {
142 contacts: Vec<(PointerId, Point)>,
144 phase: Phase,
145 velocity: f64,
147 samples: u32,
150 last_sample_ms: f64,
152 pending_log: f64,
154}
155
156impl Default for PinchRecognizer {
157 fn default() -> Self {
158 Self::new()
159 }
160}
161
162impl PinchRecognizer {
163 pub fn new() -> Self {
165 Self {
166 contacts: Vec::with_capacity(MAX_PINCH_CONTACTS),
167 phase: Phase::Idle,
168 velocity: 0.0,
169 samples: 0,
170 last_sample_ms: 0.0,
171 pending_log: 0.0,
172 }
173 }
174
175 pub fn is_pinching(&self) -> bool {
177 matches!(self.phase, Phase::Active { .. })
178 }
179
180 pub fn contact_count(&self) -> usize {
182 self.contacts.len()
183 }
184
185 pub fn reset(&mut self) {
188 self.contacts.clear();
189 self.phase = Phase::Idle;
190 self.reset_velocity(0.0);
191 }
192
193 pub fn handle(
197 &mut self,
198 id: PointerId,
199 event: &PointerEvent,
200 time_ms: f64,
201 ) -> Option<ScaleEvent> {
202 let index = self.contacts.iter().position(|(c, _)| *c == id);
203 match event.phase {
204 PointerPhase::Down => match index {
205 Some(i) => self.moved(i, event.position, time_ms),
207 None => {
208 if self.contacts.len() < MAX_PINCH_CONTACTS {
209 self.contacts.push((id, event.position));
210 if self.contacts.len() == 2 {
211 self.arm();
212 }
213 }
214 None
215 }
216 },
217 PointerPhase::Move => self.moved(index?, event.position, time_ms),
218 PointerPhase::Up | PointerPhase::Cancel => {
219 let i = index?;
220 self.contacts[i].1 = event.position;
221 let cancelled = event.phase == PointerPhase::Cancel;
222 let end = if i < 2 {
223 self.end(time_ms, cancelled)
224 } else {
225 None
226 };
227 if i == 0 {
228 self.contacts.clear();
230 self.phase = Phase::Idle;
231 } else {
232 self.contacts.remove(i);
233 if i < 2 {
234 if self.contacts.len() >= 2 {
235 self.arm();
236 } else {
237 self.phase = Phase::Idle;
238 }
239 }
240 }
241 end
242 }
243 }
244 }
245
246 fn pair(&self) -> (f64, Point) {
248 let (a, b) = (self.contacts[0].1, self.contacts[1].1);
249 ((b - a).hypot(), a.midpoint(b))
250 }
251
252 fn arm(&mut self) {
254 let (distance, _) = self.pair();
255 self.phase = Phase::Armed {
256 start_distance: distance,
257 };
258 }
259
260 fn reset_velocity(&mut self, time_ms: f64) {
261 self.velocity = 0.0;
262 self.samples = 0;
263 self.last_sample_ms = time_ms;
264 self.pending_log = 0.0;
265 }
266
267 fn sample_velocity(&mut self, log_delta: f64, time_ms: f64) {
269 self.pending_log += log_delta;
270 let dt_ms = time_ms - self.last_sample_ms;
271 if dt_ms <= 0.0 {
272 return;
274 }
275 if dt_ms > VELOCITY_WINDOW_MS {
276 self.samples = 0;
278 }
279 let instant = self.pending_log / (dt_ms / 1000.0);
280 self.velocity = if self.samples == 0 {
281 instant
282 } else {
283 let alpha = 2.0 / (PINCH_VELOCITY_SAMPLES + 1.0);
284 alpha * instant + (1.0 - alpha) * self.velocity
285 };
286 self.samples = self.samples.saturating_add(1);
287 self.last_sample_ms = time_ms;
288 self.pending_log = 0.0;
289 }
290
291 fn moved(&mut self, i: usize, position: Point, time_ms: f64) -> Option<ScaleEvent> {
293 self.contacts[i].1 = position;
294 if i >= 2 || self.contacts.len() < 2 {
295 return None;
296 }
297 let (distance, focal) = self.pair();
298 match self.phase {
299 Phase::Idle => None,
300 Phase::Armed { start_distance } => {
301 if (distance - start_distance).abs() <= PINCH_SLOP {
302 return None;
303 }
304 self.phase = Phase::Active {
305 last_distance: distance,
306 };
307 self.reset_velocity(time_ms);
308 Some(ScaleEvent {
309 phase: ScalePhase::Begin,
310 scale_delta: 1.0,
311 focal,
312 velocity: 0.0,
313 })
314 }
315 Phase::Active { last_distance } => {
316 let scale_delta = distance.max(MIN_SPAN) / last_distance.max(MIN_SPAN);
317 self.phase = Phase::Active {
318 last_distance: distance,
319 };
320 self.sample_velocity(scale_delta.ln(), time_ms);
321 Some(ScaleEvent {
322 phase: ScalePhase::Update,
323 scale_delta,
324 focal,
325 velocity: self.velocity,
326 })
327 }
328 }
329 }
330
331 fn end(&mut self, time_ms: f64, cancelled: bool) -> Option<ScaleEvent> {
333 if !self.is_pinching() || self.contacts.len() < 2 {
334 return None;
335 }
336 let (_, focal) = self.pair();
337 let stale = time_ms - self.last_sample_ms > VELOCITY_WINDOW_MS;
338 let velocity = if cancelled || stale || self.samples == 0 {
339 0.0
340 } else {
341 self.velocity
342 };
343 self.phase = Phase::Idle;
344 Some(ScaleEvent {
345 phase: ScalePhase::End,
346 scale_delta: 1.0,
347 focal,
348 velocity,
349 })
350 }
351}
352
353pub struct PinchDetectorView<State: 'static> {
355 child: AnyView<State>,
356 on_scale: Option<crate::authoring::TypedArgCallback<State, ScaleEvent>>,
357}
358
359pub fn pinch_detector<State: 'static, V: View<State>>(child: V) -> PinchDetectorView<State> {
363 PinchDetectorView {
364 child: any(child),
365 on_scale: None,
366 }
367}
368
369impl<State: 'static> PinchDetectorView<State> {
370 pub fn on_scale<F: Fn(&mut State, ScaleEvent) + 'static>(mut self, on_scale: F) -> Self {
373 self.on_scale = Some(std::rc::Rc::new(on_scale));
374 self
375 }
376}
377
378#[derive(Clone, Copy, Debug, PartialEq, Eq)]
380enum ChildRoute {
381 Forwarding,
383 StealPending,
385 Stolen,
387}
388
389pub struct PinchDetectorWidget {
391 child: ChildPod,
392 recognizer: PinchRecognizer,
393 on_scale: Option<crate::authoring::ErasedArgCallback<ScaleEvent>>,
394 claimant: Option<PointerId>,
396 route: ChildRoute,
397 last_frame_ms: f64,
399 scroll_veto: Option<Rc<Cell<bool>>>,
404}
405
406impl<State: 'static> View<State> for PinchDetectorView<State> {
407 type Element = PinchDetectorWidget;
408
409 fn build(&self, ctx: &mut BuildCtx<'_>) -> PinchDetectorWidget {
410 PinchDetectorWidget {
411 child: crate::authoring::build_child(&self.child, ctx),
412 recognizer: PinchRecognizer::new(),
413 on_scale: self
414 .on_scale
415 .as_ref()
416 .map(crate::authoring::erase_callback_arg),
417 claimant: None,
418 route: ChildRoute::Forwarding,
419 last_frame_ms: 0.0,
420 scroll_veto: None,
421 }
422 }
423
424 fn rebuild(
425 &self,
426 prev: &Self,
427 element: &mut PinchDetectorWidget,
428 ctx: &mut BuildCtx<'_>,
429 ) -> ChangeFlags {
430 element.on_scale = self
431 .on_scale
432 .as_ref()
433 .map(crate::authoring::erase_callback_arg);
434 crate::authoring::rebuild_child(&prev.child, &self.child, &mut element.child, ctx)
435 }
436
437 fn teardown(&self, element: &mut PinchDetectorWidget, ctx: &mut BuildCtx<'_>) {
438 crate::authoring::teardown_child(&self.child, &mut element.child, ctx);
439 }
440}
441
442impl PinchDetectorWidget {
443 fn fire(&mut self, ctx: &mut EventCtx, scale: ScaleEvent) {
445 if let Some(cb) = self.on_scale.as_mut() {
446 cb(ctx, scale);
447 ctx.request_redraw();
448 }
449 }
450
451 fn recognize(&mut self, ctx: &mut EventCtx, id: PointerId, p: &PointerEvent) {
454 let Some(scale) = self.recognizer.handle(id, p, self.last_frame_ms) else {
455 return;
456 };
457 if scale.phase == ScalePhase::Begin && self.route == ChildRoute::Forwarding {
458 self.route = ChildRoute::StealPending;
459 }
460 if p.phase != PointerPhase::Cancel {
461 self.fire(ctx, scale);
462 }
463 }
464
465 fn sync_scroll_veto(&self) {
472 if let Some(veto) = &self.scroll_veto {
473 veto.set(self.recognizer.contact_count() >= 2);
474 }
475 }
476
477 fn route_claimant(&mut self, ctx: &mut EventCtx, event: &InputEvent, p: &PointerEvent) {
479 let ends = matches!(p.phase, PointerPhase::Up | PointerPhase::Cancel);
480 match self.route {
481 ChildRoute::Forwarding => {
482 self.child.event_child(ctx, event);
483 if ends {
484 self.child.set_active(false);
485 }
486 }
487 ChildRoute::StealPending => {
488 let cancel = InputEvent::Pointer(PointerEvent {
489 phase: PointerPhase::Cancel,
490 ..*p
491 });
492 self.child.event_child(ctx, &cancel);
493 self.child.set_active(false);
494 self.route = ChildRoute::Stolen;
495 }
496 ChildRoute::Stolen => {}
497 }
498 if ends {
499 self.claimant = None;
500 self.route = ChildRoute::Forwarding;
501 self.recognizer.reset();
502 self.scroll_veto = None;
503 }
504 }
505}
506
507impl Widget for PinchDetectorWidget {
508 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
509 let size = self.child.layout_child(ctx, bc);
510 self.child.set_origin(Point::ZERO);
511 bc.constrain(size)
512 }
513
514 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
515 self.last_frame_ms = ctx.frame_time().as_secs_f64() * 1000.0;
518 self.child.paint_child(ctx, scene);
519 }
520
521 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
522 if event.is_broadcast() {
523 self.child.event_child(ctx, event);
524 return EventResult::Ignored;
525 }
526 match event {
527 InputEvent::Pointer(p) => {
528 let id = ctx.pointer_id();
529 let starts = p.phase == PointerPhase::Down && presses(p);
530 if self.claimant.is_none() || (starts && self.claimant == Some(id)) {
531 if !starts {
532 return self.child.event_child(ctx, event);
534 }
535 self.recognizer.reset();
537 self.claimant = Some(id);
538 self.route = ChildRoute::Forwarding;
539 self.scroll_veto = ambient_scroll_veto();
546 ctx.capture_pointer();
547 ctx.capture_contacts();
548 self.recognize(ctx, id, p);
549 self.sync_scroll_veto();
550 self.child.event_child(ctx, event);
551 return EventResult::Handled;
552 }
553 self.recognize(ctx, id, p);
554 self.sync_scroll_veto();
555 if self.claimant == Some(id) {
556 self.route_claimant(ctx, event, p);
557 }
558 EventResult::Handled
560 }
561 InputEvent::Scale(scale) => {
562 let result = self.child.event_child(ctx, event);
563 if result == EventResult::Ignored && self.on_scale.is_some() {
564 self.fire(ctx, *scale);
565 return EventResult::Handled;
566 }
567 result
568 }
569 _ => self.child.event_child(ctx, event),
570 }
571 }
572
573 fn semantics(&self, ctx: &mut SemanticsCtx) {
574 self.child.semantics_child(ctx);
576 }
577
578 crate::authoring::visit_children!(child);
579}
580
581#[cfg(test)]
582mod tests {
583 use super::*;
584 use frust_core::{FrameTime, PointerButton, RenderRoot};
585 use std::cell::RefCell;
586 use std::rc::Rc;
587
588 fn pe(phase: PointerPhase, x: f64, y: f64) -> PointerEvent {
589 PointerEvent {
590 phase,
591 position: Point::new(x, y),
592 button: PointerButton::Primary,
593 }
594 }
595
596 const T0: PointerId = PointerId::touch(0);
597 const T1: PointerId = PointerId::touch(1);
598 const T2: PointerId = PointerId::touch(2);
599
600 fn paired() -> PinchRecognizer {
602 let mut r = PinchRecognizer::new();
603 assert_eq!(r.handle(T0, &pe(PointerPhase::Down, 40.0, 50.0), 0.0), None);
604 assert_eq!(r.handle(T1, &pe(PointerPhase::Down, 60.0, 50.0), 0.0), None);
605 r
606 }
607
608 fn begun() -> PinchRecognizer {
610 let mut r = paired();
611 let begin = r
612 .handle(T1, &pe(PointerPhase::Move, 80.0, 50.0), 16.0)
613 .expect("past the slop");
614 assert_eq!(begin.phase, ScalePhase::Begin);
615 r
616 }
617
618 #[test]
619 fn two_finger_spread_begins_updates_and_ends_at_the_midpoint() {
620 let mut r = paired();
621 let begin = r
622 .handle(T1, &pe(PointerPhase::Move, 80.0, 50.0), 16.0)
623 .unwrap();
624 assert_eq!(begin.phase, ScalePhase::Begin);
625 assert_eq!(begin.scale_delta, 1.0, "the slop is consumed, not jumped");
626 assert_eq!(begin.focal, Point::new(60.0, 50.0));
627 assert!(r.is_pinching());
628
629 let update = r
630 .handle(T1, &pe(PointerPhase::Move, 100.0, 50.0), 32.0)
631 .unwrap();
632 assert_eq!(update.phase, ScalePhase::Update);
633 assert!((update.scale_delta - 1.5).abs() < 1e-12, "40 → 60 px");
634 assert_eq!(update.focal, Point::new(70.0, 50.0));
635
636 let end = r
637 .handle(T1, &pe(PointerPhase::Up, 100.0, 50.0), 48.0)
638 .unwrap();
639 assert_eq!(end.phase, ScalePhase::End);
640 assert_eq!(end.scale_delta, 1.0);
641 assert_eq!(end.focal, Point::new(70.0, 50.0));
642 assert!(!r.is_pinching());
643 }
644
645 #[test]
646 fn spreading_the_first_contact_counts_too() {
647 let mut r = paired();
648 let begin = r
649 .handle(T0, &pe(PointerPhase::Move, 20.0, 50.0), 16.0)
650 .unwrap();
651 assert_eq!(begin.phase, ScalePhase::Begin);
652 assert_eq!(begin.focal, Point::new(40.0, 50.0));
653 }
654
655 #[test]
656 fn constant_distance_drag_reports_unit_scale_and_a_moving_focal() {
657 let mut r = begun(); let mut product = 1.0;
659 let mut last_focal_x = 60.0;
660 let mut t = 16.0;
661 for step in 1..=10 {
662 t += 8.0;
663 let x = 40.0 + step as f64;
664 for (id, pos) in [(T1, x + 40.0), (T0, x)] {
666 let u = r.handle(id, &pe(PointerPhase::Move, pos, 50.0), t).unwrap();
667 assert_eq!(u.phase, ScalePhase::Update);
668 assert!((u.scale_delta - 1.0).abs() < 0.03, "{u:?}");
669 assert!(u.focal.x > last_focal_x, "focal moves with the drag");
670 last_focal_x = u.focal.x;
671 product *= u.scale_delta;
672 }
673 }
674 assert!(
675 (product - 1.0).abs() < 1e-9,
676 "net scale is 1, got {product}"
677 );
678 assert_eq!(last_focal_x, 70.0);
679 }
680
681 #[test]
682 fn a_single_finger_never_pinches() {
683 let mut r = PinchRecognizer::new();
684 assert_eq!(r.handle(T0, &pe(PointerPhase::Down, 10.0, 10.0), 0.0), None);
685 for i in 1..20 {
686 let x = 10.0 + 15.0 * i as f64;
687 assert_eq!(
688 r.handle(T0, &pe(PointerPhase::Move, x, 10.0), i as f64 * 16.0),
689 None
690 );
691 }
692 assert_eq!(
693 r.handle(T0, &pe(PointerPhase::Up, 300.0, 10.0), 400.0),
694 None
695 );
696 assert_eq!(r.contact_count(), 0);
697 }
698
699 #[test]
700 fn lifting_the_second_finger_ends_and_the_first_pans_on_unrecognised() {
701 let mut r = begun();
702 let end = r.handle(T1, &pe(PointerPhase::Up, 80.0, 50.0), 32.0);
703 assert_eq!(end.map(|e| e.phase), Some(ScalePhase::End));
704 assert_eq!(r.contact_count(), 1);
705 assert_eq!(
707 r.handle(T0, &pe(PointerPhase::Move, 140.0, 90.0), 48.0),
708 None
709 );
710 assert_eq!(r.handle(T0, &pe(PointerPhase::Up, 140.0, 90.0), 64.0), None);
711 }
712
713 #[test]
714 fn lifting_the_first_finger_ends_and_drops_every_other_contact() {
715 let mut r = begun();
716 let end = r.handle(T0, &pe(PointerPhase::Up, 40.0, 50.0), 32.0);
717 assert_eq!(end.map(|e| e.phase), Some(ScalePhase::End));
718 assert_eq!(r.contact_count(), 0, "the claimant took the others with it");
719 assert_eq!(
720 r.handle(T1, &pe(PointerPhase::Move, 120.0, 50.0), 48.0),
721 None
722 );
723 }
724
725 #[test]
726 fn cancel_ends_the_pinch_with_no_velocity() {
727 for id in [T0, T1] {
728 let mut r = begun();
729 r.handle(T1, &pe(PointerPhase::Move, 100.0, 50.0), 24.0);
730 let end = r
731 .handle(id, &pe(PointerPhase::Cancel, 0.0, 0.0), 32.0)
732 .unwrap();
733 assert_eq!(end.phase, ScalePhase::End);
734 assert_eq!(end.velocity, 0.0);
735 assert!(!r.is_pinching());
736 }
737 }
738
739 #[test]
740 fn velocity_sign_follows_spread_and_pinch() {
741 let mut spread = begun();
742 let mut v = 0.0;
743 for (i, x) in [90.0, 100.0, 110.0].into_iter().enumerate() {
744 let t = 16.0 * (i + 2) as f64;
745 v = spread
746 .handle(T1, &pe(PointerPhase::Move, x, 50.0), t)
747 .unwrap()
748 .velocity;
749 }
750 assert!(v > 0.0, "spreading is positive, got {v}");
751 let end = spread
752 .handle(T1, &pe(PointerPhase::Up, 110.0, 50.0), 64.0)
753 .unwrap();
754 assert!(end.velocity > 0.0, "a prompt release keeps the rate");
755
756 let mut pinch = begun();
757 for (i, x) in [75.0, 70.0, 65.0].into_iter().enumerate() {
758 let t = 16.0 * (i + 2) as f64;
759 v = pinch
760 .handle(T1, &pe(PointerPhase::Move, x, 50.0), t)
761 .unwrap()
762 .velocity;
763 }
764 assert!(v < 0.0, "pinching is negative, got {v}");
765 }
766
767 #[test]
768 fn velocity_is_a_log_rate_and_survives_shared_timestamps() {
769 let mut r = begun(); let a = r
772 .handle(T1, &pe(PointerPhase::Move, 90.0, 50.0), 16.0)
773 .unwrap();
774 assert_eq!(a.velocity, 0.0);
775 let b = r
777 .handle(T1, &pe(PointerPhase::Move, 120.0, 50.0), 116.0)
778 .unwrap();
779 let expected = 2.0_f64.ln() / 0.1;
780 assert!((b.velocity - expected).abs() < 1e-9, "{b:?}");
781 }
782
783 #[test]
784 fn a_stale_release_reports_no_velocity() {
785 let mut r = begun();
786 r.handle(T1, &pe(PointerPhase::Move, 100.0, 50.0), 32.0);
787 let end = r
788 .handle(
789 T1,
790 &pe(PointerPhase::Up, 100.0, 50.0),
791 32.0 + VELOCITY_WINDOW_MS + 1.0,
792 )
793 .unwrap();
794 assert_eq!(end.velocity, 0.0, "held still before lifting");
795 }
796
797 #[test]
798 fn slop_gates_begin() {
799 let mut r = paired(); assert_eq!(
802 r.handle(T1, &pe(PointerPhase::Move, 77.0, 50.0), 16.0),
803 None
804 );
805 assert_eq!(
807 r.handle(T0, &pe(PointerPhase::Move, 57.0, 50.0), 32.0),
808 None
809 );
810 assert_eq!(
811 r.handle(T1, &pe(PointerPhase::Move, 77.0, 80.0), 48.0),
812 None
813 );
814 assert!(!r.is_pinching());
815 assert_eq!(
817 r.handle(T1, &pe(PointerPhase::Move, 77.0, 50.0), 64.0),
818 None
819 );
820 let begin = r.handle(T1, &pe(PointerPhase::Move, 96.0, 50.0), 80.0);
821 assert_eq!(begin.map(|e| e.phase), Some(ScalePhase::Begin));
822 const { assert!(PINCH_SLOP > 17.0 && PINCH_SLOP < 19.0) };
823 }
824
825 #[test]
826 fn a_third_contact_waits_for_a_pair_member_to_lift() {
827 let mut r = begun();
828 assert_eq!(
829 r.handle(T2, &pe(PointerPhase::Down, 200.0, 50.0), 20.0),
830 None
831 );
832 assert_eq!(
833 r.handle(T2, &pe(PointerPhase::Move, 300.0, 50.0), 24.0),
834 None
835 );
836 let end = r.handle(T1, &pe(PointerPhase::Up, 80.0, 50.0), 32.0);
837 assert_eq!(end.map(|e| e.phase), Some(ScalePhase::End));
838 assert_eq!(r.contact_count(), 2);
840 assert_eq!(
841 r.handle(T2, &pe(PointerPhase::Move, 310.0, 50.0), 48.0),
842 None
843 );
844 let begin = r.handle(T2, &pe(PointerPhase::Move, 340.0, 50.0), 64.0);
845 assert_eq!(begin.map(|e| e.phase), Some(ScalePhase::Begin));
846 }
847
848 #[test]
849 fn coincident_contacts_never_produce_a_zero_scale() {
850 let mut r = begun();
851 let collapse = r
852 .handle(T1, &pe(PointerPhase::Move, 40.0, 50.0), 32.0)
853 .unwrap();
854 assert!(collapse.scale_delta > 0.0 && collapse.scale_delta.is_finite());
855 let reopen = r
856 .handle(T1, &pe(PointerPhase::Move, 80.0, 50.0), 48.0)
857 .unwrap();
858 assert!((collapse.scale_delta * reopen.scale_delta - 1.0).abs() < 1e-12);
859 }
860
861 #[derive(Default)]
864 struct PinchState {
865 scales: Vec<ScaleEvent>,
866 }
867
868 struct Recorder {
872 log: Rc<RefCell<Vec<PointerPhase>>>,
873 handles_scale: bool,
874 }
875 struct RecorderWidget {
876 log: Rc<RefCell<Vec<PointerPhase>>>,
877 handles_scale: bool,
878 }
879 impl View<PinchState> for Recorder {
880 type Element = RecorderWidget;
881 fn build(&self, _ctx: &mut BuildCtx<'_>) -> RecorderWidget {
882 RecorderWidget {
883 log: self.log.clone(),
884 handles_scale: self.handles_scale,
885 }
886 }
887 fn rebuild(
888 &self,
889 _p: &Self,
890 _e: &mut RecorderWidget,
891 _c: &mut BuildCtx<'_>,
892 ) -> ChangeFlags {
893 ChangeFlags::NONE
894 }
895 }
896 impl Widget for RecorderWidget {
897 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
898 bc.max()
899 }
900 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
901 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
902 match event {
903 InputEvent::Pointer(p) => {
904 self.log.borrow_mut().push(p.phase);
905 if p.phase == PointerPhase::Down {
906 ctx.capture_pointer();
907 }
908 EventResult::Handled
909 }
910 InputEvent::Scale(_) if self.handles_scale => EventResult::Handled,
911 _ => EventResult::Ignored,
912 }
913 }
914 }
915
916 struct NullScene;
917 impl PaintScene for NullScene {
918 fn fill_rect(&mut self, _o: Point, _s: Size, _c: peniko::Color) {}
919 fn draw_text(&mut self, _o: Point, _t: &str) {}
920 }
921
922 type Root = RenderRoot<PinchState, PinchDetectorView<PinchState>>;
923
924 fn detector_root(handles_scale: bool) -> (Root, Rc<RefCell<Vec<PointerPhase>>>) {
925 let log = Rc::new(RefCell::new(Vec::new()));
926 let child_log = log.clone();
927 let mut logic = move |_: &mut PinchState| {
928 pinch_detector(Recorder {
929 log: child_log.clone(),
930 handles_scale,
931 })
932 .on_scale(|s: &mut PinchState, e| s.scales.push(e))
933 };
934 let mut root: Root = RenderRoot::new();
935 root.rebuild(&mut logic, &mut PinchState::default());
936 root.layout(Size::new(400.0, 400.0));
937 (root, log)
938 }
939
940 fn touch(slot: u32, phase: PointerPhase, x: f64, y: f64) -> InputEvent {
941 InputEvent::PointerContact {
942 pointer_id: PointerId::touch(slot),
943 event: pe(phase, x, y),
944 }
945 }
946
947 fn phases(state: &PinchState) -> Vec<ScalePhase> {
948 state.scales.iter().map(|e| e.phase).collect()
949 }
950
951 #[test]
952 fn wrapper_receives_the_second_finger_and_reports_the_pinch() {
953 use PointerPhase::{Down, Move, Up};
954 let (mut root, child) = detector_root(false);
955 let mut state = PinchState::default();
956 let mut sink = NullScene;
957 root.paint(&mut sink, FrameTime::from_nanos(0));
958 root.event(&mut state, &touch(0, Down, 100.0, 100.0));
959 root.event(&mut state, &touch(1, Down, 120.0, 100.0));
960 root.paint(&mut sink, FrameTime::from_nanos(16_000_000));
961 root.event(&mut state, &touch(1, Move, 160.0, 100.0)); root.paint(&mut sink, FrameTime::from_nanos(32_000_000));
963 root.event(&mut state, &touch(1, Move, 220.0, 100.0)); root.event(&mut state, &touch(1, Up, 220.0, 100.0));
965 assert_eq!(
966 phases(&state),
967 [ScalePhase::Begin, ScalePhase::Update, ScalePhase::End]
968 );
969 let update = state.scales[1];
970 assert!((update.scale_delta - 2.0).abs() < 1e-12);
971 assert_eq!(update.focal, Point::new(160.0, 100.0));
972 assert!(update.velocity > 0.0);
973 assert!(
974 root.is_pointer_captured(),
975 "the first finger still holds it"
976 );
977
978 root.event(&mut state, &touch(0, Move, 50.0, 50.0));
981 root.event(&mut state, &touch(0, Up, 50.0, 50.0));
982 assert_eq!(state.scales.len(), 3);
983 assert!(!root.is_pointer_captured());
984 assert_eq!(*child.borrow(), [Down, PointerPhase::Cancel]);
985
986 root.event(&mut state, &touch(0, Down, 10.0, 10.0));
988 root.event(&mut state, &touch(0, Up, 10.0, 10.0));
989 assert_eq!(
990 *child.borrow(),
991 [Down, PointerPhase::Cancel, Down, Up],
992 "the steal lasted one gesture"
993 );
994 }
995
996 #[test]
997 fn wrapper_single_finger_drag_passes_through_to_the_child() {
998 use PointerPhase::{Down, Move, Up};
999 let (mut root, child) = detector_root(false);
1000 let mut state = PinchState::default();
1001 for event in [
1002 touch(0, Down, 10.0, 10.0),
1003 touch(0, Move, 200.0, 10.0),
1004 touch(0, Move, 300.0, 200.0),
1005 touch(0, Up, 300.0, 200.0),
1006 ] {
1007 root.event(&mut state, &event);
1008 }
1009 assert!(state.scales.is_empty());
1010 assert_eq!(*child.borrow(), [Down, Move, Move, Up]);
1011 assert!(!root.is_pointer_captured());
1012 }
1013
1014 #[test]
1015 fn wrapper_second_finger_inside_the_slop_leaves_the_child_alone() {
1016 use PointerPhase::{Down, Move, Up};
1017 let (mut root, child) = detector_root(false);
1018 let mut state = PinchState::default();
1019 root.event(&mut state, &touch(0, Down, 100.0, 100.0));
1020 root.event(&mut state, &touch(1, Down, 140.0, 100.0));
1021 root.event(&mut state, &touch(1, Move, 145.0, 100.0));
1022 root.event(&mut state, &touch(1, Up, 145.0, 100.0));
1023 root.event(&mut state, &touch(0, Move, 110.0, 100.0));
1024 root.event(&mut state, &touch(0, Up, 110.0, 100.0));
1025 assert!(state.scales.is_empty());
1026 assert_eq!(
1027 *child.borrow(),
1028 [Down, Move, Up],
1029 "the extra finger never reaches the child"
1030 );
1031 }
1032
1033 #[test]
1034 fn wrapper_cancel_ends_without_calling_on_scale() {
1035 use PointerPhase::{Cancel, Down, Move};
1036 let (mut root, child) = detector_root(false);
1037 let mut state = PinchState::default();
1038 root.event(&mut state, &touch(0, Down, 100.0, 100.0));
1039 root.event(&mut state, &touch(1, Down, 120.0, 100.0));
1040 root.event(&mut state, &touch(0, Move, 60.0, 100.0)); root.event(&mut state, &touch(0, Cancel, 60.0, 100.0));
1042 assert_eq!(phases(&state), [ScalePhase::Begin]);
1043 assert!(!root.is_pointer_captured());
1044 assert_eq!(*child.borrow(), [Down, Cancel]);
1046 }
1047
1048 #[test]
1049 fn wrapper_offers_a_desktop_scale_to_the_child_first() {
1050 let event = InputEvent::Scale(ScaleEvent {
1051 phase: ScalePhase::Update,
1052 scale_delta: 1.1,
1053 focal: Point::new(50.0, 50.0),
1054 velocity: 0.0,
1055 });
1056 let (mut root, _) = detector_root(false);
1057 let mut state = PinchState::default();
1058 root.event(&mut state, &event);
1059 assert_eq!(state.scales.len(), 1, "an ignored scale reaches on_scale");
1060 assert_eq!(state.scales[0].scale_delta, 1.1);
1061
1062 let (mut root, _) = detector_root(true);
1063 let mut state = PinchState::default();
1064 root.event(&mut state, &event);
1065 assert!(state.scales.is_empty(), "the child handled it");
1066 }
1067
1068 #[test]
1069 fn wrapper_mouse_press_is_an_ordinary_single_pointer_gesture() {
1070 use PointerPhase::{Down, Up};
1071 let (mut root, child) = detector_root(false);
1072 let mut state = PinchState::default();
1073 root.event(&mut state, &InputEvent::Pointer(pe(Down, 10.0, 10.0)));
1074 root.event(&mut state, &InputEvent::Pointer(pe(Up, 10.0, 10.0)));
1075 assert!(state.scales.is_empty());
1076 assert_eq!(*child.borrow(), [Down, Up]);
1077 }
1078}