Bug 1704065 - Stop consuming pan gesture events in SwipeTracker if the swiping direction is not allowed to navigate. r=spohl

This is what Safari does, i.e. once after a swipe gesture has started to a
direction where there is no navigation history, then even if the swipe gesture
switched to the opposite direction where navigation can happen, swipe navigation
will never happen. Chrome looks like they are trying to do swipe navigation in
such cases, but it looks like they sometimes fail it, the swipe navigation
indicator (an arrow image) doesn't show up sometime when the swipe navigation
goes to the opposite direction.

This change fixes stuck-in-overscroll situations where user swipes horizontally
to a direction where navigation is impossible during overscrolling, but doesn't
fix situations where navigation is possible but the user cancels the navigation.
To fix the later situations we need a different fix apart from this change.

The mochitest in this change doesn't test the stuck-in-overscroll situations at
all because we need to write a browser mochitest since swipe gesture is
implemented as a browser feature but unfortunately SpecialPowers.snapshotWindow
doesn't capture overscrolled gutter regions for some reasons, so instead the
test checks whether the swipe gesture module keeps capturing wheel events in the
situations where navigation is impossible.

Differential Revision: https://phabricator.services.mozilla.com/D113636
This commit is contained in:
Hiroyuki Ikezoe 2021-04-30 09:08:08 +00:00
Родитель a5ceb4d5dd
Коммит 023bd8d65c
4 изменённых файлов: 266 добавлений и 7 удалений

Просмотреть файл

@ -377,6 +377,87 @@ function synthesizeNativeWheel(aTarget, aX, aY, aDeltaX, aDeltaY, aObserver) {
return true;
}
// Synthesizes a native pan gesture event and returns immediately.
// NOTE: This works only on Mac.
// You can specify kCGScrollPhaseBegan = 1, kCGScrollPhaseChanged = 2 and
// kCGScrollPhaseEnded = 4 for |aPhase|.
function synthesizeNativePanGestureEvent(
aTarget,
aX,
aY,
aDeltaX,
aDeltaY,
aPhase,
aObserver
) {
if (getPlatform() != "mac") {
throw new Error(
`synthesizeNativePanGestureEvent doesn't work on ${getPlatform()}`
);
}
var pt = coordinatesRelativeToScreen({
offsetX: aX,
offsetY: aY,
target: aTarget,
});
if (aDeltaX && aDeltaY) {
throw new Error(
"Simultaneous panning of horizontal and vertical is not supported."
);
}
aDeltaX = nativeScrollUnits(aTarget, aDeltaX);
aDeltaY = nativeScrollUnits(aTarget, aDeltaY);
var element = elementForTarget(aTarget);
var utils = utilsForTarget(aTarget);
utils.sendNativeMouseScrollEvent(
pt.x,
pt.y,
aPhase,
aDeltaX,
aDeltaY,
0 /* deltaZ */,
0 /* modifiers */,
0 /* scroll event unit pixel */,
element,
aObserver
);
return true;
}
// Synthesizes a native pan gesture event and resolve the returned promise once the
// request has been successfully made to the OS.
function promiseNativePanGestureEventAndWaitForObserver(
aElement,
aX,
aY,
aDeltaX,
aDeltaY,
aPhase
) {
return new Promise(resolve => {
var observer = {
observe(aSubject, aTopic, aData) {
if (aTopic == "mousescrollevent") {
resolve();
}
},
};
synthesizeNativePanGestureEvent(
aElement,
aX,
aY,
aDeltaX,
aDeltaY,
aPhase,
observer
);
});
}
// Synthesizes a native mousewheel event and resolve the returned promise once the
// request has been successfully made to the OS. This does not necessarily
// guarantee that the OS generates the event we requested. See

Просмотреть файл

