Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { IEditorGroup } from '../../../services/editor/common/editorGroupsServic
import { IEditorOptions } from '../../../../platform/editor/common/editor.js';
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';
import { BrowserOverlayManager } from './overlayManager.js';
import { BrowserOverlayManager, BrowserOverlayType, IBrowserOverlayInfo } from './overlayManager.js';
import { getZoomFactor, onDidChangeZoomLevel } from '../../../../base/browser/browser.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';
Expand Down Expand Up @@ -162,6 +162,9 @@ export class BrowserEditor extends EditorPane {
private _navigationBar!: BrowserNavigationBar;
private _browserContainer!: HTMLElement;
private _placeholderScreenshot!: HTMLElement;
private _overlayPauseContainer!: HTMLElement;
private _overlayPauseHeading!: HTMLElement;
private _overlayPauseDetail!: HTMLElement;
private _errorContainer!: HTMLElement;
private _welcomeContainer!: HTMLElement;
private _canGoBackContext!: IContextKey<boolean>;
Expand Down Expand Up @@ -230,6 +233,16 @@ export class BrowserEditor extends EditorPane {
this._placeholderScreenshot = $('.browser-placeholder-screenshot');
this._browserContainer.appendChild(this._placeholderScreenshot);

// Create overlay pause container (hidden by default via CSS)
this._overlayPauseContainer = $('.browser-overlay-paused');
const overlayPauseMessage = $('.browser-overlay-paused-message');
this._overlayPauseHeading = $('.browser-overlay-paused-heading');
this._overlayPauseDetail = $('.browser-overlay-paused-detail');
overlayPauseMessage.appendChild(this._overlayPauseHeading);
overlayPauseMessage.appendChild(this._overlayPauseDetail);
this._overlayPauseContainer.appendChild(overlayPauseMessage);
this._browserContainer.appendChild(this._overlayPauseContainer);

// Create error container (hidden by default)
this._errorContainer = $('.browser-error-container');
this._errorContainer.style.display = 'none';
Expand Down Expand Up @@ -382,17 +395,20 @@ export class BrowserEditor extends EditorPane {
private updateVisibility(): void {
const hasUrl = !!this._model?.url;
const hasError = !!this._model?.error;
const shouldShowPlaceholder = this._editorVisible && this._overlayVisible && !hasError && hasUrl;
const isViewingPage = !hasError && hasUrl;
const isPaused = isViewingPage && this._editorVisible && this._overlayVisible;

// Welcome container: shown when no URL is loaded
this._welcomeContainer.style.display = hasUrl ? 'none' : '';

// Error container: shown when there's a load error
this._errorContainer.style.display = hasError ? '' : 'none';

// Placeholder screenshot: shown when the view is hidden due to overlays
this._placeholderScreenshot.style.display = shouldShowPlaceholder ? '' : 'none';
this._placeholderScreenshot.classList.toggle('blur', shouldShowPlaceholder);
// Placeholder screenshot: shown when there is a page loaded (even when the view is not hidden, so hiding is smooth)
this._placeholderScreenshot.style.display = isViewingPage ? '' : 'none';

// Pause overlay: fades in when an overlay is detected
Comment thread
kycutler marked this conversation as resolved.
this._overlayPauseContainer.classList.toggle('visible', isPaused);

if (this._model) {
// Blur the background placeholder screenshot if the view is hidden due to an overlay.
Expand All @@ -408,13 +424,29 @@ export class BrowserEditor extends EditorPane {
if (!this.overlayManager) {
return;
}
const hasOverlappingOverlay = this.overlayManager.isOverlappingWithOverlays(this._browserContainer);
const overlappingOverlays = this.overlayManager.getOverlappingOverlays(this._browserContainer);
const hasOverlappingOverlay = overlappingOverlays.length > 0;
this.updateOverlayPauseMessage(overlappingOverlays);
if (hasOverlappingOverlay !== this._overlayVisible) {
this._overlayVisible = hasOverlappingOverlay;
this.updateVisibility();
}
}

private updateOverlayPauseMessage(overlappingOverlays: readonly IBrowserOverlayInfo[]): void {
// Only show the pause message for notification overlays
Comment thread
kycutler marked this conversation as resolved.
const hasNotificationOverlay = overlappingOverlays.some(overlay => overlay.type === BrowserOverlayType.Notification);
this._overlayPauseContainer.classList.toggle('show-message', hasNotificationOverlay);

if (hasNotificationOverlay) {
this._overlayPauseHeading.textContent = localize('browser.overlayPauseHeading.notification', "Paused due to Notification");
this._overlayPauseDetail.textContent = localize('browser.overlayPauseDetail.notification', "Dismiss the notification to continue using the browser.");
} else {
this._overlayPauseHeading.textContent = '';
this._overlayPauseDetail.textContent = '';
}
}

private updateErrorDisplay(): void {
if (!this._model) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,56 @@
background-image: none;
background-size: contain;
background-repeat: no-repeat;
filter: blur(0px);
transition: opacity 300ms ease-out, filter 300ms ease-out;
opacity: 1.0;
}

&.blur {
opacity: 0.8;
filter: blur(2px);
.browser-overlay-paused {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
color: var(--vscode-foreground);
background-color: color-mix(in srgb, var(--vscode-editor-background) 60%, transparent);
opacity: 0;
visibility: hidden;
transition: opacity 200ms ease-out;

&.visible {
opacity: 1;
visibility: visible;
}
}

.browser-overlay-paused-message {
padding: 20px 40px;
border-radius: 4px;
border: 1px solid var(--vscode-editorWidget-border);
background-color: var(--vscode-editor-background);
box-shadow: 0 2px 8px var(--vscode-widget-shadow);
max-width: 80%;
text-align: center;
display: none;
}

.browser-overlay-paused.show-message .browser-overlay-paused-message {
display: block;
}

.browser-overlay-paused-heading {
font-size: 18px;
font-weight: 600;
margin-bottom: 8px;
}

.browser-overlay-paused-detail {
font-size: 14px;
color: var(--vscode-descriptionForeground);
}

.browser-error-container {
position: absolute;
top: 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,38 @@
*--------------------------------------------------------------------------------------------*/

import { Disposable } from '../../../../base/common/lifecycle.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { Event, MicrotaskEmitter } from '../../../../base/common/event.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { getDomNodePagePosition, IDomNodePagePosition } from '../../../../base/browser/dom.js';
import { CodeWindow } from '../../../../base/browser/window.js';

const OVERLAY_CLASSES: string[] = [
'monaco-menu-container',
'quick-input-widget',
'monaco-hover',
'monaco-dialog-modal-block',
'notifications-center',
'notification-toast-container',
'context-view'
export enum BrowserOverlayType {
Menu = 'menu',
QuickInput = 'quickInput',
Hover = 'hover',
Dialog = 'dialog',
Notification = 'notification',
Unknown = 'unknown'
}

const OVERLAY_DEFINITIONS: ReadonlyArray<{ className: string; type: BrowserOverlayType }> = [
{ className: 'monaco-menu-container', type: BrowserOverlayType.Menu },
{ className: 'quick-input-widget', type: BrowserOverlayType.QuickInput },
{ className: 'monaco-hover', type: BrowserOverlayType.Hover },
{ className: 'monaco-dialog-modal-block', type: BrowserOverlayType.Dialog },
{ className: 'notifications-center', type: BrowserOverlayType.Notification },
{ className: 'notification-toast-container', type: BrowserOverlayType.Notification },
// Context view is very generic, so treat the content as unknown
{ className: 'context-view', type: BrowserOverlayType.Unknown }
];

export const IBrowserOverlayManager = createDecorator<IBrowserOverlayManager>('browserOverlayManager');

export interface IBrowserOverlayInfo {
type: BrowserOverlayType;
rect: IDomNodePagePosition;
}

export interface IBrowserOverlayManager {
readonly _serviceBrand: undefined;

Expand All @@ -30,15 +45,15 @@ export interface IBrowserOverlayManager {
readonly onDidChangeOverlayState: Event<void>;

/**
* Check if the given element overlaps with any overlay
* Get overlays overlapping with the given element
*/
isOverlappingWithOverlays(element: HTMLElement): boolean;
getOverlappingOverlays(element: HTMLElement): IBrowserOverlayInfo[];
}

export class BrowserOverlayManager extends Disposable implements IBrowserOverlayManager {
declare readonly _serviceBrand: undefined;

private readonly _onDidChangeOverlayState = this._register(new Emitter<void>({
private readonly _onDidChangeOverlayState = this._register(new MicrotaskEmitter<void>({
onWillAddFirstListener: () => {
// Start observing the document for structural changes
this._observerIsConnected = true;
Expand All @@ -53,11 +68,14 @@ export class BrowserOverlayManager extends Disposable implements IBrowserOverlay
this._observerIsConnected = false;
this._structuralObserver.disconnect();
this.stopTrackingElements();
}
},

// Must be passed to prevent duplicate emits
merge: () => { }
}));
readonly onDidChangeOverlayState = this._onDidChangeOverlayState.event;

private readonly _overlayCollections = new Map<string, HTMLCollectionOf<Element>>();
private readonly _overlayCollections = new Map<string, { type: BrowserOverlayType; collection: HTMLCollectionOf<Element> }>();
private _overlayRectangles = new WeakMap<HTMLElement, IDomNodePagePosition>();
private _elementObservers = new WeakMap<HTMLElement, MutationObserver>();
private _structuralObserver: MutationObserver;
Expand All @@ -69,10 +87,13 @@ export class BrowserOverlayManager extends Disposable implements IBrowserOverlay
super();

// Initialize live collections for each overlay selector
for (const className of OVERLAY_CLASSES) {
// We need dynamic collections for overlay detection, using getElementsByClassName is intentional here
// eslint-disable-next-line no-restricted-syntax
this._overlayCollections.set(className, this.targetWindow.document.getElementsByClassName(className));
for (const overlayDefinition of OVERLAY_DEFINITIONS) {
this._overlayCollections.set(overlayDefinition.className, {
type: overlayDefinition.type,
// We need dynamic collections for overlay detection, using getElementsByClassName is intentional here
// eslint-disable-next-line no-restricted-syntax
collection: this.targetWindow.document.getElementsByClassName(overlayDefinition.className)
});
}

// Setup structural observer to watch for element additions/removals
Expand All @@ -96,10 +117,10 @@ export class BrowserOverlayManager extends Disposable implements IBrowserOverlay
});
}

private *overlays(): Iterable<HTMLElement> {
for (const collection of this._overlayCollections.values()) {
for (const element of collection) {
yield element as HTMLElement;
private *overlays(): Iterable<{ element: HTMLElement; type: BrowserOverlayType }> {
for (const entry of this._overlayCollections.values()) {
for (const element of entry.collection) {
yield { element: element as HTMLElement, type: entry.type };
}
}
}
Expand All @@ -108,17 +129,17 @@ export class BrowserOverlayManager extends Disposable implements IBrowserOverlay
// Scan all overlay collections for elements and ensure they have observers
for (const overlay of this.overlays()) {
// Create a new observer for this specific element if we don't already have one
if (!this._elementObservers.has(overlay)) {
if (!this._elementObservers.has(overlay.element)) {
const observer = new MutationObserver(() => {
this._overlayRectangles.delete(overlay);
this._overlayRectangles.delete(overlay.element);
this._onDidChangeOverlayState.fire();
});

// Store the observer in the WeakMap
this._elementObservers.set(overlay, observer);
this._elementObservers.set(overlay.element, observer);

// Start observing this element
observer.observe(overlay, {
observer.observe(overlay.element, {
attributes: true,
attributeFilter: ['style', 'class'],
childList: true,
Expand Down Expand Up @@ -146,18 +167,22 @@ export class BrowserOverlayManager extends Disposable implements IBrowserOverlay
return this._overlayRectangles.get(element)!;
}

isOverlappingWithOverlays(element: HTMLElement): boolean {
getOverlappingOverlays(element: HTMLElement): IBrowserOverlayInfo[] {
const elementRect = getDomNodePagePosition(element);
const overlappingOverlays: IBrowserOverlayInfo[] = [];

// Check against all precomputed overlay rectangles
for (const overlay of this.overlays()) {
const overlayRect = this.getRect(overlay);
const overlayRect = this.getRect(overlay.element);
if (overlayRect && this.isRectanglesOverlapping(elementRect, overlayRect)) {
return true;
overlappingOverlays.push({
type: overlay.type,
rect: overlayRect
});
}
}

return false;
return overlappingOverlays;
}

private isRectanglesOverlapping(rect1: IDomNodePagePosition, rect2: IDomNodePagePosition): boolean {
Expand All @@ -174,7 +199,7 @@ export class BrowserOverlayManager extends Disposable implements IBrowserOverlay

private stopTrackingElements(): void {
for (const overlay of this.overlays()) {
const observer = this._elementObservers.get(overlay);
const observer = this._elementObservers.get(overlay.element);
observer?.disconnect();
}
this._overlayRectangles = new WeakMap();
Expand Down
Loading