blob: 884de03074b698471c1c4aad5dd43bf606b05d5b [file] [log] [blame]
// Copyright 2018 The Chromium 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 "chrome/browser/vr/gesture_detector.h"
#include "base/numerics/math_constants.h"
#include "chrome/browser/vr/input_event.h"
#include "chrome/browser/vr/platform_controller.h"
namespace vr {
namespace {
constexpr float kDisplacementScaleFactor = 129.0f;
constexpr int kMaxNumOfExtrapolations = 2;
// Minimum time distance needed to call two timestamps
// not equal.
constexpr float kDelta = 1.0e-7f;
constexpr float kCutoffHz = 10.0f;
constexpr float kRC = 1.0f / (2.0f * base::kPiFloat * kCutoffHz);
// A slop represents a small rectangular region around the first touch point of
// a gesture.
// If the user does not move outside of the slop, no gesture is detected.
// Gestures start to be detected when the user moves outside of the slop.
// Vertical distance from the border to the center of slop.
constexpr float kSlopVertical = 0.165f;
// Horizontal distance from the border to the center of slop.
constexpr float kSlopHorizontal = 0.15f;
// Exceeding pressing the appbutton for longer than this threshold will result
// in a long press.
constexpr base::TimeDelta kLongPressThreshold =
base::TimeDelta::FromMilliseconds(900);
struct TouchPoint {
gfx::Vector2dF position;
base::TimeTicks timestamp;
};
} // namespace
GestureDetector::GestureDetector() {
Reset();
}
GestureDetector::~GestureDetector() = default;
InputEventList GestureDetector::DetectGestures(
const PlatformController& controller,
base::TimeTicks current_timestamp) {
touch_position_changed_ = UpdateCurrentTouchPoint(controller);
TouchPoint touch_point{.position = controller.GetPositionInTrackpad(),
.timestamp = controller.GetLastTouchTimestamp()};
ExtrapolateTouchPoint(&touch_point, current_timestamp);
if (touch_position_changed_)
UpdateOverallVelocity(touch_point);
is_select_button_pressed_ =
controller.IsButtonDown(PlatformController::kButtonSelect);
last_touching_state_ = is_touching_trackpad_;
is_touching_trackpad_ = controller.IsTouchingTrackpad();
InputEventList gesture_list;
DetectMenuButtonGestures(&gesture_list, controller, current_timestamp);
auto gesture = GetGestureFromTouchInfo(touch_point);
if (!gesture)
return gesture_list;
if (gesture->type() == InputEvent::kScrollEnd)
Reset();
if (gesture->type() != InputEvent::kTypeUndefined)
gesture_list.push_back(std::move(gesture));
return gesture_list;
}
void GestureDetector::DetectMenuButtonGestures(
InputEventList* event_list,
const PlatformController& controller,
base::TimeTicks current_timestamp) {
std::unique_ptr<InputEvent> event;
if (controller.ButtonDownHappened(PlatformController::kButtonMenu)) {
menu_button_down_timestamp_ = current_timestamp;
menu_button_long_pressed_ = false;
}
if (controller.ButtonUpHappened(PlatformController::kButtonMenu)) {
event = std::make_unique<InputEvent>(
menu_button_long_pressed_ ? InputEvent::kMenuButtonLongPressEnd
: InputEvent::kMenuButtonClicked);
}
if (!menu_button_long_pressed_ &&
controller.IsButtonDown(PlatformController::kButtonMenu) &&
current_timestamp - menu_button_down_timestamp_ > kLongPressThreshold) {
menu_button_long_pressed_ = true;
event = std::make_unique<InputEvent>(InputEvent::kMenuButtonLongPressStart);
}
if (event) {
event->set_time_stamp(current_timestamp);
event_list->push_back(std::move(event));
}
}
std::unique_ptr<InputEvent> GestureDetector::GetGestureFromTouchInfo(
const TouchPoint& touch_point) {
std::unique_ptr<InputEvent> gesture;
switch (state_->label) {
// User has not put finger on touch pad.
case WAITING:
gesture = HandleWaitingState(touch_point);
break;
// User has not started a gesture (by moving out of slop).
case TOUCHING:
gesture = HandleDetectingState(touch_point);
break;
// User is scrolling on touchpad
case SCROLLING:
gesture = HandleScrollingState(touch_point);
break;
// The user has finished scrolling, but we'll hallucinate a few points
// before really finishing.
case POST_SCROLL:
gesture = HandlePostScrollingState(touch_point);
break;
default:
NOTREACHED();
break;
}
if (gesture)
gesture->set_time_stamp(touch_point.timestamp);
return gesture;
}
std::unique_ptr<InputEvent> GestureDetector::HandleWaitingState(
const TouchPoint& touch_point) {
// User puts finger on touch pad (or when the touch down for current gesture
// is missed, initiate gesture from current touch point).
if (is_touching_trackpad_) {
// update initial touchpoint
state_->initial_touch_point = touch_point;
// update current touchpoint
state_->cur_touch_point = touch_point;
state_->label = TOUCHING;
return std::make_unique<InputEvent>(InputEvent::kFlingCancel);
}
return nullptr;
}
std::unique_ptr<InputEvent> GestureDetector::HandleDetectingState(
const TouchPoint& touch_point) {
// User lifts up finger from touch pad.
if (!is_touching_trackpad_) {
Reset();
return nullptr;
}
// Touch position is changed, the touch point moves outside of slop,
// and the Controller's button is not down.
if (touch_position_changed_ && is_touching_trackpad_ &&
!InSlop(touch_point.position) && !is_select_button_pressed_) {
state_->label = SCROLLING;
auto gesture = std::make_unique<InputEvent>(InputEvent::kScrollBegin);
UpdateGestureParameters(touch_point);
UpdateGestureWithScrollDelta(gesture.get());
return gesture;
}
return nullptr;
}
std::unique_ptr<InputEvent> GestureDetector::HandleScrollingState(
const TouchPoint& touch_point) {
if (is_select_button_pressed_) {
UpdateGestureParameters(touch_point);
return std::make_unique<InputEvent>(InputEvent::kScrollEnd);
}
if (!is_touching_trackpad_)
state_->label = POST_SCROLL;
if (touch_position_changed_) {
auto gesture = std::make_unique<InputEvent>(InputEvent::kScrollUpdate);
UpdateGestureParameters(touch_point);
UpdateGestureWithScrollDelta(gesture.get());
return gesture;
}
return nullptr;
}
std::unique_ptr<InputEvent> GestureDetector::HandlePostScrollingState(
const TouchPoint& touch_point) {
if (extrapolated_touch_ == 0 || is_select_button_pressed_) {
UpdateGestureParameters(touch_point);
return std::make_unique<InputEvent>(InputEvent::kScrollEnd);
} else {
auto gesture = std::make_unique<InputEvent>(InputEvent::kScrollUpdate);
UpdateGestureParameters(touch_point);
UpdateGestureWithScrollDelta(gesture.get());
return gesture;
}
}
void GestureDetector::UpdateGestureWithScrollDelta(InputEvent* gesture) {
gesture->scroll_data.delta_x =
state_->displacement.x() * kDisplacementScaleFactor;
gesture->scroll_data.delta_y =
state_->displacement.y() * kDisplacementScaleFactor;
}
bool GestureDetector::UpdateCurrentTouchPoint(
const PlatformController& controller) {
if (controller.IsTouchingTrackpad() || last_touching_state_) {
// Update the touch point when the touch position has changed.
if (state_->cur_touch_point.position !=
controller.GetPositionInTrackpad()) {
state_->prev_touch_point = state_->cur_touch_point;
state_->cur_touch_point = {
.position = controller.GetPositionInTrackpad(),
.timestamp = controller.GetLastTouchTimestamp()};
return true;
}
}
return false;
}
void GestureDetector::ExtrapolateTouchPoint(TouchPoint* touch_point,
base::TimeTicks current_timestamp) {
const bool effectively_scrolling =
state_->label == SCROLLING || state_->label == POST_SCROLL;
if (effectively_scrolling && extrapolated_touch_ < kMaxNumOfExtrapolations &&
(touch_point->timestamp == last_touch_timestamp_ ||
touch_point->position == state_->prev_touch_point.position)) {
extrapolated_touch_++;
touch_position_changed_ = true;
float duration = (current_timestamp - last_timestamp_).InSecondsF();
touch_point->position.set_x(state_->cur_touch_point.position.x() +
state_->overall_velocity.x() * duration);
touch_point->position.set_y(state_->cur_touch_point.position.y() +
state_->overall_velocity.y() * duration);
} else {
if (extrapolated_touch_ == kMaxNumOfExtrapolations) {
state_->overall_velocity = {0, 0};
}
extrapolated_touch_ = 0;
}
last_touch_timestamp_ = touch_point->timestamp;
last_timestamp_ = current_timestamp;
}
void GestureDetector::UpdateOverallVelocity(const TouchPoint& touch_point) {
float duration =
(touch_point.timestamp - state_->prev_touch_point.timestamp).InSecondsF();
// If the timestamp does not change, do not update velocity.
if (duration < kDelta)
return;
const gfx::Vector2dF& displacement =
touch_point.position - state_->prev_touch_point.position;
const gfx::Vector2dF& velocity = ScaleVector2d(displacement, (1 / duration));
float weight = duration / (kRC + duration);
state_->overall_velocity =
ScaleVector2d(state_->overall_velocity, (1 - weight)) +
ScaleVector2d(velocity, weight);
}
void GestureDetector::UpdateGestureParameters(const TouchPoint& touch_point) {
state_->displacement =
touch_point.position - state_->prev_touch_point.position;
}
bool GestureDetector::InSlop(const gfx::PointF touch_position) const {
return (std::abs(touch_position.x() -
state_->initial_touch_point.position.x()) <
kSlopHorizontal) &&
(std::abs(touch_position.y() -
state_->initial_touch_point.position.y()) < kSlopVertical);
}
void GestureDetector::Reset() {
state_ = std::make_unique<GestureDetectorState>();
}
} // namespace vr