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
4779 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 { Emitter, Event } 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
const OVERLAY_CLASSES: string[] = [
13
'monaco-menu-container',
14
'quick-input-widget',
15
'monaco-hover',
16
'monaco-dialog-modal-block',
17
'notifications-center',
18
'notification-toast-container',
19
'context-view'
20
];
21
22
export const IBrowserOverlayManager = createDecorator<IBrowserOverlayManager>('browserOverlayManager');
23
24
export interface IBrowserOverlayManager {
25
readonly _serviceBrand: undefined;
26
27
/**
28
* Event fired when overlay state changes
29
*/
30
readonly onDidChangeOverlayState: Event<void>;
31
32
/**
33
* Check if the given element overlaps with any overlay
34
*/
35
isOverlappingWithOverlays(element: HTMLElement): boolean;
36
}
37
38
export class BrowserOverlayManager extends Disposable implements IBrowserOverlayManager {
39
declare readonly _serviceBrand: undefined;
40
41
private readonly _onDidChangeOverlayState = this._register(new Emitter<void>({
42
onWillAddFirstListener: () => {
43
// Start observing the document for structural changes
44
this._observerIsConnected = true;
45
this._structuralObserver.observe(this.targetWindow.document.body, {
46
childList: true,
47
subtree: true
48
});
49
this.updateTrackedElements();
50
},
51
onDidRemoveLastListener: () => {
52
// Stop observing when no listeners are present
53
this._observerIsConnected = false;
54
this._structuralObserver.disconnect();
55
this.stopTrackingElements();
56
}
57
}));
58
readonly onDidChangeOverlayState = this._onDidChangeOverlayState.event;
59
60
private readonly _overlayCollections = new Map<string, HTMLCollectionOf<Element>>();
61
private _overlayRectangles = new WeakMap<HTMLElement, IDomNodePagePosition>();
62
private _elementObservers = new WeakMap<HTMLElement, MutationObserver>();
63
private _structuralObserver: MutationObserver;
64
private _observerIsConnected: boolean = false;
65
66
constructor(
67
private readonly targetWindow: CodeWindow
68
) {
69
super();
70
71
// Initialize live collections for each overlay selector
72
for (const className of OVERLAY_CLASSES) {
73
// We need dynamic collections for overlay detection, using getElementsByClassName is intentional here
74
// eslint-disable-next-line no-restricted-syntax
75
this._overlayCollections.set(className, this.targetWindow.document.getElementsByClassName(className));
76
}
77
78
// Setup structural observer to watch for element additions/removals
79
this._structuralObserver = new MutationObserver((mutations) => {
80
let didRemove = false;
81
for (const mutation of mutations) {
82
for (const node of mutation.removedNodes) {
83
if (this._elementObservers.has(node as HTMLElement)) {
84
const observer = this._elementObservers.get(node as HTMLElement);
85
observer?.disconnect();
86
this._elementObservers.delete(node as HTMLElement);
87
didRemove = true;
88
}
89
90
if (this._overlayRectangles.delete(node as HTMLElement)) {
91
didRemove = true;
92
}
93
}
94
}
95
this.updateTrackedElements(didRemove);
96
});
97
}
98
99
private *overlays(): Iterable<HTMLElement> {
100
for (const collection of this._overlayCollections.values()) {
101
for (const element of collection) {
102
yield element as HTMLElement;
103
}
104
}
105
}
106
107
private updateTrackedElements(shouldEmit = false): void {
108
// Scan all overlay collections for elements and ensure they have observers
109
for (const overlay of this.overlays()) {
110
// Create a new observer for this specific element if we don't already have one
111
if (!this._elementObservers.has(overlay)) {
112
const observer = new MutationObserver(() => {
113
this._overlayRectangles.delete(overlay);
114
this._onDidChangeOverlayState.fire();
115
});
116
117
// Store the observer in the WeakMap
118
this._elementObservers.set(overlay, observer);
119
120
// Start observing this element
121
observer.observe(overlay, {
122
attributes: true,
123
attributeFilter: ['style', 'class'],
124
childList: true,
125
subtree: true
126
});
127
128
shouldEmit = true;
129
}
130
}
131
132
if (shouldEmit) {
133
this._onDidChangeOverlayState.fire();
134
}
135
}
136
137
private getRect(element: HTMLElement): IDomNodePagePosition {
138
if (!this._overlayRectangles.has(element)) {
139
const rect = getDomNodePagePosition(element);
140
// If the observer is not connected (no listeners), do not cache rectangles as we won't know when they change.
141
if (!this._observerIsConnected) {
142
return rect;
143
}
144
this._overlayRectangles.set(element, rect);
145
}
146
return this._overlayRectangles.get(element)!;
147
}
148
149
isOverlappingWithOverlays(element: HTMLElement): boolean {
150
const elementRect = getDomNodePagePosition(element);
151
152
// Check against all precomputed overlay rectangles
153
for (const overlay of this.overlays()) {
154
const overlayRect = this.getRect(overlay);
155
if (overlayRect && this.isRectanglesOverlapping(elementRect, overlayRect)) {
156
return true;
157
}
158
}
159
160
return false;
161
}
162
163
private isRectanglesOverlapping(rect1: IDomNodePagePosition, rect2: IDomNodePagePosition): boolean {
164
// If elements are offscreen or set to zero size, consider them non-overlapping
165
if (rect1.width === 0 || rect1.height === 0 || rect2.width === 0 || rect2.height === 0) {
166
return false;
167
}
168
169
return !(rect1.left + rect1.width <= rect2.left ||
170
rect2.left + rect2.width <= rect1.left ||
171
rect1.top + rect1.height <= rect2.top ||
172
rect2.top + rect2.height <= rect1.top);
173
}
174
175
private stopTrackingElements(): void {
176
for (const overlay of this.overlays()) {
177
const observer = this._elementObservers.get(overlay);
178
observer?.disconnect();
179
}
180
this._overlayRectangles = new WeakMap();
181
this._elementObservers = new WeakMap();
182
}
183
184
override dispose(): void {
185
this._observerIsConnected = false;
186
this._structuralObserver.disconnect();
187
this.stopTrackingElements();
188
189
super.dispose();
190
}
191
}
192
193