Skip to content

Commit

Permalink
Synthesize ctrl-wheel events on touchpad pinch
Browse files Browse the repository at this point in the history
On Windows, pinch gestures on a touchpad typically sends wheel events
with the ctrlKey modifier set.  This CL makes Chrome on Mac do the same
thing so that pages that are 'naturally zoomable' (eg. maps) get a
chance to override the pinch behavior before we use it to do browser
zoom.  Getting browser zoom on pinch is a long standing source of
confusion for users of Google Maps.  See http://goo.gl/84CTaJ for
discussion.

To be compatible with existing uses of wheel events, the deltaY value
in the wheel event is computed as -100*log(scale).  So zooming in 2x is
about -70 and zooming out 2x is 70. To compute a scale factor from this
value in JavaScript use 'Math.exp(-deltaY / 100)'.  See demo at
http://jsbin.com/qiyaseza/.

We expect to eventually want this change on ChromeOS as well (once
ChromeOS wires pinch up to pinch-zoom), and so this transformation
belongs in InputRouter.

The trickiest part of this change is handling the WheelEvent ACK
messages properly.  If the wheel was synthesized from a
GesturePinchUpdate, we need to convert the ACK back to the right type.
To do this I attach a 'synthesized_from_pinch' bit to each
WebWheelEvent in InputRouter's coalesced_mouse_wheel_events_ and to the
current_wheel_event_.  Then when I get a wheel ACK I use this bit on
current_wheel_event_ to decide how it should be handled.

This removes some old code which flushed any pending wheel events
whenever we got a non-wheel event.  Doing that causes us to ignore (or
possibly misattribute) the ACKs we'll eventually get back from the
renderer for these events.  This was probably already responsible for
some bugs with the Overscroll controller (which relies on
current_wheel_event representing THE event that we received an ACK for)
and  would be very problematic for the pinch-derived wheels where
(since the GestureEventQueue expects to reliably get an ack per event).
I tracked this code back to http://crbug.com/154740 where it was added
to cope with the fact that we were discarding pending wheel events.  I
think a better fix for that is to just continue to queue and send wheel
events, even when there are other events happening (as we do for
interleaving touch and gesture events with other events).

BUG=289887

Review URL: https://codereview.chromium.org/250923004

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@267822 0039d316-1c4b-4281-b951-d872f2087c98
  • Loading branch information
rbyers@chromium.org committed May 2, 2014
1 parent 0d8afa5 commit 621b3fe
Show file tree
Hide file tree
Showing 12 changed files with 457 additions and 133 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,8 @@ class GestureEventQueueTest : public testing::Test,
float anchorX,
float anchorY,
int modifiers) {
SimulateGestureEvent(
SyntheticWebGestureEventBuilder::BuildPinchUpdate(scale,
anchorX,
anchorY,
modifiers));
SimulateGestureEvent(SyntheticWebGestureEventBuilder::BuildPinchUpdate(
scale, anchorX, anchorY, modifiers, WebGestureEvent::Touchscreen));
}

