// Copyright 2013 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 "ui/events/blink/input_handler_proxy.h"

#include <stddef.h>

#include <algorithm>

#include "base/auto_reset.h"
#include "base/command_line.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/metrics/histogram.h"
#include "base/single_thread_task_runner.h"
#include "base/thread_task_runner_handle.h"
#include "base/trace_event/trace_event.h"
#include "cc/input/main_thread_scrolling_reason.h"
#include "third_party/WebKit/public/web/WebInputEvent.h"
#include "ui/events/blink/input_handler_proxy_client.h"
#include "ui/events/blink/input_scroll_elasticity_controller.h"
#include "ui/events/latency_info.h"
#include "ui/gfx/geometry/point_conversions.h"

using blink::WebFloatPoint;
using blink::WebFloatSize;
using blink::WebGestureEvent;
using blink::WebInputEvent;
using blink::WebMouseEvent;
using blink::WebMouseWheelEvent;
using blink::WebPoint;
using blink::WebTouchEvent;
using blink::WebTouchPoint;

namespace {

const int32_t kEventDispositionUndefined = -1;

// Maximum time between a fling event's timestamp and the first |Animate| call
// for the fling curve to use the fling timestamp as the initial animation time.
// Two frames allows a minor delay between event creation and the first animate.
const double kMaxSecondsFromFlingTimestampToFirstAnimate = 2. / 60.;

// Threshold for determining whether a fling scroll delta should have caused the
// client to scroll.
const float kScrollEpsilon = 0.1f;

// Minimum fling velocity required for the active fling and new fling for the
// two to accumulate.
const double kMinBoostFlingSpeedSquare = 350. * 350.;

// Minimum velocity for the active touch scroll to preserve (boost) an active
// fling for which cancellation has been deferred.
const double kMinBoostTouchScrollSpeedSquare = 150 * 150.;

// Timeout window after which the active fling will be cancelled if no animation
// ticks, scrolls or flings of sufficient velocity relative to the current fling
// are received. The default value on Android native views is 40ms, but we use a
// slightly increased value to accomodate small IPC message delays.
const double kFlingBoostTimeoutDelaySeconds = 0.05;

gfx::Vector2dF ToClientScrollIncrement(const WebFloatSize& increment) {
  return gfx::Vector2dF(-increment.width, -increment.height);
}

double InSecondsF(const base::TimeTicks& time) {
  return (time - base::TimeTicks()).InSecondsF();
}

bool ShouldSuppressScrollForFlingBoosting(
    const gfx::Vector2dF& current_fling_velocity,
    const WebGestureEvent& scroll_update_event,
    double time_since_last_boost_event,
    double time_since_last_fling_animate) {
  DCHECK_EQ(WebInputEvent::GestureScrollUpdate, scroll_update_event.type);

  gfx::Vector2dF dx(scroll_update_event.data.scrollUpdate.deltaX,
                    scroll_update_event.data.scrollUpdate.deltaY);
  if (gfx::DotProduct(current_fling_velocity, dx) <= 0)
    return false;

  if (time_since_last_fling_animate > kFlingBoostTimeoutDelaySeconds)
    return false;

  if (time_since_last_boost_event < 0.001)
    return true;

  // TODO(jdduke): Use |scroll_update_event.data.scrollUpdate.velocity{X,Y}|.
  // The scroll must be of sufficient velocity to maintain the active fling.
  const gfx::Vector2dF scroll_velocity =
      gfx::ScaleVector2d(dx, 1. / time_since_last_boost_event);
  if (scroll_velocity.LengthSquared() < kMinBoostTouchScrollSpeedSquare)
    return false;

  return true;
}

bool ShouldBoostFling(const gfx::Vector2dF& current_fling_velocity,
                      const WebGestureEvent& fling_start_event) {
  DCHECK_EQ(WebInputEvent::GestureFlingStart, fling_start_event.type);

  gfx::Vector2dF new_fling_velocity(
      fling_start_event.data.flingStart.velocityX,
      fling_start_event.data.flingStart.velocityY);

  if (gfx::DotProduct(current_fling_velocity, new_fling_velocity) <= 0)
    return false;

  if (current_fling_velocity.LengthSquared() < kMinBoostFlingSpeedSquare)
    return false;

  if (new_fling_velocity.LengthSquared() < kMinBoostFlingSpeedSquare)
    return false;

  return true;
}

WebGestureEvent ObtainGestureScrollBegin(const WebGestureEvent& event) {
  WebGestureEvent scroll_begin_event = event;
  scroll_begin_event.type = WebInputEvent::GestureScrollBegin;
  scroll_begin_event.data.scrollBegin.deltaXHint = 0;
  scroll_begin_event.data.scrollBegin.deltaYHint = 0;
  return scroll_begin_event;
}

cc::ScrollState CreateScrollStateForGesture(const WebGestureEvent& event) {
  cc::ScrollStateData scroll_state_data;
  switch (event.type) {
    case WebInputEvent::GestureScrollBegin:
      scroll_state_data.start_position_x = event.x;
      scroll_state_data.start_position_y = event.y;
      scroll_state_data.is_beginning = true;
      break;
    case WebInputEvent::GestureFlingStart:
      scroll_state_data.velocity_x = event.data.flingStart.velocityX;
      scroll_state_data.velocity_y = event.data.flingStart.velocityY;
      scroll_state_data.is_in_inertial_phase = true;
      break;
    case WebInputEvent::GestureScrollUpdate:
      scroll_state_data.delta_x = -event.data.scrollUpdate.deltaX;
      scroll_state_data.delta_y = -event.data.scrollUpdate.deltaY;
      scroll_state_data.velocity_x = event.data.scrollUpdate.velocityX;
      scroll_state_data.velocity_y = event.data.scrollUpdate.velocityY;
      scroll_state_data.is_in_inertial_phase = event.data.scrollUpdate.inertial;
      break;
    case WebInputEvent::GestureScrollEnd:
    case WebInputEvent::GestureFlingCancel:
      scroll_state_data.is_ending = true;
      break;
    default:
      NOTREACHED();
      break;
  }
  return cc::ScrollState(scroll_state_data);
}

void ReportInputEventLatencyUma(const WebInputEvent& event,
                                const ui::LatencyInfo& latency_info) {
  if (!(event.type == WebInputEvent::GestureScrollBegin ||
        event.type == WebInputEvent::GestureScrollUpdate ||
        event.type == WebInputEvent::GesturePinchBegin ||
        event.type == WebInputEvent::GesturePinchUpdate ||
        event.type == WebInputEvent::GestureFlingStart)) {
    return;
  }

  ui::LatencyInfo::LatencyMap::const_iterator it =
      latency_info.latency_components().find(std::make_pair(
          ui::INPUT_EVENT_LATENCY_ORIGINAL_COMPONENT, 0));

  if (it == latency_info.latency_components().end())
    return;

  base::TimeDelta delta = base::TimeTicks::Now() - it->second.event_time;
  for (size_t i = 0; i < it->second.event_count; ++i) {
    switch (event.type) {
      case blink::WebInputEvent::GestureScrollBegin:
        UMA_HISTOGRAM_CUSTOM_COUNTS(
            "Event.Latency.RendererImpl.GestureScrollBegin",
            delta.InMicroseconds(), 1, 1000000, 100);
        break;
      case blink::WebInputEvent::GestureScrollUpdate:
        UMA_HISTOGRAM_CUSTOM_COUNTS(
            // So named for historical reasons.
            "Event.Latency.RendererImpl.GestureScroll2",
            delta.InMicroseconds(), 1, 1000000, 100);
        break;
      case blink::WebInputEvent::GesturePinchBegin:
        UMA_HISTOGRAM_CUSTOM_COUNTS(
            "Event.Latency.RendererImpl.GesturePinchBegin",
            delta.InMicroseconds(), 1, 1000000, 100);
        break;
      case blink::WebInputEvent::GesturePinchUpdate:
        UMA_HISTOGRAM_CUSTOM_COUNTS(
            "Event.Latency.RendererImpl.GesturePinchUpdate",
            delta.InMicroseconds(), 1, 1000000, 100);
        break;
      case blink::WebInputEvent::GestureFlingStart:
        UMA_HISTOGRAM_CUSTOM_COUNTS(
            "Event.Latency.RendererImpl.GestureFlingStart",
            delta.InMicroseconds(), 1, 1000000, 100);
        break;
      default:
        NOTREACHED();
        break;
    }
  }
}

cc::InputHandler::ScrollInputType GestureScrollInputType(
    blink::WebGestureDevice device) {
  return device == blink::WebGestureDeviceTouchpad
             ? cc::InputHandler::WHEEL
             : cc::InputHandler::TOUCHSCREEN;
}

}  // namespace

