blob: 6fdfa1bc778b7871af45cb834df73d0c61abb155 [file] [log] [blame]
// 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