Bug 1168182 - Bind wheel event targets to wheel transactions. r=masayuki,smaug

- Create wheel transactions for wheel events handled by APZ.
 - Group wheel events with the current wheel transaction, so that all
   wheel events in a wheel transaction are fired to the same element.
 - Store the current event target for the first event in a wheel
   transaction to be used for subsequent events.
 - Add the dom.event.wheel-event-groups.enabled preference as a feature
   flag for this behavior.

Differential Revision: https://phabricator.services.mozilla.com/D163484
This commit is contained in:
Dan Robertson 2023-03-20 12:19:36 +00:00
parent 17db57410f
commit 30e2548477
11 changed files with 219 additions and 55 deletions

View file

@ -2745,7 +2745,7 @@ nsIFrame* EventStateManager::ComputeScrollTargetAndMayAdjustWheelEvent(
// out of the frame, or when more than "mousewheel.transaction.timeout" // out of the frame, or when more than "mousewheel.transaction.timeout"
// milliseconds have passed after the last operation, even if the mouse // milliseconds have passed after the last operation, even if the mouse
// hasn't moved. // hasn't moved.
nsIFrame* lastScrollFrame = WheelTransaction::GetTargetFrame(); nsIFrame* lastScrollFrame = WheelTransaction::GetScrollTargetFrame();
if (lastScrollFrame) { if (lastScrollFrame) {
nsIScrollableFrame* scrollableFrame = nsIScrollableFrame* scrollableFrame =
lastScrollFrame->GetScrollTargetFrame(); lastScrollFrame->GetScrollTargetFrame();
@ -2927,7 +2927,9 @@ void EventStateManager::DoScrollText(nsIScrollableFrame* aScrollableFrame,
MOZ_ASSERT(scrollFrame); MOZ_ASSERT(scrollFrame);
AutoWeakFrame scrollFrameWeak(scrollFrame); AutoWeakFrame scrollFrameWeak(scrollFrame);
if (!WheelTransaction::WillHandleDefaultAction(aEvent, scrollFrameWeak)) { AutoWeakFrame eventFrameWeak(mCurrentTarget);
if (!WheelTransaction::WillHandleDefaultAction(aEvent, scrollFrameWeak,
eventFrameWeak)) {
return; return;
} }
@ -3718,6 +3720,14 @@ nsresult EventStateManager::PostHandleEvent(nsPresContext* aPresContext,
case WheelPrefs::ACTION_NONE: case WheelPrefs::ACTION_NONE:
default: default:
bool allDeltaOverflown = false; bool allDeltaOverflown = false;
if (wheelEvent->mDeltaX != 0.0 || wheelEvent->mDeltaY != 0.0) {
if (frameToScroll) {
WheelTransaction::WillHandleDefaultAction(
wheelEvent, frameToScroll, mCurrentTarget);
} else {
WheelTransaction::EndTransaction();
}
}
if (wheelEvent->mFlags.mHandledByAPZ) { if (wheelEvent->mFlags.mHandledByAPZ) {
if (wheelEvent->mCanTriggerSwipe) { if (wheelEvent->mCanTriggerSwipe) {
// For events that can trigger swipes, APZ needs to know whether // For events that can trigger swipes, APZ needs to know whether
@ -5859,6 +5869,7 @@ void EventStateManager::ContentRemoved(Document* aDocument,
IMEStateManager::OnRemoveContent(*presContext, IMEStateManager::OnRemoveContent(*presContext,
MOZ_KnownLive(*aContent->AsElement())); MOZ_KnownLive(*aContent->AsElement()));
} }
WheelTransaction::OnRemoveElement(aContent);
} }
// inform the focus manager that the content is being removed. If this // inform the focus manager that the content is being removed. If this

View file

@ -111,7 +111,8 @@ WheelHandlingUtils::GetDisregardedWheelScrollDirection(const nsIFrame* aFrame) {
/* mozilla::WheelTransaction */ /* mozilla::WheelTransaction */
/******************************************************************/ /******************************************************************/
AutoWeakFrame WheelTransaction::sTargetFrame(nullptr); AutoWeakFrame WheelTransaction::sScrollTargetFrame(nullptr);
AutoWeakFrame WheelTransaction::sEventTargetFrame(nullptr);
uint32_t WheelTransaction::sTime = 0; uint32_t WheelTransaction::sTime = 0;
uint32_t WheelTransaction::sMouseMoved = 0; uint32_t WheelTransaction::sMouseMoved = 0;
nsITimer* WheelTransaction::sTimer = nullptr; nsITimer* WheelTransaction::sTimer = nullptr;
@ -128,13 +129,28 @@ bool WheelTransaction::OutOfTime(uint32_t aBaseTime, uint32_t aThreshold) {
void WheelTransaction::OwnScrollbars(bool aOwn) { sOwnScrollbars = aOwn; } void WheelTransaction::OwnScrollbars(bool aOwn) { sOwnScrollbars = aOwn; }
/* static */ /* static */
void WheelTransaction::BeginTransaction(nsIFrame* aTargetFrame, void WheelTransaction::BeginTransaction(nsIFrame* aScrollTargetFrame,
nsIFrame* aEventTargetFrame,
const WidgetWheelEvent* aEvent) { const WidgetWheelEvent* aEvent) {
NS_ASSERTION(!sTargetFrame, "previous transaction is not finished!"); NS_ASSERTION(!sScrollTargetFrame && !sEventTargetFrame,
"previous transaction is not finished!");
MOZ_ASSERT(aEvent->mMessage == eWheel, MOZ_ASSERT(aEvent->mMessage == eWheel,
"Transaction must be started with a wheel event"); "Transaction must be started with a wheel event");
ScrollbarsForWheel::OwnWheelTransaction(false); ScrollbarsForWheel::OwnWheelTransaction(false);
sTargetFrame = aTargetFrame; sScrollTargetFrame = aScrollTargetFrame;
// Only set the static event target if wheel event groups are enabled.
if (StaticPrefs::dom_event_wheel_event_groups_enabled()) {
// Set a static event target for the wheel transaction. This will be used
// to override the event target frame when computing the event target from
// input coordinates. When this preference is not set or there is no stored
// event target for the current wheel transaction, the event target will
// not be overridden by the current wheel transaction, but will be computed
// from the input coordinates.
sEventTargetFrame = aEventTargetFrame;
}
sScrollSeriesCounter = 0; sScrollSeriesCounter = 0;
if (!UpdateTransaction(aEvent)) { if (!UpdateTransaction(aEvent)) {
NS_ERROR("BeginTransaction is called even cannot scroll the frame"); NS_ERROR("BeginTransaction is called even cannot scroll the frame");
@ -144,7 +160,7 @@ void WheelTransaction::BeginTransaction(nsIFrame* aTargetFrame,
/* static */ /* static */
bool WheelTransaction::UpdateTransaction(const WidgetWheelEvent* aEvent) { bool WheelTransaction::UpdateTransaction(const WidgetWheelEvent* aEvent) {
nsIFrame* scrollToFrame = GetTargetFrame(); nsIFrame* scrollToFrame = GetScrollTargetFrame();
nsIScrollableFrame* scrollableFrame = scrollToFrame->GetScrollTargetFrame(); nsIScrollableFrame* scrollableFrame = scrollToFrame->GetScrollTargetFrame();
if (scrollableFrame) { if (scrollableFrame) {
scrollToFrame = do_QueryFrame(scrollableFrame); scrollToFrame = do_QueryFrame(scrollableFrame);
@ -188,7 +204,8 @@ void WheelTransaction::EndTransaction() {
if (sTimer) { if (sTimer) {
sTimer->Cancel(); sTimer->Cancel();
} }
sTargetFrame = nullptr; sScrollTargetFrame = nullptr;
sEventTargetFrame = nullptr;
sScrollSeriesCounter = 0; sScrollSeriesCounter = 0;
if (sOwnScrollbars) { if (sOwnScrollbars) {
sOwnScrollbars = false; sOwnScrollbars = false;
@ -199,13 +216,16 @@ void WheelTransaction::EndTransaction() {
/* static */ /* static */
bool WheelTransaction::WillHandleDefaultAction( bool WheelTransaction::WillHandleDefaultAction(
WidgetWheelEvent* aWheelEvent, AutoWeakFrame& aTargetWeakFrame) { WidgetWheelEvent* aWheelEvent, AutoWeakFrame& aScrollTargetWeakFrame,
nsIFrame* lastTargetFrame = GetTargetFrame(); AutoWeakFrame& aEventTargetWeakFrame) {
nsIFrame* lastTargetFrame = GetScrollTargetFrame();
if (!lastTargetFrame) { if (!lastTargetFrame) {
BeginTransaction(aTargetWeakFrame.GetFrame(), aWheelEvent); BeginTransaction(aScrollTargetWeakFrame.GetFrame(),
} else if (lastTargetFrame != aTargetWeakFrame.GetFrame()) { aEventTargetWeakFrame.GetFrame(), aWheelEvent);
} else if (lastTargetFrame != aScrollTargetWeakFrame.GetFrame()) {
EndTransaction(); EndTransaction();
BeginTransaction(aTargetWeakFrame.GetFrame(), aWheelEvent); BeginTransaction(aScrollTargetWeakFrame.GetFrame(),
aEventTargetWeakFrame.GetFrame(), aWheelEvent);
} else { } else {
UpdateTransaction(aWheelEvent); UpdateTransaction(aWheelEvent);
} }
@ -214,7 +234,7 @@ bool WheelTransaction::WillHandleDefaultAction(
// UpdateTransaction() fires MozMouseScrollFailed event which is for // UpdateTransaction() fires MozMouseScrollFailed event which is for
// automated testing. In the event handler, the target frame might be // automated testing. In the event handler, the target frame might be
// destroyed. Then, the caller shouldn't try to handle the default action. // destroyed. Then, the caller shouldn't try to handle the default action.
if (!aTargetWeakFrame.IsAlive()) { if (!aScrollTargetWeakFrame.IsAlive()) {
EndTransaction(); EndTransaction();
return false; return false;
} }
@ -224,7 +244,7 @@ bool WheelTransaction::WillHandleDefaultAction(
/* static */ /* static */
void WheelTransaction::OnEvent(WidgetEvent* aEvent) { void WheelTransaction::OnEvent(WidgetEvent* aEvent) {
if (!sTargetFrame) { if (!sScrollTargetFrame) {
return; return;
} }
@ -255,13 +275,17 @@ void WheelTransaction::OnEvent(WidgetEvent* aEvent) {
// terminate the scrollwheel transaction. // terminate the scrollwheel transaction.
LayoutDeviceIntPoint pt = GetScreenPoint(mouseEvent); LayoutDeviceIntPoint pt = GetScreenPoint(mouseEvent);
auto r = LayoutDeviceIntRect::FromAppUnitsToNearest( auto r = LayoutDeviceIntRect::FromAppUnitsToNearest(
sTargetFrame->GetScreenRectInAppUnits(), sScrollTargetFrame->GetScreenRectInAppUnits(),
sTargetFrame->PresContext()->AppUnitsPerDevPixel()); sScrollTargetFrame->PresContext()->AppUnitsPerDevPixel());
if (!r.Contains(pt)) { if (!r.Contains(pt)) {
EndTransaction(); EndTransaction();
return; return;
} }
// For mouse move events where the wheel transaction is still valid, the
// stored event target should be reset.
sEventTargetFrame = nullptr;
// If the cursor is moving inside the frame, and it is less than // If the cursor is moving inside the frame, and it is less than
// ignoremovedelay milliseconds since the last scroll operation, ignore // ignoremovedelay milliseconds since the last scroll operation, ignore
// the mouse move; otherwise, record the current mouse move time to be // the mouse move; otherwise, record the current mouse move time to be
@ -291,35 +315,55 @@ void WheelTransaction::OnEvent(WidgetEvent* aEvent) {
} }
} }
/* static */
void WheelTransaction::OnRemoveElement(nsIContent* aContent) {
// If dom.event.wheel-event-groups.enabled is not set or we have no current
// wheel event transaction there is no internal state to be updated.
if (!sEventTargetFrame) {
return;
}
if (sEventTargetFrame->GetContent() == aContent) {
// Only invalidate the wheel transaction event target frame when the
// remove target is the event target of the wheel event group. The
// scroll target frame of the wheel event group may still be valid.
//
// With the stored event target unset, the target for any following
// events will be the frame found using the input coordinates.
sEventTargetFrame = nullptr;
}
}
/* static */ /* static */
void WheelTransaction::Shutdown() { NS_IF_RELEASE(sTimer); } void WheelTransaction::Shutdown() { NS_IF_RELEASE(sTimer); }
/* static */ /* static */
void WheelTransaction::OnFailToScrollTarget() { void WheelTransaction::OnFailToScrollTarget() {
MOZ_ASSERT(sTargetFrame, "We don't have mouse scrolling transaction"); MOZ_ASSERT(sScrollTargetFrame, "We don't have mouse scrolling transaction");
if (StaticPrefs::test_mousescroll()) { if (StaticPrefs::test_mousescroll()) {
// This event is used for automated tests, see bug 442774. // This event is used for automated tests, see bug 442774.
nsContentUtils::DispatchEventOnlyToChrome( nsContentUtils::DispatchEventOnlyToChrome(
sTargetFrame->GetContent()->OwnerDoc(), sTargetFrame->GetContent(), sScrollTargetFrame->GetContent()->OwnerDoc(),
u"MozMouseScrollFailed"_ns, CanBubble::eYes, Cancelable::eYes); sScrollTargetFrame->GetContent(), u"MozMouseScrollFailed"_ns,
CanBubble::eYes, Cancelable::eYes);
} }
// The target frame might be destroyed in the event handler, at that time, // The target frame might be destroyed in the event handler, at that time,
// we need to finish the current transaction // we need to finish the current transaction
if (!sTargetFrame) { if (!sScrollTargetFrame) {
EndTransaction(); EndTransaction();
} }
} }
/* static */ /* static */
void WheelTransaction::OnTimeout(nsITimer* aTimer, void* aClosure) { void WheelTransaction::OnTimeout(nsITimer* aTimer, void* aClosure) {
if (!sTargetFrame) { if (!sScrollTargetFrame) {
// The transaction target was destroyed already // The transaction target was destroyed already
EndTransaction(); EndTransaction();
return; return;
} }
// Store the sTargetFrame, the variable becomes null in EndTransaction. // Store the sScrollTargetFrame, the variable becomes null in EndTransaction.
nsIFrame* frame = sTargetFrame; nsIFrame* frame = sScrollTargetFrame;
// We need to finish current transaction before DOM event firing. Because // We need to finish current transaction before DOM event firing. Because
// the next DOM event might create strange situation for us. // the next DOM event might create strange situation for us.
MayEndTransaction(); MayEndTransaction();
@ -388,7 +432,7 @@ double WheelTransaction::ComputeAcceleratedWheelDelta(double aDelta,
/* static */ /* static */
DeltaValues WheelTransaction::OverrideSystemScrollSpeed( DeltaValues WheelTransaction::OverrideSystemScrollSpeed(
WidgetWheelEvent* aEvent) { WidgetWheelEvent* aEvent) {
MOZ_ASSERT(sTargetFrame, "We don't have mouse scrolling transaction"); MOZ_ASSERT(sScrollTargetFrame, "We don't have mouse scrolling transaction");
// If the event doesn't scroll to both X and Y, we don't need to do anything // If the event doesn't scroll to both X and Y, we don't need to do anything
// here. // here.
@ -446,7 +490,7 @@ void ScrollbarsForWheel::SetActiveScrollTarget(
/* static */ /* static */
void ScrollbarsForWheel::MayInactivate() { void ScrollbarsForWheel::MayInactivate() {
if (!sOwnWheelTransaction && WheelTransaction::GetTargetFrame()) { if (!sOwnWheelTransaction && WheelTransaction::GetScrollTargetFrame()) {
WheelTransaction::OwnScrollbars(true); WheelTransaction::OwnScrollbars(true);
} else { } else {
Inactivate(); Inactivate();

View file

@ -118,23 +118,40 @@ class ScrollbarsForWheel {
class WheelTransaction { class WheelTransaction {
public: public:
static nsIFrame* GetTargetFrame() { return sTargetFrame; } /**
* Get the target scroll frame for this wheel transaction. This should
* the the scrollable fame that will scroll for all wheel events in
* this wheel transaction.
*/
static nsIFrame* GetScrollTargetFrame() { return sScrollTargetFrame; }
/*
* The event target to use for all wheel events in this wheel transaction.
* This should be the event target for all wheel events in this wheel
* transaction. Note that this frame will likely be a child of the
* scrollable frame.
*/
static nsIFrame* GetEventTargetFrame() { return sEventTargetFrame; }
static void EndTransaction(); static void EndTransaction();
/** /**
* WillHandleDefaultAction() is called before handling aWheelEvent on * WillHandleDefaultAction() is called before handling aWheelEvent on
* aTargetFrame. * aScrollTargetWeakFrame given the event target aEventTargetWeakFrame.
* *
* @return false if the caller cannot continue to handle the default * @return false if the caller cannot continue to handle the default
* action. Otherwise, true. * action. Otherwise, true.
*/ */
static bool WillHandleDefaultAction(WidgetWheelEvent* aWheelEvent, static bool WillHandleDefaultAction(WidgetWheelEvent* aWheelEvent,
AutoWeakFrame& aTargetWeakFrame); AutoWeakFrame& aScrollTargetWeakFrame,
AutoWeakFrame& aEventTargetWeakFrame);
static bool WillHandleDefaultAction(WidgetWheelEvent* aWheelEvent, static bool WillHandleDefaultAction(WidgetWheelEvent* aWheelEvent,
nsIFrame* aTargetFrame) { nsIFrame* aScrollTargetFrame,
AutoWeakFrame targetWeakFrame(aTargetFrame); nsIFrame* aEventTargetFrame) {
return WillHandleDefaultAction(aWheelEvent, targetWeakFrame); AutoWeakFrame scrollTargetWeakFrame(aScrollTargetFrame);
AutoWeakFrame eventTargetWeakFrame(aEventTargetFrame);
return WillHandleDefaultAction(aWheelEvent, scrollTargetWeakFrame,
eventTargetWeakFrame);
} }
static void OnEvent(WidgetEvent* aEvent); static void OnEvent(WidgetEvent* aEvent);
static void OnRemoveElement(nsIContent* aContent);
static void Shutdown(); static void Shutdown();
static void OwnScrollbars(bool aOwn); static void OwnScrollbars(bool aOwn);
@ -142,7 +159,8 @@ class WheelTransaction {
static DeltaValues AccelerateWheelDelta(WidgetWheelEvent* aEvent); static DeltaValues AccelerateWheelDelta(WidgetWheelEvent* aEvent);
protected: protected:
static void BeginTransaction(nsIFrame* aTargetFrame, static void BeginTransaction(nsIFrame* aScrollTargetFrame,
nsIFrame* aEventTargetFrame,
const WidgetWheelEvent* aEvent); const WidgetWheelEvent* aEvent);
// Be careful, UpdateTransaction may fire a DOM event, therefore, the target // Be careful, UpdateTransaction may fire a DOM event, therefore, the target
// frame might be destroyed in the event handler. // frame might be destroyed in the event handler.
@ -157,7 +175,23 @@ class WheelTransaction {
static double ComputeAcceleratedWheelDelta(double aDelta, int32_t aFactor); static double ComputeAcceleratedWheelDelta(double aDelta, int32_t aFactor);
static bool OutOfTime(uint32_t aBaseTime, uint32_t aThreshold); static bool OutOfTime(uint32_t aBaseTime, uint32_t aThreshold);
static AutoWeakFrame sTargetFrame; /**
* The scrollable element the current wheel event group is bound to.
*/
static AutoWeakFrame sScrollTargetFrame;
/**
* The initial target of the first wheel event in the wheel event group.
* This frame is typically a child of the scrollable element. The wheel
* event should target the topmost-event-target. For a wheel event
* group, we'll use this target for the entire group.
*
* See https://w3c.github.io/uievents/#topmost-event-target and
* https://w3c.github.io/uievents/#event-type-wheel for details.
*
* Note: this is only populated if dom.event.wheel-event-groups.enabled is
* set.
*/
static AutoWeakFrame sEventTargetFrame;
static uint32_t sTime; // in milliseconds static uint32_t sTime; // in milliseconds
static uint32_t sMouseMoved; // in milliseconds static uint32_t sMouseMoved; // in milliseconds
static nsITimer* sTimer; static nsITimer* sTimer;

View file

@ -2,9 +2,10 @@
<html> <html>
<!-- <!--
https://bugzilla.mozilla.org/show_bug.cgi?id=1013412 https://bugzilla.mozilla.org/show_bug.cgi?id=1013412
https://bugzilla.mozilla.org/show_bug.cgi?id=1168182
--> -->
<head> <head>
<title>Test for Bug 1013412</title> <title>Test for Bug 1013412 and 1168182</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script> <script src="/tests/SimpleTest/SimpleTest.js"></script>
<script src="/tests/SimpleTest/paint_listener.js"></script> <script src="/tests/SimpleTest/paint_listener.js"></script>
<script src="/tests/SimpleTest/EventUtils.js"></script> <script src="/tests/SimpleTest/EventUtils.js"></script>
@ -44,9 +45,10 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1013412
</head> </head>
<body> <body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1013412">Mozilla Bug 1013412</a> <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1013412">Mozilla Bug 1013412</a>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1168182">Mozilla Bug 1168182</a>
<p id="display"></p> <p id="display"></p>
<div id="content"> <div id="content">
<p>Scrolling the page should be async, but scrolling over the dark circle should not scroll the page and instead rotate the white ball.</p> <p>Scrolling the page should be async and scrolling over the dark circle should scroll the page and avoid rotating the white ball.</p>
<div id="scroller"> <div id="scroller">
<div id="scrollbox"> <div id="scrollbox">
<div id="circle"></div> <div id="circle"></div>
@ -56,8 +58,6 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1013412
<pre id="test"> <pre id="test">
<script type="application/javascript"> <script type="application/javascript">
/** Test for Bug 1013412 **/
var rotation = 0; var rotation = 0;
var rotationAdjusted = false; var rotationAdjusted = false;
@ -80,7 +80,8 @@ document.getElementById("scrollbox").addEventListener("wheel", function (e) {
var iteration = 0; var iteration = 0;
function runTest() { function runTest() {
var content = document.getElementById('content'); var content = document.getElementById('content');
if (iteration < 300) { // enough iterations that we would scroll to the bottom of 'content' // enough iterations that we would scroll to the bottom of 'content'
if (iteration < 600 && content.scrollTop != content.scrollTopMax) {
iteration++; iteration++;
sendWheelAndPaint(content, 100, 10, sendWheelAndPaint(content, 100, 10,
{ deltaMode: WheelEvent.DOM_DELTA_LINE, { deltaMode: WheelEvent.DOM_DELTA_LINE,
@ -89,8 +90,8 @@ function runTest() {
return; return;
} }
var scrollbox = document.getElementById('scrollbox'); var scrollbox = document.getElementById('scrollbox');
is(content.scrollTop < content.scrollTopMax, true, "We should not have scrolled to the bottom of the scrollframe"); is(content.scrollTop, content.scrollTopMax, "We should have scrolled to the bottom of the scrollframe");
is(rotationAdjusted, true, "The rotation should have been adjusted"); is(rotationAdjusted, false, "The rotation should not have been adjusted");
SimpleTest.finish(); SimpleTest.finish();
} }
@ -98,7 +99,11 @@ function startTest() {
// If we allow smooth scrolling the "smooth" scrolling may cause the page to // If we allow smooth scrolling the "smooth" scrolling may cause the page to
// glide past the scrollbox (which is supposed to stop the scrolling) and so // glide past the scrollbox (which is supposed to stop the scrolling) and so
// we might end up at the bottom of the page. // we might end up at the bottom of the page.
SpecialPowers.pushPrefEnv({"set": [["general.smoothScroll", false], ["test.events.async.enabled", true]]}, runTest); SpecialPowers.pushPrefEnv({"set": [["general.smoothScroll", false],
["test.events.async.enabled", true],
["mousewheel.transaction.timeout", 100000],
["dom.event.wheel-event-groups.enabled", true]]},
runTest);
} }
SimpleTest.waitForExplicitFinish(); SimpleTest.waitForExplicitFinish();

View file

@ -2,9 +2,10 @@
<html> <html>
<!-- <!--
https://bugzilla.mozilla.org/show_bug.cgi?id=1013412 https://bugzilla.mozilla.org/show_bug.cgi?id=1013412
https://bugzilla.mozilla.org/show_bug.cgi?id=1168182
--> -->
<head> <head>
<title>Test for Bug 1013412</title> <title>Test for Bug 1013412 and 1168182</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script> <script src="/tests/SimpleTest/SimpleTest.js"></script>
<script src="/tests/SimpleTest/EventUtils.js"></script> <script src="/tests/SimpleTest/EventUtils.js"></script>
<script src="/tests/SimpleTest/paint_listener.js"></script> <script src="/tests/SimpleTest/paint_listener.js"></script>
@ -45,7 +46,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1013412
</style> </style>
</head> </head>
<body> <body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1161206">Mozilla Bug 1161206</a> <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1013412">Mozilla Bug 1013412</a>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1168182">Mozilla Bug 1168182</a>
<p id="display"></p> <p id="display"></p>
<div id="content"> <div id="content">
<p>Scrolling the page should be async, but scrolling over the dark circle should not scroll the page and instead rotate the white ball.</p> <p>Scrolling the page should be async, but scrolling over the dark circle should not scroll the page and instead rotate the white ball.</p>
@ -79,12 +81,13 @@ document.getElementById("scrollbox").addEventListener("wheel", function(e) {
async function test() { async function test() {
var content = document.getElementById("content"); var content = document.getElementById("content");
for (let i = 0; i < 300; i++) { // enough iterations that we would scroll to the bottom of 'content' // enough iterations that we would scroll to the bottom of 'content'
for (let i = 0; i < 600 && content.scrollTop != content.scrollTopMax; i++) {
await promiseNativeWheelAndWaitForWheelEvent(content, 100, 150, 0, -5); await promiseNativeWheelAndWaitForWheelEvent(content, 100, 150, 0, -5);
} }
is(content.scrollTop > 0, true, "We should have scrolled down somewhat"); is(content.scrollTop > 0, true, "We should have scrolled down somewhat");
is(content.scrollTop < content.scrollTopMax, true, "We should not have scrolled to the bottom of the scrollframe"); is(content.scrollTop, content.scrollTopMax, "We should have scrolled to the bottom of the scrollframe");
is(rotationAdjusted, true, "The rotation should have been adjusted"); is(rotationAdjusted, false, "The rotation should not have been adjusted");
} }
SimpleTest.waitForExplicitFinish(); SimpleTest.waitForExplicitFinish();
@ -92,7 +95,9 @@ SimpleTest.waitForExplicitFinish();
// If we allow smooth scrolling the "smooth" scrolling may cause the page to // If we allow smooth scrolling the "smooth" scrolling may cause the page to
// glide past the scrollbox (which is supposed to stop the scrolling) and so // glide past the scrollbox (which is supposed to stop the scrolling) and so
// we might end up at the bottom of the page. // we might end up at the bottom of the page.
pushPrefs([["general.smoothScroll", false]]) pushPrefs([["general.smoothScroll", false],
["mousewheel.transaction.timeout", 100000],
["dom.event.wheel-event-groups", true]])
.then(waitUntilApzStable) .then(waitUntilApzStable)
.then(test) .then(test)
.then(SimpleTest.finish, SimpleTest.finishWithFailure); .then(SimpleTest.finish, SimpleTest.finishWithFailure);

View file

@ -48,12 +48,19 @@ async function scrollWheelOver(element, deltaY) {
async function test() { async function test() {
var outer = document.getElementById("outer-frame"); var outer = document.getElementById("outer-frame");
var inner = document.getElementById("inner-frame"); var inner = document.getElementById("inner-frame");
var innerContent = document.getElementById("inner-content");
// Register a wheel event listener that records the target of // Register a wheel event listener that records the target of
// the last wheel event, so that we can make assertions about it. // the last wheel event, so that we can make assertions about it.
var lastWheelTarget; let lastWheelTarget;
var wheelTargetRecorder = function(e) { lastWheelTarget = e.target; }; let firstWheelTarget;
let wheelEventOccurred = false;
var wheelTargetRecorder = function(e) {
if (!wheelEventOccurred) {
firstWheelTarget = e.target;
wheelEventOccurred = true;
}
lastWheelTarget = e.target;
};
window.addEventListener("wheel", wheelTargetRecorder); window.addEventListener("wheel", wheelTargetRecorder);
// Scroll |outer| to the bottom. // Scroll |outer| to the bottom.
@ -61,8 +68,8 @@ async function test() {
await scrollWheelOver(outer, -10); await scrollWheelOver(outer, -10);
} }
// Verify that this has brought |inner| under the wheel. is(lastWheelTarget, firstWheelTarget,
is(lastWheelTarget, innerContent, "'inner-content' should have been brought under the wheel"); "target " + lastWheelTarget.id + " should be " + lastWheelTarget.id);
window.removeEventListener("wheel", wheelTargetRecorder); window.removeEventListener("wheel", wheelTargetRecorder);
// Immediately after, scroll it back up a bit. // Immediately after, scroll it back up a bit.
@ -129,7 +136,9 @@ SimpleTest.waitForExplicitFinish();
// inputs since this test is specifically testing things related to wheel // inputs since this test is specifically testing things related to wheel
// transactions. // transactions.
pushPrefs([["general.smoothScroll", false], pushPrefs([["general.smoothScroll", false],
["apz.test.mac.synth_wheel_input", true]]) ["apz.test.mac.synth_wheel_input", true],
["mousewheel.transaction.timeout", 1500],
["dom.event.wheel-event-groups", true]])
.then(waitUntilApzStable) .then(waitUntilApzStable)
.then(test) .then(test)
.then(SimpleTest.finish, SimpleTest.finishWithFailure); .then(SimpleTest.finish, SimpleTest.finishWithFailure);

View file

@ -7099,6 +7099,11 @@ nsresult PresShell::EventHandler::HandleEventUsingCoordinates(
return NS_OK; return NS_OK;
} }
// Wheel events only apply to elements. If this is a wheel event, attempt to
// update the event target from the current wheel transaction before we
// compute the element from the target frame.
eventTargetData.UpdateWheelEventTarget(aGUIEvent);
if (!eventTargetData.ComputeElementFromFrame(aGUIEvent)) { if (!eventTargetData.ComputeElementFromFrame(aGUIEvent)) {
return NS_OK; return NS_OK;
} }
@ -11813,6 +11818,34 @@ bool PresShell::EventHandler::EventTargetData::ComputeElementFromFrame(
return !!mContent; return !!mContent;
} }
void PresShell::EventHandler::EventTargetData::UpdateWheelEventTarget(
WidgetGUIEvent* aGUIEvent) {
MOZ_ASSERT(aGUIEvent);
if (aGUIEvent->mMessage != eWheel) {
return;
}
// If dom.event.wheel-event-groups.enabled is not set or the stored
// event target is removed, we will not get a event target frame from the
// wheel transaction here.
nsIFrame* groupFrame = WheelTransaction::GetEventTargetFrame();
if (!groupFrame) {
return;
}
// If the browsing context is no longer the same as the context of the
// current wheel transaction, do not override the event target.
if (!groupFrame->PresContext() || !groupFrame->PresShell() ||
groupFrame->PresContext() != GetPresContext()) {
return;
}
// If dom.event.wheel-event-groups.enabled is set and whe have a stored
// event target from the wheel transaction, override the event target.
SetFrameAndComputePresShellAndContent(groupFrame, aGUIEvent);
}
void PresShell::EventHandler::EventTargetData::UpdateTouchEventTarget( void PresShell::EventHandler::EventTargetData::UpdateTouchEventTarget(
WidgetGUIEvent* aGUIEvent) { WidgetGUIEvent* aGUIEvent) {
MOZ_ASSERT(aGUIEvent); MOZ_ASSERT(aGUIEvent);

View file

@ -2209,6 +2209,16 @@ class PresShell final : public nsStubDocumentObserver,
*/ */
void UpdateTouchEventTarget(WidgetGUIEvent* aGUIEvent); void UpdateTouchEventTarget(WidgetGUIEvent* aGUIEvent);
/**
* UpdateWheelEventTarget() updates mFrame, mPresShell, and mContent if
* aGUIEvent is a wheel event and aGUIEvent should be grouped with prior
* wheel events.
*
* @param aGUIEvent The handled event. If it's not a wheel event,
* this method does nothing.
*/
void UpdateWheelEventTarget(WidgetGUIEvent* aGUIEvent);
RefPtr<PresShell> mPresShell; RefPtr<PresShell> mPresShell;
nsIFrame* mFrame = nullptr; nsIFrame* mFrame = nullptr;
nsCOMPtr<nsIContent> mContent; nsCOMPtr<nsIContent> mContent;

View file

@ -2513,6 +2513,13 @@
value: @IS_NOT_NIGHTLY_BUILD@ value: @IS_NOT_NIGHTLY_BUILD@
mirror: always mirror: always
# Whether wheel event target's should be grouped. When enabled, all wheel
# events that occur in a given wheel transaction have the same event target.
- name: dom.event.wheel-event-groups.enabled
type: bool
value: @IS_NIGHTLY_BUILD@
mirror: always
# Whether WheelEvent should return pixels instead of lines for # Whether WheelEvent should return pixels instead of lines for
# WheelEvent.deltaX/Y/Z, when deltaMode hasn't been checked. # WheelEvent.deltaX/Y/Z, when deltaMode hasn't been checked.
# #

View file

@ -1,2 +1,2 @@
prefs: [apz.scrollend-event.content.enabled:true] prefs: [apz.scrollend-event.content.enabled:true, dom.event.wheel-event-groups.enabled:true, mousewheel.transaction.timeout:500]
lsan-allowed: [Alloc, MakeUnique, Malloc, Realloc, XPCNativeInterface::NewInstance, XPCNativeSet::NewInstance, XPCNativeSet::NewInstanceMutate, XPCWrappedNative::GetNewOrUsed, XPCWrappedNativeProto::GetNewOrUsed, mozilla::dom::WebExtensionInit::Init, mozilla::extensions::MatchPatternCore::MatchPatternCore, mozilla::extensions::MatchPatternSet::Constructor, mozilla::extensions::MatchPatternSet::GetPatterns, mozilla::extensions::ParseGlobs, mozilla::extensions::PermittedSchemes, mozilla::extensions::WebExtensionPolicy::Constructor, mozilla::extensions::WebExtensionPolicy::WebExtensionPolicy, mozilla::extensions::WebExtensionPolicyCore::WebExtensionPolicyCore, mozilla::net::nsStandardURL::TemplatedMutator, nsDynamicAtom::Create, nsJARURI::Mutator::SetSpecBaseCharset] lsan-allowed: [Alloc, MakeUnique, Malloc, Realloc, XPCNativeInterface::NewInstance, XPCNativeSet::NewInstance, XPCNativeSet::NewInstanceMutate, XPCWrappedNative::GetNewOrUsed, XPCWrappedNativeProto::GetNewOrUsed, mozilla::dom::WebExtensionInit::Init, mozilla::extensions::MatchPatternCore::MatchPatternCore, mozilla::extensions::MatchPatternSet::Constructor, mozilla::extensions::MatchPatternSet::GetPatterns, mozilla::extensions::ParseGlobs, mozilla::extensions::PermittedSchemes, mozilla::extensions::WebExtensionPolicy::Constructor, mozilla::extensions::WebExtensionPolicy::WebExtensionPolicy, mozilla::extensions::WebExtensionPolicyCore::WebExtensionPolicyCore, mozilla::net::nsStandardURL::TemplatedMutator, nsDynamicAtom::Create, nsJARURI::Mutator::SetSpecBaseCharset]

View file

@ -277,7 +277,13 @@ async function prepareRunningTests()
function* testBody() function* testBody()
{ {
yield* testRichListbox("richlistbox"); yield* testRichListbox("richlistbox");
// Perform a mousedown to ensure the wheel transaction from the previous test
// does not impact the next test.
synthesizeMouse(document.scrollingElement, 0, 0, {type: "mousedown"}, window);
yield* testArrowScrollbox("hscrollbox"); yield* testArrowScrollbox("hscrollbox");
synthesizeMouse(document.scrollingElement, -1, -1, {type: "mousedown"}, window);
yield* testArrowScrollbox("vscrollbox"); yield* testArrowScrollbox("vscrollbox");
} }