From a39a468f19cd1937257f3192edee834539e8eaf1 Mon Sep 17 00:00:00 2001 From: Mihai Parparita Date: Thu, 8 Sep 2022 16:55:08 -0700 Subject: [PATCH] Make terminal rendering work in popout windows Add support for the parent element (what is passed to Terminal.open) being in a different (same origin) window. Accesses to DOM APIs such as requestAnimationFrame and devicePixelRatio thus need to be scoped to the corrent window, instead of assuming that it's the same as the window/global scope where the code is running. This is done by inferring a parent window at the creation time, and then storing it in CoreBrowserService, which is already passed to most places that need it. To catch future regressions, an ESLint rule that checks for global accesses is added (it uses AST selectors via the no-restricted-syntax rule). This should also be applicable when the parent element is an iframe. Fixes #3758 --- .eslintrc.json | 23 ++++++++ .../xterm-addon-canvas/src/BaseRenderLayer.ts | 56 ++++++++++--------- .../xterm-addon-canvas/src/CanvasRenderer.ts | 27 ++++----- .../src/CursorRenderLayer.ts | 35 ++++++------ .../xterm-addon-canvas/src/LinkRenderLayer.ts | 6 +- .../src/SelectionRenderLayer.ts | 4 +- .../xterm-addon-canvas/src/TextRenderLayer.ts | 11 ++-- .../src/atlas/CharAtlasCache.ts | 5 +- .../src/atlas/CharAtlasUtils.ts | 4 +- addons/xterm-addon-webgl/src/WebglRenderer.ts | 16 +++--- .../src/atlas/CharAtlasCache.ts | 5 +- .../src/atlas/CharAtlasUtils.ts | 4 +- .../src/atlas/WebglCharAtlas.ts | 12 ++-- .../src/renderLayer/BaseRenderLayer.ts | 24 ++++---- .../src/renderLayer/CursorRenderLayer.ts | 34 +++++------ .../src/renderLayer/LinkRenderLayer.ts | 11 +++- src/browser/AccessibilityManager.ts | 2 +- src/browser/RenderDebouncer.ts | 7 ++- src/browser/ScreenDprMonitor.ts | 13 +++-- src/browser/Terminal.ts | 2 +- src/browser/TestUtils.test.ts | 4 ++ src/browser/Viewport.ts | 13 +++-- .../decorations/OverviewRulerRenderer.ts | 17 +++--- src/browser/renderer/CustomGlyphs.ts | 25 +++++---- src/browser/renderer/DevicePixelObserver.ts | 4 +- src/browser/renderer/dom/DomRenderer.ts | 14 +++-- src/browser/services/CoreBrowserService.ts | 11 +++- src/browser/services/RenderService.ts | 15 ++--- src/browser/services/SelectionService.test.ts | 4 +- src/browser/services/SelectionService.ts | 13 +++-- src/browser/services/Services.ts | 10 ++++ 31 files changed, 255 insertions(+), 176 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 3a60f5bc77..9510fc6b99 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -147,6 +147,29 @@ ] } ], + "no-restricted-syntax": [ + "warn", + { + "selector": "CallExpression[callee.name='requestAnimationFrame']", + "message": "The global requestAnimationFrame() should be avoided, call it on the parent window from ICoreBrowserService." + }, + { + "selector": "CallExpression[callee.name='cancelAnimationFrame']", + "message": "The global cancelAnimationFrame() should be avoided, call it on the parent window from ICoreBrowserService." + }, + { + "selector": "CallExpression > MemberExpression[object.name='window'][property.name='requestAnimationFrame']", + "message": "window.requestAnimationFrame() should be avoided, call it on the parent window from ICoreBrowserService." + }, + { + "selector": "CallExpression > MemberExpression[object.name='window'][property.name='cancelAnimationFrame']", + "message": "window.cancelAnimationFrame() should be avoided, call it on the parent window from ICoreBrowserService." + }, + { + "selector": "MemberExpression[object.name='window'][property.name='devicePixelRatio']", + "message": "window.devicePixelRatio should be avoided, get it from ICoreBrowserService." + } + ], "no-trailing-spaces": "warn", "no-unsafe-finally": "warn", "no-var": "warn", diff --git a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts index ae39601693..f37ea1b858 100644 --- a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts @@ -15,6 +15,7 @@ import { AttributeData } from 'common/buffer/AttributeData'; import { IColorSet } from 'browser/Types'; import { CellData } from 'common/buffer/CellData'; import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; +import { ICoreBrowserService } from 'browser/services/Services'; import { excludeFromContrastRatioDemands, throwIfFalsy } from 'browser/renderer/RendererUtils'; import { channels, color, rgba } from 'common/Color'; import { removeElementFromParent } from 'browser/Dom'; @@ -60,7 +61,8 @@ export abstract class BaseRenderLayer implements IRenderLayer { private _rendererId: number, protected readonly _bufferService: IBufferService, protected readonly _optionsService: IOptionsService, - protected readonly _decorationService: IDecorationService + protected readonly _decorationService: IDecorationService, + protected readonly _coreBrowserService: ICoreBrowserService ) { this._canvas = document.createElement('canvas'); this._canvas.classList.add(`xterm-${id}-layer`); @@ -125,7 +127,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) { return; } - this._charAtlas = acquireCharAtlas(this._optionsService.rawOptions, this._rendererId, colorSet, this._scaledCharWidth, this._scaledCharHeight); + this._charAtlas = acquireCharAtlas(this._optionsService.rawOptions, this._rendererId, colorSet, this._scaledCharWidth, this._scaledCharHeight, this._coreBrowserService.dpr); this._charAtlas.warmUp(); } @@ -180,9 +182,9 @@ export abstract class BaseRenderLayer implements IRenderLayer { const cellOffset = Math.ceil(this._scaledCellHeight * 0.5); this._ctx.fillRect( x * this._scaledCellWidth, - (y + 1) * this._scaledCellHeight - cellOffset - window.devicePixelRatio, + (y + 1) * this._scaledCellHeight - cellOffset - this._coreBrowserService.dpr, width * this._scaledCellWidth, - window.devicePixelRatio); + this._coreBrowserService.dpr); } /** @@ -194,23 +196,24 @@ export abstract class BaseRenderLayer implements IRenderLayer { protected _fillBottomLineAtCells(x: number, y: number, width: number = 1, pixelOffset: number = 0): void { this._ctx.fillRect( x * this._scaledCellWidth, - (y + 1) * this._scaledCellHeight + pixelOffset - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */, + (y + 1) * this._scaledCellHeight + pixelOffset - this._coreBrowserService.dpr - 1 /* Ensure it's drawn within the cell */, width * this._scaledCellWidth, - window.devicePixelRatio); + this._coreBrowserService.dpr); } protected _curlyUnderlineAtCell(x: number, y: number, width: number = 1): void { this._ctx.save(); this._ctx.beginPath(); this._ctx.strokeStyle = this._ctx.fillStyle; - this._ctx.lineWidth = window.devicePixelRatio; + const lineWidth = this._coreBrowserService.dpr; + this._ctx.lineWidth = lineWidth; for (let xOffset = 0; xOffset < width; xOffset++) { const xLeft = (x + xOffset) * this._scaledCellWidth; const xMid = (x + xOffset + 0.5) * this._scaledCellWidth; const xRight = (x + xOffset + 1) * this._scaledCellWidth; - const yMid = (y + 1) * this._scaledCellHeight - window.devicePixelRatio - 1; - const yMidBot = yMid - window.devicePixelRatio; - const yMidTop = yMid + window.devicePixelRatio; + const yMid = (y + 1) * this._scaledCellHeight - lineWidth - 1; + const yMidBot = yMid - lineWidth; + const yMidTop = yMid + lineWidth; this._ctx.moveTo(xLeft, yMid); this._ctx.bezierCurveTo( xLeft, yMidBot, @@ -231,10 +234,11 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.save(); this._ctx.beginPath(); this._ctx.strokeStyle = this._ctx.fillStyle; - this._ctx.lineWidth = window.devicePixelRatio; - this._ctx.setLineDash([window.devicePixelRatio * 2, window.devicePixelRatio]); + const lineWidth = this._coreBrowserService.dpr; + this._ctx.lineWidth = lineWidth; + this._ctx.setLineDash([lineWidth * 2, lineWidth]); const xLeft = x * this._scaledCellWidth; - const yMid = (y + 1) * this._scaledCellHeight - window.devicePixelRatio - 1; + const yMid = (y + 1) * this._scaledCellHeight - lineWidth - 1; this._ctx.moveTo(xLeft, yMid); for (let xOffset = 0; xOffset < width; xOffset++) { // const xLeft = x * this._scaledCellWidth; @@ -250,11 +254,12 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.save(); this._ctx.beginPath(); this._ctx.strokeStyle = this._ctx.fillStyle; - this._ctx.lineWidth = window.devicePixelRatio; - this._ctx.setLineDash([window.devicePixelRatio * 4, window.devicePixelRatio * 3]); + const lineWidth = this._coreBrowserService.dpr; + this._ctx.lineWidth = lineWidth; + this._ctx.setLineDash([lineWidth * 4, lineWidth * 3]); const xLeft = x * this._scaledCellWidth; const xRight = (x + width) * this._scaledCellWidth; - const yMid = (y + 1) * this._scaledCellHeight - window.devicePixelRatio - 1; + const yMid = (y + 1) * this._scaledCellHeight - lineWidth - 1; this._ctx.moveTo(xLeft, yMid); this._ctx.lineTo(xRight, yMid); this._ctx.stroke(); @@ -272,7 +277,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.fillRect( x * this._scaledCellWidth, y * this._scaledCellHeight, - window.devicePixelRatio * width, + this._coreBrowserService.dpr * width, this._scaledCellHeight); } @@ -283,12 +288,13 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param y The row to fill. */ protected _strokeRectAtCell(x: number, y: number, width: number, height: number): void { - this._ctx.lineWidth = window.devicePixelRatio; + const lineWidth = this._coreBrowserService.dpr; + this._ctx.lineWidth = lineWidth; this._ctx.strokeRect( - x * this._scaledCellWidth + window.devicePixelRatio / 2, - y * this._scaledCellHeight + (window.devicePixelRatio / 2), - width * this._scaledCellWidth - window.devicePixelRatio, - (height * this._scaledCellHeight) - window.devicePixelRatio); + x * this._scaledCellWidth + lineWidth / 2, + y * this._scaledCellHeight + (lineWidth / 2), + width * this._scaledCellWidth - lineWidth, + (height * this._scaledCellHeight) - lineWidth); } /** @@ -344,7 +350,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { // Draw custom characters if applicable let drawSuccess = false; if (this._optionsService.rawOptions.customGlyphs !== false) { - drawSuccess = tryDrawCustomChar(this._ctx, cell.getChars(), x * this._scaledCellWidth, y * this._scaledCellHeight, this._scaledCellWidth, this._scaledCellHeight, this._optionsService.rawOptions.fontSize); + drawSuccess = tryDrawCustomChar(this._ctx, cell.getChars(), x * this._scaledCellWidth, y * this._scaledCellHeight, this._scaledCellWidth, this._scaledCellHeight, this._optionsService.rawOptions.fontSize, this._coreBrowserService.dpr); } // Draw the character @@ -472,7 +478,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { // Draw custom characters if applicable let drawSuccess = false; if (this._optionsService.rawOptions.customGlyphs !== false) { - drawSuccess = tryDrawCustomChar(this._ctx, cell.getChars(), x * this._scaledCellWidth, y * this._scaledCellHeight, this._scaledCellWidth, this._scaledCellHeight, this._optionsService.rawOptions.fontSize); + drawSuccess = tryDrawCustomChar(this._ctx, cell.getChars(), x * this._scaledCellWidth, y * this._scaledCellHeight, this._scaledCellWidth, this._scaledCellHeight, this._optionsService.rawOptions.fontSize, this._coreBrowserService.dpr); } // Draw the character @@ -509,7 +515,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { const fontWeight = isBold ? this._optionsService.rawOptions.fontWeightBold : this._optionsService.rawOptions.fontWeight; const fontStyle = isItalic ? 'italic' : ''; - return `${fontStyle} ${fontWeight} ${this._optionsService.rawOptions.fontSize * window.devicePixelRatio}px ${this._optionsService.rawOptions.fontFamily}`; + return `${fontStyle} ${fontWeight} ${this._optionsService.rawOptions.fontSize * this._coreBrowserService.dpr}px ${this._optionsService.rawOptions.fontFamily}`; } private _getContrastColor(cell: CellData, x: number, y: number): IColor | undefined { diff --git a/addons/xterm-addon-canvas/src/CanvasRenderer.ts b/addons/xterm-addon-canvas/src/CanvasRenderer.ts index cb30106d37..b642efbcaf 100644 --- a/addons/xterm-addon-canvas/src/CanvasRenderer.ts +++ b/addons/xterm-addon-canvas/src/CanvasRenderer.ts @@ -39,16 +39,16 @@ export class CanvasRenderer extends Disposable implements IRenderer { private readonly _optionsService: IOptionsService, characterJoinerService: ICharacterJoinerService, coreService: ICoreService, - coreBrowserService: ICoreBrowserService, + private readonly _coreBrowserService: ICoreBrowserService, decorationService: IDecorationService ) { super(); const allowTransparency = this._optionsService.rawOptions.allowTransparency; this._renderLayers = [ - new TextRenderLayer(this._screenElement, 0, this._colors, allowTransparency, this._id, this._bufferService, this._optionsService, characterJoinerService, decorationService), - new SelectionRenderLayer(this._screenElement, 1, this._colors, this._id, this._bufferService, coreBrowserService, decorationService, this._optionsService), - new LinkRenderLayer(this._screenElement, 2, this._colors, this._id, linkifier2, this._bufferService, this._optionsService, decorationService), - new CursorRenderLayer(this._screenElement, 3, this._colors, this._id, this._onRequestRedraw, this._bufferService, this._optionsService, coreService, coreBrowserService, decorationService) + new TextRenderLayer(this._screenElement, 0, this._colors, allowTransparency, this._id, this._bufferService, this._optionsService, characterJoinerService, decorationService, this._coreBrowserService), + new SelectionRenderLayer(this._screenElement, 1, this._colors, this._id, this._bufferService, this._coreBrowserService, decorationService, this._optionsService), + new LinkRenderLayer(this._screenElement, 2, this._colors, this._id, linkifier2, this._bufferService, this._optionsService, decorationService, this._coreBrowserService), + new CursorRenderLayer(this._screenElement, 3, this._colors, this._id, this._onRequestRedraw, this._bufferService, this._optionsService, coreService, this._coreBrowserService, decorationService) ]; this.dimensions = { scaledCharWidth: 0, @@ -64,10 +64,10 @@ export class CanvasRenderer extends Disposable implements IRenderer { actualCellWidth: 0, actualCellHeight: 0 }; - this._devicePixelRatio = window.devicePixelRatio; + this._devicePixelRatio = this._coreBrowserService.dpr; this._updateDimensions(); - this.register(observeDevicePixelDimensions(this._renderLayers[0].canvas, (w, h) => this._setCanvasDevicePixelDimensions(w, h))); + this.register(observeDevicePixelDimensions(this._renderLayers[0].canvas, this._coreBrowserService.window, (w, h) => this._setCanvasDevicePixelDimensions(w, h))); this.onOptionsChanged(); } @@ -83,8 +83,8 @@ export class CanvasRenderer extends Disposable implements IRenderer { public onDevicePixelRatioChange(): void { // If the device pixel ratio changed, the char atlas needs to be regenerated // and the terminal needs to refreshed - if (this._devicePixelRatio !== window.devicePixelRatio) { - this._devicePixelRatio = window.devicePixelRatio; + if (this._devicePixelRatio !== this._coreBrowserService.dpr) { + this._devicePixelRatio = this._coreBrowserService.dpr; this.onResize(this._bufferService.cols, this._bufferService.rows); } } @@ -175,16 +175,17 @@ export class CanvasRenderer extends Disposable implements IRenderer { } // See the WebGL renderer for an explanation of this section. - this.dimensions.scaledCharWidth = Math.floor(this._charSizeService.width * window.devicePixelRatio); - this.dimensions.scaledCharHeight = Math.ceil(this._charSizeService.height * window.devicePixelRatio); + const dpr = this._coreBrowserService.dpr; + this.dimensions.scaledCharWidth = Math.floor(this._charSizeService.width * dpr); + this.dimensions.scaledCharHeight = Math.ceil(this._charSizeService.height * dpr); this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._optionsService.rawOptions.lineHeight); this.dimensions.scaledCharTop = this._optionsService.rawOptions.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2); this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._optionsService.rawOptions.letterSpacing); this.dimensions.scaledCharLeft = Math.floor(this._optionsService.rawOptions.letterSpacing / 2); this.dimensions.scaledCanvasHeight = this._bufferService.rows * this.dimensions.scaledCellHeight; this.dimensions.scaledCanvasWidth = this._bufferService.cols * this.dimensions.scaledCellWidth; - this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / window.devicePixelRatio); - this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / window.devicePixelRatio); + this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / dpr); + this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / dpr); this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._bufferService.rows; this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._bufferService.cols; } diff --git a/addons/xterm-addon-canvas/src/CursorRenderLayer.ts b/addons/xterm-addon-canvas/src/CursorRenderLayer.ts index 19d414b60e..61e2d9341b 100644 --- a/addons/xterm-addon-canvas/src/CursorRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/CursorRenderLayer.ts @@ -40,10 +40,10 @@ export class CursorRenderLayer extends BaseRenderLayer { bufferService: IBufferService, optionsService: IOptionsService, private readonly _coreService: ICoreService, - private readonly _coreBrowserService: ICoreBrowserService, + coreBrowserService: ICoreBrowserService, decorationService: IDecorationService ) { - super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService); + super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService, coreBrowserService); this._state = { x: 0, y: 0, @@ -99,7 +99,7 @@ export class CursorRenderLayer extends BaseRenderLayer { if (!this._cursorBlinkStateManager) { this._cursorBlinkStateManager = new CursorBlinkStateManager(this._coreBrowserService.isFocused, () => { this._render(true); - }); + }, this._coreBrowserService); } } else { this._cursorBlinkStateManager?.dispose(); @@ -196,7 +196,7 @@ export class CursorRenderLayer extends BaseRenderLayer { private _clearCursor(): void { if (this._state) { // Avoid potential rounding errors when device pixel ratio is less than 1 - if (window.devicePixelRatio < 1) { + if (this._coreBrowserService.dpr < 1) { this._clearAll(); } else { this._clearCells(this._state.x, this._state.y, this._state.width, 1); @@ -258,7 +258,8 @@ class CursorBlinkStateManager { constructor( isFocused: boolean, - private _renderCallback: () => void + private _renderCallback: () => void, + private _coreBrowserService: ICoreBrowserService ) { this.isCursorVisible = true; if (isFocused) { @@ -270,15 +271,15 @@ class CursorBlinkStateManager { public dispose(): void { if (this._blinkInterval) { - window.clearInterval(this._blinkInterval); + this._coreBrowserService.window.clearInterval(this._blinkInterval); this._blinkInterval = undefined; } if (this._blinkStartTimeout) { - window.clearTimeout(this._blinkStartTimeout); + this._coreBrowserService.window.clearTimeout(this._blinkStartTimeout); this._blinkStartTimeout = undefined; } if (this._animationFrame) { - window.cancelAnimationFrame(this._animationFrame); + this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame); this._animationFrame = undefined; } } @@ -292,7 +293,7 @@ class CursorBlinkStateManager { // Force a cursor render to ensure it's visible and in the correct position this.isCursorVisible = true; if (!this._animationFrame) { - this._animationFrame = window.requestAnimationFrame(() => { + this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => { this._renderCallback(); this._animationFrame = undefined; }); @@ -302,7 +303,7 @@ class CursorBlinkStateManager { private _restartInterval(timeToStart: number = BLINK_INTERVAL): void { // Clear any existing interval if (this._blinkInterval) { - window.clearInterval(this._blinkInterval); + this._coreBrowserService.window.clearInterval(this._blinkInterval); this._blinkInterval = undefined; } @@ -310,7 +311,7 @@ class CursorBlinkStateManager { // the regular interval is setup in order to support restarting the blink // animation in a lightweight way (without thrashing clearInterval and // setInterval). - this._blinkStartTimeout = window.setTimeout(() => { + this._blinkStartTimeout = this._coreBrowserService.window.setTimeout(() => { // Check if another animation restart was requested while this was being // started if (this._animationTimeRestarted) { @@ -324,13 +325,13 @@ class CursorBlinkStateManager { // Hide the cursor this.isCursorVisible = false; - this._animationFrame = window.requestAnimationFrame(() => { + this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => { this._renderCallback(); this._animationFrame = undefined; }); // Setup the blink interval - this._blinkInterval = window.setInterval(() => { + this._blinkInterval = this._coreBrowserService.window.setInterval(() => { // Adjust the animation time if it was restarted if (this._animationTimeRestarted) { // calc time diff @@ -343,7 +344,7 @@ class CursorBlinkStateManager { // Invert visibility and render this.isCursorVisible = !this.isCursorVisible; - this._animationFrame = window.requestAnimationFrame(() => { + this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => { this._renderCallback(); this._animationFrame = undefined; }); @@ -354,15 +355,15 @@ class CursorBlinkStateManager { public pause(): void { this.isCursorVisible = true; if (this._blinkInterval) { - window.clearInterval(this._blinkInterval); + this._coreBrowserService.window.clearInterval(this._blinkInterval); this._blinkInterval = undefined; } if (this._blinkStartTimeout) { - window.clearTimeout(this._blinkStartTimeout); + this._coreBrowserService.window.clearTimeout(this._blinkStartTimeout); this._blinkStartTimeout = undefined; } if (this._animationFrame) { - window.cancelAnimationFrame(this._animationFrame); + this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame); this._animationFrame = undefined; } } diff --git a/addons/xterm-addon-canvas/src/LinkRenderLayer.ts b/addons/xterm-addon-canvas/src/LinkRenderLayer.ts index e514e3d056..c07329fd66 100644 --- a/addons/xterm-addon-canvas/src/LinkRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/LinkRenderLayer.ts @@ -6,6 +6,7 @@ import { IRenderDimensions } from 'browser/renderer/Types'; import { BaseRenderLayer } from './BaseRenderLayer'; import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants'; +import { ICoreBrowserService } from 'browser/services/Services'; import { is256Color } from './atlas/CharAtlasUtils'; import { IColorSet, ILinkifierEvent, ILinkifier2 } from 'browser/Types'; import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; @@ -21,9 +22,10 @@ export class LinkRenderLayer extends BaseRenderLayer { linkifier2: ILinkifier2, bufferService: IBufferService, optionsService: IOptionsService, - decorationService: IDecorationService + decorationService: IDecorationService, + coreBrowserService: ICoreBrowserService ) { - super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService); + super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService, coreBrowserService); linkifier2.onShowLinkUnderline(e => this._onShowLinkUnderline(e)); linkifier2.onHideLinkUnderline(e => this._onHideLinkUnderline(e)); diff --git a/addons/xterm-addon-canvas/src/SelectionRenderLayer.ts b/addons/xterm-addon-canvas/src/SelectionRenderLayer.ts index 82aa056bd1..e90007b720 100644 --- a/addons/xterm-addon-canvas/src/SelectionRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/SelectionRenderLayer.ts @@ -25,11 +25,11 @@ export class SelectionRenderLayer extends BaseRenderLayer { colors: IColorSet, rendererId: number, bufferService: IBufferService, - private readonly _coreBrowserService: ICoreBrowserService, + coreBrowserService: ICoreBrowserService, decorationService: IDecorationService, optionsService: IOptionsService ) { - super(container, 'selection', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService); + super(container, 'selection', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService, coreBrowserService); this._clearState(); } diff --git a/addons/xterm-addon-canvas/src/TextRenderLayer.ts b/addons/xterm-addon-canvas/src/TextRenderLayer.ts index 86e21ad5c8..95f22fce73 100644 --- a/addons/xterm-addon-canvas/src/TextRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/TextRenderLayer.ts @@ -12,7 +12,7 @@ import { NULL_CELL_CODE, Content, UnderlineStyle } from 'common/buffer/Constants import { IColorSet } from 'browser/Types'; import { CellData } from 'common/buffer/CellData'; import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services'; -import { ICharacterJoinerService } from 'browser/services/Services'; +import { ICharacterJoinerService, ICoreBrowserService } from 'browser/services/Services'; import { JoinedCellData } from 'browser/services/CharacterJoinerService'; import { color, css } from 'common/Color'; @@ -39,9 +39,10 @@ export class TextRenderLayer extends BaseRenderLayer { bufferService: IBufferService, optionsService: IOptionsService, private readonly _characterJoinerService: ICharacterJoinerService, - decorationService: IDecorationService + decorationService: IDecorationService, + coreBrowserService: ICoreBrowserService ) { - super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService, decorationService); + super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService, decorationService, coreBrowserService); this._state = new GridCache(); } @@ -282,8 +283,8 @@ export class TextRenderLayer extends BaseRenderLayer { } switch (cell.extended.underlineStyle) { case UnderlineStyle.DOUBLE: - this._fillBottomLineAtCells(x, y, cell.getWidth(), -window.devicePixelRatio); - this._fillBottomLineAtCells(x, y, cell.getWidth(), window.devicePixelRatio); + this._fillBottomLineAtCells(x, y, cell.getWidth(), -this._coreBrowserService.dpr); + this._fillBottomLineAtCells(x, y, cell.getWidth(), this._coreBrowserService.dpr); break; case UnderlineStyle.CURLY: this._curlyUnderlineAtCell(x, y, cell.getWidth()); diff --git a/addons/xterm-addon-canvas/src/atlas/CharAtlasCache.ts b/addons/xterm-addon-canvas/src/atlas/CharAtlasCache.ts index 0c78808eee..d9349286f8 100644 --- a/addons/xterm-addon-canvas/src/atlas/CharAtlasCache.ts +++ b/addons/xterm-addon-canvas/src/atlas/CharAtlasCache.ts @@ -29,9 +29,10 @@ export function acquireCharAtlas( rendererId: number, colors: IColorSet, scaledCharWidth: number, - scaledCharHeight: number + scaledCharHeight: number, + devicePixelRatio: number ): BaseCharAtlas { - const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, options, colors); + const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, options, colors, devicePixelRatio); // Check to see if the renderer already owns this config for (let i = 0; i < charAtlasCache.length; i++) { diff --git a/addons/xterm-addon-canvas/src/atlas/CharAtlasUtils.ts b/addons/xterm-addon-canvas/src/atlas/CharAtlasUtils.ts index b5674304d8..b0151e30d9 100644 --- a/addons/xterm-addon-canvas/src/atlas/CharAtlasUtils.ts +++ b/addons/xterm-addon-canvas/src/atlas/CharAtlasUtils.ts @@ -8,7 +8,7 @@ import { DEFAULT_COLOR } from 'common/buffer/Constants'; import { IColorSet, IPartialColorSet } from 'browser/Types'; import { ITerminalOptions } from 'xterm'; -export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, options: Required, colors: IColorSet): ICharAtlasConfig { +export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, options: Required, colors: IColorSet, devicePixelRatio: number): ICharAtlasConfig { // null out some fields that don't matter const clonedColors: IPartialColorSet = { foreground: colors.foreground, @@ -19,7 +19,7 @@ export function generateConfig(scaledCharWidth: number, scaledCharHeight: number ansi: colors.ansi.slice() }; return { - devicePixelRatio: window.devicePixelRatio, + devicePixelRatio, scaledCharWidth, scaledCharHeight, fontFamily: options.fontFamily, diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 6420e812f6..d8daa0d187 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -77,7 +77,7 @@ export class WebglRenderer extends Disposable implements IRenderer { this._core = (this._terminal as any)._core; this._renderLayers = [ - new LinkRenderLayer(this._core.screenElement!, 2, this._colors, this._core), + new LinkRenderLayer(this._core.screenElement!, 2, this._colors, this._core, this._coreBrowserService), new CursorRenderLayer(_terminal, this._core.screenElement!, 3, this._colors, this._onRequestRedraw, this._coreBrowserService, coreService) ]; this.dimensions = { @@ -94,7 +94,7 @@ export class WebglRenderer extends Disposable implements IRenderer { actualCellWidth: 0, actualCellHeight: 0 }; - this._devicePixelRatio = window.devicePixelRatio; + this._devicePixelRatio = this._coreBrowserService.dpr; this._updateDimensions(); this._canvas = document.createElement('canvas'); @@ -132,13 +132,13 @@ export class WebglRenderer extends Disposable implements IRenderer { this._requestRedrawViewport(); })); - this.register(observeDevicePixelDimensions(this._canvas, (w, h) => this._setCanvasDevicePixelDimensions(w, h))); + this.register(observeDevicePixelDimensions(this._canvas, this._coreBrowserService.window, (w, h) => this._setCanvasDevicePixelDimensions(w, h))); this._core.screenElement!.appendChild(this._canvas); this._initializeWebGLState(); - this._isAttached = document.body.contains(this._core.screenElement!); + this._isAttached = this._coreBrowserService.window.document.body.contains(this._core.screenElement!); } public dispose(): void { @@ -173,8 +173,8 @@ export class WebglRenderer extends Disposable implements IRenderer { public onDevicePixelRatioChange(): void { // If the device pixel ratio changed, the char atlas needs to be regenerated // and the terminal needs to refreshed - if (this._devicePixelRatio !== window.devicePixelRatio) { - this._devicePixelRatio = window.devicePixelRatio; + if (this._devicePixelRatio !== this._coreBrowserService.dpr) { + this._devicePixelRatio = this._coreBrowserService.dpr; this.onResize(this._terminal.cols, this._terminal.rows); } } @@ -281,7 +281,7 @@ export class WebglRenderer extends Disposable implements IRenderer { return; } - const atlas = acquireCharAtlas(this._terminal, this._colors, this.dimensions.scaledCellWidth, this.dimensions.scaledCellHeight, this.dimensions.scaledCharWidth, this.dimensions.scaledCharHeight); + const atlas = acquireCharAtlas(this._terminal, this._colors, this.dimensions.scaledCellWidth, this.dimensions.scaledCellHeight, this.dimensions.scaledCharWidth, this.dimensions.scaledCharHeight, this._coreBrowserService.dpr); if (!('getRasterizedGlyph' in atlas)) { throw new Error('The webgl renderer only works with the webgl char atlas'); } @@ -329,7 +329,7 @@ export class WebglRenderer extends Disposable implements IRenderer { public renderRows(start: number, end: number): void { if (!this._isAttached) { - if (document.body.contains(this._core.screenElement!) && (this._core as any)._charSizeService.width && (this._core as any)._charSizeService.height) { + if (this._coreBrowserService.window.document.body.contains(this._core.screenElement!) && (this._core as any)._charSizeService.width && (this._core as any)._charSizeService.height) { this._updateDimensions(); this._refreshCharAtlas(); this._isAttached = true; diff --git a/addons/xterm-addon-webgl/src/atlas/CharAtlasCache.ts b/addons/xterm-addon-webgl/src/atlas/CharAtlasCache.ts index 6aba2125bc..893362c867 100644 --- a/addons/xterm-addon-webgl/src/atlas/CharAtlasCache.ts +++ b/addons/xterm-addon-webgl/src/atlas/CharAtlasCache.ts @@ -31,9 +31,10 @@ export function acquireCharAtlas( scaledCellWidth: number, scaledCellHeight: number, scaledCharWidth: number, - scaledCharHeight: number + scaledCharHeight: number, + devicePixelRatio: number ): WebglCharAtlas { - const newConfig = generateConfig(scaledCellWidth, scaledCellHeight, scaledCharWidth, scaledCharHeight, terminal, colors); + const newConfig = generateConfig(scaledCellWidth, scaledCellHeight, scaledCharWidth, scaledCharHeight, terminal, colors, devicePixelRatio); // Check to see if the terminal already owns this config for (let i = 0; i < charAtlasCache.length; i++) { diff --git a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts index 18de5739bb..83f82fa768 100644 --- a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts +++ b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts @@ -14,7 +14,7 @@ const NULL_COLOR: IColor = { rgba: 0 }; -export function generateConfig(scaledCellWidth: number, scaledCellHeight: number, scaledCharWidth: number, scaledCharHeight: number, terminal: Terminal, colors: IColorSet): ICharAtlasConfig { +export function generateConfig(scaledCellWidth: number, scaledCellHeight: number, scaledCharWidth: number, scaledCharHeight: number, terminal: Terminal, colors: IColorSet, devicePixelRatio: number): ICharAtlasConfig { // null out some fields that don't matter const clonedColors: IColorSet = { foreground: colors.foreground, @@ -33,7 +33,7 @@ export function generateConfig(scaledCellWidth: number, scaledCellHeight: number }; return { customGlyphs: terminal.options.customGlyphs, - devicePixelRatio: window.devicePixelRatio, + devicePixelRatio, letterSpacing: terminal.options.letterSpacing, lineHeight: terminal.options.lineHeight, scaledCellWidth, diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index e7dbfaa263..b4593d909f 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -409,7 +409,7 @@ export class WebglCharAtlas implements IDisposable { // Draw custom characters if applicable let customGlyph = false; if (this._config.customGlyphs !== false) { - customGlyph = tryDrawCustomChar(this._tmpCtx, chars, padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight, this._config.fontSize); + customGlyph = tryDrawCustomChar(this._tmpCtx, chars, padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight, this._config.fontSize, this._config.devicePixelRatio); } // Whether to clear pixels based on a threshold difference between the glyph color and the @@ -427,7 +427,7 @@ export class WebglCharAtlas implements IDisposable { // Draw underline if (underline) { this._tmpCtx.save(); - const lineWidth = Math.max(1, Math.floor(this._config.fontSize * window.devicePixelRatio / 15)); + const lineWidth = Math.max(1, Math.floor(this._config.fontSize * this._config.devicePixelRatio / 15)); // When the line width is odd, draw at a 0.5 position const yOffset = lineWidth % 2 === 1 ? 0.5 : 0; this._tmpCtx.lineWidth = lineWidth; @@ -501,12 +501,12 @@ export class WebglCharAtlas implements IDisposable { ); break; case UnderlineStyle.DOTTED: - this._tmpCtx.setLineDash([window.devicePixelRatio * 2, window.devicePixelRatio]); + this._tmpCtx.setLineDash([this._config.devicePixelRatio * 2, this._config.devicePixelRatio]); this._tmpCtx.moveTo(xChLeft, yTop); this._tmpCtx.lineTo(xChRight, yTop); break; case UnderlineStyle.DASHED: - this._tmpCtx.setLineDash([window.devicePixelRatio * 4, window.devicePixelRatio * 3]); + this._tmpCtx.setLineDash([this._config.devicePixelRatio * 4, this._config.devicePixelRatio * 3]); this._tmpCtx.moveTo(xChLeft, yTop); this._tmpCtx.lineTo(xChRight, yTop); break; @@ -543,7 +543,7 @@ export class WebglCharAtlas implements IDisposable { const clipRegion = new Path2D(); clipRegion.rect(xLeft, yTop - Math.ceil(lineWidth / 2), this._config.scaledCellWidth, yBot - yTop + Math.ceil(lineWidth / 2)); this._tmpCtx.clip(clipRegion); - this._tmpCtx.lineWidth = window.devicePixelRatio * 3; + this._tmpCtx.lineWidth = this._config.devicePixelRatio * 3; this._tmpCtx.strokeStyle = backgroundColor.css; this._tmpCtx.strokeText(chars, padding, padding + this._config.scaledCharHeight); this._tmpCtx.restore(); @@ -578,7 +578,7 @@ export class WebglCharAtlas implements IDisposable { // Draw strokethrough if (strikethrough) { - const lineWidth = Math.max(1, Math.floor(this._config.fontSize * window.devicePixelRatio / 10)); + const lineWidth = Math.max(1, Math.floor(this._config.fontSize * this._config.devicePixelRatio / 10)); const yOffset = this._tmpCtx.lineWidth % 2 === 1 ? 0.5 : 0; // When the width is odd, draw at 0.5 position this._tmpCtx.lineWidth = lineWidth; this._tmpCtx.strokeStyle = this._tmpCtx.fillStyle; diff --git a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts index 9d9207732a..e9c0cf4136 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts @@ -8,6 +8,7 @@ import { acquireCharAtlas } from '../atlas/CharAtlasCache'; import { Terminal } from 'xterm'; import { IColorSet } from 'browser/Types'; import { TEXT_BASELINE } from 'browser/renderer/Constants'; +import { ICoreBrowserService } from 'browser/services/Services'; import { IRenderDimensions } from 'browser/renderer/Types'; import { CellData } from 'common/buffer/CellData'; import { WebglCharAtlas } from 'atlas/WebglCharAtlas'; @@ -30,7 +31,8 @@ export abstract class BaseRenderLayer implements IRenderLayer { id: string, zIndex: number, private _alpha: boolean, - protected _colors: IColorSet + protected _colors: IColorSet, + protected readonly _coreBrowserService: ICoreBrowserService ) { this._canvas = document.createElement('canvas'); this._canvas.classList.add(`xterm-${id}-layer`); @@ -93,7 +95,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) { return; } - this._charAtlas = acquireCharAtlas(terminal, colorSet, this._scaledCellWidth, this._scaledCellHeight, this._scaledCharWidth, this._scaledCharHeight); + this._charAtlas = acquireCharAtlas(terminal, colorSet, this._scaledCellWidth, this._scaledCellHeight, this._scaledCharWidth, this._scaledCharHeight, this._coreBrowserService.dpr); this._charAtlas.warmUp(); } @@ -143,9 +145,9 @@ export abstract class BaseRenderLayer implements IRenderLayer { protected _fillBottomLineAtCells(x: number, y: number, width: number = 1): void { this._ctx.fillRect( x * this._scaledCellWidth, - (y + 1) * this._scaledCellHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */, + (y + 1) * this._scaledCellHeight - this._coreBrowserService.dpr - 1 /* Ensure it's drawn within the cell */, width * this._scaledCellWidth, - window.devicePixelRatio); + this._coreBrowserService.dpr); } /** @@ -158,7 +160,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.fillRect( x * this._scaledCellWidth, y * this._scaledCellHeight, - window.devicePixelRatio * width, + this._coreBrowserService.dpr * width, this._scaledCellHeight); } @@ -169,12 +171,12 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param y The row to fill. */ protected _strokeRectAtCell(x: number, y: number, width: number, height: number): void { - this._ctx.lineWidth = window.devicePixelRatio; + this._ctx.lineWidth = this._coreBrowserService.dpr; this._ctx.strokeRect( - x * this._scaledCellWidth + window.devicePixelRatio / 2, - y * this._scaledCellHeight + (window.devicePixelRatio / 2), - width * this._scaledCellWidth - window.devicePixelRatio, - (height * this._scaledCellHeight) - window.devicePixelRatio); + x * this._scaledCellWidth + this._coreBrowserService.dpr / 2, + y * this._scaledCellHeight + (this._coreBrowserService.dpr / 2), + width * this._scaledCellWidth - this._coreBrowserService.dpr, + (height * this._scaledCellHeight) - this._coreBrowserService.dpr); } /** @@ -258,7 +260,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { const fontWeight = isBold ? terminal.options.fontWeightBold : terminal.options.fontWeight; const fontStyle = isItalic ? 'italic' : ''; - return `${fontStyle} ${fontWeight} ${terminal.options.fontSize! * window.devicePixelRatio}px ${terminal.options.fontFamily}`; + return `${fontStyle} ${fontWeight} ${terminal.options.fontSize! * this._coreBrowserService.dpr}px ${terminal.options.fontFamily}`; } } diff --git a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts index 58d2569017..2b274516da 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts @@ -38,10 +38,10 @@ export class CursorRenderLayer extends BaseRenderLayer { zIndex: number, colors: IColorSet, private _onRequestRefreshRowsEvent: IEventEmitter, - private readonly _coreBrowserService: ICoreBrowserService, + coreBrowserService: ICoreBrowserService, private readonly _coreService: ICoreService ) { - super(container, 'cursor', zIndex, true, colors); + super(container, 'cursor', zIndex, true, colors, coreBrowserService); this._state = { x: 0, y: 0, @@ -195,7 +195,7 @@ export class CursorRenderLayer extends BaseRenderLayer { private _clearCursor(): void { if (this._state) { // Avoid potential rounding errors when device pixel ratio is less than 1 - if (window.devicePixelRatio < 1) { + if (this._coreBrowserService.dpr < 1) { this._clearAll(); } else { this._clearCells(this._state.x, this._state.y, this._state.width, 1); @@ -257,10 +257,10 @@ class CursorBlinkStateManager { constructor( private _renderCallback: () => void, - coreBrowserService: ICoreBrowserService + private _coreBrowserService: ICoreBrowserService ) { this.isCursorVisible = true; - if (coreBrowserService.isFocused) { + if (this._coreBrowserService.isFocused) { this._restartInterval(); } } @@ -269,15 +269,15 @@ class CursorBlinkStateManager { public dispose(): void { if (this._blinkInterval) { - window.clearInterval(this._blinkInterval); + this._coreBrowserService.window.clearInterval(this._blinkInterval); this._blinkInterval = undefined; } if (this._blinkStartTimeout) { - window.clearTimeout(this._blinkStartTimeout); + this._coreBrowserService.window.clearTimeout(this._blinkStartTimeout); this._blinkStartTimeout = undefined; } if (this._animationFrame) { - window.cancelAnimationFrame(this._animationFrame); + this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame); this._animationFrame = undefined; } } @@ -291,7 +291,7 @@ class CursorBlinkStateManager { // Force a cursor render to ensure it's visible and in the correct position this.isCursorVisible = true; if (!this._animationFrame) { - this._animationFrame = window.requestAnimationFrame(() => { + this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => { this._renderCallback(); this._animationFrame = undefined; }); @@ -301,7 +301,7 @@ class CursorBlinkStateManager { private _restartInterval(timeToStart: number = BLINK_INTERVAL): void { // Clear any existing interval if (this._blinkInterval) { - window.clearInterval(this._blinkInterval); + this._coreBrowserService.window.clearInterval(this._blinkInterval); this._blinkInterval = undefined; } @@ -309,7 +309,7 @@ class CursorBlinkStateManager { // the regular interval is setup in order to support restarting the blink // animation in a lightweight way (without thrashing clearInterval and // setInterval). - this._blinkStartTimeout = window.setTimeout(() => { + this._blinkStartTimeout = this._coreBrowserService.window.setTimeout(() => { // Check if another animation restart was requested while this was being // started if (this._animationTimeRestarted) { @@ -323,13 +323,13 @@ class CursorBlinkStateManager { // Hide the cursor this.isCursorVisible = false; - this._animationFrame = window.requestAnimationFrame(() => { + this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => { this._renderCallback(); this._animationFrame = undefined; }); // Setup the blink interval - this._blinkInterval = window.setInterval(() => { + this._blinkInterval = this._coreBrowserService.window.setInterval(() => { // Adjust the animation time if it was restarted if (this._animationTimeRestarted) { // calc time diff @@ -342,7 +342,7 @@ class CursorBlinkStateManager { // Invert visibility and render this.isCursorVisible = !this.isCursorVisible; - this._animationFrame = window.requestAnimationFrame(() => { + this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => { this._renderCallback(); this._animationFrame = undefined; }); @@ -353,15 +353,15 @@ class CursorBlinkStateManager { public pause(): void { this.isCursorVisible = true; if (this._blinkInterval) { - window.clearInterval(this._blinkInterval); + this._coreBrowserService.window.clearInterval(this._blinkInterval); this._blinkInterval = undefined; } if (this._blinkStartTimeout) { - window.clearTimeout(this._blinkStartTimeout); + this._coreBrowserService.window.clearTimeout(this._blinkStartTimeout); this._blinkStartTimeout = undefined; } if (this._animationFrame) { - window.cancelAnimationFrame(this._animationFrame); + this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame); this._animationFrame = undefined; } } diff --git a/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts index 440dbe7b30..e1ff08d814 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts @@ -9,12 +9,19 @@ import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants'; import { is256Color } from '../atlas/CharAtlasUtils'; import { ITerminal, IColorSet, ILinkifierEvent } from 'browser/Types'; import { IRenderDimensions } from 'browser/renderer/Types'; +import { ICoreBrowserService } from 'browser/services/Services'; export class LinkRenderLayer extends BaseRenderLayer { private _state: ILinkifierEvent | undefined; - constructor(container: HTMLElement, zIndex: number, colors: IColorSet, terminal: ITerminal) { - super(container, 'link', zIndex, true, colors); + constructor( + container: HTMLElement, + zIndex: number, + colors: IColorSet, + terminal: ITerminal, + coreBrowserService: ICoreBrowserService + ) { + super(container, 'link', zIndex, true, colors, coreBrowserService); terminal.linkifier2.onShowLinkUnderline(e => this._onShowLinkUnderline(e)); terminal.linkifier2.onHideLinkUnderline(e => this._onHideLinkUnderline(e)); diff --git a/src/browser/AccessibilityManager.ts b/src/browser/AccessibilityManager.ts index c162abcb74..eba283d54c 100644 --- a/src/browser/AccessibilityManager.ts +++ b/src/browser/AccessibilityManager.ts @@ -98,7 +98,7 @@ export class AccessibilityManager extends Disposable { this.register(this._terminal.onBlur(() => this._clearLiveRegion())); this.register(this._renderService.onDimensionsChange(() => this._refreshRowsDimensions())); - this._screenDprMonitor = new ScreenDprMonitor(); + 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 diff --git a/src/browser/RenderDebouncer.ts b/src/browser/RenderDebouncer.ts index ad2d79b45e..b3118d5f6c 100644 --- a/src/browser/RenderDebouncer.ts +++ b/src/browser/RenderDebouncer.ts @@ -16,13 +16,14 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback { private _refreshCallbacks: FrameRequestCallback[] = []; constructor( + private _parentWindow: Window, private _renderCallback: (start: number, end: number) => void ) { } public dispose(): void { if (this._animationFrame) { - window.cancelAnimationFrame(this._animationFrame); + this._parentWindow.cancelAnimationFrame(this._animationFrame); this._animationFrame = undefined; } } @@ -30,7 +31,7 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback { public addRefreshCallback(callback: FrameRequestCallback): number { this._refreshCallbacks.push(callback); if (!this._animationFrame) { - this._animationFrame = window.requestAnimationFrame(() => this._innerRefresh()); + this._animationFrame = this._parentWindow.requestAnimationFrame(() => this._innerRefresh()); } return this._animationFrame; } @@ -48,7 +49,7 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback { return; } - this._animationFrame = window.requestAnimationFrame(() => this._innerRefresh()); + this._animationFrame = this._parentWindow.requestAnimationFrame(() => this._innerRefresh()); } private _innerRefresh(): void { diff --git a/src/browser/ScreenDprMonitor.ts b/src/browser/ScreenDprMonitor.ts index 27ae231f0b..8129da0768 100644 --- a/src/browser/ScreenDprMonitor.ts +++ b/src/browser/ScreenDprMonitor.ts @@ -18,11 +18,16 @@ export type ScreenDprListener = (newDevicePixelRatio?: number, oldDevicePixelRat * monitor with a different DPI. */ export class ScreenDprMonitor extends Disposable { - private _currentDevicePixelRatio: number = window.devicePixelRatio; + 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; + } + public setListener(listener: ScreenDprListener): void { if (this._listener) { this.clearListener(); @@ -32,7 +37,7 @@ export class ScreenDprMonitor extends Disposable { if (!this._listener) { return; } - this._listener(window.devicePixelRatio, this._currentDevicePixelRatio); + this._listener(this._parentWindow.devicePixelRatio, this._currentDevicePixelRatio); this._updateDpr(); }; this._updateDpr(); @@ -52,8 +57,8 @@ export class ScreenDprMonitor extends Disposable { this._resolutionMediaMatchList?.removeListener(this._outerListener); // Add listeners for new DPR - this._currentDevicePixelRatio = window.devicePixelRatio; - this._resolutionMediaMatchList = window.matchMedia(`screen and (resolution: ${window.devicePixelRatio}dppx)`); + this._currentDevicePixelRatio = this._parentWindow.devicePixelRatio; + this._resolutionMediaMatchList = this._parentWindow.matchMedia(`screen and (resolution: ${this._parentWindow.devicePixelRatio}dppx)`); this._resolutionMediaMatchList.addListener(this._outerListener); } diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index 9ad84fd761..9aba38186d 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -495,7 +495,7 @@ export class Terminal extends CoreTerminal implements ITerminal { this.register(addDisposableDomListener(this.textarea, 'blur', () => this._onTextAreaBlur())); this._helperContainer.appendChild(this.textarea); - this._coreBrowserService = this._instantiationService.createInstance(CoreBrowserService, this.textarea); + this._coreBrowserService = this._instantiationService.createInstance(CoreBrowserService, this.textarea, this._document.defaultView ?? window); this._instantiationService.setService(ICoreBrowserService, this._coreBrowserService); this._charSizeService = this._instantiationService.createInstance(CharSizeService, this._document, this._helperContainer); diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index bb7b440cdb..0b5e00c173 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -343,6 +343,10 @@ export class MockCompositionHelper implements ICompositionHelper { export class MockCoreBrowserService implements ICoreBrowserService { public serviceBrand: undefined; public isFocused: boolean = true; + public get window(): Window & typeof globalThis { + throw Error('Window object not available in tests'); + } + public dpr: number = 1; } export class MockCharSizeService implements ICharSizeService { diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index 6cff1f98c8..f95e0bb48d 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -6,7 +6,7 @@ import { Disposable } from 'common/Lifecycle'; import { addDisposableDomListener } from 'browser/Lifecycle'; import { IColorSet, IViewport } from 'browser/Types'; -import { ICharSizeService, IRenderService } from 'browser/services/Services'; +import { ICharSizeService, ICoreBrowserService, IRenderService } from 'browser/services/Services'; import { IBufferService, IOptionsService } from 'common/services/Services'; import { IBuffer } from 'common/buffer/Types'; import { IRenderDimensions } from 'browser/renderer/Types'; @@ -56,7 +56,8 @@ export class Viewport extends Disposable implements IViewport { @IBufferService private readonly _bufferService: IBufferService, @IOptionsService private readonly _optionsService: IOptionsService, @ICharSizeService private readonly _charSizeService: ICharSizeService, - @IRenderService private readonly _renderService: IRenderService + @IRenderService private readonly _renderService: IRenderService, + @ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService ) { super(); @@ -88,18 +89,18 @@ export class Viewport extends Disposable implements IViewport { if (immediate) { this._innerRefresh(); if (this._refreshAnimationFrame !== null) { - cancelAnimationFrame(this._refreshAnimationFrame); + this._coreBrowserService.window.cancelAnimationFrame(this._refreshAnimationFrame); } return; } if (this._refreshAnimationFrame === null) { - this._refreshAnimationFrame = requestAnimationFrame(() => this._innerRefresh()); + this._refreshAnimationFrame = this._coreBrowserService.window.requestAnimationFrame(() => this._innerRefresh()); } } private _innerRefresh(): void { if (this._charSizeService.height > 0) { - this._currentRowHeight = this._renderService.dimensions.scaledCellHeight / window.devicePixelRatio; + this._currentRowHeight = this._renderService.dimensions.scaledCellHeight / this._coreBrowserService.dpr; this._currentScaledCellHeight = this._renderService.dimensions.scaledCellHeight; this._lastRecordedViewportHeight = this._viewportElement.offsetHeight; const newBufferHeight = Math.round(this._currentRowHeight * this._lastRecordedBufferLength) + (this._lastRecordedViewportHeight - this._renderService.dimensions.canvasHeight); @@ -191,7 +192,7 @@ export class Viewport extends Disposable implements IViewport { // Continue or finish smooth scroll if (percent < 1) { - window.requestAnimationFrame(() => this._smoothScroll()); + this._coreBrowserService.window.requestAnimationFrame(() => this._smoothScroll()); } else { this._clearSmoothScrollState(); } diff --git a/src/browser/decorations/OverviewRulerRenderer.ts b/src/browser/decorations/OverviewRulerRenderer.ts index 7c284a616c..e7db50c55d 100644 --- a/src/browser/decorations/OverviewRulerRenderer.ts +++ b/src/browser/decorations/OverviewRulerRenderer.ts @@ -5,7 +5,7 @@ import { ColorZoneStore, IColorZone, IColorZoneStore } from 'browser/decorations/ColorZoneStore'; import { addDisposableDomListener } from 'browser/Lifecycle'; -import { IRenderService } from 'browser/services/Services'; +import { ICoreBrowserService, IRenderService } from 'browser/services/Services'; import { Disposable } from 'common/Lifecycle'; import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; @@ -51,7 +51,8 @@ export class OverviewRulerRenderer extends Disposable { @IBufferService private readonly _bufferService: IBufferService, @IDecorationService private readonly _decorationService: IDecorationService, @IRenderService private readonly _renderService: IRenderService, - @IOptionsService private readonly _optionsService: IOptionsService + @IOptionsService private readonly _optionsService: IOptionsService, + @ICoreBrowserService private readonly _coreBrowseService: ICoreBrowserService ) { super(); this._canvas = document.createElement('canvas'); @@ -112,7 +113,7 @@ export class OverviewRulerRenderer extends Disposable { } })); // device pixel ratio changed - this.register(addDisposableDomListener(window, 'resize', () => { + this.register(addDisposableDomListener(this._coreBrowseService.window, 'resize', () => { this._queueRefresh(true); })); // set the canvas dimensions @@ -142,11 +143,11 @@ export class OverviewRulerRenderer extends Disposable { } private _refreshDrawHeightConstants(): void { - drawHeight.full = Math.round(2 * window.devicePixelRatio); + drawHeight.full = Math.round(2 * this._coreBrowseService.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) * window.devicePixelRatio); + const nonFullHeight = Math.round(Math.max(Math.min(pixelsPerLine, 12), 6) * this._coreBrowseService.dpr); drawHeight.left = nonFullHeight; drawHeight.center = nonFullHeight; drawHeight.right = nonFullHeight; @@ -164,9 +165,9 @@ export class OverviewRulerRenderer extends Disposable { private _refreshCanvasDimensions(): void { this._canvas.style.width = `${this._width}px`; - this._canvas.width = Math.round(this._width * window.devicePixelRatio); + this._canvas.width = Math.round(this._width * this._coreBrowseService.dpr); this._canvas.style.height = `${this._screenElement.clientHeight}px`; - this._canvas.height = Math.round(this._screenElement.clientHeight * window.devicePixelRatio); + this._canvas.height = Math.round(this._screenElement.clientHeight * this._coreBrowseService.dpr); this._refreshDrawConstants(); this._refreshColorZonePadding(); } @@ -220,7 +221,7 @@ export class OverviewRulerRenderer extends Disposable { if (this._animationFrame !== undefined) { return; } - this._animationFrame = window.requestAnimationFrame(() => { + this._animationFrame = this._coreBrowseService.window.requestAnimationFrame(() => { this._refreshDecorations(); this._animationFrame = undefined; }); diff --git a/src/browser/renderer/CustomGlyphs.ts b/src/browser/renderer/CustomGlyphs.ts index 2f32f98122..4c3874c7a8 100644 --- a/src/browser/renderer/CustomGlyphs.ts +++ b/src/browser/renderer/CustomGlyphs.ts @@ -383,7 +383,8 @@ export function tryDrawCustomChar( yOffset: number, scaledCellWidth: number, scaledCellHeight: number, - fontSize: number + fontSize: number, + devicePixelRatio: number ): boolean { const blockElementDefinition = blockElementDefinitions[c]; if (blockElementDefinition) { @@ -399,13 +400,13 @@ export function tryDrawCustomChar( const boxDrawingDefinition = boxDrawingDefinitions[c]; if (boxDrawingDefinition) { - drawBoxDrawingChar(ctx, boxDrawingDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight); + drawBoxDrawingChar(ctx, boxDrawingDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight, devicePixelRatio); return true; } const powerlineDefinition = powerlineDefinitions[c]; if (powerlineDefinition) { - drawPowerlineChar(ctx, powerlineDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight, fontSize); + drawPowerlineChar(ctx, powerlineDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight, fontSize, devicePixelRatio); return true; } @@ -540,12 +541,13 @@ function drawBoxDrawingChar( xOffset: number, yOffset: number, scaledCellWidth: number, - scaledCellHeight: number + scaledCellHeight: number, + devicePixelRatio: number ): void { ctx.strokeStyle = ctx.fillStyle; for (const [fontWeight, instructions] of Object.entries(charDefinition)) { ctx.beginPath(); - ctx.lineWidth = window.devicePixelRatio * Number.parseInt(fontWeight); + ctx.lineWidth = devicePixelRatio * Number.parseInt(fontWeight); let actualInstructions: string; if (typeof instructions === 'function') { const xp = .15; @@ -565,7 +567,7 @@ function drawBoxDrawingChar( if (!args[0] || !args[1]) { continue; } - f(ctx, translateArgs(args, scaledCellWidth, scaledCellHeight, xOffset, yOffset, true)); + f(ctx, translateArgs(args, scaledCellWidth, scaledCellHeight, xOffset, yOffset, true, devicePixelRatio)); } ctx.stroke(); ctx.closePath(); @@ -579,12 +581,13 @@ function drawPowerlineChar( yOffset: number, scaledCellWidth: number, scaledCellHeight: number, - fontSize: number + fontSize: number, + devicePixelRatio: number ): void { ctx.beginPath(); // Scale the stroke with DPR and font size const cssLineWidth = fontSize / 12; - ctx.lineWidth = window.devicePixelRatio * cssLineWidth; + ctx.lineWidth = devicePixelRatio * cssLineWidth; for (const instruction of charDefinition.d.split(' ')) { const type = instruction[0]; const f = svgToCanvasInstructionMap[type]; @@ -626,7 +629,7 @@ const svgToCanvasInstructionMap: { [index: string]: any } = { 'M': (ctx: CanvasRenderingContext2D, args: number[]) => ctx.moveTo(args[0], args[1]) }; -function translateArgs(args: string[], cellWidth: number, cellHeight: number, xOffset: number, yOffset: number, doClamp: boolean, leftPadding: number = 0, rightPadding: number = 0): number[] { +function translateArgs(args: string[], cellWidth: number, cellHeight: number, xOffset: number, yOffset: number, doClamp: boolean, devicePixelRatio: number, leftPadding: number = 0, rightPadding: number = 0): number[] { const result = args.map(e => parseFloat(e) || parseInt(e)); if (result.length < 2) { @@ -635,14 +638,14 @@ function translateArgs(args: string[], cellWidth: number, cellHeight: number, xO for (let x = 0; x < result.length; x += 2) { // Translate from 0-1 to 0-cellWidth - result[x] *= cellWidth - (leftPadding * window.devicePixelRatio) - (rightPadding * window.devicePixelRatio); + result[x] *= cellWidth - (leftPadding * devicePixelRatio) - (rightPadding * devicePixelRatio); // Ensure coordinate doesn't escape cell bounds and round to the nearest 0.5 to ensure a crisp // line at 100% devicePixelRatio if (doClamp && result[x] !== 0) { result[x] = clamp(Math.round(result[x] + 0.5) - 0.5, cellWidth, 0); } // Apply the cell's offset (ie. x*cellWidth) - result[x] += xOffset + (leftPadding * window.devicePixelRatio); + result[x] += xOffset + (leftPadding * devicePixelRatio); } for (let y = 1; y < result.length; y += 2) { diff --git a/src/browser/renderer/DevicePixelObserver.ts b/src/browser/renderer/DevicePixelObserver.ts index 3aea61f616..38d40eeafe 100644 --- a/src/browser/renderer/DevicePixelObserver.ts +++ b/src/browser/renderer/DevicePixelObserver.ts @@ -6,12 +6,12 @@ import { toDisposable } from 'common/Lifecycle'; import { IDisposable } from 'common/Types'; -export function observeDevicePixelDimensions(element: HTMLElement, callback: (deviceWidth: number, deviceHeight: number) => void): IDisposable { +export function observeDevicePixelDimensions(element: HTMLElement, parentWindow: Window & typeof globalThis, callback: (deviceWidth: number, deviceHeight: number) => void): IDisposable { // Observe any resizes to the element and extract the actual pixel size of the element if the // devicePixelContentBoxSize API is supported. This allows correcting rounding errors when // converting between CSS pixels and device pixels which causes blurry rendering when device // pixel ratio is not a round number. - let observer: ResizeObserver | undefined = new ResizeObserver((entries) => { + let observer: ResizeObserver | undefined = new parentWindow.ResizeObserver((entries) => { const entry = entries.find((entry) => entry.target === element); if (!entry) { return; diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index ec9b835c6f..8df6b30286 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -8,7 +8,7 @@ import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSO import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants'; import { Disposable } from 'common/Lifecycle'; import { IColorSet, ILinkifierEvent, ILinkifier2 } from 'browser/Types'; -import { ICharSizeService } from 'browser/services/Services'; +import { ICharSizeService, ICoreBrowserService } from 'browser/services/Services'; import { IOptionsService, IBufferService, IInstantiationService } from 'common/services/Services'; import { EventEmitter, IEvent } from 'common/EventEmitter'; import { color } from 'common/Color'; @@ -51,7 +51,8 @@ export class DomRenderer extends Disposable implements IRenderer { @IInstantiationService instantiationService: IInstantiationService, @ICharSizeService private readonly _charSizeService: ICharSizeService, @IOptionsService private readonly _optionsService: IOptionsService, - @IBufferService private readonly _bufferService: IBufferService + @IBufferService private readonly _bufferService: IBufferService, + @ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService ) { super(); this._rowContainer = document.createElement('div'); @@ -101,16 +102,17 @@ export class DomRenderer extends Disposable implements IRenderer { } private _updateDimensions(): void { - this.dimensions.scaledCharWidth = this._charSizeService.width * window.devicePixelRatio; - this.dimensions.scaledCharHeight = Math.ceil(this._charSizeService.height * window.devicePixelRatio); + const dpr = this._coreBrowserService.dpr; + this.dimensions.scaledCharWidth = this._charSizeService.width * dpr; + this.dimensions.scaledCharHeight = Math.ceil(this._charSizeService.height * dpr); this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._optionsService.rawOptions.letterSpacing); this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._optionsService.rawOptions.lineHeight); this.dimensions.scaledCharLeft = 0; this.dimensions.scaledCharTop = 0; this.dimensions.scaledCanvasWidth = this.dimensions.scaledCellWidth * this._bufferService.cols; this.dimensions.scaledCanvasHeight = this.dimensions.scaledCellHeight * this._bufferService.rows; - this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / window.devicePixelRatio); - this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / window.devicePixelRatio); + this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / dpr); + this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / dpr); this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._bufferService.cols; this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._bufferService.rows; diff --git a/src/browser/services/CoreBrowserService.ts b/src/browser/services/CoreBrowserService.ts index 4eabc895dc..6504e5e463 100644 --- a/src/browser/services/CoreBrowserService.ts +++ b/src/browser/services/CoreBrowserService.ts @@ -9,12 +9,17 @@ export class CoreBrowserService implements ICoreBrowserService { public serviceBrand: undefined; constructor( - private _textarea: HTMLTextAreaElement + private _textarea: HTMLTextAreaElement, + public readonly window: Window & typeof globalThis ) { } + public get dpr(): number { + return this.window.devicePixelRatio; + } + public get isFocused(): boolean { - const docOrShadowRoot = this._textarea.getRootNode ? this._textarea.getRootNode() as Document | ShadowRoot : document; - return docOrShadowRoot.activeElement === this._textarea && document.hasFocus(); + const docOrShadowRoot = this._textarea.getRootNode ? this._textarea.getRootNode() as Document | ShadowRoot : this._textarea.ownerDocument; + return docOrShadowRoot.activeElement === this._textarea && this._textarea.ownerDocument.hasFocus(); } } diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index 13cdaf9c12..7db0e5cd1f 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -11,7 +11,7 @@ import { ScreenDprMonitor } from 'browser/ScreenDprMonitor'; import { addDisposableDomListener } from 'browser/Lifecycle'; import { IColorSet, IRenderDebouncerWithCallback } from 'browser/Types'; import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services'; -import { ICharSizeService, IRenderService } from 'browser/services/Services'; +import { ICharSizeService, ICoreBrowserService, IRenderService } from 'browser/services/Services'; interface ISelectionState { start: [number, number] | undefined; @@ -55,16 +55,17 @@ export class RenderService extends Disposable implements IRenderService { @IOptionsService optionsService: IOptionsService, @ICharSizeService private readonly _charSizeService: ICharSizeService, @IDecorationService decorationService: IDecorationService, - @IBufferService bufferService: IBufferService + @IBufferService bufferService: IBufferService, + @ICoreBrowserService coreBrowserService: ICoreBrowserService ) { super(); this.register({ dispose: () => this._renderer.dispose() }); - this._renderDebouncer = new RenderDebouncer((start, end) => this._renderRows(start, end)); + this._renderDebouncer = new RenderDebouncer(coreBrowserService.window, (start, end) => this._renderRows(start, end)); this.register(this._renderDebouncer); - this._screenDprMonitor = new ScreenDprMonitor(); + this._screenDprMonitor = new ScreenDprMonitor(coreBrowserService.window); this._screenDprMonitor.setListener(() => this.onDevicePixelRatioChange()); this.register(this._screenDprMonitor); @@ -84,12 +85,12 @@ export class RenderService extends Disposable implements IRenderService { // dprchange should handle this case, we need this as well for browsers that don't support the // matchMedia query. - this.register(addDisposableDomListener(window, 'resize', () => this.onDevicePixelRatioChange())); + this.register(addDisposableDomListener(coreBrowserService.window, 'resize', () => this.onDevicePixelRatioChange())); // Detect whether IntersectionObserver is detected and enable renderer pause // and resume based on terminal visibility if so - if ('IntersectionObserver' in window) { - const observer = new IntersectionObserver(e => this._onIntersectionChange(e[e.length - 1]), { threshold: 0 }); + if ('IntersectionObserver' in coreBrowserService.window) { + const observer = new coreBrowserService.window.IntersectionObserver(e => this._onIntersectionChange(e[e.length - 1]), { threshold: 0 }); observer.observe(screenElement); this.register({ dispose: () => observer.disconnect() }); } diff --git a/src/browser/services/SelectionService.test.ts b/src/browser/services/SelectionService.test.ts index d829e39451..f8a15e51cf 100644 --- a/src/browser/services/SelectionService.test.ts +++ b/src/browser/services/SelectionService.test.ts @@ -10,7 +10,7 @@ import { IBufferLine } from 'common/Types'; import { MockBufferService, MockOptionsService, MockCoreService } from 'common/TestUtils.test'; import { BufferLine } from 'common/buffer/BufferLine'; import { IBufferService, IOptionsService } from 'common/services/Services'; -import { MockMouseService, MockRenderService } from 'browser/TestUtils.test'; +import { MockCoreBrowserService, MockMouseService, MockRenderService } from 'browser/TestUtils.test'; import { CellData } from 'common/buffer/CellData'; import { IBuffer } from 'common/buffer/Types'; import { IRenderService } from 'browser/services/Services'; @@ -21,7 +21,7 @@ class TestSelectionService extends SelectionService { optionsService: IOptionsService, renderService: IRenderService ) { - super(null!, null!, null!, bufferService, new MockCoreService(), new MockMouseService(), optionsService, renderService); + super(null!, null!, null!, bufferService, new MockCoreService(), new MockMouseService(), optionsService, renderService, new MockCoreBrowserService()); } public get model(): SelectionModel { return this._model; } diff --git a/src/browser/services/SelectionService.ts b/src/browser/services/SelectionService.ts index aad2c4664a..4ee1ffa1fc 100644 --- a/src/browser/services/SelectionService.ts +++ b/src/browser/services/SelectionService.ts @@ -10,7 +10,7 @@ import * as Browser from 'common/Platform'; import { SelectionModel } from 'browser/selection/SelectionModel'; import { CellData } from 'common/buffer/CellData'; import { EventEmitter, IEvent } from 'common/EventEmitter'; -import { IMouseService, ISelectionService, IRenderService } from 'browser/services/Services'; +import { IMouseService, ISelectionService, IRenderService, ICoreBrowserService } from 'browser/services/Services'; import { IBufferRange, ILinkifier2 } from 'browser/Types'; import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services'; import { getCoordsRelativeToElement } from 'browser/input/Mouse'; @@ -128,7 +128,8 @@ export class SelectionService extends Disposable implements ISelectionService { @ICoreService private readonly _coreService: ICoreService, @IMouseService private readonly _mouseService: IMouseService, @IOptionsService private readonly _optionsService: IOptionsService, - @IRenderService private readonly _renderService: IRenderService + @IRenderService private readonly _renderService: IRenderService, + @ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService ) { super(); @@ -270,7 +271,7 @@ export class SelectionService extends Disposable implements ISelectionService { public refresh(isLinuxMouseSelection?: boolean): void { // Queue the refresh for the renderer if (!this._refreshAnimationFrame) { - this._refreshAnimationFrame = window.requestAnimationFrame(() => this._refresh()); + this._refreshAnimationFrame = this._coreBrowserService.window.requestAnimationFrame(() => this._refresh()); } // If the platform is Linux and the refresh call comes from a mouse event, @@ -406,7 +407,7 @@ export class SelectionService extends Disposable implements ISelectionService { * @param event The mouse event. */ private _getMouseEventScrollAmount(event: MouseEvent): number { - let offset = getCoordsRelativeToElement(window, event, this._screenElement)[1]; + let offset = getCoordsRelativeToElement(this._coreBrowserService.window, event, this._screenElement)[1]; const terminalHeight = this._renderService.dimensions.canvasHeight; if (offset >= 0 && offset <= terminalHeight) { return 0; @@ -491,7 +492,7 @@ export class SelectionService extends Disposable implements ISelectionService { this._screenElement.ownerDocument.addEventListener('mousemove', this._mouseMoveListener); this._screenElement.ownerDocument.addEventListener('mouseup', this._mouseUpListener); } - this._dragScrollIntervalTimer = window.setInterval(() => this._dragScroll(), DRAG_SCROLL_INTERVAL); + this._dragScrollIntervalTimer = this._coreBrowserService.window.setInterval(() => this._dragScroll(), DRAG_SCROLL_INTERVAL); } /** @@ -502,7 +503,7 @@ export class SelectionService extends Disposable implements ISelectionService { this._screenElement.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener); this._screenElement.ownerDocument.removeEventListener('mouseup', this._mouseUpListener); } - clearInterval(this._dragScrollIntervalTimer); + this._coreBrowserService.window.clearInterval(this._dragScrollIntervalTimer); this._dragScrollIntervalTimer = undefined; } diff --git a/src/browser/services/Services.ts b/src/browser/services/Services.ts index 31167faf32..ab91f8a32f 100644 --- a/src/browser/services/Services.ts +++ b/src/browser/services/Services.ts @@ -28,6 +28,16 @@ export interface ICoreBrowserService { serviceBrand: undefined; readonly isFocused: boolean; + /** + * Parent window that the terminal is rendered into. DOM and rendering APIs + * (e.g. requestAnimationFrame) should be invoked in the context of this + * window. + */ + readonly window: Window & typeof globalThis; + /** + * Helper for getting the devicePixelRatio of the parent window. + */ + readonly dpr: number; } export const IMouseService = createDecorator('MouseService');