tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

Input.ts (16721B)


      1 /**
      2 * @license
      3 * Copyright 2017 Google Inc.
      4 * SPDX-License-Identifier: Apache-2.0
      5 */
      6 
      7 import type {Protocol} from 'devtools-protocol';
      8 
      9 import type {CDPSession} from '../api/CDPSession.js';
     10 import type {Point} from '../api/ElementHandle.js';
     11 import {
     12  Keyboard,
     13  Mouse,
     14  MouseButton,
     15  Touchscreen,
     16  type TouchHandle,
     17  type KeyDownOptions,
     18  type KeyPressOptions,
     19  type KeyboardTypeOptions,
     20  type MouseClickOptions,
     21  type MouseMoveOptions,
     22  type MouseOptions,
     23  type MouseWheelOptions,
     24 } from '../api/Input.js';
     25 import {TouchError} from '../common/Errors.js';
     26 import {
     27  _keyDefinitions,
     28  type KeyDefinition,
     29  type KeyInput,
     30 } from '../common/USKeyboardLayout.js';
     31 import {assert} from '../util/assert.js';
     32 
     33 type KeyDescription = Required<
     34  Pick<KeyDefinition, 'keyCode' | 'key' | 'text' | 'code' | 'location'>
     35 >;
     36 
     37 /**
     38 * @internal
     39 */
     40 export class CdpKeyboard extends Keyboard {
     41  #client: CDPSession;
     42  #pressedKeys = new Set<string>();
     43 
     44  _modifiers = 0;
     45 
     46  constructor(client: CDPSession) {
     47    super();
     48    this.#client = client;
     49  }
     50 
     51  updateClient(client: CDPSession): void {
     52    this.#client = client;
     53  }
     54 
     55  override async down(
     56    key: KeyInput,
     57    options: Readonly<KeyDownOptions> = {
     58      text: undefined,
     59      commands: [],
     60    },
     61  ): Promise<void> {
     62    const description = this.#keyDescriptionForString(key);
     63 
     64    const autoRepeat = this.#pressedKeys.has(description.code);
     65    this.#pressedKeys.add(description.code);
     66    this._modifiers |= this.#modifierBit(description.key);
     67 
     68    const text = options.text === undefined ? description.text : options.text;
     69    await this.#client.send('Input.dispatchKeyEvent', {
     70      type: text ? 'keyDown' : 'rawKeyDown',
     71      modifiers: this._modifiers,
     72      windowsVirtualKeyCode: description.keyCode,
     73      code: description.code,
     74      key: description.key,
     75      text: text,
     76      unmodifiedText: text,
     77      autoRepeat,
     78      location: description.location,
     79      isKeypad: description.location === 3,
     80      commands: options.commands,
     81    });
     82  }
     83 
     84  #modifierBit(key: string): number {
     85    if (key === 'Alt') {
     86      return 1;
     87    }
     88    if (key === 'Control') {
     89      return 2;
     90    }
     91    if (key === 'Meta') {
     92      return 4;
     93    }
     94    if (key === 'Shift') {
     95      return 8;
     96    }
     97    return 0;
     98  }
     99 
    100  #keyDescriptionForString(keyString: KeyInput): KeyDescription {
    101    const shift = this._modifiers & 8;
    102    const description = {
    103      key: '',
    104      keyCode: 0,
    105      code: '',
    106      text: '',
    107      location: 0,
    108    };
    109 
    110    const definition = _keyDefinitions[keyString];
    111    assert(definition, `Unknown key: "${keyString}"`);
    112 
    113    if (definition.key) {
    114      description.key = definition.key;
    115    }
    116    if (shift && definition.shiftKey) {
    117      description.key = definition.shiftKey;
    118    }
    119 
    120    if (definition.keyCode) {
    121      description.keyCode = definition.keyCode;
    122    }
    123    if (shift && definition.shiftKeyCode) {
    124      description.keyCode = definition.shiftKeyCode;
    125    }
    126 
    127    if (definition.code) {
    128      description.code = definition.code;
    129    }
    130 
    131    if (definition.location) {
    132      description.location = definition.location;
    133    }
    134 
    135    if (description.key.length === 1) {
    136      description.text = description.key;
    137    }
    138 
    139    if (definition.text) {
    140      description.text = definition.text;
    141    }
    142    if (shift && definition.shiftText) {
    143      description.text = definition.shiftText;
    144    }
    145 
    146    // if any modifiers besides shift are pressed, no text should be sent
    147    if (this._modifiers & ~8) {
    148      description.text = '';
    149    }
    150 
    151    return description;
    152  }
    153 
    154  override async up(key: KeyInput): Promise<void> {
    155    const description = this.#keyDescriptionForString(key);
    156 
    157    this._modifiers &= ~this.#modifierBit(description.key);
    158    this.#pressedKeys.delete(description.code);
    159    await this.#client.send('Input.dispatchKeyEvent', {
    160      type: 'keyUp',
    161      modifiers: this._modifiers,
    162      key: description.key,
    163      windowsVirtualKeyCode: description.keyCode,
    164      code: description.code,
    165      location: description.location,
    166    });
    167  }
    168 
    169  override async sendCharacter(char: string): Promise<void> {
    170    await this.#client.send('Input.insertText', {text: char});
    171  }
    172 
    173  private charIsKey(char: string): char is KeyInput {
    174    return !!_keyDefinitions[char as KeyInput];
    175  }
    176 
    177  override async type(
    178    text: string,
    179    options: Readonly<KeyboardTypeOptions> = {},
    180  ): Promise<void> {
    181    const delay = options.delay || undefined;
    182    for (const char of text) {
    183      if (this.charIsKey(char)) {
    184        await this.press(char, {delay});
    185      } else {
    186        if (delay) {
    187          await new Promise(f => {
    188            return setTimeout(f, delay);
    189          });
    190        }
    191        await this.sendCharacter(char);
    192      }
    193    }
    194  }
    195 
    196  override async press(
    197    key: KeyInput,
    198    options: Readonly<KeyPressOptions> = {},
    199  ): Promise<void> {
    200    const {delay = null} = options;
    201    await this.down(key, options);
    202    if (delay) {
    203      await new Promise(f => {
    204        return setTimeout(f, options.delay);
    205      });
    206    }
    207    await this.up(key);
    208  }
    209 }
    210 
    211 /**
    212 * This must follow {@link Protocol.Input.DispatchMouseEventRequest.buttons}.
    213 */
    214 const enum MouseButtonFlag {
    215  None = 0,
    216  Left = 1,
    217  Right = 1 << 1,
    218  Middle = 1 << 2,
    219  Back = 1 << 3,
    220  Forward = 1 << 4,
    221 }
    222 
    223 const getFlag = (button: MouseButton): MouseButtonFlag => {
    224  switch (button) {
    225    case MouseButton.Left:
    226      return MouseButtonFlag.Left;
    227    case MouseButton.Right:
    228      return MouseButtonFlag.Right;
    229    case MouseButton.Middle:
    230      return MouseButtonFlag.Middle;
    231    case MouseButton.Back:
    232      return MouseButtonFlag.Back;
    233    case MouseButton.Forward:
    234      return MouseButtonFlag.Forward;
    235  }
    236 };
    237 
    238 /**
    239 * This should match
    240 * https://source.chromium.org/chromium/chromium/src/+/refs/heads/main:content/browser/renderer_host/input/web_input_event_builders_mac.mm;drc=a61b95c63b0b75c1cfe872d9c8cdf927c226046e;bpv=1;bpt=1;l=221.
    241 */
    242 const getButtonFromPressedButtons = (
    243  buttons: number,
    244 ): Protocol.Input.MouseButton => {
    245  if (buttons & MouseButtonFlag.Left) {
    246    return MouseButton.Left;
    247  } else if (buttons & MouseButtonFlag.Right) {
    248    return MouseButton.Right;
    249  } else if (buttons & MouseButtonFlag.Middle) {
    250    return MouseButton.Middle;
    251  } else if (buttons & MouseButtonFlag.Back) {
    252    return MouseButton.Back;
    253  } else if (buttons & MouseButtonFlag.Forward) {
    254    return MouseButton.Forward;
    255  }
    256  return 'none';
    257 };
    258 
    259 interface MouseState {
    260  /**
    261   * The current position of the mouse.
    262   */
    263  position: Point;
    264  /**
    265   * The buttons that are currently being pressed.
    266   */
    267  buttons: number;
    268 }
    269 
    270 /**
    271 * @internal
    272 */
    273 export class CdpMouse extends Mouse {
    274  #client: CDPSession;
    275  #keyboard: CdpKeyboard;
    276 
    277  constructor(client: CDPSession, keyboard: CdpKeyboard) {
    278    super();
    279    this.#client = client;
    280    this.#keyboard = keyboard;
    281  }
    282 
    283  updateClient(client: CDPSession): void {
    284    this.#client = client;
    285  }
    286 
    287  #_state: Readonly<MouseState> = {
    288    position: {x: 0, y: 0},
    289    buttons: MouseButtonFlag.None,
    290  };
    291  get #state(): MouseState {
    292    return Object.assign({...this.#_state}, ...this.#transactions);
    293  }
    294 
    295  // Transactions can run in parallel, so we store each of thme in this array.
    296  #transactions: Array<Partial<MouseState>> = [];
    297  #createTransaction(): {
    298    update: (updates: Partial<MouseState>) => void;
    299    commit: () => void;
    300    rollback: () => void;
    301  } {
    302    const transaction: Partial<MouseState> = {};
    303    this.#transactions.push(transaction);
    304    const popTransaction = () => {
    305      this.#transactions.splice(this.#transactions.indexOf(transaction), 1);
    306    };
    307    return {
    308      update: (updates: Partial<MouseState>) => {
    309        Object.assign(transaction, updates);
    310      },
    311      commit: () => {
    312        this.#_state = {...this.#_state, ...transaction};
    313        popTransaction();
    314      },
    315      rollback: popTransaction,
    316    };
    317  }
    318 
    319  /**
    320   * This is a shortcut for a typical update, commit/rollback lifecycle based on
    321   * the error of the action.
    322   */
    323  async #withTransaction(
    324    action: (
    325      update: (updates: Partial<MouseState>) => void,
    326    ) => Promise<unknown>,
    327  ): Promise<void> {
    328    const {update, commit, rollback} = this.#createTransaction();
    329    try {
    330      await action(update);
    331      commit();
    332    } catch (error) {
    333      rollback();
    334      throw error;
    335    }
    336  }
    337 
    338  override async reset(): Promise<void> {
    339    const actions = [];
    340    for (const [flag, button] of [
    341      [MouseButtonFlag.Left, MouseButton.Left],
    342      [MouseButtonFlag.Middle, MouseButton.Middle],
    343      [MouseButtonFlag.Right, MouseButton.Right],
    344      [MouseButtonFlag.Forward, MouseButton.Forward],
    345      [MouseButtonFlag.Back, MouseButton.Back],
    346    ] as const) {
    347      if (this.#state.buttons & flag) {
    348        actions.push(this.up({button: button}));
    349      }
    350    }
    351    if (this.#state.position.x !== 0 || this.#state.position.y !== 0) {
    352      actions.push(this.move(0, 0));
    353    }
    354    await Promise.all(actions);
    355  }
    356 
    357  override async move(
    358    x: number,
    359    y: number,
    360    options: Readonly<MouseMoveOptions> = {},
    361  ): Promise<void> {
    362    const {steps = 1} = options;
    363    const from = this.#state.position;
    364    const to = {x, y};
    365    for (let i = 1; i <= steps; i++) {
    366      await this.#withTransaction(updateState => {
    367        updateState({
    368          position: {
    369            x: from.x + (to.x - from.x) * (i / steps),
    370            y: from.y + (to.y - from.y) * (i / steps),
    371          },
    372        });
    373        const {buttons, position} = this.#state;
    374        return this.#client.send('Input.dispatchMouseEvent', {
    375          type: 'mouseMoved',
    376          modifiers: this.#keyboard._modifiers,
    377          buttons,
    378          button: getButtonFromPressedButtons(buttons),
    379          ...position,
    380        });
    381      });
    382    }
    383  }
    384 
    385  override async down(options: Readonly<MouseOptions> = {}): Promise<void> {
    386    const {button = MouseButton.Left, clickCount = 1} = options;
    387    const flag = getFlag(button);
    388    if (!flag) {
    389      throw new Error(`Unsupported mouse button: ${button}`);
    390    }
    391    if (this.#state.buttons & flag) {
    392      throw new Error(`'${button}' is already pressed.`);
    393    }
    394    await this.#withTransaction(updateState => {
    395      updateState({
    396        buttons: this.#state.buttons | flag,
    397      });
    398      const {buttons, position} = this.#state;
    399      return this.#client.send('Input.dispatchMouseEvent', {
    400        type: 'mousePressed',
    401        modifiers: this.#keyboard._modifiers,
    402        clickCount,
    403        buttons,
    404        button,
    405        ...position,
    406      });
    407    });
    408  }
    409 
    410  override async up(options: Readonly<MouseOptions> = {}): Promise<void> {
    411    const {button = MouseButton.Left, clickCount = 1} = options;
    412    const flag = getFlag(button);
    413    if (!flag) {
    414      throw new Error(`Unsupported mouse button: ${button}`);
    415    }
    416    if (!(this.#state.buttons & flag)) {
    417      throw new Error(`'${button}' is not pressed.`);
    418    }
    419    await this.#withTransaction(updateState => {
    420      updateState({
    421        buttons: this.#state.buttons & ~flag,
    422      });
    423      const {buttons, position} = this.#state;
    424      return this.#client.send('Input.dispatchMouseEvent', {
    425        type: 'mouseReleased',
    426        modifiers: this.#keyboard._modifiers,
    427        clickCount,
    428        buttons,
    429        button,
    430        ...position,
    431      });
    432    });
    433  }
    434 
    435  override async click(
    436    x: number,
    437    y: number,
    438    options: Readonly<MouseClickOptions> = {},
    439  ): Promise<void> {
    440    const {delay, count = 1, clickCount = count} = options;
    441    if (count < 1) {
    442      throw new Error('Click must occur a positive number of times.');
    443    }
    444    const actions: Array<Promise<void>> = [this.move(x, y)];
    445    if (clickCount === count) {
    446      for (let i = 1; i < count; ++i) {
    447        actions.push(
    448          this.down({...options, clickCount: i}),
    449          this.up({...options, clickCount: i}),
    450        );
    451      }
    452    }
    453    actions.push(this.down({...options, clickCount}));
    454    if (typeof delay === 'number') {
    455      await Promise.all(actions);
    456      actions.length = 0;
    457      await new Promise(resolve => {
    458        setTimeout(resolve, delay);
    459      });
    460    }
    461    actions.push(this.up({...options, clickCount}));
    462    await Promise.all(actions);
    463  }
    464 
    465  override async wheel(
    466    options: Readonly<MouseWheelOptions> = {},
    467  ): Promise<void> {
    468    const {deltaX = 0, deltaY = 0} = options;
    469    const {position, buttons} = this.#state;
    470    await this.#client.send('Input.dispatchMouseEvent', {
    471      type: 'mouseWheel',
    472      pointerType: 'mouse',
    473      modifiers: this.#keyboard._modifiers,
    474      deltaY,
    475      deltaX,
    476      buttons,
    477      ...position,
    478    });
    479  }
    480 
    481  override async drag(
    482    start: Point,
    483    target: Point,
    484  ): Promise<Protocol.Input.DragData> {
    485    const promise = new Promise<Protocol.Input.DragData>(resolve => {
    486      this.#client.once('Input.dragIntercepted', event => {
    487        return resolve(event.data);
    488      });
    489    });
    490    await this.move(start.x, start.y);
    491    await this.down();
    492    await this.move(target.x, target.y);
    493    return await promise;
    494  }
    495 
    496  override async dragEnter(
    497    target: Point,
    498    data: Protocol.Input.DragData,
    499  ): Promise<void> {
    500    await this.#client.send('Input.dispatchDragEvent', {
    501      type: 'dragEnter',
    502      x: target.x,
    503      y: target.y,
    504      modifiers: this.#keyboard._modifiers,
    505      data,
    506    });
    507  }
    508 
    509  override async dragOver(
    510    target: Point,
    511    data: Protocol.Input.DragData,
    512  ): Promise<void> {
    513    await this.#client.send('Input.dispatchDragEvent', {
    514      type: 'dragOver',
    515      x: target.x,
    516      y: target.y,
    517      modifiers: this.#keyboard._modifiers,
    518      data,
    519    });
    520  }
    521 
    522  override async drop(
    523    target: Point,
    524    data: Protocol.Input.DragData,
    525  ): Promise<void> {
    526    await this.#client.send('Input.dispatchDragEvent', {
    527      type: 'drop',
    528      x: target.x,
    529      y: target.y,
    530      modifiers: this.#keyboard._modifiers,
    531      data,
    532    });
    533  }
    534 
    535  override async dragAndDrop(
    536    start: Point,
    537    target: Point,
    538    options: {delay?: number} = {},
    539  ): Promise<void> {
    540    const {delay = null} = options;
    541    const data = await this.drag(start, target);
    542    await this.dragEnter(target, data);
    543    await this.dragOver(target, data);
    544    if (delay) {
    545      await new Promise(resolve => {
    546        return setTimeout(resolve, delay);
    547      });
    548    }
    549    await this.drop(target, data);
    550    await this.up();
    551  }
    552 }
    553 
    554 /**
    555 * @internal
    556 */
    557 class CdpTouchHandle implements TouchHandle {
    558  #started = false;
    559  #touchScreen: CdpTouchscreen;
    560  #touchPoint: Protocol.Input.TouchPoint;
    561  #client: CDPSession;
    562  #keyboard: CdpKeyboard;
    563 
    564  constructor(
    565    client: CDPSession,
    566    touchScreen: CdpTouchscreen,
    567    keyboard: CdpKeyboard,
    568    touchPoint: Protocol.Input.TouchPoint,
    569  ) {
    570    this.#client = client;
    571    this.#touchScreen = touchScreen;
    572    this.#keyboard = keyboard;
    573    this.#touchPoint = touchPoint;
    574  }
    575 
    576  updateClient(client: CDPSession): void {
    577    this.#client = client;
    578  }
    579 
    580  async start(): Promise<void> {
    581    if (this.#started) {
    582      throw new TouchError('Touch has already started');
    583    }
    584    await this.#client.send('Input.dispatchTouchEvent', {
    585      type: 'touchStart',
    586      touchPoints: [this.#touchPoint],
    587      modifiers: this.#keyboard._modifiers,
    588    });
    589    this.#started = true;
    590  }
    591 
    592  move(x: number, y: number): Promise<void> {
    593    this.#touchPoint.x = Math.round(x);
    594    this.#touchPoint.y = Math.round(y);
    595    return this.#client.send('Input.dispatchTouchEvent', {
    596      type: 'touchMove',
    597      touchPoints: [this.#touchPoint],
    598      modifiers: this.#keyboard._modifiers,
    599    });
    600  }
    601 
    602  async end(): Promise<void> {
    603    await this.#client.send('Input.dispatchTouchEvent', {
    604      type: 'touchEnd',
    605      touchPoints: [this.#touchPoint],
    606      modifiers: this.#keyboard._modifiers,
    607    });
    608    this.#touchScreen.removeHandle(this);
    609  }
    610 }
    611 
    612 /**
    613 * @internal
    614 */
    615 export class CdpTouchscreen extends Touchscreen {
    616  #client: CDPSession;
    617  #keyboard: CdpKeyboard;
    618  declare touches: CdpTouchHandle[];
    619 
    620  constructor(client: CDPSession, keyboard: CdpKeyboard) {
    621    super();
    622    this.#client = client;
    623    this.#keyboard = keyboard;
    624  }
    625 
    626  updateClient(client: CDPSession): void {
    627    this.#client = client;
    628    this.touches.forEach(t => {
    629      t.updateClient(client);
    630    });
    631  }
    632 
    633  override async touchStart(x: number, y: number): Promise<TouchHandle> {
    634    const id = this.idGenerator();
    635    const touchPoint: Protocol.Input.TouchPoint = {
    636      x: Math.round(x),
    637      y: Math.round(y),
    638      radiusX: 0.5,
    639      radiusY: 0.5,
    640      force: 0.5,
    641      id,
    642    };
    643    const touch = new CdpTouchHandle(
    644      this.#client,
    645      this,
    646      this.#keyboard,
    647      touchPoint,
    648    );
    649    await touch.start();
    650    this.touches.push(touch);
    651    return touch;
    652  }
    653 }