Bug 1648491 - Have the main thread double-check APZ's consumable state. r=botond

APZ can sometimes indicate that it will be consuming touch events, even though
the touch-action properties prohibit it. This can happen if, for example, APZ
is waiting on the main-thread for accurate touch-action information. In such
cases, the main thread has enough information to filter out these false positives.
This patch makes it do that, by plumbing the allowed touch behaviors into
the APZEventState code that triggers the pointercancel event.

Differential Revision: https://phabricator.services.mozilla.com/D89303
This commit is contained in:
Kartikaya Gupta 2020-09-09 19:57:36 +00:00
parent b09be5d2f7
commit 803a237d97
7 changed files with 89 additions and 21 deletions

View File

@ -1757,12 +1757,14 @@ mozilla::ipc::IPCResult BrowserChild::RecvRealTouchEvent(
// The other values don't really matter.
InputAPZContext context(aGuid, aInputBlockId, aApzResponse);
nsTArray<TouchBehaviorFlags> allowedTouchBehaviors;
if (localEvent.mMessage == eTouchStart && AsyncPanZoomEnabled()) {
nsCOMPtr<Document> document = GetTopLevelDocument();
if (StaticPrefs::layout_css_touch_action_enabled()) {
APZCCallbackHelper::SendSetAllowedTouchBehaviorNotification(
mPuppetWidget, document, localEvent, aInputBlockId,
mSetAllowedTouchBehaviorCallback);
allowedTouchBehaviors =
APZCCallbackHelper::SendSetAllowedTouchBehaviorNotification(
mPuppetWidget, document, localEvent, aInputBlockId,
mSetAllowedTouchBehaviorCallback);
}
UniquePtr<DisplayportSetListener> postLayerization =
APZCCallbackHelper::SendSetTargetAPZCNotification(
@ -1784,7 +1786,8 @@ mozilla::ipc::IPCResult BrowserChild::RecvRealTouchEvent(
}
mAPZEventState->ProcessTouchEvent(localEvent, aGuid, aInputBlockId,
aApzResponse, status);
aApzResponse, status,
std::move(allowedTouchBehaviors));
return IPC_OK();
}

View File

@ -970,6 +970,12 @@ bool AsyncPanZoomController::ArePointerEventsConsumable(
// pointercancel event to web content, which can break certain features
// that are using touch-action and handling the pointermove events.
//
// Note that in particular this function can return true if APZ is waiting on
// the main thread for touch-action information. In this scenario, the
// APZEventState::MainThreadAgreesEventsAreConsumableByAPZ() function tries
// to use the main-thread touch-action information to filter out false
// positives.
//
// We could probably enhance this logic to determine things like "we're
// not pannable, so we can only zoom in, and the zoom is already maxed
// out, so we're not zoomable either" but no need for that at this point.

View File

@ -778,24 +778,26 @@ APZCCallbackHelper::SendSetTargetAPZCNotification(nsIWidget* aWidget,
return nullptr;
}
void APZCCallbackHelper::SendSetAllowedTouchBehaviorNotification(
nsTArray<TouchBehaviorFlags>
APZCCallbackHelper::SendSetAllowedTouchBehaviorNotification(
nsIWidget* aWidget, dom::Document* aDocument,
const WidgetTouchEvent& aEvent, uint64_t aInputBlockId,
const SetAllowedTouchBehaviorCallback& aCallback) {
nsTArray<TouchBehaviorFlags> flags;
if (!aWidget || !aDocument) {
return;
return flags;
}
if (PresShell* presShell = aDocument->GetPresShell()) {
if (nsIFrame* rootFrame = presShell->GetRootFrame()) {
nsTArray<TouchBehaviorFlags> flags;
for (uint32_t i = 0; i < aEvent.mTouches.Length(); i++) {
flags.AppendElement(TouchActionHelper::GetAllowedTouchBehavior(
aWidget, RelativeTo{rootFrame, ViewportType::Visual},
aEvent.mTouches[i]->mRefPoint));
}
aCallback(aInputBlockId, std::move(flags));
aCallback(aInputBlockId, flags);
}
}
return flags;
}
void APZCCallbackHelper::NotifyMozMouseScrollEvent(

View File

@ -148,8 +148,9 @@ class APZCCallbackHelper {
uint64_t aInputBlockId);
/* Figure out the allowed touch behaviors of each touch point in |aEvent|
* and send that information to the provided callback. */
static void SendSetAllowedTouchBehaviorNotification(
* and send that information to the provided callback. Also returns the
* allowed touch behaviors. */
static nsTArray<TouchBehaviorFlags> SendSetAllowedTouchBehaviorNotification(
nsIWidget* aWidget, mozilla::dom::Document* aDocument,
const WidgetTouchEvent& aEvent, uint64_t aInputBlockId,
const SetAllowedTouchBehaviorCallback& aCallback);

View File

@ -24,6 +24,7 @@
#include "mozilla/dom/BrowserChild.h"
#include "mozilla/dom/MouseEventBinding.h"
#include "mozilla/layers/APZCCallbackHelper.h"
#include "mozilla/layers/IAPZCTreeManager.h"
#include "mozilla/widget/nsAutoRollup.h"
#include "nsCOMPtr.h"
#include "nsDocShell.h"
@ -301,15 +302,21 @@ void APZEventState::ProcessLongTapUp(PresShell* aPresShell,
#endif
}
void APZEventState::ProcessTouchEvent(const WidgetTouchEvent& aEvent,
const ScrollableLayerGuid& aGuid,
uint64_t aInputBlockId,
nsEventStatus aApzResponse,
nsEventStatus aContentResponse) {
void APZEventState::ProcessTouchEvent(
const WidgetTouchEvent& aEvent, const ScrollableLayerGuid& aGuid,
uint64_t aInputBlockId, nsEventStatus aApzResponse,
nsEventStatus aContentResponse,
nsTArray<TouchBehaviorFlags>&& aAllowedTouchBehaviors) {
if (aEvent.mMessage == eTouchStart && aEvent.mTouches.Length() > 0) {
mActiveElementManager->SetTargetElement(aEvent.mTouches[0]->GetTarget());
mLastTouchIdentifier = aEvent.mTouches[0]->Identifier();
}
if (aEvent.mMessage == eTouchStart) {
// We get the allowed touch behaviors on a touchstart, but may not actually
// use them until the first touchmove, so we stash them in a member
// variable.
mTouchBlockAllowedBehaviors = std::move(aAllowedTouchBehaviors);
}
bool isTouchPrevented = aContentResponse == nsEventStatus_eConsumeNoDefault;
bool sentContentResponse = false;
@ -385,7 +392,8 @@ void APZEventState::ProcessTouchEvent(const WidgetTouchEvent& aEvent,
if (sentContentResponse && !isTouchPrevented &&
aApzResponse == nsEventStatus_eConsumeDoDefault &&
StaticPrefs::dom_w3c_pointer_events_enabled()) {
StaticPrefs::dom_w3c_pointer_events_enabled() &&
MainThreadAgreesEventsAreConsumableByAPZ()) {
WidgetTouchEvent cancelEvent(aEvent);
cancelEvent.mMessage = eTouchPointerCancel;
cancelEvent.mFlags.mCancelable = false; // mMessage != eTouchCancel;
@ -399,6 +407,48 @@ void APZEventState::ProcessTouchEvent(const WidgetTouchEvent& aEvent,
}
}
bool APZEventState::MainThreadAgreesEventsAreConsumableByAPZ() const {
// APZ errs on the side of saying it can consume touch events to perform
// default user-agent behaviours. In particular it may say this if it hasn't
// received accurate touch-action information. Here we double-check using
// accurate touch-action information. This code is kinda-sorta the main
// thread equivalent of AsyncPanZoomController::ArePointerEventsConsumable().
switch (mTouchBlockAllowedBehaviors.Length()) {
case 0:
// If we don't have any touch-action (e.g. because it is disabled) then
// APZ has no restrictions.
return true;
case 1: {
// If there's one touch point in this touch block, then check the pan-x
// and pan-y flags. If neither is allowed, then we disagree with APZ and
// say that it can't do anything with this touch block. Note that it would
// be even better if we could check the allowed scroll directions of the
// scrollframe at this point and refine this further.
TouchBehaviorFlags flags = mTouchBlockAllowedBehaviors[0];
return (flags & AllowedTouchBehavior::HORIZONTAL_PAN) ||
(flags & AllowedTouchBehavior::VERTICAL_PAN);
}
case 2: {
// If there's two touch points in this touch block, check that they both
// allow zooming.
for (const auto& allowed : mTouchBlockAllowedBehaviors) {
if (!(allowed & AllowedTouchBehavior::PINCH_ZOOM)) {
return false;
}
}
return true;
}
default:
// More than two touch points? APZ shouldn't be doing anything with this,
// so APZ shouldn't be consuming them.
return false;
}
}
void APZEventState::ProcessWheelEvent(const WidgetWheelEvent& aEvent,
uint64_t aInputBlockId) {
// If this event starts a swipe, indicate that it shouldn't result in a

View File

@ -66,7 +66,8 @@ class APZEventState final {
void ProcessTouchEvent(const WidgetTouchEvent& aEvent,
const ScrollableLayerGuid& aGuid,
uint64_t aInputBlockId, nsEventStatus aApzResponse,
nsEventStatus aContentResponse);
nsEventStatus aContentResponse,
nsTArray<TouchBehaviorFlags>&& aAllowedTouchBehaviors);
void ProcessWheelEvent(const WidgetWheelEvent& aEvent,
uint64_t aInputBlockId);
void ProcessMouseEvent(const WidgetMouseEvent& aEvent,
@ -84,6 +85,7 @@ class APZEventState final {
const nsCOMPtr<nsIWidget>& aWidget);
already_AddRefed<nsIWidget> GetWidget() const;
already_AddRefed<nsIContent> GetTouchRollup() const;
bool MainThreadAgreesEventsAreConsumableByAPZ() const;
private:
nsWeakPtr mWidget;
@ -97,6 +99,7 @@ class APZEventState final {
bool mFirstTouchCancelled;
bool mTouchEndCancelled;
int32_t mLastTouchIdentifier;
nsTArray<TouchBehaviorFlags> mTouchBlockAllowedBehaviors;
// Because touch-triggered mouse events (e.g. mouse events from a tap
// gesture) happen asynchronously from the touch events themselves, we

View File

@ -976,18 +976,21 @@ nsEventStatus nsBaseWidget::ProcessUntransformedAPZEvent(
UniquePtr<DisplayportSetListener> postLayerization;
if (WidgetTouchEvent* touchEvent = aEvent->AsTouchEvent()) {
nsTArray<TouchBehaviorFlags> allowedTouchBehaviors;
if (touchEvent->mMessage == eTouchStart) {
if (StaticPrefs::layout_css_touch_action_enabled()) {
APZCCallbackHelper::SendSetAllowedTouchBehaviorNotification(
this, GetDocument(), *(original->AsTouchEvent()), inputBlockId,
mSetAllowedTouchBehaviorCallback);
allowedTouchBehaviors =
APZCCallbackHelper::SendSetAllowedTouchBehaviorNotification(
this, GetDocument(), *(original->AsTouchEvent()),
inputBlockId, mSetAllowedTouchBehaviorCallback);
}
postLayerization = APZCCallbackHelper::SendSetTargetAPZCNotification(
this, GetDocument(), *(original->AsTouchEvent()), rootLayersId,
inputBlockId);
}
mAPZEventState->ProcessTouchEvent(*touchEvent, targetGuid, inputBlockId,
aApzResult.mStatus, status);
aApzResult.mStatus, status,
std::move(allowedTouchBehaviors));
} else if (WidgetWheelEvent* wheelEvent = aEvent->AsWheelEvent()) {
MOZ_ASSERT(wheelEvent->mFlags.mHandledByAPZ);
postLayerization = APZCCallbackHelper::SendSetTargetAPZCNotification(