| // Copyright 2019 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 "content/browser/renderer_host/direct_manipulation_event_handler_win.h" |
| |
| #include "base/logging.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( |
| DirectManipulationHelper* helper) |
| : helper_(helper) {} |
| |
| void DirectManipulationEventHandler::SetWindowEventTarget( |
| ui::WindowEventTarget* event_target) { |
| if (!event_target && LoggingEnabled()) { |
| DebugLogging("Event target is null.", S_OK); |
| if (event_target_) |
| DebugLogging("Previous event target is not null", S_OK); |
| else |
| DebugLogging("Previous event target is null", S_OK); |
| } |
| event_target_ = event_target; |
| } |
| |
| void DirectManipulationEventHandler::SetDeviceScaleFactor( |
| float device_scale_factor) { |
| device_scale_factor_ = device_scale_factor; |
| } |
| |
| DirectManipulationEventHandler::~DirectManipulationEventHandler() {} |
| |
| void DirectManipulationEventHandler::TransitionToState( |
| GestureState new_gesture_state) { |
| if (gesture_state_ == new_gesture_state) |
| return; |
| |
| if (LoggingEnabled()) { |
| std::string s = "TransitionToState " + |
| base::NumberToString(static_cast<int>(gesture_state_)) + |
| " -> " + |
| base::NumberToString(static_cast<int>(new_gesture_state)); |
| DebugLogging(s, S_OK); |
| } |
| |
| 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(); |
| 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(); |
| } |
| |
| // 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(); |
| } |
| } |
| |
| 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); |
| |
| if (LoggingEnabled()) { |
| std::string s = "ViewportStatusChanged " + base::NumberToString(previous) + |
| " -> " + base::NumberToString(current); |
| DebugLogging(s, S_OK); |
| } |
| |
| // 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; |
| |
| // Reset the viewport when we're idle, so the content transforms always start |
| // at identity. |
| // Every animation will receive 2 ready message, we should stop request |
| // compositor animation at the second ready. |
| first_ready_ = !first_ready_; |
| HRESULT hr = helper_->Reset(first_ready_); |
| last_scale_ = 1.0f; |
| last_x_offset_ = 0.0f; |
| last_y_offset_ = 0.0f; |
| |
| TransitionToState(GestureState::kNone); |
| |
| return hr; |
| } |
| |
| HRESULT DirectManipulationEventHandler::OnViewportUpdated( |
| IDirectManipulationViewport* viewport) { |
| if (LoggingEnabled()) |
| DebugLogging("OnViewportUpdated", S_OK); |
| // 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); |
| |
| if (LoggingEnabled()) |
| DebugLogging("OnContentUpdated", S_OK); |
| |
| // Windows should not call this when event_target_ is null since we do not |
| // pass the DM_POINTERHITTEST to DirectManipulation. |
| if (!event_target_) { |
| DebugLogging("OnContentUpdated event_target_ is null.", S_OK); |
| return S_OK; |
| } |
| |
| float xform[6]; |
| HRESULT hr = content->GetContentTransform(xform, ARRAYSIZE(xform)); |
| if (!SUCCEEDED(hr)) { |
| DebugLogging("DirectManipulationContent get transform failed.", 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(456622) 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_) { |
| if (LoggingEnabled()) { |
| std::string s = |
| "OnContentUpdated ignored. scale=" + base::NumberToString(scale) + |
| ", last_scale=" + base::NumberToString(last_scale_) + |
| ", x_offset=" + base::NumberToString(x_offset) + |
| ", last_x_offset=" + base::NumberToString(last_x_offset_) + |
| ", y_offset=" + base::NumberToString(y_offset) + |
| ", last_y_offset=" + base::NumberToString(last_y_offset_); |
| DebugLogging(s, S_OK); |
| } |
| 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; |
| } |
| |
| } // namespace content |