Bug 1844723 - Make `CreateMouseOrPointerWidgetEvent()` set `mButton` to "not pressed" value and compute `mButtons` if the source event has not been dispatched yet r=edgar,dom-core

`CreateMouseOrPointerWidgetEvent()` is designed to create `mouseenter`,
`mouseover`, `mouseout`, `mouseleave`, `pointerenter`, `pointerover`,
`pointerout` and `pointerleave` from a source event.

They are not button state change events, but the source event may be so.

According to the WPTs ([1], [2]) and the fact that the other browsers pass the
tests, the button state of pointer events of synthesizing events should be
synchronized with what the web apps notified (i.e., previous state of the
source event) if and only if the input source supports hover state and the
source event which changes a button state has not been dispatched into the DOM
yet.

1. https://searchfox.org/mozilla-central/rev/08d53deb2cf587e68d1825082c955e8a1926be73/testing/web-platform/tests/pointerevents/pointerevent_attributes_hoverable_pointers.html#44,51,60,63
2. https://searchfox.org/mozilla-central/rev/08d53deb2cf587e68d1825082c955e8a1926be73/testing/web-platform/tests/pointerevents/pointerevent_attributes_nohover_pointers.html#17,45,47,51

Differential Revision: https://phabricator.services.mozilla.com/D187644
This commit is contained in:
Masayuki Nakano 2023-10-10 07:33:05 +00:00
Родитель 4badb41d37
Коммит b8f3f296ed
10 изменённых файлов: 441 добавлений и 27 удалений

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

