// ownerGlobal isn't defined in content privileged windows. /* eslint-disable mozilla/use-ownerGlobal */ // Utilities for synthesizing of native events. async function getResolution() { let resolution = -1; // bogus value in case DWU fails us // Use window.top to get the root content window which is what has // the resolution. resolution = await SpecialPowers.spawn(window.top, [], () => { return SpecialPowers.getDOMWindowUtils(content.window).getResolution(); }); return resolution; } function getPlatform() { if (navigator.platform.indexOf("Win") == 0) { return "windows"; } if (navigator.platform.indexOf("Mac") == 0) { return "mac"; } // Check for Android before Linux if (navigator.appVersion.includes("Android")) { return "android"; } if (navigator.platform.indexOf("Linux") == 0) { return "linux"; } return "unknown"; } function nativeVerticalWheelEventMsg() { switch (getPlatform()) { case "windows": return 0x020a; // WM_MOUSEWHEEL case "mac": var useWheelCodepath = SpecialPowers.getBoolPref( "apz.test.mac.synth_wheel_input", false ); // Default to 1 (kCGScrollPhaseBegan) to trigger PanGestureInput events // from widget code. Allow setting a pref to override this behaviour and // trigger ScrollWheelInput events instead. return useWheelCodepath ? 0 : 1; case "linux": return 4; // value is unused, pass GDK_SCROLL_SMOOTH anyway } throw new Error( "Native wheel events not supported on platform " + getPlatform() ); } function nativeHorizontalWheelEventMsg() { switch (getPlatform()) { case "windows": return 0x020e; // WM_MOUSEHWHEEL case "mac": return 0; // value is unused, can be anything case "linux": return 4; // value is unused, pass GDK_SCROLL_SMOOTH anyway } throw new Error( "Native wheel events not supported on platform " + getPlatform() ); } function nativeArrowDownKey() { switch (getPlatform()) { case "windows": return WIN_VK_DOWN; case "mac": return MAC_VK_DownArrow; } throw new Error( "Native key events not supported on platform " + getPlatform() ); } function nativeArrowUpKey() { switch (getPlatform()) { case "windows": return WIN_VK_UP; case "mac": return MAC_VK_UpArrow; } throw new Error( "Native key events not supported on platform " + getPlatform() ); } function targetIsWindow(aTarget) { return aTarget.Window && aTarget instanceof aTarget.Window; } function targetIsTopWindow(aTarget) { if (!targetIsWindow(aTarget)) { return false; } return aTarget == aTarget.top; } // Given an event target which may be a window or an element, get the associated window. function windowForTarget(aTarget) { if (targetIsWindow(aTarget)) { return aTarget; } return aTarget.ownerDocument.defaultView; } // Given an event target which may be a window or an element, get the associated element. function elementForTarget(aTarget) { if (targetIsWindow(aTarget)) { return aTarget.document.documentElement; } return aTarget; } // Given an event target which may be a window or an element, get the associatd nsIDOMWindowUtils. function utilsForTarget(aTarget) { return SpecialPowers.getDOMWindowUtils(windowForTarget(aTarget)); } // Given a pixel scrolling delta, converts it to the platform's native units. function nativeScrollUnits(aTarget, aDimen) { switch (getPlatform()) { case "linux": { // GTK deltas are treated as line height divided by 3 by gecko. var targetWindow = windowForTarget(aTarget); var targetElement = elementForTarget(aTarget); var lineHeight = targetWindow.getComputedStyle(targetElement)["font-size"]; return aDimen / (parseInt(lineHeight) * 3); } } return aDimen; } function parseNativeModifiers(aModifiers, aWindow = window) { let modifiers = 0; if (aModifiers.capsLockKey) { modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CAPS_LOCK; } if (aModifiers.numLockKey) { modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_NUM_LOCK; } if (aModifiers.shiftKey) { modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_SHIFT_LEFT; } if (aModifiers.shiftRightKey) { modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_SHIFT_RIGHT; } if (aModifiers.ctrlKey) { modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_LEFT; } if (aModifiers.ctrlRightKey) { modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_RIGHT; } if (aModifiers.altKey) { modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_LEFT; } if (aModifiers.altRightKey) { modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_RIGHT; } if (aModifiers.metaKey) { modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_LEFT; } if (aModifiers.metaRightKey) { modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_RIGHT; } if (aModifiers.helpKey) { modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_HELP; } if (aModifiers.fnKey) { modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_FUNCTION; } if (aModifiers.numericKeyPadKey) { modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_NUMERIC_KEY_PAD; } if (aModifiers.accelKey) { modifiers |= _EU_isMac(aWindow) ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_LEFT : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_LEFT; } if (aModifiers.accelRightKey) { modifiers |= _EU_isMac(aWindow) ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_RIGHT : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_RIGHT; } if (aModifiers.altGrKey) { modifiers |= _EU_isMac(aWindow) ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_LEFT : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_GRAPH; } return modifiers; } // Several event sythesization functions below (and their helpers) take a "target" // parameter which may be either an element or a window. For such functions, // the target's "bounding rect" refers to the bounding client rect for an element, // and the window's origin for a window. // Not all functions have been "upgraded" to allow a window argument yet; feel // free to upgrade others as necessary. // Get the origin of |aTarget| relative to the root content document's // visual viewport in CSS coordinates. // |aTarget| may be an element (contained in the root content document or // a subdocument) or, as a special case, the root content window. // FIXME: Support iframe windows as targets. function _getTargetRect(aTarget) { let rect = { left: 0, top: 0, width: 0, height: 0 }; // If the target is the root content window, its origin relative // to the visual viewport is (0, 0). if (aTarget instanceof Window) { return rect; } if (aTarget.Window && aTarget instanceof aTarget.Window) { // iframe window // FIXME: Compute proper rect against the root content window return rect; } // Otherwise, we have an element. Start with the origin of // its bounding client rect which is relative to the enclosing // document's layout viewport. Note that for iframes, the // layout viewport is also the visual viewport. const boundingClientRect = aTarget.getBoundingClientRect(); rect.left = boundingClientRect.left; rect.top = boundingClientRect.top; rect.width = boundingClientRect.width; rect.height = boundingClientRect.height; // Iterate up the window hierarchy until we reach the root // content window, adding the offsets of any iframe windows // relative to their parent window. while (aTarget.ownerDocument.defaultView.frameElement) { const iframe = aTarget.ownerDocument.defaultView.frameElement; // The offset of the iframe window relative to the parent window // includes the iframe's border, and the iframe's origin in its // containing document. const style = iframe.ownerDocument.defaultView.getComputedStyle(iframe); const borderLeft = parseFloat(style.borderLeftWidth) || 0; const borderTop = parseFloat(style.borderTopWidth) || 0; const borderRight = parseFloat(style.borderRightWidth) || 0; const borderBottom = parseFloat(style.borderBottomWidth) || 0; const paddingLeft = parseFloat(style.paddingLeft) || 0; const paddingTop = parseFloat(style.paddingTop) || 0; const paddingRight = parseFloat(style.paddingRight) || 0; const paddingBottom = parseFloat(style.paddingBottom) || 0; const iframeRect = iframe.getBoundingClientRect(); rect.left += iframeRect.left + borderLeft + paddingLeft; rect.top += iframeRect.top + borderTop + paddingTop; if ( rect.left + rect.width > iframeRect.right - borderRight - paddingRight ) { rect.width = Math.max( iframeRect.right - borderRight - paddingRight - rect.left, 0 ); } if ( rect.top + rect.height > iframeRect.bottom - borderBottom - paddingBottom ) { rect.height = Math.max( iframeRect.bottom - borderBottom - paddingBottom - rect.top, 0 ); } aTarget = iframe; } return rect; } // Returns the in-process root window for the given |aWindow|. function getInProcessRootWindow(aWindow) { let window = aWindow; while (window.frameElement) { window = window.frameElement.ownerDocument.defaultView; } return window; } // Convert (offsetX, offsetY) of target or center of it, in CSS pixels to device // pixels relative to the screen. // TODO: this function currently does not incorporate some CSS transforms on // elements enclosing target, e.g. scale transforms. async function coordinatesRelativeToScreen(aParams) { const { target, // The target element or window offsetX, // X offset relative to `target` offsetY, // Y offset relative to `target` atCenter, // Instead of offsetX/offsetY, return center of `target` } = aParams; // Note that |window| might not be the root content window, for two // possible reasons: // 1. The mochitest that's calling into this function is not using a mechanism // like runSubtestsSeriallyInFreshWindows() to load the test page in // a top-level context, so it's loaded into an iframe by the mochitest // harness. // 2. The mochitest itself creates an iframe and calls this function from // script running in the context of the iframe. // Since the resolution applies to the top level content document, below we // use the mozInnerScreen{X,Y} of the top level content window (window.top) // only for the case where this function gets called in the top level content // document. In other cases we use nsIDOMWindowUtils.toScreenRect(). // We do often specify `window` as the target, if it's the top level window, // `nsIDOMWindowUtils.toScreenRect` isn't suitable because the function is // supposed to be called with values in the document coords, so for example // if desktop zoom is being applied, (0, 0) in the document coords might be // outside of the visual viewport, i.e. it's going to be negative with the // `toScreenRect` conversion, whereas the call sites with `window` of this // function expect (0, 0) position should be the visual viport's offset. So // in such cases we simply use mozInnerScreen{X,Y} to convert the given value // to the screen coords. if (target instanceof Window && window.parent == window) { const resolution = await getResolution(); const deviceScale = window.devicePixelRatio; return { x: window.mozInnerScreenX * deviceScale + (atCenter ? 0 : offsetX) * resolution * deviceScale, y: window.mozInnerScreenY * deviceScale + (atCenter ? 0 : offsetY) * resolution * deviceScale, }; } const rect = _getTargetRect(target); const utils = SpecialPowers.getDOMWindowUtils(getInProcessRootWindow(window)); const positionInScreenCoords = utils.toScreenRect( rect.left + (atCenter ? rect.width / 2 : offsetX), rect.top + (atCenter ? rect.height / 2 : offsetY), 0, 0 ); return { x: positionInScreenCoords.x, y: positionInScreenCoords.y, }; } // Get the bounding box of aElement, and return it in device pixels // relative to the screen. // TODO: This function should probably take into account the resolution and // the relative viewport rect like coordinatesRelativeToScreen() does. function rectRelativeToScreen(aElement) { var targetWindow = aElement.ownerDocument.defaultView; var scale = targetWindow.devicePixelRatio; var rect = aElement.getBoundingClientRect(); return { x: (targetWindow.mozInnerScreenX + rect.left) * scale, y: (targetWindow.mozInnerScreenY + rect.top) * scale, width: rect.width * scale, height: rect.height * scale, }; } // Synthesizes a native mousewheel event and returns immediately. This does not // guarantee anything; you probably want to use one of the other functions below // which actually wait for results. // aX and aY are relative to the top-left of |aTarget|'s bounding rect. // aDeltaX and aDeltaY are pixel deltas, and aObserver can be left undefined // if not needed. async function synthesizeNativeWheel( aTarget, aX, aY, aDeltaX, aDeltaY, aObserver ) { var pt = await coordinatesRelativeToScreen({ offsetX: aX, offsetY: aY, target: aTarget, }); if (aDeltaX && aDeltaY) { throw new Error( "Simultaneous wheeling of horizontal and vertical is not supported on all platforms." ); } aDeltaX = nativeScrollUnits(aTarget, aDeltaX); aDeltaY = nativeScrollUnits(aTarget, aDeltaY); var msg = aDeltaX ? nativeHorizontalWheelEventMsg() : nativeVerticalWheelEventMsg(); var utils = utilsForTarget(aTarget); var element = elementForTarget(aTarget); utils.sendNativeMouseScrollEvent( pt.x, pt.y, msg, aDeltaX, aDeltaY, 0, 0, // Specify MOUSESCROLL_SCROLL_LINES if the test wants to run through wheel // input code path on Mac since it's normal mouse wheel inputs. SpecialPowers.getBoolPref("apz.test.mac.synth_wheel_input", false) ? SpecialPowers.DOMWindowUtils.MOUSESCROLL_SCROLL_LINES : 0, element, aObserver ); return true; } // Synthesizes a native pan gesture event and returns immediately. // NOTE: This works only on Mac. // You can specify kCGScrollPhaseBegan = 1, kCGScrollPhaseChanged = 2 and // kCGScrollPhaseEnded = 4 for |aPhase|. async function synthesizeNativePanGestureEvent( aTarget, aX, aY, aDeltaX, aDeltaY, aPhase, aObserver ) { if (getPlatform() != "mac") { throw new Error( `synthesizeNativePanGestureEvent doesn't work on ${getPlatform()}` ); } var pt = await coordinatesRelativeToScreen({ offsetX: aX, offsetY: aY, target: aTarget, }); if (aDeltaX && aDeltaY) { throw new Error( "Simultaneous panning of horizontal and vertical is not supported." ); } aDeltaX = nativeScrollUnits(aTarget, aDeltaX); aDeltaY = nativeScrollUnits(aTarget, aDeltaY); var element = elementForTarget(aTarget); var utils = utilsForTarget(aTarget); utils.sendNativeMouseScrollEvent( pt.x, pt.y, aPhase, aDeltaX, aDeltaY, 0 /* deltaZ */, 0 /* modifiers */, 0 /* scroll event unit pixel */, element, aObserver ); return true; } // Sends a native touchpad pan event and resolve the returned promise once the // request has been successfully made to the OS. // NOTE: This works only on Windows and Linux. // You can specify nsIDOMWindowUtils.PHASE_BEGIN, PHASE_UPDATE and PHASE_END // for |aPhase|. async function promiseNativeTouchpadPanEventAndWaitForObserver( aTarget, aX, aY, aDeltaX, aDeltaY, aPhase ) { if (getPlatform() != "windows" && getPlatform() != "linux") { throw new Error( `promiseNativeTouchpadPanEventAndWaitForObserver doesn't work on ${getPlatform()}` ); } let pt = await coordinatesRelativeToScreen({ offsetX: aX, offsetY: aY, target: aTarget, }); const utils = utilsForTarget(aTarget); return new Promise(resolve => { var observer = { observe(aSubject, aTopic, aData) { if (aTopic == "touchpadpanevent") { resolve(); } }, }; utils.sendNativeTouchpadPan( aPhase, pt.x, pt.y, aDeltaX, aDeltaY, 0, observer ); }); } async function synthesizeSimpleGestureEvent( aElement, aType, aX, aY, aDirection, aDelta, aModifiers, aClickCount ) { let pt = await coordinatesRelativeToScreen({ offsetX: aX, offsetY: aY, target: aElement, }); let utils = utilsForTarget(aElement); utils.sendSimpleGestureEvent( aType, pt.x, pt.y, aDirection, aDelta, aModifiers, aClickCount ); } // Synthesizes a native pan gesture event and resolve the returned promise once the // request has been successfully made to the OS. function promiseNativePanGestureEventAndWaitForObserver( aElement, aX, aY, aDeltaX, aDeltaY, aPhase ) { return new Promise(resolve => { var observer = { observe(aSubject, aTopic, aData) { if (aTopic == "mousescrollevent") { resolve(); } }, }; synthesizeNativePanGestureEvent( aElement, aX, aY, aDeltaX, aDeltaY, aPhase, observer ); }); } // Synthesizes a native mousewheel event and resolve the returned promise once the // request has been successfully made to the OS. This does not necessarily // guarantee that the OS generates the event we requested. See // synthesizeNativeWheel for details on the parameters. function promiseNativeWheelAndWaitForObserver( aElement, aX, aY, aDeltaX, aDeltaY ) { return new Promise(resolve => { var observer = { observe(aSubject, aTopic, aData) { if (aTopic == "mousescrollevent") { resolve(); } }, }; synthesizeNativeWheel(aElement, aX, aY, aDeltaX, aDeltaY, observer); }); } // Synthesizes a native mousewheel event and resolve the returned promise once the // wheel event is dispatched to |aTarget|'s containing window. If the event // targets content in a subdocument, |aTarget| should be inside the // subdocument (or the subdocument's window). See synthesizeNativeWheel for // details on the other parameters. function promiseNativeWheelAndWaitForWheelEvent( aTarget, aX, aY, aDeltaX, aDeltaY ) { return new Promise((resolve, reject) => { var targetWindow = windowForTarget(aTarget); targetWindow.addEventListener( "wheel", function (e) { setTimeout(resolve, 0); }, { once: true } ); try { synthesizeNativeWheel(aTarget, aX, aY, aDeltaX, aDeltaY); } catch (e) { reject(e); } }); } // Synthesizes a native mousewheel event and resolves the returned promise once the // first resulting scroll event is dispatched to |aTarget|'s containing window. // If the event targets content in a subdocument, |aTarget| should be inside // the subdocument (or the subdocument's window). See synthesizeNativeWheel // for details on the other parameters. function promiseNativeWheelAndWaitForScrollEvent( aTarget, aX, aY, aDeltaX, aDeltaY ) { return new Promise((resolve, reject) => { var targetWindow = windowForTarget(aTarget); targetWindow.addEventListener( "scroll", function () { setTimeout(resolve, 0); }, { capture: true, once: true } ); // scroll events don't always bubble try { synthesizeNativeWheel(aTarget, aX, aY, aDeltaX, aDeltaY); } catch (e) { reject(e); } }); } async function synthesizeTouchpadPinch(scales, focusX, focusY, options) { var scalesAndFoci = []; for (let i = 0; i < scales.length; i++) { scalesAndFoci.push([scales[i], focusX, focusY]); } await synthesizeTouchpadGesture(scalesAndFoci, options); } // scalesAndFoci is an array of [scale, focusX, focuxY] tuples. async function synthesizeTouchpadGesture(scalesAndFoci, options) { // Check for options, fill in defaults if appropriate. let waitForTransformEnd = options.waitForTransformEnd !== undefined ? options.waitForTransformEnd : true; let waitForFrames = options.waitForFrames !== undefined ? options.waitForFrames : false; // Register the listener for the TransformEnd observer topic let transformEndPromise = promiseTransformEnd(); var modifierFlags = 0; var utils = utilsForTarget(document.body); for (let i = 0; i < scalesAndFoci.length; i++) { var pt = await coordinatesRelativeToScreen({ offsetX: scalesAndFoci[i][1], offsetY: scalesAndFoci[i][2], target: document.body, }); var phase; if (i === 0) { phase = SpecialPowers.DOMWindowUtils.PHASE_BEGIN; } else if (i === scalesAndFoci.length - 1) { phase = SpecialPowers.DOMWindowUtils.PHASE_END; } else { phase = SpecialPowers.DOMWindowUtils.PHASE_UPDATE; } utils.sendNativeTouchpadPinch( phase, scalesAndFoci[i][0], pt.x, pt.y, modifierFlags ); if (waitForFrames) { await promiseFrame(); } } // Wait for TransformEnd to fire. if (waitForTransformEnd) { await transformEndPromise; } } async function synthesizeTouchpadPan( focusX, focusY, deltaXs, deltaYs, options ) { // Check for options, fill in defaults if appropriate. let waitForTransformEnd = options.waitForTransformEnd !== undefined ? options.waitForTransformEnd : true; let waitForFrames = options.waitForFrames !== undefined ? options.waitForFrames : false; // Register the listener for the TransformEnd observer topic let transformEndPromise = promiseTransformEnd(); var modifierFlags = 0; var pt = await coordinatesRelativeToScreen({ offsetX: focusX, offsetY: focusY, target: document.body, }); var utils = utilsForTarget(document.body); for (let i = 0; i < deltaXs.length; i++) { var phase; if (i === 0) { phase = SpecialPowers.DOMWindowUtils.PHASE_BEGIN; } else if (i === deltaXs.length - 1) { phase = SpecialPowers.DOMWindowUtils.PHASE_END; } else { phase = SpecialPowers.DOMWindowUtils.PHASE_UPDATE; } utils.sendNativeTouchpadPan( phase, pt.x, pt.y, deltaXs[i], deltaYs[i], modifierFlags ); if (waitForFrames) { await promiseFrame(); } } // Wait for TransformEnd to fire. if (waitForTransformEnd) { await transformEndPromise; } } // Synthesizes a native touch event and dispatches it. aX and aY in CSS pixels // relative to the top-left of |aTarget|'s bounding rect. async function synthesizeNativeTouch( aTarget, aX, aY, aType, aObserver = null, aTouchId = 0 ) { var pt = await coordinatesRelativeToScreen({ offsetX: aX, offsetY: aY, target: aTarget, }); var utils = utilsForTarget(aTarget); utils.sendNativeTouchPoint(aTouchId, aType, pt.x, pt.y, 1, 90, aObserver); return true; } function sendBasicNativePointerInput( utils, aId, aPointerType, aState, aX, aY, aObserver, { pressure = 1, twist = 0, tiltX = 0, tiltY = 0, button = 0 } = {} ) { switch (aPointerType) { case "touch": utils.sendNativeTouchPoint(aId, aState, aX, aY, pressure, 90, aObserver); break; case "pen": utils.sendNativePenInput( aId, aState, aX, aY, pressure, twist, tiltX, tiltY, button, aObserver ); break; default: throw new Error(`Not supported: ${aPointerType}`); } } async function promiseNativePointerInput( aTarget, aPointerType, aState, aX, aY, options ) { const pt = await coordinatesRelativeToScreen({ offsetX: aX, offsetY: aY, target: aTarget, }); const utils = utilsForTarget(aTarget); return new Promise(resolve => { sendBasicNativePointerInput( utils, options?.pointerId ?? 0, aPointerType, aState, pt.x, pt.y, resolve, options ); }); } /** * Function to generate native pointer events as a sequence. * @param aTarget is the element or window whose bounding rect the coordinates are * relative to. * @param aPointerType "touch" or "pen". * @param aPositions is a 2D array of position data. It is indexed as [row][column], * where advancing the row counter moves forward in time, and each column * represents a single pointer. Each row must have exactly * the same number of columns, and the number of columns must match the length * of the aPointerIds parameter. * For each row, each entry is either an object with x and y fields, * or a null. A null value indicates that the pointer should be "lifted" * (i.e. send a touchend for that touch input). A non-null value therefore * indicates the position of the pointer input. * This function takes care of the state tracking necessary to send * pointerup/pointerdown inputs as necessary as the pointers go up and down. * @param aObserver is the observer that will get registered on the very last * native pointer synthesis call this function makes. * @param aPointerIds is an array holding the pointer ID values. */ async function synthesizeNativePointerSequences( aTarget, aPointerType, aPositions, aObserver = null, aPointerIds = [0], options ) { // We use lastNonNullValue to figure out which synthesizeNativeTouch call // will be the last one we make, so that we can register aObserver on it. var lastNonNullValue = -1; for (let i = 0; i < aPositions.length; i++) { if (aPositions[i] == null) { throw new Error(`aPositions[${i}] was unexpectedly null`); } if (aPositions[i].length != aPointerIds.length) { throw new Error( `aPositions[${i}] did not have the expected number of positions; ` + `expected ${aPointerIds.length} pointers but found ${aPositions[i].length}` ); } for (let j = 0; j < aPointerIds.length; j++) { if (aPositions[i][j] != null) { lastNonNullValue = i * aPointerIds.length + j; // Do the conversion to screen space before actually synthesizing // the events, otherwise the screen space may change as a result of // the touch inputs and the conversion may not work as intended. aPositions[i][j] = await coordinatesRelativeToScreen({ offsetX: aPositions[i][j].x, offsetY: aPositions[i][j].y, target: aTarget, }); } } } if (lastNonNullValue < 0) { throw new Error("All values in positions array were null!"); } // Insert a row of nulls at the end of aPositions, to ensure that all // touches get removed. If the touches have already been removed this will // just add an extra no-op iteration in the aPositions loop below. var allNullRow = new Array(aPointerIds.length); allNullRow.fill(null); aPositions.push(allNullRow); // The last sendNativeTouchPoint call will be the TOUCH_REMOVE which happens // one iteration of aPosition after the last non-null value. var lastSynthesizeCall = lastNonNullValue + aPointerIds.length; // track which touches are down and which are up. start with all up var currentPositions = new Array(aPointerIds.length); currentPositions.fill(null); var utils = utilsForTarget(aTarget); // Iterate over the position data now, and generate the touches requested for (let i = 0; i < aPositions.length; i++) { for (let j = 0; j < aPointerIds.length; j++) { if (aPositions[i][j] == null) { // null means lift the finger if (currentPositions[j] == null) { // it's already lifted, do nothing } else { // synthesize the touch-up. If this is the last call we're going to // make, pass the observer as well var thisIndex = i * aPointerIds.length + j; var observer = lastSynthesizeCall == thisIndex ? aObserver : null; sendBasicNativePointerInput( utils, aPointerIds[j], aPointerType, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE, currentPositions[j].x, currentPositions[j].y, observer, options ); currentPositions[j] = null; } } else { sendBasicNativePointerInput( utils, aPointerIds[j], aPointerType, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT, aPositions[i][j].x, aPositions[i][j].y, null, options ); currentPositions[j] = aPositions[i][j]; } } } return true; } async function synthesizeNativeTouchSequences( aTarget, aPositions, aObserver = null, aTouchIds = [0] ) { await synthesizeNativePointerSequences( aTarget, "touch", aPositions, aObserver, aTouchIds ); } async function synthesizeNativePointerDrag( aTarget, aPointerType, aX, aY, aDeltaX, aDeltaY, aObserver = null, aPointerId = 0, options ) { var steps = Math.max(Math.abs(aDeltaX), Math.abs(aDeltaY)); var positions = [[{ x: aX, y: aY }]]; for (var i = 1; i < steps; i++) { var dx = i * (aDeltaX / steps); var dy = i * (aDeltaY / steps); var pos = { x: aX + dx, y: aY + dy }; positions.push([pos]); } positions.push([{ x: aX + aDeltaX, y: aY + aDeltaY }]); return synthesizeNativePointerSequences( aTarget, aPointerType, positions, aObserver, [aPointerId], options ); } // Note that when calling this function you'll want to make sure that the pref // "apz.touch_start_tolerance" is set to 0, or some of the touchmove will get // consumed to overcome the panning threshold. async function synthesizeNativeTouchDrag( aTarget, aX, aY, aDeltaX, aDeltaY, aObserver = null, aTouchId = 0 ) { return synthesizeNativePointerDrag( aTarget, "touch", aX, aY, aDeltaX, aDeltaY, aObserver, aTouchId ); } function promiseNativePointerDrag( aTarget, aPointerType, aX, aY, aDeltaX, aDeltaY, aPointerId = 0, options ) { return new Promise(resolve => { synthesizeNativePointerDrag( aTarget, aPointerType, aX, aY, aDeltaX, aDeltaY, resolve, aPointerId, options ); }); } // Promise-returning variant of synthesizeNativeTouchDrag function promiseNativeTouchDrag( aTarget, aX, aY, aDeltaX, aDeltaY, aTouchId = 0 ) { return new Promise(resolve => { synthesizeNativeTouchDrag( aTarget, aX, aY, aDeltaX, aDeltaY, resolve, aTouchId ); }); } // Tapping is essentially a dragging with no move function promiseNativePointerTap(aTarget, aPointerType, aX, aY, options) { return promiseNativePointerDrag( aTarget, aPointerType, aX, aY, 0, 0, options?.pointerId ?? 0, options ); } async function synthesizeNativeTap(aTarget, aX, aY, aObserver = null) { var pt = await coordinatesRelativeToScreen({ offsetX: aX, offsetY: aY, target: aTarget, }); let utils = utilsForTarget(aTarget); utils.sendNativeTouchTap(pt.x, pt.y, false, aObserver); return true; } // only currently implemented on macOS async function synthesizeNativeTouchpadDoubleTap(aTarget, aX, aY) { ok( getPlatform() == "mac", "only implemented on mac. implement sendNativeTouchpadDoubleTap for this platform," + " see bug 1696802 for how it was done on macOS" ); let pt = await coordinatesRelativeToScreen({ offsetX: aX, offsetY: aY, target: aTarget, }); let utils = utilsForTarget(aTarget); utils.sendNativeTouchpadDoubleTap(pt.x, pt.y, 0); return true; } // If the event targets content in a subdocument, |aTarget| should be inside the // subdocument (or the subdocument window). async function synthesizeNativeMouseEventWithAPZ(aParams, aObserver = null) { if (aParams.win !== undefined) { throw Error( "Are you trying to use EventUtils' API? `win` won't be used with synthesizeNativeMouseClickWithAPZ." ); } if (aParams.scale !== undefined) { throw Error( "Are you trying to use EventUtils' API? `scale` won't be used with synthesizeNativeMouseClickWithAPZ." ); } if (aParams.elementOnWidget !== undefined) { throw Error( "Are you trying to use EventUtils' API? `elementOnWidget` won't be used with synthesizeNativeMouseClickWithAPZ." ); } const { type, // "click", "mousedown", "mouseup" or "mousemove" target, // Origin of offsetX and offsetY, must be an element offsetX, // X offset in `target` in CSS Pixels offsetY, // Y offset in `target` in CSS pixels atCenter, // Instead of offsetX/Y, synthesize the event at center of `target` screenX, // X offset in screen in device pixels, offsetX/Y nor atCenter must not be set if this is set screenY, // Y offset in screen in device pixels, offsetX/Y nor atCenter must not be set if this is set button = 0, // if "click", "mousedown", "mouseup", set same value as DOM MouseEvent.button modifiers = {}, // Active modifiers, see `parseNativeModifiers` } = aParams; if (atCenter) { if (offsetX != undefined || offsetY != undefined) { throw Error( `atCenter is specified, but offsetX (${offsetX}) and/or offsetY (${offsetY}) are also specified` ); } if (screenX != undefined || screenY != undefined) { throw Error( `atCenter is specified, but screenX (${screenX}) and/or screenY (${screenY}) are also specified` ); } } else if (offsetX != undefined && offsetY != undefined) { if (screenX != undefined || screenY != undefined) { throw Error( `offsetX/Y are specified, but screenX (${screenX}) and/or screenY (${screenY}) are also specified` ); } } else if (screenX != undefined && screenY != undefined) { if (offsetX != undefined || offsetY != undefined) { throw Error( `screenX/Y are specified, but offsetX (${offsetX}) and/or offsetY (${offsetY}) are also specified` ); } } const pt = await (async () => { if (screenX != undefined) { return { x: screenX, y: screenY }; } return coordinatesRelativeToScreen({ offsetX, offsetY, atCenter, target, }); })(); const utils = utilsForTarget(target); const element = elementForTarget(target); const modifierFlags = parseNativeModifiers(modifiers); if (type === "click") { utils.sendNativeMouseEvent( pt.x, pt.y, utils.NATIVE_MOUSE_MESSAGE_BUTTON_DOWN, button, modifierFlags, element, function () { utils.sendNativeMouseEvent( pt.x, pt.y, utils.NATIVE_MOUSE_MESSAGE_BUTTON_UP, button, modifierFlags, element, aObserver ); } ); return; } utils.sendNativeMouseEvent( pt.x, pt.y, (() => { switch (type) { case "mousedown": return utils.NATIVE_MOUSE_MESSAGE_BUTTON_DOWN; case "mouseup": return utils.NATIVE_MOUSE_MESSAGE_BUTTON_UP; case "mousemove": return utils.NATIVE_MOUSE_MESSAGE_MOVE; default: throw Error(`Invalid type is specified: ${type}`); } })(), button, modifierFlags, element, aObserver ); } function promiseNativeMouseEventWithAPZ(aParams) { return new Promise(resolve => synthesizeNativeMouseEventWithAPZ(aParams, resolve) ); } // See synthesizeNativeMouseEventWithAPZ for the detail of aParams. function promiseNativeMouseEventWithAPZAndWaitForEvent(aParams) { return new Promise(resolve => { const targetWindow = windowForTarget(aParams.target); const eventType = aParams.eventTypeToWait || aParams.type; targetWindow.addEventListener(eventType, resolve, { once: true, }); synthesizeNativeMouseEventWithAPZ(aParams); }); } // Move the mouse to (dx, dy) relative to |target|, and scroll the wheel // at that location. // Moving the mouse is necessary to avoid wheel events from two consecutive // promiseMoveMouseAndScrollWheelOver() calls on different elements being incorrectly // considered as part of the same wheel transaction. // We also wait for the mouse move event to be processed before sending the // wheel event, otherwise there is a chance they might get reordered, and // we have the transaction problem again. // This function returns a promise that is resolved when the resulting wheel // (if waitForScroll = false) or scroll (if waitForScroll = true) event is // received. function promiseMoveMouseAndScrollWheelOver( target, dx, dy, waitForScroll = true, scrollDelta = 10 ) { let p = promiseNativeMouseEventWithAPZAndWaitForEvent({ type: "mousemove", target, offsetX: dx, offsetY: dy, }); if (waitForScroll) { p = p.then(() => { return promiseNativeWheelAndWaitForScrollEvent( target, dx, dy, 0, -scrollDelta ); }); } else { p = p.then(() => { return promiseNativeWheelAndWaitForWheelEvent( target, dx, dy, 0, -scrollDelta ); }); } return p; } async function scrollbarDragStart(aTarget, aScaleFactor) { var targetElement = elementForTarget(aTarget); var w = {}, h = {}; utilsForTarget(aTarget).getScrollbarSizes(targetElement, w, h); var verticalScrollbarWidth = w.value; if (verticalScrollbarWidth == 0) { return null; } var upArrowHeight = verticalScrollbarWidth; // assume square scrollbar buttons var startX = targetElement.clientWidth + verticalScrollbarWidth / 2; var startY = upArrowHeight + 5; // start dragging somewhere in the thumb startX *= aScaleFactor; startY *= aScaleFactor; // targetElement.clientWidth is unaffected by the zoom, but if the target // is the root content window, the distance from the window origin to the // scrollbar in CSS pixels does decrease proportionally to the zoom, // so the CSS coordinates we return need to be scaled accordingly. if (targetIsTopWindow(aTarget)) { var resolution = await getResolution(); startX /= resolution; startY /= resolution; } return { x: startX, y: startY }; } // Synthesizes events to drag |target|'s vertical scrollbar by the distance // specified, synthesizing a mousemove for each increment as specified. // Returns null if the element doesn't have a vertical scrollbar. Otherwise, // returns an async function that should be invoked after the mousemoves have been // processed by the widget code, to end the scrollbar drag. Mousemoves being // processed by the widget code can be detected by listening for the mousemove // events in the caller, or for some other event that is triggered by the // mousemove, such as the scroll event resulting from the scrollbar drag. // The aScaleFactor argument should be provided if the scrollframe has been // scaled by an enclosing CSS transform. (TODO: this is a workaround for the // fact that coordinatesRelativeToScreen is supposed to do this automatically // but it currently does not). // Note: helper_scrollbar_snap_bug1501062.html contains a copy of this code // with modifications. Fixes here should be copied there if appropriate. // |target| can be an element (for subframes) or a window (for root frames). async function promiseVerticalScrollbarDrag( aTarget, aDistance = 20, aIncrement = 5, aScaleFactor = 1 ) { var startPoint = await scrollbarDragStart(aTarget, aScaleFactor); var targetElement = elementForTarget(aTarget); if (startPoint == null) { return null; } dump( "Starting drag at " + startPoint.x + ", " + startPoint.y + " from top-left of #" + targetElement.id + "\n" ); // Move the mouse to the scrollbar thumb and drag it down await promiseNativeMouseEventWithAPZ({ target: aTarget, offsetX: startPoint.x, offsetY: startPoint.y, type: "mousemove", }); // mouse down await promiseNativeMouseEventWithAPZ({ target: aTarget, offsetX: startPoint.x, offsetY: startPoint.y, type: "mousedown", }); // drag vertically by |aIncrement| until we reach the specified distance for (var y = aIncrement; y < aDistance; y += aIncrement) { await promiseNativeMouseEventWithAPZ({ target: aTarget, offsetX: startPoint.x, offsetY: startPoint.y + y, type: "mousemove", }); } await promiseNativeMouseEventWithAPZ({ target: aTarget, offsetX: startPoint.x, offsetY: startPoint.y + aDistance, type: "mousemove", }); // and return an async function to call afterwards to finish up the drag return async function () { dump("Finishing drag of #" + targetElement.id + "\n"); await promiseNativeMouseEventWithAPZ({ target: aTarget, offsetX: startPoint.x, offsetY: startPoint.y + aDistance, type: "mouseup", }); }; } // This is similar to promiseVerticalScrollbarDrag except this triggers // the vertical scrollbar drag with a touch drag input. This function // returns true if a scrollbar was present and false if no scrollbar // was found for the given element. async function promiseVerticalScrollbarTouchDrag( aTarget, aDistance = 20, aScaleFactor = 1 ) { var startPoint = await scrollbarDragStart(aTarget, aScaleFactor); var targetElement = elementForTarget(aTarget); if (startPoint == null) { return false; } dump( "Starting touch drag at " + startPoint.x + ", " + startPoint.y + " from top-left of #" + targetElement.id + "\n" ); await promiseNativeTouchDrag( aTarget, startPoint.x, startPoint.y, 0, aDistance ); return true; } // Synthesizes a native mouse drag, starting at offset (mouseX, mouseY) from // the given target. The drag occurs in the given number of steps, to a final // destination of (mouseX + distanceX, mouseY + distanceY) from the target. // Returns a promise (wrapped in a function, so it doesn't execute immediately) // that should be awaited after the mousemoves have been processed by the widget // code, to end the drag. This is important otherwise the OS can sometimes // reorder the events and the drag doesn't have the intended effect (see // bug 1368603). // Example usage: // let dragFinisher = await promiseNativeMouseDrag(myElement, 0, 0); // await myIndicationThatDragHadAnEffect; // await dragFinisher(); async function promiseNativeMouseDrag( target, mouseX, mouseY, distanceX = 20, distanceY = 20, steps = 20 ) { var targetElement = elementForTarget(target); dump( "Starting drag at " + mouseX + ", " + mouseY + " from top-left of #" + targetElement.id + "\n" ); // Move the mouse to the target position await promiseNativeMouseEventWithAPZ({ target, offsetX: mouseX, offsetY: mouseY, type: "mousemove", }); // mouse down await promiseNativeMouseEventWithAPZ({ target, offsetX: mouseX, offsetY: mouseY, type: "mousedown", }); // drag vertically by |increment| until we reach the specified distance for (var s = 1; s <= steps; s++) { let dx = distanceX * (s / steps); let dy = distanceY * (s / steps); dump(`Dragging to ${mouseX + dx}, ${mouseY + dy} from target\n`); await promiseNativeMouseEventWithAPZ({ target, offsetX: mouseX + dx, offsetY: mouseY + dy, type: "mousemove", }); } // and return a function-wrapped promise to call afterwards to finish the drag return function () { return promiseNativeMouseEventWithAPZ({ target, offsetX: mouseX + distanceX, offsetY: mouseY + distanceY, type: "mouseup", }); }; } // Synthesizes a native touch sequence of events corresponding to a pinch-zoom-in // at the given focus point. The focus point must be specified in CSS coordinates // relative to the document body. async function pinchZoomInTouchSequence(focusX, focusY) { // prettier-ignore var zoom_in = [ [ { x: focusX - 25, y: focusY - 50 }, { x: focusX + 25, y: focusY + 50 } ], [ { x: focusX - 30, y: focusY - 80 }, { x: focusX + 30, y: focusY + 80 } ], [ { x: focusX - 35, y: focusY - 110 }, { x: focusX + 40, y: focusY + 110 } ], [ { x: focusX - 40, y: focusY - 140 }, { x: focusX + 45, y: focusY + 140 } ], [ { x: focusX - 45, y: focusY - 170 }, { x: focusX + 50, y: focusY + 170 } ], [ { x: focusX - 50, y: focusY - 200 }, { x: focusX + 55, y: focusY + 200 } ], ]; var touchIds = [0, 1]; return synthesizeNativeTouchSequences(document.body, zoom_in, null, touchIds); } // Returns a promise that is resolved when the observer service dispatches a // message with the given topic. function promiseTopic(aTopic) { return new Promise((resolve, reject) => { SpecialPowers.Services.obs.addObserver(function observer( subject, topic, data ) { try { SpecialPowers.Services.obs.removeObserver(observer, topic); resolve([subject, data]); } catch (ex) { SpecialPowers.Services.obs.removeObserver(observer, topic); reject(ex); } }, aTopic); }); } // Returns a promise that is resolved when a APZ transform ends. function promiseTransformEnd() { return promiseTopic("APZ:TransformEnd"); } function promiseScrollend(aTarget = window) { return promiseOneEvent(aTarget, "scrollend"); } // Returns a promise that resolves after the indicated number // of touchend events have fired on the given target element. function promiseTouchEnd(element, count = 1) { return new Promise(resolve => { var eventCount = 0; var counterFunction = function (e) { eventCount++; if (eventCount == count) { element.removeEventListener("touchend", counterFunction, { passive: true, }); resolve(); } }; element.addEventListener("touchend", counterFunction, { passive: true }); }); } // This generates a touch-based pinch zoom-in gesture that is expected // to succeed. It returns after APZ has completed the zoom and reaches the end // of the transform. The focus point is expected to be in CSS coordinates // relative to the document body. async function pinchZoomInWithTouch(focusX, focusY) { // Register the listener for the TransformEnd observer topic let transformEndPromise = promiseTopic("APZ:TransformEnd"); // Dispatch all the touch events await pinchZoomInTouchSequence(focusX, focusY); // Wait for TransformEnd to fire. await transformEndPromise; } // This generates a touchpad pinch zoom-in gesture that is expected // to succeed. It returns after APZ has completed the zoom and reaches the end // of the transform. The focus point is expected to be in CSS coordinates // relative to the document body. async function pinchZoomInWithTouchpad(focusX, focusY, options = {}) { var zoomIn = [ 1.0, 1.019531, 1.035156, 1.037156, 1.039156, 1.054688, 1.056688, 1.070312, 1.072312, 1.089844, 1.091844, 1.109375, 1.128906, 1.144531, 1.160156, 1.175781, 1.191406, 1.207031, 1.222656, 1.234375, 1.246094, 1.261719, 1.273438, 1.285156, 1.296875, 1.3125, 1.328125, 1.347656, 1.363281, 1.382812, 1.402344, 1.421875, 1.0, ]; await synthesizeTouchpadPinch(zoomIn, focusX, focusY, options); } async function pinchZoomInAndPanWithTouchpad(options = {}) { var x = 584; var y = 347; var scalesAndFoci = []; // Zoom for (var scale = 1.0; scale <= 2.0; scale += 0.2) { scalesAndFoci.push([scale, x, y]); } // Pan (due to a limitation of the current implementation, events // for which the scale doesn't change are dropped, so vary the // scale slightly as well). for (var i = 1; i <= 20; i++) { x -= 4; y -= 5; scalesAndFoci.push([scale + 0.01 * i, x, y]); } await synthesizeTouchpadGesture(scalesAndFoci, options); } async function pinchZoomOutWithTouchpad(focusX, focusY, options = {}) { // The last item equal one to indicate scale end var zoomOut = [ 1.0, 1.375, 1.359375, 1.339844, 1.316406, 1.296875, 1.277344, 1.257812, 1.238281, 1.21875, 1.199219, 1.175781, 1.15625, 1.132812, 1.101562, 1.078125, 1.054688, 1.03125, 1.011719, 0.992188, 0.972656, 0.953125, 0.933594, 1.0, ]; await synthesizeTouchpadPinch(zoomOut, focusX, focusY, options); } async function pinchZoomInOutWithTouchpad(focusX, focusY, options = {}) { // Use the same scale for two events in a row to make sure the code handles this properly. var zoomInOut = [ 1.0, 1.082031, 1.089844, 1.097656, 1.101562, 1.109375, 1.121094, 1.128906, 1.128906, 1.125, 1.097656, 1.074219, 1.054688, 1.035156, 1.015625, 1.0, 1.0, ]; await synthesizeTouchpadPinch(zoomInOut, focusX, focusY, options); } // This generates a touch-based pinch gesture that is expected to succeed // and trigger an APZ:TransformEnd observer notification. // It returns after that notification has been dispatched. // The coordinates of touch events in `touchSequence` are expected to be // in CSS coordinates relative to the document body. async function synthesizeNativeTouchAndWaitForTransformEnd( touchSequence, touchIds ) { // Register the listener for the TransformEnd observer topic let transformEndPromise = promiseTopic("APZ:TransformEnd"); // Dispatch all the touch events await synthesizeNativeTouchSequences( document.body, touchSequence, null, touchIds ); // Wait for TransformEnd to fire. await transformEndPromise; } // Returns a touch sequence for a pinch-zoom-out operation in the center // of the visual viewport. The touch sequence returned is in CSS coordinates // relative to the document body. function pinchZoomOutTouchSequenceAtCenter() { // Divide the half of visual viewport size by 8, then cause touch events // starting from the 7th furthest away from the center towards the center. const deltaX = window.visualViewport.width / 16; const deltaY = window.visualViewport.height / 16; const centerX = window.visualViewport.pageLeft + window.visualViewport.width / 2; const centerY = window.visualViewport.pageTop + window.visualViewport.height / 2; // prettier-ignore var zoom_out = [ [ { x: centerX - (deltaX * 6), y: centerY - (deltaY * 6) }, { x: centerX + (deltaX * 6), y: centerY + (deltaY * 6) } ], [ { x: centerX - (deltaX * 5), y: centerY - (deltaY * 5) }, { x: centerX + (deltaX * 5), y: centerY + (deltaY * 5) } ], [ { x: centerX - (deltaX * 4), y: centerY - (deltaY * 4) }, { x: centerX + (deltaX * 4), y: centerY + (deltaY * 4) } ], [ { x: centerX - (deltaX * 3), y: centerY - (deltaY * 3) }, { x: centerX + (deltaX * 3), y: centerY + (deltaY * 3) } ], [ { x: centerX - (deltaX * 2), y: centerY - (deltaY * 2) }, { x: centerX + (deltaX * 2), y: centerY + (deltaY * 2) } ], [ { x: centerX - (deltaX * 1), y: centerY - (deltaY * 1) }, { x: centerX + (deltaX * 1), y: centerY + (deltaY * 1) } ], ]; return zoom_out; } // This generates a touch-based pinch zoom-out gesture that is expected // to succeed. It returns after APZ has completed the zoom and reaches the end // of the transform. The touch inputs are directed to the center of the // current visual viewport. async function pinchZoomOutWithTouchAtCenter() { var zoom_out = pinchZoomOutTouchSequenceAtCenter(); var touchIds = [0, 1]; await synthesizeNativeTouchAndWaitForTransformEnd(zoom_out, touchIds); } // useTouchpad is only currently implemented on macOS async function synthesizeDoubleTap(element, x, y, useTouchpad) { if (useTouchpad) { await synthesizeNativeTouchpadDoubleTap(element, x, y); } else { await synthesizeNativeTap(element, x, y); await synthesizeNativeTap(element, x, y); } } // useTouchpad is only currently implemented on macOS async function doubleTapOn(element, x, y, useTouchpad) { let transformEndPromise = promiseTransformEnd(); await synthesizeDoubleTap(element, x, y, useTouchpad); // Wait for the APZ:TransformEnd to fire await transformEndPromise; // Flush state so we can query an accurate resolution await promiseApzFlushedRepaints(); } const NativePanHandlerForLinux = { beginPhase: SpecialPowers.DOMWindowUtils.PHASE_BEGIN, updatePhase: SpecialPowers.DOMWindowUtils.PHASE_UPDATE, endPhase: SpecialPowers.DOMWindowUtils.PHASE_END, promiseNativePanEvent: promiseNativeTouchpadPanEventAndWaitForObserver, delta: -50, }; const NativePanHandlerForWindows = { beginPhase: SpecialPowers.DOMWindowUtils.PHASE_BEGIN, updatePhase: SpecialPowers.DOMWindowUtils.PHASE_UPDATE, endPhase: SpecialPowers.DOMWindowUtils.PHASE_END, promiseNativePanEvent: promiseNativeTouchpadPanEventAndWaitForObserver, delta: 50, }; const NativePanHandlerForMac = { // From https://developer.apple.com/documentation/coregraphics/cgscrollphase/kcgscrollphasebegan?language=occ , etc. beginPhase: 1, // kCGScrollPhaseBegan updatePhase: 2, // kCGScrollPhaseChanged endPhase: 4, // kCGScrollPhaseEnded promiseNativePanEvent: promiseNativePanGestureEventAndWaitForObserver, delta: -50, }; const NativePanHandlerForHeadless = { beginPhase: SpecialPowers.DOMWindowUtils.PHASE_BEGIN, updatePhase: SpecialPowers.DOMWindowUtils.PHASE_UPDATE, endPhase: SpecialPowers.DOMWindowUtils.PHASE_END, promiseNativePanEvent: promiseNativeTouchpadPanEventAndWaitForObserver, delta: 50, }; function getPanHandler() { if (SpecialPowers.isHeadless) { return NativePanHandlerForHeadless; } switch (getPlatform()) { case "linux": return NativePanHandlerForLinux; case "windows": return NativePanHandlerForWindows; case "mac": return NativePanHandlerForMac; default: throw new Error( "There's no native pan handler on platform " + getPlatform() ); } } // Lazily get `NativePanHandler` to avoid an exception where we don't support // native pan events (e.g. Android). if (!window.hasOwnProperty("NativePanHandler")) { Object.defineProperty(window, "NativePanHandler", { get() { return getPanHandler(); }, }); } async function panRightToLeftBegin(aElement, aX, aY, aMultiplier) { await NativePanHandler.promiseNativePanEvent( aElement, aX, aY, NativePanHandler.delta * aMultiplier, 0, NativePanHandler.beginPhase ); } async function panRightToLeftUpdate(aElement, aX, aY, aMultiplier) { await NativePanHandler.promiseNativePanEvent( aElement, aX, aY, NativePanHandler.delta * aMultiplier, 0, NativePanHandler.updatePhase ); } async function panRightToLeftEnd(aElement, aX, aY, aMultiplier) { await NativePanHandler.promiseNativePanEvent( aElement, aX, aY, 0, 0, NativePanHandler.endPhase ); } async function panRightToLeft(aElement, aX, aY, aMultiplier) { await panRightToLeftBegin(aElement, aX, aY, aMultiplier); await panRightToLeftUpdate(aElement, aX, aY, aMultiplier); await panRightToLeftEnd(aElement, aX, aY, aMultiplier); } async function panLeftToRight(aElement, aX, aY, aMultiplier) { await panLeftToRightBegin(aElement, aX, aY, aMultiplier); await panLeftToRightUpdate(aElement, aX, aY, aMultiplier); await panLeftToRightEnd(aElement, aX, aY, aMultiplier); } async function panLeftToRightBegin(aElement, aX, aY, aMultiplier) { await NativePanHandler.promiseNativePanEvent( aElement, aX, aY, -NativePanHandler.delta * aMultiplier, 0, NativePanHandler.beginPhase ); } async function panLeftToRightUpdate(aElement, aX, aY, aMultiplier) { await NativePanHandler.promiseNativePanEvent( aElement, aX, aY, -NativePanHandler.delta * aMultiplier, 0, NativePanHandler.updatePhase ); await NativePanHandler.promiseNativePanEvent( aElement, aX, aY, -NativePanHandler.delta * aMultiplier, 0, NativePanHandler.updatePhase ); } async function panLeftToRightEnd(aElement, aX, aY, aMultiplier) { await NativePanHandler.promiseNativePanEvent( aElement, aX, aY, 0, 0, NativePanHandler.endPhase ); }