Skip to content

Commit

Permalink
Make terminal rendering work in popout windows
Browse files Browse the repository at this point in the history
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
  • Loading branch information
mihaip committed Sep 8, 2022
1 parent 2935d9f commit a39a468
Show file tree
Hide file tree
Showing 31 changed files with 255 additions and 176 deletions.
23 changes: 23 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
56 changes: 31 additions & 25 deletions addons/xterm-addon-canvas/src/BaseRenderLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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`);
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -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,
Expand All @@ -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;
Expand All @@ -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();
Expand All @@ -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);
}

Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
27 changes: 14 additions & 13 deletions addons/xterm-addon-canvas/src/CanvasRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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();
}
Expand All @@ -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);
}
}
Expand Down Expand Up @@ -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;
}
Expand Down
Loading

0 comments on commit a39a468

Please sign in to comment.