@ -4539,6 +4539,20 @@ class MOZ_STACK_CLASS ESMEventCB : public EventDispatchingCallback {
static UniquePtr<WidgetMouseEvent> CreateMouseOrPointerWidgetEvent(
WidgetMouseEvent* aMouseEvent, EventMessage aMessage,
EventTarget* aRelatedTarget) {
// This method does not support creating a mouse/pointer button change event
// because of no data about the changing state.
MOZ_ASSERT(aMessage != eMouseDown);
MOZ_ASSERT(aMessage != eMouseUp);
MOZ_ASSERT(aMessage != ePointerDown);
MOZ_ASSERT(aMessage != ePointerUp);
// This method is currently designed to create the following events.
MOZ_ASSERT(aMessage == eMouseOver || aMessage == eMouseEnter ||
aMessage == eMouseOut || aMessage == eMouseLeave ||
aMessage == ePointerOver || aMessage == ePointerEnter ||
aMessage == ePointerOut || aMessage == ePointerLeave ||
aMessage == eMouseEnterIntoWidget ||
aMessage == eMouseExitFromWidget);
WidgetPointerEvent* sourcePointer = aMouseEvent->AsPointerEvent();
UniquePtr<WidgetMouseEvent> newEvent;
if (sourcePointer) {
@ -4560,9 +4574,62 @@ static UniquePtr<WidgetMouseEvent> CreateMouseOrPointerWidgetEvent(
newEvent->mRelatedTarget = aRelatedTarget;
newEvent->mRefPoint = aMouseEvent->mRefPoint;
newEvent->mModifiers = aMouseEvent->mModifiers;
newEvent->mButton = aMouseEvent->mButton;
newEvent->mButtons = aMouseEvent->mButtons;
newEvent->mPressure = aMouseEvent->mPressure;
if (!aMouseEvent->mFlags.mDispatchedAtLeastOnce &&
aMouseEvent->InputSourceSupportsHover()) {
// If we synthesize a pointer event or a mouse event from another event
// which changes a button state whose input soucre supports hover state and
// the source event has not been dispatched yet, we should set to the button
// state of the synthesizing event to previous one.
// Note that we don't need to do this if the input source does not support
// hover state because a WPT check the behavior (see below) and the other
// browsers pass the test even though this is inconsistent behavior.
newEvent->mButton =
sourcePointer ? MouseButton::eNotPressed : MouseButton::ePrimary;
if (aMouseEvent->IsPressingButton()) {
// If the source event has not been dispatched into the DOM yet, we
// need to remove the flag which is being pressed.
newEvent->mButtons = static_cast<decltype(WidgetMouseEvent::mButtons)>(
aMouseEvent->mButtons &
~MouseButtonsFlagToChange(
static_cast<MouseButton>(aMouseEvent->mButton)));
} else if (aMouseEvent->IsReleasingButton()) {
// If the source event has not been dispatched into the DOM yet, we
// need to add the flag which is being released.
newEvent->mButtons = static_cast<decltype(WidgetMouseEvent::mButtons)>(
aMouseEvent->mButtons |
MouseButtonsFlagToChange(
static_cast<MouseButton>(aMouseEvent->mButton)));
} else {
// The source event does not change the buttons state so that we can
// set mButtons value as-is.
newEvent->mButtons = aMouseEvent->mButtons;
}
// Adjust pressure if it does not matches with mButtons.
// FIXME: We may use wrong pressure value if the source event has not been
// dispatched into the DOM yet. However, fixing this requires to store the
// last pressure value somewhere.
if (newEvent->mButtons && aMouseEvent->mPressure == 0) {
newEvent->mPressure = 0.5f;
} else if (!newEvent->mButtons && aMouseEvent->mPressure != 0) {
newEvent->mPressure = 0;
} else {
newEvent->mPressure = aMouseEvent->mPressure;
}
} else {
// If the event has already been dispatched into the tree, web apps has
// already handled the button state change, so the button state of the
// source event has already synced.
// If the input source does not have hover state, we don't need to modify
// the state because the other browsers behave so and tested by
// pointerevent_attributes_nohover_pointers.html even though this is
// different expectation from
// pointerevent_attributes_hoverable_pointers.html, but the other browsers
// pass both of them.
newEvent->mButton = aMouseEvent->mButton;
newEvent->mButtons = aMouseEvent->mButtons;
newEvent->mPressure = aMouseEvent->mPressure;
}
newEvent->mInputSource = aMouseEvent->mInputSource;
newEvent->pointerId = aMouseEvent->pointerId;

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

@ -586,6 +586,7 @@ class MOZ_STACK_CLASS AutoPointerEventTargetUpdater final {
};
bool PresShell::sDisableNonTestMouseEvents = false;
int16_t PresShell::sMouseButtons = MouseButtonsFlag::eNoButtons;
LazyLogModule PresShell::gLog("PresShell");
@ -5847,6 +5848,7 @@ void PresShell::ProcessSynthMouseMoveEvent(bool aFromScroll) {
WidgetMouseEvent::eSynthesized);
event.mRefPoint =
LayoutDeviceIntPoint::FromAppUnitsToNearest(refpoint, viewAPD);
event.mButtons = PresShell::sMouseButtons;
// XXX set event.mModifiers ?
// XXX mnakano I think that we should get the latest information from widget.
@ -8725,6 +8727,9 @@ nsresult PresShell::EventHandler::DispatchEventToDOM(
eventTarget, presContext, browserParent, aEvent->AsCompositionEvent(),
aEventStatus, eventCBPtr);
} else {
if (aEvent->mClass == eMouseEventClass) {
PresShell::sMouseButtons = aEvent->AsMouseEvent()->mButtons;
}
RefPtr<nsPresContext> presContext = GetPresContext();
EventDispatcher::Dispatch(eventTarget, presContext, aEvent, nullptr,
aEventStatus, eventCBPtr);

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

@ -2997,6 +2997,9 @@ class PresShell final : public nsStubDocumentObserver,
// NS_UNCONSTRAINEDSIZE) if the mouse isn't over our window or there is no
// last observed mouse location for some reason.
nsPoint mMouseLocation;
// This is used for the synthetic mouse events too. This is set when a mouse
// event is dispatched into the DOM.
static int16_t sMouseButtons;
// The last observed pointer location relative to that root document in visual
// coordinates.
nsPoint mLastOverWindowPointerLocation;

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

@ -51,24 +51,12 @@
[mouse pointerleave.toElement value is null]
expected: PRECONDITION_FAILED
[Inner frame mouse pointerover.button is -1 when mouse buttons are in released state.]
expected: FAIL
[Inner frame mouse pointerover.buttons is 0 when mouse buttons are in released state.]
expected: FAIL
[Inner frame mouse pointerover.fromElement value is null]
expected: PRECONDITION_FAILED
[Inner frame mouse pointerover.toElement value is null]
expected: PRECONDITION_FAILED
[Inner frame mouse pointerenter.button is -1 when mouse buttons are in released state.]
expected: FAIL
[Inner frame mouse pointerenter.buttons is 0 when mouse buttons are in released state.]
expected: FAIL
[Inner frame mouse pointerenter.fromElement value is null]
expected: PRECONDITION_FAILED

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

@ -51,24 +51,12 @@
[mouse pointerleave.toElement value is null]
expected: PRECONDITION_FAILED
[Inner frame mouse pointerover's button is -1 when mouse buttons are in released state.]
expected: FAIL
[Inner frame mouse pointerover's buttons is 0 when mouse buttons are in released state.]
expected: FAIL
[Inner frame mouse pointerover.fromElement value is null]
expected: PRECONDITION_FAILED
[Inner frame mouse pointerover.toElement value is null]
expected: PRECONDITION_FAILED
[Inner frame mouse pointerenter's button is -1 when mouse buttons are in released state.]
expected: FAIL
[Inner frame mouse pointerenter's buttons is 0 when mouse buttons are in released state.]
expected: FAIL
[Inner frame mouse pointermove's type should be pointermove]
expected: FAIL

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

@ -0,0 +1,26 @@
[synthetic-mouse-enter-leave-over-out-button-state-after-target-removed.tentative.html?buttonType=MIDDLE&button=1&buttons=4]
[Removing an element at mousedown: mouseout and mouseleave should've been fired on the removed child]
expected: FAIL
[Removing an element at mousedown: mouseenter should not have been fired on the parent]
expected: FAIL
[Removing an element at mouseup: mouseout and mouseleave should've been fired on the removed child]
expected: FAIL
[Removing an element at mouseup: mouseenter should not have been fired on the parent]
expected: FAIL
[synthetic-mouse-enter-leave-over-out-button-state-after-target-removed.tentative.html?buttonType=LEFT&button=0&buttons=1]
[Removing an element at mousedown: mouseout and mouseleave should've been fired on the removed child]
expected: FAIL
[Removing an element at mousedown: mouseenter should not have been fired on the parent]
expected: FAIL
[Removing an element at mouseup: mouseout and mouseleave should've been fired on the removed child]
expected: FAIL
[Removing an element at mouseup: mouseenter should not have been fired on the parent]
expected: FAIL

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

@ -0,0 +1,240 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="variant" content="?buttonType=LEFT&button=0&buttons=1">
<meta name="variant" content="?buttonType=MIDDLE&button=1&buttons=4">
<title>Testing button state of synthesized mouse(out|over|leave|enter) events</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script src="/resources/testdriver-actions.js"></script>
<style>
#parent {
background-color: lightseagreen;
padding: 0;
height: 40px;
width: 40px;
}
#child {
background-color: red;
margin: 0;
height: 30px;
width: 30px;
}
</style>
</head>
<body>
<div id="parent"><div id="child">abc</div></div>
<script>
const searchParams = new URLSearchParams(document.location.search);
const buttonType = searchParams.get("buttonType");
const button = parseInt(searchParams.get("button"));
const buttons = parseInt(searchParams.get("buttons"));
let events = [];
function eventToString(data) {
if (!data) {
return "{}";
}
return `{ '${data.type}' on '${data.target}': button=${data.button}, buttons=${data.buttons} }`;
}
function eventsToString(events) {
if (!events.length) {
return "[]";
}
let ret = "[";
for (const data of events) {
if (ret != "[") {
ret += ", ";
}
ret += eventToString(data);
}
return ret + "]";
}
function removeEventsBefore(eventType) {
while (events[0]?.type != eventType) {
events.shift();
}
}
const parentElement = document.getElementById("parent");
const childElement = document.getElementById("child");
function promiseLayout() {
return new Promise(resolve => {
(childElement.isConnected ? childElement : parentElement).getBoundingClientRect();
requestAnimationFrame(() => requestAnimationFrame(resolve));
});
}
promise_test(async () => {
await new Promise(resolve => {
addEventListener("load", resolve, { once: true });
});
["mouseout", "mouseover", "mouseleave", "mouseenter", "mousemove", "mousedown"].forEach(eventType => {
parentElement.addEventListener(eventType, event => {
if (event.target != parentElement) {
return;
}
events.push({
type: event.type,
target: "parent",
button: event.button,
buttons: event.buttons,
});
});
childElement.addEventListener(eventType, event => {
if (event.target != childElement) {
return;
}
events.push({
type: event.type,
target: "child",
button: event.button,
buttons: event.buttons,
});
});
});
}, "Setup event listeners and wait for load");
promise_test(async t => {
events = [];
await promiseLayout();
childElement.addEventListener("mousedown", () => childElement.remove(), {once: true});
const {x, y} = (function () {
const rect = childElement.getBoundingClientRect();
return { x: rect.left, y: rect.top };
})();
const actions = new test_driver.Actions();
await actions.pointerMove(10, 10, {origin: childElement})
.pointerDown({button: actions.ButtonType[buttonType]})
.pause(100) // Allow browsers to synthesize mouseout, etc
.pointerUp({button: actions.ButtonType[buttonType]})
.send();
await promiseLayout();
removeEventsBefore("mousedown");
test(() => {
const maybeMouseDownEvent =
events.length && events[0].type == "mousedown" ? events.shift() : undefined;
assert_equals(
eventToString(maybeMouseDownEvent),
eventToString({ type: "mousedown", target: "child", button, buttons })
);
}, `${t.name}: mousedown should've been fired`);
assert_true(events.length > 0, `${t.name}: Some events should've been fired after mousedown`);
test(() => {
// Before `mousedown` is fired, both parent and child must have received
// `mouseenter`, only the child must have received `mouseover`. Then, the
// child is now moved away by the `mousedown` listener. Therefore,
// `mouseout` and `mouseleave` should be fired on the child as the spec of
// UI Events defines. Then, they are not a button press events. Therefore,
// the `button` should be 0, but buttons should be set to 4 because of
// pressing the middle button.
let mouseOutOrLeave = [];
while (events[0]?.type == "mouseout" || events[0]?.type == "mouseleave") {
mouseOutOrLeave.push(events.shift());
}
assert_equals(
eventsToString(mouseOutOrLeave),
eventsToString([
{ type: "mouseout", target: "child", button: 0, buttons },
{ type: "mouseleave", target: "child", button: 0, buttons },
])
);
}, `${t.name}: mouseout and mouseleave should've been fired on the removed child`);
test(() => {
// And `mouseover` should be fired on the parent as the spec of UI Events
// defines.
let mouseOver = [];
while (events[0]?.type == "mouseover") {
mouseOver.push(events.shift());
}
assert_equals(
eventsToString(mouseOver),
eventsToString([{ type: "mouseover", target: "parent", button: 0, buttons }])
);
}, `${t.name}: mouseover should've been fired on the parent`);
test(() => {
// On the other hand, it's unclear about `mouseenter`. The mouse cursor has
// never been moved out from the parent. Therefore, it shouldn't be fired
// on the parent ideally, but all browsers do not pass this test and there
// is no clear definition about this case.
let mouseEnter = [];
while (events.length && events[0].type == "mouseenter") {
mouseEnter.push(events.shift());
}
assert_equals(eventsToString(mouseEnter), eventsToString([]));
}, `${t.name}: mouseenter should not have been fired on the parent`);
assert_equals(eventsToString(events), eventsToString([]), "All events should've been checked");
parentElement.appendChild(childElement);
}, "Removing an element at mousedown");
promise_test(async t => {
events = [];
await promiseLayout();
childElement.addEventListener("mouseup", () => childElement.remove(), {once: true});
const {x, y} = (function () {
const rect = childElement.getBoundingClientRect();
return { x: rect.left, y: rect.top };
})();
const actions = new test_driver.Actions();
await actions.pointerMove(10, 10, {origin: childElement})
.pointerDown({button: actions.ButtonType[buttonType]})
.pointerUp({button: actions.ButtonType[buttonType]})
.send();
await promiseLayout();
removeEventsBefore("mousedown");
test(() => {
const maybeMouseDownEvent =
events.length && events[0].type == "mousedown" ? events.shift() : undefined;
assert_equals(
eventToString(maybeMouseDownEvent),
eventToString({ type: "mousedown", target: "child", button, buttons })
);
}, `${t.name}: mousedown should've been fired`);
assert_true(events.length > 0, `${t.name}: Some events should've been fired after mousedown`);
// Same as the `mousedown` case except `buttons` value because `mouseout`,
// `mouseleave`, `mouseover` and `mouseenter` should (or may) be fired
// after the `mouseup`. Therefore, `.buttons` should not have the button
// flag.
test(() => {
let mouseOutOrLeave = [];
while (events[0]?.type == "mouseout" || events[0]?.type == "mouseleave") {
mouseOutOrLeave.push(events.shift());
}
assert_equals(
eventsToString(mouseOutOrLeave),
eventsToString([
{ type: "mouseout", target: "child", button: 0, buttons: 0 },
{ type: "mouseleave", target: "child", button: 0, buttons: 0 },
])
);
}, `${t.name}: mouseout and mouseleave should've been fired on the removed child`);
test(() => {
let mouseOver = [];
while (events[0]?.type == "mouseover") {
mouseOver.push(events.shift());
}
assert_equals(
eventsToString(mouseOver),
eventsToString([{ type: "mouseover", target: "parent", button: 0, buttons: 0 }])
);
}, `${t.name}: mouseover should've been fired on the parent`);
test(() => {
let mouseEnter = [];
while (events[0]?.type == "mouseenter") {
mouseEnter.push(events.shift());
}
assert_equals(eventsToString(mouseEnter), eventsToString([]));
}, `${t.name}: mouseenter should not have been fired on the parent`);
assert_equals(eventsToString(events), eventsToString([]), "All events should've been checked");
parentElement.appendChild(childElement);
}, "Removing an element at mouseup");
</script>
</body>
</html>

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

@ -460,6 +460,29 @@ enum MouseButtonsFlag {
eEraserFlag = 0x20
};
/**
* Returns a MouseButtonsFlag value which is changed by a button state change
* event whose mButton is aMouseButton.
*/
inline MouseButtonsFlag MouseButtonsFlagToChange(MouseButton aMouseButton) {
switch (aMouseButton) {
case MouseButton::ePrimary:
return MouseButtonsFlag::ePrimaryFlag;
case MouseButton::eMiddle:
return MouseButtonsFlag::eMiddleFlag;
case MouseButton::eSecondary:
return MouseButtonsFlag::eSecondaryFlag;
case MouseButton::eX1:
return MouseButtonsFlag::e4thFlag;
case MouseButton::eX2:
return MouseButtonsFlag::e5thFlag;
case MouseButton::eEraser:
return MouseButtonsFlag::eEraserFlag;
default:
return MouseButtonsFlag::eNoButtons;
}
}
enum class TextRangeType : RawTextRangeType;
// IMEData.h

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

@ -9,6 +9,7 @@
#include <stdint.h>
#include "mozilla/BasicEvents.h"
#include "mozilla/EventForwards.h"
#include "mozilla/MathAlgorithms.h"
#include "mozilla/dom/DataTransfer.h"
#include "mozilla/ipc/IPCForwards.h"
@ -166,6 +167,59 @@ class WidgetMouseEventBase : public WidgetInputEvent {
bool IsLeftClickEvent() const {
return mMessage == eMouseClick && mButton == MouseButton::ePrimary;
}
/**
* Returns true if this event changes a button state to "pressed".
*/
[[nodiscard]] bool IsPressingButton() const {
MOZ_ASSERT(IsTrusted());
if (mClass == eMouseEventClass) {
return mMessage == eMouseDown;
}
if (mButton == MouseButton::eNotPressed) {
return false;
}
// If this is an ePointerDown event whose mButton is not "not pressed", this
// is a button pressing event.
if (mMessage == ePointerDown) {
return true;
}
// If 2 or more buttons are pressed at same time, they are sent with
// pointermove rather than pointerdown. Therefore, let's check whether
// mButtons contains the proper flag for the pressing button.
const bool buttonsContainButton = !!(
mButtons & MouseButtonsFlagToChange(static_cast<MouseButton>(mButton)));
return mMessage == ePointerMove && buttonsContainButton;
}
/**
* Returns true if this event changes a button state to "released".
*/
[[nodiscard]] bool IsReleasingButton() const {
MOZ_ASSERT(IsTrusted());
if (mClass == eMouseEventClass) {
return mMessage == eMouseUp;
}
if (mButton == MouseButton::eNotPressed) {
return false;
}
// If this is an ePointerUp event whose mButton is not "not pressed", this
// is a button release event.
if (mMessage == ePointerUp) {
return true;
}
// If the releasing button is not the last button of pressing buttons, web
// apps notified by pointermove rather than pointerup. Therefore, let's
// check whether mButtons loses the proper flag for the releasing button.
const bool buttonsLoseTheButton = !(
mButtons & MouseButtonsFlagToChange(static_cast<MouseButton>(mButton)));
return mMessage == ePointerMove && buttonsLoseTheButton;
}
/**
* Returns true if the input source supports hover state like a mouse.
*/
[[nodiscard]] bool InputSourceSupportsHover() const;
};
/******************************************************************************

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

@ -20,6 +20,7 @@
#include "mozilla/StaticPrefs_ui.h"
#include "mozilla/WritingModes.h"
#include "mozilla/dom/KeyboardEventBinding.h"
#include "mozilla/dom/MouseEventBinding.h"
#include "mozilla/dom/WheelEventBinding.h"
#include "nsCommandParams.h"
#include "nsContentUtils.h"
@ -647,6 +648,25 @@ Modifier WidgetInputEvent::AccelModifier() {
return sAccelModifier;
}
/******************************************************************************
* mozilla::WidgetMouseEventBase (MouseEvents.h)
******************************************************************************/
bool WidgetMouseEventBase::InputSourceSupportsHover() const {
switch (mInputSource) {
case dom::MouseEvent_Binding::MOZ_SOURCE_MOUSE:
case dom::MouseEvent_Binding::MOZ_SOURCE_PEN:
case dom::MouseEvent_Binding::MOZ_SOURCE_ERASER:
return true;
case dom::MouseEvent_Binding::MOZ_SOURCE_TOUCH:
case dom::MouseEvent_Binding::MOZ_SOURCE_UNKNOWN:
case dom::MouseEvent_Binding::MOZ_SOURCE_KEYBOARD:
case dom::MouseEvent_Binding::MOZ_SOURCE_CURSOR:
default:
return false;
}
}
/******************************************************************************
* mozilla::WidgetMouseEvent (MouseEvents.h)
******************************************************************************/