/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; export interface IPointerMoveEventData { leftButton: boolean; buttons: number; pageX: number; pageY: number; } export interface IEventMerger { (lastEvent: R | null, currentEvent: PointerEvent): R; } export interface IPointerMoveCallback { (pointerMoveData: R): void; } export interface IOnStopCallback { (browserEvent?: PointerEvent | KeyboardEvent): void; } export function standardPointerMoveMerger(lastEvent: IPointerMoveEventData | null, currentEvent: PointerEvent): IPointerMoveEventData { currentEvent.preventDefault(); return { leftButton: (currentEvent.button === 0), buttons: currentEvent.buttons, pageX: currentEvent.pageX, pageY: currentEvent.pageY }; } export class GlobalPointerMoveMonitor implements IDisposable { private readonly _hooks = new DisposableStore(); private _pointerMoveEventMerger: IEventMerger | null = null; private _pointerMoveCallback: IPointerMoveCallback | null = null; private _onStopCallback: IOnStopCallback | null = null; public dispose(): void { this.stopMonitoring(false); this._hooks.dispose(); } public stopMonitoring(invokeStopCallback: boolean, browserEvent?: PointerEvent | KeyboardEvent): void { if (!this.isMonitoring()) { // Not monitoring return; } // Unhook this._hooks.clear(); this._pointerMoveEventMerger = null; this._pointerMoveCallback = null; const onStopCallback = this._onStopCallback; this._onStopCallback = null; if (invokeStopCallback && onStopCallback) { onStopCallback(browserEvent); } } public isMonitoring(): boolean { return !!this._pointerMoveEventMerger; } public startMonitoring( initialElement: Element, pointerId: number, initialButtons: number, pointerMoveEventMerger: IEventMerger, pointerMoveCallback: IPointerMoveCallback, onStopCallback: IOnStopCallback ): void { if (this.isMonitoring()) { this.stopMonitoring(false); } this._pointerMoveEventMerger = pointerMoveEventMerger; this._pointerMoveCallback = pointerMoveCallback; this._onStopCallback = onStopCallback; let eventSource: Element | Window = initialElement; try { initialElement.setPointerCapture(pointerId); this._hooks.add(toDisposable(() => { initialElement.releasePointerCapture(pointerId); })); } catch (err) { // See https://github.com/microsoft/vscode/issues/144584 // See https://github.com/microsoft/vscode/issues/146947 // `setPointerCapture` sometimes fails when being invoked // from a `mousedown` listener on macOS and Windows // and it always fails on Linux with the exception: // DOMException: Failed to execute 'setPointerCapture' on 'Element': // No active pointer with the given id is found. // In case of failure, we bind the listeners on the window eventSource = window; } this._hooks.add(dom.addDisposableThrottledListener( eventSource, dom.EventType.POINTER_MOVE, (data: R) => { if (data.buttons !== initialButtons) { // Buttons state has changed in the meantime this.stopMonitoring(true); return; } this._pointerMoveCallback!(data); }, (lastEvent: R | null, currentEvent) => this._pointerMoveEventMerger!(lastEvent, currentEvent) )); this._hooks.add(dom.addDisposableListener( eventSource, dom.EventType.POINTER_UP, (e: PointerEvent) => this.stopMonitoring(true) )); } }