Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/editorDom.ts
3292 views
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
6
import * as dom from '../../base/browser/dom.js';
7
import * as domStylesheetsJs from '../../base/browser/domStylesheets.js';
8
import { GlobalPointerMoveMonitor } from '../../base/browser/globalPointerMoveMonitor.js';
9
import { StandardMouseEvent } from '../../base/browser/mouseEvent.js';
10
import { RunOnceScheduler } from '../../base/common/async.js';
11
import { Disposable, DisposableMap, DisposableStore, IDisposable } from '../../base/common/lifecycle.js';
12
import { ICodeEditor } from './editorBrowser.js';
13
import { asCssVariable } from '../../platform/theme/common/colorRegistry.js';
14
import { ThemeColor } from '../../base/common/themables.js';
15
16
/**
17
* Coordinates relative to the whole document (e.g. mouse event's pageX and pageY)
18
*/
19
export class PageCoordinates {
20
_pageCoordinatesBrand: void = undefined;
21
22
constructor(
23
public readonly x: number,
24
public readonly y: number
25
) { }
26
27
public toClientCoordinates(targetWindow: Window): ClientCoordinates {
28
return new ClientCoordinates(this.x - targetWindow.scrollX, this.y - targetWindow.scrollY);
29
}
30
}
31
32
/**
33
* Coordinates within the application's client area (i.e. origin is document's scroll position).
34
*
35
* For example, clicking in the top-left corner of the client area will
36
* always result in a mouse event with a client.x value of 0, regardless
37
* of whether the page is scrolled horizontally.
38
*/
39
export class ClientCoordinates {
40
_clientCoordinatesBrand: void = undefined;
41
42
constructor(
43
public readonly clientX: number,
44
public readonly clientY: number
45
) { }
46
47
public toPageCoordinates(targetWindow: Window): PageCoordinates {
48
return new PageCoordinates(this.clientX + targetWindow.scrollX, this.clientY + targetWindow.scrollY);
49
}
50
}
51
52
/**
53
* The position of the editor in the page.
54
*/
55
export class EditorPagePosition {
56
_editorPagePositionBrand: void = undefined;
57
58
constructor(
59
public readonly x: number,
60
public readonly y: number,
61
public readonly width: number,
62
public readonly height: number
63
) { }
64
}
65
66
/**
67
* Coordinates relative to the (top;left) of the editor that can be used safely with other internal editor metrics.
68
* **NOTE**: This position is obtained by taking page coordinates and transforming them relative to the
69
* editor's (top;left) position in a way in which scale transformations are taken into account.
70
* **NOTE**: These coordinates could be negative if the mouse position is outside the editor.
71
*/
72
export class CoordinatesRelativeToEditor {
73
_positionRelativeToEditorBrand: void = undefined;
74
75
constructor(
76
public readonly x: number,
77
public readonly y: number
78
) { }
79
}
80
81
export function createEditorPagePosition(editorViewDomNode: HTMLElement): EditorPagePosition {
82
const editorPos = dom.getDomNodePagePosition(editorViewDomNode);
83
return new EditorPagePosition(editorPos.left, editorPos.top, editorPos.width, editorPos.height);
84
}
85
86
export function createCoordinatesRelativeToEditor(editorViewDomNode: HTMLElement, editorPagePosition: EditorPagePosition, pos: PageCoordinates) {
87
// The editor's page position is read from the DOM using getBoundingClientRect().
88
//
89
// getBoundingClientRect() returns the actual dimensions, while offsetWidth and offsetHeight
90
// reflect the unscaled size. We can use this difference to detect a transform:scale()
91
// and we will apply the transformation in inverse to get mouse coordinates that make sense inside the editor.
92
//
93
// This could be expanded to cover rotation as well maybe by walking the DOM up from `editorViewDomNode`
94
// and computing the effective transformation matrix using getComputedStyle(element).transform.
95
//
96
const scaleX = editorPagePosition.width / editorViewDomNode.offsetWidth;
97
const scaleY = editorPagePosition.height / editorViewDomNode.offsetHeight;
98
99
// Adjust mouse offsets if editor appears to be scaled via transforms
100
const relativeX = (pos.x - editorPagePosition.x) / scaleX;
101
const relativeY = (pos.y - editorPagePosition.y) / scaleY;
102
return new CoordinatesRelativeToEditor(relativeX, relativeY);
103
}
104
105
export class EditorMouseEvent extends StandardMouseEvent {
106
_editorMouseEventBrand: void = undefined;
107
108
/**
109
* If the event is a result of using `setPointerCapture`, the `event.target`
110
* does not necessarily reflect the position in the editor.
111
*/
112
public readonly isFromPointerCapture: boolean;
113
114
/**
115
* Coordinates relative to the whole document.
116
*/
117
public readonly pos: PageCoordinates;
118
119
/**
120
* Editor's coordinates relative to the whole document.
121
*/
122
public readonly editorPos: EditorPagePosition;
123
124
/**
125
* Coordinates relative to the (top;left) of the editor.
126
* *NOTE*: These coordinates are preferred because they take into account transformations applied to the editor.
127
* *NOTE*: These coordinates could be negative if the mouse position is outside the editor.
128
*/
129
public readonly relativePos: CoordinatesRelativeToEditor;
130
131
constructor(e: MouseEvent, isFromPointerCapture: boolean, editorViewDomNode: HTMLElement) {
132
super(dom.getWindow(editorViewDomNode), e);
133
this.isFromPointerCapture = isFromPointerCapture;
134
this.pos = new PageCoordinates(this.posx, this.posy);
135
this.editorPos = createEditorPagePosition(editorViewDomNode);
136
this.relativePos = createCoordinatesRelativeToEditor(editorViewDomNode, this.editorPos, this.pos);
137
}
138
}
139
140
export class EditorMouseEventFactory {
141
142
private readonly _editorViewDomNode: HTMLElement;
143
144
constructor(editorViewDomNode: HTMLElement) {
145
this._editorViewDomNode = editorViewDomNode;
146
}
147
148
private _create(e: MouseEvent): EditorMouseEvent {
149
return new EditorMouseEvent(e, false, this._editorViewDomNode);
150
}
151
152
public onContextMenu(target: HTMLElement, callback: (e: EditorMouseEvent) => void): IDisposable {
153
return dom.addDisposableListener(target, dom.EventType.CONTEXT_MENU, (e: MouseEvent) => {
154
callback(this._create(e));
155
});
156
}
157
158
public onMouseUp(target: HTMLElement, callback: (e: EditorMouseEvent) => void): IDisposable {
159
return dom.addDisposableListener(target, dom.EventType.MOUSE_UP, (e: MouseEvent) => {
160
callback(this._create(e));
161
});
162
}
163
164
public onMouseDown(target: HTMLElement, callback: (e: EditorMouseEvent) => void): IDisposable {
165
return dom.addDisposableListener(target, dom.EventType.MOUSE_DOWN, (e: MouseEvent) => {
166
callback(this._create(e));
167
});
168
}
169
170
public onPointerDown(target: HTMLElement, callback: (e: EditorMouseEvent, pointerId: number) => void): IDisposable {
171
return dom.addDisposableListener(target, dom.EventType.POINTER_DOWN, (e: PointerEvent) => {
172
callback(this._create(e), e.pointerId);
173
});
174
}
175
176
public onMouseLeave(target: HTMLElement, callback: (e: EditorMouseEvent) => void): IDisposable {
177
return dom.addDisposableListener(target, dom.EventType.MOUSE_LEAVE, (e: MouseEvent) => {
178
callback(this._create(e));
179
});
180
}
181
182
public onMouseMove(target: HTMLElement, callback: (e: EditorMouseEvent) => void): IDisposable {
183
return dom.addDisposableListener(target, dom.EventType.MOUSE_MOVE, (e) => callback(this._create(e)));
184
}
185
}
186
187
export class EditorPointerEventFactory {
188
189
private readonly _editorViewDomNode: HTMLElement;
190
191
constructor(editorViewDomNode: HTMLElement) {
192
this._editorViewDomNode = editorViewDomNode;
193
}
194
195
private _create(e: MouseEvent): EditorMouseEvent {
196
return new EditorMouseEvent(e, false, this._editorViewDomNode);
197
}
198
199
public onPointerUp(target: HTMLElement, callback: (e: EditorMouseEvent) => void): IDisposable {
200
return dom.addDisposableListener(target, 'pointerup', (e: MouseEvent) => {
201
callback(this._create(e));
202
});
203
}
204
205
public onPointerDown(target: HTMLElement, callback: (e: EditorMouseEvent, pointerId: number) => void): IDisposable {
206
return dom.addDisposableListener(target, dom.EventType.POINTER_DOWN, (e: PointerEvent) => {
207
callback(this._create(e), e.pointerId);
208
});
209
}
210
211
public onPointerLeave(target: HTMLElement, callback: (e: EditorMouseEvent) => void): IDisposable {
212
return dom.addDisposableListener(target, dom.EventType.POINTER_LEAVE, (e: MouseEvent) => {
213
callback(this._create(e));
214
});
215
}
216
217
public onPointerMove(target: HTMLElement, callback: (e: EditorMouseEvent) => void): IDisposable {
218
return dom.addDisposableListener(target, 'pointermove', (e) => callback(this._create(e)));
219
}
220
}
221
222
export class GlobalEditorPointerMoveMonitor extends Disposable {
223
224
private readonly _editorViewDomNode: HTMLElement;
225
private readonly _globalPointerMoveMonitor: GlobalPointerMoveMonitor;
226
private _keydownListener: IDisposable | null;
227
228
constructor(editorViewDomNode: HTMLElement) {
229
super();
230
this._editorViewDomNode = editorViewDomNode;
231
this._globalPointerMoveMonitor = this._register(new GlobalPointerMoveMonitor());
232
this._keydownListener = null;
233
}
234
235
public startMonitoring(
236
initialElement: Element,
237
pointerId: number,
238
initialButtons: number,
239
pointerMoveCallback: (e: EditorMouseEvent) => void,
240
onStopCallback: (browserEvent?: PointerEvent | KeyboardEvent) => void
241
): void {
242
243
// Add a <<capture>> keydown event listener that will cancel the monitoring
244
// if something other than a modifier key is pressed
245
this._keydownListener = dom.addStandardDisposableListener(<any>initialElement.ownerDocument, 'keydown', (e) => {
246
const chord = e.toKeyCodeChord();
247
if (chord.isModifierKey()) {
248
// Allow modifier keys
249
return;
250
}
251
this._globalPointerMoveMonitor.stopMonitoring(true, e.browserEvent);
252
}, true);
253
254
this._globalPointerMoveMonitor.startMonitoring(
255
initialElement,
256
pointerId,
257
initialButtons,
258
(e) => {
259
pointerMoveCallback(new EditorMouseEvent(e, true, this._editorViewDomNode));
260
},
261
(e) => {
262
this._keydownListener!.dispose();
263
onStopCallback(e);
264
}
265
);
266
}
267
268
public stopMonitoring(): void {
269
this._globalPointerMoveMonitor.stopMonitoring(true);
270
}
271
}
272
273
274
/**
275
* A helper to create dynamic css rules, bound to a class name.
276
* Rules are reused.
277
* Reference counting and delayed garbage collection ensure that no rules leak.
278
*/
279
export class DynamicCssRules {
280
private static _idPool = 0;
281
private readonly _instanceId = ++DynamicCssRules._idPool;
282
private _counter = 0;
283
private readonly _rules = new DisposableMap<string, RefCountedCssRule>();
284
285
// We delay garbage collection so that hanging rules can be reused.
286
private readonly _garbageCollectionScheduler = new RunOnceScheduler(() => this.garbageCollect(), 1000);
287
288
constructor(
289
private readonly _editor: ICodeEditor
290
) { }
291
292
dispose(): void {
293
this._rules.dispose();
294
this._garbageCollectionScheduler.dispose();
295
}
296
297
public createClassNameRef(options: CssProperties): ClassNameReference {
298
const rule = this.getOrCreateRule(options);
299
rule.increaseRefCount();
300
301
return {
302
className: rule.className,
303
dispose: () => {
304
rule.decreaseRefCount();
305
this._garbageCollectionScheduler.schedule();
306
}
307
};
308
}
309
310
private getOrCreateRule(properties: CssProperties): RefCountedCssRule {
311
const key = this.computeUniqueKey(properties);
312
let existingRule = this._rules.get(key);
313
if (!existingRule) {
314
const counter = this._counter++;
315
existingRule = new RefCountedCssRule(key, `dyn-rule-${this._instanceId}-${counter}`,
316
dom.isInShadowDOM(this._editor.getContainerDomNode())
317
? this._editor.getContainerDomNode()
318
: undefined,
319
properties
320
);
321
this._rules.set(key, existingRule);
322
}
323
return existingRule;
324
}
325
326
private computeUniqueKey(properties: CssProperties): string {
327
return JSON.stringify(properties);
328
}
329
330
private garbageCollect() {
331
for (const rule of this._rules.values()) {
332
if (!rule.hasReferences()) {
333
this._rules.deleteAndDispose(rule.key);
334
}
335
}
336
}
337
}
338
339
export interface ClassNameReference extends IDisposable {
340
className: string;
341
}
342
343
export interface CssProperties {
344
border?: string;
345
borderColor?: string | ThemeColor;
346
borderRadius?: string;
347
fontStyle?: string;
348
fontWeight?: string;
349
fontSize?: string;
350
fontFamily?: string;
351
unicodeBidi?: string;
352
textDecoration?: string;
353
color?: string | ThemeColor;
354
backgroundColor?: string | ThemeColor;
355
opacity?: string;
356
verticalAlign?: string;
357
cursor?: string;
358
margin?: string;
359
padding?: string;
360
width?: string;
361
height?: string;
362
display?: string;
363
}
364
365
class RefCountedCssRule {
366
private _referenceCount: number = 0;
367
private _styleElement: HTMLStyleElement | undefined;
368
private readonly _styleElementDisposables: DisposableStore;
369
370
constructor(
371
public readonly key: string,
372
public readonly className: string,
373
_containerElement: HTMLElement | undefined,
374
public readonly properties: CssProperties,
375
) {
376
this._styleElementDisposables = new DisposableStore();
377
this._styleElement = domStylesheetsJs.createStyleSheet(_containerElement, undefined, this._styleElementDisposables);
378
this._styleElement.textContent = this.getCssText(this.className, this.properties);
379
}
380
381
private getCssText(className: string, properties: CssProperties): string {
382
let str = `.${className} {`;
383
for (const prop in properties) {
384
const value = (properties as any)[prop] as string | ThemeColor;
385
let cssValue;
386
if (typeof value === 'object') {
387
cssValue = asCssVariable(value.id);
388
} else {
389
cssValue = value;
390
}
391
392
const cssPropName = camelToDashes(prop);
393
str += `\n\t${cssPropName}: ${cssValue};`;
394
}
395
str += `\n}`;
396
return str;
397
}
398
399
public dispose(): void {
400
this._styleElementDisposables.dispose();
401
this._styleElement = undefined;
402
}
403
404
public increaseRefCount(): void {
405
this._referenceCount++;
406
}
407
408
public decreaseRefCount(): void {
409
this._referenceCount--;
410
}
411
412
public hasReferences(): boolean {
413
return this._referenceCount > 0;
414
}
415
}
416
417
function camelToDashes(str: string): string {
418
return str.replace(/(^[A-Z])/, ([first]) => first.toLowerCase())
419
.replace(/([A-Z])/g, ([letter]) => `-${letter.toLowerCase()}`);
420
}
421
422