Path: blob/main/src/vs/workbench/contrib/browserView/electron-browser/overlayManager.ts
5251 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Disposable } from '../../../../base/common/lifecycle.js';6import { Event, MicrotaskEmitter } from '../../../../base/common/event.js';7import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';8import { getDomNodePagePosition, IDomNodePagePosition } from '../../../../base/browser/dom.js';9import { CodeWindow } from '../../../../base/browser/window.js';1011export enum BrowserOverlayType {12Menu = 'menu',13QuickInput = 'quickInput',14Hover = 'hover',15Dialog = 'dialog',16Notification = 'notification',17Unknown = 'unknown'18}1920const OVERLAY_DEFINITIONS: ReadonlyArray<{ className: string; type: BrowserOverlayType }> = [21{ className: 'monaco-menu-container', type: BrowserOverlayType.Menu },22{ className: 'quick-input-widget', type: BrowserOverlayType.QuickInput },23{ className: 'monaco-hover', type: BrowserOverlayType.Hover },24{ className: 'editor-widget', type: BrowserOverlayType.Hover },25{ className: 'suggest-details-container', type: BrowserOverlayType.Hover },26{ className: 'monaco-dialog-modal-block', type: BrowserOverlayType.Dialog },27{ className: 'notifications-center', type: BrowserOverlayType.Notification },28{ className: 'notification-toast-container', type: BrowserOverlayType.Notification },29// Context view is very generic, so treat the content as unknown30{ className: 'context-view', type: BrowserOverlayType.Unknown }31];3233export const IBrowserOverlayManager = createDecorator<IBrowserOverlayManager>('browserOverlayManager');3435export interface IBrowserOverlayInfo {36type: BrowserOverlayType;37rect: IDomNodePagePosition;38}3940export interface IBrowserOverlayManager {41readonly _serviceBrand: undefined;4243/**44* Event fired when overlay state changes45*/46readonly onDidChangeOverlayState: Event<void>;4748/**49* Get overlays overlapping with the given element50*/51getOverlappingOverlays(element: HTMLElement): IBrowserOverlayInfo[];52}5354export class BrowserOverlayManager extends Disposable implements IBrowserOverlayManager {55declare readonly _serviceBrand: undefined;5657private readonly _onDidChangeOverlayState = this._register(new MicrotaskEmitter<void>({58onWillAddFirstListener: () => {59// Start observing the document for structural changes60this._observerIsConnected = true;61this._structuralObserver.observe(this.targetWindow.document.body, {62childList: true,63subtree: true64});65this.updateTrackedElements();66},67onDidRemoveLastListener: () => {68// Stop observing when no listeners are present69this._observerIsConnected = false;70this._structuralObserver.disconnect();71this.stopTrackingElements();72},7374// Must be passed to prevent duplicate emits75merge: () => { }76}));77readonly onDidChangeOverlayState = this._onDidChangeOverlayState.event;7879private readonly _overlayCollections = new Map<string, { type: BrowserOverlayType; collection: HTMLCollectionOf<Element> }>();80private _overlayRectangles = new WeakMap<HTMLElement, IDomNodePagePosition>();81private _elementObservers = new WeakMap<HTMLElement, MutationObserver>();82private _structuralObserver: MutationObserver;83private _observerIsConnected: boolean = false;84private _shadowRootHostCollection: HTMLCollectionOf<Element>;85private _shadowRootObservers = new WeakMap<ShadowRoot, MutationObserver>();86private _shadowRootOverlayCache = new WeakMap<ShadowRoot, Array<{ element: HTMLElement; type: BrowserOverlayType }>>();8788constructor(89private readonly targetWindow: CodeWindow90) {91super();9293// Initialize live collections for each overlay selector in main document94for (const overlayDefinition of OVERLAY_DEFINITIONS) {95this._overlayCollections.set(overlayDefinition.className, {96type: overlayDefinition.type,97// We need dynamic collections for overlay detection, using getElementsByClassName is intentional here98// eslint-disable-next-line no-restricted-syntax99collection: this.targetWindow.document.getElementsByClassName(overlayDefinition.className)100});101}102103// Initialize live collection for shadow root hosts104// We need dynamic collections for overlay detection, using getElementsByClassName is intentional here105// eslint-disable-next-line no-restricted-syntax106this._shadowRootHostCollection = this.targetWindow.document.getElementsByClassName('shadow-root-host');107108// Setup structural observer to watch for element additions/removals109this._structuralObserver = new targetWindow.MutationObserver((mutations) => {110let didRemove = false;111for (const mutation of mutations) {112for (const node of mutation.removedNodes) {113// Clean up element observers114if (this._elementObservers.has(node as HTMLElement)) {115const observer = this._elementObservers.get(node as HTMLElement);116observer?.disconnect();117this._elementObservers.delete(node as HTMLElement);118didRemove = true;119}120121if (this._overlayRectangles.delete(node as HTMLElement)) {122didRemove = true;123}124125// Clean up shadow root observers when shadow-root-host elements are removed126const hostElement = node as HTMLElement;127if (hostElement.shadowRoot) {128const shadowRoot = hostElement.shadowRoot;129const observer = this._shadowRootObservers.get(shadowRoot);130if (observer) {131observer.disconnect();132this._shadowRootObservers.delete(shadowRoot);133this._shadowRootOverlayCache.delete(shadowRoot);134didRemove = true;135}136}137}138}139this.updateTrackedElements(didRemove);140});141}142143private *overlays(): Iterable<{ element: HTMLElement; type: BrowserOverlayType }> {144// Yield overlays from main document live collections145for (const entry of this._overlayCollections.values()) {146for (const element of entry.collection) {147yield { element: element as HTMLElement, type: entry.type };148}149}150151// Yield overlays from shadow roots152for (const hostElement of this._shadowRootHostCollection) {153const shadowRoot = hostElement.shadowRoot;154if (shadowRoot) {155let cache = this._shadowRootOverlayCache.get(shadowRoot);156if (!cache) {157// Rebuild cache158cache = [];159for (const overlayDefinition of OVERLAY_DEFINITIONS) {160// We need to query shadow roots for overlay detection, using querySelectorAll is intentional here161// eslint-disable-next-line no-restricted-syntax162const elements = shadowRoot.querySelectorAll(`.${overlayDefinition.className}`);163for (const element of elements) {164cache.push({ element: element as HTMLElement, type: overlayDefinition.type });165}166}167this._shadowRootOverlayCache.set(shadowRoot, cache);168}169170yield* cache;171}172}173}174175private updateTrackedElements(shouldEmit = false): void {176// Track shadow roots using live collection177for (const host of this._shadowRootHostCollection) {178const hostElement = host as HTMLElement;179const shadowRoot = hostElement.shadowRoot;180if (shadowRoot && !this._shadowRootObservers.has(shadowRoot)) {181// Create observer for this shadow root182const observer = new this.targetWindow.MutationObserver(() => {183// Clear element cache when shadow root structure changes184this._shadowRootOverlayCache.delete(shadowRoot);185this._onDidChangeOverlayState.fire();186});187188observer.observe(shadowRoot, {189childList: true,190subtree: true191});192193this._shadowRootObservers.set(shadowRoot, observer);194shouldEmit = true;195}196}197198// Scan all overlay collections for elements and ensure they have observers199for (const overlay of this.overlays()) {200// Create a new observer for this specific element if we don't already have one201if (!this._elementObservers.has(overlay.element)) {202const observer = new this.targetWindow.MutationObserver(() => {203this._overlayRectangles.delete(overlay.element);204this._onDidChangeOverlayState.fire();205});206207// Store the observer in the WeakMap208this._elementObservers.set(overlay.element, observer);209210// Start observing this element211observer.observe(overlay.element, {212attributes: true,213attributeFilter: ['style', 'class'],214childList: true,215subtree: true216});217218shouldEmit = true;219}220}221222if (shouldEmit) {223this._onDidChangeOverlayState.fire();224}225}226227private getRect(element: HTMLElement): IDomNodePagePosition {228if (!this._overlayRectangles.has(element)) {229const rect = getDomNodePagePosition(element);230// If the observer is not connected (no listeners), do not cache rectangles as we won't know when they change.231if (!this._observerIsConnected) {232return rect;233}234this._overlayRectangles.set(element, rect);235}236return this._overlayRectangles.get(element)!;237}238239getOverlappingOverlays(element: HTMLElement): IBrowserOverlayInfo[] {240const elementRect = getDomNodePagePosition(element);241const overlappingOverlays: IBrowserOverlayInfo[] = [];242243// Check against all precomputed overlay rectangles244for (const overlay of this.overlays()) {245const overlayRect = this.getRect(overlay.element);246if (overlayRect && this.isRectanglesOverlapping(elementRect, overlayRect)) {247overlappingOverlays.push({248type: overlay.type,249rect: overlayRect250});251}252}253254return overlappingOverlays;255}256257private isRectanglesOverlapping(rect1: IDomNodePagePosition, rect2: IDomNodePagePosition): boolean {258// If elements are offscreen or set to zero size, consider them non-overlapping259if (rect1.width === 0 || rect1.height === 0 || rect2.width === 0 || rect2.height === 0) {260return false;261}262263return !(rect1.left + rect1.width <= rect2.left ||264rect2.left + rect2.width <= rect1.left ||265rect1.top + rect1.height <= rect2.top ||266rect2.top + rect2.height <= rect1.top);267}268269private stopTrackingElements(): void {270// Disconnect all element observers271for (const overlay of this.overlays()) {272const observer = this._elementObservers.get(overlay.element);273observer?.disconnect();274}275276// Disconnect all shadow root observers277for (const hostElement of this._shadowRootHostCollection) {278const shadowRoot = (hostElement as HTMLElement).shadowRoot;279const shadowObserver = this._shadowRootObservers.get(shadowRoot!);280shadowObserver?.disconnect();281}282283this._shadowRootObservers = new WeakMap();284this._shadowRootOverlayCache = new WeakMap();285this._overlayRectangles = new WeakMap();286this._elementObservers = new WeakMap();287}288289override dispose(): void {290this._observerIsConnected = false;291this._structuralObserver.disconnect();292this.stopTrackingElements();293294super.dispose();295}296}297298299