Introduce Drag-and-Scroll gesture handling
Enable users to scroll with two fingers while a physical button is
held down. This allows for navigating scroll-able content after
initiating a click.
The interpreter logic is updated to check for multiple fingers on the
touch-pad during a button press. It validates that these fingers
display a scrolling motion before recognizing the
action as a Scroll gesture instead of a standard Move. It uses the
two fastest moving fingers to validate whether a scroll gesture
was performed.
A user can lift all scrolling fingers to initiate a Fling, mirroring
normal scroll behavior. A user can also lift a single scrolling
finger to revert the gesture back to a standard drag.
A new `drag_scroll_enable` property is added to control this feature,
along with unit tests to cover the new interaction scenarios.
BUG=b:322199922
TEST=cros_sdk cros_workon_make --test --board=rex chromeos-base/gestures
TEST=touchtests
TEST=Manual testing
Change-Id: Id9a21fb8ce46b18c3e882ffdaffecc98bbf84e9b
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/gestures/+/6795666
Tested-by: Sairah Amuthan <[email protected]>
Commit-Queue: Sairah Amuthan <[email protected]>
Reviewed-by: Harry Cutts <[email protected]>
Code-Coverage: Zoss <[email protected]>
Reviewed-by: Henry Barnor <[email protected]>
diff --git a/include/immediate_interpreter.h b/include/immediate_interpreter.h
index e8227a3..0a87285 100644
--- a/include/immediate_interpreter.h
+++ b/include/immediate_interpreter.h
@@ -366,9 +366,18 @@
FRIEND_TEST(ImmediateInterpreterTest, WarpedFingersTappingTest);
FRIEND_TEST(ImmediateInterpreterTest, ZeroClickInitializationTest);
FRIEND_TEST(ImmediateInterpreterTtcEnableTest, TapToClickEnableTest);
+ FRIEND_TEST(DragScrollTest, DragScrollDisabledDefaultsToMove);
+ FRIEND_TEST(DragScrollTest, DragScrollEnabledProducesScroll);
+ FRIEND_TEST(DragScrollTest, DragScrollTransitionsToFlingOnLift);
+ FRIEND_TEST(DragScrollTest, DragScrollRevertsToMove);
+ FRIEND_TEST(DragScrollTest, DragScrollWithThreeMovingFingers);
+ FRIEND_TEST(DragScrollTest, DragScrollEnabledNormalDrag);
+ FRIEND_TEST(DragScrollTest, DragScrollTwoFingersOnly);
+
friend class TapRecord;
friend class TapToClickStateMachineTest;
friend class FingerButtonClick;
+ friend class DragScrollTest;
public:
enum TapToClickState {
@@ -777,6 +786,8 @@
BoolProperty tap_drag_enable_;
// True if drag lock is enabled
BoolProperty drag_lock_enable_;
+ // True if the Drag-and-Scroll gesture enabled
+ BoolProperty drag_scroll_enable_;
// Time [s] the finger has to be stationary to be considered dragging
DoubleProperty tap_drag_stationary_time_;
// Distance [mm] a finger can move and still register a tap
diff --git a/src/immediate_interpreter.cc b/src/immediate_interpreter.cc
index aacba01..ee6e6ba 100644
--- a/src/immediate_interpreter.cc
+++ b/src/immediate_interpreter.cc
@@ -1023,6 +1023,7 @@
tap_drag_timeout_(prop_reg, "Tap Drag Timeout", 0.3),
tap_drag_enable_(prop_reg, "Tap Drag Enable", false),
drag_lock_enable_(prop_reg, "Tap Drag Lock Enable", false),
+ drag_scroll_enable_(prop_reg, "Drag and Scroll Enable", false),
tap_drag_stationary_time_(prop_reg, "Tap Drag Stationary Time", 0),
tap_move_dist_(prop_reg, "Tap Move Distance", 2.0),
tap_min_pressure_(prop_reg, "Tap Minimum Pressure", 25.0),
@@ -1774,6 +1775,39 @@
// Physical button or tap overrides current gesture state
if (sent_button_down_ || tap_to_click_state_ == kTtcDrag) {
+ if (!drag_scroll_enable_.val_) {
+ // Drag-and-Scroll feature is disabled, so force all interactions to
+ // be a Move gesture.
+ current_gesture_type_ = kGestureTypeMove;
+ return;
+ }
+
+ // A scroll/swipe was in progress, but fingers were lifted.
+ if (IsScrollOrSwipe(prev_gesture_type_) && num_gesturing < 2) {
+ current_gesture_type_ = kGestureTypeNull;
+ return;
+ }
+
+ if (num_gesturing >= 2) {
+ vector<short, kMaxGesturingFingers> sorted_ids;
+ SortFingersByProximity(gs_fingers, hwstate, &sorted_ids);
+
+ const FingerState* finger1 = hwstate.GetFingerState(sorted_ids[0]);
+ const FingerState* finger2 = hwstate.GetFingerState(sorted_ids[1]);
+
+ // Verify this pair is performing a valid scrolling gesture.
+ if (finger1 && finger2 &&
+ GetTwoFingerGestureType(*finger1, *finger2) == kGestureTypeScroll) {
+ current_gesture_type_ = kGestureTypeScroll;
+ // Set the two scrolling fingers as the 'active' ones for this gesture.
+ active_gs_fingers->clear();
+ active_gs_fingers->insert(finger1->tracking_id);
+ active_gs_fingers->insert(finger2->tracking_id);
+ return;
+ }
+ }
+
+ // Default case for a held button is a Move gesture.
current_gesture_type_ = kGestureTypeMove;
return;
}
diff --git a/src/immediate_interpreter_unittest.cc b/src/immediate_interpreter_unittest.cc
index 3dc1922..1e44f0e 100644
--- a/src/immediate_interpreter_unittest.cc
+++ b/src/immediate_interpreter_unittest.cc
@@ -4239,7 +4239,7 @@
};
TestInterpreterWrapper wrapper(&ii, &hwprops);
- // Test touchpad with intergrated button switch.
+ // Test touchpad with integrated button switch.
EXPECT_EQ(0, ii.zero_finger_click_enable_.val_);
// Test touchpad with separate buttons.
hwprops.is_button_pad = 0;
@@ -4260,4 +4260,375 @@
EXPECT_TRUE(point != point_ne1);
}
+class DragScrollTest : public ::testing::Test {
+ protected:
+ void SetUp() override {
+ hwprops_ = {
+ .right = 1000,
+ .bottom = 1000,
+ .res_x = 50,
+ .res_y = 50,
+ .orientation_minimum = 0,
+ .orientation_maximum = 0,
+ .max_finger_cnt = 5,
+ .max_touch_cnt = 5,
+ .supports_t5r2 = false,
+ .support_semi_mt = false,
+ .is_button_pad = false,
+ .has_wheel = false,
+ .wheel_is_hi_res = false,
+ .is_haptic_pad = false,
+ };
+ button_finger_ = {0, 0, 0, 0, 50, 0, 500, 500, 1, 0};
+
+ ii_.reset(new ImmediateInterpreter(nullptr, nullptr));
+ wrapper_.reset(new TestInterpreterWrapper(ii_.get(), &hwprops_));
+ }
+
+ std::unique_ptr<ImmediateInterpreter> ii_;
+ std::unique_ptr<TestInterpreterWrapper> wrapper_;
+ HardwareProperties hwprops_;
+ FingerState button_finger_;
+};
+
+TEST_F(DragScrollTest, DragScrollDisabledDefaultsToMove) {
+ ii_->drag_scroll_enable_.val_ = false;
+
+ // Frame 1: Button down.
+ HardwareState curr_frame =
+ make_hwstate(0.1, GESTURES_BUTTON_LEFT, 1, 1, &button_finger_);
+ Gesture* gs = wrapper_->SyncInterpret(curr_frame, nullptr);
+ EXPECT_EQ(nullptr, gs);
+
+ // Frame 2: Button down timeout reached, button click registered.
+ curr_frame = make_hwstate(0.2, GESTURES_BUTTON_LEFT, 1, 1, &button_finger_);
+ gs = wrapper_->SyncInterpret(curr_frame, nullptr);
+ ASSERT_NE(nullptr, gs);
+ EXPECT_EQ(kGestureTypeButtonsChange, gs->type);
+
+ // Frame 3: Fingers added, no movement detected yet.
+ FingerState button_down_scroll_fingers_appear[] = {
+ button_finger_,
+ {0, 0, 0, 0, 50, 0, 450, 400, 2, 0},
+ {0, 0, 0, 0, 50, 0, 550, 400, 3, 0},
+ };
+ curr_frame = make_hwstate(0.3, GESTURES_BUTTON_LEFT, 3, 3,
+ button_down_scroll_fingers_appear);
+ gs = wrapper_->SyncInterpret(curr_frame, nullptr);
+ EXPECT_EQ(nullptr, gs);
+
+ // Frame 4: Fingers move in a scroll-like pattern.
+ // Since the flag is off, it should still be a Move gesture.
+ FingerState button_down_scrolling_1[] = {
+ button_finger_,
+ {0, 0, 0, 0, 50, 0, 442, 376, 2, 0},
+ {0, 0, 0, 0, 50, 0, 555, 383, 3, 0},
+ };
+ curr_frame = make_hwstate(0.4, GESTURES_BUTTON_LEFT,
+ 3, 3, button_down_scrolling_1);
+ gs = wrapper_->SyncInterpret(curr_frame, nullptr);
+ ASSERT_NE(nullptr, gs);
+ EXPECT_EQ(kGestureTypeMove, gs->type);
+
+ // Frame 5: Fingers move in a scroll-like pattern.
+ // Since the flag is off, it should still be a Move gesture.
+ FingerState button_down_scrolling_2[] = {
+ button_finger_,
+ {0, 0, 0, 0, 50, 0, 448, 364, 2, 0},
+ {0, 0, 0, 0, 50, 0, 560, 357, 3, 0},
+ };
+ curr_frame = make_hwstate(0.5, GESTURES_BUTTON_LEFT, 3, 3,
+ button_down_scrolling_2);
+ gs = wrapper_->SyncInterpret(curr_frame, nullptr);
+ ASSERT_NE(nullptr, gs);
+ EXPECT_EQ(kGestureTypeMove, gs->type);
+}
+
+TEST_F(DragScrollTest, DragScrollEnabledProducesScroll) {
+ ii_->drag_scroll_enable_.val_ = true;
+
+ // Frame 1: Button down.
+ HardwareState curr_frame =
+ make_hwstate(0.1, GESTURES_BUTTON_LEFT, 1, 1, &button_finger_);
+ Gesture* gs = wrapper_->SyncInterpret(curr_frame, nullptr);
+ EXPECT_EQ(nullptr, gs);
+
+ // Frame 2: Button down timeout reached, button click registered.
+ curr_frame = make_hwstate(0.2, GESTURES_BUTTON_LEFT, 1, 1, &button_finger_);
+ gs = wrapper_->SyncInterpret(curr_frame, nullptr);
+ ASSERT_NE(nullptr, gs);
+ EXPECT_EQ(kGestureTypeButtonsChange, gs->type);
+
+ // Frame 3: Fingers added, no movement detected yet.
+ FingerState fingers_appear[] = {
+ button_finger_,
+ {0, 0, 0, 0, 50, 0, 450, 400, 2, 0},
+ {0, 0, 0, 0, 50, 0, 550, 400, 3, 0},
+ };
+ curr_frame = make_hwstate(0.3, GESTURES_BUTTON_LEFT, 3, 3, fingers_appear);
+ gs = wrapper_->SyncInterpret(curr_frame, nullptr);
+ EXPECT_EQ(nullptr, gs);
+
+ // Frame 4: Fingers move in a scroll-like pattern.
+ FingerState scrolling_fingers_1[] = {
+ button_finger_,
+ {0, 0, 0, 0, 50, 0, 442, 389, 2, 0},
+ {0, 0, 0, 0, 50, 0, 555, 380, 3, 0},
+ };
+ curr_frame =
+ make_hwstate(0.4, GESTURES_BUTTON_LEFT, 3, 3, scrolling_fingers_1);
+ gs = wrapper_->SyncInterpret(curr_frame, nullptr);
+ ASSERT_NE(nullptr, gs);
+ EXPECT_EQ(kGestureTypeScroll, gs->type);
+ EXPECT_EQ(0, gs->details.scroll.dx);
+ EXPECT_EQ(-20, gs->details.scroll.dy);
+
+ // Frame 5: Fingers move in a scroll-like pattern again.
+ FingerState scrolling_fingers_2[] = {
+ button_finger_,
+ {0, 0, 0, 0, 50, 0, 448, 364, 2, 0},
+ {0, 0, 0, 0, 50, 0, 560, 350, 3, 0},
+ };
+ curr_frame =
+ make_hwstate(0.5, GESTURES_BUTTON_LEFT, 3, 3, scrolling_fingers_2);
+ gs = wrapper_->SyncInterpret(curr_frame, nullptr);
+ ASSERT_NE(nullptr, gs);
+ EXPECT_EQ(kGestureTypeScroll, gs->type);
+ EXPECT_EQ(0, gs->details.scroll.dx);
+ EXPECT_EQ(-30, gs->details.scroll.dy);
+}
+
+
+TEST_F(DragScrollTest, DragScrollTransitionsToFlingOnLift) {
+ ii_->drag_scroll_enable_.val_ = true;
+
+ // Frame 1: Button down.
+ HardwareState curr_frame =
+ make_hwstate(0.1, GESTURES_BUTTON_LEFT, 1, 1, &button_finger_);
+ Gesture* gs = wrapper_->SyncInterpret(curr_frame, nullptr);
+ EXPECT_EQ(nullptr, gs);
+
+ // Frame 2: Button down timeout reached, button click registered.
+ curr_frame = make_hwstate(0.2, GESTURES_BUTTON_LEFT, 1, 1, &button_finger_);
+ gs = wrapper_->SyncInterpret(curr_frame, nullptr);
+ ASSERT_NE(nullptr, gs);
+ EXPECT_EQ(kGestureTypeButtonsChange, gs->type);
+
+ // Frame 3: Fingers are added.
+ FingerState fingers_appear[] = {
+ button_finger_,
+ {0, 0, 0, 0, 50, 0, 450, 400, 2, 0},
+ {0, 0, 0, 0, 50, 0, 550, 400, 3, 0},
+ };
+ curr_frame = make_hwstate(0.3, GESTURES_BUTTON_LEFT, 3, 3, fingers_appear);
+ gs = wrapper_->SyncInterpret(curr_frame, nullptr);
+ EXPECT_EQ(nullptr, gs);
+
+ // Frame 4: Scroll occurs.
+ FingerState scrolling_fingers[] = {
+ button_finger_,
+ {0, 0, 0, 0, 50, 0, 442, 391, 2, 0},
+ {0, 0, 0, 0, 50, 0, 555, 379, 3, 0},
+ };
+ curr_frame = make_hwstate(0.4, GESTURES_BUTTON_LEFT, 3, 3, scrolling_fingers);
+ gs = wrapper_->SyncInterpret(curr_frame, nullptr);
+ ASSERT_NE(nullptr, gs);
+ EXPECT_EQ(kGestureTypeScroll, gs->type);
+ EXPECT_EQ(0, gs->details.scroll.dx);
+ EXPECT_EQ(-21, gs->details.scroll.dy);
+
+ // Frame 5: Fingers 2 & 3 are lifted, which should generate a Fling.
+ curr_frame = make_hwstate(0.5, GESTURES_BUTTON_LEFT, 1, 1, &button_finger_);
+ gs = wrapper_->SyncInterpret(curr_frame, nullptr);
+ ASSERT_NE(nullptr, gs);
+ EXPECT_EQ(kGestureTypeFling, gs->type);
+}
+
+
+TEST_F(DragScrollTest, DragScrollRevertsToMove) {
+ ii_->drag_scroll_enable_.val_ = true;
+
+ // Frame 1: Button down.
+ HardwareState curr_frame =
+ make_hwstate(0.1, GESTURES_BUTTON_LEFT, 1, 1, &button_finger_);
+ Gesture* gs = wrapper_->SyncInterpret(curr_frame, nullptr);
+ EXPECT_EQ(nullptr, gs);
+
+ // Frame 2: Button down timeout reached, button click registered.
+ curr_frame = make_hwstate(0.2, GESTURES_BUTTON_LEFT, 1, 1, &button_finger_);
+ gs = wrapper_->SyncInterpret(curr_frame, nullptr);
+ ASSERT_NE(nullptr, gs);
+ EXPECT_EQ(kGestureTypeButtonsChange, gs->type);
+
+ // Frame 3: Fingers are added.
+ FingerState fingers_appear[] = {
+ button_finger_,
+ {0, 0, 0, 0, 50, 0, 450, 400, 2, 0},
+ {0, 0, 0, 0, 50, 0, 550, 400, 3, 0},
+ };
+ curr_frame = make_hwstate(0.3, GESTURES_BUTTON_LEFT, 3, 3, fingers_appear);
+ gs = wrapper_->SyncInterpret(curr_frame, nullptr);
+ EXPECT_EQ(nullptr, gs);
+
+ // Frame 4: Scroll occurs.
+ FingerState scrolling_fingers[] = {
+ button_finger_,
+ {0, 0, 0, 0, 50, 0, 442, 382, 2, 0},
+ {0, 0, 0, 0, 50, 0, 555, 375, 3, 0},
+ };
+ curr_frame = make_hwstate(0.4, GESTURES_BUTTON_LEFT, 3, 3, scrolling_fingers);
+ gs = wrapper_->SyncInterpret(curr_frame, nullptr);
+ ASSERT_NE(nullptr, gs);
+ EXPECT_EQ(kGestureTypeScroll, gs->type);
+ EXPECT_EQ(0, gs->details.scroll.dx);
+ EXPECT_EQ(-25, gs->details.scroll.dy);
+
+ // Frame 5: One scrolling finger (3) lifts. Scroll ends, fling gesture
+ // is created.
+ FingerState one_finger_lifts[] = {
+ button_finger_,
+ {0, 0, 0, 0, 50, 0, 444, 382, 2, 0},
+ };
+ curr_frame = make_hwstate(0.5, GESTURES_BUTTON_LEFT, 2, 2, one_finger_lifts);
+ gs = wrapper_->SyncInterpret(curr_frame, nullptr);
+ ASSERT_NE(nullptr, gs);
+ EXPECT_EQ(kGestureTypeFling, gs->type);
+
+ // Frame 6: With only one moving finger left, it continues as a Move.
+ FingerState remaining_finger_moves[] = {
+ button_finger_,
+ {0, 0, 0, 0, 50, 0, 402, 370, 2, 0},
+ };
+ curr_frame =
+ make_hwstate(0.6, GESTURES_BUTTON_LEFT, 2, 2, remaining_finger_moves);
+ gs = wrapper_->SyncInterpret(curr_frame, nullptr);
+ ASSERT_NE(nullptr, gs);
+ EXPECT_EQ(kGestureTypeMove, gs->type);
+}
+
+TEST_F(DragScrollTest, DragScrollWithThreeMovingFingers) {
+ ii_->drag_scroll_enable_.val_ = true;
+
+ // Frame 1: Button down.
+ HardwareState curr_frame =
+ make_hwstate(0.1, GESTURES_BUTTON_LEFT, 1, 1, &button_finger_);
+ Gesture* gs = wrapper_->SyncInterpret(curr_frame, nullptr);
+ EXPECT_EQ(nullptr, gs);
+
+ // Frame 2: Button down timeout reached, button click registered.
+ curr_frame = make_hwstate(0.2, GESTURES_BUTTON_LEFT, 1, 1, &button_finger_);
+ gs = wrapper_->SyncInterpret(curr_frame, nullptr);
+ ASSERT_NE(nullptr, gs);
+ EXPECT_EQ(kGestureTypeButtonsChange, gs->type);
+
+ // Frame 3: Fingers 2,3 and 4 just arrived.
+ FingerState fingers_appear[] = {
+ button_finger_,
+ {0, 0, 0, 0, 50, 0, 450, 400, 2, 0},
+ {0, 0, 0, 0, 50, 0, 550, 400, 3, 0},
+ {0, 0, 0, 0, 50, 0, 650, 400, 4, 0},
+ };
+ curr_frame = make_hwstate(0.3, GESTURES_BUTTON_LEFT, 4, 4, fingers_appear);
+ gs = wrapper_->SyncInterpret(curr_frame, nullptr);
+ EXPECT_EQ(nullptr, gs);
+
+ // Frame 4: Logic should pick closest pair (2 & 3) for the scroll.
+ FingerState scrolling_fingers[] = {
+ button_finger_,
+ {0, 0, 0, 0, 50, 0, 450, 380, 2, 0},
+ {0, 0, 0, 0, 50, 0, 546, 383, 3, 0},
+ {0, 0, 0, 0, 50, 0, 650, 395, 4, 0},
+ };
+ curr_frame = make_hwstate(0.4, GESTURES_BUTTON_LEFT, 4, 4, scrolling_fingers);
+ gs = wrapper_->SyncInterpret(curr_frame, nullptr);
+ ASSERT_NE(nullptr, gs);
+ EXPECT_EQ(kGestureTypeScroll, gs->type);
+ EXPECT_EQ(-20, gs->details.scroll.dy);
+}
+
+TEST_F(DragScrollTest, DragScrollEnabledNormalDrag) {
+ ii_->drag_scroll_enable_.val_ = true;
+
+ // Frame 1: Button down.
+ FingerState button_drag_finger = {0, 0, 0, 0, 50, 0, 500, 500, 1, 0};
+ HardwareState curr_frame =
+ make_hwstate(0.1, GESTURES_BUTTON_LEFT, 1, 1, &button_drag_finger);
+ Gesture* gs = wrapper_->SyncInterpret(curr_frame, nullptr);
+ EXPECT_EQ(nullptr, gs);
+
+ // Frame 2: Button down timeout reached, button click registered.
+ curr_frame =
+ make_hwstate(0.2, GESTURES_BUTTON_LEFT, 1, 1, &button_drag_finger);
+ gs = wrapper_->SyncInterpret(curr_frame, nullptr);
+ ASSERT_NE(nullptr, gs);
+ EXPECT_EQ(kGestureTypeButtonsChange, gs->type);
+
+ // Frame 3: Finger moves, should be a standard Move gesture.
+ FingerState finger_pos_2 = {0, 0, 0, 0, 50, 0, 512, 533, 1, 0};
+ curr_frame = make_hwstate(0.3, GESTURES_BUTTON_LEFT, 1, 1, &finger_pos_2);
+ gs = wrapper_->SyncInterpret(curr_frame, nullptr);
+ ASSERT_NE(nullptr, gs);
+ EXPECT_EQ(kGestureTypeMove, gs->type);
+ EXPECT_EQ(12, gs->details.move.dx);
+ EXPECT_EQ(33, gs->details.move.dy);
+
+ // Frame 4: Finger moves again.
+ FingerState finger_pos_3 = {0, 0, 0, 0, 50, 0, 536, 552, 1, 0};
+ curr_frame = make_hwstate(0.4, GESTURES_BUTTON_LEFT, 1, 1, &finger_pos_3);
+ gs = wrapper_->SyncInterpret(curr_frame, nullptr);
+ ASSERT_NE(nullptr, gs);
+ EXPECT_EQ(kGestureTypeMove, gs->type);
+ EXPECT_EQ(24, gs->details.move.dx);
+ EXPECT_EQ(19, gs->details.move.dy);
+
+ // Frame 5: Button released.
+ curr_frame = make_hwstate(0.5, 0, 1, 1, &finger_pos_3);
+ gs = wrapper_->SyncInterpret(curr_frame, nullptr);
+ ASSERT_NE(nullptr, gs);
+ EXPECT_EQ(kGestureTypeButtonsChange, gs->type);
+ EXPECT_EQ(GESTURES_BUTTON_LEFT, gs->details.buttons.up);
+}
+
+// The test validates a two-finger drag + scroll where the button finger is also
+// one of the two scrolling fingers.
+TEST_F(DragScrollTest, DragScrollTwoFingersOnly) {
+ ii_->drag_scroll_enable_.val_ = true;
+
+ // Frame 1: Button down.
+ FingerState button_finger = {0, 0, 0, 0, 50, 0, 500, 500, 1, 0};
+ HardwareState curr_frame =
+ make_hwstate(0.1, GESTURES_BUTTON_LEFT, 1, 1, &button_finger);
+ Gesture* gs = wrapper_->SyncInterpret(curr_frame, nullptr);
+ EXPECT_EQ(nullptr, gs);
+
+ // Frame 2: Button down timeout reached, button click registered.
+ curr_frame = make_hwstate(0.2, GESTURES_BUTTON_LEFT, 1, 1, &button_finger);
+ gs = wrapper_->SyncInterpret(curr_frame, nullptr);
+ ASSERT_NE(nullptr, gs);
+ EXPECT_EQ(kGestureTypeButtonsChange, gs->type);
+
+ // Frame 3: A second finger is added.
+ FingerState fingers_appear[] = {
+ {0, 0, 0, 0, 50, 0, 500, 480, 1, 0},
+ {0, 0, 0, 0, 50, 0, 600, 480, 2, 0},
+ };
+ curr_frame = make_hwstate(0.3, GESTURES_BUTTON_LEFT, 2, 2, fingers_appear);
+ gs = wrapper_->SyncInterpret(curr_frame, nullptr);
+ EXPECT_EQ(nullptr, gs);
+
+ // Frame 4: Both fingers now move together in a scroll motion.
+ FingerState scrolling_fingers[] = {
+ {0, 0, 0, 0, 50, 0, 495, 464, 1, 0},
+ {0, 0, 0, 0, 50, 0, 598, 460, 2, 0},
+ };
+ curr_frame = make_hwstate(0.4, GESTURES_BUTTON_LEFT, 2, 2, scrolling_fingers);
+ gs = wrapper_->SyncInterpret(curr_frame, nullptr);
+
+ // The gesture should now be a Scroll.
+ ASSERT_NE(nullptr, gs);
+ EXPECT_EQ(kGestureTypeScroll, gs->type);
+ EXPECT_EQ(0, gs->details.scroll.dx);
+ EXPECT_EQ(-20, gs->details.scroll.dy);
+}
+
} // namespace gestures