forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			2123 lines
		
	
	
	
		
			61 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			2123 lines
		
	
	
	
		
			61 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* This Source Code Form is subject to the terms of the Mozilla Public
 | |
|  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 | |
|  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 | |
| 
 | |
| /* eslint no-dupe-keys:off */
 | |
| /* eslint-disable no-restricted-globals */
 | |
| 
 | |
| const lazy = {};
 | |
| 
 | |
| ChromeUtils.defineESModuleGetters(lazy, {
 | |
|   AppInfo: "chrome://remote/content/shared/AppInfo.sys.mjs",
 | |
|   assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
 | |
|   dom: "chrome://remote/content/shared/DOM.sys.mjs",
 | |
|   error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
 | |
|   event: "chrome://remote/content/marionette/event.sys.mjs",
 | |
|   keyData: "chrome://remote/content/shared/webdriver/KeyData.sys.mjs",
 | |
|   Log: "chrome://remote/content/shared/Log.sys.mjs",
 | |
|   pprint: "chrome://remote/content/shared/Format.sys.mjs",
 | |
|   Sleep: "chrome://remote/content/marionette/sync.sys.mjs",
 | |
| });
 | |
| 
 | |
| ChromeUtils.defineLazyGetter(lazy, "logger", () =>
 | |
|   lazy.Log.get(lazy.Log.TYPES.MARIONETTE)
 | |
| );
 | |
| 
 | |
| // TODO? With ES 2016 and Symbol you can make a safer approximation
 | |
| // to an enum e.g. https://gist.github.com/xmlking/e86e4f15ec32b12c4689
 | |
| /**
 | |
|  * Implements WebDriver Actions API: a low-level interface for providing
 | |
|  * virtualised device input to the web browser.
 | |
|  *
 | |
|  * Typical usage is to construct an action chain and then dispatch it:
 | |
|  * const state = new action.State();
 | |
|  * const chain = new action.Chain.fromJSON(state, protocolData);
 | |
|  * await chain.dispatch(state, window);
 | |
|  *
 | |
|  * @namespace
 | |
|  */
 | |
| export const action = {};
 | |
| 
 | |
| /** Map from normalized key value to UI Events modifier key name */
 | |
