input_pipeline/gestures/
scroll.rs

1// Copyright 2022 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use super::gesture_arena::{
6    self, DetailedReasonFloat, DetailedReasonUint, EndGestureEvent, ExamineEventResult, MouseEvent,
7    ProcessBufferedEventsResult, ProcessNewEventResult, Reason, RecognizedGesture, TouchpadEvent,
8    VerifyEventResult,
9};
10use crate::mouse_binding;
11use crate::utils::{Position, euclidean_distance};
12use maplit::hashset;
13
14/// The initial state of this recognizer, before 2 finger contact has been detected.
15#[derive(Debug)]
16pub(super) struct InitialContender {
17    /// When one finger contacted, movement > this threshold, recognizer should exit.
18    pub(super) motion_threshold_in_mm: f32,
19
20    /// The minimum movement in millimeters on surface to recognize as a scroll.
21    pub(super) min_movement_in_mm: f32,
22
23    /// The maximum movement in millimeters on surface to recognize as a scroll. If recognizer can
24    /// not detect the scroll direction when movement more than this number, return Mismatch to
25    /// end this recognizer.
26    pub(super) max_movement_in_mm: f32,
27
28    /// The limit tangent for direction detect, for example if we only want to allow ±15° the number
29    /// is 0.26794919243.
30    pub(super) limit_tangent_for_direction: f32,
31}
32
33/// The state when this recognizer has detected 1 finger contact, before 2 finger contact, and finger movement > threshold.
34#[derive(Debug)]
35struct OneFingerContactContender {
36    /// When one finger contacted, movement > this threshold, recognizer should exit.
37    motion_threshold_in_mm: f32,
38
39    /// The minimum movement in millimeters on surface to recognize as a scroll.
40    min_movement_in_mm: f32,
41
42    /// The maximum movement in millimeters on surface to recognize as a scroll. If recognizer can
43    /// not detect the scroll direction when movement more than this number, return Mismatch to
44    /// end this recognizer.
45    max_movement_in_mm: f32,
46
47    /// The limit tangent for direction detect.
48    limit_tangent_for_direction: f32,
49
50    /// The initial contact position on touchpad surface.
51    initial_position: ContactPosition,
52}
53
54/// The state when this recognizer has detected 2 finger contact, before finger movement > threshold.
55#[derive(Debug)]
56struct TwoFingerContactContender {
57    /// The minimum movement in millimeters on surface to recognize as a scroll.
58    min_movement_in_mm: f32,
59
60    /// The maximum movement in millimeters on surface to recognize as a scroll. If recognizer can
61    /// not detect the scroll direction when movement more than this number, return Mismatch to
62    /// end this recognizer.
63    max_movement_in_mm: f32,
64
65    /// The limit tangent for direction detect.
66    limit_tangent_for_direction: f32,
67
68    /// The initial contact position on touchpad surface.
69    initial_positions: ContactPositions,
70}
71
72#[derive(Debug, PartialEq, Clone, Copy)]
73enum ScrollDirection {
74    Left,
75    Right,
76    Up,
77    Down,
78}
79
80/// The state when this recognizer has detected 2 finger contact and a movement > threshold and
81/// detected a scroll direction, but the gesture arena has not declared this recognizer the winner.
82#[derive(Debug, PartialEq)]
83struct MatchedContender {
84    /// The limit tangent for direction detect.
85    limit_tangent_for_direction: f32,
86
87    /// The initial contact position on touchpad surface.
88    initial_positions: ContactPositions,
89
90    /// detected direction of scrolling.
91    direction: ScrollDirection,
92}
93
94/// The state when this recognizer has won the contest.
95#[derive(Debug)]
96struct Winner {
97    /// The limit tangent for direction detect.
98    limit_tangent_for_direction: f32,
99
100    /// detected direction of scrolling.
101    direction: ScrollDirection,
102
103    /// The last contact position on touchpad surface.
104    last_positions: ContactPositions,
105}
106
107enum Error {
108    FingerNotMatch,
109    MustBe2Finger,
110}
111
112#[derive(Debug, PartialEq, Clone, Copy)]
113struct ContactPosition {
114    id: u32,
115    position: Position,
116}
117
118#[derive(Debug, PartialEq)]
119struct ContactPositions {
120    first_contact: ContactPosition,
121    second_contact: ContactPosition,
122}
123
124impl ContactPositions {
125    fn from(event: &TouchpadEvent) -> Result<Self, Error> {
126        let mut contact_positions: Vec<ContactPosition> = Vec::new();
127        for c in &event.contacts {
128            contact_positions.push(ContactPosition { id: c.id, position: c.position.clone() });
129        }
130
131        if contact_positions.len() != 2 {
132            return Err(Error::MustBe2Finger);
133        }
134        contact_positions.sort_by_key(|a| a.id);
135        Ok(ContactPositions {
136            first_contact: contact_positions[0],
137            second_contact: contact_positions[1],
138        })
139    }
140
141    fn get_movements(&self, other: &Self) -> Result<Vec<Movement>, Error> {
142        if self.first_contact.id != other.first_contact.id
143            || self.second_contact.id != other.second_contact.id
144        {
145            return Err(Error::FingerNotMatch);
146        }
147
148        Ok(vec![
149            Movement { from: self.first_contact.position, to: other.first_contact.position },
150            Movement { from: self.second_contact.position, to: other.second_contact.position },
151        ])
152    }
153}
154
155/// a movement action, includes from position and to position.
156struct Movement {
157    /// From position.
158    from: Position,
159    /// To position.
160    to: Position,
161}
162
163impl Movement {
164    fn is_in_direction(
165        &self,
166        direction: ScrollDirection,
167        limit_tangent_for_direction: f32,
168    ) -> bool {
169        let dx = self.to.x - self.from.x;
170        let dy = self.to.y - self.from.y;
171
172        // filter out the case movement not in given direction.
173        match direction {
174            ScrollDirection::Left => {
175                if dx >= 0.0 {
176                    return false;
177                }
178            }
179            ScrollDirection::Right => {
180                if dx <= 0.0 {
181                    return false;
182                }
183            }
184            ScrollDirection::Up => {
185                if dy >= 0.0 {
186                    return false;
187                }
188            }
189            ScrollDirection::Down => {
190                if dy <= 0.0 {
191                    return false;
192                }
193            }
194        }
195
196        let (long, short) = match direction {
197            ScrollDirection::Left => (dx.abs(), dy.abs()),
198            ScrollDirection::Right => (dx.abs(), dy.abs()),
199            ScrollDirection::Up => (dy.abs(), dx.abs()),
200            ScrollDirection::Down => (dy.abs(), dx.abs()),
201        };
202
203        short < (long * limit_tangent_for_direction)
204    }
205
206    fn direction(&self, limit_tangent_for_direction: f32) -> Option<ScrollDirection> {
207        let directions = [
208            ScrollDirection::Left,
209            ScrollDirection::Right,
210            ScrollDirection::Up,
211            ScrollDirection::Down,
212        ];
213        for d in directions {
214            if self.is_in_direction(d, limit_tangent_for_direction) {
215                return Some(d);
216            }
217        }
218
219        None
220    }
221
222    fn has_delta_on_reverse_direction(&self, want_direction: ScrollDirection) -> bool {
223        let dx = self.to.x - self.from.x;
224        let dy = self.to.y - self.from.y;
225        match want_direction {
226            ScrollDirection::Up => dy > 0.0,
227            ScrollDirection::Down => dy < 0.0,
228            ScrollDirection::Left => dx > 0.0,
229            ScrollDirection::Right => dx < 0.0,
230        }
231    }
232}
233
234impl InitialContender {
235    #[allow(clippy::boxed_local, reason = "mass allow for https://fxbug.dev/381896734")]
236    fn into_one_finger_contact_contender(
237        self: Box<Self>,
238        initial_position: ContactPosition,
239    ) -> Box<dyn gesture_arena::Contender> {
240        Box::new(OneFingerContactContender {
241            motion_threshold_in_mm: self.motion_threshold_in_mm,
242            min_movement_in_mm: self.min_movement_in_mm,
243            max_movement_in_mm: self.max_movement_in_mm,
244            limit_tangent_for_direction: self.limit_tangent_for_direction,
245            initial_position,
246        })
247    }
248
249    #[allow(clippy::boxed_local, reason = "mass allow for https://fxbug.dev/381896734")]
250    fn into_two_finger_contact_contender(
251        self: Box<Self>,
252        initial_positions: ContactPositions,
253    ) -> Box<dyn gesture_arena::Contender> {
254        Box::new(TwoFingerContactContender {
255            min_movement_in_mm: self.min_movement_in_mm,
256            max_movement_in_mm: self.max_movement_in_mm,
257            limit_tangent_for_direction: self.limit_tangent_for_direction,
258            initial_positions,
259        })
260    }
261}
262
263impl gesture_arena::Contender for InitialContender {
264    fn examine_event(self: Box<Self>, event: &TouchpadEvent) -> ExamineEventResult {
265        let num_pressed_buttons = event.pressed_buttons.len();
266        if num_pressed_buttons > 0 {
267            return ExamineEventResult::Mismatch(Reason::DetailedUint(DetailedReasonUint {
268                criterion: "num_pressed_buttons",
269                min: None,
270                max: Some(0),
271                actual: num_pressed_buttons,
272            }));
273        }
274
275        let num_contacts = event.contacts.len();
276        match num_contacts {
277            1 => ExamineEventResult::Contender(self.into_one_finger_contact_contender(
278                ContactPosition { position: event.contacts[0].position, id: event.contacts[0].id },
279            )),
280            2 => {
281                match ContactPositions::from(event) {
282                    Ok(positions) => {
283                        return ExamineEventResult::Contender(
284                            self.into_two_finger_contact_contender(positions),
285                        );
286                    }
287                    Err(_) => {
288                        log::error!("failed to parse positions");
289                        return ExamineEventResult::Mismatch(Reason::Basic(
290                            "failed to parse positions",
291                        ));
292                    }
293                };
294            }
295            _ => ExamineEventResult::Mismatch(Reason::DetailedUint(DetailedReasonUint {
296                criterion: "num_contacts",
297                min: Some(1),
298                max: Some(2),
299                actual: num_contacts,
300            })),
301        }
302    }
303}
304
305impl OneFingerContactContender {
306    #[allow(clippy::boxed_local, reason = "mass allow for https://fxbug.dev/381896734")]
307    fn into_two_finger_contact_contender(
308        self: Box<Self>,
309        initial_positions: ContactPositions,
310    ) -> Box<dyn gesture_arena::Contender> {
311        Box::new(TwoFingerContactContender {
312            min_movement_in_mm: self.min_movement_in_mm,
313            max_movement_in_mm: self.max_movement_in_mm,
314            limit_tangent_for_direction: self.limit_tangent_for_direction,
315            initial_positions,
316        })
317    }
318}
319
320impl gesture_arena::Contender for OneFingerContactContender {
321    fn examine_event(self: Box<Self>, event: &TouchpadEvent) -> ExamineEventResult {
322        let num_pressed_buttons = event.pressed_buttons.len();
323        if num_pressed_buttons > 0 {
324            return ExamineEventResult::Mismatch(Reason::DetailedUint(DetailedReasonUint {
325                criterion: "num_pressed_buttons",
326                min: None,
327                max: Some(0),
328                actual: num_pressed_buttons,
329            }));
330        }
331
332        let num_contacts = event.contacts.len();
333        // Exit if the initial contact is moved > threshold.
334        if num_contacts > 0 {
335            match event.contacts.iter().find(|&c| c.id == self.initial_position.id) {
336                Some(contact) => {
337                    let displacement_mm =
338                        euclidean_distance(contact.position, self.initial_position.position);
339                    if displacement_mm >= self.motion_threshold_in_mm {
340                        return ExamineEventResult::Mismatch(Reason::DetailedFloat(
341                            DetailedReasonFloat {
342                                criterion: "displacement_mm",
343                                min: None,
344                                max: Some(self.motion_threshold_in_mm),
345                                actual: displacement_mm,
346                            },
347                        ));
348                    }
349                }
350                None => {
351                    return ExamineEventResult::Mismatch(Reason::Basic("initial contact lift"));
352                }
353            }
354        }
355        match num_contacts {
356            1 => ExamineEventResult::Contender(self),
357            2 => {
358                let current_positions = match ContactPositions::from(event) {
359                    Ok(positions) => positions,
360                    Err(_) => {
361                        log::error!("failed to parse positions");
362                        return ExamineEventResult::Mismatch(Reason::Basic(
363                            "failed to parse positions",
364                        ));
365                    }
366                };
367
368                ExamineEventResult::Contender(
369                    self.into_two_finger_contact_contender(current_positions),
370                )
371            }
372            _ => ExamineEventResult::Mismatch(Reason::DetailedUint(DetailedReasonUint {
373                criterion: "num_contacts",
374                min: Some(1),
375                max: Some(2),
376                actual: num_contacts,
377            })),
378        }
379    }
380}
381
382impl TwoFingerContactContender {
383    #[allow(clippy::boxed_local, reason = "mass allow for https://fxbug.dev/381896734")]
384    fn into_matched_contender(
385        self: Box<Self>,
386        direction: ScrollDirection,
387    ) -> Box<dyn gesture_arena::MatchedContender> {
388        Box::new(MatchedContender {
389            limit_tangent_for_direction: self.limit_tangent_for_direction,
390            initial_positions: self.initial_positions,
391            direction,
392        })
393    }
394}
395
396impl gesture_arena::Contender for TwoFingerContactContender {
397    fn examine_event(self: Box<Self>, event: &TouchpadEvent) -> ExamineEventResult {
398        let num_contacts = event.contacts.len();
399        if num_contacts != 2 {
400            return ExamineEventResult::Mismatch(Reason::DetailedUint(DetailedReasonUint {
401                criterion: "num_contacts",
402                min: Some(2),
403                max: Some(2),
404                actual: num_contacts,
405            }));
406        }
407
408        let num_pressed_buttons = event.pressed_buttons.len();
409        if num_pressed_buttons > 0 {
410            return ExamineEventResult::Mismatch(Reason::DetailedUint(DetailedReasonUint {
411                criterion: "num_pressed_buttons",
412                min: None,
413                max: Some(0),
414                actual: num_pressed_buttons,
415            }));
416        }
417
418        let current_positions = match ContactPositions::from(event) {
419            Ok(positions) => positions,
420            Err(_) => {
421                log::error!("failed to parse positions");
422                return ExamineEventResult::Mismatch(Reason::Basic("failed to parse positions"));
423            }
424        };
425
426        let movements = match self.initial_positions.get_movements(&current_positions) {
427            // new event contact id not match old event without a finger leave surface
428            // event, this is likely a bug in firmware or driver.
429            Err(_) => {
430                log::error!("new event contact id not match old event");
431                return ExamineEventResult::Mismatch(Reason::Basic(
432                    "contact ids changed since last event",
433                ));
434            }
435            Ok(m) => m,
436        };
437
438        // Both 2 fingers movement must > min_threshold.
439        if movements.iter().any(|movement| {
440            euclidean_distance(movement.to, movement.from) < self.min_movement_in_mm
441        }) {
442            return ExamineEventResult::Contender(self);
443        }
444
445        let directions: Vec<Option<ScrollDirection>> =
446            movements.iter().map(|m| m.direction(self.limit_tangent_for_direction)).collect();
447        if let Some(first_direction) = directions[0] {
448            if directions.iter().all(|&d| d == directions[0]) {
449                return ExamineEventResult::MatchedContender(
450                    self.into_matched_contender(first_direction),
451                );
452            }
453        }
454
455        // Stop try to match if any finger movement > max_threshold still no direction
456        // detected.
457        if movements.iter().any(|movement| {
458            euclidean_distance(movement.to, movement.from) > self.max_movement_in_mm
459        }) {
460            return ExamineEventResult::Mismatch(Reason::Basic(
461                "too much motion without clear direction",
462            ));
463        }
464
465        ExamineEventResult::Contender(self)
466    }
467}
468
469impl MatchedContender {
470    #[allow(clippy::boxed_local, reason = "mass allow for https://fxbug.dev/381896734")]
471    fn into_winner(
472        self: Box<Self>,
473        last_positions: ContactPositions,
474    ) -> Box<dyn gesture_arena::Winner> {
475        Box::new(Winner {
476            limit_tangent_for_direction: self.limit_tangent_for_direction,
477            direction: self.direction,
478            last_positions,
479        })
480    }
481}
482
483impl gesture_arena::MatchedContender for MatchedContender {
484    fn verify_event(self: Box<Self>, event: &TouchpadEvent) -> VerifyEventResult {
485        let num_contacts = event.contacts.len();
486        if num_contacts != 2 {
487            return VerifyEventResult::Mismatch(Reason::DetailedUint(DetailedReasonUint {
488                criterion: "num_contacts",
489                min: Some(2),
490                max: Some(2),
491                actual: num_contacts,
492            }));
493        }
494
495        let num_pressed_buttons = event.pressed_buttons.len();
496        if num_pressed_buttons > 0 {
497            return VerifyEventResult::Mismatch(Reason::DetailedUint(DetailedReasonUint {
498                criterion: "num_pressed_buttons",
499                min: None,
500                max: Some(0),
501                actual: num_pressed_buttons,
502            }));
503        }
504
505        let current_positions = match ContactPositions::from(event) {
506            Ok(positions) => positions,
507            Err(_) => {
508                log::error!("failed to parse positions");
509                return VerifyEventResult::Mismatch(Reason::Basic("failed to parse positions"));
510            }
511        };
512        let movements = match self.initial_positions.get_movements(&current_positions) {
513            // new event contact id not match old event without a finger leave surface
514            // event, this is likely a bug in firmware or driver.
515            Err(_) => {
516                log::error!("new event contact id not match old event");
517                return VerifyEventResult::Mismatch(Reason::Basic(
518                    "contact ids changed since last event",
519                ));
520            }
521            Ok(m) => m,
522        };
523
524        let directions: Vec<Option<ScrollDirection>> =
525            movements.into_iter().map(|m| m.direction(self.limit_tangent_for_direction)).collect();
526
527        if directions.iter().all(|&d| d == Some(self.direction)) {
528            return VerifyEventResult::MatchedContender(self);
529        }
530
531        VerifyEventResult::Mismatch(Reason::Basic("contacts moved in different directions"))
532    }
533
534    fn process_buffered_events(
535        self: Box<Self>,
536        events: Vec<TouchpadEvent>,
537    ) -> ProcessBufferedEventsResult {
538        let mut mouse_events: Vec<MouseEvent> = Vec::new();
539
540        for pair in events.windows(2) {
541            // Ignore events not having 2 contacts. No need to check pair[1],
542            // because if pair[1] is 1 and pair[0] is 2, MatchedContender or
543            // TwoFingerContactContender will return miss match, never reach to
544            // here.
545            if pair[0].contacts.len() != 2 {
546                continue;
547            }
548            assert_eq!(pair[1].contacts.len(), 2);
549            let old_positions = match ContactPositions::from(&pair[0]) {
550                Ok(positions) => positions,
551                Err(_) => ContactPositions {
552                    // Likely a bug in `GestureArena`, because all event here has been
553                    // verified in `examine_event` or `verify_event`.
554                    first_contact: ContactPosition { id: 0, position: Position { x: 0.0, y: 0.0 } },
555                    second_contact: ContactPosition {
556                        id: 0,
557                        position: Position { x: 0.0, y: 0.0 },
558                    },
559                },
560            };
561            mouse_events.push(touchpad_event_to_mouse_scroll_event(
562                self.direction,
563                old_positions,
564                &pair[1],
565            ));
566        }
567
568        let last_positions = match ContactPositions::from(&events[events.len() - 1]) {
569            Ok(positions) => positions,
570            Err(_) => ContactPositions {
571                // Likely a bug in `GestureArena`, because all event here has been
572                // verified in `examine_event` or `verify_event`.
573                first_contact: ContactPosition { id: 0, position: Position { x: 0.0, y: 0.0 } },
574                second_contact: ContactPosition { id: 0, position: Position { x: 0.0, y: 0.0 } },
575            },
576        };
577
578        ProcessBufferedEventsResult {
579            generated_events: mouse_events,
580            winner: Some(self.into_winner(last_positions)),
581            recognized_gesture: RecognizedGesture::Scroll,
582        }
583    }
584}
585
586impl gesture_arena::Winner for Winner {
587    fn process_new_event(self: Box<Self>, event: TouchpadEvent) -> ProcessNewEventResult {
588        match u8::try_from(event.contacts.len()).unwrap_or(u8::MAX) {
589            0 => ProcessNewEventResult::EndGesture(
590                EndGestureEvent::NoEvent,
591                Reason::DetailedUint(DetailedReasonUint {
592                    criterion: "num_contacts",
593                    min: Some(2),
594                    max: Some(2),
595                    actual: 0,
596                }),
597            ),
598            2 => {
599                let num_pressed_buttons = event.pressed_buttons.len();
600                if num_pressed_buttons > 0 {
601                    return ProcessNewEventResult::EndGesture(
602                        EndGestureEvent::UnconsumedEvent(event),
603                        Reason::DetailedUint(DetailedReasonUint {
604                            criterion: "num_pressed_buttons",
605                            min: None,
606                            max: Some(0),
607                            actual: num_pressed_buttons,
608                        }),
609                    );
610                }
611
612                let positions = match ContactPositions::from(&event) {
613                    Ok(positions) => positions,
614                    Err(_) => {
615                        log::error!("failed to parse positions");
616                        return ProcessNewEventResult::EndGesture(
617                            EndGestureEvent::UnconsumedEvent(event),
618                            Reason::Basic("failed to parse positions"),
619                        );
620                    }
621                };
622
623                let movements = match self.last_positions.get_movements(&positions) {
624                    // new event contact id not match old event without a finger leave
625                    // surface event, this is likely a bug in firmware or driver.
626                    Err(_) => {
627                        log::error!("new event contact id not match old event");
628                        return ProcessNewEventResult::EndGesture(
629                            EndGestureEvent::UnconsumedEvent(event),
630                            Reason::Basic("contact ids changed since last event"),
631                        );
632                    }
633                    Ok(m) => m,
634                };
635
636                if movements.iter().any(|m| m.has_delta_on_reverse_direction(self.direction)) {
637                    return ProcessNewEventResult::EndGesture(
638                        EndGestureEvent::UnconsumedEvent(event),
639                        Reason::Basic("inconsistent direction"),
640                    );
641                }
642
643                ProcessNewEventResult::ContinueGesture(
644                    Some(touchpad_event_to_mouse_scroll_event(
645                        self.direction,
646                        self.last_positions,
647                        &event,
648                    )),
649                    Box::new(Winner {
650                        limit_tangent_for_direction: self.limit_tangent_for_direction,
651                        direction: self.direction,
652                        last_positions: positions,
653                    }),
654                )
655            }
656            1 => ProcessNewEventResult::EndGesture(
657                EndGestureEvent::UnconsumedEvent(event),
658                Reason::DetailedUint(DetailedReasonUint {
659                    criterion: "num_contacts",
660                    min: Some(2),
661                    max: Some(2),
662                    actual: 1,
663                }),
664            ),
665            num_contacts @ 3.. => ProcessNewEventResult::EndGesture(
666                EndGestureEvent::UnconsumedEvent(event),
667                Reason::DetailedUint(DetailedReasonUint {
668                    criterion: "num_contacts",
669                    min: Some(2),
670                    max: Some(2),
671                    actual: usize::from(num_contacts),
672                }),
673            ),
674        }
675    }
676}
677
678fn wheel_delta_mm(delta: f32) -> Option<mouse_binding::WheelDelta> {
679    Some(mouse_binding::WheelDelta {
680        raw_data: mouse_binding::RawWheelDelta::Millimeters(delta),
681        physical_pixel: None,
682    })
683}
684
685// filter out motion not in given direction.
686fn filter_off_direction_movement(
687    direction: ScrollDirection,
688    offset_v: f32,
689    offset_h: f32,
690) -> (Option<mouse_binding::WheelDelta>, Option<mouse_binding::WheelDelta>) {
691    match direction {
692        ScrollDirection::Left => {
693            if offset_h > 0.0 {
694                (None, wheel_delta_mm(0.0))
695            } else {
696                (None, wheel_delta_mm(offset_h))
697            }
698        }
699        ScrollDirection::Right => {
700            if offset_h < 0.0 {
701                (None, wheel_delta_mm(0.0))
702            } else {
703                (None, wheel_delta_mm(offset_h))
704            }
705        }
706        ScrollDirection::Up => {
707            if offset_v > 0.0 {
708                (wheel_delta_mm(0.0), None)
709            } else {
710                (wheel_delta_mm(offset_v), None)
711            }
712        }
713        ScrollDirection::Down => {
714            if offset_v < 0.0 {
715                (wheel_delta_mm(0.0), None)
716            } else {
717                (wheel_delta_mm(offset_v), None)
718            }
719        }
720    }
721}
722
723fn touchpad_event_to_mouse_scroll_event(
724    direction: ScrollDirection,
725    old_positions: ContactPositions,
726    new_event: &TouchpadEvent,
727) -> MouseEvent {
728    // Pre-check already ensure contact fingers in new event are same with the old event.
729    // Use avg(finger1 movement + finger2 movement) as scroll offset.
730
731    // To compute the offset, for example the Y offset
732    // Y offset = avg(finger1 movement + finger2 movement) =
733    // ((new_finger1.y - old_finger1.y) + (new_finger2.y - old_finger2.y)) / 2 =
734    // (new_finger1.y + new_finger2.y - old_finger1.y - old_finger2.y) / 2
735
736    let offset_v = (new_event.contacts[0].position.y + new_event.contacts[1].position.y
737        - old_positions.first_contact.position.y
738        - old_positions.second_contact.position.y)
739        / 2.0;
740    let offset_h = (new_event.contacts[0].position.x + new_event.contacts[1].position.x
741        - old_positions.first_contact.position.x
742        - old_positions.second_contact.position.x)
743        / 2.0;
744
745    // If offset not in given ScrollDirection (maybe happen in early events), result 0
746    // scroll offset.
747    let (wheel_delta_v, wheel_delta_h) =
748        filter_off_direction_movement(direction, offset_v, offset_h);
749    MouseEvent {
750        timestamp: new_event.timestamp,
751        mouse_data: mouse_binding::MouseEvent::new(
752            mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
753                millimeters: Position { x: 0.0, y: 0.0 },
754            }),
755            wheel_delta_v,
756            wheel_delta_h,
757            mouse_binding::MousePhase::Wheel,
758            /* affected_buttons= */ hashset! {},
759            /* pressed_buttons= */ hashset! {},
760            Some(mouse_binding::PrecisionScroll::Yes),
761            /* wake_lease= */ None,
762        ),
763    }
764}
765
766#[cfg(test)]
767mod tests {
768    use super::*;
769    use crate::touch_binding;
770    use assert_matches::assert_matches;
771
772    use test_case::test_case;
773
774    const MOTION_THRESHOLD_IN_MM: f32 = 5.0;
775    const MIN_MOVEMENT_IN_MM: f32 = 10.0;
776    const MAX_MOVEMENT_IN_MM: f32 = 20.0;
777    const LIMIT_TANGENT_FOR_DIRECTION: f32 = 0.2;
778
779    #[test_case(Position {x: 0.0, y: 1.0}, Some(ScrollDirection::Down); "scroll down")]
780    #[test_case(Position {x: 0.0, y: -1.0}, Some(ScrollDirection::Up); "scroll up")]
781    #[test_case(Position {x: 1.0, y: 0.0}, Some(ScrollDirection::Right); "scroll right")]
782    #[test_case(Position {x: -1.0, y: 0.0}, Some(ScrollDirection::Left); "scroll left")]
783    #[test_case(Position {x: 1.0, y: 1.0}, None; "scroll No 45°")]
784    #[test_case(
785        Position {x: 0.9, y: 5.0}, Some(ScrollDirection::Down);
786    "scroll down inside tolerated right")]
787    #[test_case(
788        Position {x: -0.9, y: 5.0}, Some(ScrollDirection::Down);
789        "scroll down inside tolerated left")]
790    #[test_case(Position {x: 1.0, y: 5.0}, None; "scroll No outside tolerated right")]
791    #[test_case(Position {x: -1.0, y: 5.0}, None; "scroll No outside tolerated left")]
792    #[fuchsia::test]
793    fn direction(to: Position, want: Option<ScrollDirection>) {
794        let movement = Movement { from: Position { x: 0.0, y: 0.0 }, to };
795        let got = movement.direction(LIMIT_TANGENT_FOR_DIRECTION);
796        pretty_assertions::assert_eq!(want, got);
797    }
798
799    fn make_touch_contact(id: u32, position: Position) -> touch_binding::TouchContact {
800        touch_binding::TouchContact { id, position, pressure: None, contact_size: None }
801    }
802
803    #[test_case(TouchpadEvent{
804        timestamp: zx::MonotonicInstant::ZERO,
805        pressed_buttons: vec![1],
806        contacts: vec![
807            make_touch_contact(1, Position{x: 1.0, y: 1.0}),
808            make_touch_contact(2, Position{x: 5.0, y: 5.0})
809        ],
810        filtered_palm_contacts: vec![],
811    };"button down")]
812    #[test_case(TouchpadEvent{
813        timestamp: zx::MonotonicInstant::ZERO,
814        pressed_buttons: vec![],
815        contacts: vec![],
816        filtered_palm_contacts: vec![],
817    };"0 fingers")]
818    #[fuchsia::test]
819    fn initial_contender_examine_event_mismatch(event: TouchpadEvent) {
820        let contender: Box<dyn gesture_arena::Contender> = Box::new(InitialContender {
821            motion_threshold_in_mm: MOTION_THRESHOLD_IN_MM,
822            min_movement_in_mm: MIN_MOVEMENT_IN_MM,
823            max_movement_in_mm: MAX_MOVEMENT_IN_MM,
824            limit_tangent_for_direction: LIMIT_TANGENT_FOR_DIRECTION,
825        });
826
827        let got = contender.examine_event(&event);
828        assert_matches!(got, ExamineEventResult::Mismatch(_));
829    }
830
831    #[fuchsia::test]
832    fn initial_contender_examine_event_one_finger_contact_contender() {
833        let contender: Box<dyn gesture_arena::Contender> = Box::new(InitialContender {
834            motion_threshold_in_mm: MOTION_THRESHOLD_IN_MM,
835            min_movement_in_mm: MIN_MOVEMENT_IN_MM,
836            max_movement_in_mm: MAX_MOVEMENT_IN_MM,
837            limit_tangent_for_direction: LIMIT_TANGENT_FOR_DIRECTION,
838        });
839
840        let event = TouchpadEvent {
841            timestamp: zx::MonotonicInstant::ZERO,
842            pressed_buttons: vec![],
843            contacts: vec![make_touch_contact(1, Position { x: 1.0, y: 1.0 })],
844            filtered_palm_contacts: vec![],
845        };
846        let got = contender.examine_event(&event);
847        assert_matches!(got, ExamineEventResult::Contender(contender) => {
848            pretty_assertions::assert_eq!(contender.get_type_name(), "input_pipeline_lib_test::gestures::scroll::OneFingerContactContender");
849        });
850    }
851
852    #[fuchsia::test]
853    fn initial_contender_examine_event_two_finger_contact_contender() {
854        let contender: Box<dyn gesture_arena::Contender> = Box::new(InitialContender {
855            motion_threshold_in_mm: MOTION_THRESHOLD_IN_MM,
856            min_movement_in_mm: MIN_MOVEMENT_IN_MM,
857            max_movement_in_mm: MAX_MOVEMENT_IN_MM,
858            limit_tangent_for_direction: LIMIT_TANGENT_FOR_DIRECTION,
859        });
860
861        let event = TouchpadEvent {
862            timestamp: zx::MonotonicInstant::ZERO,
863            pressed_buttons: vec![],
864            contacts: vec![
865                make_touch_contact(1, Position { x: 1.0, y: 1.0 }),
866                make_touch_contact(2, Position { x: 5.0, y: 5.0 }),
867            ],
868            filtered_palm_contacts: vec![],
869        };
870        let got = contender.examine_event(&event);
871        assert_matches!(got, ExamineEventResult::Contender(contender) => {
872            pretty_assertions::assert_eq!(contender.get_type_name(), "input_pipeline_lib_test::gestures::scroll::TwoFingerContactContender");
873        });
874    }
875
876    #[test_case(TouchpadEvent{
877        timestamp: zx::MonotonicInstant::ZERO,
878        pressed_buttons: vec![1],
879        contacts: vec![
880            make_touch_contact(1, Position{x: 1.0, y: 1.0}),
881        ],
882        filtered_palm_contacts: vec![],
883    };"button down")]
884    #[test_case(TouchpadEvent{
885        timestamp: zx::MonotonicInstant::ZERO,
886        pressed_buttons: vec![],
887        contacts: vec![],
888        filtered_palm_contacts: vec![],
889    };"0 fingers")]
890    #[test_case(TouchpadEvent{
891        timestamp: zx::MonotonicInstant::ZERO,
892        pressed_buttons: vec![],
893        contacts: vec![
894            make_touch_contact(1, Position{x: 1.0, y: 7.0}),
895        ],
896        filtered_palm_contacts: vec![],
897    };"> motion threshold")]
898    #[test_case(TouchpadEvent{
899        timestamp: zx::MonotonicInstant::ZERO,
900        pressed_buttons: vec![],
901        contacts: vec![
902            make_touch_contact(1, Position{x: 1.0, y: 7.0}),
903            make_touch_contact(2, Position{x: 1.0, y: 5.0}),
904        ],
905        filtered_palm_contacts: vec![],
906    };"2 finger and > motion threshold")]
907    #[test_case(TouchpadEvent{
908        timestamp: zx::MonotonicInstant::ZERO,
909        pressed_buttons: vec![],
910        contacts: vec![
911            make_touch_contact(2, Position{x: 1.0, y: 5.0}),
912        ],
913        filtered_palm_contacts: vec![],
914    };"inital finger lift")]
915    #[fuchsia::test]
916    fn one_finger_contact_contender_examine_event_mismatch(event: TouchpadEvent) {
917        let contender: Box<dyn gesture_arena::Contender> = Box::new(OneFingerContactContender {
918            motion_threshold_in_mm: MOTION_THRESHOLD_IN_MM,
919            min_movement_in_mm: MIN_MOVEMENT_IN_MM,
920            max_movement_in_mm: MAX_MOVEMENT_IN_MM,
921            limit_tangent_for_direction: LIMIT_TANGENT_FOR_DIRECTION,
922            initial_position: ContactPosition { id: 1, position: Position { x: 1.0, y: 1.0 } },
923        });
924
925        let got = contender.examine_event(&event);
926        assert_matches!(got, ExamineEventResult::Mismatch(_));
927    }
928
929    #[test_case(TouchpadEvent{
930        timestamp: zx::MonotonicInstant::ZERO,
931        pressed_buttons: vec![],
932        contacts: vec![
933            make_touch_contact(1, Position{x: 1.0, y: 1.0}),
934        ],
935        filtered_palm_contacts: vec![],
936    };"hold")]
937    #[test_case(TouchpadEvent{
938        timestamp: zx::MonotonicInstant::ZERO,
939        pressed_buttons: vec![],
940        contacts: vec![
941            make_touch_contact(1, Position{x: 1.0, y: 2.0}),
942        ],
943        filtered_palm_contacts: vec![],
944    };"< motion threshold")]
945    #[fuchsia::test]
946    fn one_finger_contact_contender_examine_event_contender(event: TouchpadEvent) {
947        let contender: Box<dyn gesture_arena::Contender> = Box::new(OneFingerContactContender {
948            motion_threshold_in_mm: MOTION_THRESHOLD_IN_MM,
949            min_movement_in_mm: MIN_MOVEMENT_IN_MM,
950            max_movement_in_mm: MAX_MOVEMENT_IN_MM,
951            limit_tangent_for_direction: LIMIT_TANGENT_FOR_DIRECTION,
952            initial_position: ContactPosition { id: 1, position: Position { x: 1.0, y: 1.0 } },
953        });
954
955        let got = contender.examine_event(&event);
956        assert_matches!(got, ExamineEventResult::Contender(contender) => {
957            pretty_assertions::assert_eq!(contender.get_type_name(), "input_pipeline_lib_test::gestures::scroll::OneFingerContactContender");
958        });
959    }
960
961    #[fuchsia::test]
962    fn one_finger_contact_contender_examine_event_two_finger_contact_contender() {
963        let contender: Box<dyn gesture_arena::Contender> = Box::new(OneFingerContactContender {
964            motion_threshold_in_mm: MOTION_THRESHOLD_IN_MM,
965            min_movement_in_mm: MIN_MOVEMENT_IN_MM,
966            max_movement_in_mm: MAX_MOVEMENT_IN_MM,
967            limit_tangent_for_direction: LIMIT_TANGENT_FOR_DIRECTION,
968            initial_position: ContactPosition { id: 1, position: Position { x: 1.0, y: 1.0 } },
969        });
970
971        let event = TouchpadEvent {
972            timestamp: zx::MonotonicInstant::ZERO,
973            pressed_buttons: vec![],
974            contacts: vec![
975                make_touch_contact(1, Position { x: 1.0, y: 1.0 }),
976                make_touch_contact(2, Position { x: 1.0, y: 5.0 }),
977            ],
978            filtered_palm_contacts: vec![],
979        };
980        let got = contender.examine_event(&event);
981        assert_matches!(got, ExamineEventResult::Contender(contender) => {
982            pretty_assertions::assert_eq!(contender.get_type_name(), "input_pipeline_lib_test::gestures::scroll::TwoFingerContactContender");
983        });
984    }
985
986    #[test_case(TouchpadEvent{
987        timestamp: zx::MonotonicInstant::ZERO,
988        pressed_buttons: vec![1],
989        contacts: vec![
990            make_touch_contact(1, Position{x: 1.0, y: 1.0}),
991            make_touch_contact(2, Position{x: 5.0, y: 5.0}),
992        ],
993        filtered_palm_contacts: vec![],
994    };"button down")]
995    #[test_case(TouchpadEvent{
996        timestamp: zx::MonotonicInstant::ZERO,
997        pressed_buttons: vec![],
998        contacts: vec![],
999        filtered_palm_contacts: vec![],
1000    };"0 fingers")]
1001    #[test_case(TouchpadEvent{
1002        timestamp: zx::MonotonicInstant::ZERO,
1003        pressed_buttons: vec![],
1004        contacts: vec![
1005            make_touch_contact(1, Position{x: 1.0, y: 1.0}),
1006        ],
1007        filtered_palm_contacts: vec![],
1008    };"1 fingers")]
1009    #[test_case(TouchpadEvent{
1010        timestamp: zx::MonotonicInstant::ZERO,
1011        pressed_buttons: vec![],
1012        contacts: vec![
1013            make_touch_contact(1, Position{x: 20.0, y: 20.0}),
1014            make_touch_contact(2, Position{x: 25.0, y: 25.0}),
1015        ],
1016        filtered_palm_contacts: vec![],
1017    };"> max movement no direction")]
1018    #[test_case(TouchpadEvent{
1019        timestamp: zx::MonotonicInstant::ZERO,
1020        pressed_buttons: vec![],
1021        contacts: vec![
1022            make_touch_contact(1, Position{x: 1.0, y: 21.0}),
1023            make_touch_contact(2, Position{x: 5.0, y: -16.0}),
1024        ],
1025        filtered_palm_contacts: vec![],
1026    };"> max movement different direction")]
1027    #[fuchsia::test]
1028    fn two_finger_contact_contender_examine_event_mismatch(event: TouchpadEvent) {
1029        let contender: Box<dyn gesture_arena::Contender> = Box::new(TwoFingerContactContender {
1030            min_movement_in_mm: MIN_MOVEMENT_IN_MM,
1031            max_movement_in_mm: MAX_MOVEMENT_IN_MM,
1032            limit_tangent_for_direction: LIMIT_TANGENT_FOR_DIRECTION,
1033            initial_positions: ContactPositions {
1034                first_contact: ContactPosition { id: 1, position: Position { x: 1.0, y: 1.0 } },
1035                second_contact: ContactPosition { id: 2, position: Position { x: 5.0, y: 5.0 } },
1036            },
1037        });
1038
1039        let got = contender.examine_event(&event);
1040        assert_matches!(got, ExamineEventResult::Mismatch(_));
1041    }
1042
1043    #[test_case(TouchpadEvent{
1044        timestamp: zx::MonotonicInstant::ZERO,
1045        pressed_buttons: vec![],
1046        contacts: vec![
1047            make_touch_contact(1, Position{x: 1.0, y: 1.0}),
1048            make_touch_contact(2, Position{x: 5.0, y: 5.0}),
1049        ],
1050        filtered_palm_contacts: vec![],
1051    };"finger hold")]
1052    #[test_case(TouchpadEvent{
1053        timestamp: zx::MonotonicInstant::ZERO,
1054        pressed_buttons: vec![],
1055        contacts: vec![
1056            make_touch_contact(1, Position{x: 2.0, y: 1.0}),
1057            make_touch_contact(2, Position{x: 6.0, y: 5.0}),
1058        ],
1059        filtered_palm_contacts: vec![],
1060    };"2 finger move less than threshold")]
1061    #[test_case(TouchpadEvent{
1062        timestamp: zx::MonotonicInstant::ZERO,
1063        pressed_buttons: vec![],
1064        contacts: vec![
1065            make_touch_contact(1, Position{x: 12.0, y: 1.0}),
1066            make_touch_contact(2, Position{x: 6.0, y: 5.0}),
1067        ],
1068        filtered_palm_contacts: vec![],
1069    };"both finger move, 1 finger move less than threshold")]
1070    #[test_case(TouchpadEvent{
1071        timestamp: zx::MonotonicInstant::ZERO,
1072        pressed_buttons: vec![],
1073        contacts: vec![
1074            make_touch_contact(1, Position{x: 10.0, y: 10.0}),
1075            make_touch_contact(2, Position{x: 15.0, y: 15.0}),
1076        ],
1077        filtered_palm_contacts: vec![],
1078    };"no direction")]
1079    #[test_case(TouchpadEvent{
1080        timestamp: zx::MonotonicInstant::ZERO,
1081        pressed_buttons: vec![],
1082        contacts: vec![
1083            make_touch_contact(1, Position{x: 1.0, y: 11.0}),
1084            make_touch_contact(2, Position{x: 5.0, y: -6.0}),
1085        ],
1086        filtered_palm_contacts: vec![],
1087    };"different direction")]
1088    #[fuchsia::test]
1089    fn two_finger_contact_contender_examine_event_finger_contact_contender(event: TouchpadEvent) {
1090        let contender: Box<dyn gesture_arena::Contender> = Box::new(TwoFingerContactContender {
1091            min_movement_in_mm: MIN_MOVEMENT_IN_MM,
1092            max_movement_in_mm: MAX_MOVEMENT_IN_MM,
1093            limit_tangent_for_direction: LIMIT_TANGENT_FOR_DIRECTION,
1094            initial_positions: ContactPositions {
1095                first_contact: ContactPosition { id: 1, position: Position { x: 1.0, y: 1.0 } },
1096                second_contact: ContactPosition { id: 2, position: Position { x: 5.0, y: 5.0 } },
1097            },
1098        });
1099
1100        let got = contender.examine_event(&event);
1101        assert_matches!(got, ExamineEventResult::Contender(contender) => {
1102            pretty_assertions::assert_eq!(contender.get_type_name(), "input_pipeline_lib_test::gestures::scroll::TwoFingerContactContender");
1103        });
1104    }
1105
1106    #[fuchsia::test]
1107    fn two_finger_contact_contender_examine_event_matched_contender() {
1108        let initial_positions = ContactPositions {
1109            first_contact: ContactPosition { id: 1, position: Position { x: 1.0, y: 1.0 } },
1110            second_contact: ContactPosition { id: 2, position: Position { x: 5.0, y: 5.0 } },
1111        };
1112        let contender: Box<dyn gesture_arena::Contender> = Box::new(TwoFingerContactContender {
1113            min_movement_in_mm: MIN_MOVEMENT_IN_MM,
1114            max_movement_in_mm: MAX_MOVEMENT_IN_MM,
1115            limit_tangent_for_direction: LIMIT_TANGENT_FOR_DIRECTION,
1116            initial_positions,
1117        });
1118
1119        let event = TouchpadEvent {
1120            timestamp: zx::MonotonicInstant::ZERO,
1121            pressed_buttons: vec![],
1122            contacts: vec![
1123                make_touch_contact(1, Position { x: 1.0, y: 11.0 }),
1124                make_touch_contact(2, Position { x: 5.0, y: 15.0 }),
1125            ],
1126            filtered_palm_contacts: vec![],
1127        };
1128        let got = contender.examine_event(&event);
1129        assert_matches!(got, ExamineEventResult::MatchedContender(_));
1130    }
1131
1132    #[test_case(
1133        TouchpadEvent{
1134            timestamp: zx::MonotonicInstant::ZERO,
1135            pressed_buttons: vec![1],
1136            contacts: vec![make_touch_contact(1, Position{x: 1.0, y: 1.0})],
1137            filtered_palm_contacts: vec![],
1138        };"button down")]
1139    #[test_case(
1140        TouchpadEvent{
1141            timestamp: zx::MonotonicInstant::ZERO,
1142            pressed_buttons: vec![],
1143            contacts: vec![],
1144            filtered_palm_contacts: vec![],
1145        };"0 fingers")]
1146    #[test_case(
1147        TouchpadEvent{
1148            timestamp: zx::MonotonicInstant::ZERO,
1149            pressed_buttons: vec![],
1150            contacts: vec![
1151                make_touch_contact(1, Position{x: 1.0, y: 1.0}),
1152            ],
1153            filtered_palm_contacts: vec![],
1154        };"1 fingers")]
1155    #[test_case(
1156        TouchpadEvent{
1157            timestamp: zx::MonotonicInstant::ZERO,
1158            pressed_buttons: vec![],
1159            contacts: vec![
1160                make_touch_contact(1, Position{x: 1.0, y: 1.0}),
1161                make_touch_contact(2, Position{x: 5.0, y: 5.0}),
1162            ],
1163            filtered_palm_contacts: vec![],
1164        };"finger hold")]
1165    #[test_case(
1166        TouchpadEvent{
1167            timestamp: zx::MonotonicInstant::ZERO,
1168            pressed_buttons: vec![],
1169            contacts: vec![
1170                make_touch_contact(1, Position{x: 10.0, y: 10.0}),
1171                make_touch_contact(2, Position{x: 15.0, y: 15.0}),
1172            ],
1173            filtered_palm_contacts: vec![],
1174        };"no direction")]
1175    #[test_case(
1176        TouchpadEvent{
1177            timestamp: zx::MonotonicInstant::ZERO,
1178            pressed_buttons: vec![],
1179            contacts: vec![
1180                make_touch_contact(1, Position{x: 1.0, y: 11.0}),
1181                make_touch_contact(2, Position{x: 5.0, y: -6.0}),
1182            ],
1183            filtered_palm_contacts: vec![],
1184        };"different direction")]
1185    #[test_case(
1186        TouchpadEvent{
1187            timestamp: zx::MonotonicInstant::ZERO,
1188            pressed_buttons: vec![],
1189            contacts: vec![
1190                make_touch_contact(1, Position{x: 1.0, y: 11.0}),
1191                make_touch_contact(2, Position{x: 5.0, y: 16.0}),
1192            ],
1193            filtered_palm_contacts: vec![],
1194        };"wrong direction")]
1195    #[fuchsia::test]
1196    fn matched_contender_verify_event_mismatch(event: TouchpadEvent) {
1197        let contender: Box<dyn gesture_arena::MatchedContender> = Box::new(MatchedContender {
1198            limit_tangent_for_direction: LIMIT_TANGENT_FOR_DIRECTION,
1199            initial_positions: ContactPositions {
1200                first_contact: ContactPosition { id: 1, position: Position { x: 1.0, y: 1.0 } },
1201                second_contact: ContactPosition { id: 2, position: Position { x: 5.0, y: 5.0 } },
1202            },
1203            direction: ScrollDirection::Up,
1204        });
1205
1206        let got = contender.verify_event(&event);
1207        assert_matches!(got, VerifyEventResult::Mismatch(_));
1208    }
1209
1210    #[test_case(TouchpadEvent{
1211            timestamp: zx::MonotonicInstant::ZERO,
1212            pressed_buttons: vec![],
1213            contacts: vec![
1214                make_touch_contact(1, Position{x: 1.0, y: -11.0}),
1215                make_touch_contact(2, Position{x: 5.0, y: -5.0}),
1216            ],
1217            filtered_palm_contacts: vec![],
1218        };"on direction")]
1219    #[fuchsia::test]
1220    fn matched_contender_verify_event_matched_contender(event: TouchpadEvent) {
1221        let contender: Box<dyn gesture_arena::MatchedContender> = Box::new(MatchedContender {
1222            limit_tangent_for_direction: LIMIT_TANGENT_FOR_DIRECTION,
1223            initial_positions: ContactPositions {
1224                first_contact: ContactPosition { id: 1, position: Position { x: 1.0, y: 1.0 } },
1225                second_contact: ContactPosition { id: 2, position: Position { x: 5.0, y: 5.0 } },
1226            },
1227            direction: ScrollDirection::Up,
1228        });
1229
1230        let got = contender.verify_event(&event);
1231        assert_matches!(got, VerifyEventResult::MatchedContender(_));
1232    }
1233
1234    #[fuchsia::test]
1235    fn matched_contender_process_buffered_events() {
1236        let contender: Box<dyn gesture_arena::MatchedContender> = Box::new(MatchedContender {
1237            limit_tangent_for_direction: LIMIT_TANGENT_FOR_DIRECTION,
1238            initial_positions: ContactPositions {
1239                first_contact: ContactPosition { id: 1, position: Position { x: 1.0, y: 1.0 } },
1240                second_contact: ContactPosition { id: 2, position: Position { x: 5.0, y: 5.0 } },
1241            },
1242            direction: ScrollDirection::Up,
1243        });
1244
1245        let got = contender.process_buffered_events(vec![
1246            TouchpadEvent {
1247                timestamp: zx::MonotonicInstant::from_nanos(1),
1248                pressed_buttons: vec![],
1249                contacts: vec![
1250                    make_touch_contact(1, Position { x: 1.0, y: 1.0 }),
1251                    make_touch_contact(2, Position { x: 5.0, y: 5.0 }),
1252                ],
1253                filtered_palm_contacts: vec![],
1254            },
1255            TouchpadEvent {
1256                timestamp: zx::MonotonicInstant::from_nanos(2),
1257                pressed_buttons: vec![],
1258                contacts: vec![
1259                    make_touch_contact(1, Position { x: 1.0, y: 1.0 }),
1260                    make_touch_contact(2, Position { x: 5.0, y: 5.0 }),
1261                ],
1262                filtered_palm_contacts: vec![],
1263            },
1264            TouchpadEvent {
1265                timestamp: zx::MonotonicInstant::from_nanos(3),
1266                pressed_buttons: vec![],
1267                contacts: vec![
1268                    make_touch_contact(1, Position { x: 1.0, y: 2.0 }),
1269                    make_touch_contact(2, Position { x: 5.0, y: 6.0 }),
1270                ],
1271                filtered_palm_contacts: vec![],
1272            },
1273            TouchpadEvent {
1274                timestamp: zx::MonotonicInstant::from_nanos(4),
1275                pressed_buttons: vec![],
1276                contacts: vec![
1277                    make_touch_contact(1, Position { x: 1.0, y: -11.0 }),
1278                    make_touch_contact(2, Position { x: 5.0, y: -5.0 }),
1279                ],
1280                filtered_palm_contacts: vec![],
1281            },
1282        ]);
1283
1284        pretty_assertions::assert_eq!(
1285            got.generated_events,
1286            vec![
1287                // Finger hold, expect no scroll delta.
1288                MouseEvent {
1289                    timestamp: zx::MonotonicInstant::from_nanos(2),
1290                    mouse_data: mouse_binding::MouseEvent::new(
1291                        mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
1292                            millimeters: Position { x: 0.0, y: 0.0 },
1293                        }),
1294                        /* wheel_delta_v= */ wheel_delta_mm(0.0),
1295                        /* wheel_delta_h= */ None,
1296                        mouse_binding::MousePhase::Wheel,
1297                        /* affected_buttons= */ hashset! {},
1298                        /* pressed_buttons= */ hashset! {},
1299                        /* is_precision_scroll= */ Some(mouse_binding::PrecisionScroll::Yes),
1300                        /* wake_lease= */ None,
1301                    ),
1302                },
1303                // Finger move to wrong direction, expect no scroll delta.
1304                MouseEvent {
1305                    timestamp: zx::MonotonicInstant::from_nanos(3),
1306                    mouse_data: mouse_binding::MouseEvent::new(
1307                        mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
1308                            millimeters: Position { x: 0.0, y: 0.0 },
1309                        }),
1310                        /* wheel_delta_v= */ wheel_delta_mm(0.0),
1311                        /* wheel_delta_h= */ None,
1312                        mouse_binding::MousePhase::Wheel,
1313                        /* affected_buttons= */ hashset! {},
1314                        /* pressed_buttons= */ hashset! {},
1315                        /* is_precision_scroll= */ Some(mouse_binding::PrecisionScroll::Yes),
1316                        /* wake_lease= */ None,
1317                    ),
1318                },
1319                MouseEvent {
1320                    timestamp: zx::MonotonicInstant::from_nanos(4),
1321                    mouse_data: mouse_binding::MouseEvent::new(
1322                        mouse_binding::MouseLocation::Relative(mouse_binding::RelativeLocation {
1323                            millimeters: Position { x: 0.0, y: 0.0 },
1324                        }),
1325                        /* wheel_delta_v= */ wheel_delta_mm(-12.0),
1326                        /* wheel_delta_h= */ None,
1327                        mouse_binding::MousePhase::Wheel,
1328                        /* affected_buttons= */ hashset! {},
1329                        /* pressed_buttons= */ hashset! {},
1330                        /* is_precision_scroll= */ Some(mouse_binding::PrecisionScroll::Yes),
1331                        /* wake_lease= */ None,
1332                    ),
1333                },
1334            ]
1335        );
1336        pretty_assertions::assert_eq!(got.recognized_gesture, RecognizedGesture::Scroll);
1337    }
1338
1339    #[fuchsia::test]
1340    fn winner_process_new_event_end_gesture_none() {
1341        let winner: Box<dyn gesture_arena::Winner> = Box::new(Winner {
1342            limit_tangent_for_direction: LIMIT_TANGENT_FOR_DIRECTION,
1343            direction: ScrollDirection::Up,
1344            last_positions: ContactPositions {
1345                first_contact: ContactPosition { id: 1, position: Position { x: 1.0, y: 1.0 } },
1346                second_contact: ContactPosition { id: 2, position: Position { x: 5.0, y: 5.0 } },
1347            },
1348        });
1349        let event = TouchpadEvent {
1350            timestamp: zx::MonotonicInstant::ZERO,
1351            pressed_buttons: vec![],
1352            contacts: vec![],
1353            filtered_palm_contacts: vec![],
1354        };
1355        let got = winner.process_new_event(event);
1356
1357        assert_matches!(got, ProcessNewEventResult::EndGesture(EndGestureEvent::NoEvent, _reason));
1358    }
1359
1360    #[test_case(
1361        TouchpadEvent{
1362            timestamp: zx::MonotonicInstant::ZERO,
1363            pressed_buttons: vec![1],
1364            contacts: vec![make_touch_contact(1, Position{x: 1.0, y: 1.0})],
1365            filtered_palm_contacts: vec![],
1366        };"button down")]
1367    #[test_case(
1368        TouchpadEvent{
1369            timestamp: zx::MonotonicInstant::ZERO,
1370            pressed_buttons: vec![],
1371            contacts: vec![
1372                make_touch_contact(1, Position{x: 1.0, y: 1.0}),
1373            ],
1374            filtered_palm_contacts: vec![],
1375        };"1 fingers")]
1376    #[test_case(
1377        TouchpadEvent{
1378            timestamp: zx::MonotonicInstant::ZERO,
1379            pressed_buttons: vec![],
1380            contacts: vec![
1381                make_touch_contact(1, Position{x: 1.0, y: 11.0}),
1382                make_touch_contact(2, Position{x: 5.0, y: -6.0}),
1383            ],
1384            filtered_palm_contacts: vec![],
1385        };"1 finger reverse direction")]
1386    #[test_case(
1387        TouchpadEvent{
1388            timestamp: zx::MonotonicInstant::ZERO,
1389            pressed_buttons: vec![],
1390            contacts: vec![
1391                make_touch_contact(1, Position{x: 1.0, y: 11.0}),
1392                make_touch_contact(2, Position{x: 5.0, y: 16.0}),
1393            ],
1394            filtered_palm_contacts: vec![],
1395        };"2 fingers reverse direction")]
1396    #[fuchsia::test]
1397    fn winner_process_new_event_end_gesture_some(event: TouchpadEvent) {
1398        let winner: Box<dyn gesture_arena::Winner> = Box::new(Winner {
1399            limit_tangent_for_direction: LIMIT_TANGENT_FOR_DIRECTION,
1400            direction: ScrollDirection::Up,
1401            last_positions: ContactPositions {
1402                first_contact: ContactPosition { id: 1, position: Position { x: 1.0, y: 1.0 } },
1403                second_contact: ContactPosition { id: 2, position: Position { x: 5.0, y: 5.0 } },
1404            },
1405        });
1406        let got = winner.process_new_event(event);
1407
1408        assert_matches!(
1409            got,
1410            ProcessNewEventResult::EndGesture(EndGestureEvent::UnconsumedEvent(_), _reason)
1411        );
1412    }
1413
1414    #[test_case(
1415        TouchpadEvent{
1416            timestamp: zx::MonotonicInstant::ZERO,
1417            pressed_buttons: vec![],
1418            contacts: vec![
1419                make_touch_contact(1, Position{x: 1.0, y: 1.0}),
1420                make_touch_contact(2, Position{x: 5.0, y: 5.0}),
1421            ],
1422            filtered_palm_contacts: vec![],
1423        } => MouseEvent {
1424            timestamp: zx::MonotonicInstant::from_nanos(0),
1425            mouse_data: mouse_binding::MouseEvent {
1426                location: mouse_binding::MouseLocation::Relative(
1427                    mouse_binding::RelativeLocation {
1428                        millimeters: Position { x: 0.0, y: 0.0 },
1429                    }
1430                ),
1431                wheel_delta_v: wheel_delta_mm(0.0),
1432                wheel_delta_h: None,
1433                phase: mouse_binding::MousePhase::Wheel,
1434                affected_buttons: hashset! {},
1435                pressed_buttons: hashset! {},
1436                is_precision_scroll: Some(mouse_binding::PrecisionScroll::Yes),
1437                wake_lease: None.into(),
1438            },
1439        };"finger hold")]
1440    #[test_case(
1441        TouchpadEvent{
1442            timestamp: zx::MonotonicInstant::ZERO,
1443            pressed_buttons: vec![],
1444            contacts: vec![
1445                make_touch_contact(1, Position{x: 10.0, y: -10.0}),
1446                make_touch_contact(2, Position{x: 15.0, y: -15.0}),
1447            ],
1448            filtered_palm_contacts: vec![],
1449        } => MouseEvent {
1450            timestamp: zx::MonotonicInstant::from_nanos(0),
1451            mouse_data: mouse_binding::MouseEvent {
1452                location: mouse_binding::MouseLocation::Relative(
1453                    mouse_binding::RelativeLocation {
1454                        millimeters: Position { x: 0.0, y: 0.0 },
1455                    }
1456                ),
1457                wheel_delta_v: wheel_delta_mm(-15.5),
1458                wheel_delta_h: None,
1459                phase: mouse_binding::MousePhase::Wheel,
1460                affected_buttons: hashset! {},
1461                pressed_buttons: hashset! {},
1462                is_precision_scroll: Some(mouse_binding::PrecisionScroll::Yes),
1463                wake_lease: None.into(),
1464            },
1465        };"direction contact1 only")]
1466    #[test_case(
1467        TouchpadEvent{
1468            timestamp: zx::MonotonicInstant::ZERO,
1469            pressed_buttons: vec![],
1470            contacts: vec![
1471                make_touch_contact(1, Position{x: 1.0, y: -11.0}),
1472                make_touch_contact(2, Position{x: 5.0, y: -5.0}),
1473            ],
1474            filtered_palm_contacts: vec![],
1475        } => MouseEvent {
1476            timestamp: zx::MonotonicInstant::from_nanos(0),
1477            mouse_data: mouse_binding::MouseEvent {
1478                location: mouse_binding::MouseLocation::Relative(
1479                    mouse_binding::RelativeLocation {
1480                        millimeters: Position { x: 0.0, y: 0.0 },
1481                    }
1482                ),
1483                wheel_delta_v: wheel_delta_mm(-11.0),
1484                wheel_delta_h: None,
1485                phase: mouse_binding::MousePhase::Wheel,
1486                affected_buttons: hashset! {},
1487                pressed_buttons: hashset! {},
1488                is_precision_scroll: Some(mouse_binding::PrecisionScroll::Yes),
1489                wake_lease: None.into(),
1490            },
1491        };"on direction")]
1492    #[fuchsia::test]
1493    fn winner_process_new_event_continue_gesture(event: TouchpadEvent) -> MouseEvent {
1494        let winner: Box<dyn gesture_arena::Winner> = Box::new(Winner {
1495            limit_tangent_for_direction: LIMIT_TANGENT_FOR_DIRECTION,
1496            direction: ScrollDirection::Up,
1497            last_positions: ContactPositions {
1498                first_contact: ContactPosition { id: 1, position: Position { x: 1.0, y: 1.0 } },
1499                second_contact: ContactPosition { id: 2, position: Position { x: 5.0, y: 5.0 } },
1500            },
1501        });
1502        let got = winner.process_new_event(event);
1503
1504        // This not able to use `assert_eq` or `assert_matches` because:
1505        // - assert_matches: floating point is not allow in match.
1506        // - assert_eq: `ContinueGesture` has Box dyn type.
1507        match got {
1508            ProcessNewEventResult::EndGesture(..) => {
1509                panic!("Got {:?}, want ContinueGesture()", got)
1510            }
1511            ProcessNewEventResult::ContinueGesture(got_mouse_event, _) => got_mouse_event.unwrap(),
1512        }
1513    }
1514}