diff --git a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts index 8c199336..62fe71a7 100644 --- a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts @@ -59,7 +59,7 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer ) { super(); this._cellColorResolver = new CellColorResolver(this._terminal, this._selectionModel, this._decorationService, this._coreBrowserService, this._themeService); - this._canvas = document.createElement('canvas'); + this._canvas = this._coreBrowserService.mainDocument.createElement('canvas'); this._canvas.classList.add(`xterm-${id}-layer`); this._canvas.style.zIndex = zIndex.toString(); this._initCanvas(); diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 8cc81b92..51749984 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -88,7 +88,7 @@ export class WebglRenderer extends Disposable implements IRenderer { this._updateCursorBlink(); this.register(_optionsService.onOptionChange(() => this._handleOptionsChanged())); - this._canvas = document.createElement('canvas'); + this._canvas = this._coreBrowserService.mainDocument.createElement('canvas'); const contextAttributes = { antialias: false, diff --git a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts index 3aaac435..11f96ed8 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts @@ -38,7 +38,7 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer protected readonly _themeService: IThemeService ) { super(); - this._canvas = document.createElement('canvas'); + this._canvas = this._coreBrowserService.mainDocument.createElement('canvas'); this._canvas.classList.add(`xterm-${id}-layer`); this._canvas.style.zIndex = zIndex.toString(); this._initCanvas(); diff --git a/src/browser/AccessibilityManager.ts b/src/browser/AccessibilityManager.ts index c878bd0a..5f529589 100644 --- a/src/browser/AccessibilityManager.ts +++ b/src/browser/AccessibilityManager.ts @@ -7,10 +7,9 @@ import * as Strings from 'browser/LocalizableStrings'; import { ITerminal, IRenderDebouncer } from 'browser/Types'; import { TimeBasedDebouncer } from 'browser/TimeBasedDebouncer'; import { Disposable, toDisposable } from 'common/Lifecycle'; -import { ScreenDprMonitor } from 'browser/ScreenDprMonitor'; -import { IRenderService } from 'browser/services/Services'; -import { addDisposableDomListener } from 'browser/Lifecycle'; +import { ICoreBrowserService, IRenderService } from 'browser/services/Services'; import { IBuffer } from 'common/buffer/Types'; +import { IInstantiationService } from 'common/services/Services'; const MAX_ROWS_TO_READ = 20; @@ -29,8 +28,6 @@ export class AccessibilityManager extends Disposable { private _liveRegionLineCount: number = 0; private _liveRegionDebouncer: IRenderDebouncer; - private _screenDprMonitor: ScreenDprMonitor; - private _topBoundaryFocusListener: (e: FocusEvent) => void; private _bottomBoundaryFocusListener: (e: FocusEvent) => void; @@ -49,13 +46,15 @@ export class AccessibilityManager extends Disposable { constructor( private readonly _terminal: ITerminal, + @IInstantiationService instantiationService: IInstantiationService, + @ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService, @IRenderService private readonly _renderService: IRenderService ) { super(); - this._accessibilityContainer = document.createElement('div'); + this._accessibilityContainer = this._coreBrowserService.mainDocument.createElement('div'); this._accessibilityContainer.classList.add('xterm-accessibility'); - this._rowContainer = document.createElement('div'); + this._rowContainer = this._coreBrowserService.mainDocument.createElement('div'); this._rowContainer.setAttribute('role', 'list'); this._rowContainer.classList.add('xterm-accessibility-tree'); this._rowElements = []; @@ -72,7 +71,7 @@ export class AccessibilityManager extends Disposable { this._refreshRowsDimensions(); this._accessibilityContainer.appendChild(this._rowContainer); - this._liveRegion = document.createElement('div'); + this._liveRegion = this._coreBrowserService.mainDocument.createElement('div'); this._liveRegion.classList.add('live-region'); this._liveRegion.setAttribute('aria-live', 'assertive'); this._accessibilityContainer.appendChild(this._liveRegion); @@ -93,13 +92,7 @@ export class AccessibilityManager extends Disposable { this.register(this._terminal.onKey(e => this._handleKey(e.key))); this.register(this._terminal.onBlur(() => this._clearLiveRegion())); this.register(this._renderService.onDimensionsChange(() => this._refreshRowsDimensions())); - - this._screenDprMonitor = new ScreenDprMonitor(window); - this.register(this._screenDprMonitor); - this._screenDprMonitor.setListener(() => this._refreshRowsDimensions()); - // This shouldn't be needed on modern browsers but is present in case the - // media query that drives the ScreenDprMonitor isn't supported - this.register(addDisposableDomListener(window, 'resize', () => this._refreshRowsDimensions())); + this.register(this._coreBrowserService.onDprChange(() => this._refreshRowsDimensions())); this._refreshRows(); this.register(toDisposable(() => { @@ -261,7 +254,7 @@ export class AccessibilityManager extends Disposable { } private _createAccessibilityTreeNode(): HTMLElement { - const element = document.createElement('div'); + const element = this._coreBrowserService.mainDocument.createElement('div'); element.setAttribute('role', 'listitem'); element.tabIndex = -1; this._refreshRowDimensions(element); diff --git a/src/browser/ScreenDprMonitor.ts b/src/browser/ScreenDprMonitor.ts deleted file mode 100644 index 1c3f31b7..00000000 --- a/src/browser/ScreenDprMonitor.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { Disposable, toDisposable } from 'common/Lifecycle'; - -export type ScreenDprListener = (newDevicePixelRatio?: number, oldDevicePixelRatio?: number) => void; - -/** - * The screen device pixel ratio monitor allows listening for when the - * window.devicePixelRatio value changes. This is done not with polling but with - * the use of window.matchMedia to watch media queries. When the event fires, - * the listener will be reattached using a different media query to ensure that - * any further changes will register. - * - * The listener should fire on both window zoom changes and switching to a - * monitor with a different DPI. - */ -export class ScreenDprMonitor extends Disposable { - private _currentDevicePixelRatio: number; - private _outerListener: ((this: MediaQueryList, ev: MediaQueryListEvent) => any) | undefined; - private _listener: ScreenDprListener | undefined; - private _resolutionMediaMatchList: MediaQueryList | undefined; - - constructor(private _parentWindow: Window) { - super(); - this._currentDevicePixelRatio = this._parentWindow.devicePixelRatio; - this.register(toDisposable(() => { - this.clearListener(); - })); - } - - public setListener(listener: ScreenDprListener): void { - if (this._listener) { - this.clearListener(); - } - this._listener = listener; - this._outerListener = () => { - if (!this._listener) { - return; - } - this._listener(this._parentWindow.devicePixelRatio, this._currentDevicePixelRatio); - this._updateDpr(); - }; - this._updateDpr(); - } - - private _updateDpr(): void { - if (!this._outerListener) { - return; - } - - // Clear listeners for old DPR - this._resolutionMediaMatchList?.removeListener(this._outerListener); - - // Add listeners for new DPR - this._currentDevicePixelRatio = this._parentWindow.devicePixelRatio; - this._resolutionMediaMatchList = this._parentWindow.matchMedia(`screen and (resolution: ${this._parentWindow.devicePixelRatio}dppx)`); - this._resolutionMediaMatchList.addListener(this._outerListener); - } - - public clearListener(): void { - if (!this._resolutionMediaMatchList || !this._listener || !this._outerListener) { - return; - } - this._resolutionMediaMatchList.removeListener(this._outerListener); - this._resolutionMediaMatchList = undefined; - this._listener = undefined; - this._outerListener = undefined; - } -} diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index 12963b4c..281bebd5 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -58,9 +58,6 @@ import { IDecoration, IDecorationOptions, IDisposable, ILinkProvider, IMarker, I import { WindowsOptionsReportType } from '../common/InputHandler'; import { AccessibilityManager } from './AccessibilityManager'; -// Let it work inside Node.js for automated testing purposes. -const document: Document = (typeof window !== 'undefined') ? window.document : null as any; - export class Terminal extends CoreTerminal implements ITerminal { public textarea: HTMLTextAreaElement | undefined; public element: HTMLElement | undefined; @@ -399,7 +396,19 @@ export class Terminal extends CoreTerminal implements ITerminal { this._logService.debug('Terminal.open was called on an element that was not attached to the DOM'); } - this._document = parent.ownerDocument!; + // If the terminal is already opened + if (this.element?.ownerDocument.defaultView && this._coreBrowserService) { + // Adjust the window if needed + if (this.element.ownerDocument.defaultView !== this._coreBrowserService.window) { + this._coreBrowserService.window = this.element.ownerDocument.defaultView; + } + return; + } + + this._document = parent.ownerDocument; + if (this.options.documentOverride && this.options.documentOverride instanceof Document) { + this._document = this.optionsService.rawOptions.documentOverride as Document; + } // Create main element container this.element = this._document.createElement('div'); @@ -410,25 +419,25 @@ export class Terminal extends CoreTerminal implements ITerminal { // Performance: Use a document fragment to build the terminal // viewport and helper elements detached from the DOM - const fragment = document.createDocumentFragment(); - this._viewportElement = document.createElement('div'); + const fragment = this._document.createDocumentFragment(); + this._viewportElement = this._document.createElement('div'); this._viewportElement.classList.add('xterm-viewport'); fragment.appendChild(this._viewportElement); - this._viewportScrollArea = document.createElement('div'); + this._viewportScrollArea = this._document.createElement('div'); this._viewportScrollArea.classList.add('xterm-scroll-area'); this._viewportElement.appendChild(this._viewportScrollArea); - this.screenElement = document.createElement('div'); + this.screenElement = this._document.createElement('div'); this.screenElement.classList.add('xterm-screen'); // Create the container that will hold helpers like the textarea for // capturing DOM Events. Then produce the helpers. - this._helperContainer = document.createElement('div'); + this._helperContainer = this._document.createElement('div'); this._helperContainer.classList.add('xterm-helpers'); this.screenElement.appendChild(this._helperContainer); fragment.appendChild(this.screenElement); - this.textarea = document.createElement('textarea'); + this.textarea = this._document.createElement('textarea'); this.textarea.classList.add('xterm-helper-textarea'); this.textarea.setAttribute('aria-label', Strings.promptLabel); if (!Browser.isChromeOS) { @@ -443,7 +452,12 @@ export class Terminal extends CoreTerminal implements ITerminal { // Register the core browser service before the generic textarea handlers are registered so it // handles them first. Otherwise the renderers may use the wrong focus state. - this._coreBrowserService = this._instantiationService.createInstance(CoreBrowserService, this.textarea, this._document.defaultView ?? window); + this._coreBrowserService = this.register(this._instantiationService.createInstance(CoreBrowserService, + this.textarea, + parent.ownerDocument.defaultView ?? window, + // Force unsafe null in node.js environment for tests + this._document ?? (typeof window !== 'undefined') ? window.document : null as any + )); this._instantiationService.setService(ICoreBrowserService, this._coreBrowserService); this.register(addDisposableDomListener(this.textarea, 'focus', (ev: KeyboardEvent) => this._handleTextAreaFocus(ev))); @@ -465,7 +479,7 @@ export class Terminal extends CoreTerminal implements ITerminal { this.register(this._renderService.onRenderedViewportChange(e => this._onRender.fire(e))); this.onResize(e => this._renderService!.resize(e.cols, e.rows)); - this._compositionView = document.createElement('div'); + this._compositionView = this._document.createElement('div'); this._compositionView.classList.add('composition-view'); this._compositionHelper = this._instantiationService.createInstance(CompositionHelper, this.textarea, this._compositionView); this._helperContainer.appendChild(this._compositionView); diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index e38017e1..3b3f39f6 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -353,11 +353,16 @@ export class MockCompositionHelper implements ICompositionHelper { } export class MockCoreBrowserService implements ICoreBrowserService { + public onDprChange = new EventEmitter().event; + public onWindowChange = new EventEmitter().event; public serviceBrand: undefined; public isFocused: boolean = true; public get window(): Window & typeof globalThis { throw Error('Window object not available in tests'); } + public get mainDocument(): Document { + throw Error('Document object not available in tests'); + } public dpr: number = 1; } diff --git a/src/browser/decorations/BufferDecorationRenderer.ts b/src/browser/decorations/BufferDecorationRenderer.ts index ba4f6ca8..6ba9ae1e 100644 --- a/src/browser/decorations/BufferDecorationRenderer.ts +++ b/src/browser/decorations/BufferDecorationRenderer.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { addDisposableDomListener } from 'browser/Lifecycle'; -import { IRenderService } from 'browser/services/Services'; +import { ICoreBrowserService, IRenderService } from 'browser/services/Services'; import { Disposable, toDisposable } from 'common/Lifecycle'; import { IBufferService, IDecorationService, IInternalDecoration } from 'common/services/Services'; @@ -19,6 +18,7 @@ export class BufferDecorationRenderer extends Disposable { constructor( private readonly _screenElement: HTMLElement, @IBufferService private readonly _bufferService: IBufferService, + @ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService, @IDecorationService private readonly _decorationService: IDecorationService, @IRenderService private readonly _renderService: IRenderService ) { @@ -33,7 +33,7 @@ export class BufferDecorationRenderer extends Disposable { this._dimensionsChanged = true; this._queueRefresh(); })); - this.register(addDisposableDomListener(window, 'resize', () => this._queueRefresh())); + this.register(this._coreBrowserService.onDprChange(() => this._queueRefresh())); this.register(this._bufferService.buffers.onBufferActivate(() => { this._altBufferIsActive = this._bufferService.buffer === this._bufferService.buffers.alt; })); @@ -70,7 +70,7 @@ export class BufferDecorationRenderer extends Disposable { } private _createElement(decoration: IInternalDecoration): HTMLElement { - const element = document.createElement('div'); + const element = this._coreBrowserService.mainDocument.createElement('div'); element.classList.add('xterm-decoration'); element.classList.toggle('xterm-decoration-top-layer', decoration?.options?.layer === 'top'); element.style.width = `${Math.round((decoration.options.width || 1) * this._renderService.dimensions.css.cell.width)}px`; diff --git a/src/browser/decorations/OverviewRulerRenderer.ts b/src/browser/decorations/OverviewRulerRenderer.ts index d5765385..103d5d9a 100644 --- a/src/browser/decorations/OverviewRulerRenderer.ts +++ b/src/browser/decorations/OverviewRulerRenderer.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { ColorZoneStore, IColorZone, IColorZoneStore } from 'browser/decorations/ColorZoneStore'; -import { addDisposableDomListener } from 'browser/Lifecycle'; import { ICoreBrowserService, IRenderService } from 'browser/services/Services'; import { Disposable, toDisposable } from 'common/Lifecycle'; import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; @@ -52,10 +51,10 @@ export class OverviewRulerRenderer extends Disposable { @IDecorationService private readonly _decorationService: IDecorationService, @IRenderService private readonly _renderService: IRenderService, @IOptionsService private readonly _optionsService: IOptionsService, - @ICoreBrowserService private readonly _coreBrowseService: ICoreBrowserService + @ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService ) { super(); - this._canvas = document.createElement('canvas'); + this._canvas = this._coreBrowserService.mainDocument.createElement('canvas'); this._canvas.classList.add('xterm-decoration-overview-ruler'); this._refreshCanvasDimensions(); this._viewportElement.parentElement?.insertBefore(this._canvas, this._viewportElement); @@ -112,7 +111,7 @@ export class OverviewRulerRenderer extends Disposable { // overview ruler width changed this.register(this._optionsService.onSpecificOptionChange('overviewRulerWidth', () => this._queueRefresh(true))); // device pixel ratio changed - this.register(addDisposableDomListener(this._coreBrowseService.window, 'resize', () => this._queueRefresh(true))); + this.register(this._coreBrowserService.onDprChange(() => this._queueRefresh(true))); // set the canvas dimensions this._queueRefresh(true); } @@ -135,11 +134,11 @@ export class OverviewRulerRenderer extends Disposable { } private _refreshDrawHeightConstants(): void { - drawHeight.full = Math.round(2 * this._coreBrowseService.dpr); + drawHeight.full = Math.round(2 * this._coreBrowserService.dpr); // Calculate actual pixels per line const pixelsPerLine = this._canvas.height / this._bufferService.buffer.lines.length; // Clamp actual pixels within a range - const nonFullHeight = Math.round(Math.max(Math.min(pixelsPerLine, 12), 6) * this._coreBrowseService.dpr); + const nonFullHeight = Math.round(Math.max(Math.min(pixelsPerLine, 12), 6) * this._coreBrowserService.dpr); drawHeight.left = nonFullHeight; drawHeight.center = nonFullHeight; drawHeight.right = nonFullHeight; @@ -157,9 +156,9 @@ export class OverviewRulerRenderer extends Disposable { private _refreshCanvasDimensions(): void { this._canvas.style.width = `${this._width}px`; - this._canvas.width = Math.round(this._width * this._coreBrowseService.dpr); + this._canvas.width = Math.round(this._width * this._coreBrowserService.dpr); this._canvas.style.height = `${this._screenElement.clientHeight}px`; - this._canvas.height = Math.round(this._screenElement.clientHeight * this._coreBrowseService.dpr); + this._canvas.height = Math.round(this._screenElement.clientHeight * this._coreBrowserService.dpr); this._refreshDrawConstants(); this._refreshColorZonePadding(); } @@ -211,7 +210,7 @@ export class OverviewRulerRenderer extends Disposable { if (this._animationFrame !== undefined) { return; } - this._animationFrame = this._coreBrowseService.window.requestAnimationFrame(() => { + this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => { this._refreshDecorations(); this._animationFrame = undefined; }); diff --git a/src/browser/renderer/shared/CustomGlyphs.ts b/src/browser/renderer/shared/CustomGlyphs.ts index c08bc4b1..cf6292af 100644 --- a/src/browser/renderer/shared/CustomGlyphs.ts +++ b/src/browser/renderer/shared/CustomGlyphs.ts @@ -474,7 +474,7 @@ function drawPatternChar( if (!pattern) { const width = charDefinition[0].length; const height = charDefinition.length; - const tmpCanvas = document.createElement('canvas'); + const tmpCanvas = ctx.canvas.ownerDocument.createElement('canvas'); tmpCanvas.width = width; tmpCanvas.height = height; const tmpCtx = throwIfFalsy(tmpCanvas.getContext('2d')); diff --git a/src/browser/services/CoreBrowserService.ts b/src/browser/services/CoreBrowserService.ts index e992f255..575b62b6 100644 --- a/src/browser/services/CoreBrowserService.ts +++ b/src/browser/services/CoreBrowserService.ts @@ -3,22 +3,49 @@ * @license MIT */ +import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle'; import { ICoreBrowserService } from './Services'; +import { EventEmitter, forwardEvent } from 'common/EventEmitter'; +import { addDisposableDomListener } from 'browser/Lifecycle'; -export class CoreBrowserService implements ICoreBrowserService { +export class CoreBrowserService extends Disposable implements ICoreBrowserService { public serviceBrand: undefined; private _isFocused = false; private _cachedIsFocused: boolean | undefined = undefined; + private _screenDprMonitor = new ScreenDprMonitor(this._window); + + private readonly _onDprChange = this.register(new EventEmitter()); + public readonly onDprChange = this._onDprChange.event; + private readonly _onWindowChange = this.register(new EventEmitter()); + public readonly onWindowChange = this._onWindowChange.event; constructor( private _textarea: HTMLTextAreaElement, - public readonly window: Window & typeof globalThis + private _window: Window & typeof globalThis, + public readonly mainDocument: Document ) { + super(); + + // Monitor device pixel ratio + this.register(this.onWindowChange(w => this._screenDprMonitor.setWindow(w))); + this.register(forwardEvent(this._screenDprMonitor.onDprChange, this._onDprChange)); + this._textarea.addEventListener('focus', () => this._isFocused = true); this._textarea.addEventListener('blur', () => this._isFocused = false); } + public get window(): Window & typeof globalThis { + return this._window; + } + + public set window(value: Window & typeof globalThis) { + if (this._window !== value) { + this._window = value; + this._onWindowChange.fire(this._window); + } + } + public get dpr(): number { return this.window.devicePixelRatio; } @@ -31,3 +58,80 @@ export class CoreBrowserService implements ICoreBrowserService { return this._cachedIsFocused; } } + + +/** + * The screen device pixel ratio monitor allows listening for when the + * window.devicePixelRatio value changes. This is done not with polling but with + * the use of window.matchMedia to watch media queries. When the event fires, + * the listener will be reattached using a different media query to ensure that + * any further changes will register. + * + * The listener should fire on both window zoom changes and switching to a + * monitor with a different DPI. + */ +class ScreenDprMonitor extends Disposable { + private _currentDevicePixelRatio: number; + private _outerListener: ((this: MediaQueryList, ev: MediaQueryListEvent) => any) | undefined; + private _resolutionMediaMatchList: MediaQueryList | undefined; + private _windowResizeListener = this.register(new MutableDisposable()); + + private readonly _onDprChange = this.register(new EventEmitter()); + public readonly onDprChange = this._onDprChange.event; + + constructor(private _parentWindow: Window) { + super(); + + // Initialize listener and dpr value + this._outerListener = () => this._setDprAndFireIfDiffers(); + this._currentDevicePixelRatio = this._parentWindow.devicePixelRatio; + this._updateDpr(); + + // Monitor active window resize + this._setWindowResizeListener(); + + // Setup additional disposables + this.register(toDisposable(() => this.clearListener())); + } + + + public setWindow(parentWindow: Window): void { + this._parentWindow = parentWindow; + this._setWindowResizeListener(); + this._setDprAndFireIfDiffers(); + } + + private _setWindowResizeListener(): void { + this._windowResizeListener.value = addDisposableDomListener(this._parentWindow, 'resize', () => this._setDprAndFireIfDiffers()); + } + + private _setDprAndFireIfDiffers(): void { + if (this._parentWindow.devicePixelRatio !== this._currentDevicePixelRatio) { + this._onDprChange.fire(this._parentWindow.devicePixelRatio); + } + this._updateDpr(); + } + + private _updateDpr(): void { + if (!this._outerListener) { + return; + } + + // Clear listeners for old DPR + this._resolutionMediaMatchList?.removeListener(this._outerListener); + + // Add listeners for new DPR + this._currentDevicePixelRatio = this._parentWindow.devicePixelRatio; + this._resolutionMediaMatchList = this._parentWindow.matchMedia(`screen and (resolution: ${this._parentWindow.devicePixelRatio}dppx)`); + this._resolutionMediaMatchList.addListener(this._outerListener); + } + + public clearListener(): void { + if (!this._resolutionMediaMatchList || !this._outerListener) { + return; + } + this._resolutionMediaMatchList.removeListener(this._outerListener); + this._resolutionMediaMatchList = undefined; + this._outerListener = undefined; + } +} diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index e6d259c2..9fa8d234 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -3,16 +3,14 @@ * @license MIT */ -import { addDisposableDomListener } from 'browser/Lifecycle'; import { RenderDebouncer } from 'browser/RenderDebouncer'; -import { ScreenDprMonitor } from 'browser/ScreenDprMonitor'; import { IRenderDebouncerWithCallback } from 'browser/Types'; import { IRenderDimensions, IRenderer } from 'browser/renderer/shared/Types'; import { ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services'; import { EventEmitter } from 'common/EventEmitter'; import { Disposable, MutableDisposable } from 'common/Lifecycle'; import { DebouncedIdleTask } from 'common/TaskQueue'; -import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; +import { IBufferService, IDecorationService, IInstantiationService, IOptionsService } from 'common/services/Services'; interface ISelectionState { start: [number, number] | undefined; @@ -25,7 +23,6 @@ export class RenderService extends Disposable implements IRenderService { private _renderer: MutableDisposable = this.register(new MutableDisposable()); private _renderDebouncer: IRenderDebouncerWithCallback; - private _screenDprMonitor: ScreenDprMonitor; private _pausedResizeTask = new DebouncedIdleTask(); private _isPaused: boolean = false; @@ -59,6 +56,7 @@ export class RenderService extends Disposable implements IRenderService { @IDecorationService decorationService: IDecorationService, @IBufferService bufferService: IBufferService, @ICoreBrowserService coreBrowserService: ICoreBrowserService, + @IInstantiationService instantiationService: IInstantiationService, @IThemeService themeService: IThemeService ) { super(); @@ -66,9 +64,7 @@ export class RenderService extends Disposable implements IRenderService { this._renderDebouncer = new RenderDebouncer(coreBrowserService.window, (start, end) => this._renderRows(start, end)); this.register(this._renderDebouncer); - this._screenDprMonitor = new ScreenDprMonitor(coreBrowserService.window); - this._screenDprMonitor.setListener(() => this.handleDevicePixelRatioChange()); - this.register(this._screenDprMonitor); + this.register(coreBrowserService.onDprChange(() => this.handleDevicePixelRatioChange())); this.register(bufferService.onResize(() => this._fullRefresh())); this.register(bufferService.buffers.onBufferActivate(() => this._renderer.value?.clear())); @@ -104,10 +100,6 @@ export class RenderService extends Disposable implements IRenderService { 'cursorStyle' ], () => this.refreshRows(bufferService.buffer.y, bufferService.buffer.y, true))); - // dprchange should handle this case, we need this as well for browsers that don't support the - // matchMedia query. - this.register(addDisposableDomListener(coreBrowserService.window, 'resize', () => this.handleDevicePixelRatioChange())); - this.register(themeService.onChangeColors(() => this._fullRefresh())); // Detect whether IntersectionObserver is detected and enable renderer pause diff --git a/src/browser/services/Services.ts b/src/browser/services/Services.ts index d96285f7..5c14fa8a 100644 --- a/src/browser/services/Services.ts +++ b/src/browser/services/Services.ts @@ -28,12 +28,21 @@ export interface ICoreBrowserService { serviceBrand: undefined; readonly isFocused: boolean; + + readonly onDprChange: IEvent; + readonly onWindowChange: IEvent; + /** - * Parent window that the terminal is rendered into. DOM and rendering APIs - * (e.g. requestAnimationFrame) should be invoked in the context of this - * window. + * Gets or sets the parent window that the terminal is rendered into. DOM and rendering APIs (e.g. + * requestAnimationFrame) should be invoked in the context of this window. This should be set when + * the window hosting the xterm.js instance changes. */ - readonly window: Window & typeof globalThis; + window: Window & typeof globalThis; + /** + * The document of the primary window to be used to create elements when working with multiple + * windows. This is defined by the documentOverride setting. + */ + readonly mainDocument: Document; /** * Helper for getting the devicePixelRatio of the parent window. */ diff --git a/src/common/Color.ts b/src/common/Color.ts index 108d72f3..9bfed4e6 100644 --- a/src/common/Color.ts +++ b/src/common/Color.ts @@ -113,6 +113,7 @@ export namespace css { let $ctx: CanvasRenderingContext2D | undefined; let $litmusColor: CanvasGradient | undefined; if (!isNode) { + // This is guaranteed to run in the first window, so document should be correct const canvas = document.createElement('canvas'); canvas.width = 1; canvas.height = 1; diff --git a/src/common/EventEmitter.ts b/src/common/EventEmitter.ts index fd959042..589748a3 100644 --- a/src/common/EventEmitter.ts +++ b/src/common/EventEmitter.ts @@ -71,3 +71,8 @@ export class EventEmitter implements IEventEmitter { export function forwardEvent(from: IEvent, to: IEventEmitter): IDisposable { return from(e => to.fire(e)); } + +export function runAndSubscribe(event: IEvent, handler: (e: T | undefined) => any): IDisposable { + handler(undefined); + return event(e => handler(e)); +} diff --git a/src/common/Platform.ts b/src/common/Platform.ts index 41d8552e..1007fc0a 100644 --- a/src/common/Platform.ts +++ b/src/common/Platform.ts @@ -12,8 +12,9 @@ interface INavigator { // We're declaring a navigator global here as we expect it in all runtimes (node and browser), but // we want this module to live in common. declare const navigator: INavigator; +declare const process: unknown; -export const isNode = (typeof navigator === 'undefined') ? true : false; +export const isNode = (typeof process !== 'undefined') ? true : false; const userAgent = (isNode) ? 'node' : navigator.userAgent; const platform = (isNode) ? 'node' : navigator.platform; diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index 5907d2da..17741596 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -18,6 +18,7 @@ export const DEFAULT_OPTIONS: Readonly> = { cursorInactiveStyle: 'outline', customGlyphs: true, drawBoldTextInBrightColors: true, + documentOverride: null, fastScrollModifier: 'alt', fastScrollSensitivity: 5, fontFamily: 'courier-new, courier, monospace', diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index b68189af..e7108855 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -218,6 +218,7 @@ export interface ITerminalOptions { cursorInactiveStyle?: CursorInactiveStyle; customGlyphs?: boolean; disableStdin?: boolean; + documentOverride?: any | null; drawBoldTextInBrightColors?: boolean; fastScrollModifier?: 'none' | 'alt' | 'ctrl' | 'shift'; fastScrollSensitivity?: number; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index ba5b1630..d93e1d4c 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -95,6 +95,17 @@ declare module 'xterm' { */ disableStdin?: boolean; + /** + * A {@link Document} to use instead of the one that xterm.js was attached + * to. The purpose of this is to improve support in multi-window + * applications where HTML elements may be references across multiple + * windows which can cause problems with `instanceof`. + * + * The type is `any` because using `Document` can cause TS to have + * performance/compiler problems. + */ + documentOverride?: any | null; + /** * Whether to draw bold text in bright colors. The default is true. */ @@ -968,7 +979,8 @@ declare module 'xterm' { resize(columns: number, rows: number): void; /** - * Opens the terminal within an element. + * Opens the terminal within an element. This should also be called if the + * xterm.js element ever changes browser window. * @param parent The element to create the terminal within. This element * must be visible (have dimensions) when `open` is called as several DOM- * based measurements need to be performed when this function is called. diff --git a/yarn.lock b/yarn.lock index ffebb5c5..a0cb39da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15,6 +15,14 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" +"@babel/code-frame@^7.22.13": + version "7.22.13" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" + integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== + dependencies: + "@babel/highlight" "^7.22.13" + chalk "^2.4.2" + "@babel/code-frame@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.5.tgz#234d98e1551960604f1246e6475891a570ad5658" @@ -48,7 +56,7 @@ json5 "^2.2.2" semver "^6.3.1" -"@babel/generator@^7.22.7", "@babel/generator@^7.22.9": +"@babel/generator@^7.22.9": version "7.22.9" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.9.tgz#572ecfa7a31002fa1de2a9d91621fd895da8493d" integrity sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw== @@ -58,6 +66,16 @@ "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" +"@babel/generator@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420" + integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g== + dependencies: + "@babel/types" "^7.23.0" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + "@babel/helper-compilation-targets@^7.22.9": version "7.22.9" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.9.tgz#f9d0a7aaaa7cd32a3f31c9316a69f5a9bcacb892" @@ -69,18 +87,23 @@ lru-cache "^5.1.1" semver "^6.3.1" +"@babel/helper-environment-visitor@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" + integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== + "@babel/helper-environment-visitor@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz#f06dd41b7c1f44e1f8da6c4055b41ab3a09a7e98" integrity sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q== -"@babel/helper-function-name@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz#ede300828905bb15e582c037162f99d5183af1be" - integrity sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ== +"@babel/helper-function-name@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" + integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== dependencies: - "@babel/template" "^7.22.5" - "@babel/types" "^7.22.5" + "@babel/template" "^7.22.15" + "@babel/types" "^7.23.0" "@babel/helper-hoist-variables@^7.22.5": version "7.22.5" @@ -126,6 +149,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== +"@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== + "@babel/helper-validator-identifier@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193" @@ -145,6 +173,15 @@ "@babel/traverse" "^7.22.6" "@babel/types" "^7.22.5" +"@babel/highlight@^7.22.13": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54" + integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg== + dependencies: + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" + js-tokens "^4.0.0" + "@babel/highlight@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.5.tgz#aa6c05c5407a67ebce408162b7ede789b4d22031" @@ -154,6 +191,11 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/parser@^7.22.15", "@babel/parser@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719" + integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw== + "@babel/parser@^7.22.5", "@babel/parser@^7.22.7": version "7.22.7" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.7.tgz#df8cf085ce92ddbdbf668a7f186ce848c9036cae" @@ -166,6 +208,15 @@ dependencies: regenerator-runtime "^0.13.11" +"@babel/template@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" + integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== + dependencies: + "@babel/code-frame" "^7.22.13" + "@babel/parser" "^7.22.15" + "@babel/types" "^7.22.15" + "@babel/template@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec" @@ -176,21 +227,30 @@ "@babel/types" "^7.22.5" "@babel/traverse@^7.22.6", "@babel/traverse@^7.22.8": - version "7.22.8" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.8.tgz#4d4451d31bc34efeae01eac222b514a77aa4000e" - integrity sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw== + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" + integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw== dependencies: - "@babel/code-frame" "^7.22.5" - "@babel/generator" "^7.22.7" - "@babel/helper-environment-visitor" "^7.22.5" - "@babel/helper-function-name" "^7.22.5" + "@babel/code-frame" "^7.22.13" + "@babel/generator" "^7.23.0" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" "@babel/helper-hoist-variables" "^7.22.5" "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/parser" "^7.22.7" - "@babel/types" "^7.22.5" + "@babel/parser" "^7.23.0" + "@babel/types" "^7.23.0" debug "^4.1.0" globals "^11.1.0" +"@babel/types@^7.22.15", "@babel/types@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb" + integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg== + dependencies: + "@babel/helper-string-parser" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.20" + to-fast-properties "^2.0.0" + "@babel/types@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.5.tgz#cd93eeaab025880a3a47ec881f4b096a5b786fbe" @@ -1145,7 +1205,7 @@ chai@^4.3.4: pathval "^1.1.1" type-detect "^4.0.5" -chalk@^2.0.0: +chalk@^2.0.0, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==