diff options
Diffstat (limited to 'chromium/content/browser/renderer_host/input/touch_event_queue.cc')
-rw-r--r-- | chromium/content/browser/renderer_host/input/touch_event_queue.cc | 644 |
1 files changed, 493 insertions, 151 deletions
diff --git a/chromium/content/browser/renderer_host/input/touch_event_queue.cc b/chromium/content/browser/renderer_host/input/touch_event_queue.cc index 3524b18250e..bbac01c3ebf 100644 --- a/chromium/content/browser/renderer_host/input/touch_event_queue.cc +++ b/chromium/content/browser/renderer_host/input/touch_event_queue.cc @@ -5,67 +5,79 @@ #include "content/browser/renderer_host/input/touch_event_queue.h" #include "base/auto_reset.h" -#include "base/command_line.h" #include "base/debug/trace_event.h" #include "base/stl_util.h" #include "content/browser/renderer_host/input/timeout_monitor.h" -#include "content/common/input/web_input_event_traits.h" -#include "content/public/common/content_switches.h" +#include "content/common/input/web_touch_event_traits.h" +#include "ui/gfx/geometry/point_f.h" using blink::WebInputEvent; using blink::WebTouchEvent; using blink::WebTouchPoint; +using ui::LatencyInfo; namespace content { namespace { -const InputEventAckState kDefaultNotForwardedAck = - INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS; +// Time interval at which touchmove events will be forwarded to the client while +// scrolling is active and possible. +const double kAsyncTouchMoveIntervalSec = .2; -typedef std::vector<TouchEventWithLatencyInfo> WebTouchEventWithLatencyList; +// A slop region just larger than that used by many web applications. When +// touchmove's are being sent asynchronously, movement outside this region will +// trigger an immediate async touchmove to cancel potential tap-related logic. +const double kApplicationSlopRegionLengthDipsSqared = 15. * 15.; + +// Using a small epsilon when comparing slop distances allows pixel perfect +// slop determination when using fractional DIP coordinates (assuming the slop +// region and DPI scale are reasonably proportioned). +const float kSlopEpsilon = .05f; TouchEventWithLatencyInfo ObtainCancelEventForTouchEvent( const TouchEventWithLatencyInfo& event_to_cancel) { TouchEventWithLatencyInfo event = event_to_cancel; - event.event.type = WebInputEvent::TouchCancel; - for (size_t i = 0; i < event.event.touchesLength; i++) - event.event.touches[i].state = WebTouchPoint::StateCancelled; + WebTouchEventTraits::ResetTypeAndTouchStates( + WebInputEvent::TouchCancel, + // TODO(rbyers): Shouldn't we use a fresh timestamp? + event.event.timeStampSeconds, + &event.event); return event; } -bool IsNewTouchGesture(const WebTouchEvent& event) { - if (event.type != WebInputEvent::TouchStart) - return false; - if (!event.touchesLength) - return false; - for (size_t i = 0; i < event.touchesLength; i++) { - if (event.touches[i].state != WebTouchPoint::StatePressed) - return false; - } - return true; +bool ShouldTouchTriggerTimeout(const WebTouchEvent& event) { + return (event.type == WebInputEvent::TouchStart || + event.type == WebInputEvent::TouchMove) && + !WebInputEventTraits::IgnoresAckDisposition(event); } -bool ShouldTouchTypeTriggerTimeout(WebInputEvent::Type type) { - return type == WebInputEvent::TouchStart || - type == WebInputEvent::TouchMove; +bool OutsideApplicationSlopRegion(const WebTouchEvent& event, + const gfx::PointF& anchor) { + return (gfx::PointF(event.touches[0].position) - anchor).LengthSquared() > + kApplicationSlopRegionLengthDipsSqared; } } // namespace + +// Cancels a touch sequence if a touchstart or touchmove ack response is +// sufficiently delayed. class TouchEventQueue::TouchTimeoutHandler { public: - TouchTimeoutHandler(TouchEventQueue* touch_queue, size_t timeout_delay_ms) + TouchTimeoutHandler(TouchEventQueue* touch_queue, + base::TimeDelta timeout_delay) : touch_queue_(touch_queue), - timeout_delay_(base::TimeDelta::FromMilliseconds(timeout_delay_ms)), + timeout_delay_(timeout_delay), pending_ack_state_(PENDING_ACK_NONE), timeout_monitor_(base::Bind(&TouchTimeoutHandler::OnTimeOut, - base::Unretained(this))) {} + base::Unretained(this))) { + DCHECK(timeout_delay != base::TimeDelta()); + } ~TouchTimeoutHandler() {} void Start(const TouchEventWithLatencyInfo& event) { DCHECK_EQ(pending_ack_state_, PENDING_ACK_NONE); - DCHECK(ShouldTouchTypeTriggerTimeout(event.event.type)); + DCHECK(ShouldTouchTriggerTimeout(event.event)); timeout_event_ = event; timeout_monitor_.Restart(timeout_delay_); } @@ -80,9 +92,7 @@ class TouchEventQueue::TouchTimeoutHandler { SetPendingAckState(PENDING_ACK_CANCEL_EVENT); TouchEventWithLatencyInfo cancel_event = ObtainCancelEventForTouchEvent(timeout_event_); - touch_queue_->UpdateTouchAckStates( - cancel_event.event, kDefaultNotForwardedAck); - touch_queue_->client_->SendTouchEventImmediately(cancel_event); + touch_queue_->SendTouchEventImmediately(cancel_event); } else { SetPendingAckState(PENDING_ACK_NONE); touch_queue_->UpdateTouchAckStates(timeout_event_.event, ack_result); @@ -95,14 +105,23 @@ class TouchEventQueue::TouchTimeoutHandler { return false; } - bool HasTimeoutEvent() const { - return pending_ack_state_ != PENDING_ACK_NONE; + bool FilterEvent(const WebTouchEvent& event) { + return HasTimeoutEvent(); } bool IsTimeoutTimerRunning() const { return timeout_monitor_.IsRunning(); } + void Reset() { + pending_ack_state_ = PENDING_ACK_NONE; + timeout_monitor_.Stop(); + } + + void set_timeout_delay(base::TimeDelta timeout_delay) { + timeout_delay_ = timeout_delay; + } + private: enum PendingAckState { PENDING_ACK_NONE, @@ -121,7 +140,7 @@ class TouchEventQueue::TouchTimeoutHandler { DCHECK(HasTimeoutEvent()); if (ack_result != INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS) return true; - return !IsNewTouchGesture(timeout_event_.event); + return !WebTouchEventTraits::IsTouchSequenceStart(timeout_event_.event); } void SetPendingAckState(PendingAckState new_pending_ack_state) { @@ -147,6 +166,10 @@ class TouchEventQueue::TouchTimeoutHandler { pending_ack_state_ = new_pending_ack_state; } + bool HasTimeoutEvent() const { + return pending_ack_state_ != PENDING_ACK_NONE; + } + TouchEventQueue* touch_queue_; @@ -163,6 +186,60 @@ class TouchEventQueue::TouchTimeoutHandler { TimeoutMonitor timeout_monitor_; }; +// Provides touchmove slop suppression for a single touch that remains within +// a given slop region, unless the touchstart is preventDefault'ed. +// TODO(jdduke): Use a flag bundled with each TouchEvent declaring whether it +// has exceeded the slop region, removing duplicated slop determination logic. +class TouchEventQueue::TouchMoveSlopSuppressor { + public: + TouchMoveSlopSuppressor(double slop_suppression_length_dips) + : slop_suppression_length_dips_squared_(slop_suppression_length_dips * + slop_suppression_length_dips), + suppressing_touchmoves_(false) {} + + bool FilterEvent(const WebTouchEvent& event) { + if (WebTouchEventTraits::IsTouchSequenceStart(event)) { + touch_sequence_start_position_ = + gfx::PointF(event.touches[0].position); + suppressing_touchmoves_ = slop_suppression_length_dips_squared_ != 0; + } + + if (event.type == WebInputEvent::TouchEnd || + event.type == WebInputEvent::TouchCancel) + suppressing_touchmoves_ = false; + + if (event.type != WebInputEvent::TouchMove) + return false; + + if (suppressing_touchmoves_) { + // Movement with a secondary pointer should terminate suppression. + if (event.touchesLength > 1) { + suppressing_touchmoves_ = false; + } else if (event.touchesLength == 1) { + // Movement outside of the slop region should terminate suppression. + gfx::PointF position(event.touches[0].position); + if ((position - touch_sequence_start_position_).LengthSquared() > + slop_suppression_length_dips_squared_) + suppressing_touchmoves_ = false; + } + } + return suppressing_touchmoves_; + } + + void ConfirmTouchEvent(InputEventAckState ack_result) { + if (ack_result == INPUT_EVENT_ACK_STATE_CONSUMED) + suppressing_touchmoves_ = false; + } + + bool suppressing_touchmoves() const { return suppressing_touchmoves_; } + + private: + double slop_suppression_length_dips_squared_; + gfx::PointF touch_sequence_start_position_; + bool suppressing_touchmoves_; + + DISALLOW_COPY_AND_ASSIGN(TouchMoveSlopSuppressor); +}; // This class represents a single coalesced touch event. However, it also keeps // track of all the original touch-events that were coalesced into a single @@ -171,25 +248,27 @@ class TouchEventQueue::TouchTimeoutHandler { // the Client receives the event with their original timestamp. class CoalescedWebTouchEvent { public: - CoalescedWebTouchEvent(const TouchEventWithLatencyInfo& event, - bool ignore_ack) - : coalesced_event_(event), - ignore_ack_(ignore_ack) { - events_.push_back(event); - TRACE_EVENT_ASYNC_BEGIN0( - "input", "TouchEventQueue::QueueEvent", this); + // Events for which |async| is true will not be ack'ed to the client after the + // corresponding ack is received following dispatch. + CoalescedWebTouchEvent(const TouchEventWithLatencyInfo& event, bool async) + : coalesced_event_(event) { + if (async) + coalesced_event_.event.cancelable = false; + else + events_to_ack_.push_back(event); + + TRACE_EVENT_ASYNC_BEGIN0("input", "TouchEventQueue::QueueEvent", this); } ~CoalescedWebTouchEvent() { - TRACE_EVENT_ASYNC_END0( - "input", "TouchEventQueue::QueueEvent", this); + TRACE_EVENT_ASYNC_END0("input", "TouchEventQueue::QueueEvent", this); } // Coalesces the event with the existing event if possible. Returns whether // the event was coalesced. bool CoalesceEventIfPossible( const TouchEventWithLatencyInfo& event_with_latency) { - if (ignore_ack_) + if (!WillDispatchAckToClient()) return false; if (!coalesced_event_.CanCoalesceWith(event_with_latency)) @@ -198,48 +277,84 @@ class CoalescedWebTouchEvent { TRACE_EVENT_INSTANT0( "input", "TouchEventQueue::MoveCoalesced", TRACE_EVENT_SCOPE_THREAD); coalesced_event_.CoalesceWith(event_with_latency); - events_.push_back(event_with_latency); + events_to_ack_.push_back(event_with_latency); return true; } - const TouchEventWithLatencyInfo& coalesced_event() const { - return coalesced_event_; - } + void UpdateLatencyInfoForAck(const ui::LatencyInfo& renderer_latency_info) { + if (!WillDispatchAckToClient()) + return; - WebTouchEventWithLatencyList::iterator begin() { - return events_.begin(); + for (WebTouchEventWithLatencyList::iterator iter = events_to_ack_.begin(), + end = events_to_ack_.end(); + iter != end; + ++iter) { + iter->latency.AddNewLatencyFrom(renderer_latency_info); + } } - WebTouchEventWithLatencyList::iterator end() { - return events_.end(); - } + void DispatchAckToClient(InputEventAckState ack_result, + TouchEventQueueClient* client) { + DCHECK(client); + if (!WillDispatchAckToClient()) + return; - size_t size() const { return events_.size(); } + for (WebTouchEventWithLatencyList::const_iterator + iter = events_to_ack_.begin(), + end = events_to_ack_.end(); + iter != end; + ++iter) { + client->OnTouchEventAck(*iter, ack_result); + } + } - bool ignore_ack() const { return ignore_ack_; } + const TouchEventWithLatencyInfo& coalesced_event() const { + return coalesced_event_; + } private: + bool WillDispatchAckToClient() const { return !events_to_ack_.empty(); } + // This is the event that is forwarded to the renderer. TouchEventWithLatencyInfo coalesced_event_; - // This is the list of the original events that were coalesced. - WebTouchEventWithLatencyList events_; - - // If |ignore_ack_| is true, don't send this touch event to client - // when the event is acked. - bool ignore_ack_; + // This is the list of the original events that were coalesced, each requiring + // future ack dispatch to the client. + typedef std::vector<TouchEventWithLatencyInfo> WebTouchEventWithLatencyList; + WebTouchEventWithLatencyList events_to_ack_; DISALLOW_COPY_AND_ASSIGN(CoalescedWebTouchEvent); }; -TouchEventQueue::TouchEventQueue(TouchEventQueueClient* client) +TouchEventQueue::Config::Config() + : touchmove_slop_suppression_length_dips(0), + touchmove_slop_suppression_region_includes_boundary(true), + touch_scrolling_mode(TOUCH_SCROLLING_MODE_DEFAULT), + touch_ack_timeout_delay(base::TimeDelta::FromMilliseconds(200)), + touch_ack_timeout_supported(false) { +} + +TouchEventQueue::TouchEventQueue(TouchEventQueueClient* client, + const Config& config) : client_(client), dispatching_touch_ack_(NULL), dispatching_touch_(false), - no_touch_to_renderer_(false), - renderer_is_consuming_touch_gesture_(false), - ack_timeout_enabled_(false) { + touch_filtering_state_(TOUCH_FILTERING_STATE_DEFAULT), + ack_timeout_enabled_(config.touch_ack_timeout_supported), + touchmove_slop_suppressor_(new TouchMoveSlopSuppressor( + config.touchmove_slop_suppression_length_dips + + (config.touchmove_slop_suppression_region_includes_boundary + ? kSlopEpsilon + : -kSlopEpsilon))), + send_touch_events_async_(false), + needs_async_touchmove_for_outer_slop_region_(false), + last_sent_touch_timestamp_sec_(0), + touch_scrolling_mode_(config.touch_scrolling_mode) { DCHECK(client); + if (ack_timeout_enabled_) { + timeout_handler_.reset( + new TouchTimeoutHandler(this, config.touch_ack_timeout_delay)); + } } TouchEventQueue::~TouchEventQueue() { @@ -248,13 +363,26 @@ TouchEventQueue::~TouchEventQueue() { } void TouchEventQueue::QueueEvent(const TouchEventWithLatencyInfo& event) { + TRACE_EVENT0("input", "TouchEventQueue::QueueEvent"); + // If the queueing of |event| was triggered by an ack dispatch, defer // processing the event until the dispatch has finished. if (touch_queue_.empty() && !dispatching_touch_ack_) { + // Optimization of the case without touch handlers. Removing this path + // yields identical results, but this avoids unnecessary allocations. + PreFilterResult filter_result = FilterBeforeForwarding(event.event); + if (filter_result != FORWARD_TO_RENDERER) { + client_->OnTouchEventAck(event, + filter_result == ACK_WITH_NO_CONSUMER_EXISTS + ? INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS + : INPUT_EVENT_ACK_STATE_NOT_CONSUMED); + return; + } + // There is no touch event in the queue. Forward it to the renderer // immediately. touch_queue_.push_back(new CoalescedWebTouchEvent(event, false)); - TryForwardNextEventToRenderer(); + ForwardNextEventToRenderer(); return; } @@ -269,22 +397,25 @@ void TouchEventQueue::QueueEvent(const TouchEventWithLatencyInfo& event) { } void TouchEventQueue::ProcessTouchAck(InputEventAckState ack_result, - const ui::LatencyInfo& latency_info) { + const LatencyInfo& latency_info) { + TRACE_EVENT0("input", "TouchEventQueue::ProcessTouchAck"); + DCHECK(!dispatching_touch_ack_); dispatching_touch_ = false; if (timeout_handler_ && timeout_handler_->ConfirmTouchEvent(ack_result)) return; + touchmove_slop_suppressor_->ConfirmTouchEvent(ack_result); + if (touch_queue_.empty()) return; - if (ack_result == INPUT_EVENT_ACK_STATE_CONSUMED) - renderer_is_consuming_touch_gesture_ = true; + if (ack_result == INPUT_EVENT_ACK_STATE_CONSUMED && + touch_filtering_state_ == FORWARD_TOUCHES_UNTIL_TIMEOUT) { + touch_filtering_state_ = FORWARD_ALL_TOUCHES; + } - const WebTouchEvent& acked_event = - touch_queue_.front()->coalesced_event().event; - UpdateTouchAckStates(acked_event, ack_result); PopTouchEventToClient(ack_result, latency_info); TryForwardNextEventToRenderer(); } @@ -294,29 +425,108 @@ void TouchEventQueue::TryForwardNextEventToRenderer() { // If there are queued touch events, then try to forward them to the renderer // immediately, or ACK the events back to the client if appropriate. while (!touch_queue_.empty()) { - const TouchEventWithLatencyInfo& touch = - touch_queue_.front()->coalesced_event(); - if (IsNewTouchGesture(touch.event)) - renderer_is_consuming_touch_gesture_ = false; - if (ShouldForwardToRenderer(touch.event)) { - ForwardToRenderer(touch); - break; + PreFilterResult filter_result = + FilterBeforeForwarding(touch_queue_.front()->coalesced_event().event); + switch (filter_result) { + case ACK_WITH_NO_CONSUMER_EXISTS: + PopTouchEventToClient(INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS); + break; + case ACK_WITH_NOT_CONSUMED: + PopTouchEventToClient(INPUT_EVENT_ACK_STATE_NOT_CONSUMED); + break; + case FORWARD_TO_RENDERER: + ForwardNextEventToRenderer(); + return; } - PopTouchEventToClient(kDefaultNotForwardedAck, ui::LatencyInfo()); } } -void TouchEventQueue::ForwardToRenderer( - const TouchEventWithLatencyInfo& touch) { +void TouchEventQueue::ForwardNextEventToRenderer() { + TRACE_EVENT0("input", "TouchEventQueue::ForwardNextEventToRenderer"); + + DCHECK(!empty()); DCHECK(!dispatching_touch_); + DCHECK_NE(touch_filtering_state_, DROP_ALL_TOUCHES); + TouchEventWithLatencyInfo touch = touch_queue_.front()->coalesced_event(); + + if (WebTouchEventTraits::IsTouchSequenceStart(touch.event)) { + touch_filtering_state_ = + ack_timeout_enabled_ ? FORWARD_TOUCHES_UNTIL_TIMEOUT + : FORWARD_ALL_TOUCHES; + touch_ack_states_.clear(); + send_touch_events_async_ = false; + touch_sequence_start_position_ = + gfx::PointF(touch.event.touches[0].position); + } + + if (send_touch_events_async_ && + touch.event.type == WebInputEvent::TouchMove) { + // Throttling touchmove's in a continuous touchmove stream while scrolling + // reduces the risk of jank. However, it's still important that the web + // application be sent touches at key points in the gesture stream, + // e.g., when the application slop region is exceeded or touchmove + // coalescing fails because of different modifiers. + const bool send_touchmove_now = + size() > 1 || + (touch.event.timeStampSeconds >= + last_sent_touch_timestamp_sec_ + kAsyncTouchMoveIntervalSec) || + (needs_async_touchmove_for_outer_slop_region_ && + OutsideApplicationSlopRegion(touch.event, + touch_sequence_start_position_)) || + (pending_async_touchmove_ && + !pending_async_touchmove_->CanCoalesceWith(touch)); + + if (!send_touchmove_now) { + if (!pending_async_touchmove_) { + pending_async_touchmove_.reset(new TouchEventWithLatencyInfo(touch)); + } else { + DCHECK(pending_async_touchmove_->CanCoalesceWith(touch)); + pending_async_touchmove_->CoalesceWith(touch); + } + DCHECK_EQ(1U, size()); + PopTouchEventToClient(INPUT_EVENT_ACK_STATE_NOT_CONSUMED); + // It's possible (though unlikely) that ack'ing the current touch will + // trigger the queueing of another touch event (e.g., a touchcancel). As + // forwarding of the queued event will be deferred while the ack is being + // dispatched (see |OnTouchEvent()|), try forwarding it now. + TryForwardNextEventToRenderer(); + return; + } + } + + last_sent_touch_timestamp_sec_ = touch.event.timeStampSeconds; + + // Flush any pending async touch move. If it can be combined with the current + // (touchmove) event, great, otherwise send it immediately but separately. Its + // ack will trigger forwarding of the original |touch| event. + if (pending_async_touchmove_) { + if (pending_async_touchmove_->CanCoalesceWith(touch)) { + pending_async_touchmove_->CoalesceWith(touch); + pending_async_touchmove_->event.cancelable = !send_touch_events_async_; + touch = *pending_async_touchmove_.Pass(); + } else { + scoped_ptr<TouchEventWithLatencyInfo> async_move = + pending_async_touchmove_.Pass(); + async_move->event.cancelable = false; + touch_queue_.push_front(new CoalescedWebTouchEvent(*async_move, true)); + SendTouchEventImmediately(*async_move); + return; + } + } + + // Note: Marking touchstart events as not-cancelable prevents them from + // blocking subsequent gestures, but it may not be the best long term solution + // for tracking touch point dispatch. + if (send_touch_events_async_) + touch.event.cancelable = false; + // A synchronous ack will reset |dispatching_touch_|, in which case // the touch timeout should not be started. base::AutoReset<bool> dispatching_touch(&dispatching_touch_, true); - client_->SendTouchEventImmediately(touch); - if (ack_timeout_enabled_ && - dispatching_touch_ && - !renderer_is_consuming_touch_gesture_ && - ShouldTouchTypeTriggerTimeout(touch.event.type)) { + SendTouchEventImmediately(touch); + if (dispatching_touch_ && + touch_filtering_state_ == FORWARD_TOUCHES_UNTIL_TIMEOUT && + ShouldTouchTriggerTimeout(touch.event)) { DCHECK(timeout_handler_); timeout_handler_->Start(touch); } @@ -324,41 +534,107 @@ void TouchEventQueue::ForwardToRenderer( void TouchEventQueue::OnGestureScrollEvent( const GestureEventWithLatencyInfo& gesture_event) { - blink::WebInputEvent::Type type = gesture_event.event.type; - if (type == blink::WebInputEvent::GestureScrollBegin) { - // We assume the scroll event are generated synchronously from - // dispatching a touch event ack, so that we can fake a cancel - // event that has the correct touch ids as the touch event that - // is being acked. If not, we don't do the touch-cancel optimization. - if (no_touch_to_renderer_ || !dispatching_touch_ack_) - return; - no_touch_to_renderer_ = true; + if (gesture_event.event.type != blink::WebInputEvent::GestureScrollBegin) + return; - // If we have a timeout event, a cancel has already been dispatched - // for the current touch stream. - if (HasTimeoutEvent()) - return; + if (touch_filtering_state_ != DROP_ALL_TOUCHES && + touch_filtering_state_ != DROP_TOUCHES_IN_SEQUENCE) { + DCHECK(!touchmove_slop_suppressor_->suppressing_touchmoves()) + << "The renderer should be offered a touchmove before scrolling begins"; + } + + if (touch_scrolling_mode_ == TOUCH_SCROLLING_MODE_ASYNC_TOUCHMOVE) { + if (touch_filtering_state_ != DROP_ALL_TOUCHES && + touch_filtering_state_ != DROP_TOUCHES_IN_SEQUENCE) { + // If no touch points have a consumer, prevent all subsequent touch events + // received during the scroll from reaching the renderer. This ensures + // that the first touchstart the renderer sees in any given sequence can + // always be preventDefault'ed (cancelable == true). + // TODO(jdduke): Revisit if touchstarts during scroll are made cancelable. + if (touch_ack_states_.empty() || + AllTouchAckStatesHaveState( + INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS)) { + touch_filtering_state_ = DROP_TOUCHES_IN_SEQUENCE; + return; + } + } - // Fake a TouchCancel to cancel the touch points of the touch event - // that is currently being acked. - // Note: |dispatching_touch_ack_| is non-null when we reach here, meaning we - // are in the scope of PopTouchEventToClient() and that no touch event - // in the queue is waiting for ack from renderer. So we can just insert - // the touch cancel at the beginning of the queue. - touch_queue_.push_front(new CoalescedWebTouchEvent( - ObtainCancelEventForTouchEvent( - dispatching_touch_ack_->coalesced_event()), true)); - } else if (type == blink::WebInputEvent::GestureScrollEnd || - type == blink::WebInputEvent::GestureFlingStart) { - no_touch_to_renderer_ = false; + pending_async_touchmove_.reset(); + send_touch_events_async_ = true; + needs_async_touchmove_for_outer_slop_region_ = true; + return; } + + if (touch_scrolling_mode_ != TOUCH_SCROLLING_MODE_TOUCHCANCEL) + return; + + // We assume that scroll events are generated synchronously from + // dispatching a touch event ack. This allows us to generate a synthetic + // cancel event that has the same touch ids as the touch event that + // is being acked. Otherwise, we don't perform the touch-cancel optimization. + if (!dispatching_touch_ack_) + return; + + if (touch_filtering_state_ == DROP_TOUCHES_IN_SEQUENCE) + return; + + touch_filtering_state_ = DROP_TOUCHES_IN_SEQUENCE; + + // Fake a TouchCancel to cancel the touch points of the touch event + // that is currently being acked. + // Note: |dispatching_touch_ack_| is non-null when we reach here, meaning we + // are in the scope of PopTouchEventToClient() and that no touch event + // in the queue is waiting for ack from renderer. So we can just insert + // the touch cancel at the beginning of the queue. + touch_queue_.push_front(new CoalescedWebTouchEvent( + ObtainCancelEventForTouchEvent( + dispatching_touch_ack_->coalesced_event()), true)); } -void TouchEventQueue::FlushQueue() { +void TouchEventQueue::OnGestureEventAck( + const GestureEventWithLatencyInfo& event, + InputEventAckState ack_result) { + if (touch_scrolling_mode_ != TOUCH_SCROLLING_MODE_ASYNC_TOUCHMOVE) + return; + + if (event.event.type != blink::WebInputEvent::GestureScrollUpdate) + return; + + // Throttle sending touchmove events as long as the scroll events are handled. + // Note that there's no guarantee that this ACK is for the most recent + // gesture event (or even part of the current sequence). Worst case, the + // delay in updating the absorption state will result in minor UI glitches. + // A valid |pending_async_touchmove_| will be flushed when the next event is + // forwarded. + send_touch_events_async_ = (ack_result == INPUT_EVENT_ACK_STATE_CONSUMED); + if (!send_touch_events_async_) + needs_async_touchmove_for_outer_slop_region_ = false; +} + +void TouchEventQueue::OnHasTouchEventHandlers(bool has_handlers) { DCHECK(!dispatching_touch_ack_); DCHECK(!dispatching_touch_); - while (!touch_queue_.empty()) - PopTouchEventToClient(kDefaultNotForwardedAck, ui::LatencyInfo()); + + if (has_handlers) { + if (touch_filtering_state_ == DROP_ALL_TOUCHES) { + // If no touch handler was previously registered, ensure that we don't + // send a partial touch sequence to the renderer. + DCHECK(touch_queue_.empty()); + touch_filtering_state_ = DROP_TOUCHES_IN_SEQUENCE; + } + } else { + // TODO(jdduke): Synthesize a TouchCancel if necessary to update Blink touch + // state tracking and/or touch-action filtering (e.g., if the touch handler + // was removed mid-sequence), crbug.com/375940. + touch_filtering_state_ = DROP_ALL_TOUCHES; + pending_async_touchmove_.reset(); + if (timeout_handler_) + timeout_handler_->Reset(); + if (!touch_queue_.empty()) + ProcessTouchAck(INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS, LatencyInfo()); + // As there is no touch handler, ack'ing the event should flush the queue. + DCHECK(touch_queue_.empty()); + } } bool TouchEventQueue::IsPendingAckTouchStart() const { @@ -371,22 +647,30 @@ bool TouchEventQueue::IsPendingAckTouchStart() const { return (event.type == WebInputEvent::TouchStart); } -void TouchEventQueue::SetAckTimeoutEnabled(bool enabled, - size_t ack_timeout_delay_ms) { - if (!enabled) { - // Avoid resetting |timeout_handler_|, as an outstanding timeout may - // be active and must be completed for ack handling consistency. - ack_timeout_enabled_ = false; +void TouchEventQueue::SetAckTimeoutEnabled(bool enabled) { + // The timeout handler is valid only if explicitly supported in the config. + if (!timeout_handler_) + return; + + if (ack_timeout_enabled_ == enabled) return; - } - ack_timeout_enabled_ = true; - if (!timeout_handler_) - timeout_handler_.reset(new TouchTimeoutHandler(this, ack_timeout_delay_ms)); + ack_timeout_enabled_ = enabled; + + if (enabled) + return; + + if (touch_filtering_state_ == FORWARD_TOUCHES_UNTIL_TIMEOUT) + touch_filtering_state_ = FORWARD_ALL_TOUCHES; + // Only reset the |timeout_handler_| if the timer is running and has not yet + // timed out. This ensures that an already timed out sequence is properly + // flushed by the handler. + if (timeout_handler_ && timeout_handler_->IsTimeoutTimerRunning()) + timeout_handler_->Reset(); } -bool TouchEventQueue::HasTimeoutEvent() const { - return timeout_handler_ && timeout_handler_->HasTimeoutEvent(); +bool TouchEventQueue::HasPendingAsyncTouchMoveForTesting() const { + return pending_async_touchmove_; } bool TouchEventQueue::IsTimeoutRunningForTesting() const { @@ -398,43 +682,85 @@ TouchEventQueue::GetLatestEventForTesting() const { return touch_queue_.back()->coalesced_event(); } +void TouchEventQueue::FlushQueue() { + DCHECK(!dispatching_touch_ack_); + DCHECK(!dispatching_touch_); + pending_async_touchmove_.reset(); + if (touch_filtering_state_ != DROP_ALL_TOUCHES) + touch_filtering_state_ = DROP_TOUCHES_IN_SEQUENCE; + while (!touch_queue_.empty()) + PopTouchEventToClient(INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS); +} + +void TouchEventQueue::PopTouchEventToClient(InputEventAckState ack_result) { + AckTouchEventToClient(ack_result, PopTouchEvent()); +} + void TouchEventQueue::PopTouchEventToClient( InputEventAckState ack_result, - const ui::LatencyInfo& renderer_latency_info) { - DCHECK(!dispatching_touch_ack_); - if (touch_queue_.empty()) - return; - scoped_ptr<CoalescedWebTouchEvent> acked_event(touch_queue_.front()); - touch_queue_.pop_front(); + const LatencyInfo& renderer_latency_info) { + scoped_ptr<CoalescedWebTouchEvent> acked_event = PopTouchEvent(); + acked_event->UpdateLatencyInfoForAck(renderer_latency_info); + AckTouchEventToClient(ack_result, acked_event.Pass()); +} - if (acked_event->ignore_ack()) - return; +void TouchEventQueue::AckTouchEventToClient( + InputEventAckState ack_result, + scoped_ptr<CoalescedWebTouchEvent> acked_event) { + DCHECK(acked_event); + DCHECK(!dispatching_touch_ack_); + UpdateTouchAckStates(acked_event->coalesced_event().event, ack_result); // Note that acking the touch-event may result in multiple gestures being sent // to the renderer, or touch-events being queued. - base::AutoReset<CoalescedWebTouchEvent*> - dispatching_touch_ack(&dispatching_touch_ack_, acked_event.get()); + base::AutoReset<const CoalescedWebTouchEvent*> dispatching_touch_ack( + &dispatching_touch_ack_, acked_event.get()); + acked_event->DispatchAckToClient(ack_result, client_); +} - for (WebTouchEventWithLatencyList::iterator iter = acked_event->begin(), - end = acked_event->end(); - iter != end; ++iter) { - iter->latency.AddNewLatencyFrom(renderer_latency_info); - client_->OnTouchEventAck((*iter), ack_result); +scoped_ptr<CoalescedWebTouchEvent> TouchEventQueue::PopTouchEvent() { + DCHECK(!touch_queue_.empty()); + scoped_ptr<CoalescedWebTouchEvent> event(touch_queue_.front()); + touch_queue_.pop_front(); + return event.Pass(); +} + +void TouchEventQueue::SendTouchEventImmediately( + const TouchEventWithLatencyInfo& touch) { + if (needs_async_touchmove_for_outer_slop_region_) { + // Any event other than a touchmove (e.g., touchcancel or secondary + // touchstart) after a scroll has started will interrupt the need to send a + // an outer slop-region exceeding touchmove. + if (touch.event.type != WebInputEvent::TouchMove || + OutsideApplicationSlopRegion(touch.event, + touch_sequence_start_position_)) + needs_async_touchmove_for_outer_slop_region_ = false; } + + client_->SendTouchEventImmediately(touch); } -bool TouchEventQueue::ShouldForwardToRenderer( - const WebTouchEvent& event) const { - if (HasTimeoutEvent()) - return false; +TouchEventQueue::PreFilterResult +TouchEventQueue::FilterBeforeForwarding(const WebTouchEvent& event) { + if (timeout_handler_ && timeout_handler_->FilterEvent(event)) + return ACK_WITH_NO_CONSUMER_EXISTS; - if (no_touch_to_renderer_ && - event.type != blink::WebInputEvent::TouchCancel) - return false; + if (touchmove_slop_suppressor_->FilterEvent(event)) + return ACK_WITH_NOT_CONSUMED; + + if (touch_filtering_state_ == DROP_ALL_TOUCHES) + return ACK_WITH_NO_CONSUMER_EXISTS; + + if (touch_filtering_state_ == DROP_TOUCHES_IN_SEQUENCE && + event.type != WebInputEvent::TouchCancel) { + if (WebTouchEventTraits::IsTouchSequenceStart(event)) + return FORWARD_TO_RENDERER; + return ACK_WITH_NO_CONSUMER_EXISTS; + } // Touch press events should always be forwarded to the renderer. if (event.type == WebInputEvent::TouchStart) - return true; + return FORWARD_TO_RENDERER; for (unsigned int i = 0; i < event.touchesLength; ++i) { const WebTouchPoint& point = event.touches[i]; @@ -445,15 +771,15 @@ bool TouchEventQueue::ShouldForwardToRenderer( if (touch_ack_states_.count(point.id) > 0) { if (touch_ack_states_.find(point.id)->second != INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS) - return true; + return FORWARD_TO_RENDERER; } else { // If the ACK status of a point is unknown, then the event should be // forwarded to the renderer. - return true; + return FORWARD_TO_RENDERER; } } - return false; + return ACK_WITH_NO_CONSUMER_EXISTS; } void TouchEventQueue::UpdateTouchAckStates(const WebTouchEvent& event, @@ -477,4 +803,20 @@ void TouchEventQueue::UpdateTouchAckStates(const WebTouchEvent& event, } } +bool TouchEventQueue::AllTouchAckStatesHaveState( + InputEventAckState ack_state) const { + if (touch_ack_states_.empty()) + return false; + + for (TouchPointAckStates::const_iterator iter = touch_ack_states_.begin(), + end = touch_ack_states_.end(); + iter != end; + ++iter) { + if (iter->second != ack_state) + return false; + } + + return true; +} + } // namespace content |