Bug 1891304 - Make APZEventState manage whether the pointerdown was consumed by content or not r=smaug,hiro

The Pointer Events spec defines that:

> Authors can prevent the firing of certain compatibility mouse events by
> canceling the pointerdown event (if the isPrimary property is true).
> <snip>
> Note, however, that this does not prevent the mouseover, mouseenter, mouseout,
> or mouseleave events from firing.
https://w3c.github.io/pointerevents/#the-pointerdown-event

The other browsers conform to this.  Therefore, we should stop dispatching
compatibility mouse events only if the preceding `pointerdown` is consumed by
content.  I.e., we need to keep dispatching touch events and `click` etc which
indicate what should happen on the element.

Currently, `APZEventState` does not manage whether the preceding `pointerdown`
is canceled or not.  So, it dispatches compatibility mouse events via
`APZCCallbackHelper` after the consumed pointer is removed.  Therefore, we
need to make it manage whether the preceding `pointerdown` of the first touch
is consumed or not and `APZCCallbackHelper` needs an option to dispatch the
compatibility mouse events only to chrome (they are required to dispatch
`click` etc).

However, if `APZEventState` is not available like test API used in the
remote process, `TouchManager` needs to manage it instead of `APZEventState`.

I don't think only `TouchManager` should manage it because `APZEventState`
manages complicated state of touch gestures and that can know whether the
synthesizing compatibility mouse events related to the consumed `pointerdown`
or not strictly.  Therefore, this patch makes the `TouchManager` state used
only in the path handling synthesized events for tests.

Differential Revision: https://phabricator.services.mozilla.com/D208706
This commit is contained in:
Masayuki Nakano 2024-05-14 01:07:49 +00:00
parent dfafc3b528
commit 888cde525c
8 changed files with 114 additions and 28 deletions

View file

@ -147,6 +147,30 @@ SimpleTest.waitForFocus(async () => {
);
})();
await (async function test_single_tap_with_consuming_pointerdown() {
await promiseFlushingAPZGestureState();
info("test_single_tap_with_consuming_pointerdown: testing...");
events = [];
const waitForTouchEnd = promiseEvent("click");
child.addEventListener("pointerdown", event => {
event.preventDefault();
}, {once: true});
synthesizeTouch(child, 5, 5);
await waitForTouchEnd;
const result = stringifyEvents(events);
const expected = stringifyEvents([
{ type: "touchend", target: child },
{ type: "click", target: child, detail: 1, button: 0, buttons: 0 },
]);
// If testing on Windows, the result is really unstable. Let's allow to
// fail for now.
(navigator.platform.includes("Win") && result != expected ? todo_is : is)(
result,
expected,
`Single tap should not cause mouse events if pointerdown is consumed, but click event should be fired ${desc}`
);
})();
await (async function test_single_tap_with_consuming_touchstart() {
await promiseFlushingAPZGestureState();
info("test_single_tap_with_consuming_touchstart: testing...");
@ -223,6 +247,8 @@ SimpleTest.waitForFocus(async () => {
`Multiple touch should not cause mouse events ${desc}`
);
})();
// FIXME: Add long tap tests which won't frequently fail.
}
SimpleTest.finish();
});

View file