void SimulateGestureFlingStartEvent(
Expand Down
142 changes: 100 additions & 42 deletions content/browser/renderer_host/input/input_router_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

#include "content/browser/renderer_host/input/input_router_impl.h"

#include <math.h>

#include "base/auto_reset.h"
#include "base/command_line.h"
#include "base/metrics/histogram.h"
Expand Down Expand Up @@ -180,26 +182,39 @@ void InputRouterImpl::SendMouseEvent(

void InputRouterImpl::SendWheelEvent(
const MouseWheelEventWithLatencyInfo& wheel_event) {
// If there's already a mouse wheel event waiting to be sent to the renderer,
// add the new deltas to that event. Not doing so (e.g., by dropping the old
// event, as for mouse moves) results in very slow scrolling on the Mac (on
// which many, very small wheel events are sent).
SendWheelEvent(QueuedWheelEvent(wheel_event, false));
}

void InputRouterImpl::SendWheelEvent(const QueuedWheelEvent& wheel_event) {
if (mouse_wheel_pending_) {
// If there's already a mouse wheel event waiting to be sent to the
// renderer, add the new deltas to that event. Not doing so (e.g., by
// dropping the old event, as for mouse moves) results in very slow
// scrolling on the Mac (on which many, very small wheel events are sent).
// Note that we can't coalesce wheel events for pinches because the GEQ
// expects one ACK for each (but it's fine to coalesce non-pinch wheels
// into a pinch one). Note that the GestureEventQueue ensures we only
// ever have a single pinch event queued here.
if (coalesced_mouse_wheel_events_.empty() ||
!coalesced_mouse_wheel_events_.back().CanCoalesceWith(wheel_event)) {
wheel_event.synthesized_from_pinch ||
!coalesced_mouse_wheel_events_.back().event.CanCoalesceWith(
wheel_event.event)) {
coalesced_mouse_wheel_events_.push_back(wheel_event);
} else {
coalesced_mouse_wheel_events_.back().CoalesceWith(wheel_event);
coalesced_mouse_wheel_events_.back().event.CoalesceWith(
wheel_event.event);
}
return;
}

mouse_wheel_pending_ = true;
current_wheel_event_ = wheel_event;

HISTOGRAM_COUNTS_100("Renderer.WheelQueueSize",
coalesced_mouse_wheel_events_.size());

FilterAndSendWebInputEvent(wheel_event.event, wheel_event.latency, false);
FilterAndSendWebInputEvent(
wheel_event.event.event, wheel_event.event.latency, false);
}

void InputRouterImpl::SendKeyboardEvent(const NativeWebKeyboardEvent& key_event,
Expand Down Expand Up @@ -235,7 +250,7 @@ void InputRouterImpl::SendGestureEvent(
return;
}

FilterAndSendWebInputEvent(gesture_event.event, gesture_event.latency, false);
SendGestureEventImmediately(gesture_event);
}

void InputRouterImpl::SendTouchEvent(
Expand Down Expand Up @@ -281,6 +296,12 @@ void InputRouterImpl::SendTouchEventImmediately(

void InputRouterImpl::SendGestureEventImmediately(
const GestureEventWithLatencyInfo& gesture_event) {
if (gesture_event.event.type == WebInputEvent::GesturePinchUpdate &&
gesture_event.event.sourceDevice == WebGestureEvent::Touchpad) {
SendSyntheticWheelEventForPinch(gesture_event);
return;
}

FilterAndSendWebInputEvent(gesture_event.event, gesture_event.latency, false);
}

Expand Down Expand Up @@ -378,19 +399,6 @@ void InputRouterImpl::FilterAndSendWebInputEvent(
"type",
WebInputEventTraits::GetName(input_event.type));

// Transmit any pending wheel events on a non-wheel event. This ensures that
// final PhaseEnded wheel event is received, which is necessary to terminate
// rubber-banding, for example.
if (input_event.type != WebInputEvent::MouseWheel) {
WheelEventQueue mouse_wheel_events;
mouse_wheel_events.swap(coalesced_mouse_wheel_events_);
for (size_t i = 0; i < mouse_wheel_events.size(); ++i) {
OfferToHandlers(mouse_wheel_events[i].event,
mouse_wheel_events[i].latency,
false);
}
}

// Any input event cancels a pending mouse move event.
next_mouse_move_.reset();

Expand All @@ -400,18 +408,6 @@ void InputRouterImpl::FilterAndSendWebInputEvent(
void InputRouterImpl::OfferToHandlers(const WebInputEvent& input_event,
const ui::LatencyInfo& latency_info,
bool is_keyboard_shortcut) {
// Trackpad pinch gestures are not yet handled by the renderer.
// TODO(rbyers): Send mousewheel for trackpad pinch - crbug.com/289887.
if (input_event.type == WebInputEvent::GesturePinchUpdate &&
static_cast<const WebGestureEvent&>(input_event).sourceDevice ==
WebGestureEvent::Touchpad) {
ProcessInputEventAck(input_event.type,
INPUT_EVENT_ACK_STATE_NOT_CONSUMED,
latency_info,
ACK_SOURCE_NONE);
return;
}

if (OfferToOverscrollController(input_event, latency_info))
return;

Expand Down Expand Up @@ -515,6 +511,43 @@ bool InputRouterImpl::OfferToRenderer(const WebInputEvent& input_event,
return false;
}

void InputRouterImpl::SendSyntheticWheelEventForPinch(
const GestureEventWithLatencyInfo& pinch_event) {
// We match typical trackpad behavior on Windows by sending fake wheel events
// with the ctrl modifier set when we see trackpad pinch gestures. Ideally
// we'd someday get a standard 'pinch' event and send that instead.

WebMouseWheelEvent wheelEvent;
wheelEvent.type = WebInputEvent::MouseWheel;
wheelEvent.timeStampSeconds = pinch_event.event.timeStampSeconds;
wheelEvent.windowX = wheelEvent.x = pinch_event.event.x;
wheelEvent.windowY = wheelEvent.y = pinch_event.event.y;
wheelEvent.globalX = pinch_event.event.globalX;
wheelEvent.globalY = pinch_event.event.globalY;
wheelEvent.modifiers =
pinch_event.event.modifiers | WebInputEvent::ControlKey;
wheelEvent.deltaX = 0;
// The function to convert scales to deltaY values is designed to be
// compatible with websites existing use of wheel events, and with existing
// Windows trackpad behavior. In particular, we want:
// - deltas should accumulate via addition: f(s1*s2)==f(s1)+f(s2)
// - deltas should invert via negation: f(1/s) == -f(s)
// - zoom in should be positive: f(s) > 0 iff s > 1
// - magnitude roughly matches wheels: f(2) > 25 && f(2) < 100
// - a formula that's relatively easy to use from JavaScript
// Note that 'wheel' event deltaY values have their sign inverted. So to
// convert a wheel deltaY back to a scale use Math.exp(-deltaY/100).
DCHECK_GT(pinch_event.event.data.pinchUpdate.scale, 0);
wheelEvent.deltaY = 100.0f * log(pinch_event.event.data.pinchUpdate.scale);
wheelEvent.hasPreciseScrollingDeltas = true;
wheelEvent.wheelTicksX = 0;
wheelEvent.wheelTicksY =
pinch_event.event.data.pinchUpdate.scale > 1 ? 1 : -1;

SendWheelEvent(QueuedWheelEvent(
MouseWheelEventWithLatencyInfo(wheelEvent, pinch_event.latency), true));
}

void InputRouterImpl::OnInputEventAck(WebInputEvent::Type event_type,
InputEventAckState ack_result,
const ui::LatencyInfo& latency_info) {
Expand Down Expand Up @@ -647,20 +680,32 @@ void InputRouterImpl::ProcessMouseAck(blink::WebInputEvent::Type type,

void InputRouterImpl::ProcessWheelAck(InputEventAckState ack_result,
const ui::LatencyInfo& latency) {
ProcessAckForOverscroll(current_wheel_event_.event, ack_result);

// TODO(miletus): Add renderer side latency to each uncoalesced mouse
// wheel event and add terminal component to each of them.
current_wheel_event_.latency.AddNewLatencyFrom(latency);
// Process the unhandled wheel event here before calling SendWheelEvent()
// since it will mutate current_wheel_event_.
ack_handler_->OnWheelEventAck(current_wheel_event_, ack_result);
current_wheel_event_.event.latency.AddNewLatencyFrom(latency);

if (current_wheel_event_.synthesized_from_pinch) {
// Ack the GesturePinchUpdate event that generated this wheel event.
ProcessInputEventAck(WebInputEvent::GesturePinchUpdate,
ack_result,
current_wheel_event_.event.latency,
current_ack_source_);
} else {
// Process the unhandled wheel event here before calling SendWheelEvent()
// since it will mutate current_wheel_event_.
ProcessAckForOverscroll(current_wheel_event_.event.event, ack_result);
ack_handler_->OnWheelEventAck(current_wheel_event_.event, ack_result);
}

// Mark the wheel event complete only after the ACKs have been handled above.
// For example, ACKing the GesturePinchUpdate could cause another
// GesturePinchUpdate to be sent, which should queue a wheel event rather than
// send it immediately.
mouse_wheel_pending_ = false;

// Now send the next (coalesced) mouse wheel event.
// Send the next (coalesced or synthetic) mouse wheel event.
if (!coalesced_mouse_wheel_events_.empty()) {
MouseWheelEventWithLatencyInfo next_wheel_event =
coalesced_mouse_wheel_events_.front();
QueuedWheelEvent next_wheel_event = coalesced_mouse_wheel_events_.front();
coalesced_mouse_wheel_events_.pop_front();
SendWheelEvent(next_wheel_event);
}
Expand Down Expand Up @@ -752,4 +797,17 @@ bool InputRouterImpl::IsInOverscrollGesture() const {
return controller && controller->overscroll_mode() != OVERSCROLL_NONE;
}

InputRouterImpl::QueuedWheelEvent::QueuedWheelEvent()
: synthesized_from_pinch(false) {
}

InputRouterImpl::QueuedWheelEvent::QueuedWheelEvent(
const MouseWheelEventWithLatencyInfo& event,
bool synthesized_from_pinch)
: event(event), synthesized_from_pinch(synthesized_from_pinch) {
}

InputRouterImpl::QueuedWheelEvent::~QueuedWheelEvent() {
}

} // namespace content
24 changes: 22 additions & 2 deletions content/browser/renderer_host/input/input_router_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,26 @@ class CONTENT_EXPORT InputRouterImpl
const ui::LatencyInfo& latency_info,
bool is_keyboard_shortcut);

// A data structure that attaches some metadata to a WebMouseWheelEvent
// and its latency info.
struct QueuedWheelEvent {
QueuedWheelEvent();
QueuedWheelEvent(const MouseWheelEventWithLatencyInfo& event,
bool synthesized_from_pinch);
~QueuedWheelEvent();

MouseWheelEventWithLatencyInfo event;
bool synthesized_from_pinch;
};

// Enqueue or send a mouse wheel event.
void SendWheelEvent(const QueuedWheelEvent& wheel_event);

// Given a Touchpad GesturePinchUpdate event, create and send a synthetic
// wheel event for it.
void SendSyntheticWheelEventForPinch(
const GestureEventWithLatencyInfo& pinch_event);

// IPC message handlers
void OnInputEventAck(blink::WebInputEvent::Type event_type,
InputEventAckState ack_result,
Expand Down Expand Up @@ -216,7 +236,7 @@ class CONTENT_EXPORT InputRouterImpl
// (Similar to |mouse_move_pending_|.) True if a mouse wheel event was sent
// and we are waiting for a corresponding ack.
bool mouse_wheel_pending_;
MouseWheelEventWithLatencyInfo current_wheel_event_;
QueuedWheelEvent current_wheel_event_;

// (Similar to |next_mouse_move_|.) The next mouse wheel events to send.
// Unlike mouse moves, mouse wheel events received while one is pending are
Expand All @@ -225,7 +245,7 @@ class CONTENT_EXPORT InputRouterImpl
// high rate; not waiting for the ack results in jankiness, and using the same
// mechanism as for mouse moves (just dropping old events when multiple ones
// would be queued) results in very slow scrolling.
typedef std::deque<MouseWheelEventWithLatencyInfo> WheelEventQueue;
typedef std::deque<QueuedWheelEvent> WheelEventQueue;
WheelEventQueue coalesced_mouse_wheel_events_;

// A queue of keyboard events. We can't trust data from the renderer so we
Expand Down
Loading

0 comments on commit 621b3fe

Please sign in to comment.