| const MODIFIER_NAME_LOOKUP = {
 | |
|   Alt: "alt",
 | |
|   Shift: "shift",
 | |
|   Control: "ctrl",
 | |
|   Meta: "meta",
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * State associated with actions
 | |
|  *
 | |
|  * Typically each top-level browsing context in a session should have a single State object
 | |
|  */
 | |
| action.State = class {
 | |
|   constructor() {
 | |
|     /**
 | |
|      * A map between input ID and the device state for that input
 | |
|      * source, with one entry for each active input source.
 | |
|      *
 | |
|      * Maps string => InputSource
 | |
|      */
 | |
|     this.inputStateMap = new Map();
 | |
| 
 | |
|     /**
 | |
|      * List of {@link Action} associated with current session.  Used to
 | |
|      * manage dispatching events when resetting the state of the input sources.
 | |
|      * Reset operations are assumed to be idempotent.
 | |
|      */
 | |
|     this.inputsToCancel = new TickActions();
 | |
| 
 | |
|     /**
 | |
|      * Map between string input id and numeric pointer id
 | |
|      */
 | |
|     this.pointerIdMap = new Map();
 | |
|   }
 | |
| 
 | |
|   toString() {
 | |
|     return `[object ${this.constructor.name} ${JSON.stringify(this)}]`;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Reset state stored in this object.
 | |
|    * It is an error to use the State object after calling release().
 | |
|    *
 | |
|    * @param {WindowProxy} win Current window global.
 | |
|    */
 | |
|   async release(win) {
 | |
|     this.inputsToCancel.reverse();
 | |
|     await this.inputsToCancel.dispatch(this, win);
 | |
|     lazy.event.DoubleClickTracker.resetClick();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Get the state for a given input source.
 | |
|    *
 | |
|    * @param {string} id Input source id.
 | |
|    * @returns {InputSource} Input source state.
 | |
|    */
 | |
|   getInputSource(id) {
 | |
|     return this.inputStateMap.get(id);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Find or add state for an input source. The caller should verify
 | |
|    * that the returned state is the expected type.
 | |
|    *
 | |
|    * @param {string} id Input source id.
 | |
|    * @param {InputSource} newInputSource Input source state.
 | |
|    */
 | |
|   getOrAddInputSource(id, newInputSource) {
 | |
|     let inputSource = this.getInputSource(id);
 | |
|     if (inputSource === undefined) {
 | |
|       this.inputStateMap.set(id, newInputSource);
 | |
|       inputSource = newInputSource;
 | |
|     }
 | |
|     return inputSource;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Iterate over all input states of a given type
 | |
|    *
 | |
|    * @param {string} type Input source type name (e.g. "pointer").
 | |
|    * @returns {Iterator} Iterator over [id, input source].
 | |
|    */
 | |
|   *inputSourcesByType(type) {
 | |
|     for (const [id, inputSource] of this.inputStateMap) {
 | |
|       if (inputSource.type === type) {
 | |
|         yield [id, inputSource];
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Get a numerical pointer id for a given pointer
 | |
|    *
 | |
|    * Pointer ids are positive integers. Mouse pointers are typically
 | |
|    * ids 0 or 1. Non-mouse pointers never get assigned id < 2. Each
 | |
|    * pointer gets a unique id.
 | |
|    *
 | |
|    * @param {string} id Pointer id.
 | |
|    * @param {string} type Pointer type.
 | |
|    * @returns {number} Numerical pointer id.
 | |
|    */
 | |
|   getPointerId(id, type) {
 | |
|     let pointerId = this.pointerIdMap.get(id);
 | |
|     if (pointerId === undefined) {
 | |
|       // Reserve pointer ids 0 and 1 for mouse pointers
 | |
|       const idValues = Array.from(this.pointerIdMap.values());
 | |
|       if (type === "mouse") {
 | |
|         for (const mouseId of [0, 1]) {
 | |
|           if (!idValues.includes(mouseId)) {
 | |
|             pointerId = mouseId;
 | |
|             break;
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|       if (pointerId === undefined) {
 | |
|         pointerId = Math.max(1, ...idValues) + 1;
 | |
|       }
 | |
|       this.pointerIdMap.set(id, pointerId);
 | |
|     }
 | |
|     return pointerId;
 | |
|   }
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Device state for an input source.
 | |
|  */
 | |
| class InputSource {
 | |
|   #id;
 | |
|   static type = null;
 | |
| 
 | |
|   constructor(id) {
 | |
|     this.#id = id;
 | |
|     this.type = this.constructor.type;
 | |
|   }
 | |
| 
 | |
|   toString() {
 | |
|     return `[object ${this.constructor.name} id: ${this.#id} type: ${
 | |
|       this.type
 | |
|     }]`;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @param {State} state Actions state.
 | |
|    * @param {Sequence} actionSequence Actions for a specific input source.
 | |
|    *
 | |
|    * @returns {InputSource}
 | |
|    *     An {@link InputSource} object for the type of the
 | |
|    *     {@link actionSequence}.
 | |
|    *
 | |
|    * @throws {InvalidArgumentError}
 | |
|    *     If {@link actionSequence.type} is not valid.
 | |
|    */
 | |
|   static fromJSON(state, actionSequence) {
 | |
|     const { id, type } = actionSequence;
 | |
|     lazy.assert.string(
 | |
|       id,
 | |
|       lazy.pprint`Expected 'id' to be a string, got ${id}`
 | |
|     );
 | |
|     const cls = inputSourceTypes.get(type);
 | |
|     if (cls === undefined) {
 | |
|       throw new lazy.error.InvalidArgumentError(
 | |
|         lazy.pprint`Unknown action type: ${type}`
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     const sequenceInputSource = cls.fromJSON(state, actionSequence);
 | |
|     const inputSource = state.getOrAddInputSource(id, sequenceInputSource);
 | |
|     if (inputSource.type !== type) {
 | |
|       throw new lazy.error.InvalidArgumentError(
 | |
|         `Expected input source ${id} to be type ${inputSource.type}, ` +
 | |
|           `got ${type}`
 | |
|       );
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Input state not associated with a specific physical device.
 | |
|  */
 | |
| class NullInputSource extends InputSource {
 | |
|   static type = "none";
 | |
| 
 | |
|   static fromJSON(state, actionSequence) {
 | |
|     const { id } = actionSequence;
 | |
|     return new this(id);
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Input state associated with a keyboard-type device.
 | |
|  */
 | |
| class KeyInputSource extends InputSource {
 | |
|   static type = "key";
 | |
| 
 | |
|   constructor(id) {
 | |
|     super(id);
 | |
|     this.pressed = new Set();
 | |
|     this.alt = false;
 | |
|     this.shift = false;
 | |
|     this.ctrl = false;
 | |
|     this.meta = false;
 | |
|   }
 | |
| 
 | |
|   static fromJSON(state, actionSequence) {
 | |
|     const { id } = actionSequence;
 | |
|     return new this(id);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Update modifier state according to |key|.
 | |
|    *
 | |
|    * @param {string} key
 | |
|    *     Normalized key value of a modifier key.
 | |
|    * @param {boolean} value
 | |
|    *     Value to set the modifier attribute to.
 | |
|    *
 | |
|    * @throws {InvalidArgumentError}
 | |
|    *     If |key| is not a modifier.
 | |
|    */
 | |
|   setModState(key, value) {
 | |
|     if (key in MODIFIER_NAME_LOOKUP) {
 | |
|       this[MODIFIER_NAME_LOOKUP[key]] = value;
 | |
|     } else {
 | |
|       throw new lazy.error.InvalidArgumentError(
 | |
|         "Expected 'key' to be one of " +
 | |
|           Object.keys(MODIFIER_NAME_LOOKUP) +
 | |
|           lazy.pprint`, got ${key}`
 | |
|       );
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Check whether |key| is pressed.
 | |
|    *
 | |
|    * @param {string} key
 | |
|    *     Normalized key value.
 | |
|    *
 | |
|    * @returns {boolean}
 | |
|    *     True if |key| is in set of pressed keys.
 | |
|    */
 | |
|   isPressed(key) {
 | |
|     return this.pressed.has(key);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Add |key| to the set of pressed keys.
 | |
|    *
 | |
|    * @param {string} key
 | |
|    *     Normalized key value.
 | |
|    *
 | |
|    * @returns {boolean}
 | |
|    *     True if |key| is in list of pressed keys.
 | |
|    */
 | |
|   press(key) {
 | |
|     return this.pressed.add(key);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Remove |key| from the set of pressed keys.
 | |
|    *
 | |
|    * @param {string} key
 | |
|    *     Normalized key value.
 | |
|    *
 | |
|    * @returns {boolean}
 | |
|    *     True if |key| was present before removal, false otherwise.
 | |
|    */
 | |
|   release(key) {
 | |
|     return this.pressed.delete(key);
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Input state associated with a pointer-type device.
 | |
|  */
 | |
| class PointerInputSource extends InputSource {
 | |
|   static type = "pointer";
 | |
| 
 | |
|   /**
 | |
|    * @param {string} id InputSource id.
 | |
|    * @param {Pointer} pointer Object representing the specific pointer
 | |
|    * type associated with this input source.
 | |
|    */
 | |
|   constructor(id, pointer) {
 | |
|     super(id);
 | |
|     this.pointer = pointer;
 | |
|     this.x = 0;
 | |
|     this.y = 0;
 | |
|     this.pressed = new Set();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Check whether |button| is pressed.
 | |
|    *
 | |
|    * @param {number} button
 | |
|    *     Positive integer that refers to a mouse button.
 | |
|    *
 | |
|    * @returns {boolean}
 | |
|    *     True if |button| is in set of pressed buttons.
 | |
|    */
 | |
|   isPressed(button) {
 | |
|     lazy.assert.positiveInteger(button);
 | |
|     return this.pressed.has(button);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Add |button| to the set of pressed keys.
 | |
|    *
 | |
|    * @param {number} button
 | |
|    *     Positive integer that refers to a mouse button.
 | |
|    *
 | |
|    * @returns {Set}
 | |
|    *     Set of pressed buttons.
 | |
|    */
 | |
|   press(button) {
 | |
|     lazy.assert.positiveInteger(button);
 | |
|     this.pressed.add(button);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Remove |button| from the set of pressed buttons.
 | |
|    *
 | |
|    * @param {number} button
 | |
|    *     A positive integer that refers to a mouse button.
 | |
|    *
 | |
|    * @returns {boolean}
 | |
|    *     True if |button| was present before removals, false otherwise.
 | |
|    */
 | |
|   release(button) {
 | |
|     lazy.assert.positiveInteger(button);
 | |
|     return this.pressed.delete(button);
 | |
|   }
 | |
| 
 | |
|   static fromJSON(state, actionSequence) {
 | |
|     const { id, parameters } = actionSequence;
 | |
| 
 | |
|     const pointerType = parameters?.pointerType ?? "mouse";
 | |
|     const pointerId = state.getPointerId(id, pointerType);
 | |
|     const pointer = Pointer.fromJSON(pointerId, pointerType);
 | |
|     return new this(id, pointer);
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Input state associated with a wheel-type device.
 | |
|  */
 | |
| class WheelInputSource extends InputSource {
 | |
|   static type = "wheel";
 | |
| 
 | |
|   static fromJSON(state, actionSequence) {
 | |
|     const { id } = actionSequence;
 | |
|     return new this(id);
 | |
|   }
 | |
| }
 | |
| 
 | |
| const inputSourceTypes = new Map();
 | |
| for (const cls of [
 | |
|   NullInputSource,
 | |
|   KeyInputSource,
 | |
|   PointerInputSource,
 | |
|   WheelInputSource,
 | |
| ]) {
 | |
|   inputSourceTypes.set(cls.type, cls);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Representation of a coordinate origin
 | |
|  */
 | |
| class Origin {
 | |
|   /**
 | |
|    * Viewport coordinates of the origin of this coordinate system.
 | |
|    *
 | |
|    * This is overridden in subclasses to provide a class-specific origin.
 | |
|    *
 | |
|    * @param {InputSource} inputSource - State of current input device.
 | |
|    * @param {WindowProxy} win - Current window global
 | |
|    */
 | |
|   getOriginCoordinates(inputSource, win) {
 | |
|     throw new Error(
 | |
|       `originCoordinates not defined for ${this.constructor.name}`
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Convert [x, y] coordinates to viewport coordinates
 | |
|    *
 | |
|    * @param {InputSource} inputSource - State of the current input device
 | |
|    * @param {Array<number>} coords - [x, y] coordinate of target relative to origin
 | |
|    * @param {WindowProxy} win - Current window global
 | |
|    */
 | |
|   getTargetCoordinates(inputSource, coords, win) {
 | |
|     const [x, y] = coords;
 | |
|     const origin = this.getOriginCoordinates(inputSource, win);
 | |
|     return [origin.x + x, origin.y + y];
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @param {Element|string=} origin - Type of orgin, one of "viewport", "pointer", element or undefined.
 | |
|    *
 | |
|    * @returns {Origin} - An origin object representing the origin.
 | |
|    *
 | |
|    * @throws {InvalidArgumentError}
 | |
|    *     If <code>origin</code> isn't a valid origin.
 | |
|    */
 | |
|   static fromJSON(origin) {
 | |
|     if (origin === undefined || origin === "viewport") {
 | |
|       return new ViewportOrigin();
 | |
|     }
 | |
|     if (origin === "pointer") {
 | |
|       return new PointerOrigin();
 | |
|     }
 | |
|     if (lazy.dom.isElement(origin)) {
 | |
|       return new ElementOrigin(origin);
 | |
|     }
 | |
| 
 | |
|     throw new lazy.error.InvalidArgumentError(
 | |
|       `Expected 'origin' to be undefined, "viewport", "pointer", ` +
 | |
|         lazy.pprint`or an element, got: ${origin}`
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class ViewportOrigin extends Origin {
 | |
|   getOriginCoordinates(inputSource, win) {
 | |
|     return { x: 0, y: 0 };
 | |
|   }
 | |
| }
 | |
| 
 | |
| class PointerOrigin extends Origin {
 | |
|   getOriginCoordinates(inputSource, win) {
 | |
|     return { x: inputSource.x, y: inputSource.y };
 | |
|   }
 | |
| }
 | |
| 
 | |
| class ElementOrigin extends Origin {
 | |
|   /**
 | |
|    * @param {Element} element - The element providing the coordinate origin.
 | |
|    */
 | |
|   constructor(element) {
 | |
|     super();
 | |
|     this.element = element;
 | |
|   }
 | |
| 
 | |
|   getOriginCoordinates(inputSource, win) {
 | |
|     const clientRects = this.element.getClientRects();
 | |
|     // The spec doesn't handle this case; https://github.com/w3c/webdriver/issues/1642
 | |
|     if (!clientRects.length) {
 | |
|       throw new lazy.error.MoveTargetOutOfBoundsError(
 | |
|         `Origin element is not displayed`
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     return lazy.dom.getInViewCentrePoint(clientRects[0], win);
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Repesents the behaviour of a single input source at a single
 | |
|  * point in time.
 | |
|  *
 | |
|  * @param {string} id - Input source ID.
 | |
|  */
 | |
| class Action {
 | |
|   /** Type of the input source associated with this action */
 | |
|   static type = null;
 | |
|   /** Type of action specific to the input source */
 | |
|   static subtype = null;
 | |
|   /** Whether this kind of action affects the overall duration of a tick */
 | |
|   affectsWallClockTime = false;
 | |
| 
 | |
|   constructor(id) {
 | |
|     this.id = id;
 | |
|     this.type = this.constructor.type;
 | |
|     this.subtype = this.constructor.subtype;
 | |
|   }
 | |
| 
 | |
|   toString() {
 | |
|     return `[${this.constructor.name} ${this.type}:${this.subtype}]`;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Dispatch the action to the relevant window.
 | |
|    *
 | |
|    * This is overridden by subclasses to implement the type-specific
 | |
|    * dispatch of the action.
 | |
|    *
 | |
|    * @param {State} state - Actions state.
 | |
|    * @param {InputSource} inputSource - State of the current input device.
 | |
|    * @param {number} tickDuration - Length of the current tick, in ms.
 | |
|    * @param {WindowProxy} win - Current window global.
 | |
|    * @returns {Promise} - Promise that is resolved once the action is complete.
 | |
|    */
 | |
|   dispatch(state, inputSource, tickDuration, win) {
 | |
|     throw new Error(
 | |
|       `Action subclass ${this.constructor.name} must override dispatch()`
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @param {string} type - Input source type.
 | |
|    * @param {string} id - Input source id.
 | |
|    * @param {object} actionItem - Object representing a single action.
 | |
|    *
 | |
|    * @returns {Action} - An action that can be dispatched.
 | |
|    *
 | |
|    * @throws {InvalidArgumentError}
 | |
|    *     If any <code>actionSequence</code> or <code>actionItem</code>
 | |
|    *     attributes are invalid.
 | |
|    */
 | |
|   static fromJSON(type, id, actionItem) {
 | |
|     const subtype = actionItem.type;
 | |
|     const subtypeMap = actionTypes.get(type);
 | |
|     if (subtypeMap === undefined) {
 | |
|       throw new lazy.error.InvalidArgumentError(`Unknown action type: ${type}`);
 | |
|     }
 | |
|     let cls = subtypeMap.get(subtype);
 | |
|     // Non-device specific actions can happen for any action type
 | |
|     if (cls === undefined) {
 | |
|       cls = actionTypes.get("none").get(subtype);
 | |
|     }
 | |
|     if (cls === undefined) {
 | |
|       throw new lazy.error.InvalidArgumentError(
 | |
|         `Unknown subtype ${subtype} for type ${type}`
 | |
|       );
 | |
|     }
 | |
|     return cls.fromJSON(id, actionItem);
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Action not associated with a specific input device.
 | |
|  */
 | |
| class NullAction extends Action {
 | |
|   static type = "none";
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Action that waits for a given duration.
 | |
|  *
 | |
|  * @param {string} id - Input source ID.
 | |
|  * @param {object} options - Named arguments.
 | |
|  * @param {number} options.duration - Time to pause, in ms.
 | |
|  */
 | |
| class PauseAction extends NullAction {
 | |
|   static subtype = "pause";
 | |
|   affectsWallClockTime = true;
 | |
| 
 | |
|   constructor(id, options) {
 | |
|     super(id);
 | |
|     const { duration } = options;
 | |
|     this.duration = duration;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Dispatch pause action
 | |
|    *
 | |
|    * @param {State} state - Actions state.
 | |
|    * @param {InputSource} inputSource - State of the current input device.
 | |
|    * @param {number} tickDuration - Length of the current tick, in ms.
 | |
|    * @param {WindowProxy} win - Current window global.
 | |
|    * @returns {Promise} - Promise that is resolved once the action is complete.
 | |
|    */
 | |
|   dispatch(state, inputSource, tickDuration, win) {
 | |
|     const ms = this.duration ?? tickDuration;
 | |
|     lazy.logger.trace(
 | |
|       ` Dispatch ${this.constructor.name} with ${this.id} ${ms}`
 | |
|     );
 | |
|     return lazy.Sleep(ms);
 | |
|   }
 | |
| 
 | |
|   static fromJSON(id, actionItem) {
 | |
|     const { duration } = actionItem;
 | |
|     if (duration !== undefined) {
 | |
|       lazy.assert.positiveInteger(
 | |
|         duration,
 | |
|         lazy.pprint`Expected 'duration' (${duration}) to be >= 0`
 | |
|       );
 | |
|     }
 | |
|     return new this(id, { duration });
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Action associated with a keyboard input device
 | |
|  *
 | |
|  * @param {string} id - Input source ID.
 | |
|  * @param {object} options - Named arguments.
 | |
|  * @param {string} options.value - Key character.
 | |
|  */
 | |
| class KeyAction extends Action {
 | |
|   static type = "key";
 | |
| 
 | |
|   constructor(id, options) {
 | |
|     super(id);
 | |
|     const { value } = options;
 | |
|     this.value = value;
 | |
|   }
 | |
| 
 | |
|   getEventData(inputSource) {
 | |
|     let value = this.value;
 | |
|     if (inputSource.shift) {
 | |
|       value = lazy.keyData.getShiftedKey(value);
 | |
|     }
 | |
|     return new KeyEventData(value);
 | |
|   }
 | |
| 
 | |
|   static fromJSON(id, actionItem) {
 | |
|     // TODO countGraphemes
 | |
|     // TODO key.value could be a single code point like "\uE012"
 | |
|     // (see rawKey) or "grapheme cluster"
 | |
|     // https://bugzilla.mozilla.org/show_bug.cgi?id=1496323
 | |
|     const value = actionItem.value;
 | |
|     lazy.assert.string(
 | |
|       value,
 | |
|       "Expected 'value' to be a string that represents single code point " +
 | |
|         lazy.pprint`or grapheme cluster, got ${value}`
 | |
|     );
 | |
|     return new this(id, { value });
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Action equivalent to pressing a key on a keyboard.
 | |
|  *
 | |
|  * @param {string} id - Input source ID.
 | |
|  * @param {string} value - Key character.
 | |
|  */
 | |
| class KeyDownAction extends KeyAction {
 | |
|   static subtype = "keyDown";
 | |
| 
 | |
|   dispatch(state, inputSource, tickDuration, win) {
 | |
|     lazy.logger.trace(
 | |
|       `Dispatch ${this.constructor.name} with ${this.id} ${this.value}`
 | |
|     );
 | |
|     return new Promise(resolve => {
 | |
|       const keyEvent = this.getEventData(inputSource);
 | |
|       keyEvent.repeat = inputSource.isPressed(keyEvent.key);
 | |
|       inputSource.press(keyEvent.key);
 | |
|       if (keyEvent.key in MODIFIER_NAME_LOOKUP) {
 | |
|         inputSource.setModState(keyEvent.key, true);
 | |
|       }
 | |
| 
 | |
|       // Append a copy of |a| with keyUp subtype
 | |
|       state.inputsToCancel.push(new KeyUpAction(this.id, this));
 | |
|       keyEvent.update(state, inputSource);
 | |
|       lazy.event.sendKeyDown(keyEvent, win);
 | |
| 
 | |
|       resolve();
 | |
|     });
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Action equivalent to releasing a key on a keyboard.
 | |
|  *
 | |
|  * @param {string} id - Input source ID.
 | |
|  * @param {string} value - Key character.
 | |
|  */
 | |
| class KeyUpAction extends KeyAction {
 | |
|   static subtype = "keyUp";
 | |
| 
 | |
|   dispatch(state, inputSource, tickDuration, win) {
 | |
|     lazy.logger.trace(
 | |
|       `Dispatch ${this.constructor.name} with ${this.id} ${this.value}`
 | |
|     );
 | |
|     return new Promise(resolve => {
 | |
|       const keyEvent = this.getEventData(inputSource);
 | |
|       if (!inputSource.isPressed(keyEvent.key)) {
 | |
|         resolve();
 | |
|         return;
 | |
|       }
 | |
|       if (keyEvent.key in MODIFIER_NAME_LOOKUP) {
 | |
|         inputSource.setModState(keyEvent.key, false);
 | |
|       }
 | |
|       inputSource.release(keyEvent.key);
 | |
|       keyEvent.update(state, inputSource);
 | |
| 
 | |
|       lazy.event.sendKeyUp(keyEvent, win);
 | |
|       resolve();
 | |
|     });
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Action associated with a pointer input device
 | |
|  *
 | |
|  * @param {string} id - Input source ID.
 | |
|  * @param {object} options - Named arguments.
 | |
|  * @param {number=} options.width - Pointer width in pixels.
 | |
|  * @param {number=} options.height - Pointer height in pixels.
 | |
|  * @param {number=} options.pressure - Pointer pressure.
 | |
|  * @param {number=} options.tangentialPressure - Pointer tangential pressure.
 | |
|  * @param {number=} options.tiltX - Pointer X tilt angle.
 | |
|  * @param {number=} options.tiltX - Pointer Y tilt angle.
 | |
|  * @param {number=} options.twist - Pointer twist angle.
 | |
|  * @param {number=} options.altitudeAngle - Pointer altitude angle.
 | |
|  * @param {number=} options.azimuthAngle - Pointer azimuth angle.
 | |
|  */
 | |
| class PointerAction extends Action {
 | |
|   static type = "pointer";
 | |
| 
 | |
|   constructor(id, options) {
 | |
|     super(id);
 | |
|     const {
 | |
|       width,
 | |
|       height,
 | |
|       pressure,
 | |
|       tangentialPressure,
 | |
|       tiltX,
 | |
|       tiltY,
 | |
|       twist,
 | |
|       altitudeAngle,
 | |
|       azimuthAngle,
 | |
|     } = options;
 | |
|     this.width = width;
 | |
|     this.height = height;
 | |
|     this.pressure = pressure;
 | |
|     this.tangentialPressure = tangentialPressure;
 | |
|     this.tiltX = tiltX;
 | |
|     this.tiltY = tiltY;
 | |
|     this.twist = twist;
 | |
|     this.altitudeAngle = altitudeAngle;
 | |
|     this.azimuthAngle = azimuthAngle;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Validate properties common to all pointer types
 | |
|    *
 | |
|    * @param {object} actionItem - Object representing a single action.
 | |
|    */
 | |
|   static validateCommon(actionItem) {
 | |
|     const {
 | |
|       width,
 | |
|       height,
 | |
|       pressure,
 | |
|       tangentialPressure,
 | |
|       tiltX,
 | |
|       tiltY,
 | |
|       twist,
 | |
|       altitudeAngle,
 | |
|       azimuthAngle,
 | |
|     } = actionItem;
 | |
|     if (width !== undefined) {
 | |
|       lazy.assert.positiveInteger(
 | |
|         width,
 | |
|         lazy.pprint`Expected 'width' (${width}) to be >= 0`
 | |
|       );
 | |
|     }
 | |
|     if (height !== undefined) {
 | |
|       lazy.assert.positiveInteger(
 | |
|         height,
 | |
|         lazy.pprint`Expected 'height' (${height}) to be >= 0`
 | |
|       );
 | |
|     }
 | |
|     if (pressure !== undefined) {
 | |
|       lazy.assert.numberInRange(
 | |
|         pressure,
 | |
|         [0, 1],
 | |
|         lazy.pprint`Expected 'pressure' (${pressure}) to be in range 0 to 1`
 | |
|       );
 | |
|     }
 | |
|     if (tangentialPressure !== undefined) {
 | |
|       lazy.assert.numberInRange(
 | |
|         tangentialPressure,
 | |
|         [-1, 1],
 | |
|         lazy.pprint`Expected 'tangentialPressure' (${tangentialPressure}) to be in range -1 to 1`
 | |
|       );
 | |
|     }
 | |
|     if (tiltX !== undefined) {
 | |
|       lazy.assert.integerInRange(
 | |
|         tiltX,
 | |
|         [-90, 90],
 | |
|         lazy.pprint`Expected 'tiltX' (${tiltX}) to be in range -90 to 90`
 | |
|       );
 | |
|     }
 | |
|     if (tiltY !== undefined) {
 | |
|       lazy.assert.integerInRange(
 | |
|         tiltY,
 | |
|         [-90, 90],
 | |
|         lazy.pprint`Expected 'tiltY' (${tiltY}) to be in range -90 to 90`
 | |
|       );
 | |
|     }
 | |
|     if (twist !== undefined) {
 | |
|       lazy.assert.integerInRange(
 | |
|         twist,
 | |
|         [0, 359],
 | |
|         lazy.pprint`Expected 'twist' (${twist}) to be in range 0 to 359`
 | |
|       );
 | |
|     }
 | |
|     if (altitudeAngle !== undefined) {
 | |
|       lazy.assert.numberInRange(
 | |
|         altitudeAngle,
 | |
|         [0, Math.PI / 2],
 | |
|         lazy.pprint`Expected 'altitudeAngle' (${altitudeAngle}) to be in range 0 to ${
 | |
|           Math.PI / 2
 | |
|         }`
 | |
|       );
 | |
|     }
 | |
|     if (azimuthAngle !== undefined) {
 | |
|       lazy.assert.numberInRange(
 | |
|         azimuthAngle,
 | |
|         [0, 2 * Math.PI],
 | |
|         lazy.pprint`Expected 'azimuthAngle' (${azimuthAngle}) to be in range 0 to ${
 | |
|           2 * Math.PI
 | |
|         }`
 | |
|       );
 | |
|     }
 | |
|     return {
 | |
|       width,
 | |
|       height,
 | |
|       pressure,
 | |
|       tangentialPressure,
 | |
|       tiltX,
 | |
|       tiltY,
 | |
|       twist,
 | |
|       altitudeAngle,
 | |
|       azimuthAngle,
 | |
|     };
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Action associated with a pointer input device being depressed.
 | |
|  *
 | |
|  * @param {string} id - Input source ID.
 | |
|  * @param {object} options - Named arguments.
 | |
|  * @param {number} options.button - Button being pressed. For devices without buttons (e.g. touch), this should be 0.
 | |
|  * @param {number=} options.width - Pointer width in pixels.
 | |
|  * @param {number=} options.height - Pointer height in pixels.
 | |
|  * @param {number=} options.pressure - Pointer pressure.
 | |
|  * @param {number=} options.tangentialPressure - Pointer tangential pressure.
 | |
|  * @param {number=} options.tiltX - Pointer X tilt angle.
 | |
|  * @param {number=} options.tiltX - Pointer Y tilt angle.
 | |
|  * @param {number=} options.twist - Pointer twist angle.
 | |
|  * @param {number=} options.altitudeAngle - Pointer altitude angle.
 | |
|  * @param {number=} options.azimuthAngle - Pointer azimuth angle.
 | |
|  */
 | |
| class PointerDownAction extends PointerAction {
 | |
|   static subtype = "pointerDown";
 | |
| 
 | |
|   constructor(id, options) {
 | |
|     super(id, options);
 | |
|     const { button } = options;
 | |
|     this.button = button;
 | |
|   }
 | |
| 
 | |
|   dispatch(state, inputSource, tickDuration, win) {
 | |
|     lazy.logger.trace(
 | |
|       `Dispatch ${this.constructor.name} ${inputSource.pointer.type} with id: ${this.id} button: ${this.button}`
 | |
|     );
 | |
|     return new Promise(resolve => {
 | |
|       if (inputSource.isPressed(this.button)) {
 | |
|         resolve();
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       inputSource.press(this.button);
 | |
|       // Append a copy of |a| with pointerUp subtype
 | |
|       state.inputsToCancel.push(new PointerUpAction(this.id, this));
 | |
|       inputSource.pointer.pointerDown(state, inputSource, this, win);
 | |
|       resolve();
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   static fromJSON(id, actionItem) {
 | |
|     const props = PointerAction.validateCommon(actionItem);
 | |
|     const { button } = actionItem;
 | |
|     lazy.assert.positiveInteger(
 | |
|       button,
 | |
|       lazy.pprint`Expected 'button' (${button}) to be >= 0`
 | |
|     );
 | |
|     props.button = button;
 | |
|     return new this(id, props);
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Action associated with a pointer input device being released.
 | |
|  *
 | |
|  * @param {string} id - Input source ID.
 | |
|  * @param {object} options - Named arguments.
 | |
|  * @param {number} options.button - Button being released. For devices without buttons (e.g. touch), this should be 0.
 | |
|  * @param {number=} options.width - Pointer width in pixels.
 | |
|  * @param {number=} options.height - Pointer height in pixels.
 | |
|  * @param {number=} options.pressure - Pointer pressure.
 | |
|  * @param {number=} options.tangentialPressure - Pointer tangential pressure.
 | |
|  * @param {number=} options.tiltX - Pointer X tilt angle.
 | |
|  * @param {number=} options.tiltX - Pointer Y tilt angle.
 | |
|  * @param {number=} options.twist - Pointer twist angle.
 | |
|  * @param {number=} options.altitudeAngle - Pointer altitude angle.
 | |
|  * @param {number=} options.azimuthAngle - Pointer azimuth angle.
 | |
|  */
 | |
| class PointerUpAction extends PointerAction {
 | |
|   static subtype = "pointerUp";
 | |
| 
 | |
|   constructor(id, options) {
 | |
|     super(id, options);
 | |
|     const { button } = options;
 | |
|     this.button = button;
 | |
|   }
 | |
| 
 | |
|   dispatch(state, inputSource, tickDuration, win) {
 | |
|     lazy.logger.trace(
 | |
|       `Dispatch ${this.constructor.name} ${inputSource.pointer.type} with id: ${this.id} button: ${this.button}`
 | |
|     );
 | |
|     return new Promise(resolve => {
 | |
|       if (!inputSource.isPressed(this.button)) {
 | |
|         resolve();
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       inputSource.release(this.button);
 | |
|       inputSource.pointer.pointerUp(state, inputSource, this, win);
 | |
| 
 | |
|       resolve();
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   static fromJSON(id, actionItem) {
 | |
|     const props = PointerAction.validateCommon(actionItem);
 | |
|     const { button } = actionItem;
 | |
|     lazy.assert.positiveInteger(
 | |
|       button,
 | |
|       lazy.pprint`Expected 'button' (${button}) to be >= 0`
 | |
|     );
 | |
|     props.button = button;
 | |
|     return new this(id, props);
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Action associated with a pointer input device being moved.
 | |
|  *
 | |
|  * @param {string} id - Input source ID.
 | |
|  * @param {object} options - Named arguments.
 | |
|  * @param {number=} options.width - Pointer width in pixels.
 | |
|  * @param {number=} options.height - Pointer height in pixels.
 | |
|  * @param {number=} options.pressure - Pointer pressure.
 | |
|  * @param {number=} options.tangentialPressure - Pointer tangential pressure.
 | |
|  * @param {number=} options.tiltX - Pointer X tilt angle.
 | |
|  * @param {number=} options.tiltX - Pointer Y tilt angle.
 | |
|  * @param {number=} options.twist - Pointer twist angle.
 | |
|  * @param {number=} options.altitudeAngle - Pointer altitude angle.
 | |
|  * @param {number=} options.azimuthAngle - Pointer azimuth angle.
 | |
|  * @param {number=} options.duration - Duration of move in ms.
 | |
|  * @param {Origin} options.origin - Origin of target coordinates.
 | |
|  * @param {number} options.x - X value of target coordinates.
 | |
|  * @param {number} options.y - Y value of target coordinates.
 | |
|  */
 | |
| class PointerMoveAction extends PointerAction {
 | |
|   static subtype = "pointerMove";
 | |
|   affectsWallClockTime = true;
 | |
| 
 | |
|   constructor(id, options) {
 | |
|     super(id, options);
 | |
|     const { duration, origin, x, y } = options;
 | |
|     this.duration = duration;
 | |
|     this.origin = origin;
 | |
|     this.x = x;
 | |
|     this.y = y;
 | |
|   }
 | |
| 
 | |
|   dispatch(state, inputSource, tickDuration, win) {
 | |
|     lazy.logger.trace(
 | |
|       `Dispatch ${this.constructor.name} ${inputSource.pointer.type} with id: ${this.id} x: ${this.x} y: ${this.y}`
 | |
|     );
 | |
|     const target = this.origin.getTargetCoordinates(
 | |
|       inputSource,
 | |
|       [this.x, this.y],
 | |
|       win
 | |
|     );
 | |
| 
 | |
|     assertInViewPort(target, win);
 | |
| 
 | |
|     return moveOverTime(
 | |
|       [[inputSource.x, inputSource.y]],
 | |
|       [target],
 | |
|       this.duration ?? tickDuration,
 | |
|       target => this.performPointerMoveStep(state, inputSource, target, win)
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Perform one part of a pointer move corresponding to a specific emitted event.
 | |
|    *
 | |
|    * @param {State} state - Actions state.
 | |
|    * @param {InputSource} inputSource - State of the current input device.
 | |
|    * @param {Array<Array<number>>} targets - Array of [x, y] arrays
 | |
|    * specifying the viewport coordinates to move to.
 | |
|    * @param {WindowProxy} win - Current window global.
 | |
|    */
 | |
|   performPointerMoveStep(state, inputSource, targets, win) {
 | |
|     if (targets.length !== 1) {
 | |
|       throw new Error(
 | |
|         "PointerMoveAction.performPointerMoveStep requires a single target"
 | |
|       );
 | |
|     }
 | |
|     const target = targets[0];
 | |
|     lazy.logger.trace(
 | |
|       `PointerMoveAction.performPointerMoveStep ${JSON.stringify(target)}`
 | |
|     );
 | |
|     if (target[0] == inputSource.x && target[1] == inputSource.y) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     inputSource.pointer.pointerMove(
 | |
|       state,
 | |
|       inputSource,
 | |
|       this,
 | |
|       target[0],
 | |
|       target[1],
 | |
|       win
 | |
|     );
 | |
| 
 | |
|     inputSource.x = target[0];
 | |
|     inputSource.y = target[1];
 | |
|   }
 | |
| 
 | |
|   static fromJSON(id, actionItem) {
 | |
|     const props = PointerAction.validateCommon(actionItem);
 | |
|     const { duration, origin, x, y } = actionItem;
 | |
|     if (duration !== undefined) {
 | |
|       lazy.assert.positiveInteger(
 | |
|         duration,
 | |
|         lazy.pprint`Expected 'duration' (${duration}) to be >= 0`
 | |
|       );
 | |
|     }
 | |
|     const originObject = Origin.fromJSON(origin);
 | |
|     lazy.assert.integer(x, lazy.pprint`Expected 'x' (${x}) to be an Integer`);
 | |
|     lazy.assert.integer(y, lazy.pprint`Expected 'y' (${y}) to be an Integer`);
 | |
|     props.duration = duration;
 | |
|     props.origin = originObject;
 | |
|     props.x = x;
 | |
|     props.y = y;
 | |
|     return new this(id, props);
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Action associated with a wheel input device
 | |
|  *
 | |
|  */
 | |
| class WheelAction extends Action {
 | |
|   static type = "wheel";
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Action associated with scrolling a scroll wheel
 | |
|  *
 | |
|  * @param {number} duration - Duration of scroll in ms.
 | |
|  * @param {Origin} origin - Origin of target coordinates.
 | |
|  * @param {number} x - X value of scroll coordinates.
 | |
|  * @param {number} y - Y value of scroll coordinates.
 | |
|  * @param {number} deltaX - Number of CSS pixels to scroll in X direction.
 | |
|  * @param {number} deltaY - Number of CSS pixels to scroll in Y direction
 | |
|  */
 | |
| class WheelScrollAction extends WheelAction {
 | |
|   static subtype = "scroll";
 | |
|   affectsWallClockTime = true;
 | |
| 
 | |
|   constructor(id, { duration, origin, x, y, deltaX, deltaY }) {
 | |
|     super(id);
 | |
|     this.duration = duration;
 | |
|     this.origin = origin;
 | |
|     this.x = x;
 | |
|     this.y = y;
 | |
|     this.deltaX = deltaX;
 | |
|     this.deltaY = deltaY;
 | |
|   }
 | |
| 
 | |
|   static fromJSON(id, actionItem) {
 | |
|     const { duration, origin, x, y, deltaX, deltaY } = actionItem;
 | |
|     if (duration !== undefined) {
 | |
|       lazy.assert.positiveInteger(
 | |
|         duration,
 | |
|         lazy.pprint`Expected 'duration' (${duration}) to be >= 0`
 | |
|       );
 | |
|     }
 | |
|     const originObject = Origin.fromJSON(origin);
 | |
|     lazy.assert.integer(x, lazy.pprint`Expected 'x' (${x}) to be an Integer`);
 | |
|     lazy.assert.integer(y, lazy.pprint`Expected 'y' (${y}) to be an Integer`);
 | |
|     lazy.assert.integer(
 | |
|       deltaX,
 | |
|       lazy.pprint`Expected 'deltaX' (${deltaX}) to be an Integer`
 | |
|     );
 | |
|     lazy.assert.integer(
 | |
|       deltaY,
 | |
|       lazy.pprint`Expected 'deltaY' (${deltaY}) to be an Integer`
 | |
|     );
 | |
| 
 | |
|     return new this(id, {
 | |
|       duration,
 | |
|       origin: originObject,
 | |
|       x,
 | |
|       y,
 | |
|       deltaX,
 | |
|       deltaY,
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   async dispatch(state, inputSource, tickDuration, win) {
 | |
|     lazy.logger.trace(
 | |
|       `Dispatch ${this.constructor.name} with id: ${this.id} deltaX: ${this.deltaX} deltaY: ${this.deltaY}`
 | |
|     );
 | |
|     const scrollCoordinates = this.origin.getTargetCoordinates(
 | |
|       inputSource,
 | |
|       [this.x, this.y],
 | |
|       win
 | |
|     );
 | |
|     assertInViewPort(scrollCoordinates, win);
 | |
| 
 | |
|     const startX = 0;
 | |
|     const startY = 0;
 | |
|     // This is an action-local state that holds the amount of scroll completed
 | |
|     const deltaPosition = [startX, startY];
 | |
|     await moveOverTime(
 | |
|       [[startX, startY]],
 | |
|       [[this.deltaX, this.deltaY]],
 | |
|       this.duration ?? tickDuration,
 | |
|       deltaTarget =>
 | |
|         this.performOneWheelScroll(
 | |
|           scrollCoordinates,
 | |
|           deltaPosition,
 | |
|           deltaTarget,
 | |
|           win
 | |
|         )
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Perform one part of a wheel scroll corresponding to a specific emitted event.
 | |
|    *
 | |
|    * @param {Array<number>} scrollCoordinates - [x, y] viewport coordinates of the scroll.
 | |
|    * @param {Array<number>} deltaPosition - [deltaX, deltaY] coordinates of the scroll before this event.
 | |
|    * @param {Array<Array<number>>} deltaTargets - Array of [deltaX, deltaY] coordinates to scroll to.
 | |
|    * @param {WindowProxy} win - Current window global.
 | |
|    */
 | |
|   performOneWheelScroll(scrollCoordinates, deltaPosition, deltaTargets, win) {
 | |
|     if (deltaTargets.length !== 1) {
 | |
|       throw new Error("Can only scroll one wheel at a time");
 | |
|     }
 | |
|     if (deltaPosition[0] == this.deltaX && deltaPosition[1] == this.deltaY) {
 | |
|       return;
 | |
|     }
 | |
|     const deltaTarget = deltaTargets[0];
 | |
|     const deltaX = deltaTarget[0] - deltaPosition[0];
 | |
|     const deltaY = deltaTarget[1] - deltaPosition[1];
 | |
|     const eventData = new WheelEventData({
 | |
|       deltaX,
 | |
|       deltaY,
 | |
|       deltaZ: 0,
 | |
|     });
 | |
|     lazy.event.synthesizeWheelAtPoint(
 | |
|       scrollCoordinates[0],
 | |
|       scrollCoordinates[1],
 | |
|       eventData,
 | |
|       win
 | |
|     );
 | |
| 
 | |
|     // Update the current scroll position for the caller
 | |
|     deltaPosition[0] = deltaTarget[0];
 | |
|     deltaPosition[1] = deltaTarget[1];
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Group of actions representing behaviour of all touch pointers during a single tick.
 | |
|  *
 | |
|  * For touch pointers, we need to call into the platform once with all
 | |
|  * the actions so that they are regarded as simultaneous. This means
 | |
|  * we don't use the `dispatch()` method on the underlying actions, but
 | |
|  * instead use one on this group object.
 | |
|  */
 | |
| class TouchActionGroup {
 | |
|   static type = null;
 | |
| 
 | |
|   constructor() {
 | |
|     this.type = this.constructor.type;
 | |
|     this.actions = new Map();
 | |
|   }
 | |
| 
 | |
|   static forType(type) {
 | |
|     const cls = touchActionGroupTypes.get(type);
 | |
|     return new cls();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Add action corresponding to a specific pointer to the group.
 | |
|    *
 | |
|    * @param {InputSource} inputSource - State of the current input device.
 | |
|    * @param {Action} action - Action to add to the group
 | |
|    */
 | |
|   addPointer(inputSource, action) {
 | |
|     if (action.subtype !== this.type) {
 | |
|       throw new Error(
 | |
|         `Added action of unexpected type, got ${action.subtype}, expected ${this.type}`
 | |
|       );
 | |
|     }
 | |
|     this.actions.set(action.id, [inputSource, action]);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Dispatch the action group to the relevant window.
 | |
|    *
 | |
|    * This is overridden by subclasses to implement the type-specific
 | |
|    * dispatch of the action.
 | |
|    *
 | |
|    * @param {State} state - Actions state.
 | |
|    * @param {null} inputSource
 | |
|    *     This is always null; the argument only exists for compatibility
 | |
|    *     with {@link Action.dispatch}.
 | |
|    * @param {number} tickDuration - Length of the current tick, in ms.
 | |
|    * @param {WindowProxy} win - Current window global.
 | |
|    * @returns {Promise} - Promise that is resolved once the action is complete.
 | |
|    */
 | |
|   dispatch(state, inputSource, tickDuration, win) {
 | |
|     throw new Error(
 | |
|       "TouchActionGroup subclass missing dispatch implementation"
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Group of actions representing behaviour of all touch pointers
 | |
|  * depressed during a single tick.
 | |
|  */
 | |
| class PointerDownTouchActionGroup extends TouchActionGroup {
 | |
|   static type = "pointerDown";
 | |
| 
 | |
|   dispatch(state, inputSource, tickDuration, win) {
 | |
|     lazy.logger.trace(
 | |
|       `Dispatch ${this.constructor.name} with ${Array.from(
 | |
|         this.actions.values()
 | |
|       ).map(x => x[1].id)}`
 | |
|     );
 | |
|     return new Promise(resolve => {
 | |
|       if (inputSource !== null) {
 | |
|         throw new Error(
 | |
|           "Expected null inputSource for PointerDownTouchActionGroup.dispatch"
 | |
|         );
 | |
|       }
 | |
| 
 | |
|       // Only include pointers that are not already depressed
 | |
|       const actions = Array.from(this.actions.values()).filter(
 | |
|         ([actionInputSource, action]) =>
 | |
|           !actionInputSource.isPressed(action.button)
 | |
|       );
 | |
|       if (actions.length) {
 | |
|         const eventData = new MultiTouchEventData("touchstart");
 | |
|         for (const [actionInputSource, action] of actions) {
 | |
|           // Skip if already pressed
 | |
|           eventData.addPointerEventData(actionInputSource, action);
 | |
|           actionInputSource.press(action.button);
 | |
|           // Append a copy of |action| with pointerUp subtype
 | |
|           state.inputsToCancel.push(new PointerUpAction(action.id, action));
 | |
|           eventData.update(state, actionInputSource);
 | |
|         }
 | |
| 
 | |
|         // Touch start events must include all depressed touch pointers
 | |
|         for (const [id, pointerInputSource] of state.inputSourcesByType(
 | |
|           "pointer"
 | |
|         )) {
 | |
|           if (
 | |
|             pointerInputSource.pointer.type === "touch" &&
 | |
|             !this.actions.has(id) &&
 | |
|             pointerInputSource.isPressed(0)
 | |
|           ) {
 | |
|             eventData.addPointerEventData(pointerInputSource, {});
 | |
|             eventData.update(state, pointerInputSource);
 | |
|           }
 | |
|         }
 | |
|         lazy.event.synthesizeMultiTouch(eventData, win);
 | |
|       }
 | |
|       resolve();
 | |
|     });
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Group of actions representing behaviour of all touch pointers
 | |
|  * released during a single tick.
 | |
|  */
 | |
| class PointerUpTouchActionGroup extends TouchActionGroup {
 | |
|   static type = "pointerUp";
 | |
| 
 | |
|   dispatch(state, inputSource, tickDuration, win) {
 | |
|     lazy.logger.trace(
 | |
|       `Dispatch ${this.constructor.name} with ${Array.from(
 | |
|         this.actions.values()
 | |
|       ).map(x => x[1].id)}`
 | |
|     );
 | |
|     return new Promise(resolve => {
 | |
|       if (inputSource !== null) {
 | |
|         throw new Error(
 | |
|           "Expected null inputSource for PointerUpTouchActionGroup.dispatch"
 | |
|         );
 | |
|       }
 | |
| 
 | |
|       // Only include pointers that are not already depressed
 | |
|       const actions = Array.from(this.actions.values()).filter(
 | |
|         ([actionInputSource, action]) =>
 | |
|           actionInputSource.isPressed(action.button)
 | |
|       );
 | |
|       if (actions.length) {
 | |
|         const eventData = new MultiTouchEventData("touchend");
 | |
|         for (const [actionInputSource, action] of actions) {
 | |
|           eventData.addPointerEventData(actionInputSource, action);
 | |
|           actionInputSource.release(action.button);
 | |
|           eventData.update(state, actionInputSource);
 | |
|         }
 | |
|         lazy.event.synthesizeMultiTouch(eventData, win);
 | |
|       }
 | |
|       resolve();
 | |
|     });
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Group of actions representing behaviour of all touch pointers
 | |
|  * moved during a single tick.
 | |
|  */
 | |
| class PointerMoveTouchActionGroup extends TouchActionGroup {
 | |
|   static type = "pointerMove";
 | |
| 
 | |
|   dispatch(state, inputSource, tickDuration, win) {
 | |
|     lazy.logger.trace(
 | |
|       `Dispatch ${this.constructor.name} with ${Array.from(this.actions).map(
 | |
|         x => x[1].id
 | |
|       )}`
 | |
|     );
 | |
|     if (inputSource !== null) {
 | |
|       throw new Error(
 | |
|         "Expected null inputSource for PointerMoveTouchActionGroup.dispatch"
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     let startCoords = [];
 | |
|     let targetCoords = [];
 | |
|     for (const [actionInputSource, action] of this.actions.values()) {
 | |
|       const target = action.origin.getTargetCoordinates(
 | |
|         actionInputSource,
 | |
|         [action.x, action.y],
 | |
|         win
 | |
|       );
 | |
|       assertInViewPort(target, win);
 | |
|       startCoords.push([actionInputSource.x, actionInputSource.y]);
 | |
|       targetCoords.push(target);
 | |
|     }
 | |
|     // Touch move events must include all depressed touch pointers, even if they are static
 | |
|     // This can end up generating pointermove events even for static pointers, but Gecko
 | |
|     // seems to generate a lot of pointermove events anyway, so this seems like the lesser
 | |
|     // problem.
 | |
|     // See https://bugzilla.mozilla.org/show_bug.cgi?id=1779206
 | |
|     const staticTouchPointers = [];
 | |
|     for (const [id, pointerInputSource] of state.inputSourcesByType(
 | |
|       "pointer"
 | |
|     )) {
 | |
|       if (
 | |
|         pointerInputSource.pointer.type === "touch" &&
 | |
|         !this.actions.has(id) &&
 | |
|         pointerInputSource.isPressed(0)
 | |
|       ) {
 | |
|         staticTouchPointers.push(pointerInputSource);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return moveOverTime(
 | |
|       startCoords,
 | |
|       targetCoords,
 | |
|       this.duration ?? tickDuration,
 | |
|       currentTargetCoords =>
 | |
|         this.performPointerMoveStep(
 | |
|           state,
 | |
|           staticTouchPointers,
 | |
|           currentTargetCoords,
 | |
|           win
 | |
|         )
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Perform one part of a pointer move corresponding to a specific emitted event.
 | |
|    *
 | |
|    * @param {State} state - Actions state.
 | |
|    * @param {Array.<PointerInputSource>} staticTouchPointers
 | |
|    *      Array of PointerInputSource objects for pointers that aren't involved in
 | |
|    *      the touch move.
 | |
|    * @param {Array.<Array>} targetCoords
 | |
|    *      Array of [x, y] arrays specifying the viewport coordinates to move to.
 | |
|    * @param {WindowProxy} win - Current window global.
 | |
|    */
 | |
|   performPointerMoveStep(state, staticTouchPointers, targetCoords, win) {
 | |
|     if (targetCoords.length !== this.actions.size) {
 | |
|       throw new Error("Expected one target per pointer");
 | |
|     }
 | |
| 
 | |
|     const perPointerData = Array.from(this.actions.values()).map(
 | |
|       ([inputSource, action], i) => {
 | |
|         const target = targetCoords[i];
 | |
|         return [inputSource, action, target];
 | |
|       }
 | |
|     );
 | |
|     const reachedTarget = perPointerData.every(
 | |
|       ([inputSource, action, target]) =>
 | |
|         target[0] === inputSource.x && target[1] === inputSource.y
 | |
|     );
 | |
|     if (reachedTarget) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const eventData = new MultiTouchEventData("touchmove");
 | |
|     for (const [inputSource, action, target] of perPointerData) {
 | |
|       inputSource.x = target[0];
 | |
|       inputSource.y = target[1];
 | |
|       eventData.addPointerEventData(inputSource, action);
 | |
|       eventData.update(state, inputSource);
 | |
|     }
 | |
|     for (const inputSource of staticTouchPointers) {
 | |
|       eventData.addPointerEventData(inputSource, {});
 | |
|       eventData.update(state, inputSource);
 | |
|     }
 | |
|     lazy.event.synthesizeMultiTouch(eventData, win);
 | |
|   }
 | |
| }
 | |
| 
 | |
| const touchActionGroupTypes = new Map();
 | |
| for (const cls of [
 | |
|   PointerDownTouchActionGroup,
 | |
|   PointerUpTouchActionGroup,
 | |
|   PointerMoveTouchActionGroup,
 | |
| ]) {
 | |
|   touchActionGroupTypes.set(cls.type, cls);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Split a transition from startCoord to targetCoord linearly over duration.
 | |
|  *
 | |
|  * startCoords and targetCoords are lists of [x,y] positions in some space
 | |
|  * (e.g. screen position or scroll delta). This function will linearly
 | |
|  * interpolate intermediate positions, sending out roughly one event
 | |
|  * per frame to simulate moving between startCoord and targetCoord in
 | |
|  * a time of tickDuration milliseconds. The callback function is
 | |
|  * responsible for actually emitting the event, given the current
 | |
|  * position in the coordinate space.
 | |
|  *
 | |
|  * @param {Array.<Array>} startCoords
 | |
|  *    Array of initial [x, y] coordinates for each input source involved
 | |
|  *    in the move.
 | |
|  * @param {Array.<Array>} targetCoords
 | |
|  *    Array of target [x, y] coordinates for each input source involved
 | |
|  *    in the move.
 | |
|  * @param {number} duration - Time in ms the move will take.
 | |
|  * @param {Function} callback
 | |
|  *    Function that actually performs the move. This takes a single parameter
 | |
|  *    which is an array of [x, y] coordinates corresponding to the move
 | |
|  *    targets.
 | |
|  */
 | |
| async function moveOverTime(startCoords, targetCoords, duration, callback) {
 | |
|   lazy.logger.trace(
 | |
|     `moveOverTime start: ${startCoords} target: ${targetCoords} duration: ${duration}`
 | |
|   );
 | |
| 
 | |
|   if (startCoords.length !== targetCoords.length) {
 | |
|     throw new Error(
 | |
|       "Expected equal number of start coordinates and target coordinates"
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   if (
 | |
|     !startCoords.every(item => item.length == 2) ||
 | |
|     !targetCoords.every(item => item.length == 2)
 | |
|   ) {
 | |
|     throw new Error(
 | |
|       "Expected start coordinates target coordinates to be Array of multiple [x,y] coordinates."
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   if (duration === 0) {
 | |
|     // transition to destination in one step
 | |
|     callback(targetCoords);
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
 | |
|   // interval between transitions in ms, based on common vsync
 | |
|   const fps60 = 17;
 | |
| 
 | |
|   const distances = targetCoords.map((targetCoord, i) => {
 | |
|     const startCoord = startCoords[i];
 | |
|     return [targetCoord[0] - startCoord[0], targetCoord[1] - startCoord[1]];
 | |
|   });
 | |
|   const ONE_SHOT = Ci.nsITimer.TYPE_ONE_SHOT;
 | |
|   const startTime = Date.now();
 | |
|   const transitions = (async () => {
 | |
|     // wait |fps60| ms before performing first incremental transition
 | |
|     await new Promise(resolveTimer =>
 | |
|       timer.initWithCallback(resolveTimer, fps60, ONE_SHOT)
 | |
|     );
 | |
| 
 | |
|     let durationRatio = Math.floor(Date.now() - startTime) / duration;
 | |
|     const epsilon = fps60 / duration / 10;
 | |
|     while (1 - durationRatio > epsilon) {
 | |
|       const intermediateTargets = startCoords.map((startCoord, i) => {
 | |
|         let distance = distances[i];
 | |
|         return [
 | |
|           Math.floor(durationRatio * distance[0] + startCoord[0]),
 | |
|           Math.floor(durationRatio * distance[1] + startCoord[1]),
 | |
|         ];
 | |
|       });
 | |
|       callback(intermediateTargets);
 | |
|       // wait |fps60| ms before performing next transition
 | |
|       await new Promise(resolveTimer =>
 | |
|         timer.initWithCallback(resolveTimer, fps60, ONE_SHOT)
 | |
|       );
 | |
| 
 | |
|       durationRatio = Math.floor(Date.now() - startTime) / duration;
 | |
|     }
 | |
|   })();
 | |
| 
 | |
|   await transitions;
 | |
| 
 | |
|   // perform last transitionafter all incremental moves are resolved and
 | |
|   // durationRatio is close enough to 1
 | |
|   callback(targetCoords);
 | |
| }
 | |
| 
 | |
| const actionTypes = new Map();
 | |
| for (const cls of [
 | |
|   KeyDownAction,
 | |
|   KeyUpAction,
 | |
|   PauseAction,
 | |
|   PointerDownAction,
 | |
|   PointerUpAction,
 | |
|   PointerMoveAction,
 | |
|   WheelScrollAction,
 | |
| ]) {
 | |
|   if (!actionTypes.has(cls.type)) {
 | |
|     actionTypes.set(cls.type, new Map());
 | |
|   }
 | |
|   actionTypes.get(cls.type).set(cls.subtype, cls);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Implementation of the behaviour of a specific type of pointer
 | |
|  */
 | |
| class Pointer {
 | |
|   /** Type of pointer */
 | |
|   static type = null;
 | |
| 
 | |
|   constructor(id) {
 | |
|     this.id = id;
 | |
|     this.type = this.constructor.type;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Implementation of depressing the pointer.
 | |
|    *
 | |
|    * @param {State} state - Actions state.
 | |
|    * @param {InputSource} inputSource - State of the current input device.
 | |
|    * @param {Action} action - The Action object invoking the pointer
 | |
|    * @param {WindowProxy} win - Current window global.
 | |
|    */
 | |
|   pointerDown(state, inputSource, action, win) {
 | |
|     throw new Error(`Unimplemented pointerDown for pointerType ${this.type}`);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Implementation of releasing the pointer.
 | |
|    *
 | |
|    * @param {State} state - Actions state.
 | |
|    * @param {InputSource} inputSource - State of the current input device.
 | |
|    * @param {Action} action - The Action object invoking the pointer
 | |
|    * @param {WindowProxy} win - Current window global.
 | |
|    */
 | |
|   pointerUp(state, inputSource, action, win) {
 | |
|     throw new Error(`Unimplemented pointerUp for pointerType ${this.type}`);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Implementation of moving the pointer.
 | |
|    *
 | |
|    * @param {State} state - Actions state.
 | |
|    * @param {InputSource} inputSource - State of the current input device.
 | |
|    * @param {number} targetX - Target X coordinate of the pointer move
 | |
|    * @param {number} targetY - Target Y coordinate of the pointer move
 | |
|    * @param {WindowProxy} win - Current window global.
 | |
|    */
 | |
|   pointerMove(state, inputSource, targetX, targetY, win) {
 | |
|     throw new Error(`Unimplemented pointerMove for pointerType ${this.type}`);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @param {number} pointerId - Numeric pointer id.
 | |
|    * @param {string} pointerType - Pointer type.
 | |
|    * @returns {Pointer} - The pointer class for {@link pointerType}
 | |
|    *
 | |
|    * @throws {InvalidArgumentError} - If {@link pointerType} is not a valid pointer type.
 | |
|    */
 | |
|   static fromJSON(pointerId, pointerType) {
 | |
|     const cls = pointerTypes.get(pointerType);
 | |
|     if (cls === undefined) {
 | |
|       throw new lazy.error.InvalidArgumentError(
 | |
|         `Unknown pointerType: ${pointerType}`
 | |
|       );
 | |
|     }
 | |
|     return new cls(pointerId);
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Implementation of mouse pointer behaviour
 | |
|  */
 | |
| class MousePointer extends Pointer {
 | |
|   static type = "mouse";
 | |
| 
 | |
|   pointerDown(state, inputSource, action, win) {
 | |
|     const mouseEvent = new MouseEventData("mousedown", {
 | |
|       button: action.button,
 | |
|     });
 | |
|     mouseEvent.update(state, inputSource);
 | |
|     if (mouseEvent.ctrlKey) {
 | |
|       if (lazy.AppInfo.isMac) {
 | |
|         mouseEvent.button = 2;
 | |
|         lazy.event.DoubleClickTracker.resetClick();
 | |
|       }
 | |
|     } else if (lazy.event.DoubleClickTracker.isClicked()) {
 | |
|       mouseEvent.clickCount = 2;
 | |
|     }
 | |
|     lazy.event.synthesizeMouseAtPoint(
 | |
|       inputSource.x,
 | |
|       inputSource.y,
 | |
|       mouseEvent,
 | |
|       win
 | |
|     );
 | |
|     if (
 | |
|       lazy.event.MouseButton.isSecondary(mouseEvent.button) ||
 | |
|       (mouseEvent.ctrlKey && lazy.AppInfo.isMac)
 | |
|     ) {
 | |
|       const contextMenuEvent = { ...mouseEvent, type: "contextmenu" };
 | |
|       lazy.event.synthesizeMouseAtPoint(
 | |
|         inputSource.x,
 | |
|         inputSource.y,
 | |
|         contextMenuEvent,
 | |
|         win
 | |
|       );
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   pointerUp(state, inputSource, action, win) {
 | |
|     const mouseEvent = new MouseEventData("mouseup", {
 | |
|       button: action.button,
 | |
|     });
 | |
|     mouseEvent.update(state, inputSource);
 | |
|     if (lazy.event.DoubleClickTracker.isClicked()) {
 | |
|       mouseEvent.clickCount = 2;
 | |
|     }
 | |
|     lazy.event.synthesizeMouseAtPoint(
 | |
|       inputSource.x,
 | |
|       inputSource.y,
 | |
|       mouseEvent,
 | |
|       win
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   pointerMove(state, inputSource, action, targetX, targetY, win) {
 | |
|     const mouseEvent = new MouseEventData("mousemove");
 | |
|     mouseEvent.update(state, inputSource);
 | |
|     lazy.event.synthesizeMouseAtPoint(targetX, targetY, mouseEvent, win);
 | |
|   }
 | |
| }
 | |
| 
 | |
| /*
 | |
|  * The implementation here is empty because touch actions have to go via the
 | |
|  * TouchActionGroup. So if we end up calling these methods that's a bug in
 | |
|  * the code.
 | |
|  */
 | |
| class TouchPointer extends Pointer {
 | |
|   static type = "touch";
 | |
| }
 | |
| 
 | |
| /*
 | |
|  * Placeholder for future pen type pointer support.
 | |
|  */
 | |
| class PenPointer extends Pointer {
 | |
|   static type = "pen";
 | |
| }
 | |
| 
 | |
| const pointerTypes = new Map();
 | |
| for (const cls of [MousePointer, TouchPointer, PenPointer]) {
 | |
|   pointerTypes.set(cls.type, cls);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Represents a series of ticks, specifying which actions to perform at
 | |
|  * each tick.
 | |
|  */
 | |
| action.Chain = class extends Array {
 | |
|   toString() {
 | |
|     return `[chain ${super.toString()}]`;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Dispatch the action chain to the relevant window.
 | |
|    *
 | |
|    * @param {State} state - Actions state.
 | |
|    * @param {WindowProxy} win - Current window global.
 | |
|    * @returns {Promise} - Promise that is resolved once the action
 | |
|    * chain is complete.
 | |
|    */
 | |
|   dispatch(state, win) {
 | |
|     let i = 1;
 | |
|     const chainEvents = (async () => {
 | |
|       for (const tickActions of this) {
 | |
|         lazy.logger.trace(`Dispatching tick ${i++}/${this.length}`);
 | |
|         await tickActions.dispatch(state, win);
 | |
|       }
 | |
|     })();
 | |
|     return chainEvents;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @param {State} state - Actions state.
 | |
|    * @param {Array.<object>} actions - Array of objects that each
 | |
|    * represent an action sequence.
 | |
|    * @returns {action.Chain} - Object that allows dispatching a chain
 | |
|    * of actions.
 | |
|    * @throws {InvalidArgumentError} - If actions doesn't correspond to
 | |
|    * a valid action chain.
 | |
|    */
 | |
|   static fromJSON(state, actions) {
 | |
|     lazy.assert.array(
 | |
|       actions,
 | |
|       lazy.pprint`Expected 'actions' to be an array, got ${actions}`
 | |
|     );
 | |
| 
 | |
|     const actionsByTick = new this();
 | |
|     for (const actionSequence of actions) {
 | |
|       const inputSourceActions = Sequence.fromJSON(state, actionSequence);
 | |
|       for (let i = 0; i < inputSourceActions.length; i++) {
 | |
|         // new tick
 | |
|         if (actionsByTick.length < i + 1) {
 | |
|           actionsByTick.push(new TickActions());
 | |
|         }
 | |
|         actionsByTick[i].push(inputSourceActions[i]);
 | |
|       }
 | |
|     }
 | |
|     return actionsByTick;
 | |
|   }
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Represents the action for each input device to perform in a single tick.
 | |
|  */
 | |
| class TickActions extends Array {
 | |
|   /**
 | |
|    * Tick duration in milliseconds.
 | |
|    *
 | |
|    * @returns {number} - Longest action duration in |tickActions| if any, or 0.
 | |
|    */
 | |
|   getDuration() {
 | |
|     let max = 0;
 | |
|     for (const action of this) {
 | |
|       if (action.affectsWallClockTime && action.duration) {
 | |
|         max = Math.max(action.duration, max);
 | |
|       }
 | |
|     }
 | |
|     return max;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Dispatch sequence of actions for this tick.
 | |
|    *
 | |
|    * This creates a Promise for one tick that resolves once the Promise
 | |
|    * for each tick-action is resolved, which takes at least |tickDuration|
 | |
|    * milliseconds.  The resolved set of events for each tick is followed by
 | |
|    * firing of pending DOM events.
 | |
|    *
 | |
|    * Note that the tick-actions are dispatched in order, but they may have
 | |
|    * different durations and therefore may not end in the same order.
 | |
|    *
 | |
|    * @param {State} state - Actions state.
 | |
|    * @param {WindowProxy} win - Current window global.
 | |
|    *
 | |
|    * @returns {Promise} - Promise that resolves when tick is complete.
 | |
|    */
 | |
|   dispatch(state, win) {
 | |
|     const tickDuration = this.getDuration();
 | |
|     const tickActions = this.groupTickActions(state);
 | |
|     const pendingEvents = tickActions.map(([inputSource, action]) =>
 | |
|       action.dispatch(state, inputSource, tickDuration, win)
 | |
|     );
 | |
|     return Promise.all(pendingEvents);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Group together actions from input sources that have to be
 | |
|    * dispatched together.
 | |
|    *
 | |
|    * The actual transformation here is to group together touch pointer
 | |
|    * actions into {@link TouchActionGroup} instances.
 | |
|    *
 | |
|    * @param {State} state - Actions state.
 | |
|    * @returns {Array.<Array.<InputSource?,Action|TouchActionGroup>>}
 | |
|    *    Array of pairs. For ungrouped actions each element is
 | |
|    *    [InputSource, Action] For touch actions there are multiple
 | |
|    *    pointers handled at once, so the first item of the array is
 | |
|    *    null, meaning the group has to perform its own handling of the
 | |
|    *    relevant state, and the second element is a TouuchActionGroup.
 | |
|    */
 | |
|   groupTickActions(state) {
 | |
|     const touchActions = new Map();
 | |
|     const actions = [];
 | |
|     for (const action of this) {
 | |
|       const inputSource = state.getInputSource(action.id);
 | |
|       if (action.type == "pointer" && inputSource.pointer.type === "touch") {
 | |
|         lazy.logger.debug(
 | |
|           `Grouping action ${action.type} ${action.id} ${action.subtype}`
 | |
|         );
 | |
|         let group = touchActions.get(action.subtype);
 | |
|         if (group === undefined) {
 | |
|           group = TouchActionGroup.forType(action.subtype);
 | |
|           touchActions.set(action.subtype, group);
 | |
|           actions.push([null, group]);
 | |
|         }
 | |
|         group.addPointer(inputSource, action);
 | |
|       } else {
 | |
|         actions.push([inputSource, action]);
 | |
|       }
 | |
|     }
 | |
|     return actions;
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Represents one input source action sequence; this is essentially an
 | |
|  * |Array.<Action>|.
 | |
|  *
 | |
|  * This is a temporary object only used when constructing an {@link
 | |
|  * action.Chain}.
 | |
|  */
 | |
| class Sequence extends Array {
 | |
|   toString() {
 | |
|     return `[sequence ${super.toString()}]`;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @param {State} state - Actions state.
 | |
|    * @param {object} actionSequence
 | |
|    *     Protocol representation of the actions for a specific input source.
 | |
|    * @returns {Array.<Array>} - Array of [InputSource?,Action|TouchActionGroup]
 | |
|    */
 | |
|   static fromJSON(state, actionSequence) {
 | |
|     // used here to validate 'type' in addition to InputSource type below
 | |
|     const { id, type, actions } = actionSequence;
 | |
| 
 | |
|     // type and id get validated in InputSource.fromJSON
 | |
|     lazy.assert.array(
 | |
|       actions,
 | |
|       "Expected 'actionSequence.actions' to be an array, " +
 | |
|         lazy.pprint`got ${actionSequence.actions}`
 | |
|     );
 | |
| 
 | |
|     // This sets the input state in the global state map, if it's new
 | |
|     InputSource.fromJSON(state, actionSequence);
 | |
| 
 | |
|     const sequence = new this();
 | |
|     for (const actionItem of actions) {
 | |
|       sequence.push(Action.fromJSON(type, id, actionItem));
 | |
|     }
 | |
| 
 | |
|     return sequence;
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Representation of an input event
 | |
|  */
 | |
| class InputEventData {
 | |
|   constructor() {
 | |
|     this.altKey = false;
 | |
|     this.shiftKey = false;
 | |
|     this.ctrlKey = false;
 | |
|     this.metaKey = false;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Update the input data based on global and input state
 | |
|    *
 | |
|    * @param {State} state - Actions state.
 | |
|    * @param {InputSource} inputSource - State of the current input device.
 | |
|    */
 | |
|   update(state, inputSource) {}
 | |
| 
 | |
|   toString() {
 | |
|     return `${this.constructor.name} ${JSON.stringify(this)}`;
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Representation of a key input event
 | |
|  *
 | |
|  * @param {string} rawKey - Key value.
 | |
|  */
 | |
| class KeyEventData extends InputEventData {
 | |
|   constructor(rawKey) {
 | |
|     super();
 | |
|     const { key, code, location, printable } = lazy.keyData.getData(rawKey);
 | |
|     this.key = key;
 | |
|     this.code = code;
 | |
|     this.location = location;
 | |
|     this.printable = printable;
 | |
|     this.repeat = false;
 | |
|     // keyCode will be computed by event.sendKeyDown
 | |
|   }
 | |
| 
 | |
|   update(state, inputSource) {
 | |
|     this.altKey = inputSource.alt;
 | |
|     this.shiftKey = inputSource.shift;
 | |
|     this.ctrlKey = inputSource.ctrl;
 | |
|     this.metaKey = inputSource.meta;
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Representation of a pointer input event
 | |
|  *
 | |
|  * @param {string} type - Event type.
 | |
|  */
 | |
| class PointerEventData extends InputEventData {
 | |
|   constructor(type) {
 | |
|     super();
 | |
|     this.type = type;
 | |
|     this.buttons = 0;
 | |
|   }
 | |
| 
 | |
|   update(state, inputSource) {
 | |
|     // set modifier properties based on whether any corresponding keys are
 | |
|     // pressed on any key input source
 | |
|     for (const [, otherInputSource] of state.inputSourcesByType("key")) {
 | |
|       this.altKey = otherInputSource.alt || this.altKey;
 | |
|       this.ctrlKey = otherInputSource.ctrl || this.ctrlKey;
 | |
|       this.metaKey = otherInputSource.meta || this.metaKey;
 | |
|       this.shiftKey = otherInputSource.shift || this.shiftKey;
 | |
|     }
 | |
|     const allButtons = Array.from(inputSource.pressed);
 | |
|     this.buttons = allButtons.reduce((a, i) => a + Math.pow(2, i), 0);
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Representation of a mouse input event
 | |
|  *
 | |
|  * @param {string} type - Event type.
 | |
|  * @param {number} button - Mouse button number.
 | |
|  */
 | |
| class MouseEventData extends PointerEventData {
 | |
|   constructor(type, options = {}) {
 | |
|     super(type);
 | |
|     const { button = 0 } = options;
 | |
|     lazy.assert.positiveInteger(button);
 | |
|     this.button = button;
 | |
|     this.buttons = 0;
 | |
|   }
 | |
| 
 | |
|   update(state, inputSource) {
 | |
|     super.update(state, inputSource);
 | |
|     this.id = inputSource.pointer.id;
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Representation of a wheel scroll event
 | |
|  *
 | |
|  * @param {object} options - Named arguments.
 | |
|  * @param {number} options.deltaX - Scroll delta X.
 | |
|  * @param {number} options.deltaY - Scroll delta Y.
 | |
|  * @param {number} options.deltaY - Scroll delta Z (current always 0).
 | |
|  * @param {number=} deltaMode - Scroll delta mode (current always 0).
 | |
|  */
 | |
| class WheelEventData extends InputEventData {
 | |
|   constructor(options) {
 | |
|     super();
 | |
|     const { deltaX, deltaY, deltaZ, deltaMode = 0 } = options;
 | |
|     this.deltaX = deltaX;
 | |
|     this.deltaY = deltaY;
 | |
|     this.deltaZ = deltaZ;
 | |
|     this.deltaMode = deltaMode;
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Representation of a multitouch event
 | |
|  *
 | |
|  * @param {string} type - Event type.
 | |
|  */
 | |
| class MultiTouchEventData extends PointerEventData {
 | |
|   #setGlobalState;
 | |
| 
 | |
|   constructor(type) {
 | |
|     super(type);
 | |
|     this.id = [];
 | |
|     this.x = [];
 | |
|     this.y = [];
 | |
|     this.rx = [];
 | |
|     this.ry = [];
 | |
|     this.angle = [];
 | |
|     this.force = [];
 | |
|     this.tiltx = [];
 | |
|     this.tilty = [];
 | |
|     this.twist = [];
 | |
|     this.#setGlobalState = false;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Add the data from one pointer to the event.
 | |
|    *
 | |
|    * @param {InputSource} inputSource - State of the pointer.
 | |
|    * @param {PointerAction} action - Action for the pointer.
 | |
|    */
 | |
|   addPointerEventData(inputSource, action) {
 | |
|     this.x.push(inputSource.x);
 | |
|     this.y.push(inputSource.y);
 | |
|     this.id.push(inputSource.pointer.id);
 | |
|     this.rx.push(action.width || 1);
 | |
|     this.ry.push(action.height || 1);
 | |
|     this.angle.push(0);
 | |
|     this.force.push(action.pressure || (this.type === "touchend" ? 0 : 1));
 | |
|     this.tiltx.push(action.tiltX || 0);
 | |
|     this.tilty.push(action.tiltY || 0);
 | |
|     this.twist.push(action.twist || 0);
 | |
|   }
 | |
| 
 | |
|   update(state, inputSource) {
 | |
|     // We call update once per input source, but only want to update global state once.
 | |
|     // Instead of introducing a new lifecycle method, or changing the API to allow multiple
 | |
|     // input sources in a single call, use a small bit of state to avoid repeatedly setting
 | |
|     // global state.
 | |
|     if (!this.#setGlobalState) {
 | |
|       // set modifier properties based on whether any corresponding keys are
 | |
|       // pressed on any key input source
 | |
|       for (const [, otherInputSource] of state.inputSourcesByType("key")) {
 | |
|         this.altKey = otherInputSource.alt || this.altKey;
 | |
|         this.ctrlKey = otherInputSource.ctrl || this.ctrlKey;
 | |
|         this.metaKey = otherInputSource.meta || this.metaKey;
 | |
|         this.shiftKey = otherInputSource.shift || this.shiftKey;
 | |
|       }
 | |
|       this.#setGlobalState = true;
 | |
|     }
 | |
| 
 | |
|     // Note that we currently emit Touch events that don't have this property
 | |
|     // but pointer events should have a `buttons` property, so we'll compute it
 | |
|     // anyway.
 | |
|     const allButtons = Array.from(inputSource.pressed);
 | |
|     this.buttons =
 | |
|       this.buttons | allButtons.reduce((a, i) => a + Math.pow(2, i), 0);
 | |
|   }
 | |
| }
 | |
| 
 | |
| // helpers
 | |
| 
 | |
| /**
 | |
|  * Assert that target is in the viewport of win.
 | |
|  *
 | |
|  * @param {Array.<number>} target - [x, y] coordinates of target
 | |
|  * relative to viewport.
 | |
|  * @param {WindowProxy} win - target window.
 | |
|  * @throws {MoveTargetOutOfBoundsError} - If target is outside the
 | |
|  * viewport.
 | |
|  */
 | |
| function assertInViewPort(target, win) {
 | |
|   const [x, y] = target;
 | |
|   lazy.assert.number(x, `Expected x to be finite number`);
 | |
|   lazy.assert.number(y, `Expected y to be finite number`);
 | |
|   // Viewport includes scrollbars if rendered.
 | |
|   if (x < 0 || y < 0 || x > win.innerWidth || y > win.innerHeight) {
 | |
|     throw new lazy.error.MoveTargetOutOfBoundsError(
 | |
|       `(${x}, ${y}) is out of bounds of viewport ` +
 | |
|         `width (${win.innerWidth}) ` +
 | |
|         `and height (${win.innerHeight})`
 | |
|     );
 | |
|   }
 | |
| }
 | 
