| OLD | NEW |
| 1 // Copyright 2013 The Chromium Authors. All rights reserved. | 1 // Copyright 2013 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #include "content/browser/renderer_host/input/touch_event_queue.h" | 5 #include "content/browser/renderer_host/input/touch_event_queue.h" |
| 6 | 6 |
| 7 #include "base/auto_reset.h" | 7 #include "base/auto_reset.h" |
| 8 #include "base/command_line.h" | 8 #include "base/command_line.h" |
| 9 #include "base/debug/trace_event.h" | 9 #include "base/debug/trace_event.h" |
| 10 #include "base/stl_util.h" | 10 #include "base/stl_util.h" |
| 11 #include "content/browser/renderer_host/input/timeout_monitor.h" | 11 #include "content/browser/renderer_host/input/timeout_monitor.h" |
| 12 #include "content/common/input/web_touch_event_traits.h" | 12 #include "content/common/input/web_touch_event_traits.h" |
| 13 #include "content/public/common/content_switches.h" | 13 #include "content/public/common/content_switches.h" |
| 14 #include "ui/gfx/geometry/point_f.h" | 14 #include "ui/gfx/geometry/point_f.h" |
| 15 | 15 |
| 16 using blink::WebInputEvent; | 16 using blink::WebInputEvent; |
| 17 using blink::WebTouchEvent; | 17 using blink::WebTouchEvent; |
| 18 using blink::WebTouchPoint; | 18 using blink::WebTouchPoint; |
| 19 using ui::LatencyInfo; | 19 using ui::LatencyInfo; |
| 20 | 20 |
| 21 namespace content { | 21 namespace content { |
| 22 namespace { | 22 namespace { |
| 23 | 23 |
| 24 // Time interval at which touchmove events will be forwarded to the client while | |
| 25 // scrolling is active and possible. | |
| 26 const double kAsyncTouchMoveIntervalSec = .2; | |
| 27 | |
| 28 // A slop region just larger than that used by many web applications. When | |
| 29 // touchmove's are being sent asynchronously, movement outside this region will | |
| 30 // trigger an immediate async touchmove to cancel potential tap-related logic. | |
| 31 const double kApplicationSlopRegionLengthDipsSqared = 15. * 15.; | |
| 32 | |
| 33 // Using a small epsilon when comparing slop distances allows pixel perfect | 24 // Using a small epsilon when comparing slop distances allows pixel perfect |
| 34 // slop determination when using fractional DIP coordinates (assuming the slop | 25 // slop determination when using fractional DIP coordinates (assuming the slop |
| 35 // region and DPI scale are reasonably proportioned). | 26 // region and DPI scale are reasonably proportioned). |
| 36 const float kSlopEpsilon = .05f; | 27 const float kSlopEpsilon = .05f; |
| 37 | 28 |
| 38 typedef std::vector<TouchEventWithLatencyInfo> WebTouchEventWithLatencyList; | 29 typedef std::vector<TouchEventWithLatencyInfo> WebTouchEventWithLatencyList; |
| 39 | 30 |
| 40 TouchEventWithLatencyInfo ObtainCancelEventForTouchEvent( | 31 TouchEventWithLatencyInfo ObtainCancelEventForTouchEvent( |
| 41 const TouchEventWithLatencyInfo& event_to_cancel) { | 32 const TouchEventWithLatencyInfo& event_to_cancel) { |
| 42 TouchEventWithLatencyInfo event = event_to_cancel; | 33 TouchEventWithLatencyInfo event = event_to_cancel; |
| 43 WebTouchEventTraits::ResetTypeAndTouchStates( | 34 WebTouchEventTraits::ResetTypeAndTouchStates( |
| 44 WebInputEvent::TouchCancel, | 35 WebInputEvent::TouchCancel, |
| 45 // TODO(rbyers): Shouldn't we use a fresh timestamp? | 36 // TODO(rbyers): Shouldn't we use a fresh timestamp? |
| 46 event.event.timeStampSeconds, | 37 event.event.timeStampSeconds, |
| 47 &event.event); | 38 &event.event); |
| 48 return event; | 39 return event; |
| 49 } | 40 } |
| 50 | 41 |
| 51 bool ShouldTouchTriggerTimeout(const WebTouchEvent& event) { | 42 bool ShouldTouchTypeTriggerTimeout(WebInputEvent::Type type) { |
| 52 return (event.type == WebInputEvent::TouchStart || | 43 return type == WebInputEvent::TouchStart || |
| 53 event.type == WebInputEvent::TouchMove) && | 44 type == WebInputEvent::TouchMove; |
| 54 !WebInputEventTraits::IgnoresAckDisposition(event); | |
| 55 } | |
| 56 | |
| 57 bool OutsideApplicationSlopRegion(const WebTouchEvent& event, | |
| 58 const gfx::PointF& anchor) { | |
| 59 return (gfx::PointF(event.touches[0].position) - anchor).LengthSquared() > | |
| 60 kApplicationSlopRegionLengthDipsSqared; | |
| 61 } | 45 } |
| 62 | 46 |
| 63 } // namespace | 47 } // namespace |
| 64 | 48 |
| 65 | 49 |
| 66 // Cancels a touch sequence if a touchstart or touchmove ack response is | 50 // Cancels a touch sequence if a touchstart or touchmove ack response is |
| 67 // sufficiently delayed. | 51 // sufficiently delayed. |
| 68 class TouchEventQueue::TouchTimeoutHandler { | 52 class TouchEventQueue::TouchTimeoutHandler { |
| 69 public: | 53 public: |
| 70 TouchTimeoutHandler(TouchEventQueue* touch_queue, | 54 TouchTimeoutHandler(TouchEventQueue* touch_queue, |
| 71 base::TimeDelta timeout_delay) | 55 base::TimeDelta timeout_delay) |
| 72 : touch_queue_(touch_queue), | 56 : touch_queue_(touch_queue), |
| 73 timeout_delay_(timeout_delay), | 57 timeout_delay_(timeout_delay), |
| 74 pending_ack_state_(PENDING_ACK_NONE), | 58 pending_ack_state_(PENDING_ACK_NONE), |
| 75 timeout_monitor_(base::Bind(&TouchTimeoutHandler::OnTimeOut, | 59 timeout_monitor_(base::Bind(&TouchTimeoutHandler::OnTimeOut, |
| 76 base::Unretained(this))) {} | 60 base::Unretained(this))) {} |
| 77 | 61 |
| 78 ~TouchTimeoutHandler() {} | 62 ~TouchTimeoutHandler() {} |
| 79 | 63 |
| 80 void Start(const TouchEventWithLatencyInfo& event) { | 64 void Start(const TouchEventWithLatencyInfo& event) { |
| 81 DCHECK_EQ(pending_ack_state_, PENDING_ACK_NONE); | 65 DCHECK_EQ(pending_ack_state_, PENDING_ACK_NONE); |
| 82 DCHECK(ShouldTouchTriggerTimeout(event.event)); | 66 DCHECK(ShouldTouchTypeTriggerTimeout(event.event.type)); |
| 83 timeout_event_ = event; | 67 timeout_event_ = event; |
| 84 timeout_monitor_.Restart(timeout_delay_); | 68 timeout_monitor_.Restart(timeout_delay_); |
| 85 } | 69 } |
| 86 | 70 |
| 87 bool ConfirmTouchEvent(InputEventAckState ack_result) { | 71 bool ConfirmTouchEvent(InputEventAckState ack_result) { |
| 88 switch (pending_ack_state_) { | 72 switch (pending_ack_state_) { |
| 89 case PENDING_ACK_NONE: | 73 case PENDING_ACK_NONE: |
| 90 timeout_monitor_.Stop(); | 74 timeout_monitor_.Stop(); |
| 91 return false; | 75 return false; |
| 92 case PENDING_ACK_ORIGINAL_EVENT: | 76 case PENDING_ACK_ORIGINAL_EVENT: |
| 93 if (AckedTimeoutEventRequiresCancel(ack_result)) { | 77 if (AckedTimeoutEventRequiresCancel(ack_result)) { |
| 94 SetPendingAckState(PENDING_ACK_CANCEL_EVENT); | 78 SetPendingAckState(PENDING_ACK_CANCEL_EVENT); |
| 95 TouchEventWithLatencyInfo cancel_event = | 79 TouchEventWithLatencyInfo cancel_event = |
| 96 ObtainCancelEventForTouchEvent(timeout_event_); | 80 ObtainCancelEventForTouchEvent(timeout_event_); |
| 97 touch_queue_->SendTouchEventImmediately(cancel_event); | 81 touch_queue_->client_->SendTouchEventImmediately(cancel_event); |
| 98 } else { | 82 } else { |
| 99 SetPendingAckState(PENDING_ACK_NONE); | 83 SetPendingAckState(PENDING_ACK_NONE); |
| 100 touch_queue_->UpdateTouchAckStates(timeout_event_.event, ack_result); | 84 touch_queue_->UpdateTouchAckStates(timeout_event_.event, ack_result); |
| 101 } | 85 } |
| 102 return true; | 86 return true; |
| 103 case PENDING_ACK_CANCEL_EVENT: | 87 case PENDING_ACK_CANCEL_EVENT: |
| 104 SetPendingAckState(PENDING_ACK_NONE); | 88 SetPendingAckState(PENDING_ACK_NONE); |
| 105 return true; | 89 return true; |
| 106 } | 90 } |
| 107 return false; | 91 return false; |
| (...skipping 129 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 237 DISALLOW_COPY_AND_ASSIGN(TouchMoveSlopSuppressor); | 221 DISALLOW_COPY_AND_ASSIGN(TouchMoveSlopSuppressor); |
| 238 }; | 222 }; |
| 239 | 223 |
| 240 // This class represents a single coalesced touch event. However, it also keeps | 224 // This class represents a single coalesced touch event. However, it also keeps |
| 241 // track of all the original touch-events that were coalesced into a single | 225 // track of all the original touch-events that were coalesced into a single |
| 242 // event. The coalesced event is forwarded to the renderer, while the original | 226 // event. The coalesced event is forwarded to the renderer, while the original |
| 243 // touch-events are sent to the Client (on ACK for the coalesced event) so that | 227 // touch-events are sent to the Client (on ACK for the coalesced event) so that |
| 244 // the Client receives the event with their original timestamp. | 228 // the Client receives the event with their original timestamp. |
| 245 class CoalescedWebTouchEvent { | 229 class CoalescedWebTouchEvent { |
| 246 public: | 230 public: |
| 247 // Events for which |async| is true will not be ack'ed to the client after the | 231 CoalescedWebTouchEvent(const TouchEventWithLatencyInfo& event, |
| 248 // corresponding ack is received following dispatch. | 232 bool ignore_ack) |
| 249 CoalescedWebTouchEvent(const TouchEventWithLatencyInfo& event, bool async) | 233 : coalesced_event_(event), |
| 250 : coalesced_event_(event) { | 234 ignore_ack_(ignore_ack) { |
| 251 if (async) | 235 events_.push_back(event); |
| 252 coalesced_event_.event.cancelable = false; | 236 TRACE_EVENT_ASYNC_BEGIN0( |
| 253 else | 237 "input", "TouchEventQueue::QueueEvent", this); |
| 254 events_to_ack_.push_back(event); | |
| 255 | |
| 256 TRACE_EVENT_ASYNC_BEGIN0("input", "TouchEventQueue::QueueEvent", this); | |
| 257 } | 238 } |
| 258 | 239 |
| 259 ~CoalescedWebTouchEvent() { | 240 ~CoalescedWebTouchEvent() { |
| 260 TRACE_EVENT_ASYNC_END0("input", "TouchEventQueue::QueueEvent", this); | 241 TRACE_EVENT_ASYNC_END0( |
| 242 "input", "TouchEventQueue::QueueEvent", this); |
| 261 } | 243 } |
| 262 | 244 |
| 263 // Coalesces the event with the existing event if possible. Returns whether | 245 // Coalesces the event with the existing event if possible. Returns whether |
| 264 // the event was coalesced. | 246 // the event was coalesced. |
| 265 bool CoalesceEventIfPossible( | 247 bool CoalesceEventIfPossible( |
| 266 const TouchEventWithLatencyInfo& event_with_latency) { | 248 const TouchEventWithLatencyInfo& event_with_latency) { |
| 267 if (!WillDispatchAckToClient()) | 249 if (ignore_ack_) |
| 268 return false; | 250 return false; |
| 269 | 251 |
| 270 if (!coalesced_event_.CanCoalesceWith(event_with_latency)) | 252 if (!coalesced_event_.CanCoalesceWith(event_with_latency)) |
| 271 return false; | 253 return false; |
| 272 | 254 |
| 273 TRACE_EVENT_INSTANT0( | 255 TRACE_EVENT_INSTANT0( |
| 274 "input", "TouchEventQueue::MoveCoalesced", TRACE_EVENT_SCOPE_THREAD); | 256 "input", "TouchEventQueue::MoveCoalesced", TRACE_EVENT_SCOPE_THREAD); |
| 275 coalesced_event_.CoalesceWith(event_with_latency); | 257 coalesced_event_.CoalesceWith(event_with_latency); |
| 276 events_to_ack_.push_back(event_with_latency); | 258 events_.push_back(event_with_latency); |
| 277 return true; | 259 return true; |
| 278 } | 260 } |
| 279 | 261 |
| 280 void UpdateLatencyInfoForAck(const ui::LatencyInfo& renderer_latency_info) { | |
| 281 if (!WillDispatchAckToClient()) | |
| 282 return; | |
| 283 | |
| 284 for (WebTouchEventWithLatencyList::iterator iter = events_to_ack_.begin(), | |
| 285 end = events_to_ack_.end(); | |
| 286 iter != end; | |
| 287 ++iter) { | |
| 288 iter->latency.AddNewLatencyFrom(renderer_latency_info); | |
| 289 } | |
| 290 } | |
| 291 | |
| 292 void DispatchAckToClient(InputEventAckState ack_result, | |
| 293 TouchEventQueueClient* client) { | |
| 294 DCHECK(client); | |
| 295 if (!WillDispatchAckToClient()) | |
| 296 return; | |
| 297 | |
| 298 for (WebTouchEventWithLatencyList::const_iterator | |
| 299 iter = events_to_ack_.begin(), | |
| 300 end = events_to_ack_.end(); | |
| 301 iter != end; | |
| 302 ++iter) { | |
| 303 client->OnTouchEventAck(*iter, ack_result); | |
| 304 } | |
| 305 } | |
| 306 | |
| 307 const TouchEventWithLatencyInfo& coalesced_event() const { | 262 const TouchEventWithLatencyInfo& coalesced_event() const { |
| 308 return coalesced_event_; | 263 return coalesced_event_; |
| 309 } | 264 } |
| 310 | 265 |
| 266 WebTouchEventWithLatencyList::iterator begin() { |
| 267 return events_.begin(); |
| 268 } |
| 269 |
| 270 WebTouchEventWithLatencyList::iterator end() { |
| 271 return events_.end(); |
| 272 } |
| 273 |
| 274 size_t size() const { return events_.size(); } |
| 275 |
| 276 bool ignore_ack() const { return ignore_ack_; } |
| 277 |
| 311 private: | 278 private: |
| 312 bool WillDispatchAckToClient() const { return !events_to_ack_.empty(); } | |
| 313 | |
| 314 // This is the event that is forwarded to the renderer. | 279 // This is the event that is forwarded to the renderer. |
| 315 TouchEventWithLatencyInfo coalesced_event_; | 280 TouchEventWithLatencyInfo coalesced_event_; |
| 316 | 281 |
| 317 // This is the list of the original events that were coalesced, each requiring | 282 // This is the list of the original events that were coalesced. |
| 318 // future ack dispatch to the client. | 283 WebTouchEventWithLatencyList events_; |
| 319 WebTouchEventWithLatencyList events_to_ack_; | 284 |
| 285 // If |ignore_ack_| is true, don't send this touch event to client |
| 286 // when the event is acked. |
| 287 bool ignore_ack_; |
| 320 | 288 |
| 321 DISALLOW_COPY_AND_ASSIGN(CoalescedWebTouchEvent); | 289 DISALLOW_COPY_AND_ASSIGN(CoalescedWebTouchEvent); |
| 322 }; | 290 }; |
| 323 | 291 |
| 324 TouchEventQueue::TouchEventQueue(TouchEventQueueClient* client, | 292 TouchEventQueue::TouchEventQueue(TouchEventQueueClient* client, |
| 325 TouchScrollingMode mode, | 293 TouchScrollingMode mode, |
| 326 double touchmove_suppression_length_dips) | 294 double touchmove_suppression_length_dips) |
| 327 : client_(client), | 295 : client_(client), |
| 328 dispatching_touch_ack_(NULL), | 296 dispatching_touch_ack_(NULL), |
| 329 dispatching_touch_(false), | 297 dispatching_touch_(false), |
| 330 touch_filtering_state_(TOUCH_FILTERING_STATE_DEFAULT), | 298 touch_filtering_state_(TOUCH_FILTERING_STATE_DEFAULT), |
| 331 ack_timeout_enabled_(false), | 299 ack_timeout_enabled_(false), |
| 332 touchmove_slop_suppressor_(new TouchMoveSlopSuppressor( | 300 touchmove_slop_suppressor_(new TouchMoveSlopSuppressor( |
| 333 touchmove_suppression_length_dips + kSlopEpsilon)), | 301 touchmove_suppression_length_dips + kSlopEpsilon)), |
| 334 send_touch_events_async_(false), | 302 absorbing_touch_moves_(false), |
| 335 last_sent_touch_timestamp_sec_(0), | |
| 336 touch_scrolling_mode_(mode) { | 303 touch_scrolling_mode_(mode) { |
| 337 DCHECK(client); | 304 DCHECK(client); |
| 338 } | 305 } |
| 339 | 306 |
| 340 TouchEventQueue::~TouchEventQueue() { | 307 TouchEventQueue::~TouchEventQueue() { |
| 341 if (!touch_queue_.empty()) | 308 if (!touch_queue_.empty()) |
| 342 STLDeleteElements(&touch_queue_); | 309 STLDeleteElements(&touch_queue_); |
| 343 } | 310 } |
| 344 | 311 |
| 345 void TouchEventQueue::QueueEvent(const TouchEventWithLatencyInfo& event) { | 312 void TouchEventQueue::QueueEvent(const TouchEventWithLatencyInfo& event) { |
| 346 TRACE_EVENT0("input", "TouchEventQueue::QueueEvent"); | 313 TRACE_EVENT0("input", "TouchEventQueue::QueueEvent"); |
| 347 | 314 |
| 348 // If the queueing of |event| was triggered by an ack dispatch, defer | 315 // If the queueing of |event| was triggered by an ack dispatch, defer |
| 349 // processing the event until the dispatch has finished. | 316 // processing the event until the dispatch has finished. |
| 350 if (touch_queue_.empty() && !dispatching_touch_ack_) { | 317 if (touch_queue_.empty() && !dispatching_touch_ack_) { |
| 351 // Optimization of the case without touch handlers. Removing this path | 318 // Optimization of the case without touch handlers. Removing this path |
| 352 // yields identical results, but this avoids unnecessary allocations. | 319 // yields identical results, but this avoids unnecessary allocations. |
| 353 PreFilterResult filter_result = FilterBeforeForwarding(event.event); | 320 if (touch_filtering_state_ == DROP_ALL_TOUCHES || |
| 354 if (filter_result != FORWARD_TO_RENDERER) { | 321 (touch_filtering_state_ == DROP_TOUCHES_IN_SEQUENCE && |
| 355 client_->OnTouchEventAck(event, | 322 !WebTouchEventTraits::IsTouchSequenceStart(event.event))) { |
| 356 filter_result == ACK_WITH_NO_CONSUMER_EXISTS | 323 client_->OnTouchEventAck(event, INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS); |
| 357 ? INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS | |
| 358 : INPUT_EVENT_ACK_STATE_NOT_CONSUMED); | |
| 359 return; | 324 return; |
| 360 } | 325 } |
| 361 | 326 |
| 362 // There is no touch event in the queue. Forward it to the renderer | 327 // There is no touch event in the queue. Forward it to the renderer |
| 363 // immediately. | 328 // immediately. |
| 364 touch_queue_.push_back(new CoalescedWebTouchEvent(event, false)); | 329 touch_queue_.push_back(new CoalescedWebTouchEvent(event, false)); |
| 365 ForwardNextEventToRenderer(); | 330 TryForwardNextEventToRenderer(); |
| 366 return; | 331 return; |
| 367 } | 332 } |
| 368 | 333 |
| 369 // If the last queued touch-event was a touch-move, and the current event is | 334 // If the last queued touch-event was a touch-move, and the current event is |
| 370 // also a touch-move, then the events can be coalesced into a single event. | 335 // also a touch-move, then the events can be coalesced into a single event. |
| 371 if (touch_queue_.size() > 1) { | 336 if (touch_queue_.size() > 1) { |
| 372 CoalescedWebTouchEvent* last_event = touch_queue_.back(); | 337 CoalescedWebTouchEvent* last_event = touch_queue_.back(); |
| 373 if (last_event->CoalesceEventIfPossible(event)) | 338 if (last_event->CoalesceEventIfPossible(event)) |
| 374 return; | 339 return; |
| 375 } | 340 } |
| (...skipping 22 matching lines...) Expand all Loading... |
| 398 touch_filtering_state_ == FORWARD_TOUCHES_UNTIL_TIMEOUT) { | 363 touch_filtering_state_ == FORWARD_TOUCHES_UNTIL_TIMEOUT) { |
| 399 touch_filtering_state_ = FORWARD_ALL_TOUCHES; | 364 touch_filtering_state_ = FORWARD_ALL_TOUCHES; |
| 400 } | 365 } |
| 401 | 366 |
| 402 if (ack_result == INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS && | 367 if (ack_result == INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS && |
| 403 touch_filtering_state_ != DROP_ALL_TOUCHES && | 368 touch_filtering_state_ != DROP_ALL_TOUCHES && |
| 404 WebTouchEventTraits::IsTouchSequenceStart(acked_event)) { | 369 WebTouchEventTraits::IsTouchSequenceStart(acked_event)) { |
| 405 touch_filtering_state_ = DROP_TOUCHES_IN_SEQUENCE; | 370 touch_filtering_state_ = DROP_TOUCHES_IN_SEQUENCE; |
| 406 } | 371 } |
| 407 | 372 |
| 373 UpdateTouchAckStates(acked_event, ack_result); |
| 408 PopTouchEventToClient(ack_result, latency_info); | 374 PopTouchEventToClient(ack_result, latency_info); |
| 409 TryForwardNextEventToRenderer(); | 375 TryForwardNextEventToRenderer(); |
| 410 } | 376 } |
| 411 | 377 |
| 412 void TouchEventQueue::TryForwardNextEventToRenderer() { | 378 void TouchEventQueue::TryForwardNextEventToRenderer() { |
| 413 DCHECK(!dispatching_touch_ack_); | 379 DCHECK(!dispatching_touch_ack_); |
| 414 // If there are queued touch events, then try to forward them to the renderer | 380 // If there are queued touch events, then try to forward them to the renderer |
| 415 // immediately, or ACK the events back to the client if appropriate. | 381 // immediately, or ACK the events back to the client if appropriate. |
| 416 while (!touch_queue_.empty()) { | 382 while (!touch_queue_.empty()) { |
| 417 PreFilterResult filter_result = | 383 const TouchEventWithLatencyInfo& touch = |
| 418 FilterBeforeForwarding(touch_queue_.front()->coalesced_event().event); | 384 touch_queue_.front()->coalesced_event(); |
| 419 switch (filter_result) { | 385 PreFilterResult result = FilterBeforeForwarding(touch.event); |
| 386 switch (result) { |
| 420 case ACK_WITH_NO_CONSUMER_EXISTS: | 387 case ACK_WITH_NO_CONSUMER_EXISTS: |
| 421 PopTouchEventToClient(INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS); | 388 PopTouchEventToClient(INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS, |
| 389 LatencyInfo()); |
| 422 break; | 390 break; |
| 423 case ACK_WITH_NOT_CONSUMED: | 391 case ACK_WITH_NOT_CONSUMED: |
| 424 PopTouchEventToClient(INPUT_EVENT_ACK_STATE_NOT_CONSUMED); | 392 PopTouchEventToClient(INPUT_EVENT_ACK_STATE_NOT_CONSUMED, |
| 393 LatencyInfo()); |
| 425 break; | 394 break; |
| 426 case FORWARD_TO_RENDERER: | 395 case FORWARD_TO_RENDERER: |
| 427 ForwardNextEventToRenderer(); | 396 ForwardToRenderer(touch); |
| 428 return; | 397 return; |
| 429 } | 398 } |
| 430 } | 399 } |
| 431 } | 400 } |
| 432 | 401 |
| 433 void TouchEventQueue::ForwardNextEventToRenderer() { | 402 void TouchEventQueue::ForwardToRenderer( |
| 434 TRACE_EVENT0("input", "TouchEventQueue::ForwardNextEventToRenderer"); | 403 const TouchEventWithLatencyInfo& touch) { |
| 404 TRACE_EVENT0("input", "TouchEventQueue::ForwardToRenderer"); |
| 435 | 405 |
| 436 DCHECK(!empty()); | |
| 437 DCHECK(!dispatching_touch_); | 406 DCHECK(!dispatching_touch_); |
| 438 DCHECK_NE(touch_filtering_state_, DROP_ALL_TOUCHES); | 407 DCHECK_NE(touch_filtering_state_, DROP_ALL_TOUCHES); |
| 439 TouchEventWithLatencyInfo touch = touch_queue_.front()->coalesced_event(); | |
| 440 | 408 |
| 441 if (WebTouchEventTraits::IsTouchSequenceStart(touch.event)) { | 409 if (WebTouchEventTraits::IsTouchSequenceStart(touch.event)) { |
| 442 touch_filtering_state_ = | 410 touch_filtering_state_ = |
| 443 ack_timeout_enabled_ ? FORWARD_TOUCHES_UNTIL_TIMEOUT | 411 ack_timeout_enabled_ ? FORWARD_TOUCHES_UNTIL_TIMEOUT |
| 444 : FORWARD_ALL_TOUCHES; | 412 : FORWARD_ALL_TOUCHES; |
| 445 touch_ack_states_.clear(); | 413 touch_ack_states_.clear(); |
| 446 send_touch_events_async_ = false; | 414 absorbing_touch_moves_ = false; |
| 447 touch_sequence_start_position_ = | |
| 448 gfx::PointF(touch.event.touches[0].position); | |
| 449 } | 415 } |
| 450 | 416 |
| 451 if (send_touch_events_async_ && | |
| 452 touch.event.type == WebInputEvent::TouchMove) { | |
| 453 // Throttling touchmove's in a continuous touchmove stream while scrolling | |
| 454 // reduces the risk of jank. However, it's still important that the web | |
| 455 // application be sent touches at key points in the gesture stream, | |
| 456 // e.g., when the application slop region is exceeded or touchmove | |
| 457 // coalescing fails because of different modifiers. | |
| 458 const bool send_touch_move_now = | |
| 459 size() > 1 || | |
| 460 (touch.event.timeStampSeconds >= | |
| 461 last_sent_touch_timestamp_sec_ + kAsyncTouchMoveIntervalSec) || | |
| 462 (needs_async_touch_move_for_outer_slop_region_ && | |
| 463 OutsideApplicationSlopRegion(touch.event, | |
| 464 touch_sequence_start_position_)) || | |
| 465 (pending_async_touch_move_ && | |
| 466 !pending_async_touch_move_->CanCoalesceWith(touch)); | |
| 467 | |
| 468 if (!send_touch_move_now) { | |
| 469 if (!pending_async_touch_move_) { | |
| 470 pending_async_touch_move_.reset(new TouchEventWithLatencyInfo(touch)); | |
| 471 } else { | |
| 472 DCHECK(pending_async_touch_move_->CanCoalesceWith(touch)); | |
| 473 pending_async_touch_move_->CoalesceWith(touch); | |
| 474 } | |
| 475 PopTouchEventToClient(INPUT_EVENT_ACK_STATE_NOT_CONSUMED); | |
| 476 return; | |
| 477 } | |
| 478 } | |
| 479 | |
| 480 last_sent_touch_timestamp_sec_ = touch.event.timeStampSeconds; | |
| 481 | |
| 482 // Flush any pending async touch move. If it can be combined with the current | |
| 483 // (touchmove) event, great, otherwise send it immediately but separately. Its | |
| 484 // ack will trigger forwarding of the original |touch| event. | |
| 485 if (pending_async_touch_move_) { | |
| 486 if (pending_async_touch_move_->CanCoalesceWith(touch)) { | |
| 487 pending_async_touch_move_->CoalesceWith(touch); | |
| 488 pending_async_touch_move_->event.cancelable = !send_touch_events_async_; | |
| 489 touch = *pending_async_touch_move_.Pass(); | |
| 490 } else { | |
| 491 scoped_ptr<TouchEventWithLatencyInfo> async_move = | |
| 492 pending_async_touch_move_.Pass(); | |
| 493 async_move->event.cancelable = false; | |
| 494 touch_queue_.push_front(new CoalescedWebTouchEvent(*async_move, true)); | |
| 495 SendTouchEventImmediately(*async_move); | |
| 496 return; | |
| 497 } | |
| 498 } | |
| 499 | |
| 500 // Note: Marking touchstart events as not-cancelable prevents them from | |
| 501 // blocking subsequent gestures, but it may not be the best long term solution | |
| 502 // for tracking touch point dispatch. | |
| 503 if (send_touch_events_async_) | |
| 504 touch.event.cancelable = false; | |
| 505 | |
| 506 // A synchronous ack will reset |dispatching_touch_|, in which case | 417 // A synchronous ack will reset |dispatching_touch_|, in which case |
| 507 // the touch timeout should not be started. | 418 // the touch timeout should not be started. |
| 508 base::AutoReset<bool> dispatching_touch(&dispatching_touch_, true); | 419 base::AutoReset<bool> dispatching_touch(&dispatching_touch_, true); |
| 509 SendTouchEventImmediately(touch); | 420 client_->SendTouchEventImmediately(touch); |
| 510 if (dispatching_touch_ && | 421 if (dispatching_touch_ && |
| 511 touch_filtering_state_ == FORWARD_TOUCHES_UNTIL_TIMEOUT && | 422 touch_filtering_state_ == FORWARD_TOUCHES_UNTIL_TIMEOUT && |
| 512 ShouldTouchTriggerTimeout(touch.event)) { | 423 ShouldTouchTypeTriggerTimeout(touch.event.type)) { |
| 513 DCHECK(timeout_handler_); | 424 DCHECK(timeout_handler_); |
| 514 timeout_handler_->Start(touch); | 425 timeout_handler_->Start(touch); |
| 515 } | 426 } |
| 516 } | 427 } |
| 517 | 428 |
| 518 void TouchEventQueue::OnGestureScrollEvent( | 429 void TouchEventQueue::OnGestureScrollEvent( |
| 519 const GestureEventWithLatencyInfo& gesture_event) { | 430 const GestureEventWithLatencyInfo& gesture_event) { |
| 520 if (gesture_event.event.type != blink::WebInputEvent::GestureScrollBegin) | 431 if (gesture_event.event.type != blink::WebInputEvent::GestureScrollBegin) |
| 521 return; | 432 return; |
| 522 | 433 |
| 523 if (touch_scrolling_mode_ == TOUCH_SCROLLING_MODE_ASYNC_TOUCHMOVE) { | 434 if (touch_scrolling_mode_ == TOUCH_SCROLLING_MODE_ABSORB_TOUCHMOVE) |
| 524 pending_async_touch_move_.reset(); | 435 absorbing_touch_moves_ = true; |
| 525 send_touch_events_async_ = true; | |
| 526 needs_async_touch_move_for_outer_slop_region_ = true; | |
| 527 return; | |
| 528 } | |
| 529 | 436 |
| 530 if (touch_scrolling_mode_ != TOUCH_SCROLLING_MODE_TOUCHCANCEL) | 437 if (touch_scrolling_mode_ != TOUCH_SCROLLING_MODE_TOUCHCANCEL) |
| 531 return; | 438 return; |
| 532 | 439 |
| 533 // We assume that scroll events are generated synchronously from | 440 // We assume that scroll events are generated synchronously from |
| 534 // dispatching a touch event ack. This allows us to generate a synthetic | 441 // dispatching a touch event ack. This allows us to generate a synthetic |
| 535 // cancel event that has the same touch ids as the touch event that | 442 // cancel event that has the same touch ids as the touch event that |
| 536 // is being acked. Otherwise, we don't perform the touch-cancel optimization. | 443 // is being acked. Otherwise, we don't perform the touch-cancel optimization. |
| 537 if (!dispatching_touch_ack_) | 444 if (!dispatching_touch_ack_) |
| 538 return; | 445 return; |
| (...skipping 10 matching lines...) Expand all Loading... |
| 549 // in the queue is waiting for ack from renderer. So we can just insert | 456 // in the queue is waiting for ack from renderer. So we can just insert |
| 550 // the touch cancel at the beginning of the queue. | 457 // the touch cancel at the beginning of the queue. |
| 551 touch_queue_.push_front(new CoalescedWebTouchEvent( | 458 touch_queue_.push_front(new CoalescedWebTouchEvent( |
| 552 ObtainCancelEventForTouchEvent( | 459 ObtainCancelEventForTouchEvent( |
| 553 dispatching_touch_ack_->coalesced_event()), true)); | 460 dispatching_touch_ack_->coalesced_event()), true)); |
| 554 } | 461 } |
| 555 | 462 |
| 556 void TouchEventQueue::OnGestureEventAck( | 463 void TouchEventQueue::OnGestureEventAck( |
| 557 const GestureEventWithLatencyInfo& event, | 464 const GestureEventWithLatencyInfo& event, |
| 558 InputEventAckState ack_result) { | 465 InputEventAckState ack_result) { |
| 559 if (touch_scrolling_mode_ != TOUCH_SCROLLING_MODE_ASYNC_TOUCHMOVE) | 466 if (touch_scrolling_mode_ != TOUCH_SCROLLING_MODE_ABSORB_TOUCHMOVE) |
| 560 return; | 467 return; |
| 561 | 468 |
| 562 if (event.event.type != blink::WebInputEvent::GestureScrollUpdate) | 469 if (event.event.type != blink::WebInputEvent::GestureScrollUpdate) |
| 563 return; | 470 return; |
| 564 | 471 |
| 565 // Throttle sending touchmove events as long as the scroll events are handled. | 472 // Suspend sending touchmove events as long as the scroll events are handled. |
| 566 // Note that there's no guarantee that this ACK is for the most recent | 473 // Note that there's no guarantee that this ACK is for the most recent |
| 567 // gesture event (or even part of the current sequence). Worst case, the | 474 // gesture event (or even part of the current sequence). Worst case, the |
| 568 // delay in updating the absorption state will result in minor UI glitches. | 475 // delay in updating the absorption state should only result in minor UI |
| 569 // A valid |pending_async_touch_move_| will be flushed when the next event is | 476 // glitches. |
| 570 // forwarded. | 477 absorbing_touch_moves_ = (ack_result == INPUT_EVENT_ACK_STATE_CONSUMED); |
| 571 send_touch_events_async_ = (ack_result == INPUT_EVENT_ACK_STATE_CONSUMED); | |
| 572 if (!send_touch_events_async_) | |
| 573 needs_async_touch_move_for_outer_slop_region_ = false; | |
| 574 } | 478 } |
| 575 | 479 |
| 576 void TouchEventQueue::OnHasTouchEventHandlers(bool has_handlers) { | 480 void TouchEventQueue::OnHasTouchEventHandlers(bool has_handlers) { |
| 577 DCHECK(!dispatching_touch_ack_); | 481 DCHECK(!dispatching_touch_ack_); |
| 578 DCHECK(!dispatching_touch_); | 482 DCHECK(!dispatching_touch_); |
| 579 | 483 |
| 580 if (has_handlers) { | 484 if (has_handlers) { |
| 581 if (touch_filtering_state_ == DROP_ALL_TOUCHES) { | 485 if (touch_filtering_state_ == DROP_ALL_TOUCHES) { |
| 582 // If no touch handler was previously registered, ensure that we don't | 486 // If no touch handler was previously registered, ensure that we don't |
| 583 // send a partial touch sequence to the renderer. | 487 // send a partial touch sequence to the renderer. |
| 584 DCHECK(touch_queue_.empty()); | 488 DCHECK(touch_queue_.empty()); |
| 585 touch_filtering_state_ = DROP_TOUCHES_IN_SEQUENCE; | 489 touch_filtering_state_ = DROP_TOUCHES_IN_SEQUENCE; |
| 586 } | 490 } |
| 587 } else { | 491 } else { |
| 588 // TODO(jdduke): Synthesize a TouchCancel if necessary to update Blink touch | 492 // TODO(jdduke): Synthesize a TouchCancel if necessary to update Blink touch |
| 589 // state tracking (e.g., if the touch handler was removed mid-sequence). | 493 // state tracking (e.g., if the touch handler was removed mid-sequence). |
| 590 touch_filtering_state_ = DROP_ALL_TOUCHES; | 494 touch_filtering_state_ = DROP_ALL_TOUCHES; |
| 591 pending_async_touch_move_.reset(); | |
| 592 if (timeout_handler_) | 495 if (timeout_handler_) |
| 593 timeout_handler_->Reset(); | 496 timeout_handler_->Reset(); |
| 594 if (!touch_queue_.empty()) | 497 if (!touch_queue_.empty()) |
| 595 ProcessTouchAck(INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS, LatencyInfo()); | 498 ProcessTouchAck(INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS, LatencyInfo()); |
| 596 // As there is no touch handler, ack'ing the event should flush the queue. | 499 // As there is no touch handler, ack'ing the event should flush the queue. |
| 597 DCHECK(touch_queue_.empty()); | 500 DCHECK(touch_queue_.empty()); |
| 598 } | 501 } |
| 599 } | 502 } |
| 600 | 503 |
| 601 bool TouchEventQueue::IsPendingAckTouchStart() const { | 504 bool TouchEventQueue::IsPendingAckTouchStart() const { |
| (...skipping 20 matching lines...) Expand all Loading... |
| 622 return; | 525 return; |
| 623 } | 526 } |
| 624 | 527 |
| 625 ack_timeout_enabled_ = true; | 528 ack_timeout_enabled_ = true; |
| 626 if (!timeout_handler_) | 529 if (!timeout_handler_) |
| 627 timeout_handler_.reset(new TouchTimeoutHandler(this, ack_timeout_delay)); | 530 timeout_handler_.reset(new TouchTimeoutHandler(this, ack_timeout_delay)); |
| 628 else | 531 else |
| 629 timeout_handler_->set_timeout_delay(ack_timeout_delay); | 532 timeout_handler_->set_timeout_delay(ack_timeout_delay); |
| 630 } | 533 } |
| 631 | 534 |
| 632 bool TouchEventQueue::HasPendingAsyncTouchMoveForTesting() const { | |
| 633 return pending_async_touch_move_; | |
| 634 } | |
| 635 | |
| 636 bool TouchEventQueue::IsTimeoutRunningForTesting() const { | 535 bool TouchEventQueue::IsTimeoutRunningForTesting() const { |
| 637 return timeout_handler_ && timeout_handler_->IsTimeoutTimerRunning(); | 536 return timeout_handler_ && timeout_handler_->IsTimeoutTimerRunning(); |
| 638 } | 537 } |
| 639 | 538 |
| 640 const TouchEventWithLatencyInfo& | 539 const TouchEventWithLatencyInfo& |
| 641 TouchEventQueue::GetLatestEventForTesting() const { | 540 TouchEventQueue::GetLatestEventForTesting() const { |
| 642 return touch_queue_.back()->coalesced_event(); | 541 return touch_queue_.back()->coalesced_event(); |
| 643 } | 542 } |
| 644 | 543 |
| 645 void TouchEventQueue::FlushQueue() { | 544 void TouchEventQueue::FlushQueue() { |
| 646 DCHECK(!dispatching_touch_ack_); | 545 DCHECK(!dispatching_touch_ack_); |
| 647 DCHECK(!dispatching_touch_); | 546 DCHECK(!dispatching_touch_); |
| 648 pending_async_touch_move_.reset(); | |
| 649 if (touch_filtering_state_ != DROP_ALL_TOUCHES) | 547 if (touch_filtering_state_ != DROP_ALL_TOUCHES) |
| 650 touch_filtering_state_ = DROP_TOUCHES_IN_SEQUENCE; | 548 touch_filtering_state_ = DROP_TOUCHES_IN_SEQUENCE; |
| 651 while (!touch_queue_.empty()) | 549 while (!touch_queue_.empty()) { |
| 652 PopTouchEventToClient(INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS); | 550 PopTouchEventToClient(INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS, |
| 653 } | 551 LatencyInfo()); |
| 654 | 552 } |
| 655 void TouchEventQueue::PopTouchEventToClient(InputEventAckState ack_result) { | |
| 656 AckTouchEventToClient(ack_result, PopTouchEvent()); | |
| 657 } | 553 } |
| 658 | 554 |
| 659 void TouchEventQueue::PopTouchEventToClient( | 555 void TouchEventQueue::PopTouchEventToClient( |
| 660 InputEventAckState ack_result, | 556 InputEventAckState ack_result, |
| 661 const LatencyInfo& renderer_latency_info) { | 557 const LatencyInfo& renderer_latency_info) { |
| 662 scoped_ptr<CoalescedWebTouchEvent> acked_event = PopTouchEvent(); | 558 DCHECK(!dispatching_touch_ack_); |
| 663 acked_event->UpdateLatencyInfoForAck(renderer_latency_info); | 559 if (touch_queue_.empty()) |
| 664 AckTouchEventToClient(ack_result, acked_event.Pass()); | 560 return; |
| 665 } | 561 scoped_ptr<CoalescedWebTouchEvent> acked_event(touch_queue_.front()); |
| 562 touch_queue_.pop_front(); |
| 666 | 563 |
| 667 void TouchEventQueue::AckTouchEventToClient( | 564 if (acked_event->ignore_ack()) |
| 668 InputEventAckState ack_result, | 565 return; |
| 669 scoped_ptr<CoalescedWebTouchEvent> acked_event) { | |
| 670 DCHECK(acked_event); | |
| 671 DCHECK(!dispatching_touch_ack_); | |
| 672 UpdateTouchAckStates(acked_event->coalesced_event().event, ack_result); | |
| 673 | 566 |
| 674 // Note that acking the touch-event may result in multiple gestures being sent | 567 // Note that acking the touch-event may result in multiple gestures being sent |
| 675 // to the renderer, or touch-events being queued. | 568 // to the renderer, or touch-events being queued. |
| 676 base::AutoReset<const CoalescedWebTouchEvent*> dispatching_touch_ack( | 569 base::AutoReset<CoalescedWebTouchEvent*> |
| 677 &dispatching_touch_ack_, acked_event.get()); | 570 dispatching_touch_ack(&dispatching_touch_ack_, acked_event.get()); |
| 678 acked_event->DispatchAckToClient(ack_result, client_); | |
| 679 } | |
| 680 | 571 |
| 681 scoped_ptr<CoalescedWebTouchEvent> TouchEventQueue::PopTouchEvent() { | 572 for (WebTouchEventWithLatencyList::iterator iter = acked_event->begin(), |
| 682 DCHECK(!touch_queue_.empty()); | 573 end = acked_event->end(); |
| 683 scoped_ptr<CoalescedWebTouchEvent> event(touch_queue_.front()); | 574 iter != end; ++iter) { |
| 684 touch_queue_.pop_front(); | 575 iter->latency.AddNewLatencyFrom(renderer_latency_info); |
| 685 return event.Pass(); | 576 client_->OnTouchEventAck((*iter), ack_result); |
| 686 } | |
| 687 | |
| 688 void TouchEventQueue::SendTouchEventImmediately( | |
| 689 const TouchEventWithLatencyInfo& touch) { | |
| 690 if (needs_async_touch_move_for_outer_slop_region_) { | |
| 691 // Any event other than a touchmove (e.g., touchcancel or secondary | |
| 692 // touchstart) after a scroll has started will interrupt the need to send a | |
| 693 // an outer slop-region exceeding touchmove. | |
| 694 if (touch.event.type != WebInputEvent::TouchMove || | |
| 695 OutsideApplicationSlopRegion(touch.event, | |
| 696 touch_sequence_start_position_)) | |
| 697 needs_async_touch_move_for_outer_slop_region_ = false; | |
| 698 } | 577 } |
| 699 | |
| 700 client_->SendTouchEventImmediately(touch); | |
| 701 } | 578 } |
| 702 | 579 |
| 703 TouchEventQueue::PreFilterResult | 580 TouchEventQueue::PreFilterResult |
| 704 TouchEventQueue::FilterBeforeForwarding(const WebTouchEvent& event) { | 581 TouchEventQueue::FilterBeforeForwarding(const WebTouchEvent& event) { |
| 705 if (timeout_handler_ && timeout_handler_->FilterEvent(event)) | 582 if (timeout_handler_ && timeout_handler_->FilterEvent(event)) |
| 706 return ACK_WITH_NO_CONSUMER_EXISTS; | 583 return ACK_WITH_NO_CONSUMER_EXISTS; |
| 707 | 584 |
| 708 if (touchmove_slop_suppressor_->FilterEvent(event)) | 585 if (touchmove_slop_suppressor_->FilterEvent(event)) |
| 709 return ACK_WITH_NOT_CONSUMED; | 586 return ACK_WITH_NOT_CONSUMED; |
| 710 | 587 |
| 711 if (touch_filtering_state_ == DROP_ALL_TOUCHES) | 588 if (touch_filtering_state_ == DROP_ALL_TOUCHES) |
| 712 return ACK_WITH_NO_CONSUMER_EXISTS; | 589 return ACK_WITH_NO_CONSUMER_EXISTS; |
| 713 | 590 |
| 714 if (touch_filtering_state_ == DROP_TOUCHES_IN_SEQUENCE && | 591 if (touch_filtering_state_ == DROP_TOUCHES_IN_SEQUENCE && |
| 715 event.type != WebInputEvent::TouchCancel) { | 592 event.type != WebInputEvent::TouchCancel) { |
| 716 if (WebTouchEventTraits::IsTouchSequenceStart(event)) | 593 if (WebTouchEventTraits::IsTouchSequenceStart(event)) |
| 717 return FORWARD_TO_RENDERER; | 594 return FORWARD_TO_RENDERER; |
| 718 return ACK_WITH_NO_CONSUMER_EXISTS; | 595 return ACK_WITH_NOT_CONSUMED; |
| 719 } | 596 } |
| 720 | 597 |
| 598 if (absorbing_touch_moves_ && event.type == WebInputEvent::TouchMove) |
| 599 return ACK_WITH_NOT_CONSUMED; |
| 600 |
| 721 // Touch press events should always be forwarded to the renderer. | 601 // Touch press events should always be forwarded to the renderer. |
| 722 if (event.type == WebInputEvent::TouchStart) | 602 if (event.type == WebInputEvent::TouchStart) |
| 723 return FORWARD_TO_RENDERER; | 603 return FORWARD_TO_RENDERER; |
| 724 | 604 |
| 725 for (unsigned int i = 0; i < event.touchesLength; ++i) { | 605 for (unsigned int i = 0; i < event.touchesLength; ++i) { |
| 726 const WebTouchPoint& point = event.touches[i]; | 606 const WebTouchPoint& point = event.touches[i]; |
| 727 // If a point has been stationary, then don't take it into account. | 607 // If a point has been stationary, then don't take it into account. |
| 728 if (point.state == WebTouchPoint::StateStationary) | 608 if (point.state == WebTouchPoint::StateStationary) |
| 729 continue; | 609 continue; |
| 730 | 610 |
| (...skipping 26 matching lines...) Expand all Loading... |
| 757 } else if (event.type == WebInputEvent::TouchStart) { | 637 } else if (event.type == WebInputEvent::TouchStart) { |
| 758 for (unsigned i = 0; i < event.touchesLength; ++i) { | 638 for (unsigned i = 0; i < event.touchesLength; ++i) { |
| 759 const WebTouchPoint& point = event.touches[i]; | 639 const WebTouchPoint& point = event.touches[i]; |
| 760 if (point.state == WebTouchPoint::StatePressed) | 640 if (point.state == WebTouchPoint::StatePressed) |
| 761 touch_ack_states_[point.id] = ack_result; | 641 touch_ack_states_[point.id] = ack_result; |
| 762 } | 642 } |
| 763 } | 643 } |
| 764 } | 644 } |
| 765 | 645 |
| 766 } // namespace content | 646 } // namespace content |
| OLD | NEW |