chromium/content/browser/renderer_host/direct_manipulation_event_handler_win.cc

// 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