| // Copyright 2017 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/render_widget_targeter.h" |
| |
| #include <memory> |
| #include <utility> |
| |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/rand_util.h" |
| #include "base/time/time.h" |
| #include "base/trace_event/trace_event.h" |
| #include "components/input/render_widget_host_view_input.h" |
| #include "components/viz/common/surfaces/frame_sink_id.h" |
| #include "third_party/blink/public/common/input/web_input_event.h" |
| #include "third_party/perfetto/include/perfetto/tracing/track_event_args.h" |
| #include "ui/events/blink/blink_event_util.h" |
| #include "ui/gfx/geometry/point_f.h" |
| #include "ui/latency/latency_info.h" |
| |
| namespace input { |
| |
| namespace { |
| |
| gfx::PointF ComputeEventLocation(const blink::WebInputEvent& event) { |
| if (blink::WebInputEvent::IsMouseEventType(event.GetType()) || |
| event.GetType() == blink::WebInputEvent::Type::kMouseWheel) { |
| return static_cast<const blink::WebMouseEvent&>(event).PositionInWidget(); |
| } |
| if (blink::WebInputEvent::IsTouchEventType(event.GetType())) { |
| return static_cast<const blink::WebTouchEvent&>(event) |
| .touches[0] |
| .PositionInWidget(); |
| } |
| if (blink::WebInputEvent::IsGestureEventType(event.GetType())) |
| return static_cast<const blink::WebGestureEvent&>(event).PositionInWidget(); |
| |
| return gfx::PointF(); |
| } |
| |
| bool IsMouseMiddleClick(const blink::WebInputEvent& event) { |
| return (event.GetType() == blink::WebInputEvent::Type::kMouseDown && |
| static_cast<const blink::WebMouseEvent&>(event).button == |
| blink::WebPointerProperties::Button::kMiddle); |
| } |
| |
| constexpr base::TimeDelta kAsyncHitTestTimeout = base::Seconds(5); |
| |
| } // namespace |
| |
| RenderWidgetTargetResult::RenderWidgetTargetResult() = default; |
| |
| RenderWidgetTargetResult::RenderWidgetTargetResult( |
| const RenderWidgetTargetResult&) = default; |
| |
| RenderWidgetTargetResult::RenderWidgetTargetResult( |
| RenderWidgetHostViewInput* in_view, |
| bool in_should_query_view, |
| std::optional<gfx::PointF> in_location) |
| : view(in_view), |
| should_query_view(in_should_query_view), |
| target_location(in_location) {} |
| |
| RenderWidgetTargetResult::~RenderWidgetTargetResult() = default; |
| |
| RenderWidgetTargeter::TargetingRequest::TargetingRequest( |
| base::WeakPtr<RenderWidgetHostViewInput> root_view, |
| const blink::WebInputEvent& event, |
| const ui::LatencyInfo& latency) { |
| this->root_view = std::move(root_view); |
| this->location = ComputeEventLocation(event); |
| this->event = event.Clone(); |
| this->latency = latency; |
| } |
| |
| RenderWidgetTargeter::TargetingRequest::TargetingRequest( |
| base::WeakPtr<RenderWidgetHostViewInput> root_view, |
| const gfx::PointF& location, |
| RenderWidgetHostAtPointCallback callback) { |
| this->root_view = std::move(root_view); |
| this->location = location; |
| this->callback = std::move(callback); |
| } |
| |
| RenderWidgetTargeter::TargetingRequest::TargetingRequest( |
| TargetingRequest&& request) = default; |
| |
| RenderWidgetTargeter::TargetingRequest& RenderWidgetTargeter::TargetingRequest:: |
| operator=(TargetingRequest&&) = default; |
| |
| RenderWidgetTargeter::TargetingRequest::~TargetingRequest() = default; |
| |
| void RenderWidgetTargeter::TargetingRequest::RunCallback( |
| RenderWidgetHostViewInput* target, |
| std::optional<gfx::PointF> point) { |
| if (!callback.is_null()) { |
| std::move(callback).Run(target ? target->GetInputWeakPtr() : nullptr, |
| point); |
| } |
| } |
| |
| bool RenderWidgetTargeter::TargetingRequest::MergeEventIfPossible( |
| const blink::WebInputEvent& new_event) { |
| if (event && !blink::WebInputEvent::IsTouchEventType(new_event.GetType()) && |
| !blink::WebInputEvent::IsGestureEventType(new_event.GetType()) && |
| event->CanCoalesce(new_event)) { |
| event->Coalesce(new_event); |
| return true; |
| } |
| return false; |
| } |
| |
| bool RenderWidgetTargeter::TargetingRequest::IsWebInputEventRequest() const { |
| return !!event; |
| } |
| |
| blink::WebInputEvent* RenderWidgetTargeter::TargetingRequest::GetEvent() { |
| return event.get(); |
| } |
| |
| RenderWidgetHostViewInput* |
| RenderWidgetTargeter::TargetingRequest::GetRootView() const { |
| return root_view.get(); |
| } |
| |
| gfx::PointF RenderWidgetTargeter::TargetingRequest::GetLocation() const { |
| return location; |
| } |
| |
| const ui::LatencyInfo& RenderWidgetTargeter::TargetingRequest::GetLatency() |
| const { |
| return latency; |
| } |
| |
| RenderWidgetTargeter::RenderWidgetTargeter(Delegate* delegate) |
| : async_hit_test_timeout_delay_(kAsyncHitTestTimeout), |
| trace_id_(base::RandUint64()), |
| delegate_(delegate) { |
| DCHECK(delegate_); |
| } |
| |
| RenderWidgetTargeter::~RenderWidgetTargeter() = default; |
| |
| void RenderWidgetTargeter::FindTargetAndDispatch( |
| RenderWidgetHostViewInput* root_view, |
| const blink::WebInputEvent& event, |
| const ui::LatencyInfo& latency) { |
| DCHECK(blink::WebInputEvent::IsMouseEventType(event.GetType()) || |
| event.GetType() == blink::WebInputEvent::Type::kMouseWheel || |
| blink::WebInputEvent::IsTouchEventType(event.GetType()) || |
| (blink::WebInputEvent::IsGestureEventType(event.GetType()) && |
| (static_cast<const blink::WebGestureEvent&>(event).SourceDevice() == |
| blink::WebGestureDevice::kTouchscreen || |
| static_cast<const blink::WebGestureEvent&>(event).SourceDevice() == |
| blink::WebGestureDevice::kTouchpad))); |
| |
| if (!requests_.empty()) { |
| auto& request = requests_.back(); |
| if (request.MergeEventIfPossible(event)) |
| return; |
| } |
| |
| TargetingRequest request(root_view->GetInputWeakPtr(), event, latency); |
| |
| ResolveTargetingRequest(std::move(request)); |
| } |
| |
| void RenderWidgetTargeter::FindTargetAndCallback( |
| RenderWidgetHostViewInput* root_view, |
| const gfx::PointF& point, |
| RenderWidgetHostAtPointCallback callback) { |
| TargetingRequest request(root_view->GetInputWeakPtr(), point, |
| std::move(callback)); |
| |
| ResolveTargetingRequest(std::move(request)); |
| } |
| |
| void RenderWidgetTargeter::ResolveTargetingRequest(TargetingRequest request) { |
| if (request_in_flight_) { |
| requests_.push(std::move(request)); |
| return; |
| } |
| |
| RenderWidgetTargetResult result; |
| auto* request_target = request.GetRootView(); |
| auto request_target_location = request.GetLocation(); |
| |
| if (request.IsWebInputEventRequest()) { |
| result = is_autoscroll_in_progress_ |
| ? middle_click_result_ |
| : delegate_->FindTargetSynchronously(request_target, |
| *request.GetEvent()); |
| // |result.target_location| is utilized to update the position in widget for |
| // an event. If we are in autoscroll mode, we used cached data. So we need |
| // to update the target location of the |result|. |
| if (is_autoscroll_in_progress_) { |
| gfx::PointF transformed_point; |
| bool success = |
| request_target && result.view && |
| request_target->TransformPointToCoordSpaceForView( |
| request_target_location, result.view, &transformed_point); |
| if (success) { |
| result.target_location = transformed_point; |
| } else { |
| result.target_location = request_target_location; |
| } |
| } |
| |
| if (!is_autoscroll_in_progress_ && |
| IsMouseMiddleClick(*request.GetEvent())) { |
| if (!result.should_query_view) |
| middle_click_result_ = result; |
| } |
| } else { |
| result = delegate_->FindTargetSynchronouslyAtPoint(request_target, |
| request_target_location); |
| } |
| RenderWidgetHostViewInput* target = result.view; |
| if (!is_autoscroll_in_progress_ && result.should_query_view) { |
| if (request.IsWebInputEventRequest() && |
| IsMouseMiddleClick(*request.GetEvent())) { |
| middle_click_targeting_pending_ = true; |
| } |
| TRACE_EVENT("viz,benchmark", "Event.Pipeline", |
| perfetto::Flow::Global(trace_id_), "step", "QueryClient(Start)", |
| "event_location", request.GetLocation().ToString()); |
| |
| // TODO(kenrb, sadrul): When all event types support asynchronous hit |
| // testing, we should be able to have FindTargetSynchronously return the |
| // view and location to use for the renderer hit test query. |
| // Currently it has to return the surface hit test target, for event types |
| // that ignore |result.should_query_view|, and therefore we have to use |
| // root_view and the original event location for the initial query. |
| // Do not compare hit test results if we are forced to do async hit testing |
| // by HitTestQuery. |
| QueryClient(request_target, request_target_location, nullptr, gfx::PointF(), |
| std::move(request)); |
| } else { |
| FoundTarget(target, result.target_location, &request); |
| } |
| } |
| |
| void RenderWidgetTargeter::ViewWillBeDestroyed( |
| RenderWidgetHostViewInput* view) { |
| unresponsive_views_.erase(view); |
| |
| if (pending_autoscroll_view_ == view) { |
| pending_autoscroll_view_ = nullptr; |
| } |
| |
| if (middle_click_result_.view == view) { |
| middle_click_result_ = RenderWidgetTargetResult(); |
| is_autoscroll_in_progress_ = false; |
| } |
| } |
| |
| bool RenderWidgetTargeter::HasEventsPendingDispatch() const { |
| return request_in_flight_ || !requests_.empty(); |
| } |
| |
| RenderWidgetTargeter::AutoscrollStatus |
| RenderWidgetTargeter::SetIsAutoScrollInProgress(RenderWidgetHostViewInput* view, |
| bool autoscroll_in_progress) { |
| if (autoscroll_in_progress && (!view || view != middle_click_result_.view)) { |
| if (!middle_click_result_.view) { |
| if (middle_click_targeting_pending_) { |
| pending_autoscroll_view_ = view; |
| return AutoscrollStatus::kDeferred; |
| } |
| // During testing it is possible for us to have never done any input |
| // targeting. This is because WebDriver tests inject input events via |
| // JavaScript, and are processed immediately by the test page. Thus |
| // bypassing all input targeting in the Browser process. |
| // |
| // However Autoscroll and Flings are still driven by the Browser process. |
| // In order to support these tests we enable autoscroll if we do not |
| // have either a found target in `middle_click_result_.view` not a |
| // pending targeting in `middle_click_targeting_pending_`. |
| is_autoscroll_in_progress_ = autoscroll_in_progress; |
| return AutoscrollStatus::kProcessed; |
| } |
| return AutoscrollStatus::kFailed; |
| } |
| is_autoscroll_in_progress_ = autoscroll_in_progress; |
| |
| // If middle click autoscroll ends, reset |middle_click_result_|. |
| if (!autoscroll_in_progress) { |
| middle_click_result_ = RenderWidgetTargetResult(); |
| pending_autoscroll_view_ = nullptr; |
| } |
| return AutoscrollStatus::kProcessed; |
| } |
| |
| void RenderWidgetTargeter::QueryClient( |
| RenderWidgetHostViewInput* target, |
| const gfx::PointF& target_location, |
| RenderWidgetHostViewInput* last_request_target, |
| const gfx::PointF& last_target_location, |
| TargetingRequest request) { |
| auto& target_client = |
| target->GetViewRenderInputRouter()->input_target_client(); |
| // |target_client| may not be set yet for this |target| on Mac, need to |
| // understand why this happens. https://crbug.com/859492. |
| // We do not verify hit testing result under this circumstance. |
| if (!target_client.is_bound() || !target_client.is_connected()) { |
| FoundTarget(target, target_location, &request); |
| return; |
| } |
| |
| const gfx::PointF location = request.GetLocation(); |
| |
| request_in_flight_ = std::move(request); |
| |
| async_hit_test_timeout_.Start( |
| FROM_HERE, async_hit_test_timeout_delay_, |
| base::BindOnce(&RenderWidgetTargeter::AsyncHitTestTimedOut, |
| weak_ptr_factory_.GetWeakPtr(), target->GetInputWeakPtr(), |
| target_location, |
| last_request_target |
| ? last_request_target->GetInputWeakPtr() |
| : nullptr, |
| last_target_location)); |
| |
| target_client.set_disconnect_handler( |
| base::BindOnce(&RenderWidgetTargeter::OnInputTargetDisconnect, |
| weak_ptr_factory_.GetWeakPtr(), target->GetInputWeakPtr(), |
| target_location)); |
| |
| TRACE_EVENT("viz,benchmark", "Event.Pipeline", |
| perfetto::Flow::Global(trace_id_), "step", "QueryClient", |
| "event_location", location.ToString()); |
| |
| target_client->FrameSinkIdAt( |
| target_location, trace_id_, |
| base::BindOnce(&RenderWidgetTargeter::FoundFrameSinkId, |
| weak_ptr_factory_.GetWeakPtr(), target->GetInputWeakPtr(), |
| ++last_request_id_, target_location)); |
| } |
| |
| void RenderWidgetTargeter::FlushEventQueue() { |
| bool events_being_flushed = false; |
| while (!request_in_flight_ && !requests_.empty()) { |
| auto request = std::move(requests_.front()); |
| requests_.pop(); |
| // The root-view has gone away. Ignore this event, and try to process the |
| // next event. |
| if (!request.GetRootView()) |
| continue; |
| |
| // Only notify the delegate once that the current event queue is being |
| // flushed. Once all the events are flushed, notify the delegate again. |
| if (!events_being_flushed) { |
| delegate_->SetEventsBeingFlushed(true); |
| events_being_flushed = true; |
| } |
| ResolveTargetingRequest(std::move(request)); |
| } |
| delegate_->SetEventsBeingFlushed(false); |
| } |
| |
| void RenderWidgetTargeter::FoundFrameSinkId( |
| base::WeakPtr<RenderWidgetHostViewInput> target, |
| uint32_t request_id, |
| const gfx::PointF& target_location, |
| const viz::FrameSinkId& frame_sink_id, |
| const gfx::PointF& transformed_location) { |
| if (!target) { |
| return; |
| } |
| |
| uint32_t last_id = last_request_id_; |
| bool in_flight = request_in_flight_.has_value(); |
| if (request_id != last_id || !in_flight) { |
| // This is a response to a request that already timed out, so the event |
| // should have already been dispatched. Mark the renderer as responsive |
| // and otherwise ignore this response. |
| unresponsive_views_.erase(target.get()); |
| return; |
| } |
| |
| TargetingRequest request = std::move(request_in_flight_.value()); |
| |
| request_in_flight_.reset(); |
| async_hit_test_timeout_.Stop(); |
| target->GetViewRenderInputRouter() |
| ->input_target_client() |
| .set_disconnect_handler(base::OnceClosure()); |
| |
| // Ensure the returned view is a valid descendant of the frame we queried |
| // (|target.get()|) to prevent a compromised renderer from redirecting input. |
| RenderWidgetHostViewInput* resolved_view = |
| delegate_->FindViewFromFrameSinkId(frame_sink_id, target.get()); |
| |
| // Compute final target and location. |
| RenderWidgetHostViewInput* final_view = |
| resolved_view ? resolved_view : target.get(); |
| gfx::PointF final_location = |
| resolved_view ? transformed_location : target_location; |
| |
| // If a client returned an embedded target, then it might be necessary to |
| // continue asking the clients until a client claims an event for itself. |
| if (final_view == target.get() || |
| unresponsive_views_.find(final_view) != unresponsive_views_.end() || |
| !delegate_->ShouldContinueHitTesting(final_view)) { |
| // Reduced scope is required since FoundTarget can trigger another query |
| // which would end up linked to the current query. |
| { |
| TRACE_EVENT("viz,benchmark", "Event.Pipeline", |
| perfetto::TerminatingFlow::Global(trace_id_), "step", |
| "FoundTarget"); |
| } |
| |
| if (request.IsWebInputEventRequest() && |
| IsMouseMiddleClick(*request.GetEvent())) { |
| middle_click_result_ = {final_view, /*should_query_view=*/false, |
| final_location}; |
| if (pending_autoscroll_view_) { |
| bool success = (pending_autoscroll_view_ == final_view) || |
| RenderWidgetHostViewInput::IsAncestorView( |
| final_view, pending_autoscroll_view_) || |
| RenderWidgetHostViewInput::IsAncestorView( |
| pending_autoscroll_view_, final_view); |
| pending_autoscroll_view_->OnAutoscrollTargetResolved(success); |
| if (!success) { |
| delegate_->CancelAutoscroll(pending_autoscroll_view_); |
| } |
| pending_autoscroll_view_ = nullptr; |
| } |
| } |
| |
| FoundTarget(final_view, final_location, &request); |
| } else { |
| QueryClient(final_view, final_location, target.get(), target_location, |
| std::move(request)); |
| } |
| } |
| |
| void RenderWidgetTargeter::FoundTarget( |
| RenderWidgetHostViewInput* target, |
| const std::optional<gfx::PointF>& target_location, |
| TargetingRequest* request) { |
| DCHECK(request); |
| if (request->IsWebInputEventRequest() && |
| IsMouseMiddleClick(*request->GetEvent())) { |
| middle_click_targeting_pending_ = false; |
| } |
| // RenderWidgetHostViewMac can be deleted asynchronously, in which case the |
| // View will be valid but there will no longer be a RenderWidgetHostImpl. |
| if (!request->GetRootView() || |
| !request->GetRootView()->GetViewRenderInputRouter()) { |
| return; |
| } |
| |
| if (request->IsWebInputEventRequest()) { |
| delegate_->DispatchEventToTarget(request->GetRootView(), target, |
| request->GetEvent(), request->GetLatency(), |
| target_location); |
| } else { |
| request->RunCallback(target, target_location); |
| } |
| |
| FlushEventQueue(); |
| } |
| |
| void RenderWidgetTargeter::AsyncHitTestTimedOut( |
| base::WeakPtr<RenderWidgetHostViewInput> current_request_target, |
| const gfx::PointF& current_target_location, |
| base::WeakPtr<RenderWidgetHostViewInput> last_request_target, |
| const gfx::PointF& last_target_location) { |
| DCHECK(request_in_flight_); |
| |
| TargetingRequest request = std::move(request_in_flight_.value()); |
| request_in_flight_.reset(); |
| |
| if (!request.GetRootView()) |
| return; |
| |
| if (current_request_target) { |
| // Mark view as unresponsive so further events will not be sent to it. |
| unresponsive_views_.insert(current_request_target.get()); |
| |
| // Reset disconnect handler for view. |
| current_request_target->GetViewRenderInputRouter() |
| ->input_target_client() |
| .set_disconnect_handler(base::OnceClosure()); |
| } |
| |
| if (request.GetRootView() == current_request_target.get()) { |
| // When a request to the top-level frame times out then the event gets |
| // sent there anyway. It will trigger the hung renderer dialog if the |
| // renderer fails to process it. |
| FoundTarget(current_request_target.get(), current_target_location, |
| &request); |
| } else { |
| FoundTarget(last_request_target.get(), last_target_location, &request); |
| } |
| } |
| |
| void RenderWidgetTargeter::OnInputTargetDisconnect( |
| base::WeakPtr<RenderWidgetHostViewInput> target, |
| const gfx::PointF& location) { |
| if (!async_hit_test_timeout_.IsRunning()) |
| return; |
| |
| async_hit_test_timeout_.Stop(); |
| TargetingRequest request = std::move(request_in_flight_.value()); |
| request_in_flight_.reset(); |
| |
| // Since we couldn't find the target frame among the child-frames |
| // we process the event in the current frame. |
| FoundTarget(target.get(), location, &request); |
| } |
| |
| } // namespace input |