namespace ui {

InputHandlerProxy::InputHandlerProxy(cc::InputHandler* input_handler,
                                     InputHandlerProxyClient* client)
    : client_(client),
      input_handler_(input_handler),
      deferred_fling_cancel_time_seconds_(0),
      synchronous_input_handler_(nullptr),
      allow_root_animate_(true),
#ifndef NDEBUG
      expect_scroll_update_end_(false),
#endif
      gesture_scroll_on_impl_thread_(false),
      gesture_pinch_on_impl_thread_(false),
      fling_may_be_active_on_main_thread_(false),
      disallow_horizontal_fling_scroll_(false),
      disallow_vertical_fling_scroll_(false),
      has_fling_animation_started_(false),
      smooth_scroll_enabled_(false),
      uma_latency_reporting_enabled_(base::TimeTicks::IsHighResolution()),
      use_gesture_events_for_mouse_wheel_(true),
      touch_start_result_(kEventDispositionUndefined) {
  DCHECK(client);
  input_handler_->BindToClient(this);
  cc::ScrollElasticityHelper* scroll_elasticity_helper =
      input_handler_->CreateScrollElasticityHelper();
  if (scroll_elasticity_helper) {
    scroll_elasticity_controller_.reset(
        new InputScrollElasticityController(scroll_elasticity_helper));
  }
}

InputHandlerProxy::~InputHandlerProxy() {}

void InputHandlerProxy::WillShutdown() {
  scroll_elasticity_controller_.reset();
  input_handler_ = NULL;
  client_->WillShutdown();
}

InputHandlerProxy::EventDisposition
InputHandlerProxy::HandleInputEventWithLatencyInfo(
    const WebInputEvent& event,
    ui::LatencyInfo* latency_info) {
  DCHECK(input_handler_);

  if (uma_latency_reporting_enabled_)
    ReportInputEventLatencyUma(event, *latency_info);

  TRACE_EVENT_WITH_FLOW1("input,benchmark",
                         "LatencyInfo.Flow",
                         TRACE_ID_DONT_MANGLE(latency_info->trace_id()),
                         TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT,
                         "step", "HandleInputEventImpl");

  scoped_ptr<cc::SwapPromiseMonitor> latency_info_swap_promise_monitor =
      input_handler_->CreateLatencyInfoSwapPromiseMonitor(latency_info);
  InputHandlerProxy::EventDisposition disposition = HandleInputEvent(event);
  return disposition;
}

InputHandlerProxy::EventDisposition InputHandlerProxy::HandleInputEvent(
    const WebInputEvent& event) {
  DCHECK(input_handler_);

  if (FilterInputEventForFlingBoosting(event))
    return DID_HANDLE;

  switch (event.type) {
    case WebInputEvent::MouseWheel:
      return HandleMouseWheel(static_cast<const WebMouseWheelEvent&>(event));

    case WebInputEvent::GestureScrollBegin:
      return HandleGestureScrollBegin(
          static_cast<const WebGestureEvent&>(event));

    case WebInputEvent::GestureScrollUpdate:
      return HandleGestureScrollUpdate(
          static_cast<const WebGestureEvent&>(event));

    case WebInputEvent::GestureScrollEnd:
      return HandleGestureScrollEnd(static_cast<const WebGestureEvent&>(event));

    case WebInputEvent::GesturePinchBegin: {
      DCHECK(!gesture_pinch_on_impl_thread_);
      const WebGestureEvent& gesture_event =
          static_cast<const WebGestureEvent&>(event);
      if (gesture_event.sourceDevice == blink::WebGestureDeviceTouchpad &&
          input_handler_->GetEventListenerProperties(
              cc::EventListenerClass::kMouseWheel) !=
              cc::EventListenerProperties::kNone) {
        return DID_NOT_HANDLE;
      } else {
        input_handler_->PinchGestureBegin();
        gesture_pinch_on_impl_thread_ = true;
        return DID_HANDLE;
      }
    }

    case WebInputEvent::GesturePinchEnd:
      if (gesture_pinch_on_impl_thread_) {
        gesture_pinch_on_impl_thread_ = false;
        input_handler_->PinchGestureEnd();
        return DID_HANDLE;
      } else {
        return DID_NOT_HANDLE;
      }

    case WebInputEvent::GesturePinchUpdate: {
      if (gesture_pinch_on_impl_thread_) {
        const WebGestureEvent& gesture_event =
            static_cast<const WebGestureEvent&>(event);
        if (gesture_event.data.pinchUpdate.zoomDisabled)
            return DROP_EVENT;
        input_handler_->PinchGestureUpdate(
            gesture_event.data.pinchUpdate.scale,
            gfx::Point(gesture_event.x, gesture_event.y));
        return DID_HANDLE;
      } else {
        return DID_NOT_HANDLE;
      }
    }

    case WebInputEvent::GestureFlingStart:
      return HandleGestureFlingStart(
          *static_cast<const WebGestureEvent*>(&event));

    case WebInputEvent::GestureFlingCancel:
      if (CancelCurrentFling())
        return DID_HANDLE;
      else if (!fling_may_be_active_on_main_thread_)
        return DROP_EVENT;
      return DID_NOT_HANDLE;

    case WebInputEvent::TouchStart:
      return HandleTouchStart(static_cast<const WebTouchEvent&>(event));

    case WebInputEvent::TouchMove:
      return HandleTouchMove(static_cast<const WebTouchEvent&>(event));

    case WebInputEvent::TouchEnd:
      return HandleTouchEnd(static_cast<const WebTouchEvent&>(event));

    case WebInputEvent::MouseMove: {
      const WebMouseEvent& mouse_event =
          static_cast<const WebMouseEvent&>(event);
      // TODO(tony): Ignore when mouse buttons are down?
      // TODO(davemoore): This should never happen, but bug #326635 showed some
      // surprising crashes.
      CHECK(input_handler_);
      input_handler_->MouseMoveAt(gfx::Point(mouse_event.x, mouse_event.y));
      return DID_NOT_HANDLE;
    }

    default:
      if (WebInputEvent::isKeyboardEventType(event.type)) {
        // Only call |CancelCurrentFling()| if a fling was active, as it will
        // otherwise disrupt an in-progress touch scroll.
        if (fling_curve_)
          CancelCurrentFling();
      }
      break;
  }

  return DID_NOT_HANDLE;
}

void InputHandlerProxy::RecordMainThreadScrollingReasons(
    WebInputEvent::Type type,
    uint32_t reasons) {
  static const char* kGestureHistogramName =
      "Renderer4.MainThreadGestureScrollReason";
  static const char* kWheelHistogramName =
      "Renderer4.MainThreadWheelScrollReason";

  DCHECK(type == WebInputEvent::GestureScrollBegin ||
         type == WebInputEvent::MouseWheel);

  if (type != WebInputEvent::GestureScrollBegin &&
      type != WebInputEvent::MouseWheel) {
    return;
  }

  if (reasons == cc::MainThreadScrollingReason::kNotScrollingOnMain) {
    if (type == WebInputEvent::GestureScrollBegin) {
      UMA_HISTOGRAM_ENUMERATION(
          kGestureHistogramName,
          cc::MainThreadScrollingReason::kNotScrollingOnMain,
          cc::MainThreadScrollingReason::kMainThreadScrollingReasonCount);
    } else {
      UMA_HISTOGRAM_ENUMERATION(
          kWheelHistogramName,
          cc::MainThreadScrollingReason::kNotScrollingOnMain,
          cc::MainThreadScrollingReason::kMainThreadScrollingReasonCount);
    }
  }

  for (uint32_t i = 0;
       i < cc::MainThreadScrollingReason::kMainThreadScrollingReasonCount - 1;
       ++i) {
    unsigned val = 1 << i;
    if (reasons & val) {
      if (type == WebInputEvent::GestureScrollBegin) {
        UMA_HISTOGRAM_ENUMERATION(
            kGestureHistogramName, i + 1,
            cc::MainThreadScrollingReason::kMainThreadScrollingReasonCount);
      } else {
        UMA_HISTOGRAM_ENUMERATION(
            kWheelHistogramName, i + 1,
            cc::MainThreadScrollingReason::kMainThreadScrollingReasonCount);
      }
    }
  }
}

bool InputHandlerProxy::ShouldAnimate(bool has_precise_scroll_deltas) const {
#if defined(OS_MACOSX)
  // Mac does not smooth scroll wheel events (crbug.com/574283).
  return false;
#else
  return smooth_scroll_enabled_ && !has_precise_scroll_deltas;
#endif
}

InputHandlerProxy::EventDisposition InputHandlerProxy::HandleMouseWheel(
    const WebMouseWheelEvent& wheel_event) {
  // Only call |CancelCurrentFling()| if a fling was active, as it will
  // otherwise disrupt an in-progress touch scroll.
  if (!wheel_event.hasPreciseScrollingDeltas && fling_curve_)
    CancelCurrentFling();

  if (use_gesture_events_for_mouse_wheel_) {
    cc::EventListenerProperties properties =
        input_handler_->GetEventListenerProperties(
            cc::EventListenerClass::kMouseWheel);
    switch (properties) {
      case cc::EventListenerProperties::kPassive:
        return DID_HANDLE_NON_BLOCKING;
      case cc::EventListenerProperties::kBlockingAndPassive:
      case cc::EventListenerProperties::kBlocking:
        return DID_NOT_HANDLE;
      case cc::EventListenerProperties::kNone:
        return DROP_EVENT;
      default:
        NOTREACHED();
        return DROP_EVENT;
    }
  }
  return ScrollByMouseWheel(wheel_event);
}

InputHandlerProxy::EventDisposition InputHandlerProxy::ScrollByMouseWheel(
    const WebMouseWheelEvent& wheel_event) {
  InputHandlerProxy::EventDisposition result = DID_NOT_HANDLE;
  cc::InputHandlerScrollResult scroll_result;

  // TODO(ccameron): The rail information should be pushed down into
  // InputHandler.
  gfx::Vector2dF scroll_delta(
      wheel_event.railsMode != WebInputEvent::RailsModeVertical
          ? -wheel_event.deltaX
          : 0,
      wheel_event.railsMode != WebInputEvent::RailsModeHorizontal
          ? -wheel_event.deltaY
          : 0);

  if (wheel_event.scrollByPage) {
    // TODO(jamesr): We don't properly handle scroll by page in the compositor
    // thread, so punt it to the main thread. http://crbug.com/236639
    result = DID_NOT_HANDLE;
    RecordMainThreadScrollingReasons(
        wheel_event.type, cc::MainThreadScrollingReason::kPageBasedScrolling);

  } else if (!wheel_event.canScroll) {
    // Wheel events with |canScroll| == false will not trigger scrolling,
    // only event handlers.  Forward to the main thread.
    result = DID_NOT_HANDLE;
  } else if (ShouldAnimate(wheel_event.hasPreciseScrollingDeltas)) {
    cc::InputHandler::ScrollStatus scroll_status =
        input_handler_->ScrollAnimated(gfx::Point(wheel_event.x, wheel_event.y),
                                       scroll_delta);

    RecordMainThreadScrollingReasons(
        wheel_event.type, scroll_status.main_thread_scrolling_reasons);

    switch (scroll_status.thread) {
      case cc::InputHandler::SCROLL_ON_IMPL_THREAD:
        result = DID_HANDLE;
        break;
      case cc::InputHandler::SCROLL_IGNORED:
        result = DROP_EVENT;
        break;
      default:
        result = DID_NOT_HANDLE;
        break;
    }
  } else {
    cc::ScrollStateData scroll_state_begin_data;
    scroll_state_begin_data.start_position_x = wheel_event.x;
    scroll_state_begin_data.start_position_y = wheel_event.y;
    scroll_state_begin_data.is_beginning = true;
    cc::ScrollState scroll_state_begin(scroll_state_begin_data);
    cc::InputHandler::ScrollStatus scroll_status = input_handler_->ScrollBegin(
        &scroll_state_begin, cc::InputHandler::WHEEL);

    RecordMainThreadScrollingReasons(
        wheel_event.type, scroll_status.main_thread_scrolling_reasons);

    switch (scroll_status.thread) {
      case cc::InputHandler::SCROLL_ON_IMPL_THREAD: {
        TRACE_EVENT_INSTANT2("input",
                             "InputHandlerProxy::handle_input wheel scroll",
                             TRACE_EVENT_SCOPE_THREAD, "deltaX",
                             scroll_delta.x(), "deltaY", scroll_delta.y());

        cc::ScrollStateData scroll_state_update_data;
        scroll_state_update_data.delta_x = scroll_delta.x();
        scroll_state_update_data.delta_y = scroll_delta.y();
        scroll_state_update_data.start_position_x = wheel_event.x;
        scroll_state_update_data.start_position_y = wheel_event.y;
        cc::ScrollState scroll_state_update(scroll_state_update_data);

        scroll_result = input_handler_->ScrollBy(&scroll_state_update);
        HandleOverscroll(gfx::Point(wheel_event.x, wheel_event.y),
                         scroll_result);

        cc::ScrollStateData scroll_state_end_data;
        scroll_state_end_data.is_ending = true;
        cc::ScrollState scroll_state_end(scroll_state_end_data);
        input_handler_->ScrollEnd(&scroll_state_end);

        result = scroll_result.did_scroll ? DID_HANDLE : DROP_EVENT;
        break;
      }
      case cc::InputHandler::SCROLL_IGNORED:
        // TODO(jamesr): This should be DROP_EVENT, but in cases where we fail
        // to properly sync scrollability it's safer to send the event to the
        // main thread. Change back to DROP_EVENT once we have synchronization
        // bugs sorted out.
        result = DID_NOT_HANDLE;
        break;
      case cc::InputHandler::SCROLL_UNKNOWN:
      case cc::InputHandler::SCROLL_ON_MAIN_THREAD:
        result = DID_NOT_HANDLE;
        break;
    }
  }

  // Send the event and its disposition to the elasticity controller to update
  // the over-scroll animation. If the event is to be handled on the main
  // thread, the event and its disposition will be sent to the elasticity
  // controller after being handled on the main thread.
  if (scroll_elasticity_controller_ && result != DID_NOT_HANDLE) {
    // Note that the call to the elasticity controller is made asynchronously,
    // to minimize divergence between main thread and impl thread event
    // handling paths.
    base::ThreadTaskRunnerHandle::Get()->PostTask(
        FROM_HERE,
        base::Bind(&InputScrollElasticityController::ObserveWheelEventAndResult,
                   scroll_elasticity_controller_->GetWeakPtr(), wheel_event,
                   scroll_result));
  }
  return result;
}

InputHandlerProxy::EventDisposition InputHandlerProxy::HandleGestureScrollBegin(
    const WebGestureEvent& gesture_event) {
  if (gesture_scroll_on_impl_thread_)
    CancelCurrentFling();

#ifndef NDEBUG
  DCHECK(!expect_scroll_update_end_);
  expect_scroll_update_end_ = true;
#endif
  cc::ScrollState scroll_state = CreateScrollStateForGesture(gesture_event);
  cc::InputHandler::ScrollStatus scroll_status;
  if (gesture_event.data.scrollBegin.deltaHintUnits ==
      blink::WebGestureEvent::ScrollUnits::Page) {
    scroll_status.thread = cc::InputHandler::SCROLL_ON_MAIN_THREAD;
    scroll_status.main_thread_scrolling_reasons =
        cc::MainThreadScrollingReason::kContinuingMainThreadScroll;
  } else if (gesture_event.data.scrollBegin.targetViewport) {
    scroll_status = input_handler_->RootScrollBegin(
        &scroll_state, GestureScrollInputType(gesture_event.sourceDevice));
  } else if (ShouldAnimate(gesture_event.data.scrollBegin.deltaHintUnits !=
                           blink::WebGestureEvent::ScrollUnits::Pixels)) {
    gfx::Point scroll_point(gesture_event.x, gesture_event.y);
    scroll_status = input_handler_->ScrollAnimatedBegin(scroll_point);
  } else {
    scroll_status = input_handler_->ScrollBegin(
        &scroll_state, GestureScrollInputType(gesture_event.sourceDevice));
  }
  UMA_HISTOGRAM_ENUMERATION("Renderer4.CompositorScrollHitTestResult",
                            scroll_status.thread,
                            cc::InputHandler::LAST_SCROLL_STATUS + 1);

  RecordMainThreadScrollingReasons(gesture_event.type,
                                   scroll_status.main_thread_scrolling_reasons);

  InputHandlerProxy::EventDisposition result = DID_NOT_HANDLE;
  switch (scroll_status.thread) {
    case cc::InputHandler::SCROLL_ON_IMPL_THREAD:
      TRACE_EVENT_INSTANT0("input",
                           "InputHandlerProxy::handle_input gesture scroll",
                           TRACE_EVENT_SCOPE_THREAD);
      gesture_scroll_on_impl_thread_ = true;
      result = DID_HANDLE;
      break;
    case cc::InputHandler::SCROLL_UNKNOWN:
    case cc::InputHandler::SCROLL_ON_MAIN_THREAD:
      result = DID_NOT_HANDLE;
      break;
    case cc::InputHandler::SCROLL_IGNORED:
      result = DROP_EVENT;
      break;
  }
  if (scroll_elasticity_controller_ && result != DID_NOT_HANDLE)
    HandleScrollElasticityOverscroll(gesture_event,
                                     cc::InputHandlerScrollResult());

  return result;
}

InputHandlerProxy::EventDisposition
InputHandlerProxy::HandleGestureScrollUpdate(
  const WebGestureEvent& gesture_event) {
#ifndef NDEBUG
  DCHECK(expect_scroll_update_end_);
#endif
  if (!gesture_scroll_on_impl_thread_ && !gesture_pinch_on_impl_thread_)
    return DID_NOT_HANDLE;

  cc::ScrollState scroll_state = CreateScrollStateForGesture(gesture_event);
  gfx::Point scroll_point(gesture_event.x, gesture_event.y);
  gfx::Vector2dF scroll_delta(-gesture_event.data.scrollUpdate.deltaX,
                              -gesture_event.data.scrollUpdate.deltaY);

  if (ShouldAnimate(gesture_event.data.scrollUpdate.deltaUnits !=
                    blink::WebGestureEvent::ScrollUnits::Pixels)) {
    switch (input_handler_->ScrollAnimated(scroll_point, scroll_delta).thread) {
      case cc::InputHandler::SCROLL_ON_IMPL_THREAD:
        return DID_HANDLE;
      case cc::InputHandler::SCROLL_IGNORED:
        return DROP_EVENT;
      default:
        return DID_NOT_HANDLE;
    }
  }
  cc::InputHandlerScrollResult scroll_result =
      input_handler_->ScrollBy(&scroll_state);
  HandleOverscroll(scroll_point, scroll_result);

  if (scroll_elasticity_controller_)
    HandleScrollElasticityOverscroll(gesture_event, scroll_result);

  return scroll_result.did_scroll ? DID_HANDLE : DROP_EVENT;
}

InputHandlerProxy::EventDisposition InputHandlerProxy::HandleGestureScrollEnd(
  const WebGestureEvent& gesture_event) {
#ifndef NDEBUG
  DCHECK(expect_scroll_update_end_);
  expect_scroll_update_end_ = false;
#endif
  if (ShouldAnimate(gesture_event.data.scrollEnd.deltaUnits !=
                    blink::WebGestureEvent::ScrollUnits::Pixels)) {
    // Do nothing if the scroll is being animated; the scroll animation will
    // generate the ScrollEnd when it is done.
  } else {
    cc::ScrollState scroll_state = CreateScrollStateForGesture(gesture_event);
    input_handler_->ScrollEnd(&scroll_state);
  }
  if (!gesture_scroll_on_impl_thread_)
    return DID_NOT_HANDLE;

  if (scroll_elasticity_controller_)
    HandleScrollElasticityOverscroll(gesture_event,
                                     cc::InputHandlerScrollResult());

  gesture_scroll_on_impl_thread_ = false;
  return DID_HANDLE;
}

InputHandlerProxy::EventDisposition InputHandlerProxy::HandleGestureFlingStart(
    const WebGestureEvent& gesture_event) {
  cc::ScrollState scroll_state = CreateScrollStateForGesture(gesture_event);
  cc::InputHandler::ScrollStatus scroll_status;
  scroll_status.main_thread_scrolling_reasons =
      cc::MainThreadScrollingReason::kNotScrollingOnMain;
  switch (gesture_event.sourceDevice) {
  case blink::WebGestureDeviceTouchpad:
    if (gesture_event.data.flingStart.targetViewport) {
      scroll_status = input_handler_->RootScrollBegin(
          &scroll_state, cc::InputHandler::NON_BUBBLING_GESTURE);
    } else {
      scroll_status = input_handler_->ScrollBegin(
          &scroll_state, cc::InputHandler::NON_BUBBLING_GESTURE);
    }
    break;
  case blink::WebGestureDeviceTouchscreen:
    if (!gesture_scroll_on_impl_thread_) {
      scroll_status.thread = cc::InputHandler::SCROLL_ON_MAIN_THREAD;
      scroll_status.main_thread_scrolling_reasons =
          cc::MainThreadScrollingReason::kContinuingMainThreadScroll;
    } else {
      scroll_status = input_handler_->FlingScrollBegin();
    }
    break;
  case blink::WebGestureDeviceUninitialized:
    NOTREACHED();
    return DID_NOT_HANDLE;
  }

#ifndef NDEBUG
  expect_scroll_update_end_ = false;
#endif

  switch (scroll_status.thread) {
    case cc::InputHandler::SCROLL_ON_IMPL_THREAD: {
      if (gesture_event.sourceDevice == blink::WebGestureDeviceTouchpad) {
        scroll_state.set_is_ending(true);
        input_handler_->ScrollEnd(&scroll_state);
      }

      const float vx = gesture_event.data.flingStart.velocityX;
      const float vy = gesture_event.data.flingStart.velocityY;
      current_fling_velocity_ = gfx::Vector2dF(vx, vy);
      DCHECK(!current_fling_velocity_.IsZero());
      fling_curve_.reset(client_->CreateFlingAnimationCurve(
          gesture_event.sourceDevice,
          WebFloatPoint(vx, vy),
          blink::WebSize()));
      disallow_horizontal_fling_scroll_ = !vx;
      disallow_vertical_fling_scroll_ = !vy;
      TRACE_EVENT_ASYNC_BEGIN2("input,benchmark",
                               "InputHandlerProxy::HandleGestureFling::started",
                               this, "vx", vx, "vy", vy);
      // Note that the timestamp will only be used to kickstart the animation if
      // its sufficiently close to the timestamp of the first call |Animate()|.
      has_fling_animation_started_ = false;
      fling_parameters_.startTime = gesture_event.timeStampSeconds;
      fling_parameters_.delta = WebFloatPoint(vx, vy);
      fling_parameters_.point = WebPoint(gesture_event.x, gesture_event.y);
      fling_parameters_.globalPoint =
          WebPoint(gesture_event.globalX, gesture_event.globalY);
      fling_parameters_.modifiers = gesture_event.modifiers;
      fling_parameters_.sourceDevice = gesture_event.sourceDevice;
      RequestAnimation();
      return DID_HANDLE;
    }
    case cc::InputHandler::SCROLL_UNKNOWN:
    case cc::InputHandler::SCROLL_ON_MAIN_THREAD: {
      TRACE_EVENT_INSTANT0("input",
                           "InputHandlerProxy::HandleGestureFling::"
                           "scroll_on_main_thread",
                           TRACE_EVENT_SCOPE_THREAD);
      gesture_scroll_on_impl_thread_ = false;
      fling_may_be_active_on_main_thread_ = true;
      return DID_NOT_HANDLE;
    }
    case cc::InputHandler::SCROLL_IGNORED: {
      TRACE_EVENT_INSTANT0(
          "input",
          "InputHandlerProxy::HandleGestureFling::ignored",
          TRACE_EVENT_SCOPE_THREAD);
      gesture_scroll_on_impl_thread_ = false;
      if (gesture_event.sourceDevice == blink::WebGestureDeviceTouchpad) {
        // We still pass the curve to the main thread if there's nothing
        // scrollable, in case something
        // registers a handler before the curve is over.
        return DID_NOT_HANDLE;
      }
      return DROP_EVENT;
    }
  }
  return DID_NOT_HANDLE;
}

InputHandlerProxy::EventDisposition InputHandlerProxy::HandleTouchStart(
    const blink::WebTouchEvent& touch_event) {
  EventDisposition result = DROP_EVENT;
  for (size_t i = 0; i < touch_event.touchesLength; ++i) {
    if (touch_event.touches[i].state != WebTouchPoint::StatePressed)
      continue;
    if (input_handler_->DoTouchEventsBlockScrollAt(
            gfx::Point(touch_event.touches[i].position.x,
                       touch_event.touches[i].position.y))) {
      result = DID_NOT_HANDLE;
      break;
    }
  }

  // If |result| is DROP_EVENT it wasn't processed above.
  if (result == DROP_EVENT) {
    switch (input_handler_->GetEventListenerProperties(
        cc::EventListenerClass::kTouch)) {
      case cc::EventListenerProperties::kPassive:
        result = DID_HANDLE_NON_BLOCKING;
        break;
      case cc::EventListenerProperties::kBlocking:
        // The touch area rects above already have checked whether it hits
        // a blocking region. Since it does not the event can be dropped.
        result = DROP_EVENT;
        break;
      case cc::EventListenerProperties::kBlockingAndPassive:
        // There is at least one passive listener that needs to possibly
        // be notified so it can't be dropped.
        result = DID_HANDLE_NON_BLOCKING;
        break;
      case cc::EventListenerProperties::kNone:
        result = DROP_EVENT;
        break;
      default:
        NOTREACHED();
        result = DROP_EVENT;
        break;
    }
  }

  // Merge |touch_start_result_| and |result| so the result has the highest
  // priority value according to the sequence; (DROP_EVENT,
  // DID_HANDLE_NON_BLOCKING, DID_NOT_HANDLE).
  if (touch_start_result_ == kEventDispositionUndefined ||
      touch_start_result_ == DROP_EVENT || result == DID_NOT_HANDLE)
    touch_start_result_ = result;

  return result;
}

InputHandlerProxy::EventDisposition InputHandlerProxy::HandleTouchMove(
    const blink::WebTouchEvent& touch_event) {
  if (touch_start_result_ != kEventDispositionUndefined)
    return static_cast<EventDisposition>(touch_start_result_);
  return DID_NOT_HANDLE;
}

InputHandlerProxy::EventDisposition InputHandlerProxy::HandleTouchEnd(
    const blink::WebTouchEvent& touch_event) {
  if (touch_event.touchesLength == 1)
    touch_start_result_ = kEventDispositionUndefined;
  return DID_NOT_HANDLE;
}

bool InputHandlerProxy::FilterInputEventForFlingBoosting(
    const WebInputEvent& event) {
  if (!WebInputEvent::isGestureEventType(event.type))
    return false;

  if (!fling_curve_) {
    DCHECK(!deferred_fling_cancel_time_seconds_);
    return false;
  }

  const WebGestureEvent& gesture_event =
      static_cast<const WebGestureEvent&>(event);
  if (gesture_event.type == WebInputEvent::GestureFlingCancel) {
    if (gesture_event.data.flingCancel.preventBoosting)
      return false;

    if (current_fling_velocity_.LengthSquared() < kMinBoostFlingSpeedSquare)
      return false;

    TRACE_EVENT_INSTANT0("input",
                         "InputHandlerProxy::FlingBoostStart",
                         TRACE_EVENT_SCOPE_THREAD);
    deferred_fling_cancel_time_seconds_ =
        event.timeStampSeconds + kFlingBoostTimeoutDelaySeconds;
    return true;
  }

  // A fling is either inactive or is "free spinning", i.e., has yet to be
  // interrupted by a touch gesture, in which case there is nothing to filter.
  if (!deferred_fling_cancel_time_seconds_)
    return false;

  // Gestures from a different source should immediately interrupt the fling.
  if (gesture_event.sourceDevice != fling_parameters_.sourceDevice) {
    CancelCurrentFling();
    return false;
  }

  switch (gesture_event.type) {
    case WebInputEvent::GestureTapCancel:
    case WebInputEvent::GestureTapDown:
      return false;

    case WebInputEvent::GestureScrollBegin:
      if (!input_handler_->IsCurrentlyScrollingLayerAt(
              gfx::Point(gesture_event.x, gesture_event.y),
              fling_parameters_.sourceDevice == blink::WebGestureDeviceTouchpad
                  ? cc::InputHandler::NON_BUBBLING_GESTURE
                  : cc::InputHandler::TOUCHSCREEN)) {
        CancelCurrentFling();
        return false;
      }

      // TODO(jdduke): Use |gesture_event.data.scrollBegin.delta{X,Y}Hint| to
      // determine if the ScrollBegin should immediately cancel the fling.
      ExtendBoostedFlingTimeout(gesture_event);
      return true;

    case WebInputEvent::GestureScrollUpdate: {
      const double time_since_last_boost_event =
          event.timeStampSeconds - last_fling_boost_event_.timeStampSeconds;
      const double time_since_last_fling_animate = std::max(
          0.0, event.timeStampSeconds - InSecondsF(last_fling_animate_time_));
      if (ShouldSuppressScrollForFlingBoosting(current_fling_velocity_,
                                               gesture_event,
                                               time_since_last_boost_event,
                                               time_since_last_fling_animate)) {
        ExtendBoostedFlingTimeout(gesture_event);
        return true;
      }

      CancelCurrentFling();
      return false;
    }

    case WebInputEvent::GestureScrollEnd:
      // Clear the last fling boost event *prior* to fling cancellation,
      // preventing insertion of a synthetic GestureScrollBegin.
      last_fling_boost_event_ = WebGestureEvent();
      CancelCurrentFling();
      return true;

    case WebInputEvent::GestureFlingStart: {
      DCHECK_EQ(fling_parameters_.sourceDevice, gesture_event.sourceDevice);

      bool fling_boosted =
          fling_parameters_.modifiers == gesture_event.modifiers &&
          ShouldBoostFling(current_fling_velocity_, gesture_event);

      gfx::Vector2dF new_fling_velocity(
          gesture_event.data.flingStart.velocityX,
          gesture_event.data.flingStart.velocityY);
      DCHECK(!new_fling_velocity.IsZero());

      if (fling_boosted)
        current_fling_velocity_ += new_fling_velocity;
      else
        current_fling_velocity_ = new_fling_velocity;

      WebFloatPoint velocity(current_fling_velocity_.x(),
                             current_fling_velocity_.y());
      deferred_fling_cancel_time_seconds_ = 0;
      disallow_horizontal_fling_scroll_ = !velocity.x;
      disallow_vertical_fling_scroll_ = !velocity.y;
      last_fling_boost_event_ = WebGestureEvent();
      fling_curve_.reset(client_->CreateFlingAnimationCurve(
          gesture_event.sourceDevice,
          velocity,
          blink::WebSize()));
      fling_parameters_.startTime = gesture_event.timeStampSeconds;
      fling_parameters_.delta = velocity;
      fling_parameters_.point = WebPoint(gesture_event.x, gesture_event.y);
      fling_parameters_.globalPoint =
          WebPoint(gesture_event.globalX, gesture_event.globalY);

      TRACE_EVENT_INSTANT2("input",
                           fling_boosted ? "InputHandlerProxy::FlingBoosted"
                                         : "InputHandlerProxy::FlingReplaced",
                           TRACE_EVENT_SCOPE_THREAD,
                           "vx",
                           current_fling_velocity_.x(),
                           "vy",
                           current_fling_velocity_.y());

      // The client expects balanced calls between a consumed GestureFlingStart
      // and |DidStopFlinging()|.
      client_->DidStopFlinging();
      return true;
    }

    default:
      // All other types of gestures (taps, presses, etc...) will complete the
      // deferred fling cancellation.
      CancelCurrentFling();
      return false;
  }
}

void InputHandlerProxy::ExtendBoostedFlingTimeout(
    const blink::WebGestureEvent& event) {
  TRACE_EVENT_INSTANT0("input",
                       "InputHandlerProxy::ExtendBoostedFlingTimeout",
                       TRACE_EVENT_SCOPE_THREAD);
  deferred_fling_cancel_time_seconds_ =
      event.timeStampSeconds + kFlingBoostTimeoutDelaySeconds;
  last_fling_boost_event_ = event;
}

void InputHandlerProxy::Animate(base::TimeTicks time) {
  // If using synchronous animate, then only expect Animate attempts started by
  // the synchronous system. Don't let the InputHandler try to Animate also.
  DCHECK(!input_handler_->IsCurrentlyScrollingInnerViewport() ||
         allow_root_animate_);

  if (scroll_elasticity_controller_)
    scroll_elasticity_controller_->Animate(time);

  if (!fling_curve_)
    return;

  last_fling_animate_time_ = time;
  double monotonic_time_sec = InSecondsF(time);

  if (deferred_fling_cancel_time_seconds_ &&
      monotonic_time_sec > deferred_fling_cancel_time_seconds_) {
    CancelCurrentFling();
    return;
  }

  client_->DidAnimateForInput();

  if (!has_fling_animation_started_) {
    has_fling_animation_started_ = true;
    // Guard against invalid, future or sufficiently stale start times, as there
    // are no guarantees fling event and animation timestamps are compatible.
    if (!fling_parameters_.startTime ||
        monotonic_time_sec <= fling_parameters_.startTime ||
        monotonic_time_sec >= fling_parameters_.startTime +
                                  kMaxSecondsFromFlingTimestampToFirstAnimate) {
      fling_parameters_.startTime = monotonic_time_sec;
      RequestAnimation();
      return;
    }
  }

  bool fling_is_active =
      fling_curve_->apply(monotonic_time_sec - fling_parameters_.startTime,
                          this);

  if (disallow_vertical_fling_scroll_ && disallow_horizontal_fling_scroll_)
    fling_is_active = false;

  if (fling_is_active) {
    RequestAnimation();
  } else {
    TRACE_EVENT_INSTANT0("input",
                         "InputHandlerProxy::animate::flingOver",
                         TRACE_EVENT_SCOPE_THREAD);
    CancelCurrentFling();
  }
}

void InputHandlerProxy::MainThreadHasStoppedFlinging() {
  fling_may_be_active_on_main_thread_ = false;
  client_->DidStopFlinging();
}

void InputHandlerProxy::ReconcileElasticOverscrollAndRootScroll() {
  if (scroll_elasticity_controller_)
    scroll_elasticity_controller_->ReconcileStretchAndScroll();
}

void InputHandlerProxy::UpdateRootLayerStateForSynchronousInputHandler(
    const gfx::ScrollOffset& total_scroll_offset,
    const gfx::ScrollOffset& max_scroll_offset,
    const gfx::SizeF& scrollable_size,
    float page_scale_factor,
    float min_page_scale_factor,
    float max_page_scale_factor) {
  if (synchronous_input_handler_) {
    synchronous_input_handler_->UpdateRootLayerState(
        total_scroll_offset, max_scroll_offset, scrollable_size,
        page_scale_factor, min_page_scale_factor, max_page_scale_factor);
  }
}

void InputHandlerProxy::SetOnlySynchronouslyAnimateRootFlings(
    SynchronousInputHandler* synchronous_input_handler) {
  allow_root_animate_ = !synchronous_input_handler;
  synchronous_input_handler_ = synchronous_input_handler;
  if (synchronous_input_handler_)
    input_handler_->RequestUpdateForSynchronousInputHandler();
}

void InputHandlerProxy::SynchronouslyAnimate(base::TimeTicks time) {
  // When this function is used, SetOnlySynchronouslyAnimate() should have been
  // previously called. IOW you should either be entirely in synchronous mode or
  // not.
  DCHECK(synchronous_input_handler_);
  DCHECK(!allow_root_animate_);
  base::AutoReset<bool> reset(&allow_root_animate_, true);
  Animate(time);
}

void InputHandlerProxy::SynchronouslySetRootScrollOffset(
    const gfx::ScrollOffset& root_offset) {
  DCHECK(synchronous_input_handler_);
  input_handler_->SetSynchronousInputHandlerRootScrollOffset(root_offset);
}

void InputHandlerProxy::HandleOverscroll(
    const gfx::Point& causal_event_viewport_point,
    const cc::InputHandlerScrollResult& scroll_result) {
  DCHECK(client_);
  if (!scroll_result.did_overscroll_root)
    return;

  TRACE_EVENT2("input",
               "InputHandlerProxy::DidOverscroll",
               "dx",
               scroll_result.unused_scroll_delta.x(),
               "dy",
               scroll_result.unused_scroll_delta.y());

  if (fling_curve_) {
    static const int kFlingOverscrollThreshold = 1;
    disallow_horizontal_fling_scroll_ |=
        std::abs(scroll_result.accumulated_root_overscroll.x()) >=
        kFlingOverscrollThreshold;
    disallow_vertical_fling_scroll_ |=
        std::abs(scroll_result.accumulated_root_overscroll.y()) >=
        kFlingOverscrollThreshold;
  }

  client_->DidOverscroll(scroll_result.accumulated_root_overscroll,
                         scroll_result.unused_scroll_delta,
                         ToClientScrollIncrement(current_fling_velocity_),
                         gfx::PointF(causal_event_viewport_point));
}

bool InputHandlerProxy::CancelCurrentFling() {
  if (CancelCurrentFlingWithoutNotifyingClient()) {
    client_->DidStopFlinging();
    return true;
  }
  return false;
}

bool InputHandlerProxy::CancelCurrentFlingWithoutNotifyingClient() {
  bool had_fling_animation = !!fling_curve_;
  if (had_fling_animation &&
      fling_parameters_.sourceDevice == blink::WebGestureDeviceTouchscreen) {
    cc::ScrollStateData scroll_state_data;
    scroll_state_data.is_ending = true;
    cc::ScrollState scroll_state(scroll_state_data);
    input_handler_->ScrollEnd(&scroll_state);
    TRACE_EVENT_ASYNC_END0(
        "input",
        "InputHandlerProxy::HandleGestureFling::started",
        this);
  }

  TRACE_EVENT_INSTANT1("input",
                       "InputHandlerProxy::CancelCurrentFling",
                       TRACE_EVENT_SCOPE_THREAD,
                       "had_fling_animation",
                       had_fling_animation);
  fling_curve_.reset();
  has_fling_animation_started_ = false;
  gesture_scroll_on_impl_thread_ = false;
  current_fling_velocity_ = gfx::Vector2dF();
  fling_parameters_ = blink::WebActiveWheelFlingParameters();

  if (deferred_fling_cancel_time_seconds_) {
    deferred_fling_cancel_time_seconds_ = 0;

    WebGestureEvent last_fling_boost_event = last_fling_boost_event_;
    last_fling_boost_event_ = WebGestureEvent();
    if (last_fling_boost_event.type == WebInputEvent::GestureScrollBegin ||
        last_fling_boost_event.type == WebInputEvent::GestureScrollUpdate) {
      // Synthesize a GestureScrollBegin, as the original was suppressed.
      HandleInputEvent(ObtainGestureScrollBegin(last_fling_boost_event));
    }
  }

  return had_fling_animation;
}

void InputHandlerProxy::RequestAnimation() {
  // When a SynchronousInputHandler is present, root flings should go through
  // it to allow it to control when or if the root fling is animated. Non-root
  // flings always go through the normal InputHandler.
  if (synchronous_input_handler_ &&
      input_handler_->IsCurrentlyScrollingInnerViewport())
    synchronous_input_handler_->SetNeedsSynchronousAnimateInput();
  else
    input_handler_->SetNeedsAnimateInput();
}

bool InputHandlerProxy::TouchpadFlingScroll(
    const WebFloatSize& increment) {
  InputHandlerProxy::EventDisposition disposition;
  cc::EventListenerProperties properties =
      input_handler_->GetEventListenerProperties(
          cc::EventListenerClass::kMouseWheel);
  switch (properties) {
    case cc::EventListenerProperties::kPassive:
      disposition = DID_HANDLE_NON_BLOCKING;
      break;
    case cc::EventListenerProperties::kBlocking:
      disposition = DID_NOT_HANDLE;
      break;
    case cc::EventListenerProperties::kNone: {
      WebMouseWheelEvent synthetic_wheel;
      synthetic_wheel.type = WebInputEvent::MouseWheel;
      synthetic_wheel.timeStampSeconds = InSecondsF(base::TimeTicks::Now());
      synthetic_wheel.deltaX = increment.width;
      synthetic_wheel.deltaY = increment.height;
      synthetic_wheel.hasPreciseScrollingDeltas = true;
      synthetic_wheel.x = fling_parameters_.point.x;
      synthetic_wheel.y = fling_parameters_.point.y;
      synthetic_wheel.globalX = fling_parameters_.globalPoint.x;
      synthetic_wheel.globalY = fling_parameters_.globalPoint.y;
      synthetic_wheel.modifiers = fling_parameters_.modifiers;

      disposition = ScrollByMouseWheel(synthetic_wheel);
      break;
    }
    default:
      NOTREACHED();
      return false;
  }

  switch (disposition) {
    case DID_HANDLE:
      return true;
    case DROP_EVENT:
      break;
    case DID_HANDLE_NON_BLOCKING:
    // TODO(dtapuska): Process the fling on the compositor thread
    // but post the events to the main thread; for now just pass it to the
    // main thread.
    case DID_NOT_HANDLE:
      TRACE_EVENT_INSTANT0("input",
                           "InputHandlerProxy::scrollBy::AbortFling",
                           TRACE_EVENT_SCOPE_THREAD);
      // If we got a DID_NOT_HANDLE, that means we need to deliver wheels on the
      // main thread. In this case we need to schedule a commit and transfer the
      // fling curve over to the main thread and run the rest of the wheels from
      // there. This can happen when flinging a page that contains a scrollable
      // subarea that we can't scroll on the thread if the fling starts outside
      // the subarea but then is flung "under" the pointer.
      client_->TransferActiveWheelFlingAnimation(fling_parameters_);
      fling_may_be_active_on_main_thread_ = true;
      CancelCurrentFlingWithoutNotifyingClient();
      break;
  }

  return false;
}

bool InputHandlerProxy::scrollBy(const WebFloatSize& increment,
                                 const WebFloatSize& velocity) {
  WebFloatSize clipped_increment;
  WebFloatSize clipped_velocity;
  if (!disallow_horizontal_fling_scroll_) {
    clipped_increment.width = increment.width;
    clipped_velocity.width = velocity.width;
  }
  if (!disallow_vertical_fling_scroll_) {
    clipped_increment.height = increment.height;
    clipped_velocity.height = velocity.height;
  }

  current_fling_velocity_ = clipped_velocity;

  // Early out if the increment is zero, but avoid early termination if the
  // velocity is still non-zero.
  if (clipped_increment == WebFloatSize())
    return clipped_velocity != WebFloatSize();

  TRACE_EVENT2("input",
               "InputHandlerProxy::scrollBy",
               "x",
               clipped_increment.width,
               "y",
               clipped_increment.height);

  bool did_scroll = false;

  switch (fling_parameters_.sourceDevice) {
    case blink::WebGestureDeviceTouchpad:
      did_scroll = TouchpadFlingScroll(clipped_increment);
      break;
    case blink::WebGestureDeviceTouchscreen: {
      clipped_increment = ToClientScrollIncrement(clipped_increment);
      cc::ScrollStateData scroll_state_data;
      scroll_state_data.delta_x = clipped_increment.width;
      scroll_state_data.delta_y = clipped_increment.height;
      scroll_state_data.velocity_x = clipped_velocity.width;
      scroll_state_data.velocity_y = clipped_velocity.height;
      scroll_state_data.is_in_inertial_phase = true;
      cc::ScrollState scroll_state(scroll_state_data);
      cc::InputHandlerScrollResult scroll_result =
          input_handler_->ScrollBy(&scroll_state);
      HandleOverscroll(fling_parameters_.point, scroll_result);
      did_scroll = scroll_result.did_scroll;
    } break;
    case blink::WebGestureDeviceUninitialized:
      NOTREACHED();
      return false;
  }

  if (did_scroll) {
    fling_parameters_.cumulativeScroll.width += clipped_increment.width;
    fling_parameters_.cumulativeScroll.height += clipped_increment.height;
  }

  // It's possible the provided |increment| is sufficiently small as to not
  // trigger a scroll, e.g., with a trivial time delta between fling updates.
  // Return true in this case to prevent early fling termination.
  if (std::abs(clipped_increment.width) < kScrollEpsilon &&
      std::abs(clipped_increment.height) < kScrollEpsilon)
    return true;

  return did_scroll;
}

void InputHandlerProxy::HandleScrollElasticityOverscroll(
    const WebGestureEvent& gesture_event,
    const cc::InputHandlerScrollResult& scroll_result) {
  DCHECK(scroll_elasticity_controller_);
  // Send the event and its disposition to the elasticity controller to update
  // the over-scroll animation. Note that the call to the elasticity controller
  // is made asynchronously, to minimize divergence between main thread and
  // impl thread event handling paths.
  base::ThreadTaskRunnerHandle::Get()->PostTask(
      FROM_HERE,
      base::Bind(&InputScrollElasticityController::ObserveGestureEventAndResult,
                 scroll_elasticity_controller_->GetWeakPtr(), gesture_event,
                 scroll_result));
}

}  // namespace ui