// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/renderer_host/direct_manipulation_event_handler_win.h"
#include "base/logging.h"
#include "base/notreached.h"
#include "base/strings/string_number_conversions.h"
#include "content/browser/renderer_host/direct_manipulation_helper_win.h"
#include "ui/base/ui_base_features.h"
#include "ui/base/win/window_event_target.h"
namespace content {
namespace {
bool FloatEquals(float f1, float f2) {
// The idea behind this is to use this fraction of the larger of the
// two numbers as the limit of the difference. This breaks down near
// zero, so we reuse this as the minimum absolute size we will use
// for the base of the scale too.
static const float epsilon_scale = 0.00001f;
return fabs(f1 - f2) <
epsilon_scale *
std::fmax(std::fmax(std::fabs(f1), std::fabs(f2)), epsilon_scale);
}
} // namespace
DirectManipulationEventHandler::DirectManipulationEventHandler(
ui::WindowEventTarget* event_target)
: event_target_(event_target) {}
bool DirectManipulationEventHandler::SetViewportSizeInPixels(
const gfx::Size& viewport_size_in_pixels) {
if (viewport_size_in_pixels_ == viewport_size_in_pixels)
return false;
viewport_size_in_pixels_ = viewport_size_in_pixels;
return true;
}
void DirectManipulationEventHandler::SetDeviceScaleFactor(
float device_scale_factor) {
device_scale_factor_ = device_scale_factor;
}
void DirectManipulationEventHandler::SetDirectManipulationHelper(
DirectManipulationHelper* helper) {
helper_ = helper;
}
DirectManipulationEventHandler::~DirectManipulationEventHandler() {}
void DirectManipulationEventHandler::TransitionToState(
GestureState new_gesture_state) {
if (gesture_state_ == new_gesture_state)
return;
GestureState previous_gesture_state = gesture_state_;
gesture_state_ = new_gesture_state;
// End the previous sequence.
switch (previous_gesture_state) {
case GestureState::kScroll: {
// kScroll -> kNone, kPinch, ScrollEnd.
// kScroll -> kFling, we don't want to end the current scroll sequence.
if (new_gesture_state != GestureState::kFling)
event_target_->ApplyPanGestureScrollEnd(new_gesture_state ==
GestureState::kPinch);
break;
}
case GestureState::kFling: {
// kFling -> *, FlingEnd.
event_target_->ApplyPanGestureFlingEnd();
break;
}
case GestureState::kPinch: {
DCHECK_EQ(new_gesture_state, GestureState::kNone);
// kPinch -> kNone, PinchEnd. kPinch should only transition to kNone.
event_target_->ApplyPinchZoomEnd();
break;
}
case GestureState::kNone: {
// kNone -> *, no cleanup is needed.
break;
}
default:
NOTREACHED_IN_MIGRATION();
}
// Start the new sequence.
switch (new_gesture_state) {
case GestureState::kScroll: {
// kFling, kNone -> kScroll, ScrollBegin.
// ScrollBegin is different phase event with others. It must send within
// the first scroll event.
should_send_scroll_begin_ = true;
break;
}
case GestureState::kFling: {
// Only kScroll can transition to kFling.
DCHECK_EQ(previous_gesture_state, GestureState::kScroll);
event_target_->ApplyPanGestureFlingBegin();
break;
}
case GestureState::kPinch: {
// * -> kPinch, PinchBegin.
// Pinch gesture may begin with some scroll events.
event_target_->ApplyPinchZoomBegin();
break;
}
case GestureState::kNone: {
// * -> kNone, only cleanup is needed.
break;
}
default:
NOTREACHED_IN_MIGRATION();
}
}
HRESULT DirectManipulationEventHandler::OnViewportStatusChanged(
IDirectManipulationViewport* viewport,
DIRECTMANIPULATION_STATUS current,
DIRECTMANIPULATION_STATUS previous) {
// MSDN never mention |viewport| are nullable and we never saw it is null when
// testing.
DCHECK(viewport);
// The state of our viewport has changed! We'l be in one of three states:
// - ENABLED: initial state
// - READY: the previous gesture has been completed
// - RUNNING: gesture updating
// - INERTIA: finger leave touchpad content still updating by inertia
// Windows should not call this when event_target_ is null since we do not
// pass the DM_POINTERHITTEST to DirectManipulation.
if (!event_target_)
return S_OK;
if (current == previous)
return S_OK;
if (current == DIRECTMANIPULATION_INERTIA) {
// Fling must lead by Scroll. We can actually hit here when user pinch then
// quickly pan gesture and leave touchpad. In this case, we don't want to
// start a new sequence until the gesture end. The rest events in sequence
// will be ignore since sequence still in pinch and only scale factor
// changes will be applied.
if (previous != DIRECTMANIPULATION_RUNNING ||
gesture_state_ != GestureState::kScroll) {
return S_OK;
}
TransitionToState(GestureState::kFling);
}
if (current == DIRECTMANIPULATION_RUNNING) {
// INERTIA -> RUNNING, should start a new sequence.
if (previous == DIRECTMANIPULATION_INERTIA)
TransitionToState(GestureState::kNone);
}
if (current != DIRECTMANIPULATION_READY)
return S_OK;
// Normally gesture sequence will receive 2 READY message, the first one is
// gesture end, the second one is from viewport reset. We don't have content
// transform in the second RUNNING -> READY. We should not reset on an empty
// RUNNING -> READY sequence.
if (last_scale_ != 1.0f || last_x_offset_ != 0 || last_y_offset_ != 0) {
HRESULT hr = viewport->ZoomToRect(
static_cast<float>(0), static_cast<float>(0),
static_cast<float>(viewport_size_in_pixels_.width()),
static_cast<float>(viewport_size_in_pixels_.height()), FALSE);
if (!SUCCEEDED(hr)) {
return hr;
}
}
last_scale_ = 1.0f;
last_x_offset_ = 0.0f;
last_y_offset_ = 0.0f;
TransitionToState(GestureState::kNone);
return S_OK;
}
HRESULT DirectManipulationEventHandler::OnViewportUpdated(
IDirectManipulationViewport* viewport) {
// Nothing to do here.
return S_OK;
}
HRESULT DirectManipulationEventHandler::OnContentUpdated(
IDirectManipulationViewport* viewport,
IDirectManipulationContent* content) {
// MSDN never mention these params are nullable and we never saw they are null
// when testing.
DCHECK(viewport);
DCHECK(content);
// Windows should not call this when event_target_ is null since we do not
// pass the DM_POINTERHITTEST to DirectManipulation.
if (!event_target_)
return S_OK;
float xform[6];
HRESULT hr = content->GetContentTransform(xform, ARRAYSIZE(xform));
if (!SUCCEEDED(hr))
return hr;
float scale = xform[0];
int x_offset = xform[4] / device_scale_factor_;
int y_offset = xform[5] / device_scale_factor_;
// Ignore if Windows pass scale=0 to us.
if (scale == 0.0f) {
LOG(ERROR) << "Windows DirectManipulation API pass scale = 0.";
return hr;
}
// Ignore the scale factor change less than float point rounding error and
// scroll offset change less than 1.
// TODO(crbug.com/41156440) Because we don't fully support fractional scroll,
// pass float scroll offset feels steppy. eg. first x_offset is 0.1 ignored,
// but last_x_offset_ set to 0.1 second x_offset is 1 but x_offset -
// last_x_offset_ is 0.9 ignored.
if (FloatEquals(scale, last_scale_) && x_offset == last_x_offset_ &&
y_offset == last_y_offset_) {
return hr;
}
DCHECK_NE(last_scale_, 0.0f);
// DirectManipulation will send xy transform move to down-right which is noise
// when pinch zoom. We should consider the gesture either Scroll or Pinch at
// one sequence. But Pinch gesture may begin with some scroll transform since
// DirectManipulation recognition maybe wrong at start if the user pinch with
// slow motion. So we allow kScroll -> kPinch.
// Consider this is a Scroll when scale factor equals 1.0.
if (FloatEquals(scale, 1.0f)) {
if (gesture_state_ == GestureState::kNone)
TransitionToState(GestureState::kScroll);
} else {
// Pinch gesture may begin with some scroll events.
TransitionToState(GestureState::kPinch);
}
if (gesture_state_ == GestureState::kScroll) {
if (should_send_scroll_begin_) {
event_target_->ApplyPanGestureScrollBegin(x_offset - last_x_offset_,
y_offset - last_y_offset_);
should_send_scroll_begin_ = false;
} else {
event_target_->ApplyPanGestureScroll(x_offset - last_x_offset_,
y_offset - last_y_offset_);
}
} else if (gesture_state_ == GestureState::kFling) {
event_target_->ApplyPanGestureFling(x_offset - last_x_offset_,
y_offset - last_y_offset_);
} else {
event_target_->ApplyPinchZoomScale(scale / last_scale_);
}
last_scale_ = scale;
last_x_offset_ = x_offset;
last_y_offset_ = y_offset;
return hr;
}
HRESULT DirectManipulationEventHandler::OnInteraction(
IDirectManipulationViewport2* viewport,
DIRECTMANIPULATION_INTERACTION_TYPE interaction) {
if (!helper_)
return S_OK;
if (interaction == DIRECTMANIPULATION_INTERACTION_BEGIN)
helper_->AddAnimationObserver();
else if (interaction == DIRECTMANIPULATION_INTERACTION_END)
helper_->RemoveAnimationObserver();
return S_OK;
}
} // namespace content