blob: f9b93b05eb5751cc4cdf2990f224fa34915139ab [file]
// 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