| // Copyright 2018 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 "src/lib/ui/input/gesture_detector.h" |
| |
| #include <lib/syslog/cpp/macros.h> |
| |
| #include <glm/gtx/norm.hpp> |
| |
| namespace input { |
| namespace { |
| |
| GestureDetector::TapType ClassifyTap(const fuchsia::ui::input::PointerEvent& event, |
| const Gesture& state) { |
| // TODO(fxbug.dev/18121): Allow custom mappings. |
| switch (event.type) { |
| case fuchsia::ui::input::PointerEventType::MOUSE: |
| if (event.buttons == fuchsia::ui::input::kMouseTertiaryButton) { |
| // Map the tertiary mouse button to the same tap type (3) as left + |
| // right click. |
| return 3; |
| } else { |
| return event.buttons; |
| } |
| case fuchsia::ui::input::PointerEventType::TOUCH: |
| return state.pointer_count(); |
| case fuchsia::ui::input::PointerEventType::STYLUS: |
| // For stylus, map the buttonless case to tap type 1, and decorate with |
| // buttons. |
| return 1 + event.buttons; |
| case fuchsia::ui::input::PointerEventType::INVERTED_STYLUS: |
| // When the stylus is inverted, bump the tap type by 1 (e.g. the |
| // buttonless case becomes tap type 2). |
| return 2 + event.buttons; |
| } |
| } |
| |
| #ifndef NDEBUG |
| |
| // A delegating |GestureDetector::Interaction| wrapper that checks that |
| // |tap_type > 0|. |
| class CheckedInteraction : public GestureDetector::Interaction { |
| public: |
| CheckedInteraction(std::unique_ptr<GestureDetector::Interaction> interaction) |
| : interaction_(std::move(interaction)) {} |
| |
| private: |
| void OnTapBegin(const glm::vec2& coordinate, GestureDetector::TapType tap_type) override { |
| FX_CHECK(tap_type > 0); |
| interaction_->OnTapBegin(coordinate, tap_type); |
| } |
| |
| void OnTapUpdate(GestureDetector::TapType tap_type) override { |
| FX_CHECK(tap_type > 0); |
| interaction_->OnTapUpdate(tap_type); |
| } |
| |
| void OnTapCommit() override { interaction_->OnTapCommit(); } |
| |
| void OnMultidrag(GestureDetector::TapType tap_type, const Gesture::Delta& delta) override { |
| FX_CHECK(tap_type > 0); |
| interaction_->OnMultidrag(tap_type, delta); |
| } |
| |
| std::unique_ptr<GestureDetector::Interaction> interaction_; |
| }; |
| |
| #endif // NDEBUG |
| |
| } // namespace |
| |
| GestureDetector::Interaction::~Interaction() = default; |
| |
| void GestureDetector::Interaction::OnTapBegin(const glm::vec2& coordinate, |
| GestureDetector::TapType tap_type) {} |
| void GestureDetector::Interaction::OnTapUpdate(GestureDetector::TapType tap_type) {} |
| void GestureDetector::Interaction::OnTapCommit() {} |
| void GestureDetector::Interaction::OnMultidrag(GestureDetector::TapType tap_type, |
| const Gesture::Delta& delta) {} |
| |
| GestureDetector::Delegate::~Delegate() = default; |
| |
| GestureDetector::DevicePointerState::DevicePointerState() : weak_ptr_factory_(this) {} |
| |
| fxl::WeakPtr<GestureDetector::DevicePointerState> |
| GestureDetector::DevicePointerState::GetWeakPtr() { |
| return weak_ptr_factory_.GetWeakPtr(); |
| } |
| |
| GestureDetector::GestureDetector(Delegate* delegate, float drag_threshold) |
| : delegate_(delegate), drag_threshold_squared_(drag_threshold * drag_threshold) {} |
| |
| void GestureDetector::OnPointerEvent(fuchsia::ui::input::PointerEvent event) { |
| switch (event.phase) { |
| case fuchsia::ui::input::PointerEventPhase::DOWN: { |
| fxl::WeakPtr<DevicePointerState> state = devices_[event.device_id].GetWeakPtr(); |
| state->gesture.AddPointer(event.pointer_id, {event.x, event.y}); |
| |
| if (!state->interaction) { |
| std::unique_ptr<Interaction> interaction = delegate_->BeginInteraction(&state->gesture); |
| // Store interaction in temporary variable so we can guard against state having been |
| // destroyed in |BeginInteraction|. This can happen because the delegate can implement |
| // |BeginInteraction| however it likes, including by resetting or destroying the |
| // |GestureDetector|. |
| if (!state) { |
| return; |
| } |
| state->interaction = std::move(interaction); |
| #ifndef NDEBUG |
| state->interaction = std::make_unique<CheckedInteraction>(std::move(state->interaction)); |
| #endif |
| // Only start a tap if we're the first pointer. Otherwise, if we've |
| // already committed a tap, immediately enter a multidrag. |
| if (state->gesture.pointer_count() == 1) { |
| state->tap_type = ClassifyTap(event, state->gesture); |
| state->interaction->OnTapBegin({event.x, event.y}, state->tap_type); |
| if (!state) { |
| return; |
| } |
| } |
| state->origins[event.pointer_id] = {event.x, event.y}; |
| } else if (state->tap_type > 0) { |
| TapType tap_type = ClassifyTap(event, state->gesture); |
| if (tap_type > state->tap_type) { |
| state->tap_type = tap_type; |
| state->interaction->OnTapUpdate(tap_type); |
| if (!state) { |
| return; |
| } |
| } |
| state->origins[event.pointer_id] = {event.x, event.y}; |
| } else { |
| // This is an in-progress multidrag that we should update with the new |
| // tap_type. |
| state->interaction->OnMultidrag(ClassifyTap(event, state->gesture), {}); |
| } |
| break; |
| } |
| case fuchsia::ui::input::PointerEventPhase::MOVE: { |
| auto it = devices_.find(event.device_id); |
| if (it == devices_.end()) { |
| // TODO:(fxbug.dev/24628): This ignores the mouse move case, which happens |
| // outside of a DOWN/UP pair. |
| break; |
| } |
| |
| DevicePointerState& state = it->second; |
| FX_DCHECK(state.interaction); |
| // Unlike in the other handlers, we don't need to guard state with a weak pointer here since |
| // all the |Interaction| callbacks in this one are the last line in the method. |
| |
| Gesture::Delta delta = state.gesture.UpdatePointer(event.pointer_id, {event.x, event.y}); |
| if (!state.tap_type) { |
| // 0 signifies in-progress multidrag (see |tap_type| docs for details). |
| state.interaction->OnMultidrag(ClassifyTap(event, state.gesture), delta); |
| } else { |
| // Decide whether we've exceeded the threshold to start a multidrag. |
| state.pending_delta += delta; |
| |
| if (glm::distance2(state.origins[event.pointer_id], {event.x, event.y}) >= |
| drag_threshold_squared_) { |
| // Kill the tap and handle as a multidrag from now on. |
| state.tap_type = 0; |
| state.origins.clear(); |
| state.interaction->OnMultidrag(ClassifyTap(event, state.gesture), state.pending_delta); |
| } |
| } |
| break; |
| } |
| case fuchsia::ui::input::PointerEventPhase::UP: { |
| auto it = devices_.find(event.device_id); |
| if (it != devices_.end()) { |
| fxl::WeakPtr<DevicePointerState> state = it->second.GetWeakPtr(); |
| if (state->tap_type > 0) { |
| state->interaction->OnTapCommit(); |
| if (!state) { |
| return; |
| } |
| state->tap_type = -state->tap_type; |
| } |
| state->gesture.RemovePointer(event.pointer_id); |
| if (!state->gesture.has_pointers()) { |
| devices_.erase(it); // This destroys the interaction as well. |
| } else if (!state->tap_type) { |
| // If there's still a drag active, update the |tap_type|. |
| state->interaction->OnMultidrag(ClassifyTap(event, state->gesture), {}); |
| } |
| } |
| break; |
| } |
| default: |
| break; |
| } |
| } |
| |
| } // namespace input |