@ -88,8 +88,9 @@ bool SwipeTracker::ComputeSwipeSuccess() const {
}
nsEventStatus SwipeTracker::ProcessEvent(const PanGestureInput& aEvent) {
// If the fingers have already been lifted, don't process this event for swiping.
if (!mEventsAreControllingSwipe) {
// If the fingers have already been lifted or the swipe direction is where
// navigation is impossible, don't process this event for swiping.
if (!mEventsAreControllingSwipe || !SwipingInAllowedDirection()) {
// Return nsEventStatus_eConsumeNoDefault for events from the swipe gesture
// and nsEventStatus_eIgnore for events of subsequent scroll gestures.
if (aEvent.mType == PanGestureInput::PANGESTURE_MAYSTART ||
@ -100,9 +101,6 @@ nsEventStatus SwipeTracker::ProcessEvent(const PanGestureInput& aEvent) {
}
double delta = -aEvent.mPanDisplacement.x / mWidget.BackingScaleFactor() / kWholePagePixelSize;
if (!SwipingInAllowedDirection()) {
delta /= kRubberBandResistanceFactor;
}
mGestureAmount = ClampToAllowedRange(mGestureAmount + delta);
SendSwipeEvent(eSwipeGestureUpdate, 0, mGestureAmount, aEvent.mTimeStamp);
@ -112,9 +110,8 @@ nsEventStatus SwipeTracker::ProcessEvent(const PanGestureInput& aEvent) {
mLastEventTimeStamp = aEvent.mTimeStamp;
} else {
mEventsAreControllingSwipe = false;
bool didSwipeSucceed = SwipingInAllowedDirection() && ComputeSwipeSuccess();
double targetValue = 0.0;
if (didSwipeSucceed) {
if (ComputeSwipeSuccess()) {
// Let's use same timestamp as previous event because this is caused by
// the preceding event.
SendSwipeEvent(eSwipeGesture, mSwipeDirection, 0.0, aEvent.mTimeStamp);

Просмотреть файл

@ -1,2 +1,6 @@
[browser_test_clipboardcache.js]
skip-if = os == 'android' || (os == 'linux' && ccov) || tsan # Bug 1613516, the test consistently timeouts on Linux coverage builds.
[browser_test_swipe_gesture.js]
run-if = (os == 'mac')
support-files =
!/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js

Просмотреть файл

@ -0,0 +1,177 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
/* import-globals-from ../../..//gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js */
"use strict";
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js",
this
);
function waitForWhile() {
return new Promise(resolve => {
requestIdleCallback(resolve, { timeout: 300 });
});
}
// From https://developer.apple.com/documentation/coregraphics/cgscrollphase/kcgscrollphasebegan?language=occ , etc.
const kCGScrollPhaseBegan = 1;
const kCGScrollPhaseChanged = 2;
const kCGScrollPhaseEnded = 4;
async function panRightToLeft(aElement, aX, aY) {
await promiseNativePanGestureEventAndWaitForObserver(
aElement,
aX,
aY,
-50,
0,
kCGScrollPhaseBegan
);
await promiseNativePanGestureEventAndWaitForObserver(
aElement,
aX,
aY,
-50,
0,
kCGScrollPhaseChanged
);
await promiseNativePanGestureEventAndWaitForObserver(
aElement,
aX,
aY,
-50,
0,
kCGScrollPhaseChanged
);
await promiseNativePanGestureEventAndWaitForObserver(
aElement,
aX,
aY,
0,
0,
kCGScrollPhaseEnded
);
}
async function panLeftToRight(aElement, aX, aY) {
await promiseNativePanGestureEventAndWaitForObserver(
aElement,
aX,
aY,
50,
0,
kCGScrollPhaseBegan
);
await promiseNativePanGestureEventAndWaitForObserver(
aElement,
aX,
aY,
50,
0,
kCGScrollPhaseChanged
);
await promiseNativePanGestureEventAndWaitForObserver(
aElement,
aX,
aY,
50,
0,
kCGScrollPhaseChanged
);
await promiseNativePanGestureEventAndWaitForObserver(
aElement,
aX,
aY,
0,
0,
kCGScrollPhaseEnded
);
}
add_task(async () => {
await SpecialPowers.pushPrefEnv({
set: [
["browser.gesture.swipe.left", "Browser:BackOrBackDuplicate"],
["browser.gesture.swipe.eight", "Browser:ForwardOrForwardDuplicate"],
],
});
const firstPage = "about:about";
const secondPage = "about:mozilla";
const tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
firstPage,
true /* waitForLoad */
);
BrowserTestUtils.loadURI(tab.linkedBrowser, secondPage);
await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, secondPage);
// Make sure we can go back to the previous page.
ok(gBrowser.webNavigation.canGoBack);
// and we cannot go forward to the next page.
ok(!gBrowser.webNavigation.canGoForward);
let wheelEventCount = 0;
tab.linkedBrowser.addEventListener("wheel", () => {
wheelEventCount++;
});
// Try to navigate forward.
await panRightToLeft(tab.linkedBrowser, 100, 100);
// NOTE: The last kCGScrollPhaseEnded shouldn't fire a wheel event since
// its delta is zero.
is(wheelEventCount, 3, "Received 3 wheel events");
await waitForWhile();
// Make sure any navigation didn't happen.
is(tab.linkedBrowser.currentURI.spec, secondPage);
// Try to navigate backward.
wheelEventCount = 0;
let startLoadingPromise = BrowserTestUtils.browserStarted(
tab.linkedBrowser,
firstPage
);
await panLeftToRight(tab.linkedBrowser, 100, 100);
// NOTE: We only get a wheel event for the kCGScrollPhaseBegan, rest of events
// have been captured by the swipe gesture module.
is(wheelEventCount, 1, "Received a wheel event");
// Make sure the gesture triggered going back to the previous page.
await startLoadingPromise;
await BrowserTestUtils.browserStopped(tab.linkedBrowser, firstPage);
ok(gBrowser.webNavigation.canGoForward);
// Now try to navigate forward again.
wheelEventCount = 0;
startLoadingPromise = BrowserTestUtils.browserStarted(
tab.linkedBrowser,
secondPage
);
await panRightToLeft(tab.linkedBrowser, 100, 100);
is(wheelEventCount, 1, "Received a wheel event");
await startLoadingPromise;
await BrowserTestUtils.browserStopped(tab.linkedBrowser, secondPage);
ok(gBrowser.webNavigation.canGoBack);
// Now try to navigate backward again but with preventDefault-ed event
// handler.
wheelEventCount = 0;
tab.linkedBrowser.addEventListener("wheel", event => {
event.preventDefault();
});
await panLeftToRight(tab.linkedBrowser, 100, 100);
is(wheelEventCount, 3, "Received all wheel events");
await waitForWhile();
// Make sure any navigation didn't happen.
is(tab.linkedBrowser.currentURI.spec, secondPage);
BrowserTestUtils.removeTab(tab);
});