diff --git a/dom/events/WheelEvent.cpp b/dom/events/WheelEvent.cpp index bfe1676a1d0c..0d0473a6992a 100644 --- a/dom/events/WheelEvent.cpp +++ b/dom/events/WheelEvent.cpp @@ -18,6 +18,11 @@ WheelEvent::WheelEvent(EventTarget* aOwner, nsPresContext* aPresContext, ? aWheelEvent : new WidgetWheelEvent(false, eVoidEvent, nullptr)), mAppUnitsPerDevPixel(0) { + + if (StaticPrefs::dom_event_wheel_deltaMode_lines_always_disabled()) { + mDeltaModeCheckingState = DeltaModeCheckingState::Unchecked; + } + if (aWheelEvent) { mEventIsInternal = false; // If the delta mode is pixel, the WidgetWheelEvent's delta values are in @@ -55,31 +60,55 @@ void WheelEvent::InitWheelEvent( wheelEvent->mDeltaMode = aDeltaMode; } -double WheelEvent::DeltaX() { - if (!mAppUnitsPerDevPixel) { - return mEvent->AsWheelEvent()->mDeltaX; +double WheelEvent::ToWebExposedDelta(const WidgetWheelEvent& aWidgetEvent, + double aDelta, CallerType aCallerType) { + if (aCallerType != CallerType::System) { + if (mDeltaModeCheckingState == DeltaModeCheckingState::Unknown) { + mDeltaModeCheckingState = DeltaModeCheckingState::Unchecked; + } + if (mDeltaModeCheckingState == DeltaModeCheckingState::Unchecked && + aWidgetEvent.mDeltaMode == WheelEvent_Binding::DOM_DELTA_LINE && + StaticPrefs::dom_event_wheel_deltaMode_lines_disabled()) { + // TODO(emilio, bug 1675949): Consider not using a fixed multiplier here? + return aDelta * + StaticPrefs::dom_event_wheel_deltaMode_lines_to_pixel_scale(); + } } - return mEvent->AsWheelEvent()->mDeltaX * mAppUnitsPerDevPixel / - AppUnitsPerCSSPixel(); + if (!mAppUnitsPerDevPixel) { + return aDelta; + } + return aDelta * mAppUnitsPerDevPixel / AppUnitsPerCSSPixel(); } -double WheelEvent::DeltaY() { - if (!mAppUnitsPerDevPixel) { - return mEvent->AsWheelEvent()->mDeltaY; - } - return mEvent->AsWheelEvent()->mDeltaY * mAppUnitsPerDevPixel / - AppUnitsPerCSSPixel(); +double WheelEvent::DeltaX(CallerType aCallerType) { + WidgetWheelEvent* ev = mEvent->AsWheelEvent(); + return ToWebExposedDelta(*ev, ev->mDeltaX, aCallerType); } -double WheelEvent::DeltaZ() { - if (!mAppUnitsPerDevPixel) { - return mEvent->AsWheelEvent()->mDeltaZ; - } - return mEvent->AsWheelEvent()->mDeltaZ * mAppUnitsPerDevPixel / - AppUnitsPerCSSPixel(); +double WheelEvent::DeltaY(CallerType aCallerType) { + WidgetWheelEvent* ev = mEvent->AsWheelEvent(); + return ToWebExposedDelta(*ev, ev->mDeltaY, aCallerType); } -uint32_t WheelEvent::DeltaMode() { return mEvent->AsWheelEvent()->mDeltaMode; } +double WheelEvent::DeltaZ(CallerType aCallerType) { + WidgetWheelEvent* ev = mEvent->AsWheelEvent(); + return ToWebExposedDelta(*ev, ev->mDeltaZ, aCallerType); +} + +uint32_t WheelEvent::DeltaMode(CallerType aCallerType) { + uint32_t mode = mEvent->AsWheelEvent()->mDeltaMode; + if (aCallerType != CallerType::System) { + if (mDeltaModeCheckingState == DeltaModeCheckingState::Unknown) { + mDeltaModeCheckingState = DeltaModeCheckingState::Checked; + } else if (mDeltaModeCheckingState == DeltaModeCheckingState::Unchecked && + mode == WheelEvent_Binding::DOM_DELTA_LINE && + StaticPrefs::dom_event_wheel_deltaMode_lines_disabled()) { + return WheelEvent_Binding::DOM_DELTA_PIXEL; + } + } + + return mode; +} already_AddRefed WheelEvent::Constructor( const GlobalObject& aGlobal, const nsAString& aType, diff --git a/dom/events/WheelEvent.h b/dom/events/WheelEvent.h index 5a23f4f2ddf3..a1da439091e4 100644 --- a/dom/events/WheelEvent.h +++ b/dom/events/WheelEvent.h @@ -33,10 +33,10 @@ class WheelEvent : public MouseEvent { // NOTE: DeltaX(), DeltaY() and DeltaZ() return CSS pixels when deltaMode is // DOM_DELTA_PIXEL. (The internal event's delta values are device pixels // if it's dispatched by widget) - double DeltaX(); - double DeltaY(); - double DeltaZ(); - uint32_t DeltaMode(); + double DeltaX(CallerType); + double DeltaY(CallerType); + double DeltaZ(CallerType); + uint32_t DeltaMode(CallerType); void InitWheelEvent(const nsAString& aType, bool aCanBubble, bool aCancelable, nsGlobalWindowInner* aView, int32_t aDetail, @@ -49,8 +49,26 @@ class WheelEvent : public MouseEvent { protected: ~WheelEvent() = default; + double ToWebExposedDelta(const WidgetWheelEvent&, double aDelta, CallerType); + private: int32_t mAppUnitsPerDevPixel; + enum class DeltaModeCheckingState : uint8_t { + // Neither deltaMode nor the delta values have been accessed. + Unknown, + // The delta values have been accessed, without checking deltaMode first. + Unchecked, + // The deltaMode has been checked. + Checked, + }; + + // For compat reasons, we might expose a DOM_DELTA_LINE event as + // DOM_DELTA_PIXEL instead. Whether we do that depends on whether the event + // has been asked for the deltaMode before the deltas. If it has, we assume + // that the page will correctly handle DOM_DELTA_LINE. This variable tracks + // that state. See bug 1392460. + DeltaModeCheckingState mDeltaModeCheckingState = + DeltaModeCheckingState::Unknown; }; } // namespace dom diff --git a/dom/events/test/test_continuous_wheel_events.html b/dom/events/test/test_continuous_wheel_events.html index 00cb3be97711..158b7dc9efe7 100644 --- a/dom/events/test/test_continuous_wheel_events.html +++ b/dom/events/test/test_continuous_wheel_events.html @@ -2494,6 +2494,26 @@ function* testContinuousTrustedEvents() horizontal: { expected: true, preventDefault: true, detail: gHorizontalLine }, vertical: { expected: true, preventDefault: true, detail: gLineHeight } }, }, + { description: "modifier key tests without content checking mode (alt, line)", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: true, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + skipDeltaModeCheck: true, + deltaX: SpecialPowers.getIntPref("dom.event.wheel-deltaMode-lines-to-pixel-scale"), + deltaY: SpecialPowers.getIntPref("dom.event.wheel-deltaMode-lines-to-pixel-scale"), + deltaZ: SpecialPowers.getIntPref("dom.event.wheel-deltaMode-lines-to-pixel-scale"), + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: 1 }, + vertical: { expected: true, preventDefault: false, detail: 1 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: true, detail: gHorizontalLine }, + vertical: { expected: true, preventDefault: true, detail: gLineHeight } }, + }, { description: "modifier key tests (alt, page)", event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0, isMomentum: false, @@ -3058,14 +3078,24 @@ function* testContinuousTrustedEvents() is(aEvent.target, gScrolledElement, description + "target was invalid"); - is(aEvent.deltaMode, currentWheelEventTest.event.deltaMode, - description + "deltaMode was invalid"); - is(aEvent.deltaX, currentWheelEventTest.wheel.deltaX, - description + "deltaX was invalid"); - is(aEvent.deltaY, currentWheelEventTest.wheel.deltaY, - description + "deltaY was invalid"); - is(aEvent.deltaZ, currentWheelEventTest.wheel.deltaZ, - description + "deltaZ was invalid"); + if (!currentWheelEventTest.wheel.skipDeltaModeCheck) { + is(aEvent.deltaMode, currentWheelEventTest.event.deltaMode, + description + "deltaMode was invalid"); + } + is(SpecialPowers.wrap(aEvent).deltaMode, currentWheelEventTest.event.deltaMode, + description + "deltaMode is raw value from privileged script"); + for (let prop of ["deltaX", "deltaY", "deltaZ"]) { + is(aEvent[prop], currentWheelEventTest.wheel[prop], + description + prop + " was invalid"); + if (currentWheelEventTest.wheel.skipDeltaModeCheck) { + is(aEvent.deltaMode, WheelEvent.DOM_DELTA_PIXEL, + description + "deltaMode should become pixels for line scrolling if unchecked by content") + if (aEvent[prop] != 0) { + isnot(aEvent[prop], SpecialPowers.wrap(aEvent)[prop], + description + "should keep returning raw value for privileged script"); + } + } + } is(aEvent.shiftKey, currentWheelEventTest.event.shiftKey, description + "shiftKey was invalid"); is(aEvent.ctrlKey, currentWheelEventTest.event.ctrlKey, @@ -3225,6 +3255,12 @@ function* testBody() function runTests() { SpecialPowers.pushPrefEnv({"set": [ + // FIXME(emilio): This test is broken in HiDPI, unclear if + // MozMousePixelScroll is not properly converting to CSS pixels, or + // whether sendWheelAndWait expectes device rather than CSS pixels, or + // something else. + ["layout.css.devPixelsPerPx", 1.0], + ["mousewheel.transaction.timeout", 100000], ["mousewheel.default.delta_multiplier_x", 100], ["mousewheel.default.delta_multiplier_y", 100], diff --git a/dom/events/test/test_dom_wheel_event.html b/dom/events/test/test_dom_wheel_event.html index b9a5c30687c7..61720f364b5c 100644 --- a/dom/events/test/test_dom_wheel_event.html +++ b/dom/events/test/test_dom_wheel_event.html @@ -183,6 +183,8 @@ function* testDeltaMultiplierPrefs() deltaX: -gHorizontalLine, deltaY: -gLineHeight, deltaZ: -gLineHeight, lineOrPageDeltaX: -1, lineOrPageDeltaY: -1 }, { deltaMode: WheelEvent.DOM_DELTA_LINE, deltaX: -1.0, deltaY: -1.0, deltaZ: -1.0, lineOrPageDeltaX: -1, lineOrPageDeltaY: -1 }, + { deltaMode: WheelEvent.DOM_DELTA_LINE, skipDeltaModeCheck: true, + deltaX: -1.0, deltaY: -1.0, deltaZ: -1.0, lineOrPageDeltaX: -1, lineOrPageDeltaY: -1 }, { deltaMode: WheelEvent.DOM_DELTA_PAGE, deltaX: -1.0, deltaY: -1.0, deltaZ: -1.0, lineOrPageDeltaX: -1, lineOrPageDeltaY: -1 }, ]; @@ -222,9 +224,20 @@ function* testDeltaMultiplierPrefs() break; } } + if (currentEvent.skipDeltaModeCheck) { + let linesToPixel = SpecialPowers.getIntPref("dom.event.wheel-deltaMode-lines-to-pixel-scale"); + expectedDeltaX *= linesToPixel; + expectedDeltaY *= linesToPixel; + expectedDeltaZ *= linesToPixel; + } else { + is(aEvent.deltaMode, currentEvent.deltaMode, description + "deltaMode (" + currentEvent.deltaMode + ") was invalid"); + } is(aEvent.deltaX, expectedDeltaX, description + "deltaX (" + currentEvent.deltaX + ") was invalid"); is(aEvent.deltaY, expectedDeltaY, description + "deltaY (" + currentEvent.deltaY + ") was invalid"); is(aEvent.deltaZ, expectedDeltaZ, description + "deltaZ (" + currentEvent.deltaZ + ") was invalid"); + if (currentEvent.skipDeltaModeCheck) { + isnot(SpecialPowers.wrap(aEvent).deltaMode, aEvent.deltaMode, description + "deltaMode should be changed for content if unchecked"); + } if (expectedAsyncHandlerCalls > 0 && --expectedAsyncHandlerCalls == 0) { setTimeout(continueTest, 0); @@ -785,6 +798,12 @@ function continueTest() function runTest() { SpecialPowers.pushPrefEnv({"set": [ + // FIXME(emilio): This test is broken in HiDPI, unclear if + // MozMousePixelScroll is not properly converting to CSS pixels, or + // whether sendWheelAndWait expectes device rather than CSS pixels, or + // something else. + ["layout.css.devPixelsPerPx", 1.0], + ["mousewheel.default.delta_multiplier_x", 100], ["mousewheel.default.delta_multiplier_y", 100], ["mousewheel.default.delta_multiplier_z", 100], diff --git a/dom/webidl/WheelEvent.webidl b/dom/webidl/WheelEvent.webidl index 64ece720df05..48586b793312 100644 --- a/dom/webidl/WheelEvent.webidl +++ b/dom/webidl/WheelEvent.webidl @@ -19,11 +19,10 @@ interface WheelEvent : MouseEvent const unsigned long DOM_DELTA_LINE = 0x01; const unsigned long DOM_DELTA_PAGE = 0x02; - readonly attribute double deltaX; - readonly attribute double deltaY; - readonly attribute double deltaZ; - readonly attribute unsigned long deltaMode; - + [NeedsCallerType] readonly attribute double deltaX; + [NeedsCallerType] readonly attribute double deltaY; + [NeedsCallerType] readonly attribute double deltaZ; + [NeedsCallerType] readonly attribute unsigned long deltaMode; }; dictionary WheelEventInit : MouseEventInit diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml index fb91b235acd8..8fd40408465b 100644 --- a/modules/libpref/init/StaticPrefList.yaml +++ b/modules/libpref/init/StaticPrefList.yaml @@ -1670,6 +1670,33 @@ value: true mirror: always +# Whether WheelEvent should return pixels instead of lines for +# WheelEvent.deltaX/Y/Z, when deltaMode hasn't been checked. +# +# Other browsers don't use line deltas and websites forget to check for it, see +# bug 1392460. +- name: dom.event.wheel-deltaMode-lines.disabled + type: bool + value: @IS_NIGHTLY_BUILD@ + mirror: always + +# Mostly for debugging. Whether we should do the same as +# dom.event.wheel-deltaMode-lines.disabled, but unconditionally rather than +# only when deltaMode hasn't been checked. +- name: dom.event.wheel-deltaMode-lines.always-disabled + type: bool + value: false + mirror: always + +# When dom.event.wheel-deltaMode-lines.disabled is true, this is the lines to +# pixels multiplier that gets used. +# +# The value here is taken from PIXELS_PER_LINE_SCALE from pdf.js. +- name: dom.event.wheel-deltaMode-lines-to-pixel-scale + type: uint32_t + value: 30 + mirror: always + #if defined(XP_MACOSX) # Whether to disable treating ctrl click as right click - name: dom.event.treat_ctrl_click_as_right_click.disabled