tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit ea5cdb606718658bbea1d5c4fca0dcc9e3e06430
parent 02507fc25181b779f999c0d6994d6774a3825f03
Author: Masayuki Nakano <masayuki@d-toybox.com>
Date:   Wed, 10 Dec 2025 02:55:09 +0000

Bug 1987671 - Support `movementX` and `movementY` of `pointerrawupdate` event r=smaug,dom-core

Although this has not been standardized yet [1][2].  However, Chromium
has already supported this and that must be reasonable for web
developers.

This patch adds the support with a new pref.

However, as written with XXX comment, I think `movementX` and
`movementY` of `pointermove` and `pointerrawupdate` should be stored per
`pointerId`.  However, let's keep the existing way for `pointermove` for
now.

1. https://github.com/w3c/pointerlock/issues/100
2. https://github.com/w3c/pointerevents/issues/535

Differential Revision: https://phabricator.services.mozilla.com/D275171

Diffstat:
Mdom/events/EventStateManager.cpp | 43+++++++++++++++++++++++++++++++------------
Mdom/events/EventStateManager.h | 1+
Mdom/events/MouseEvent.cpp | 4+++-
Mdom/events/test/pointerevents/test_pointerrawupdate_event_count.html | 21+++++++++++++++++++++
Mdom/tests/mochitest/pointerlock/file_childIframe.html | 69++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Mdom/tests/mochitest/pointerlock/file_movementXY.html | 128++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Mdom/tests/mochitest/pointerlock/mochitest.toml | 1+
Mmodules/libpref/init/StaticPrefList.yaml | 6++++++
8 files changed, 196 insertions(+), 77 deletions(-)

