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:
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