| // Copyright 2019 The Fuchsia Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include <fuchsia/ui/input/cpp/fidl.h> | |
| #include <lib/syslog/cpp/macros.h> | |
| #include <gtest/gtest.h> | |
| #include "src/lib/ui/input/gesture_detector.h" | |
| namespace { | |
| using fuchsia::ui::input::PointerEvent; | |
| using fuchsia::ui::input::PointerEventPhase; | |
| using fuchsia::ui::input::PointerEventType; | |
| enum class InteractionType { kUnknown, kPreTap, kTap, kDrag }; | |
| struct InteractionRecord { | |
| bool active = false; | |
| InteractionType interaction_type = InteractionType::kUnknown; | |
| input::GestureDetector::TapType tap_type; | |
| glm::vec2 coordinate; | |
| input::Gesture::Delta delta; | |
| }; | |
| class TestInteraction : public input::GestureDetector::Interaction { | |
| public: | |
| TestInteraction(InteractionRecord* record) : record_(record) { *record_ = {.active = true}; } | |
| ~TestInteraction() override { record_->active = false; } | |
| private: | |
| void OnTapBegin(const glm::vec2& coordinate, input::GestureDetector::TapType tap_type) override { | |
| EXPECT_EQ(record_->interaction_type, InteractionType::kUnknown); | |
| record_->interaction_type = InteractionType::kPreTap; | |
| record_->coordinate = coordinate; | |
| record_->tap_type = tap_type; | |
| } | |
| void OnTapUpdate(input::GestureDetector::TapType tap_type) override { | |
| EXPECT_EQ(record_->interaction_type, InteractionType::kPreTap); | |
| record_->tap_type = tap_type; | |
| } | |
| void OnTapCommit() override { | |
| EXPECT_EQ(record_->interaction_type, InteractionType::kPreTap); | |
| record_->interaction_type = InteractionType::kTap; | |
| } | |
| void OnMultidrag(input::GestureDetector::TapType tap_type, | |
| const input::Gesture::Delta& delta) override { | |
| record_->interaction_type = InteractionType::kDrag; | |
| record_->tap_type = tap_type; | |
| record_->delta += delta; | |
| } | |
| InteractionRecord* record_; | |
| }; | |
| class FakeDelegate : public input::GestureDetector::Delegate { | |
| public: | |
| void SetNextInteraction(std::unique_ptr<input::GestureDetector::Interaction> next_interaction) { | |
| next_interaction_ = std::move(next_interaction); | |
| } | |
| private: | |
| // |input::GestureDetector::Delegate| | |
| std::unique_ptr<input::GestureDetector::Interaction> BeginInteraction( | |
| const input::Gesture* gesture) override { | |
| EXPECT_TRUE(next_interaction_) << "Unexpected BeginInteraction"; | |
| return std::move(next_interaction_); | |
| } | |
| std::unique_ptr<input::GestureDetector::Interaction> next_interaction_; | |
| }; | |
| class GestureDetectorTest : public testing::Test { | |
| public: | |
| GestureDetectorTest() : gesture_detector_(&delegate_) {} | |
| protected: | |
| // Sets up a |TestInteraction| to be the next |Interaction| returned by | |
| // |BeginInteraction|. | |
| void RecordInteraction(InteractionRecord* record) { | |
| delegate_.SetNextInteraction(std::make_unique<TestInteraction>(record)); | |
| } | |
| input::GestureDetector* gesture_detector() { return &gesture_detector_; } | |
| private: | |
| FakeDelegate delegate_; | |
| input::GestureDetector gesture_detector_; | |
| }; | |
| TEST_F(GestureDetectorTest, IgnoreMouseHover) { | |
| PointerEvent mouse = {.device_id = 0, | |
| .pointer_id = 0, | |
| .type = PointerEventType::MOUSE, | |
| .phase = PointerEventPhase::MOVE, | |
| .x = 0, | |
| .y = 0}; | |
| // If we erroneously create a new interaction at any point, this will violate | |
| // the expectation in |GestureDetectorTest::BeginInteraction|. | |
| gesture_detector()->OnPointerEvent(mouse); | |
| // Move the pointer around past the drag threshold. | |
| mouse.x += 2 * input::GestureDetector::kDefaultDragThreshold; | |
| gesture_detector()->OnPointerEvent(mouse); | |
| } | |
| // A default starting point for touch events. Usages should explicitly set any | |
| // members (other than |.type|) that they care about. | |
| constexpr PointerEvent kDefaultTouchEvent = { | |
| .device_id = 0, .pointer_id = 0, .type = PointerEventType::TOUCH}; | |
| TEST_F(GestureDetectorTest, Tap) { | |
| InteractionRecord ixn; | |
| RecordInteraction(&ixn); | |
| auto ptr = kDefaultTouchEvent; | |
| ptr.x = 0; | |
| ptr.y = 0; | |
| ptr.phase = PointerEventPhase::DOWN; | |
| gesture_detector()->OnPointerEvent(ptr); | |
| EXPECT_TRUE(ixn.active); | |
| ptr.phase = PointerEventPhase::UP; | |
| gesture_detector()->OnPointerEvent(ptr); | |
| EXPECT_FALSE(ixn.active); | |
| // The interaction ends when the last pointer comes up. | |
| EXPECT_EQ(ixn.interaction_type, InteractionType::kTap); | |
| EXPECT_EQ(ixn.tap_type, 1); | |
| EXPECT_EQ(ixn.coordinate, glm::vec2(0, 0)); | |
| } | |
| TEST_F(GestureDetectorTest, TwoFingerTap) { | |
| InteractionRecord ixn; | |
| RecordInteraction(&ixn); | |
| auto ptr0 = kDefaultTouchEvent; | |
| ptr0.pointer_id = 0; | |
| ptr0.x = 0; | |
| ptr0.y = 0; | |
| ptr0.phase = PointerEventPhase::DOWN; | |
| gesture_detector()->OnPointerEvent(ptr0); | |
| auto ptr1 = kDefaultTouchEvent; | |
| ptr1.pointer_id = 1; | |
| ptr1.x = 1; | |
| ptr1.y = 0; | |
| ptr1.phase = PointerEventPhase::DOWN; | |
| gesture_detector()->OnPointerEvent(ptr1); | |
| ptr1.phase = PointerEventPhase::UP; | |
| gesture_detector()->OnPointerEvent(ptr1); | |
| EXPECT_TRUE(ixn.active); | |
| ptr0.phase = PointerEventPhase::UP; | |
| gesture_detector()->OnPointerEvent(ptr0); | |
| EXPECT_FALSE(ixn.active); | |
| EXPECT_EQ(ixn.interaction_type, InteractionType::kTap); | |
| EXPECT_EQ(ixn.tap_type, 2); | |
| EXPECT_EQ(ixn.coordinate, glm::vec2(0, 0)); | |
| } | |
| TEST_F(GestureDetectorTest, TwoFingerTapWithDrift) { | |
| FX_CHECK(1 < input::GestureDetector::kDefaultDragThreshold) | |
| << "kDefaultDragThreshold is too low; rewrite this test to override."; | |
| InteractionRecord ixn; | |
| RecordInteraction(&ixn); | |
| auto ptr0 = kDefaultTouchEvent; | |
| ptr0.pointer_id = 0; | |
| ptr0.x = 0; | |
| ptr0.y = 0; | |
| ptr0.phase = PointerEventPhase::DOWN; | |
| gesture_detector()->OnPointerEvent(ptr0); | |
| ptr0.x++; | |
| ptr0.phase = PointerEventPhase::MOVE; | |
| gesture_detector()->OnPointerEvent(ptr0); | |
| auto ptr1 = kDefaultTouchEvent; | |
| ptr1.pointer_id = 1; | |
| ptr1.x = 10; | |
| ptr1.y = 0; | |
| ptr1.phase = PointerEventPhase::DOWN; | |
| gesture_detector()->OnPointerEvent(ptr1); | |
| ptr1.x++; | |
| ptr1.phase = PointerEventPhase::MOVE; | |
| gesture_detector()->OnPointerEvent(ptr1); | |
| ptr0.x--; | |
| gesture_detector()->OnPointerEvent(ptr0); | |
| // The tap should end after a pointer comes up... | |
| ptr0.x++; | |
| ptr0.phase = PointerEventPhase::UP; | |
| gesture_detector()->OnPointerEvent(ptr0); | |
| EXPECT_TRUE(ixn.active); | |
| // ...after which further movement within the drag threshold should not | |
| // trigger a drag or a new interaction. | |
| ptr1.x--; | |
| gesture_detector()->OnPointerEvent(ptr1); | |
| EXPECT_TRUE(ixn.active); | |
| ptr1.x++; | |
| ptr1.phase = PointerEventPhase::UP; | |
| gesture_detector()->OnPointerEvent(ptr1); | |
| EXPECT_FALSE(ixn.active); | |
| EXPECT_EQ(ixn.interaction_type, InteractionType::kTap); | |
| EXPECT_EQ(ixn.tap_type, 2); | |
| EXPECT_EQ(ixn.coordinate, glm::vec2(0, 0)); | |
| } | |
| TEST_F(GestureDetectorTest, Drag) { | |
| InteractionRecord ixn; | |
| RecordInteraction(&ixn); | |
| auto ptr0 = kDefaultTouchEvent; | |
| ptr0.pointer_id = 0; | |
| ptr0.x = 0; | |
| ptr0.y = 0; | |
| ptr0.phase = PointerEventPhase::DOWN; | |
| gesture_detector()->OnPointerEvent(ptr0); | |
| ptr0.x += input::GestureDetector::kDefaultDragThreshold; | |
| ptr0.phase = PointerEventPhase::MOVE; | |
| gesture_detector()->OnPointerEvent(ptr0); | |
| EXPECT_EQ(ixn.interaction_type, InteractionType::kDrag); | |
| EXPECT_EQ(ixn.tap_type, 1); | |
| EXPECT_EQ(ixn.delta, input::Gesture::Delta( | |
| {.translation = {input::GestureDetector::kDefaultDragThreshold, 0}, | |
| .rotation = 0, | |
| .scale = 1})); | |
| auto ptr1 = kDefaultTouchEvent; | |
| ptr1.pointer_id = 1; | |
| ptr1.x = 0; | |
| ptr1.y = 10; | |
| ptr1.phase = PointerEventPhase::DOWN; | |
| gesture_detector()->OnPointerEvent(ptr1); | |
| EXPECT_EQ(ixn.tap_type, 2); | |
| ptr1.x++; | |
| ptr1.phase = PointerEventPhase::MOVE; | |
| gesture_detector()->OnPointerEvent(ptr1); | |
| EXPECT_GT(ixn.delta.translation.x, input::GestureDetector::kDefaultDragThreshold); | |
| ptr0.phase = PointerEventPhase::UP; | |
| gesture_detector()->OnPointerEvent(ptr0); | |
| EXPECT_EQ(ixn.tap_type, 1); | |
| } | |
| // This covers the case where a tap has been committed due to a pointer release | |
| // but the remaining pointer is dragged past the threshold. | |
| TEST_F(GestureDetectorTest, TapIntoDrag) { | |
| const float dx = 2 * input::GestureDetector::kDefaultDragThreshold; | |
| InteractionRecord ixn; | |
| RecordInteraction(&ixn); | |
| auto ptr0 = kDefaultTouchEvent; | |
| ptr0.pointer_id = 0; | |
| ptr0.x = 0; | |
| ptr0.y = 0; | |
| ptr0.phase = PointerEventPhase::DOWN; | |
| gesture_detector()->OnPointerEvent(ptr0); | |
| auto ptr1 = kDefaultTouchEvent; | |
| ptr1.pointer_id = 1; | |
| ptr1.x = 0; | |
| ptr1.y = 10; | |
| ptr1.phase = PointerEventPhase::DOWN; | |
| gesture_detector()->OnPointerEvent(ptr1); | |
| ptr1.phase = PointerEventPhase::UP; | |
| gesture_detector()->OnPointerEvent(ptr1); | |
| EXPECT_EQ(ixn.interaction_type, InteractionType::kTap); | |
| ptr0.x += dx; | |
| ptr0.phase = PointerEventPhase::MOVE; | |
| gesture_detector()->OnPointerEvent(ptr0); | |
| EXPECT_EQ(ixn.interaction_type, InteractionType::kDrag); | |
| EXPECT_EQ(ixn.tap_type, 1); | |
| EXPECT_EQ(ixn.delta, input::Gesture::Delta({.translation = {dx, 0}, .rotation = 0, .scale = 1})); | |
| } | |
| // An |input::GestureDetector::Interaction| that owns and destroys its gesture detector on command, | |
| // for testing lifecycle robustness. It's fragile to assume that an interaction will never destroy | |
| // or |Reset()| its gesture detector on a callback. | |
| class PoisonInteraction : public input::GestureDetector::Interaction { | |
| public: | |
| PoisonInteraction(std::unique_ptr<input::GestureDetector> gesture_detector) | |
| : gesture_detector_(std::move(gesture_detector)) {} | |
| // Sets the gesture detector to self destruct on the next event. | |
| void Poison() { poisoned_ = true; } | |
| private: | |
| void OnTapBegin(const glm::vec2&, input::GestureDetector::TapType) override { CheckPoison(); } | |
| void OnTapUpdate(input::GestureDetector::TapType) override { CheckPoison(); } | |
| void OnTapCommit() override { CheckPoison(); } | |
| void OnMultidrag(input::GestureDetector::TapType, const input::Gesture::Delta&) override { | |
| CheckPoison(); | |
| } | |
| void CheckPoison() { | |
| if (poisoned_) { | |
| gesture_detector_ = nullptr; | |
| } | |
| } | |
| bool poisoned_ = false; | |
| std::unique_ptr<input::GestureDetector> gesture_detector_; | |
| }; | |
| // These tests won't necessarily fail if the code is incorrect as referencing released memory is | |
| // undefined behavior, but the allocator does catch some cases. | |
| class PoisonInteractionTest : public testing::Test { | |
| public: | |
| PoisonInteractionTest() { | |
| auto gesture_detector = std::make_unique<input::GestureDetector>(&delegate); | |
| gesture_detector_ = gesture_detector.get(); | |
| auto interaction = std::make_unique<PoisonInteraction>(std::move(gesture_detector)); | |
| interaction_ = interaction.get(); | |
| delegate.SetNextInteraction(std::move(interaction)); | |
| } | |
| input::GestureDetector* gesture_detector() { return gesture_detector_; } | |
| PoisonInteraction* interaction() { return interaction_; } | |
| private: | |
| FakeDelegate delegate; | |
| input::GestureDetector* gesture_detector_; | |
| PoisonInteraction* interaction_; | |
| }; | |
| TEST_F(PoisonInteractionTest, PoisonTapBegin) { | |
| interaction()->Poison(); | |
| auto ptr = kDefaultTouchEvent; | |
| ptr.phase = PointerEventPhase::DOWN; | |
| gesture_detector()->OnPointerEvent(ptr); | |
| } | |
| TEST_F(PoisonInteractionTest, PoisonTapUpdate) { | |
| auto ptr0 = kDefaultTouchEvent; | |
| ptr0.phase = PointerEventPhase::DOWN; | |
| gesture_detector()->OnPointerEvent(ptr0); | |
| interaction()->Poison(); | |
| auto ptr1 = kDefaultTouchEvent; | |
| ptr1.pointer_id = 1; | |
| ptr1.phase = PointerEventPhase::DOWN; | |
| gesture_detector()->OnPointerEvent(ptr1); | |
| } | |
| TEST_F(PoisonInteractionTest, PoisonTapCommit) { | |
| auto ptr = kDefaultTouchEvent; | |
| ptr.phase = PointerEventPhase::DOWN; | |
| gesture_detector()->OnPointerEvent(ptr); | |
| interaction()->Poison(); | |
| ptr.phase = PointerEventPhase::UP; | |
| gesture_detector()->OnPointerEvent(ptr); | |
| } | |
| TEST_F(PoisonInteractionTest, PoisonMultidrag) { | |
| auto ptr = kDefaultTouchEvent; | |
| ptr.x = 0; | |
| ptr.y = 0; | |
| ptr.phase = PointerEventPhase::DOWN; | |
| gesture_detector()->OnPointerEvent(ptr); | |
| interaction()->Poison(); | |
| ptr.x += input::GestureDetector::kDefaultDragThreshold; | |
| ptr.phase = PointerEventPhase::MOVE; | |
| gesture_detector()->OnPointerEvent(ptr); | |
| } | |
| class PoisonDelegate : public input::GestureDetector::Delegate { | |
| public: | |
| void SetGestureDetector(std::unique_ptr<input::GestureDetector> gesture_detector) { | |
| gesture_detector_ = std::move(gesture_detector); | |
| } | |
| input::GestureDetector* gesture_detector() { return gesture_detector_.get(); } | |
| private: | |
| // |input::GestureDetector::Delegate| | |
| std::unique_ptr<input::GestureDetector::Interaction> BeginInteraction( | |
| const input::Gesture* gesture) override { | |
| gesture_detector_ = nullptr; | |
| return nullptr; | |
| } | |
| std::unique_ptr<input::GestureDetector> gesture_detector_; | |
| }; | |
| TEST(PoisonDelegateTest, PoisonBeginInteraction) { | |
| PoisonDelegate delegate; | |
| delegate.SetGestureDetector(std::make_unique<input::GestureDetector>(&delegate)); | |
| auto ptr = kDefaultTouchEvent; | |
| ptr.phase = PointerEventPhase::DOWN; | |
| delegate.gesture_detector()->OnPointerEvent(ptr); | |
| } | |
| } // namespace |