/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as dom from '../../base/browser/dom.js'; import { GlobalMouseMoveMonitor } from '../../base/browser/globalMouseMoveMonitor.js'; import { StandardMouseEvent } from '../../base/browser/mouseEvent.js'; import { RunOnceScheduler } from '../../base/common/async.js'; import { Disposable } from '../../base/common/lifecycle.js'; import { asCssVariableName } from '../../platform/theme/common/colorRegistry.js'; /** * Coordinates relative to the whole document (e.g. mouse event's pageX and pageY) */ export class PageCoordinates { constructor(x, y) { this.x = x; this.y = y; this._pageCoordinatesBrand = undefined; } toClientCoordinates() { return new ClientCoordinates(this.x - dom.StandardWindow.scrollX, this.y - dom.StandardWindow.scrollY); } } /** * Coordinates within the application's client area (i.e. origin is document's scroll position). * * For example, clicking in the top-left corner of the client area will * always result in a mouse event with a client.x value of 0, regardless * of whether the page is scrolled horizontally. */ export class ClientCoordinates { constructor(clientX, clientY) { this.clientX = clientX; this.clientY = clientY; this._clientCoordinatesBrand = undefined; } toPageCoordinates() { return new PageCoordinates(this.clientX + dom.StandardWindow.scrollX, this.clientY + dom.StandardWindow.scrollY); } } /** * The position of the editor in the page. */ export class EditorPagePosition { constructor(x, y, width, height) { this.x = x; this.y = y; this.width = width; this.height = height; this._editorPagePositionBrand = undefined; } } export function createEditorPagePosition(editorViewDomNode) { const editorPos = dom.getDomNodePagePosition(editorViewDomNode); return new EditorPagePosition(editorPos.left, editorPos.top, editorPos.width, editorPos.height); } export class EditorMouseEvent extends StandardMouseEvent { constructor(e, editorViewDomNode) { super(e); this._editorMouseEventBrand = undefined; this.pos = new PageCoordinates(this.posx, this.posy); this.editorPos = createEditorPagePosition(editorViewDomNode); } } export class EditorMouseEventFactory { constructor(editorViewDomNode) { this._editorViewDomNode = editorViewDomNode; } _create(e) { return new EditorMouseEvent(e, this._editorViewDomNode); } onContextMenu(target, callback) { return dom.addDisposableListener(target, 'contextmenu', (e) => { callback(this._create(e)); }); } onMouseUp(target, callback) { return dom.addDisposableListener(target, 'mouseup', (e) => { callback(this._create(e)); }); } onMouseDown(target, callback) { return dom.addDisposableListener(target, 'mousedown', (e) => { callback(this._create(e)); }); } onMouseLeave(target, callback) { return dom.addDisposableNonBubblingMouseOutListener(target, (e) => { callback(this._create(e)); }); } onMouseMoveThrottled(target, callback, merger, minimumTimeMs) { const myMerger = (lastEvent, currentEvent) => { return merger(lastEvent, this._create(currentEvent)); }; return dom.addDisposableThrottledListener(target, 'mousemove', callback, myMerger, minimumTimeMs); } } export class EditorPointerEventFactory { constructor(editorViewDomNode) { this._editorViewDomNode = editorViewDomNode; } _create(e) { return new EditorMouseEvent(e, this._editorViewDomNode); } onPointerUp(target, callback) { return dom.addDisposableListener(target, 'pointerup', (e) => { callback(this._create(e)); }); } onPointerDown(target, callback) { return dom.addDisposableListener(target, 'pointerdown', (e) => { callback(this._create(e)); }); } onPointerLeave(target, callback) { return dom.addDisposableNonBubblingPointerOutListener(target, (e) => { callback(this._create(e)); }); } onPointerMoveThrottled(target, callback, merger, minimumTimeMs) { const myMerger = (lastEvent, currentEvent) => { return merger(lastEvent, this._create(currentEvent)); }; return dom.addDisposableThrottledListener(target, 'pointermove', callback, myMerger, minimumTimeMs); } } export class GlobalEditorMouseMoveMonitor extends Disposable { constructor(editorViewDomNode) { super(); this._editorViewDomNode = editorViewDomNode; this._globalMouseMoveMonitor = this._register(new GlobalMouseMoveMonitor()); this._keydownListener = null; } startMonitoring(initialElement, initialButtons, merger, mouseMoveCallback, onStopCallback) { // Add a <> keydown event listener that will cancel the monitoring // if something other than a modifier key is pressed this._keydownListener = dom.addStandardDisposableListener(document, 'keydown', (e) => { const kb = e.toKeybinding(); if (kb.isModifierKey()) { // Allow modifier keys return; } this._globalMouseMoveMonitor.stopMonitoring(true, e.browserEvent); }, true); const myMerger = (lastEvent, currentEvent) => { return merger(lastEvent, new EditorMouseEvent(currentEvent, this._editorViewDomNode)); }; this._globalMouseMoveMonitor.startMonitoring(initialElement, initialButtons, myMerger, mouseMoveCallback, (e) => { this._keydownListener.dispose(); onStopCallback(e); }); } stopMonitoring() { this._globalMouseMoveMonitor.stopMonitoring(true); } } /** * A helper to create dynamic css rules, bound to a class name. * Rules are reused. * Reference counting and delayed garbage collection ensure that no rules leak. */ export class DynamicCssRules { constructor(_editor) { this._editor = _editor; this._counter = 0; this._rules = new Map(); // We delay garbage collection so that hanging rules can be reused. this._garbageCollectionScheduler = new RunOnceScheduler(() => this.garbageCollect(), 1000); } createClassNameRef(options) { const rule = this.getOrCreateRule(options); rule.increaseRefCount(); return { className: rule.className, dispose: () => { rule.decreaseRefCount(); this._garbageCollectionScheduler.schedule(); } }; } getOrCreateRule(properties) { const key = this.computeUniqueKey(properties); let existingRule = this._rules.get(key); if (!existingRule) { const counter = this._counter++; existingRule = new RefCountedCssRule(key, `dyn-rule-${counter}`, dom.isInShadowDOM(this._editor.getContainerDomNode()) ? this._editor.getContainerDomNode() : undefined, properties); this._rules.set(key, existingRule); } return existingRule; } computeUniqueKey(properties) { return JSON.stringify(properties); } garbageCollect() { for (const rule of this._rules.values()) { if (!rule.hasReferences()) { this._rules.delete(rule.key); rule.dispose(); } } } } class RefCountedCssRule { constructor(key, className, _containerElement, properties) { this.key = key; this.className = className; this.properties = properties; this._referenceCount = 0; this._styleElement = dom.createStyleSheet(_containerElement); this._styleElement.textContent = this.getCssText(this.className, this.properties); } getCssText(className, properties) { let str = `.${className} {`; for (const prop in properties) { const value = properties[prop]; let cssValue; if (typeof value === 'object') { cssValue = `var(${asCssVariableName(value.id)})`; } else { cssValue = value; } const cssPropName = camelToDashes(prop); str += `\n\t${cssPropName}: ${cssValue};`; } str += `\n}`; return str; } dispose() { this._styleElement.remove(); } increaseRefCount() { this._referenceCount++; } decreaseRefCount() { this._referenceCount--; } hasReferences() { return this._referenceCount > 0; } } function camelToDashes(str) { return str.replace(/(^[A-Z])/, ([first]) => first.toLowerCase()) .replace(/([A-Z])/g, ([letter]) => `-${letter.toLowerCase()}`); }