diff --git a/dom/events/EventStateManager.cpp b/dom/events/EventStateManager.cpp @@ -620,6 +620,8 @@ constinit AutoWeakFrame EventStateManager::sLastDragOverFrame{}; LayoutDeviceIntPoint EventStateManager::sPreLockScreenPoint = LayoutDeviceIntPoint(0, 0); LayoutDeviceIntPoint EventStateManager::sLastRefPoint = kInvalidRefPoint; +LayoutDeviceIntPoint EventStateManager::sLastRefPointOfRawUpdate = + kInvalidRefPoint; CSSIntPoint EventStateManager::sLastScreenPoint = CSSIntPoint(0, 0); LayoutDeviceIntPoint EventStateManager::sSynthCenteringPoint = kInvalidRefPoint; CSSIntPoint EventStateManager::sLastClientPoint = CSSIntPoint(0, 0); @@ -5638,6 +5640,10 @@ void EventStateManager::UpdateLastRefPointOfMouseEvent( return; } + const LayoutDeviceIntPoint& lastRefPoint = + aMouseEvent->mMessage == ePointerRawUpdate ? sLastRefPointOfRawUpdate + : sLastRefPoint; + // Mouse movement is reported on the MouseEvent.movement{X,Y} fields. // Movement is calculated in UIEvent::GetMovementPoint() as: // previous_mousemove_mRefPoint - current_mousemove_mRefPoint. @@ -5651,14 +5657,14 @@ void EventStateManager::UpdateLastRefPointOfMouseEvent( aMouseEvent->mLastRefPoint = GetWindowClientRectCenter(aMouseEvent->mWidget); - } else if (sLastRefPoint == kInvalidRefPoint) { + } else if (lastRefPoint == kInvalidRefPoint) { // We don't have a valid previous mousemove mRefPoint. This is either // the first move we've encountered, or the mouse has just re-entered // the application window. We should report (0,0) movement for this // case, so make the current and previous mRefPoints the same. aMouseEvent->mLastRefPoint = aMouseEvent->mRefPoint; } else { - aMouseEvent->mLastRefPoint = sLastRefPoint; + aMouseEvent->mLastRefPoint = lastRefPoint; } } @@ -5712,10 +5718,21 @@ void EventStateManager::ResetPointerToWindowCenterWhilePointerLocked( /* static */ void EventStateManager::UpdateLastPointerPosition( WidgetMouseEvent* aMouseEvent) { - if (aMouseEvent->mMessage != eMouseMove) { + if (aMouseEvent->IsSynthesized()) { return; } - sLastRefPoint = aMouseEvent->mRefPoint; + if (aMouseEvent->mMessage == eMouseMove) { + sLastRefPoint = aMouseEvent->mRefPoint; + } else if (aMouseEvent->mMessage == ePointerRawUpdate || + // FYI: ePointerRawUpdate is handled only when there are some + // `pointerrawupdate` event listeners. Therefore, we need to + // update the last ref point for ePointerRawUpdate when we dispatch + // ePointerMove too since the first `pointerrawupdate` event + // listener may be added after the ePointerMove. + aMouseEvent->mMessage == ePointerMove) { + // XXX Shouldn't we store last refpoint of PointerEvent per pointerId? + sLastRefPointOfRawUpdate = aMouseEvent->mRefPoint; + } } void EventStateManager::GenerateMouseEnterExit(WidgetMouseEvent* aMouseEvent) { @@ -5797,9 +5814,9 @@ void EventStateManager::GenerateMouseEnterExit(WidgetMouseEvent* aMouseEvent) { } } - // Reset sLastRefPoint, so that we'll know not to report any - // movement the next time we re-enter the window. - sLastRefPoint = kInvalidRefPoint; + // Reset sLastRefPoint and sLastRefPointOfRawUpdate, so that we'll know + // not to report any movement the next time we re-enter the window. + sLastRefPoint = sLastRefPointOfRawUpdate = kInvalidRefPoint; NotifyMouseOut(aMouseEvent, nullptr); break; @@ -5856,7 +5873,8 @@ void EventStateManager::SetPointerLock(nsIWidget* aWidget, // XXX Cannot we do synthesize the native mousemove in the parent process // with calling LockNativePointer below? Then, we could make this API // work only in the automation mode. - sLastRefPoint = GetWindowClientRectCenter(aWidget); + sLastRefPoint = sLastRefPointOfRawUpdate = + GetWindowClientRectCenter(aWidget); aWidget->SynthesizeNativeMouseMove( sLastRefPoint + aWidget->WidgetToScreenOffset(), nullptr); @@ -5878,10 +5896,11 @@ void EventStateManager::SetPointerLock(nsIWidget* aWidget, sSynthCenteringPoint = kInvalidRefPoint; if (aWidget) { // Unlocking, so return pointer to the original position by firing a - // synthetic mouse event. We first reset sLastRefPoint to its - // pre-pointerlock position, so that the synthetic mouse event reports - // no movement. - sLastRefPoint = sPreLockScreenPoint - aWidget->WidgetToScreenOffset(); + // synthetic mouse event. We first reset sLastRefPoint and + // sLastRefPointOfRawUpdate to its pre-pointerlock position, so that the + // synthetic mouse event reports no movement. + sLastRefPoint = sLastRefPointOfRawUpdate = + sPreLockScreenPoint - aWidget->WidgetToScreenOffset(); // XXX Cannot we do synthesize the native mousemove in the parent process // with calling `UnlockNativePointer` above? Then, we could make this // API work only in the automation mode. diff --git a/dom/events/EventStateManager.h b/dom/events/EventStateManager.h @@ -1381,6 +1381,7 @@ class EventStateManager : public nsSupportsWeakReference, public nsIObserver { // Stores the mRefPoint (the offset from the widget's origin in device // pixels) of the last mouse event. static LayoutDeviceIntPoint sLastRefPoint; + static LayoutDeviceIntPoint sLastRefPointOfRawUpdate; // member variables for the d&d gesture state machine LayoutDeviceIntPoint mGestureDownPoint; // screen coordinates diff --git a/dom/events/MouseEvent.cpp b/dom/events/MouseEvent.cpp @@ -440,7 +440,9 @@ nsIntPoint MouseEvent::GetMovementPoint() const { } if (!mEvent || !mEvent->AsGUIEvent()->mWidget || - (mEvent->mMessage != eMouseMove && mEvent->mMessage != ePointerMove)) { + (mEvent->mMessage != eMouseMove && mEvent->mMessage != ePointerMove && + !(StaticPrefs::dom_event_pointer_rawupdate_movement_enabled() && + mEvent->mMessage == ePointerRawUpdate))) { // Pointer Lock spec defines that movementX/Y must be zero for all mouse // events except mousemove. return nsIntPoint(0, 0); diff --git a/dom/events/test/pointerevents/test_pointerrawupdate_event_count.html b/dom/events/test/pointerevents/test_pointerrawupdate_event_count.html @@ -16,6 +16,7 @@ SimpleTest.requestCompleteLog(); SimpleTest.waitForFocus(async () => { await SpecialPowers.pushPrefEnv({"set": [ ["dom.event.pointer.rawupdate.enabled", true], + ["dom.event.pointer.rawupdate.movement.enabled", true], ["dom.events.coalesce.mousemove", true], ]}); @@ -116,6 +117,26 @@ SimpleTest.waitForFocus(async () => { `pointerrawupdate(${i++}): should have same values as coalesced pointermove events` ); } + { + const lastPoint = {x: pointerRawUpdateEvents[0].screenX, y: pointerRawUpdateEvents[0].screenY}; + for (let i = 1; i < pointerRawUpdateEvents.length; i++) { + const event = pointerRawUpdateEvents[i]; + isfuzzy( + event.movementX, + event.screenX - lastPoint.x, + 1, + `pointerrawupdate[${i}].movementX (screenX: ${event.screenX}, lastPoint.x: ${lastPoint.x})` + ); + isfuzzy( + event.movementY, + event.screenY - lastPoint.y, + 1, + `pointerrawupdate[${i}].movementY (screenY: ${event.screenY}, lastPoint.y: ${lastPoint.y})` + ); + lastPoint.x = event.screenX; + lastPoint.y = event.screenY; + } + } info("Waiting for a click for waiting for stable state after the test..."); await new Promise(resolve => { diff --git a/dom/tests/mochitest/pointerlock/file_childIframe.html b/dom/tests/mochitest/pointerlock/file_childIframe.html @@ -55,7 +55,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=633602 * Check if pointer is locked when over a child iframe of * the locked element * Check if pointer is being repositioned back to center of - * the locked element even when pointer goes over a child ifame + * the locked element even when pointer goes over a child iframe */ SimpleTest.waitForExplicitFinish(); @@ -64,58 +64,79 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=633602 , childDiv = document.getElementById("childDiv") , iframe = document.getElementById("iframe"); - function MovementStats() { - this.movementX = false; - this.movementY = false; + function MovementStats(aEvent) { + this.movementX = aEvent.movementX; + this.movementY = aEvent.movementY; } - var firstMove = new MovementStats() - , secondMove = new MovementStats() - , hoverIframe = false; + var hoverIframe; + var firstMove, secondMove; function runTests () { - ok(hoverIframe, "Pointer should be locked even when pointer " + - "hovers over a child iframe"); - is(firstMove.movementX, secondMove.movementX, "MovementX of first " + - "move to childDiv should be equal to movementX of second move " + - "to child div"); - is(firstMove.movementY, secondMove.movementY, "MovementY of first " + - "move to childDiv should be equal to movementY of second move " + - "to child div"); + ok( + hoverIframe, + "Pointer should be locked even when pointer hovers over a child iframe" + ); + is( + firstMove?.movementX, + secondMove?.movementX, + "MovementX of first move to childDiv should be equal to movementX of second move to child div" + ); + is( + firstMove?.movementY, + secondMove?.movementY, + "MovementY of first move to childDiv should be equal to movementY of second move to child div" + ); } var firstMoveChild = function (e) { - firstMove.movementX = e.movementX; - firstMove.movementY = e.movementY; + info(`got firstMoveChild called: { screenX: ${e.screenX}, screenY: ${ + e.screenY + }, movementX: ${e.movementX}, movementY: ${e.movementY} }`); + + firstMove = new MovementStats(e); parent.removeEventListener("mousemove", firstMoveChild); - parent.addEventListener("mousemove", moveIframe); - synthesizeMouseAtCenter(iframe, {type: "mousemove"}, window); + requestAnimationFrame(() => { + parent.addEventListener("mousemove", moveIframe); + synthesizeMouseAtCenter(iframe, {type: "mousemove"}, window); + }); }; var moveIframe = function (e) { + info(`got moveIframe called: { screenX: ${e.screenX}, screenY: ${ + e.screenY + }, movementX: ${e.movementX}, movementY: ${e.movementY} }`); + hoverIframe = !!document.pointerLockElement; parent.removeEventListener("mousemove", moveIframe); - parent.addEventListener("mousemove", secondMoveChild); - synthesizeMouseAtCenter(childDiv, {type: "mousemove"}, window); + requestAnimationFrame(() => { + parent.addEventListener("mousemove", secondMoveChild); + synthesizeMouseAtCenter(childDiv, {type: "mousemove"}, window); + }); }; var secondMoveChild = function (e) { - secondMove.movementX = e.movementX; - secondMove.movementY = e.movementY; + info(`got secondMoveChild called: { screenX: ${e.screenX}, screenY: ${ + e.screenY + }, movementX: ${e.movementX}, movementY: ${e.movementY} }`); + + secondMove = new MovementStats(e); parent.removeEventListener("mousemove", secondMoveChild); addFullscreenChangeContinuation("exit", function() { runTests(); SimpleTest.finish(); }); + info("Calling exitFullscreen()..."); document.exitFullscreen(); }; document.addEventListener("pointerlockchange", function () { + info(`Got pointerlockchange: pointerLockElement:\n${document.pointerLockElement?.outerHTML}`); if (document.pointerLockElement === parent) { parent.addEventListener("mousemove", firstMoveChild); synthesizeMouseAtCenter(childDiv, {type: "mousemove"}, window); @@ -124,8 +145,10 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=633602 function start() { addFullscreenChangeContinuation("enter", function() { + info("Calling requestPointerLock()..."); parent.requestPointerLock(); }); + info("Calling requestFullscreen()..."); parent.requestFullscreen(); } </script> diff --git a/dom/tests/mochitest/pointerlock/file_movementXY.html b/dom/tests/mochitest/pointerlock/file_movementXY.html @@ -32,71 +32,117 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=633602 SimpleTest.waitForExplicitFinish(); SimpleTest.requestFlakyTimeout("We may need to wait for window's moving"); - function MouseMovementStats() { - this.screenX = false; - this.screenY = false; - this.movementX = false; - this.movementY = false; + function MouseMovementStats(aEvent) { + this.screenX = aEvent.screenX; + this.screenY = aEvent.screenY; + this.movementX = aEvent.movementX; + this.movementY = aEvent.movementY; } - var div = document.getElementById("div") - , divCenterWidth = 0 - , divCenterHeight = 0 - , movementX = false - , movementY = false - , firstMove = new MouseMovementStats() - , secondMove = new MouseMovementStats(); + var div = document.getElementById("div"); + var movementX, movementY; + var firstMouseMove, secondMouseMove, secondPointerMove, secondPointerRawUpdate; function runTests () { ok(movementX && movementY, "movementX and " + "movementY should exist in mouse events objects."); - is(secondMove.movementX, secondMove.screenX - firstMove.screenX, - "movementX should be equal to eNow.screenX-ePrevious.screenX"); - is(secondMove.movementY, secondMove.screenY - firstMove.screenY, - "movementY should be equal to eNow.screenY-ePrevious.screenY"); + ok(firstMouseMove, "firstMouseMove should be recorded"); + ok(secondMouseMove, "secondMouseMove should be recorded"); + ok(secondPointerMove, "secondPointerMove should be recorded"); + ok(secondPointerRawUpdate, "secondPointerRawUpdate should be recorded"); + is( + secondMouseMove?.movementX, + secondMouseMove?.screenX - firstMouseMove?.screenX, + `movementX should be equal to eNow.screenX (${secondMouseMove.screenX}) - ePrevious.screenX (${firstMouseMove.screenX})` + ); + is( + secondPointerMove?.movementX, + secondMouseMove?.movementX, + "movementX of pointermove should be equal to movementX of the corresponding mousemove" + ); + is( + secondPointerRawUpdate?.movementX, + secondPointerMove?.movementX, + "movementX of pointerrawupdate should be equal to movementX of the corresponding pointermove" + ); + is( + secondMouseMove?.movementY, + secondMouseMove?.screenY - firstMouseMove?.screenY, + `movementY should be equal to eNow.screenY (${secondMouseMove.screenY}) - ePrevious.screenY (${firstMouseMove.screenY})` + ); + is( + secondPointerMove?.movementY, + secondMouseMove?.movementY, + "movementY of pointermove should be equal to movementY of the corresponding mousemove" + ); + is( + secondPointerRawUpdate?.movementY, + secondPointerMove?.movementY, + "movementY of pointerrawupdate should be equal to movementY of the corresponding pointermove" + ); } - var moveMouse = function(e) { - info("Got mouse move"); - movementX = ("movementX" in e); - movementY = ("movementY" in e); + var onFirstMouseMove = function(e) { + info(`Got first ${e.type}`); - div.removeEventListener("mousemove", moveMouse); - div.addEventListener("mousemove", moveMouseAgain); + div.removeEventListener(e.type, onFirstMouseMove); - firstMove.screenX = e.screenX; - firstMove.screenY = e.screenY; + firstMouseMove = new MouseMovementStats(e); - divCenterWidth = Math.round(div.getBoundingClientRect().width / 2); - divCenterHeight = Math.round(div.getBoundingClientRect().height / 2); + movementX = ("movementX" in e); + movementY = ("movementY" in e); + div.addEventListener("mousemove", onSecondMouseMoveOrPointerMove); + div.addEventListener("pointermove", onSecondMouseMoveOrPointerMove); + div.addEventListener("pointerrawupdate", onSecondMouseMoveOrPointerMove); + const divCenterWidth = Math.round(div.getBoundingClientRect().width / 2); + const divCenterHeight = Math.round(div.getBoundingClientRect().height / 2); + // FIXME: We should synthesize another mousemove after the propagation of + // current mousemove ends. However, doing that will fail so that it does + // not make sense what this test checks. synthesizeMouse(div, (divCenterWidth + 10), (divCenterHeight + 10), { type: "mousemove" }, window); }; - var moveMouseAgain = function(e) { - info("Got mouse move again"); - secondMove.screenX = e.screenX; - secondMove.screenY = e.screenY; - secondMove.movementX = e.movementX; - secondMove.movementY = e.movementY; + var onSecondMouseMoveOrPointerMove = function(e) { + info(`Got second ${e.type}`); - div.removeEventListener("mousemove", moveMouseAgain); - addFullscreenChangeContinuation("exit", function() { - info("Got fullscreenchange for exiting"); - runTests(); - SimpleTest.finish(); - }); - document.exitFullscreen(); + div.removeEventListener(e.type, onSecondMouseMoveOrPointerMove); + switch (e.type) { + case "mousemove": + secondMouseMove = new MouseMovementStats(e); + addFullscreenChangeContinuation("exit", function() { + info("Got fullscreenchange for exiting"); + runTests(); + SimpleTest.finish(); + }); + document.exitFullscreen(); + break; + case "pointermove": + secondPointerMove = new MouseMovementStats(e); + break; + case "pointerrawupdate": + secondPointerRawUpdate = new MouseMovementStats(e); + break; + } }; function start() { info("Requesting fullscreen on parent"); + for (const type of ["mousemove", "pointermove", "pointerrawupdate"]) { + addEventListener(type, event => { + info(`${type}: { screenX: ${event.screenX}, screenY: ${ + event.screenY + }, movementX: ${event.movementX}, movementY: ${event.movementY} }`); + }, {capture: true}); + } addFullscreenChangeContinuation("enter", function() { info("Got fullscreenchange for entering"); - div.addEventListener("mousemove", moveMouse); - synthesizeMouseAtCenter(div, {type: "mousemove"}, window); + div.addEventListener("mousemove", onFirstMouseMove); + requestAnimationFrame( + () => synthesizeMouseAtCenter(div, {type: "mousemove"}, window) + ); }); div.requestFullscreen(); } diff --git a/dom/tests/mochitest/pointerlock/mochitest.toml b/dom/tests/mochitest/pointerlock/mochitest.toml @@ -9,6 +9,7 @@ support-files = [ ["test_pointerlock-api.html"] tags = "fullscreen" +scheme = "https" support-files = [ "file_pointerlock-api.html", "file_pointerlock-api-with-shadow.html", diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml @@ -2904,6 +2904,12 @@ value: true mirror: always +# Whether pointerrawupdate's movementX and movementY are set to non-zero values. +- name: dom.event.pointer.rawupdate.movement.enabled + type: bool + value: true + 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