@ -6,6 +6,8 @@
#include "APZCCallbackHelper.h"
#include "APZEventState.h" // for PrecedingPointerDown
#include "gfxPlatform.h" // For gfxPlatform::UseTiling
#include "mozilla/AsyncEventDispatcher.h"
@ -510,7 +512,8 @@ nsEventStatus APZCCallbackHelper::DispatchWidgetEvent(WidgetGUIEvent& aEvent) {
nsEventStatus APZCCallbackHelper::DispatchSynthesizedMouseEvent(
EventMessage aMsg, const LayoutDevicePoint& aRefPoint, Modifiers aModifiers,
int32_t aClickCount, nsIWidget* aWidget) {
int32_t aClickCount, PrecedingPointerDown aPrecedingPointerDownState,
nsIWidget* aWidget) {
MOZ_ASSERT(aMsg == eMouseMove || aMsg == eMouseDown || aMsg == eMouseUp ||
aMsg == eMouseLongTap);
@ -524,6 +527,15 @@ nsEventStatus APZCCallbackHelper::DispatchSynthesizedMouseEvent(
if (aMsg == eMouseLongTap) {
event.mFlags.mOnlyChromeDispatch = true;
}
// If the preceding `pointerdown` was canceled by content, we should not
// dispatch the compatibility mouse events into the content, but they are
// required to dispatch `click`, `dblclick` and `auxclick` events by
// EventStateManager. Therefore, we need to dispatch them only to chrome.
else if (aPrecedingPointerDownState ==
PrecedingPointerDown::ConsumedByContent) {
event.PreventDefault(false);
event.mFlags.mOnlyChromeDispatch = true;
}
if (aMsg != eMouseMove) {
event.mClickCount = aClickCount;
}
@ -551,21 +563,20 @@ PreventDefaultResult APZCCallbackHelper::DispatchMouseEvent(
return preventDefaultResult;
}
void APZCCallbackHelper::FireSingleTapEvent(const LayoutDevicePoint& aPoint,
Modifiers aModifiers,
int32_t aClickCount,
nsIWidget* aWidget) {
void APZCCallbackHelper::FireSingleTapEvent(
const LayoutDevicePoint& aPoint, Modifiers aModifiers, int32_t aClickCount,
PrecedingPointerDown aPrecedingPointerDownState, nsIWidget* aWidget) {
if (aWidget->Destroyed()) {
return;
}
APZCCH_LOG("Dispatching single-tap component events to %s\n",
ToString(aPoint).c_str());
DispatchSynthesizedMouseEvent(eMouseMove, aPoint, aModifiers, aClickCount,
aWidget);
aPrecedingPointerDownState, aWidget);
DispatchSynthesizedMouseEvent(eMouseDown, aPoint, aModifiers, aClickCount,
aWidget);
aPrecedingPointerDownState, aWidget);
DispatchSynthesizedMouseEvent(eMouseUp, aPoint, aModifiers, aClickCount,
aWidget);
aPrecedingPointerDownState, aWidget);
}
static dom::Element* GetDisplayportElementFor(

View file

@ -33,6 +33,10 @@ namespace layers {
struct RepaintRequest;
namespace apz {
enum class PrecedingPointerDown : bool;
}
/* Refer to documentation on SendSetTargetAPZCNotification for this class */
class DisplayportSetListener : public ManagedPostRefreshObserver {
public:
@ -61,6 +65,8 @@ class APZCCallbackHelper {
typedef mozilla::layers::ScrollableLayerGuid ScrollableLayerGuid;
public:
using PrecedingPointerDown = apz::PrecedingPointerDown;
static void NotifyLayerTransforms(const nsTArray<MatrixMessage>& aTransforms);
/* Applies the scroll and zoom parameters from the given RepaintRequest object
@ -107,7 +113,8 @@ class APZCCallbackHelper {
MOZ_CAN_RUN_SCRIPT
static nsEventStatus DispatchSynthesizedMouseEvent(
EventMessage aMsg, const LayoutDevicePoint& aRefPoint,
Modifiers aModifiers, int32_t aClickCount, nsIWidget* aWidget);
Modifiers aModifiers, int32_t aClickCount,
PrecedingPointerDown aPrecedingPointerDownState, nsIWidget* aWidget);
/* Dispatch a mouse event with the given parameters.
* Return whether or not any listeners have called preventDefault on the
@ -123,9 +130,10 @@ class APZCCallbackHelper {
/* Fire a single-tap event at the given point. The event is dispatched
* via the given widget. */
MOZ_CAN_RUN_SCRIPT
static void FireSingleTapEvent(const LayoutDevicePoint& aPoint,
Modifiers aModifiers, int32_t aClickCount,
nsIWidget* aWidget);
static void FireSingleTapEvent(
const LayoutDevicePoint& aPoint, Modifiers aModifiers,
int32_t aClickCount, PrecedingPointerDown aPrecedingPointerDownState,
nsIWidget* aWidget);
/* Perform hit-testing on the touch points of |aEvent| to determine
* which scrollable frames they target. If any of these frames don't have

View file

@ -26,6 +26,7 @@
#include "mozilla/ViewportUtils.h"
#include "mozilla/dom/BrowserChild.h"
#include "mozilla/dom/MouseEventBinding.h"
#include "mozilla/dom/PointerEventHandler.h"
#include "mozilla/layers/APZCCallbackHelper.h"
#include "mozilla/layers/APZUtils.h"
#include "mozilla/layers/IAPZCTreeManager.h"
@ -100,13 +101,8 @@ APZEventState::APZEventState(nsIWidget* aWidget,
,
mActiveElementManager(new ActiveElementManager()),
mContentReceivedInputBlockCallback(std::move(aCallback)),
mPendingTouchPreventedResponse(false),
mPendingTouchPreventedBlockId(0),
mEndTouchState(apz::SingleTapState::NotClick),
mFirstTouchCancelled(false),
mTouchEndCancelled(false),
mReceivedNonTouchStart(false),
mTouchStartPrevented(false),
mLastTouchIdentifier(0) {
nsresult rv;
mWidget = do_GetWeakReference(aWidget, &rv);
@ -139,8 +135,9 @@ void APZEventState::ProcessSingleTap(const CSSPoint& aPoint,
nsCOMPtr<nsIWidget> localWidget = do_QueryReferent(mWidget);
if (localWidget) {
widget::nsAutoRollup rollup(touchRollup);
APZCCallbackHelper::FireSingleTapEvent(aPoint * aScale, aModifiers,
aClickCount, localWidget);
APZCCallbackHelper::FireSingleTapEvent(
aPoint * aScale, aModifiers, aClickCount, mPrecedingPointerDownState,
localWidget);
}
mActiveElementManager->ProcessSingleTap();
@ -161,7 +158,8 @@ PreventDefaultResult APZEventState::FireContextmenuEvents(
// Note that we don't need to check whether mousemove event is consumed or
// not because Chrome also ignores the result.
APZCCallbackHelper::DispatchSynthesizedMouseEvent(
eMouseMove, aPoint * aScale, aModifiers, 0 /* clickCount */, aWidget);
eMouseMove, aPoint * aScale, aModifiers, 0 /* clickCount */,
mPrecedingPointerDownState, aWidget);
// Converting the modifiers to DOM format for the DispatchMouseEvent call
// is the most useless thing ever because nsDOMWindowUtils::SendMouseEvent
@ -186,7 +184,7 @@ PreventDefaultResult APZEventState::FireContextmenuEvents(
// If the contextmenu wasn't consumed, fire the eMouseLongTap event.
nsEventStatus status = APZCCallbackHelper::DispatchSynthesizedMouseEvent(
eMouseLongTap, aPoint * aScale, aModifiers,
/*clickCount*/ 1, aWidget);
/*clickCount*/ 1, mPrecedingPointerDownState, aWidget);
APZES_LOG("eMouseLongTap event %s\n", ToString(status).c_str());
#endif
}
@ -228,7 +226,8 @@ void APZEventState::ProcessLongTap(PresShell* aPresShell,
// at this time, because things like text selection or dragging may want
// to know about it.
APZCCallbackHelper::DispatchSynthesizedMouseEvent(
eMouseLongTap, aPoint * aScale, aModifiers, /*clickCount*/ 1, widget);
eMouseLongTap, aPoint * aScale, aModifiers, /*clickCount*/ 1,
mPrecedingPointerDownState, widget);
#else
PreventDefaultResult preventDefaultResult =
FireContextmenuEvents(aPresShell, aPoint, aScale, aModifiers, widget);
@ -324,6 +323,14 @@ void APZEventState::ProcessTouchEvent(
// touchstart was prevented by content.
if (mTouchCounter.GetActiveTouchCount() == 0) {
mFirstTouchCancelled = isTouchPrevented;
const PointerInfo* pointerInfo =
!aEvent.mTouches.IsEmpty() ? PointerEventHandler::GetPointerInfo(
aEvent.mTouches[0]->Identifier())
: nullptr;
mPrecedingPointerDownState =
pointerInfo && pointerInfo->mPreventMouseEventByContent
? PrecedingPointerDown::ConsumedByContent
: PrecedingPointerDown::NotConsumed;
} else {
if (mFirstTouchCancelled && !isTouchPrevented) {
APZES_LOG(

View file

@ -40,6 +40,7 @@ namespace layers {
class ActiveElementManager;
namespace apz {
enum class PrecedingPointerDown : bool { NotConsumed, ConsumedByContent };
enum class SingleTapState : uint8_t;
} // namespace apz
@ -56,6 +57,8 @@ class APZEventState final {
typedef ScrollableLayerGuid::ViewID ViewID;
public:
using PrecedingPointerDown = apz::PrecedingPointerDown;
APZEventState(nsIWidget* aWidget,
ContentReceivedInputBlockCallback&& aCallback);
@ -107,17 +110,19 @@ class APZEventState final {
RefPtr<ActiveElementManager> mActiveElementManager;
ContentReceivedInputBlockCallback mContentReceivedInputBlockCallback;
TouchCounter mTouchCounter;
bool mPendingTouchPreventedResponse;
ScrollableLayerGuid mPendingTouchPreventedGuid;
uint64_t mPendingTouchPreventedBlockId;
apz::SingleTapState mEndTouchState;
bool mFirstTouchCancelled;
bool mTouchEndCancelled;
PrecedingPointerDown mPrecedingPointerDownState =
PrecedingPointerDown::NotConsumed;
bool mPendingTouchPreventedResponse = false;
bool mFirstTouchCancelled = false;
bool mTouchEndCancelled = false;
// Set to true when we have received any one of
// touch-move/touch-end/touch-cancel events in the touch block being
// processed.
bool mReceivedNonTouchStart;
bool mTouchStartPrevented;
bool mReceivedNonTouchStart = false;
bool mTouchStartPrevented = false;
int32_t mLastTouchIdentifier;
nsTArray<TouchBehaviorFlags> mTouchBlockAllowedBehaviors;

View file

@ -7746,6 +7746,10 @@ void PresShell::EventHandler::MaybeSynthesizeCompatMouseEventsForTouchEnd(
event.mClickCount = message == eMouseMove ? 0 : 1;
event.mModifiers = aTouchEndEvent->mModifiers;
event.convertToPointer = false;
if (TouchManager::IsPrecedingTouchPointerDownConsumedByContent()) {
event.PreventDefault(false);
event.mFlags.mOnlyChromeDispatch = true;
}
nsEventStatus mouseEventStatus = nsEventStatus_eIgnore;
presShell->HandleEvent(frameForPresShell, &event, false, &mouseEventStatus);
}

View file

@ -14,6 +14,7 @@
#include "mozilla/TimeStamp.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/EventTarget.h"
#include "mozilla/dom/PointerEventHandler.h"
#include "mozilla/layers/InputAPZContext.h"
#include "nsIContent.h"
#include "nsIFrame.h"
@ -30,6 +31,7 @@ StaticAutoPtr<nsTHashMap<nsUint32HashKey, TouchManager::TouchInfo>>
layers::LayersId TouchManager::sCaptureTouchLayersId;
TimeStamp TouchManager::sSingleTouchStartTimeStamp;
LayoutDeviceIntPoint TouchManager::sSingleTouchStartPoint;
bool TouchManager::sPrecedingTouchPointerDownConsumedByContent = false;
/*static*/
void TouchManager::InitializeStatics() {
@ -271,7 +273,11 @@ bool TouchManager::PreHandleEvent(WidgetEvent* aEvent, nsEventStatus* aStatus,
// event, all subsequent touch events will use the same layers id.
sCaptureTouchLayersId = aEvent->mLayersId;
sSingleTouchStartTimeStamp = aEvent->mTimeStamp;
sSingleTouchStartPoint = aEvent->AsTouchEvent()->mTouches[0]->mRefPoint;
sSingleTouchStartPoint = touchEvent->mTouches[0]->mRefPoint;
const PointerInfo* pointerInfo = PointerEventHandler::GetPointerInfo(
touchEvent->mTouches[0]->Identifier());
sPrecedingTouchPointerDownConsumedByContent =
pointerInfo && pointerInfo->mPreventMouseEventByContent;
} else {
touchEvent->mLayersId = sCaptureTouchLayersId;
sSingleTouchStartTimeStamp = TimeStamp();
@ -585,4 +591,9 @@ bool TouchManager::IsSingleTapEndToDoDefault(
return true;
}
/* static */
bool TouchManager::IsPrecedingTouchPointerDownConsumedByContent() {
return sPrecedingTouchPointerDownConsumedByContent;
}
} // namespace mozilla

View file

@ -69,6 +69,10 @@ class TouchManager {
// dispatch mouse events for touch events synthesized without APZ.
static bool IsSingleTapEndToDoDefault(const WidgetTouchEvent* aTouchEndEvent);
// Returns true if the preceding `pointerdown` was consumed by content of
// the last active pointers of touches.
static bool IsPrecedingTouchPointerDownConsumedByContent();
private:
void EvictTouches(dom::Document* aLimitToDocument = nullptr);
static void EvictTouchPoint(RefPtr<dom::Touch>& aTouch,
@ -89,10 +93,20 @@ class TouchManager {
static layers::LayersId sCaptureTouchLayersId;
// The last start of a single tap. This will be set to "Null" if the tap is
// consumed or becomes not a single tap.
// NOTE: This is used for touches without APZ, i.e., if they are synthesized
// in-process for tests.
static TimeStamp sSingleTouchStartTimeStamp;
// The last start point of the single tap tracked with
// sSingleTouchStartTimeStamp.
// NOTE: This is used for touches without APZ, i.e., if they are synthesized
// in-process for tests.
static LayoutDeviceIntPoint sSingleTouchStartPoint;
// Whether the preceding `pointerdown` of the last active touches is consumed
// by content or not. If APZ is enabled, same state is managed by
// APZEventState.
// NOTE: This is used for touches without APZ, i.e., if they are synthesized
// in-process for tests.
static bool sPrecedingTouchPointerDownConsumedByContent;
};
} // namespace mozilla