| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280 |
- /*---------------------------------------------------------------------------------------------
- * 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 { GlobalPointerMoveMonitor } from '../../base/browser/globalPointerMoveMonitor.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;
- }
- }
- /**
- * Coordinates relative to the the (top;left) of the editor that can be used safely with other internal editor metrics.
- * **NOTE**: This position is obtained by taking page coordinates and transforming them relative to the
- * editor's (top;left) position in a way in which scale transformations are taken into account.
- * **NOTE**: These coordinates could be negative if the mouse position is outside the editor.
- */
- export class CoordinatesRelativeToEditor {
- constructor(x, y) {
- this.x = x;
- this.y = y;
- this._positionRelativeToEditorBrand = undefined;
- }
- }
- export function createEditorPagePosition(editorViewDomNode) {
- const editorPos = dom.getDomNodePagePosition(editorViewDomNode);
- return new EditorPagePosition(editorPos.left, editorPos.top, editorPos.width, editorPos.height);
- }
- export function createCoordinatesRelativeToEditor(editorViewDomNode, editorPagePosition, pos) {
- // The editor's page position is read from the DOM using getBoundingClientRect().
- //
- // getBoundingClientRect() returns the actual dimensions, while offsetWidth and offsetHeight
- // reflect the unscaled size. We can use this difference to detect a transform:scale()
- // and we will apply the transformation in inverse to get mouse coordinates that make sense inside the editor.
- //
- // This could be expanded to cover rotation as well maybe by walking the DOM up from `editorViewDomNode`
- // and computing the effective transformation matrix using getComputedStyle(element).transform.
- //
- const scaleX = editorPagePosition.width / editorViewDomNode.offsetWidth;
- const scaleY = editorPagePosition.height / editorViewDomNode.offsetHeight;
- // Adjust mouse offsets if editor appears to be scaled via transforms
- const relativeX = (pos.x - editorPagePosition.x) / scaleX;
- const relativeY = (pos.y - editorPagePosition.y) / scaleY;
- return new CoordinatesRelativeToEditor(relativeX, relativeY);
- }
- export class EditorMouseEvent extends StandardMouseEvent {
- constructor(e, isFromPointerCapture, editorViewDomNode) {
- super(e);
- this._editorMouseEventBrand = undefined;
- this.isFromPointerCapture = isFromPointerCapture;
- this.pos = new PageCoordinates(this.posx, this.posy);
- this.editorPos = createEditorPagePosition(editorViewDomNode);
- this.relativePos = createCoordinatesRelativeToEditor(editorViewDomNode, this.editorPos, this.pos);
- }
- }
- export class EditorMouseEventFactory {
- constructor(editorViewDomNode) {
- this._editorViewDomNode = editorViewDomNode;
- }
- _create(e) {
- return new EditorMouseEvent(e, false, 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, dom.EventType.MOUSE_DOWN, (e) => {
- callback(this._create(e));
- });
- }
- onPointerDown(target, callback) {
- return dom.addDisposableListener(target, dom.EventType.POINTER_DOWN, (e) => {
- callback(this._create(e), e.pointerId);
- });
- }
- onMouseLeave(target, callback) {
- return dom.addDisposableListener(target, dom.EventType.MOUSE_LEAVE, (e) => {
- callback(this._create(e));
- });
- }
- onMouseMove(target, callback) {
- return dom.addDisposableListener(target, 'mousemove', (e) => callback(this._create(e)));
- }
- }
- export class EditorPointerEventFactory {
- constructor(editorViewDomNode) {
- this._editorViewDomNode = editorViewDomNode;
- }
- _create(e) {
- return new EditorMouseEvent(e, false, this._editorViewDomNode);
- }
- onPointerUp(target, callback) {
- return dom.addDisposableListener(target, 'pointerup', (e) => {
- callback(this._create(e));
- });
- }
- onPointerDown(target, callback) {
- return dom.addDisposableListener(target, dom.EventType.POINTER_DOWN, (e) => {
- callback(this._create(e), e.pointerId);
- });
- }
- onPointerLeave(target, callback) {
- return dom.addDisposableListener(target, dom.EventType.POINTER_LEAVE, (e) => {
- callback(this._create(e));
- });
- }
- onPointerMove(target, callback) {
- return dom.addDisposableListener(target, 'pointermove', (e) => callback(this._create(e)));
- }
- }
- export class GlobalEditorPointerMoveMonitor extends Disposable {
- constructor(editorViewDomNode) {
- super();
- this._editorViewDomNode = editorViewDomNode;
- this._globalPointerMoveMonitor = this._register(new GlobalPointerMoveMonitor());
- this._keydownListener = null;
- }
- startMonitoring(initialElement, pointerId, initialButtons, pointerMoveCallback, onStopCallback) {
- // Add a <<capture>> 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._globalPointerMoveMonitor.stopMonitoring(true, e.browserEvent);
- }, true);
- this._globalPointerMoveMonitor.startMonitoring(initialElement, pointerId, initialButtons, (e) => {
- pointerMoveCallback(new EditorMouseEvent(e, true, this._editorViewDomNode));
- }, (e) => {
- this._keydownListener.dispose();
- onStopCallback(e);
- });
- }
- stopMonitoring() {
- this._globalPointerMoveMonitor.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._instanceId = ++DynamicCssRules._idPool;
- 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-${this._instanceId}-${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();
- }
- }
- }
- }
- DynamicCssRules._idPool = 0;
- 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()}`);
- }
|