// 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 "content/renderer/input/input_handler_proxy.h"
#include "base/debug/trace_event.h"
#include "base/logging.h"
#include "base/metrics/histogram.h"
#include "content/renderer/input/input_handler_proxy_client.h"
#include "third_party/WebKit/public/platform/Platform.h"
#include "third_party/WebKit/public/web/WebInputEvent.h"
#include "ui/events/latency_info.h"
#include "ui/gfx/frame_time.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 {
// Validate provided event timestamps that interact with animation timestamps.
const double kBadTimestampDeltaFromNowInS = 60. * 60. * 24. * 7.;
double InSecondsF(const base::TimeTicks& time) {
return (time - base::TimeTicks()).InSecondsF();
}
void SendScrollLatencyUma(const WebInputEvent& event,
const ui::LatencyInfo& latency_info) {
if (!(event.type == WebInputEvent::GestureScrollBegin ||
event.type == WebInputEvent::GestureScrollUpdate ||
event.type == WebInputEvent::GestureScrollUpdateWithoutPropagation))
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::HighResNow() - it->second.event_time;
for (size_t i = 0; i < it->second.event_count; ++i) {
UMA_HISTOGRAM_CUSTOM_COUNTS(
"Event.Latency.RendererImpl.GestureScroll2",
delta.InMicroseconds(),
0,
1000000,
100);
}
} // namespace
}
namespace content {
InputHandlerProxy::InputHandlerProxy(cc::InputHandler* input_handler)
: client_(NULL),
input_handler_(input_handler),
#ifndef NDEBUG
expect_scroll_update_end_(false),
expect_pinch_update_end_(false),
#endif
gesture_scroll_on_impl_thread_(false),
gesture_pinch_on_impl_thread_(false),
fling_may_be_active_on_main_thread_(false),
fling_overscrolled_horizontally_(false),
fling_overscrolled_vertically_(false) {
input_handler_->BindToClient(this);
}
InputHandlerProxy::~InputHandlerProxy() {}
void InputHandlerProxy::WillShutdown() {
input_handler_ = NULL;
DCHECK(client_);
client_->WillShutdown();
}
void InputHandlerProxy::SetClient(InputHandlerProxyClient* client) {
DCHECK(!client_ || !client);
client_ = client;
}
InputHandlerProxy::EventDisposition
InputHandlerProxy::HandleInputEventWithLatencyInfo(
const WebInputEvent& event,
ui::LatencyInfo* latency_info) {
DCHECK(input_handler_);
SendScrollLatencyUma(event, *latency_info);
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(client_);
DCHECK(input_handler_);
if (event.type == WebInputEvent::MouseWheel) {
const WebMouseWheelEvent& wheel_event =
*static_cast<const WebMouseWheelEvent*>(&event);
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
return DID_NOT_HANDLE;
}
cc::InputHandler::ScrollStatus scroll_status = input_handler_->ScrollBegin(
gfx::Point(wheel_event.x, wheel_event.y), cc::InputHandler::Wheel);
switch (scroll_status) {
case cc::InputHandler::ScrollStarted: {
TRACE_EVENT_INSTANT2(
"renderer",
"InputHandlerProxy::handle_input wheel scroll",
TRACE_EVENT_SCOPE_THREAD,
"deltaX",
-wheel_event.deltaX,
"deltaY",
-wheel_event.deltaY);
bool did_scroll = input_handler_->ScrollBy(
gfx::Point(wheel_event.x, wheel_event.y),
gfx::Vector2dF(-wheel_event.deltaX, -wheel_event.deltaY));
input_handler_->ScrollEnd();
return did_scroll ? DID_HANDLE : DROP_EVENT;
}
case cc::InputHandler::ScrollIgnored:
// 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.
return DID_NOT_HANDLE;
case cc::InputHandler::ScrollOnMainThread:
return DID_NOT_HANDLE;
}
} else if (event.type == WebInputEvent::GestureScrollBegin) {
DCHECK(!gesture_scroll_on_impl_thread_);
#ifndef NDEBUG
DCHECK(!expect_scroll_update_end_);
expect_scroll_update_end_ = true;
#endif
const WebGestureEvent& gesture_event =
*static_cast<const WebGestureEvent*>(&event);
cc::InputHandler::ScrollStatus scroll_status = input_handler_->ScrollBegin(
gfx::Point(gesture_event.x, gesture_event.y),
cc::InputHandler::Gesture);
switch (scroll_status) {
case cc::InputHandler::ScrollStarted:
gesture_scroll_on_impl_thread_ = true;
return DID_HANDLE;
case cc::InputHandler::ScrollOnMainThread:
return DID_NOT_HANDLE;
case cc::InputHandler::ScrollIgnored:
return DROP_EVENT;
}
} else if (event.type == WebInputEvent::GestureScrollUpdate) {
#ifndef NDEBUG
DCHECK(expect_scroll_update_end_);
#endif
if (!gesture_scroll_on_impl_thread_ && !gesture_pinch_on_impl_thread_)
return DID_NOT_HANDLE;
const WebGestureEvent& gesture_event =
*static_cast<const WebGestureEvent*>(&event);
bool did_scroll = input_handler_->ScrollBy(
gfx::Point(gesture_event.x, gesture_event.y),
gfx::Vector2dF(-gesture_event.data.scrollUpdate.deltaX,
-gesture_event.data.scrollUpdate.deltaY));
return did_scroll ? DID_HANDLE : DROP_EVENT;
} else if (event.type == WebInputEvent::GestureScrollEnd) {
#ifndef NDEBUG
DCHECK(expect_scroll_update_end_);
expect_scroll_update_end_ = false;
#endif
input_handler_->ScrollEnd();
if (!gesture_scroll_on_impl_thread_)
return DID_NOT_HANDLE;
gesture_scroll_on_impl_thread_ = false;
return DID_HANDLE;
} else if (event.type == WebInputEvent::GesturePinchBegin) {
#ifndef NDEBUG
DCHECK(!expect_pinch_update_end_);
expect_pinch_update_end_ = true;
#endif
input_handler_->PinchGestureBegin();
gesture_pinch_on_impl_thread_ = true;
return DID_HANDLE;
} else if (event.type == WebInputEvent::GesturePinchEnd) {
#ifndef NDEBUG
DCHECK(expect_pinch_update_end_);
expect_pinch_update_end_ = false;
#endif
gesture_pinch_on_impl_thread_ = false;
input_handler_->PinchGestureEnd();
return DID_HANDLE;
} else if (event.type == WebInputEvent::GesturePinchUpdate) {
#ifndef NDEBUG
DCHECK(expect_pinch_update_end_);
#endif
const WebGestureEvent& gesture_event =
*static_cast<const WebGestureEvent*>(&event);
input_handler_->PinchGestureUpdate(
gesture_event.data.pinchUpdate.scale,
gfx::Point(gesture_event.x, gesture_event.y));
return DID_HANDLE;
} else if (event.type == WebInputEvent::GestureFlingStart) {
const WebGestureEvent& gesture_event =
*static_cast<const WebGestureEvent*>(&event);
return HandleGestureFling(gesture_event);
} else if (event.type == WebInputEvent::GestureFlingCancel) {
if (CancelCurrentFling())
return DID_HANDLE;
else if (!fling_may_be_active_on_main_thread_)
return DROP_EVENT;
} else if (event.type == WebInputEvent::TouchStart) {
const WebTouchEvent& touch_event =
*static_cast<const WebTouchEvent*>(&event);
for (size_t i = 0; i < touch_event.touchesLength; ++i) {
if (touch_event.touches[i].state != WebTouchPoint::StatePressed)
continue;
if (input_handler_->HaveTouchEventHandlersAt(touch_event.touches[i]
.position))
return DID_NOT_HANDLE;
}
return DROP_EVENT;
} else if (WebInputEvent::isKeyboardEventType(event.type)) {
CancelCurrentFling();
}
return DID_NOT_HANDLE;
}
InputHandlerProxy::EventDisposition
InputHandlerProxy::HandleGestureFling(
const WebGestureEvent& gesture_event) {
cc::InputHandler::ScrollStatus scroll_status;
if (gesture_event.sourceDevice == WebGestureEvent::Touchpad) {
scroll_status = input_handler_->ScrollBegin(
gfx::Point(gesture_event.x, gesture_event.y),
cc::InputHandler::NonBubblingGesture);
} else {
if (!gesture_scroll_on_impl_thread_)
scroll_status = cc::InputHandler::ScrollOnMainThread;
else
scroll_status = input_handler_->FlingScrollBegin();
}
#ifndef NDEBUG
expect_scroll_update_end_ = false;
#endif
switch (scroll_status) {
case cc::InputHandler::ScrollStarted: {
if (gesture_event.sourceDevice == WebGestureEvent::Touchpad)
input_handler_->ScrollEnd();
fling_curve_.reset(client_->CreateFlingAnimationCurve(
gesture_event.sourceDevice,
WebFloatPoint(gesture_event.data.flingStart.velocityX,
gesture_event.data.flingStart.velocityY),
blink::WebSize()));
fling_overscrolled_horizontally_ = false;
fling_overscrolled_vertically_ = false;
TRACE_EVENT_ASYNC_BEGIN0(
"renderer",
"InputHandlerProxy::HandleGestureFling::started",
this);
if (gesture_event.timeStampSeconds) {
fling_parameters_.startTime = gesture_event.timeStampSeconds;
DCHECK_LT(fling_parameters_.startTime -
InSecondsF(gfx::FrameTime::Now()),
kBadTimestampDeltaFromNowInS);
}
fling_parameters_.delta =
WebFloatPoint(gesture_event.data.flingStart.velocityX,
gesture_event.data.flingStart.velocityY);
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;
input_handler_->ScheduleAnimation();
return DID_HANDLE;
}
case cc::InputHandler::ScrollOnMainThread: {
TRACE_EVENT_INSTANT0("renderer",
"InputHandlerProxy::HandleGestureFling::"
"scroll_on_main_thread",
TRACE_EVENT_SCOPE_THREAD);
fling_may_be_active_on_main_thread_ = true;
return DID_NOT_HANDLE;
}
case cc::InputHandler::ScrollIgnored: {
TRACE_EVENT_INSTANT0(
"renderer",
"InputHandlerProxy::HandleGestureFling::ignored",
TRACE_EVENT_SCOPE_THREAD);
if (gesture_event.sourceDevice == WebGestureEvent::Touchpad) {
// 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;
}
void InputHandlerProxy::Animate(base::TimeTicks time) {
if (!fling_curve_)
return;
double monotonic_time_sec = InSecondsF(time);
if (!fling_parameters_.startTime) {
fling_parameters_.startTime = monotonic_time_sec;
input_handler_->ScheduleAnimation();
return;
}
if (fling_curve_->apply(monotonic_time_sec - fling_parameters_.startTime,
this)) {
input_handler_->ScheduleAnimation();
} else {
TRACE_EVENT_INSTANT0("renderer",
"InputHandlerProxy::animate::flingOver",
TRACE_EVENT_SCOPE_THREAD);
CancelCurrentFling();
}
}
void InputHandlerProxy::MainThreadHasStoppedFlinging() {
fling_may_be_active_on_main_thread_ = false;
}
void InputHandlerProxy::DidOverscroll(const cc::DidOverscrollParams& params) {
DCHECK(client_);
if (fling_curve_) {
static const int kFlingOverscrollThreshold = 1;
fling_overscrolled_horizontally_ |=
std::abs(params.accumulated_overscroll.x()) >=
kFlingOverscrollThreshold;
fling_overscrolled_vertically_ |=
std::abs(params.accumulated_overscroll.y()) >=
kFlingOverscrollThreshold;
}
client_->DidOverscroll(params);
}
bool InputHandlerProxy::CancelCurrentFling() {
bool had_fling_animation = fling_curve_;
if (had_fling_animation &&
fling_parameters_.sourceDevice == WebGestureEvent::Touchscreen) {
input_handler_->ScrollEnd();
TRACE_EVENT_ASYNC_END0(
"renderer",
"InputHandlerProxy::HandleGestureFling::started",
this);
}
TRACE_EVENT_INSTANT1("renderer",
"InputHandlerProxy::CancelCurrentFling",
TRACE_EVENT_SCOPE_THREAD,
"had_fling_animation",
had_fling_animation);
fling_curve_.reset();
gesture_scroll_on_impl_thread_ = false;
fling_parameters_ = blink::WebActiveWheelFlingParameters();
return had_fling_animation;
}
bool InputHandlerProxy::TouchpadFlingScroll(
const WebFloatSize& increment) {
WebMouseWheelEvent synthetic_wheel;
synthetic_wheel.type = WebInputEvent::MouseWheel;
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;
InputHandlerProxy::EventDisposition disposition =
HandleInputEvent(synthetic_wheel);
switch (disposition) {
case DID_HANDLE:
return true;
case DROP_EVENT:
break;
case DID_NOT_HANDLE:
TRACE_EVENT_INSTANT0("renderer",
"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;
CancelCurrentFling();
break;
}
return false;
}
static gfx::Vector2dF ToClientScrollIncrement(const WebFloatSize& increment) {
return gfx::Vector2dF(-increment.width, -increment.height);
}
void InputHandlerProxy::scrollBy(const WebFloatSize& increment) {
WebFloatSize clipped_increment;
if (!fling_overscrolled_horizontally_)
clipped_increment.width = increment.width;
if (!fling_overscrolled_vertically_)
clipped_increment.height = increment.height;
if (clipped_increment == WebFloatSize())
return;
TRACE_EVENT2("renderer",
"InputHandlerProxy::scrollBy",
"x",
clipped_increment.width,
"y",
clipped_increment.height);
bool did_scroll = false;
switch (fling_parameters_.sourceDevice) {
case WebGestureEvent::Touchpad:
did_scroll = TouchpadFlingScroll(clipped_increment);
break;
case WebGestureEvent::Touchscreen:
clipped_increment = ToClientScrollIncrement(clipped_increment);
did_scroll = input_handler_->ScrollBy(fling_parameters_.point,
clipped_increment);
break;
}
if (did_scroll) {
fling_parameters_.cumulativeScroll.width += clipped_increment.width;
fling_parameters_.cumulativeScroll.height += clipped_increment.height;
}
}
void InputHandlerProxy::notifyCurrentFlingVelocity(
const WebFloatSize& velocity) {
TRACE_EVENT2("renderer",
"InputHandlerProxy::notifyCurrentFlingVelocity",
"vx",
velocity.width,
"vy",
velocity.height);
input_handler_->NotifyCurrentFlingVelocity(ToClientScrollIncrement(velocity));
}
} // namespace content