203c6ef532ce48d69a27a3fcc9971ddc097107b9525621aa02af5491169693ca8ab8208b8207e6c9efeabf2233975558b43d465ef3715fa167a4670a1ec735 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. /*---------------------------------------------------------------------------------------------
  2. * Copyright (c) Microsoft Corporation. All rights reserved.
  3. * Licensed under the MIT License. See License.txt in the project root for license information.
  4. *--------------------------------------------------------------------------------------------*/
  5. import * as dom from '../../base/browser/dom.js';
  6. import { GlobalPointerMoveMonitor } from '../../base/browser/globalPointerMoveMonitor.js';
  7. import { StandardMouseEvent } from '../../base/browser/mouseEvent.js';
  8. import { RunOnceScheduler } from '../../base/common/async.js';
  9. import { Disposable } from '../../base/common/lifecycle.js';
  10. import { asCssVariableName } from '../../platform/theme/common/colorRegistry.js';
  11. /**
  12. * Coordinates relative to the whole document (e.g. mouse event's pageX and pageY)
  13. */
  14. export class PageCoordinates {
  15. constructor(x, y) {
  16. this.x = x;
  17. this.y = y;
  18. this._pageCoordinatesBrand = undefined;
  19. }
  20. toClientCoordinates() {
  21. return new ClientCoordinates(this.x - dom.StandardWindow.scrollX, this.y - dom.StandardWindow.scrollY);
  22. }
  23. }
  24. /**
  25. * Coordinates within the application's client area (i.e. origin is document's scroll position).
  26. *
  27. * For example, clicking in the top-left corner of the client area will
  28. * always result in a mouse event with a client.x value of 0, regardless
  29. * of whether the page is scrolled horizontally.
  30. */
  31. export class ClientCoordinates {
  32. constructor(clientX, clientY) {
  33. this.clientX = clientX;
  34. this.clientY = clientY;
  35. this._clientCoordinatesBrand = undefined;
  36. }
  37. toPageCoordinates() {
  38. return new PageCoordinates(this.clientX + dom.StandardWindow.scrollX, this.clientY + dom.StandardWindow.scrollY);
  39. }
  40. }
  41. /**
  42. * The position of the editor in the page.
  43. */
  44. export class EditorPagePosition {
  45. constructor(x, y, width, height) {
  46. this.x = x;
  47. this.y = y;
  48. this.width = width;
  49. this.height = height;
  50. this._editorPagePositionBrand = undefined;
  51. }
  52. }
  53. /**
  54. * Coordinates relative to the the (top;left) of the editor that can be used safely with other internal editor metrics.
  55. * **NOTE**: This position is obtained by taking page coordinates and transforming them relative to the
  56. * editor's (top;left) position in a way in which scale transformations are taken into account.
  57. * **NOTE**: These coordinates could be negative if the mouse position is outside the editor.
  58. */
  59. export class CoordinatesRelativeToEditor {
  60. constructor(x, y) {
  61. this.x = x;
  62. this.y = y;
  63. this._positionRelativeToEditorBrand = undefined;
  64. }
  65. }
  66. export function createEditorPagePosition(editorViewDomNode) {
  67. const editorPos = dom.getDomNodePagePosition(editorViewDomNode);
  68. return new EditorPagePosition(editorPos.left, editorPos.top, editorPos.width, editorPos.height);
  69. }
  70. export function createCoordinatesRelativeToEditor(editorViewDomNode, editorPagePosition, pos) {
  71. // The editor's page position is read from the DOM using getBoundingClientRect().
  72. //
  73. // getBoundingClientRect() returns the actual dimensions, while offsetWidth and offsetHeight
  74. // reflect the unscaled size. We can use this difference to detect a transform:scale()
  75. // and we will apply the transformation in inverse to get mouse coordinates that make sense inside the editor.
  76. //
  77. // This could be expanded to cover rotation as well maybe by walking the DOM up from `editorViewDomNode`
  78. // and computing the effective transformation matrix using getComputedStyle(element).transform.
  79. //
  80. const scaleX = editorPagePosition.width / editorViewDomNode.offsetWidth;
  81. const scaleY = editorPagePosition.height / editorViewDomNode.offsetHeight;
  82. // Adjust mouse offsets if editor appears to be scaled via transforms
  83. const relativeX = (pos.x - editorPagePosition.x) / scaleX;
  84. const relativeY = (pos.y - editorPagePosition.y) / scaleY;
  85. return new CoordinatesRelativeToEditor(relativeX, relativeY);
  86. }
  87. export class EditorMouseEvent extends StandardMouseEvent {
  88. constructor(e, isFromPointerCapture, editorViewDomNode) {
  89. super(e);
  90. this._editorMouseEventBrand = undefined;
  91. this.isFromPointerCapture = isFromPointerCapture;
  92. this.pos = new PageCoordinates(this.posx, this.posy);
  93. this.editorPos = createEditorPagePosition(editorViewDomNode);
  94. this.relativePos = createCoordinatesRelativeToEditor(editorViewDomNode, this.editorPos, this.pos);
  95. }
  96. }
  97. export class EditorMouseEventFactory {
  98. constructor(editorViewDomNode) {
  99. this._editorViewDomNode = editorViewDomNode;
  100. }
  101. _create(e) {
  102. return new EditorMouseEvent(e, false, this._editorViewDomNode);
  103. }
  104. onContextMenu(target, callback) {
  105. return dom.addDisposableListener(target, 'contextmenu', (e) => {
  106. callback(this._create(e));
  107. });
  108. }
  109. onMouseUp(target, callback) {
  110. return dom.addDisposableListener(target, 'mouseup', (e) => {
  111. callback(this._create(e));
  112. });
  113. }
  114. onMouseDown(target, callback) {
  115. return dom.addDisposableListener(target, dom.EventType.MOUSE_DOWN, (e) => {
  116. callback(this._create(e));
  117. });
  118. }
  119. onPointerDown(target, callback) {
  120. return dom.addDisposableListener(target, dom.EventType.POINTER_DOWN, (e) => {
  121. callback(this._create(e), e.pointerId);
  122. });
  123. }
  124. onMouseLeave(target, callback) {
  125. return dom.addDisposableListener(target, dom.EventType.MOUSE_LEAVE, (e) => {
  126. callback(this._create(e));
  127. });
  128. }
  129. onMouseMove(target, callback) {
  130. return dom.addDisposableListener(target, 'mousemove', (e) => callback(this._create(e)));
  131. }
  132. }
  133. export class EditorPointerEventFactory {
  134. constructor(editorViewDomNode) {
  135. this._editorViewDomNode = editorViewDomNode;
  136. }
  137. _create(e) {
  138. return new EditorMouseEvent(e, false, this._editorViewDomNode);
  139. }
  140. onPointerUp(target, callback) {
  141. return dom.addDisposableListener(target, 'pointerup', (e) => {
  142. callback(this._create(e));
  143. });
  144. }
  145. onPointerDown(target, callback) {
  146. return dom.addDisposableListener(target, dom.EventType.POINTER_DOWN, (e) => {
  147. callback(this._create(e), e.pointerId);
  148. });
  149. }
  150. onPointerLeave(target, callback) {
  151. return dom.addDisposableListener(target, dom.EventType.POINTER_LEAVE, (e) => {
  152. callback(this._create(e));
  153. });
  154. }
  155. onPointerMove(target, callback) {
  156. return dom.addDisposableListener(target, 'pointermove', (e) => callback(this._create(e)));
  157. }
  158. }
  159. export class GlobalEditorPointerMoveMonitor extends Disposable {
  160. constructor(editorViewDomNode) {
  161. super();
  162. this._editorViewDomNode = editorViewDomNode;
  163. this._globalPointerMoveMonitor = this._register(new GlobalPointerMoveMonitor());
  164. this._keydownListener = null;
  165. }
  166. startMonitoring(initialElement, pointerId, initialButtons, pointerMoveCallback, onStopCallback) {
  167. // Add a <<capture>> keydown event listener that will cancel the monitoring
  168. // if something other than a modifier key is pressed
  169. this._keydownListener = dom.addStandardDisposableListener(document, 'keydown', (e) => {
  170. const kb = e.toKeybinding();
  171. if (kb.isModifierKey()) {
  172. // Allow modifier keys
  173. return;
  174. }
  175. this._globalPointerMoveMonitor.stopMonitoring(true, e.browserEvent);
  176. }, true);
  177. this._globalPointerMoveMonitor.startMonitoring(initialElement, pointerId, initialButtons, (e) => {
  178. pointerMoveCallback(new EditorMouseEvent(e, true, this._editorViewDomNode));
  179. }, (e) => {
  180. this._keydownListener.dispose();
  181. onStopCallback(e);
  182. });
  183. }
  184. stopMonitoring() {
  185. this._globalPointerMoveMonitor.stopMonitoring(true);
  186. }
  187. }
  188. /**
  189. * A helper to create dynamic css rules, bound to a class name.
  190. * Rules are reused.
  191. * Reference counting and delayed garbage collection ensure that no rules leak.
  192. */
  193. export class DynamicCssRules {
  194. constructor(_editor) {
  195. this._editor = _editor;
  196. this._instanceId = ++DynamicCssRules._idPool;
  197. this._counter = 0;
  198. this._rules = new Map();
  199. // We delay garbage collection so that hanging rules can be reused.
  200. this._garbageCollectionScheduler = new RunOnceScheduler(() => this.garbageCollect(), 1000);
  201. }
  202. createClassNameRef(options) {
  203. const rule = this.getOrCreateRule(options);
  204. rule.increaseRefCount();
  205. return {
  206. className: rule.className,
  207. dispose: () => {
  208. rule.decreaseRefCount();
  209. this._garbageCollectionScheduler.schedule();
  210. }
  211. };
  212. }
  213. getOrCreateRule(properties) {
  214. const key = this.computeUniqueKey(properties);
  215. let existingRule = this._rules.get(key);
  216. if (!existingRule) {
  217. const counter = this._counter++;
  218. existingRule = new RefCountedCssRule(key, `dyn-rule-${this._instanceId}-${counter}`, dom.isInShadowDOM(this._editor.getContainerDomNode())
  219. ? this._editor.getContainerDomNode()
  220. : undefined, properties);
  221. this._rules.set(key, existingRule);
  222. }
  223. return existingRule;
  224. }
  225. computeUniqueKey(properties) {
  226. return JSON.stringify(properties);
  227. }
  228. garbageCollect() {
  229. for (const rule of this._rules.values()) {
  230. if (!rule.hasReferences()) {
  231. this._rules.delete(rule.key);
  232. rule.dispose();
  233. }
  234. }
  235. }
  236. }
  237. DynamicCssRules._idPool = 0;
  238. class RefCountedCssRule {
  239. constructor(key, className, _containerElement, properties) {
  240. this.key = key;
  241. this.className = className;
  242. this.properties = properties;
  243. this._referenceCount = 0;
  244. this._styleElement = dom.createStyleSheet(_containerElement);
  245. this._styleElement.textContent = this.getCssText(this.className, this.properties);
  246. }
  247. getCssText(className, properties) {
  248. let str = `.${className} {`;
  249. for (const prop in properties) {
  250. const value = properties[prop];
  251. let cssValue;
  252. if (typeof value === 'object') {
  253. cssValue = `var(${asCssVariableName(value.id)})`;
  254. }
  255. else {
  256. cssValue = value;
  257. }
  258. const cssPropName = camelToDashes(prop);
  259. str += `\n\t${cssPropName}: ${cssValue};`;
  260. }
  261. str += `\n}`;
  262. return str;
  263. }
  264. dispose() {
  265. this._styleElement.remove();
  266. }
  267. increaseRefCount() {
  268. this._referenceCount++;
  269. }
  270. decreaseRefCount() {
  271. this._referenceCount--;
  272. }
  273. hasReferences() {
  274. return this._referenceCount > 0;
  275. }
  276. }
  277. function camelToDashes(str) {
  278. return str.replace(/(^[A-Z])/, ([first]) => first.toLowerCase())
  279. .replace(/([A-Z])/g, ([letter]) => `-${letter.toLowerCase()}`);
  280. }