Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/browserView/electron-browser/overlayManager.ts
5257 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 { Disposable } from '../../../../base/common/lifecycle.js';
7
import { Event, MicrotaskEmitter } from '../../../../base/common/event.js';
8
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
9
import { getDomNodePagePosition, IDomNodePagePosition } from '../../../../base/browser/dom.js';
10
import { CodeWindow } from '../../../../base/browser/window.js';
11
12
export enum BrowserOverlayType {
13
Menu = 'menu',
14
QuickInput = 'quickInput',
15
Hover = 'hover',
16
Dialog = 'dialog',
17
Notification = 'notification',
18
Unknown = 'unknown'
19
}
20
21
const OVERLAY_DEFINITIONS: ReadonlyArray<{ className: string; type: BrowserOverlayType }> = [
22
{ className: 'monaco-menu-container', type: BrowserOverlayType.Menu },
23
{ className: 'quick-input-widget', type: BrowserOverlayType.QuickInput },
24
{ className: 'monaco-hover', type: BrowserOverlayType.Hover },
25
{ className: 'editor-widget', type: BrowserOverlayType.Hover },
26
{ className: 'suggest-details-container', type: BrowserOverlayType.Hover },
27
{ className: 'monaco-dialog-modal-block', type: BrowserOverlayType.Dialog },
28
{ className: 'notifications-center', type: BrowserOverlayType.Notification },
29
{ className: 'notification-toast-container', type: BrowserOverlayType.Notification },
30
// Context view is very generic, so treat the content as unknown
31
{ className: 'context-view', type: BrowserOverlayType.Unknown }
32
];
33
34
export const IBrowserOverlayManager = createDecorator<IBrowserOverlayManager>('browserOverlayManager');
35
36
export interface IBrowserOverlayInfo {
37
type: BrowserOverlayType;
38
rect: IDomNodePagePosition;
39
}
40
41
export interface IBrowserOverlayManager {
42
readonly _serviceBrand: undefined;
43
44
/**
45
* Event fired when overlay state changes
46
*/
47
readonly onDidChangeOverlayState: Event<void>;
48
49
/**
50
* Get overlays overlapping with the given element
51
*/
52
getOverlappingOverlays(element: HTMLElement): IBrowserOverlayInfo[];
53
}
54
55
export class BrowserOverlayManager extends Disposable implements IBrowserOverlayManager {
56
declare readonly _serviceBrand: undefined;
57
58
private readonly _onDidChangeOverlayState = this._register(new MicrotaskEmitter<void>({
59
onWillAddFirstListener: () => {
60
// Start observing the document for structural changes
61
this._observerIsConnected = true;
62
this._structuralObserver.observe(this.targetWindow.document.body, {
63
childList: true,
64
subtree: true
65
});
66
this.updateTrackedElements();
67
},
68
onDidRemoveLastListener: () => {
69
// Stop observing when no listeners are present
70
this._observerIsConnected = false;
71
this._structuralObserver.disconnect();
72
this.stopTrackingElements();
73
},
74
75
// Must be passed to prevent duplicate emits
76
merge: () => { }
77
}));
78
readonly onDidChangeOverlayState = this._onDidChangeOverlayState.event;
79
80
private readonly _overlayCollections = new Map<string, { type: BrowserOverlayType; collection: HTMLCollectionOf<Element> }>();
81
private _overlayRectangles = new WeakMap<HTMLElement, IDomNodePagePosition>();
82
private _elementObservers = new WeakMap<HTMLElement, MutationObserver>();
83
private _structuralObserver: MutationObserver;
84
private _observerIsConnected: boolean = false;
85
private _shadowRootHostCollection: HTMLCollectionOf<Element>;
86
private _shadowRootObservers = new WeakMap<ShadowRoot, MutationObserver>();
87
private _shadowRootOverlayCache = new WeakMap<ShadowRoot, Array<{ element: HTMLElement; type: BrowserOverlayType }>>();
88
89
constructor(
90
private readonly targetWindow: CodeWindow
91
) {
92
super();
93
94
// Initialize live collections for each overlay selector in main document
95
for (const overlayDefinition of OVERLAY_DEFINITIONS) {
96
this._overlayCollections.set(overlayDefinition.className, {
97
type: overlayDefinition.type,
98
// We need dynamic collections for overlay detection, using getElementsByClassName is intentional here
99
// eslint-disable-next-line no-restricted-syntax
100
collection: this.targetWindow.document.getElementsByClassName(overlayDefinition.className)
101
});
102
}
103
104
// Initialize live collection for shadow root hosts
105
// We need dynamic collections for overlay detection, using getElementsByClassName is intentional here
106
// eslint-disable-next-line no-restricted-syntax
107
this._shadowRootHostCollection = this.targetWindow.document.getElementsByClassName('shadow-root-host');
108
109
// Setup structural observer to watch for element additions/removals
110
this._structuralObserver = new targetWindow.MutationObserver((mutations) => {
111
let didRemove = false;
112
for (const mutation of mutations) {
113
for (const node of mutation.removedNodes) {
114
// Clean up element observers
115
if (this._elementObservers.has(node as HTMLElement)) {
116
const observer = this._elementObservers.get(node as HTMLElement);
117
observer?.disconnect();
118
this._elementObservers.delete(node as HTMLElement);
119
didRemove = true;
120
}
121
122
if (this._overlayRectangles.delete(node as HTMLElement)) {
123
didRemove = true;
124
}
125
126
// Clean up shadow root observers when shadow-root-host elements are removed
127
const hostElement = node as HTMLElement;
128
if (hostElement.shadowRoot) {
129
const shadowRoot = hostElement.shadowRoot;
130
const observer = this._shadowRootObservers.get(shadowRoot);
131
if (observer) {
132
observer.disconnect();
133
this._shadowRootObservers.delete(shadowRoot);
134
this._shadowRootOverlayCache.delete(shadowRoot);
135
didRemove = true;
136
}
137
}
138
}
139
}
140
this.updateTrackedElements(didRemove);
141
});
142
}
143
144
private *overlays(): Iterable<{ element: HTMLElement; type: BrowserOverlayType }> {
145
// Yield overlays from main document live collections
146
for (const entry of this._overlayCollections.values()) {
147
for (const element of entry.collection) {
148
yield { element: element as HTMLElement, type: entry.type };
149
}
150
}
151
152
// Yield overlays from shadow roots
153
for (const hostElement of this._shadowRootHostCollection) {
154
const shadowRoot = hostElement.shadowRoot;
155
if (shadowRoot) {
156
let cache = this._shadowRootOverlayCache.get(shadowRoot);
157
if (!cache) {
158
// Rebuild cache
159
cache = [];
160
for (const overlayDefinition of OVERLAY_DEFINITIONS) {
161
// We need to query shadow roots for overlay detection, using querySelectorAll is intentional here
162
// eslint-disable-next-line no-restricted-syntax
163
const elements = shadowRoot.querySelectorAll(`.${overlayDefinition.className}`);
164
for (const element of elements) {
165
cache.push({ element: element as HTMLElement, type: overlayDefinition.type });
166
}
167
}
168
this._shadowRootOverlayCache.set(shadowRoot, cache);
169
}
170
171
yield* cache;
172
}
173
}
174
}
175
176
private updateTrackedElements(shouldEmit = false): void {
177
// Track shadow roots using live collection
178
for (const host of this._shadowRootHostCollection) {
179
const hostElement = host as HTMLElement;
180
const shadowRoot = hostElement.shadowRoot;
181
if (shadowRoot && !this._shadowRootObservers.has(shadowRoot)) {
182
// Create observer for this shadow root
183
const observer = new this.targetWindow.MutationObserver(() => {
184
// Clear element cache when shadow root structure changes
185
this._shadowRootOverlayCache.delete(shadowRoot);
186
this._onDidChangeOverlayState.fire();
187
});
188
189
observer.observe(shadowRoot, {
190
childList: true,
191
subtree: true
192
});
193
194
this._shadowRootObservers.set(shadowRoot, observer);
195
shouldEmit = true;
196
}
197
}
198
199
// Scan all overlay collections for elements and ensure they have observers
200
for (const overlay of this.overlays()) {
201
// Create a new observer for this specific element if we don't already have one
202
if (!this._elementObservers.has(overlay.element)) {
203
const observer = new this.targetWindow.MutationObserver(() => {
204
this._overlayRectangles.delete(overlay.element);
205
this._onDidChangeOverlayState.fire();
206
});
207
208
// Store the observer in the WeakMap
209
this._elementObservers.set(overlay.element, observer);
210
211
// Start observing this element
212
observer.observe(overlay.element, {
213
attributes: true,
214
attributeFilter: ['style', 'class'],
215
childList: true,
216
subtree: true
217
});
218
219
shouldEmit = true;
220
}
221
}
222
223
if (shouldEmit) {
224
this._onDidChangeOverlayState.fire();
225
}
226
}
227
228
private getRect(element: HTMLElement): IDomNodePagePosition {
229
if (!this._overlayRectangles.has(element)) {
230
const rect = getDomNodePagePosition(element);
231
// If the observer is not connected (no listeners), do not cache rectangles as we won't know when they change.
232
if (!this._observerIsConnected) {
233
return rect;
234
}
235
this._overlayRectangles.set(element, rect);
236
}
237
return this._overlayRectangles.get(element)!;
238
}
239
240
getOverlappingOverlays(element: HTMLElement): IBrowserOverlayInfo[] {
241
const elementRect = getDomNodePagePosition(element);
242
const overlappingOverlays: IBrowserOverlayInfo[] = [];
243
244
// Check against all precomputed overlay rectangles
245
for (const overlay of this.overlays()) {
246
const overlayRect = this.getRect(overlay.element);
247
if (overlayRect && this.isRectanglesOverlapping(elementRect, overlayRect)) {
248
overlappingOverlays.push({
249
type: overlay.type,
250
rect: overlayRect
251
});
252
}
253
}
254
255
return overlappingOverlays;
256
}
257
258
private isRectanglesOverlapping(rect1: IDomNodePagePosition, rect2: IDomNodePagePosition): boolean {
259
// If elements are offscreen or set to zero size, consider them non-overlapping
260
if (rect1.width === 0 || rect1.height === 0 || rect2.width === 0 || rect2.height === 0) {
261
return false;
262
}
263
264
return !(rect1.left + rect1.width <= rect2.left ||
265
rect2.left + rect2.width <= rect1.left ||
266
rect1.top + rect1.height <= rect2.top ||
267
rect2.top + rect2.height <= rect1.top);
268
}
269
270
private stopTrackingElements(): void {
271
// Disconnect all element observers
272
for (const overlay of this.overlays()) {
273
const observer = this._elementObservers.get(overlay.element);
274
observer?.disconnect();
275
}
276
277
// Disconnect all shadow root observers
278
for (const hostElement of this._shadowRootHostCollection) {
279
const shadowRoot = (hostElement as HTMLElement).shadowRoot;
280
const shadowObserver = this._shadowRootObservers.get(shadowRoot!);
281
shadowObserver?.disconnect();
282
}
283
284
this._shadowRootObservers = new WeakMap();
285
this._shadowRootOverlayCache = new WeakMap();
286
this._overlayRectangles = new WeakMap();
287
this._elementObservers = new WeakMap();
288
}
289
290
override dispose(): void {
291
this._observerIsConnected = false;
292
this._structuralObserver.disconnect();
293
this.stopTrackingElements();
294
295
super.dispose();
296
}
297
}
298
299