| // Copyright 2016 The Chromium Authors | 
 | // Use of this source code is governed by a BSD-style license that can be | 
 | // found in the LICENSE file. | 
 |  | 
 | #include "components/input/mouse_wheel_event_queue.h" | 
 |  | 
 | #include "base/functional/bind.h" | 
 | #include "base/metrics/histogram_macros.h" | 
 | #include "base/trace_event/trace_event.h" | 
 | #include "build/build_config.h" | 
 | #include "third_party/perfetto/include/perfetto/tracing/track.h" | 
 | #include "ui/events/base_event_utils.h" | 
 | #include "ui/events/blink/web_input_event_traits.h" | 
 |  | 
 | using blink::WebGestureEvent; | 
 | using blink::WebInputEvent; | 
 | using blink::WebMouseWheelEvent; | 
 | using ui::LatencyInfo; | 
 |  | 
 | namespace input { | 
 |  | 
 | // This class represents a single queued mouse wheel event. Its main use | 
 | // is that it is reported via trace events. | 
 | class QueuedWebMouseWheelEvent : public MouseWheelEventWithLatencyInfo { | 
 |  public: | 
 |   QueuedWebMouseWheelEvent(const MouseWheelEventWithLatencyInfo& original_event, | 
 |                            DispatchToRendererCallback callback) | 
 |       : MouseWheelEventWithLatencyInfo(original_event), | 
 |         dispatch_callback(std::move(callback)) { | 
 |     TRACE_EVENT_BEGIN("input", "MouseWheelEventQueue::QueueEvent", | 
 |                       perfetto::Track::FromPointer(this)); | 
 |   } | 
 |  | 
 |   QueuedWebMouseWheelEvent(const QueuedWebMouseWheelEvent&) = delete; | 
 |   QueuedWebMouseWheelEvent& operator=(const QueuedWebMouseWheelEvent&) = delete; | 
 |  | 
 |   ~QueuedWebMouseWheelEvent() { | 
 |     TRACE_EVENT_END( | 
 |         "input", | 
 |         /* MouseWheelEventQueue::QueueEvent */ perfetto::Track::FromPointer( | 
 |             this)); | 
 |   } | 
 |  | 
 |   DispatchToRendererCallback dispatch_callback; | 
 | }; | 
 |  | 
 | MouseWheelEventQueue::MouseWheelEventQueue(MouseWheelEventQueueClient* client) | 
 |     : client_(client), | 
 |       send_wheel_events_async_(false), | 
 |       scrolling_device_(blink::WebGestureDevice::kUninitialized) { | 
 |   DCHECK(client); | 
 | } | 
 |  | 
 | MouseWheelEventQueue::~MouseWheelEventQueue() { | 
 | } | 
 |  | 
 | void MouseWheelEventQueue::QueueEvent( | 
 |     const MouseWheelEventWithLatencyInfo& event, | 
 |     DispatchToRendererCallback& dispatch_callback) { | 
 |   TRACE_EVENT0("input", "MouseWheelEventQueue::QueueEvent"); | 
 |  | 
 |   if (event_sent_for_gesture_ack_ && !wheel_queue_.empty()) { | 
 |     QueuedWebMouseWheelEvent* last_event = wheel_queue_.back().get(); | 
 |     if (last_event->CanCoalesceWith(event)) { | 
 |       // Terminate the LatencyInfo of the event before it gets coalesced away. | 
 |       event.latency.Terminate(); | 
 |  | 
 |       std::move(dispatch_callback) | 
 |           .Run(event.event, DispatchToRendererResult::kNotDispatched); | 
 |       last_event->CoalesceWith(event); | 
 |       // The deltas for the coalesced event change; the corresponding action | 
 |       // might be different now. | 
 |       last_event->event.event_action = | 
 |           WebMouseWheelEvent::GetPlatformSpecificDefaultEventAction( | 
 |               last_event->event); | 
 |       TRACE_EVENT_INSTANT2("input", "MouseWheelEventQueue::CoalescedWheelEvent", | 
 |                            TRACE_EVENT_SCOPE_THREAD, "total_dx", | 
 |                            last_event->event.delta_x, "total_dy", | 
 |                            last_event->event.delta_y); | 
 |       return; | 
 |     } | 
 |   } | 
 |  | 
 |   MouseWheelEventWithLatencyInfo event_with_action(event.event, | 
 |                                                           event.latency); | 
 |   event_with_action.event.event_action = | 
 |       WebMouseWheelEvent::GetPlatformSpecificDefaultEventAction(event.event); | 
 |   // Update the expected event action before queuing the event. From this point | 
 |   // on, the action should not change. | 
 |   wheel_queue_.push_back(std::make_unique<QueuedWebMouseWheelEvent>( | 
 |       event_with_action, std::move(dispatch_callback))); | 
 |   TryForwardNextEventToRenderer(); | 
 |   LOCAL_HISTOGRAM_COUNTS_100("Renderer.WheelQueueSize", wheel_queue_.size()); | 
 | } | 
 |  | 
 | bool MouseWheelEventQueue::CanGenerateGestureScroll( | 
 |     blink::mojom::InputEventResultState ack_result) const { | 
 |   if (ack_result == blink::mojom::InputEventResultState::kConsumed) { | 
 |     TRACE_EVENT_INSTANT0("input", "Wheel Event Consumed", | 
 |                          TRACE_EVENT_SCOPE_THREAD); | 
 |     return false; | 
 |   } | 
 |  | 
 |   if (event_sent_for_gesture_ack_->event.event_action == | 
 |       blink::WebMouseWheelEvent::EventAction::kPageZoom) { | 
 |     TRACE_EVENT_INSTANT0("input", "Wheel Event Cannot Cause Scroll", | 
 |                          TRACE_EVENT_SCOPE_THREAD); | 
 |     return false; | 
 |   } | 
 |  | 
 |   if (scrolling_device_ != blink::WebGestureDevice::kUninitialized && | 
 |       scrolling_device_ != blink::WebGestureDevice::kTouchpad) { | 
 |     TRACE_EVENT_INSTANT0("input", | 
 |                          "Autoscroll or Touchscreen Scroll In Progress", | 
 |                          TRACE_EVENT_SCOPE_THREAD); | 
 |     return false; | 
 |   } | 
 |  | 
 |   // When the cursor has entered the autoscroll mode but no mouse move has | 
 |   // arrived yet, We should still ignore wheel scrolling even though no GSB with | 
 |   // autoscroll source has been sent yet. | 
 |   if (client_->IsAutoscrollInProgress()) { | 
 |     TRACE_EVENT_INSTANT0("input", "In Autoscrolling mode", | 
 |                          TRACE_EVENT_SCOPE_THREAD); | 
 |     return false; | 
 |   } | 
 |  | 
 |   return true; | 
 | } | 
 |  | 
 | void MouseWheelEventQueue::ProcessMouseWheelAck( | 
 |     const MouseWheelEventWithLatencyInfo& ack_event, | 
 |     blink::mojom::InputEventResultSource ack_source, | 
 |     blink::mojom::InputEventResultState ack_result) { | 
 |   TRACE_EVENT0("input", "MouseWheelEventQueue::ProcessMouseWheelAck"); | 
 |   if (!event_sent_for_gesture_ack_) | 
 |     return; | 
 |  | 
 |   event_sent_for_gesture_ack_->latency.AddNewLatencyFrom(ack_event.latency); | 
 |   client_->OnMouseWheelEventAck(*event_sent_for_gesture_ack_, ack_source, | 
 |                                 ack_result); | 
 |  | 
 |   // If event wasn't consumed then generate a gesture scroll for it. | 
 |   if (CanGenerateGestureScroll(ack_result)) { | 
 |     WebGestureEvent scroll_update( | 
 |         WebInputEvent::Type::kGestureScrollUpdate, WebInputEvent::kNoModifiers, | 
 |         event_sent_for_gesture_ack_->event.TimeStamp(), | 
 |         blink::WebGestureDevice::kTouchpad); | 
 |  | 
 |     scroll_update.SetPositionInWidget( | 
 |         event_sent_for_gesture_ack_->event.PositionInWidget()); | 
 |     scroll_update.SetPositionInScreen( | 
 |         event_sent_for_gesture_ack_->event.PositionInScreen()); | 
 |  | 
 | #if !BUILDFLAG(IS_MAC) | 
 |     // Swap X & Y if Shift is down and when there is no horizontal movement. | 
 |     if (event_sent_for_gesture_ack_->event.event_action == | 
 |             blink::WebMouseWheelEvent::EventAction::kScrollHorizontal && | 
 |         event_sent_for_gesture_ack_->event.delta_x == 0) { | 
 |       scroll_update.data.scroll_update.delta_x = | 
 |           event_sent_for_gesture_ack_->event.delta_y; | 
 |       scroll_update.data.scroll_update.delta_y = | 
 |           event_sent_for_gesture_ack_->event.delta_x; | 
 |     } else | 
 | #endif  // BUILDFLAG(IS_MAC) | 
 |     { | 
 |       scroll_update.data.scroll_update.delta_x = | 
 |           event_sent_for_gesture_ack_->event.delta_x; | 
 |       scroll_update.data.scroll_update.delta_y = | 
 |           event_sent_for_gesture_ack_->event.delta_y; | 
 |     } | 
 |  | 
 |     if (event_sent_for_gesture_ack_->event.momentum_phase != | 
 |         blink::WebMouseWheelEvent::kPhaseNone) { | 
 |       scroll_update.data.scroll_update.inertial_phase = | 
 |           WebGestureEvent::InertialPhaseState::kMomentum; | 
 |     } else if (event_sent_for_gesture_ack_->event.phase != | 
 |                blink::WebMouseWheelEvent::kPhaseNone) { | 
 |       scroll_update.data.scroll_update.inertial_phase = | 
 |           WebGestureEvent::InertialPhaseState::kNonMomentum; | 
 |     } | 
 |  | 
 |     // WebMouseWheelEvent only supports these units for the delta. | 
 |     DCHECK(event_sent_for_gesture_ack_->event.delta_units == | 
 |                ui::ScrollGranularity::kScrollByPage || | 
 |            event_sent_for_gesture_ack_->event.delta_units == | 
 |                ui::ScrollGranularity::kScrollByPrecisePixel || | 
 |            event_sent_for_gesture_ack_->event.delta_units == | 
 |                ui::ScrollGranularity::kScrollByPixel); | 
 |     scroll_update.data.scroll_update.delta_units = | 
 |         event_sent_for_gesture_ack_->event.delta_units; | 
 |  | 
 |     if (event_sent_for_gesture_ack_->event.delta_units == | 
 |         ui::ScrollGranularity::kScrollByPage) { | 
 |       // Turn page scrolls into a *single* page scroll because | 
 |       // the magnitude the number of ticks is lost when coalescing. | 
 |       if (scroll_update.data.scroll_update.delta_x) | 
 |         scroll_update.data.scroll_update.delta_x = | 
 |             scroll_update.data.scroll_update.delta_x > 0 ? 1 : -1; | 
 |       if (scroll_update.data.scroll_update.delta_y) | 
 |         scroll_update.data.scroll_update.delta_y = | 
 |             scroll_update.data.scroll_update.delta_y > 0 ? 1 : -1; | 
 |     } else { | 
 |       if (event_sent_for_gesture_ack_->event.rails_mode == | 
 |           WebInputEvent::kRailsModeVertical) | 
 |         scroll_update.data.scroll_update.delta_x = 0; | 
 |       if (event_sent_for_gesture_ack_->event.rails_mode == | 
 |           WebInputEvent::kRailsModeHorizontal) | 
 |         scroll_update.data.scroll_update.delta_y = 0; | 
 |     } | 
 |  | 
 |     bool current_phase_ended = false; | 
 |     bool scroll_phase_ended = false; | 
 |     bool momentum_phase_ended = false; | 
 |  | 
 |     if (event_sent_for_gesture_ack_->event.phase != | 
 |             blink::WebMouseWheelEvent::kPhaseNone || | 
 |         event_sent_for_gesture_ack_->event.momentum_phase != | 
 |             blink::WebMouseWheelEvent::kPhaseNone) { | 
 |       scroll_phase_ended = event_sent_for_gesture_ack_->event.phase == | 
 |                                blink::WebMouseWheelEvent::kPhaseEnded || | 
 |                            event_sent_for_gesture_ack_->event.phase == | 
 |                                blink::WebMouseWheelEvent::kPhaseCancelled; | 
 |       momentum_phase_ended = | 
 |           event_sent_for_gesture_ack_->event.momentum_phase == | 
 |               blink::WebMouseWheelEvent::kPhaseEnded || | 
 |           event_sent_for_gesture_ack_->event.momentum_phase == | 
 |               blink::WebMouseWheelEvent::kPhaseCancelled; | 
 |       current_phase_ended = scroll_phase_ended || momentum_phase_ended; | 
 |     } | 
 |  | 
 |     bool needs_update = scroll_update.data.scroll_update.delta_x != 0 || | 
 |                         scroll_update.data.scroll_update.delta_y != 0; | 
 |  | 
 |     bool synthetic = event_sent_for_gesture_ack_->event.has_synthetic_phase; | 
 |  | 
 |     // Generally, there should always be a non-zero delta with kPhaseBegan | 
 |     // events. However, sometimes this is not the case and the delta in both | 
 |     // directions is 0. When this occurs, don't call SendScrollBegin because | 
 |     // scroll direction is necessary in order to determine the correct scroller | 
 |     // to target and latch to. | 
 |     if (needs_update && event_sent_for_gesture_ack_->event.phase == | 
 |                             blink::WebMouseWheelEvent::kPhaseBegan) { | 
 |       send_wheel_events_async_ = true; | 
 |  | 
 |       if (!client_->IsWheelScrollInProgress()) | 
 |         SendScrollBegin(scroll_update, synthetic); | 
 |     } | 
 |  | 
 |     if (needs_update) { | 
 |       // It is possible that the wheel event with phaseBegan is consumed and | 
 |       // no GSB is sent. | 
 |       if (!client_->IsWheelScrollInProgress()) | 
 |         SendScrollBegin(scroll_update, synthetic); | 
 |       client_->ForwardGestureEventWithLatencyInfo(scroll_update, | 
 |                                                   ui::LatencyInfo()); | 
 |     } | 
 |  | 
 |     if (current_phase_ended && client_->IsWheelScrollInProgress()) { | 
 |       // Send GSE when GSB is sent and no fling is going to happen next. | 
 |       SendScrollEnd(scroll_update, synthetic); | 
 |     } | 
 |   } | 
 |  | 
 |   event_sent_for_gesture_ack_.reset(); | 
 |   TryForwardNextEventToRenderer(); | 
 | } | 
 |  | 
 | void MouseWheelEventQueue::OnGestureScrollEvent( | 
 |     const GestureEventWithLatencyInfo& gesture_event) { | 
 |   if (gesture_event.event.GetType() == | 
 |       blink::WebInputEvent::Type::kGestureScrollBegin) { | 
 |     scrolling_device_ = gesture_event.event.SourceDevice(); | 
 |   } else if (scrolling_device_ == gesture_event.event.SourceDevice() && | 
 |              gesture_event.event.GetType() == | 
 |                  blink::WebInputEvent::Type::kGestureScrollEnd) { | 
 |     scrolling_device_ = blink::WebGestureDevice::kUninitialized; | 
 |   } else if (gesture_event.event.GetType() == | 
 |              blink::WebInputEvent::Type::kGestureFlingStart) { | 
 |     // With browser side fling we shouldn't reset scrolling_device_ on GFS since | 
 |     // the fling_controller processes the GFS to generate and send GSU events. | 
 |   } | 
 | } | 
 |  | 
 | void MouseWheelEventQueue::TryForwardNextEventToRenderer() { | 
 |   TRACE_EVENT0("input", "MouseWheelEventQueue::TryForwardNextEventToRenderer"); | 
 |  | 
 |   if (wheel_queue_.empty() || event_sent_for_gesture_ack_) | 
 |     return; | 
 |  | 
 |   event_sent_for_gesture_ack_ = std::move(wheel_queue_.front()); | 
 |   wheel_queue_.pop_front(); | 
 |  | 
 |   DCHECK(event_sent_for_gesture_ack_->event.phase != | 
 |              blink::WebMouseWheelEvent::kPhaseNone || | 
 |          event_sent_for_gesture_ack_->event.momentum_phase != | 
 |              blink::WebMouseWheelEvent::kPhaseNone); | 
 |   if (event_sent_for_gesture_ack_->event.phase == | 
 |       blink::WebMouseWheelEvent::kPhaseBegan) { | 
 |     send_wheel_events_async_ = false; | 
 |   } else if (send_wheel_events_async_) { | 
 |     event_sent_for_gesture_ack_->event.dispatch_type = | 
 |         WebInputEvent::DispatchType::kEventNonBlocking; | 
 |   } | 
 |  | 
 |   client_->SendMouseWheelEventImmediately( | 
 |       *event_sent_for_gesture_ack_, | 
 |       base::BindOnce(&MouseWheelEventQueue::ProcessMouseWheelAck, | 
 |                      base::Unretained(this)), | 
 |       event_sent_for_gesture_ack_->dispatch_callback); | 
 | } | 
 |  | 
 | void MouseWheelEventQueue::SendScrollEnd(WebGestureEvent update_event, | 
 |                                          bool synthetic) { | 
 |   DCHECK(client_->IsWheelScrollInProgress()); | 
 |  | 
 |   WebGestureEvent scroll_end(update_event); | 
 |   scroll_end.SetTimeStamp(ui::EventTimeForNow()); | 
 |   scroll_end.SetType(WebInputEvent::Type::kGestureScrollEnd); | 
 |   scroll_end.data.scroll_end.synthetic = synthetic; | 
 |   scroll_end.data.scroll_end.inertial_phase = | 
 |       update_event.data.scroll_update.inertial_phase; | 
 |   scroll_end.data.scroll_end.delta_units = | 
 |       update_event.data.scroll_update.delta_units; | 
 | #if BUILDFLAG(IS_CHROMEOS) | 
 |   // On ChromeOS wheel events with synthetic momentum_phase == | 
 |   // blink::WebMouseWheelEvent::kPhaseEnded are generated by the fling | 
 |   // controller to terminate touchpad flings. | 
 |   if (scroll_end.data.scroll_end.inertial_phase == | 
 |           WebGestureEvent::InertialPhaseState::kMomentum && | 
 |       synthetic) { | 
 |     scroll_end.data.scroll_end.generated_by_fling_controller = true; | 
 |   } | 
 | #endif  // BUILDFLAG(IS_CHROMEOS) | 
 |   client_->ForwardGestureEventWithLatencyInfo(scroll_end, ui::LatencyInfo()); | 
 | } | 
 |  | 
 | void MouseWheelEventQueue::SendScrollBegin( | 
 |     const WebGestureEvent& gesture_update, | 
 |     bool synthetic) { | 
 |   DCHECK(!client_->IsWheelScrollInProgress()); | 
 |  | 
 |   WebGestureEvent scroll_begin = | 
 |       ui::ScrollBeginFromScrollUpdate(gesture_update); | 
 |   scroll_begin.data.scroll_begin.synthetic = synthetic; | 
 |  | 
 |   client_->ForwardGestureEventWithLatencyInfo(scroll_begin, ui::LatencyInfo()); | 
 | } | 
 |  | 
 | }  // namespace input |