diff --git a/.eslintrc.json b/.eslintrc.json index e6db42e2..822ee4ba 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -39,7 +39,8 @@ "**/*.js" ], "plugins": [ - "@typescript-eslint" + "@typescript-eslint", + "jsdoc" ], "rules": { "no-extra-semi": "error", @@ -141,6 +142,9 @@ "warn", "always" ], + "jsdoc/check-alignment": 1, + "jsdoc/check-param-names": 1, + "jsdoc/no-multi-asterisks": 1, "keyword-spacing": "warn", "new-parens": "warn", "no-duplicate-imports": "warn", diff --git a/README.md b/README.md index c78ae76e..f672942d 100644 --- a/README.md +++ b/README.md @@ -186,7 +186,7 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**DockerStacks**](https://docker-stacks.com/): Local LAMP/LEMP development studio - [**Codecademy**](https://codecademy.com/): Uses xterm.js in its courses on Bash. - [**Laravel Ssh Web Client**](https://github.com/roke22/Laravel-ssh-client): Laravel server inventory with ssh web client to connect at server using xterm.js -- [**Repl.it**](https://repl.it): Collaborative browser based IDE with support for 50+ different languages. +- [**Replit**](https://replit.com): Collaborative browser based IDE with support for 50+ different languages. - [**TeleType**](https://github.com/akshaykmr/TeleType): cli tool that allows you to share your terminal online conveniently. Show off mad cli-fu, help a colleague, teach, or troubleshoot. - [**Intervue**](https://www.intervue.io): Pair programming for interviews. Multiple programming languages are supported, with results displayed by xterm.js. - [**TRASA**](https://trasa.io): Zero trust access to Web, SSH, RDP, and Database services. diff --git a/addons/xterm-addon-attach/src/AttachAddon.ts b/addons/xterm-addon-attach/src/AttachAddon.ts index 9fbd796b..7fd8df29 100644 --- a/addons/xterm-addon-attach/src/AttachAddon.ts +++ b/addons/xterm-addon-attach/src/AttachAddon.ts @@ -47,16 +47,14 @@ export class AttachAddon implements ITerminalAddon { } private _sendData(data: string): void { - // TODO: do something better than just swallowing - // the data if the socket is not in a working condition - if (this._socket.readyState !== 1) { + if (!this._checkOpenSocket()) { return; } this._socket.send(data); } private _sendBinary(data: string): void { - if (this._socket.readyState !== 1) { + if (!this._checkOpenSocket()) { return; } const buffer = new Uint8Array(data.length); @@ -65,6 +63,22 @@ export class AttachAddon implements ITerminalAddon { } this._socket.send(buffer); } + + private _checkOpenSocket(): boolean { + switch (this._socket.readyState) { + case WebSocket.OPEN: + return true; + case WebSocket.CONNECTING: + throw new Error('Attach addon was loaded before socket was open'); + case WebSocket.CLOSING: + console.warn('Attach addon socket is closing'); + return false; + case WebSocket.CLOSED: + throw new Error('Attach addon socket is closed'); + default: + throw new Error('Unexpected socket state'); + } + } } function addSocketListener(socket: WebSocket, type: K, handler: (this: WebSocket, ev: WebSocketEventMap[K]) => any): IDisposable { diff --git a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts index 54fea5f2..3d16467d 100644 --- a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts @@ -11,34 +11,40 @@ import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; import { IRasterizedGlyph, IRenderDimensions, ISelectionRenderModel, ITextureAtlas } from 'browser/renderer/shared/Types'; import { createSelectionRenderModel } from 'browser/renderer/shared/SelectionRenderModel'; import { ICoreBrowserService, IThemeService } from 'browser/services/Services'; -import { IColorSet, ReadonlyColorSet } from 'browser/Types'; +import { ReadonlyColorSet } from 'browser/Types'; import { CellData } from 'common/buffer/CellData'; import { WHITESPACE_CELL_CODE } from 'common/buffer/Constants'; import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; -import { ICellData } from 'common/Types'; +import { ICellData, IDisposable } from 'common/Types'; import { Terminal } from 'xterm'; import { IRenderLayer } from './Types'; import { CellColorResolver } from 'browser/renderer/shared/CellColorResolver'; import { Disposable, toDisposable } from 'common/Lifecycle'; +import { isSafari } from 'common/Platform'; +import { EventEmitter, forwardEvent } from 'common/EventEmitter'; export abstract class BaseRenderLayer extends Disposable implements IRenderLayer { private _canvas: HTMLCanvasElement; protected _ctx!: CanvasRenderingContext2D; - private _scaledCharWidth: number = 0; - private _scaledCharHeight: number = 0; - private _scaledCellWidth: number = 0; - private _scaledCellHeight: number = 0; - private _scaledCharLeft: number = 0; - private _scaledCharTop: number = 0; + private _deviceCharWidth: number = 0; + private _deviceCharHeight: number = 0; + private _deviceCellWidth: number = 0; + private _deviceCellHeight: number = 0; + private _deviceCharLeft: number = 0; + private _deviceCharTop: number = 0; protected _selectionModel: ISelectionRenderModel = createSelectionRenderModel(); private _cellColorResolver: CellColorResolver; - private _bitmapGenerator?: BitmapGenerator; + private _bitmapGenerator: (BitmapGenerator | undefined)[] = []; protected _charAtlas!: ITextureAtlas; + private _charAtlasDisposable?: IDisposable; public get canvas(): HTMLCanvasElement { return this._canvas; } - public get cacheCanvas(): HTMLCanvasElement { return this._charAtlas?.cacheCanvas!; } + public get cacheCanvas(): HTMLCanvasElement { return this._charAtlas?.pages[0].canvas!; } + + private readonly _onAddTextureAtlasCanvas = this.register(new EventEmitter()); + public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event; constructor( private readonly _terminal: Terminal, @@ -79,7 +85,6 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer } } - public handleOptionsChanged(): void {} public handleBlur(): void {} public handleFocus(): void {} public handleCursorMove(): void {} @@ -113,25 +118,29 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer * @param colorSet The color set to use for the char atlas. */ private _refreshCharAtlas(colorSet: ReadonlyColorSet): void { - if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) { + if (this._deviceCharWidth <= 0 && this._deviceCharHeight <= 0) { return; } - this._charAtlas = acquireTextureAtlas(this._terminal, colorSet, this._scaledCellWidth, this._scaledCellHeight, this._scaledCharWidth, this._scaledCharHeight, this._coreBrowserService.dpr); + this._charAtlasDisposable?.dispose(); + this._charAtlas = acquireTextureAtlas(this._terminal, colorSet, this._deviceCellWidth, this._deviceCellHeight, this._deviceCharWidth, this._deviceCharHeight, this._coreBrowserService.dpr); + this._charAtlasDisposable = forwardEvent(this._charAtlas.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas); this._charAtlas.warmUp(); - this._bitmapGenerator = new BitmapGenerator(this._charAtlas.cacheCanvas); + for (let i = 0; i < this._charAtlas.pages.length; i++) { + this._bitmapGenerator[i] = new BitmapGenerator(this._charAtlas.pages[i].canvas); + } } public resize(dim: IRenderDimensions): void { - this._scaledCellWidth = dim.scaledCellWidth; - this._scaledCellHeight = dim.scaledCellHeight; - this._scaledCharWidth = dim.scaledCharWidth; - this._scaledCharHeight = dim.scaledCharHeight; - this._scaledCharLeft = dim.scaledCharLeft; - this._scaledCharTop = dim.scaledCharTop; - this._canvas.width = dim.scaledCanvasWidth; - this._canvas.height = dim.scaledCanvasHeight; - this._canvas.style.width = `${dim.canvasWidth}px`; - this._canvas.style.height = `${dim.canvasHeight}px`; + this._deviceCellWidth = dim.device.cell.width; + this._deviceCellHeight = dim.device.cell.height; + this._deviceCharWidth = dim.device.char.width; + this._deviceCharHeight = dim.device.char.height; + this._deviceCharLeft = dim.device.char.left; + this._deviceCharTop = dim.device.char.top; + this._canvas.width = dim.device.canvas.width; + this._canvas.height = dim.device.canvas.height; + this._canvas.style.width = `${dim.css.canvas.width}px`; + this._canvas.style.height = `${dim.css.canvas.height}px`; // Draw the background if this is an opaque layer if (!this._alpha) { @@ -156,24 +165,24 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer */ protected _fillCells(x: number, y: number, width: number, height: number): void { this._ctx.fillRect( - x * this._scaledCellWidth, - y * this._scaledCellHeight, - width * this._scaledCellWidth, - height * this._scaledCellHeight); + x * this._deviceCellWidth, + y * this._deviceCellHeight, + width * this._deviceCellWidth, + height * this._deviceCellHeight); } /** - * Fills a 1px line (2px on HDPI) at the middle of the cell. This uses the - * existing fillStyle on the context. - * @param x The column to fill. - * @param y The row to fill. - */ + * Fills a 1px line (2px on HDPI) at the middle of the cell. This uses the + * existing fillStyle on the context. + * @param x The column to fill. + * @param y The row to fill. + */ protected _fillMiddleLineAtCells(x: number, y: number, width: number = 1): void { - const cellOffset = Math.ceil(this._scaledCellHeight * 0.5); + const cellOffset = Math.ceil(this._deviceCellHeight * 0.5); this._ctx.fillRect( - x * this._scaledCellWidth, - (y + 1) * this._scaledCellHeight - cellOffset - this._coreBrowserService.dpr, - width * this._scaledCellWidth, + x * this._deviceCellWidth, + (y + 1) * this._deviceCellHeight - cellOffset - this._coreBrowserService.dpr, + width * this._deviceCellWidth, this._coreBrowserService.dpr); } @@ -185,9 +194,9 @@ export abstract class BaseRenderLayer extends Disposable 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 - this._coreBrowserService.dpr - 1 /* Ensure it's drawn within the cell */, - width * this._scaledCellWidth, + x * this._deviceCellWidth, + (y + 1) * this._deviceCellHeight + pixelOffset - this._coreBrowserService.dpr - 1 /* Ensure it's drawn within the cell */, + width * this._deviceCellWidth, this._coreBrowserService.dpr); } @@ -198,10 +207,10 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer 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 - lineWidth - 1; + const xLeft = (x + xOffset) * this._deviceCellWidth; + const xMid = (x + xOffset + 0.5) * this._deviceCellWidth; + const xRight = (x + xOffset + 1) * this._deviceCellWidth; + const yMid = (y + 1) * this._deviceCellHeight - lineWidth - 1; const yMidBot = yMid - lineWidth; const yMidTop = yMid + lineWidth; this._ctx.moveTo(xLeft, yMid); @@ -227,12 +236,12 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer 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 - lineWidth - 1; + const xLeft = x * this._deviceCellWidth; + const yMid = (y + 1) * this._deviceCellHeight - lineWidth - 1; this._ctx.moveTo(xLeft, yMid); for (let xOffset = 0; xOffset < width; xOffset++) { - // const xLeft = x * this._scaledCellWidth; - const xRight = (x + width + xOffset) * this._scaledCellWidth; + // const xLeft = x * this._deviceCellWidth; + const xRight = (x + width + xOffset) * this._deviceCellWidth; this._ctx.lineTo(xRight, yMid); } this._ctx.stroke(); @@ -247,9 +256,9 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer 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 - lineWidth - 1; + const xLeft = x * this._deviceCellWidth; + const xRight = (x + width) * this._deviceCellWidth; + const yMid = (y + 1) * this._deviceCellHeight - lineWidth - 1; this._ctx.moveTo(xLeft, yMid); this._ctx.lineTo(xRight, yMid); this._ctx.stroke(); @@ -265,10 +274,10 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer */ protected _fillLeftLineAtCell(x: number, y: number, width: number): void { this._ctx.fillRect( - x * this._scaledCellWidth, - y * this._scaledCellHeight, + x * this._deviceCellWidth, + y * this._deviceCellHeight, this._coreBrowserService.dpr * width, - this._scaledCellHeight); + this._deviceCellHeight); } /** @@ -281,10 +290,10 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer const lineWidth = this._coreBrowserService.dpr; this._ctx.lineWidth = lineWidth; this._ctx.strokeRect( - x * this._scaledCellWidth + lineWidth / 2, - y * this._scaledCellHeight + (lineWidth / 2), - width * this._scaledCellWidth - lineWidth, - (height * this._scaledCellHeight) - lineWidth); + x * this._deviceCellWidth + lineWidth / 2, + y * this._deviceCellHeight + (lineWidth / 2), + width * this._deviceCellWidth - lineWidth, + (height * this._deviceCellHeight) - lineWidth); } /** @@ -309,17 +318,17 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer protected _clearCells(x: number, y: number, width: number, height: number): void { if (this._alpha) { this._ctx.clearRect( - x * this._scaledCellWidth, - y * this._scaledCellHeight, - width * this._scaledCellWidth, - height * this._scaledCellHeight); + x * this._deviceCellWidth, + y * this._deviceCellHeight, + width * this._deviceCellWidth, + height * this._deviceCellHeight); } else { this._ctx.fillStyle = this._themeService.colors.background.css; this._ctx.fillRect( - x * this._scaledCellWidth, - y * this._scaledCellHeight, - width * this._scaledCellWidth, - height * this._scaledCellHeight); + x * this._deviceCellWidth, + y * this._deviceCellHeight, + width * this._deviceCellWidth, + height * this._deviceCellHeight); } } @@ -330,7 +339,6 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer * @param cell The cell data for the character to draw. * @param x The column to draw at. * @param y The row to draw at. - * @param color The color of the character. */ protected _fillCharTrueColor(cell: CellData, x: number, y: number): void { this._ctx.font = this._getFont(false, false); @@ -340,15 +348,15 @@ export abstract class BaseRenderLayer extends Disposable 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, this._coreBrowserService.dpr); + drawSuccess = tryDrawCustomChar(this._ctx, cell.getChars(), x * this._deviceCellWidth, y * this._deviceCellHeight, this._deviceCellWidth, this._deviceCellHeight, this._optionsService.rawOptions.fontSize, this._coreBrowserService.dpr); } // Draw the character if (!drawSuccess) { this._ctx.fillText( cell.getChars(), - x * this._scaledCellWidth + this._scaledCharLeft, - y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight); + x * this._deviceCellWidth + this._deviceCharLeft, + y * this._deviceCellHeight + this._deviceCharTop + this._deviceCharHeight); } } @@ -358,7 +366,7 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer */ protected _drawChars(cell: ICellData, x: number, y: number): void { const chars = cell.getChars(); - this._cellColorResolver.resolve(cell, x, y); + this._cellColorResolver.resolve(cell, x, this._bufferService.buffer.ydisp + y); let glyph: IRasterizedGlyph; if (chars && chars.length > 1) { glyph = this._charAtlas.getRasterizedGlyphCombinedChar(chars, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext); @@ -368,18 +376,21 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer this._ctx.save(); this._clipRow(y); // Draw the image, use the bitmap if it's available - if (this._charAtlas.hasCanvasChanged) { - this._bitmapGenerator?.refresh(); - this._charAtlas.hasCanvasChanged = false; + if (this._charAtlas.pages[glyph.texturePage].hasCanvasChanged) { + if (!this._bitmapGenerator[glyph.texturePage]) { + this._bitmapGenerator[glyph.texturePage] = new BitmapGenerator(this._charAtlas.pages[glyph.texturePage].canvas); + } + this._bitmapGenerator[glyph.texturePage]?.refresh(); + this._charAtlas.pages[glyph.texturePage].hasCanvasChanged = false; } this._ctx.drawImage( - this._bitmapGenerator?.bitmap || this._charAtlas!.cacheCanvas, + this._bitmapGenerator[glyph.texturePage]?.bitmap || this._charAtlas!.pages[glyph.texturePage].canvas, glyph.texturePosition.x, glyph.texturePosition.y, glyph.size.x, glyph.size.y, - x * this._scaledCellWidth - glyph.offset.x, - y * this._scaledCellHeight - glyph.offset.y, + x * this._deviceCellWidth - glyph.offset.x, + y * this._deviceCellHeight - glyph.offset.y, glyph.size.x, glyph.size.y ); @@ -394,9 +405,9 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer this._ctx.beginPath(); this._ctx.rect( 0, - y * this._scaledCellHeight, - this._bufferService.cols * this._scaledCellWidth, - this._scaledCellHeight); + y * this._deviceCellHeight, + this._bufferService.cols * this._deviceCellWidth, + this._deviceCellHeight); this._ctx.clip(); } @@ -436,6 +447,10 @@ class BitmapGenerator { public refresh(): void { // Clear the bitmap immediately as it's stale this._bitmap = undefined; + // Disable ImageBitmaps on Safari because of https://bugs.webkit.org/show_bug.cgi?id=149990 + if (isSafari) { + return; + } if (this._commitTimeout === undefined) { this._commitTimeout = window.setTimeout(() => this._generate(), GLYPH_BITMAP_COMMIT_DELAY); } diff --git a/addons/xterm-addon-canvas/src/CanvasAddon.ts b/addons/xterm-addon-canvas/src/CanvasAddon.ts index 1dc607e5..2a13c95e 100644 --- a/addons/xterm-addon-canvas/src/CanvasAddon.ts +++ b/addons/xterm-addon-canvas/src/CanvasAddon.ts @@ -4,7 +4,7 @@ */ import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services'; -import { IColorSet } from 'browser/Types'; +import { IColorSet, ITerminal } from 'browser/Types'; import { CanvasRenderer } from './CanvasRenderer'; import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; import { ITerminalAddon, Terminal } from 'xterm'; @@ -17,33 +17,38 @@ export class CanvasAddon extends Disposable implements ITerminalAddon { private readonly _onChangeTextureAtlas = this.register(new EventEmitter()); public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event; + private readonly _onAddTextureAtlasCanvas = this.register(new EventEmitter()); + public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event; public get textureAtlas(): HTMLCanvasElement | undefined { return this._renderer?.textureAtlas; } public activate(terminal: Terminal): void { - const core = (terminal as any)._core; + const core = (terminal as any)._core as ITerminal; if (!terminal.element) { this.register(core.onWillOpen(() => this.activate(terminal))); return; } this._terminal = terminal; - const bufferService: IBufferService = core._bufferService; - const renderService: IRenderService = core._renderService; - const characterJoinerService: ICharacterJoinerService = core._characterJoinerService; - const charSizeService: ICharSizeService = core._charSizeService; - const coreService: ICoreService = core.coreService; - const coreBrowserService: ICoreBrowserService = core._coreBrowserService; - const decorationService: IDecorationService = core._decorationService; - const optionsService: IOptionsService = core.optionsService; - const themeService: IThemeService = core._themeService; - const screenElement: HTMLElement = core.screenElement; + const coreService = core.coreService; + const optionsService = core.optionsService; + const screenElement = core.screenElement!; const linkifier = core.linkifier2; + const unsafeCore = core as any; + const bufferService: IBufferService = unsafeCore._bufferService; + const renderService: IRenderService = unsafeCore._renderService; + const characterJoinerService: ICharacterJoinerService = unsafeCore._characterJoinerService; + const charSizeService: ICharSizeService = unsafeCore._charSizeService; + const coreBrowserService: ICoreBrowserService = unsafeCore._coreBrowserService; + const decorationService: IDecorationService = unsafeCore._decorationService; + const themeService: IThemeService = unsafeCore._themeService; + this._renderer = new CanvasRenderer(terminal, screenElement, linkifier, bufferService, charSizeService, optionsService, characterJoinerService, coreService, coreBrowserService, decorationService, themeService); this.register(forwardEvent(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas)); + this.register(forwardEvent(this._renderer.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas)); renderService.setRenderer(this._renderer); renderService.handleResize(bufferService.cols, bufferService.rows); diff --git a/addons/xterm-addon-canvas/src/CanvasRenderer.ts b/addons/xterm-addon-canvas/src/CanvasRenderer.ts index ff92e94b..090d0e09 100644 --- a/addons/xterm-addon-canvas/src/CanvasRenderer.ts +++ b/addons/xterm-addon-canvas/src/CanvasRenderer.ts @@ -5,10 +5,11 @@ import { removeTerminalFromCache } from 'browser/renderer/shared/CharAtlasCache'; import { observeDevicePixelDimensions } from 'browser/renderer/shared/DevicePixelObserver'; +import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils'; import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types'; import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IThemeService } from 'browser/services/Services'; -import { IColorSet, ILinkifier2, ReadonlyColorSet } from 'browser/Types'; -import { EventEmitter } from 'common/EventEmitter'; +import { ILinkifier2 } from 'browser/Types'; +import { EventEmitter, forwardEvent } from 'common/EventEmitter'; import { Disposable, toDisposable } from 'common/Lifecycle'; import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; import { Terminal } from 'xterm'; @@ -28,6 +29,8 @@ export class CanvasRenderer extends Disposable implements IRenderer { public readonly onRequestRedraw = this._onRequestRedraw.event; private readonly _onChangeTextureAtlas = this.register(new EventEmitter()); public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event; + private readonly _onAddTextureAtlasCanvas = this.register(new EventEmitter()); + public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event; constructor( private readonly _terminal: Terminal, @@ -50,27 +53,14 @@ export class CanvasRenderer extends Disposable implements IRenderer { new LinkRenderLayer(this._terminal, this._screenElement, 2, linkifier2, this._bufferService, this._optionsService, decorationService, this._coreBrowserService, _themeService), new CursorRenderLayer(this._terminal, this._screenElement, 3, this._onRequestRedraw, this._bufferService, this._optionsService, coreService, this._coreBrowserService, decorationService, _themeService) ]; - this.dimensions = { - scaledCharWidth: 0, - scaledCharHeight: 0, - scaledCellWidth: 0, - scaledCellHeight: 0, - scaledCharLeft: 0, - scaledCharTop: 0, - scaledCanvasWidth: 0, - scaledCanvasHeight: 0, - canvasWidth: 0, - canvasHeight: 0, - actualCellWidth: 0, - actualCellHeight: 0 - }; + for (const layer of this._renderLayers) { + forwardEvent(layer.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas); + } + this.dimensions = createRenderDimensions(); this._devicePixelRatio = this._coreBrowserService.dpr; this._updateDimensions(); this.register(observeDevicePixelDimensions(this._renderLayers[0].canvas, this._coreBrowserService.window, (w, h) => this._setCanvasDevicePixelDimensions(w, h))); - - this.handleOptionsChanged(); - this.register(toDisposable(() => { for (const l of this._renderLayers) { l.dispose(); @@ -102,8 +92,8 @@ export class CanvasRenderer extends Disposable implements IRenderer { } // Resize the screen - this._screenElement.style.width = `${this.dimensions.canvasWidth}px`; - this._screenElement.style.height = `${this.dimensions.canvasHeight}px`; + this._screenElement.style.width = `${this.dimensions.css.canvas.width}px`; + this._screenElement.style.height = `${this.dimensions.css.canvas.height}px`; } public handleCharSizeChanged(): void { @@ -130,10 +120,6 @@ export class CanvasRenderer extends Disposable implements IRenderer { this._runOperation(l => l.handleCursorMove()); } - public handleOptionsChanged(): void { - this._runOperation(l => l.handleOptionsChanged()); - } - public clear(): void { this._runOperation(l => l.reset()); } @@ -170,23 +156,23 @@ export class CanvasRenderer extends Disposable implements IRenderer { // See the WebGL renderer for an explanation of this section. 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 / 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; + this.dimensions.device.char.width = Math.floor(this._charSizeService.width * dpr); + this.dimensions.device.char.height = Math.ceil(this._charSizeService.height * dpr); + this.dimensions.device.cell.height = Math.floor(this.dimensions.device.char.height * this._optionsService.rawOptions.lineHeight); + this.dimensions.device.char.top = this._optionsService.rawOptions.lineHeight === 1 ? 0 : Math.round((this.dimensions.device.cell.height - this.dimensions.device.char.height) / 2); + this.dimensions.device.cell.width = this.dimensions.device.char.width + Math.round(this._optionsService.rawOptions.letterSpacing); + this.dimensions.device.char.left = Math.floor(this._optionsService.rawOptions.letterSpacing / 2); + this.dimensions.device.canvas.height = this._bufferService.rows * this.dimensions.device.cell.height; + this.dimensions.device.canvas.width = this._bufferService.cols * this.dimensions.device.cell.width; + this.dimensions.css.canvas.height = Math.round(this.dimensions.device.canvas.height / dpr); + this.dimensions.css.canvas.width = Math.round(this.dimensions.device.canvas.width / dpr); + this.dimensions.css.cell.height = this.dimensions.css.canvas.height / this._bufferService.rows; + this.dimensions.css.cell.width = this.dimensions.css.canvas.width / this._bufferService.cols; } private _setCanvasDevicePixelDimensions(width: number, height: number): void { - this.dimensions.scaledCanvasHeight = height; - this.dimensions.scaledCanvasWidth = width; + this.dimensions.device.canvas.height = height; + this.dimensions.device.canvas.width = width; // Resize all render layers for (const l of this._renderLayers) { l.resize(this.dimensions); diff --git a/addons/xterm-addon-canvas/src/CursorRenderLayer.ts b/addons/xterm-addon-canvas/src/CursorRenderLayer.ts index ab8b1e66..83806697 100644 --- a/addons/xterm-addon-canvas/src/CursorRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/CursorRenderLayer.ts @@ -58,6 +58,7 @@ export class CursorRenderLayer extends BaseRenderLayer { 'block': this._renderBlockCursor.bind(this), 'underline': this._renderUnderlineCursor.bind(this) }; + this.register(optionsService.onOptionChange(() => this._handleOptionsChanged())); this.register(toDisposable(() => { this._cursorBlinkStateManager?.dispose(); this._cursorBlinkStateManager = undefined; @@ -79,7 +80,7 @@ export class CursorRenderLayer extends BaseRenderLayer { public reset(): void { this._clearCursor(); this._cursorBlinkStateManager?.restartBlinkAnimation(); - this.handleOptionsChanged(); + this._handleOptionsChanged(); } public handleBlur(): void { @@ -92,7 +93,7 @@ export class CursorRenderLayer extends BaseRenderLayer { this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y }); } - public handleOptionsChanged(): void { + private _handleOptionsChanged(): void { if (this._optionsService.rawOptions.cursorBlink) { if (!this._cursorBlinkStateManager) { this._cursorBlinkStateManager = new CursorBlinkStateManager(this._coreBrowserService.isFocused, () => { diff --git a/addons/xterm-addon-canvas/src/TextRenderLayer.ts b/addons/xterm-addon-canvas/src/TextRenderLayer.ts index ca9eae56..66fc5106 100644 --- a/addons/xterm-addon-canvas/src/TextRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/TextRenderLayer.ts @@ -45,6 +45,7 @@ export class TextRenderLayer extends BaseRenderLayer { ) { super(terminal, container, 'text', zIndex, alpha, themeService, bufferService, optionsService, decorationService, coreBrowserService); this._state = new GridCache(); + this.register(optionsService.onSpecificOptionChange('allowTransparency', value => this._setTransparency(value))); } public resize(dim: IRenderDimensions): void { @@ -52,8 +53,8 @@ export class TextRenderLayer extends BaseRenderLayer { // Clear the character width cache if the font or width has changed const terminalFont = this._getFont(false, false); - if (this._characterWidth !== dim.scaledCharWidth || this._characterFont !== terminalFont) { - this._characterWidth = dim.scaledCharWidth; + if (this._characterWidth !== dim.device.char.width || this._characterFont !== terminalFont) { + this._characterWidth = dim.device.char.width; this._characterFont = terminalFont; this._characterOverlapCache = {}; } @@ -251,10 +252,6 @@ export class TextRenderLayer extends BaseRenderLayer { this._drawForeground(firstRow, lastRow); } - public handleOptionsChanged(): void { - this._setTransparency(this._optionsService.rawOptions.allowTransparency); - } - /** * Whether a character is overlapping to the next cell. */ diff --git a/addons/xterm-addon-canvas/src/Types.d.ts b/addons/xterm-addon-canvas/src/Types.d.ts index 1840284f..73e6c836 100644 --- a/addons/xterm-addon-canvas/src/Types.d.ts +++ b/addons/xterm-addon-canvas/src/Types.d.ts @@ -4,24 +4,8 @@ */ import { IDisposable } from 'common/Types'; -import { IColorSet, ReadonlyColorSet } from 'browser/Types'; import { IEvent } from 'common/EventEmitter'; - -// TODO: Use core interfaces -export interface IRenderDimensions { - scaledCharWidth: number; - scaledCharHeight: number; - scaledCellWidth: number; - scaledCellHeight: number; - scaledCharLeft: number; - scaledCharTop: number; - scaledCanvasWidth: number; - scaledCanvasHeight: number; - canvasWidth: number; - canvasHeight: number; - actualCellWidth: number; - actualCellHeight: number; -} +import { IRenderDimensions } from 'browser/renderer/shared/Types'; export interface IRequestRedrawEvent { start: number; @@ -58,13 +42,14 @@ export interface IRenderLayer extends IDisposable { readonly canvas: HTMLCanvasElement; readonly cacheCanvas: HTMLCanvasElement; + readonly onAddTextureAtlasCanvas: IEvent; /** * Called when the terminal loses focus. */ handleBlur(): void; /** - * * Called when the terminal gets focus. + * Called when the terminal gets focus. */ handleFocus(): void; @@ -73,11 +58,6 @@ export interface IRenderLayer extends IDisposable { */ handleCursorMove(): void; - /** - * Called when options change. - */ - handleOptionsChanged(): void; - /** * Called when the data in the grid has changed (or needs to be rendered * again). diff --git a/addons/xterm-addon-canvas/typings/xterm-addon-canvas.d.ts b/addons/xterm-addon-canvas/typings/xterm-addon-canvas.d.ts index 6a2b98d4..c983825c 100644 --- a/addons/xterm-addon-canvas/typings/xterm-addon-canvas.d.ts +++ b/addons/xterm-addon-canvas/typings/xterm-addon-canvas.d.ts @@ -17,6 +17,11 @@ declare module 'xterm-addon-canvas' { */ public readonly onChangeTextureAtlas: IEvent; + /** + * An event that is fired when the a new page is added to the texture atlas. + */ + public readonly onAddTextureAtlasCanvas: IEvent; + constructor(); /** diff --git a/addons/xterm-addon-fit/src/FitAddon.ts b/addons/xterm-addon-fit/src/FitAddon.ts index 7b9c228f..6b3df6f0 100644 --- a/addons/xterm-addon-fit/src/FitAddon.ts +++ b/addons/xterm-addon-fit/src/FitAddon.ts @@ -4,6 +4,7 @@ */ import { Terminal, ITerminalAddon } from 'xterm'; +import { IRenderDimensions } from 'browser/renderer/shared/Types'; interface ITerminalDimensions { /** @@ -58,8 +59,9 @@ export class FitAddon implements ITerminalAddon { // TODO: Remove reliance on private API const core = (this._terminal as any)._core; + const dims: IRenderDimensions = core._renderService.dimensions; - if (core._renderService.dimensions.actualCellWidth === 0 || core._renderService.dimensions.actualCellHeight === 0) { + if (dims.css.cell.width === 0 || dims.css.cell.height === 0) { return undefined; } @@ -81,8 +83,8 @@ export class FitAddon implements ITerminalAddon { const availableHeight = parentElementHeight - elementPaddingVer; const availableWidth = parentElementWidth - elementPaddingHor - scrollbarWidth; const geometry = { - cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / core._renderService.dimensions.actualCellWidth)), - rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / core._renderService.dimensions.actualCellHeight)) + cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / dims.css.cell.width)), + rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / dims.css.cell.height)) }; return geometry; } diff --git a/addons/xterm-addon-fit/src/tsconfig.json b/addons/xterm-addon-fit/src/tsconfig.json index f3e409d1..3bfbea67 100644 --- a/addons/xterm-addon-fit/src/tsconfig.json +++ b/addons/xterm-addon-fit/src/tsconfig.json @@ -13,10 +13,20 @@ "strict": true, "types": [ "../../../node_modules/@types/mocha" - ] + ], + "paths": { + "browser/*": [ + "../../../src/browser/*" + ] + } }, "include": [ "./**/*", "../../../typings/xterm.d.ts" + ], + "references": [ + { + "path": "../../../src/browser" + } ] } diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 249dd594..3f71af29 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -126,7 +126,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon { * doesn't exist, do nothing. * @param term The search term. * @param searchOptions Search options. - * @return Whether a result was found. + * @returns Whether a result was found. */ public findNext(term: string, searchOptions?: ISearchOptions): boolean { if (!this._terminal) { @@ -307,7 +307,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon { * doesn't exist, do nothing. * @param term The search term. * @param searchOptions Search options. - * @return Whether a result was found. + * @returns Whether a result was found. */ public findPrevious(term: string, searchOptions?: ISearchOptions): boolean { if (!this._terminal) { @@ -477,10 +477,10 @@ export class SearchAddon extends Disposable implements ITerminalAddon { * started on an earlier line then it is skipped since it will be properly searched when the terminal line that the * text starts on is searched. * @param term The search term. - * @param position The position to start the search. + * @param searchPosition The position to start the search. * @param searchOptions Search options. * @param isReverseSearch Whether the search should start from the right side of the terminal and search to the left. - * @return The search result if it was found. + * @returns The search result if it was found. */ protected _findInLine(term: string, searchPosition: ISearchPosition, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult | undefined { const terminal = this._terminal!; @@ -627,7 +627,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon { * Wide characters will count as two columns in the resulting string. This * function is useful for getting the actual text underneath the raw selection * position. - * @param line The line being translated. + * @param lineIndex The index of the line being translated. * @param trimRight Whether to trim whitespace to the right. */ private _translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): LineCacheEntry { @@ -662,7 +662,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon { /** * Selects and scrolls to a result. * @param result The result to select. - * @return Whether a result was selected. + * @returns Whether a result was selected. */ private _selectResult(result: ISearchResult | undefined, options?: ISearchDecorationOptions, noScroll?: boolean): boolean { const terminal = this._terminal!; @@ -702,10 +702,10 @@ export class SearchAddon extends Disposable implements ITerminalAddon { } /** - * Applies styles to the decoration when it is rendered - * @param element the decoration's element - * @param backgroundColor the background color to apply - * @param borderColor the border color to apply + * Applies styles to the decoration when it is rendered. + * @param element The decoration's element. + * @param borderColor The border color to apply. + * @param isActiveResult Whether the element is part of the active search result. * @returns */ private _applyStyles(element: HTMLElement, borderColor: string | undefined, isActiveResult: boolean): void { diff --git a/addons/xterm-addon-unicode11/src/UnicodeV11.ts b/addons/xterm-addon-unicode11/src/UnicodeV11.ts index d9d548d6..b616091a 100644 --- a/addons/xterm-addon-unicode11/src/UnicodeV11.ts +++ b/addons/xterm-addon-unicode11/src/UnicodeV11.ts @@ -4,7 +4,6 @@ */ import { IUnicodeVersionProvider } from 'xterm'; -import { fill } from 'common/TypedArrayUtils'; type CharWidth = 0 | 1 | 2; @@ -198,15 +197,15 @@ export class UnicodeV11 implements IUnicodeVersionProvider { constructor() { if (!table) { table = new Uint8Array(65536); - fill(table, 1); + table.fill(1); table[0] = 0; - fill(table, 0, 1, 32); - fill(table, 0, 0x7f, 0xa0); + table.fill(0, 1, 32); + table.fill(0, 0x7f, 0xa0); for (let r = 0; r < BMP_COMBINING.length; ++r) { - fill(table, 0, BMP_COMBINING[r][0], BMP_COMBINING[r][1] + 1); + table.fill(0, BMP_COMBINING[r][0], BMP_COMBINING[r][1] + 1); } for (let r = 0; r < BMP_WIDE.length; ++r) { - fill(table, 2, BMP_WIDE[r][0], BMP_WIDE[r][1] + 1); + table.fill(2, BMP_WIDE[r][0], BMP_WIDE[r][1] + 1); } } } diff --git a/addons/xterm-addon-web-links/src/WebLinkProvider.ts b/addons/xterm-addon-web-links/src/WebLinkProvider.ts index 8e9a8408..fafbb614 100644 --- a/addons/xterm-addon-web-links/src/WebLinkProvider.ts +++ b/addons/xterm-addon-web-links/src/WebLinkProvider.ts @@ -47,6 +47,12 @@ export class LinkComputer { const [line, startLineIndex] = LinkComputer._translateBufferLineToStringWithWrap(y - 1, false, terminal); + // Don't try if the wrapped line if excessively large as the regex matching will block the main + // thread. + if (line.length > 1024) { + return []; + } + let match; let stringIndex = -1; const result: ILink[] = []; @@ -105,9 +111,8 @@ export class LinkComputer { /** * Gets the entire line for the buffer line - * @param line The line being translated. + * @param lineIndex The index of the line being translated. * @param trimRight Whether to trim whitespace to the right. - * @param terminal The terminal */ private static _translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean, terminal: Terminal): [string, number] { let lineString = ''; diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index 511c9381..ce71f7b6 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -5,13 +5,12 @@ import { createProgram, PROJECTION_MATRIX } from './WebglUtils'; import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel } from './Types'; -import { fill } from 'common/TypedArrayUtils'; import { NULL_CELL_CODE } from 'common/buffer/Constants'; import { Terminal } from 'xterm'; -import { IColorSet } from 'browser/Types'; import { IRasterizedGlyph, IRenderDimensions, ITextureAtlas } from 'browser/renderer/shared/Types'; import { Disposable, toDisposable } from 'common/Lifecycle'; import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; +import { TextureAtlas } from 'browser/renderer/shared/TextureAtlas'; interface IVertices { attributes: Float32Array; @@ -30,8 +29,9 @@ const enum VertexAttribLocations { CELL_POSITION = 1, OFFSET = 2, SIZE = 3, - TEXCOORD = 4, - TEXSIZE = 5 + TEXPAGE = 4, + TEXCOORD = 5, + TEXSIZE = 6 } const vertexShaderSource = `#version 300 es @@ -39,6 +39,7 @@ layout (location = ${VertexAttribLocations.UNIT_QUAD}) in vec2 a_unitquad; layout (location = ${VertexAttribLocations.CELL_POSITION}) in vec2 a_cellpos; layout (location = ${VertexAttribLocations.OFFSET}) in vec2 a_offset; layout (location = ${VertexAttribLocations.SIZE}) in vec2 a_size; +layout (location = ${VertexAttribLocations.TEXPAGE}) in float a_texpage; layout (location = ${VertexAttribLocations.TEXCOORD}) in vec2 a_texcoord; layout (location = ${VertexAttribLocations.TEXSIZE}) in vec2 a_texsize; @@ -46,27 +47,38 @@ uniform mat4 u_projection; uniform vec2 u_resolution; out vec2 v_texcoord; +flat out int v_texpage; void main() { vec2 zeroToOne = (a_offset / u_resolution) + a_cellpos + (a_unitquad * a_size); gl_Position = u_projection * vec4(zeroToOne, 0.0, 1.0); + v_texpage = int(a_texpage); v_texcoord = a_texcoord + a_unitquad * a_texsize; }`; -const fragmentShaderSource = `#version 300 es +function createFragmentShaderSource(maxFragmentShaderTextureUnits: number): string { + let textureConditionals = ''; + for (let i = 1; i < maxFragmentShaderTextureUnits; i++) { + textureConditionals += ` else if (v_texpage == ${i}) { outColor = texture(u_texture[${i}], v_texcoord); }`; + } + return (`#version 300 es precision lowp float; in vec2 v_texcoord; +flat in int v_texpage; -uniform sampler2D u_texture; +uniform sampler2D u_texture[${maxFragmentShaderTextureUnits}]; out vec4 outColor; void main() { - outColor = texture(u_texture, v_texcoord); -}`; + if (v_texpage == 0) { + outColor = texture(u_texture[0], v_texcoord); + } ${textureConditionals} +}`); +} -const INDICES_PER_CELL = 10; +const INDICES_PER_CELL = 11; const BYTES_PER_CELL = INDICES_PER_CELL * Float32Array.BYTES_PER_ELEMENT; const CELL_POSITION_INDICES = 2; @@ -77,18 +89,17 @@ let $leftCellPadding = 0; let $clippedPixels = 0; export class GlyphRenderer extends Disposable { + private readonly _program: WebGLProgram; + private readonly _vertexArrayObject: IWebGLVertexArrayObject; + private readonly _projectionLocation: WebGLUniformLocation; + private readonly _resolutionLocation: WebGLUniformLocation; + private readonly _textureLocation: WebGLUniformLocation; + private readonly _atlasTextures: WebGLTexture[]; + private readonly _attributesBuffer: WebGLBuffer; + private _atlas: ITextureAtlas | undefined; - - private _program: WebGLProgram; - private _vertexArrayObject: IWebGLVertexArrayObject; - private _projectionLocation: WebGLUniformLocation; - private _resolutionLocation: WebGLUniformLocation; - private _textureLocation: WebGLUniformLocation; - private _atlasTexture: WebGLTexture; - private _attributesBuffer: WebGLBuffer; private _activeBuffer: number = 0; - - private _vertices: IVertices = { + private readonly _vertices: IVertices = { count: 0, attributes: new Float32Array(0), attributesBuffers: [ @@ -98,14 +109,22 @@ export class GlyphRenderer extends Disposable { }; constructor( - private _terminal: Terminal, - private _gl: IWebGL2RenderingContext, + private readonly _terminal: Terminal, + private readonly _gl: IWebGL2RenderingContext, private _dimensions: IRenderDimensions ) { super(); const gl = this._gl; - this._program = throwIfFalsy(createProgram(gl, vertexShaderSource, fragmentShaderSource)); + + if (TextureAtlas.maxAtlasPages === undefined) { + // Typically 8 or 16 + TextureAtlas.maxAtlasPages = throwIfFalsy(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS) as number | null); + // Almost all clients will support >= 4096 + TextureAtlas.maxTextureSize = throwIfFalsy(gl.getParameter(gl.MAX_TEXTURE_SIZE) as number | null); + } + + this._program = throwIfFalsy(createProgram(gl, vertexShaderSource, createFragmentShaderSource(TextureAtlas.maxAtlasPages))); this.register(toDisposable(() => gl.deleteProgram(this._program))); // Uniform locations @@ -127,8 +146,9 @@ export class GlyphRenderer extends Disposable { gl.vertexAttribPointer(VertexAttribLocations.UNIT_QUAD, 2, this._gl.FLOAT, false, 0, 0); // Setup the unit quad element array buffer, this points to indices in - // unitQuadVertices to allow is to draw 2 triangles from the vertices - const unitQuadElementIndices = new Uint8Array([0, 1, 3, 0, 2, 3]); + // unitQuadVertices to allow is to draw 2 triangles from the vertices via a + // triangle strip + const unitQuadElementIndices = new Uint8Array([0, 1, 2, 3]); const elementIndicesBuffer = gl.createBuffer(); this.register(toDisposable(() => gl.deleteBuffer(elementIndicesBuffer))); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elementIndicesBuffer); @@ -144,23 +164,41 @@ export class GlyphRenderer extends Disposable { gl.enableVertexAttribArray(VertexAttribLocations.SIZE); gl.vertexAttribPointer(VertexAttribLocations.SIZE, 2, gl.FLOAT, false, BYTES_PER_CELL, 2 * Float32Array.BYTES_PER_ELEMENT); gl.vertexAttribDivisor(VertexAttribLocations.SIZE, 1); + gl.enableVertexAttribArray(VertexAttribLocations.TEXPAGE); + gl.vertexAttribPointer(VertexAttribLocations.TEXPAGE, 1, gl.FLOAT, false, BYTES_PER_CELL, 4 * Float32Array.BYTES_PER_ELEMENT); + gl.vertexAttribDivisor(VertexAttribLocations.TEXPAGE, 1); gl.enableVertexAttribArray(VertexAttribLocations.TEXCOORD); - gl.vertexAttribPointer(VertexAttribLocations.TEXCOORD, 2, gl.FLOAT, false, BYTES_PER_CELL, 4 * Float32Array.BYTES_PER_ELEMENT); + gl.vertexAttribPointer(VertexAttribLocations.TEXCOORD, 2, gl.FLOAT, false, BYTES_PER_CELL, 5 * Float32Array.BYTES_PER_ELEMENT); gl.vertexAttribDivisor(VertexAttribLocations.TEXCOORD, 1); gl.enableVertexAttribArray(VertexAttribLocations.TEXSIZE); - gl.vertexAttribPointer(VertexAttribLocations.TEXSIZE, 2, gl.FLOAT, false, BYTES_PER_CELL, 6 * Float32Array.BYTES_PER_ELEMENT); + gl.vertexAttribPointer(VertexAttribLocations.TEXSIZE, 2, gl.FLOAT, false, BYTES_PER_CELL, 7 * Float32Array.BYTES_PER_ELEMENT); gl.vertexAttribDivisor(VertexAttribLocations.TEXSIZE, 1); gl.enableVertexAttribArray(VertexAttribLocations.CELL_POSITION); - gl.vertexAttribPointer(VertexAttribLocations.CELL_POSITION, 2, gl.FLOAT, false, BYTES_PER_CELL, 8 * Float32Array.BYTES_PER_ELEMENT); + gl.vertexAttribPointer(VertexAttribLocations.CELL_POSITION, 2, gl.FLOAT, false, BYTES_PER_CELL, 9 * Float32Array.BYTES_PER_ELEMENT); gl.vertexAttribDivisor(VertexAttribLocations.CELL_POSITION, 1); - // Setup empty texture atlas - this._atlasTexture = throwIfFalsy(gl.createTexture()); - this.register(toDisposable(() => gl.deleteTexture(this._atlasTexture))); - gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 255, 255])); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + // Setup static uniforms + gl.useProgram(this._program); + const textureUnits = new Int32Array(TextureAtlas.maxAtlasPages); + for (let i = 0; i < TextureAtlas.maxAtlasPages; i++) { + textureUnits[i] = i; + } + gl.uniform1iv(this._textureLocation, textureUnits); + gl.uniformMatrix4fv(this._projectionLocation, false, PROJECTION_MATRIX); + + // Setup 1x1 red pixel textures for all potential atlas pages, if one of these invalid textures + // is ever drawn it will show characters as red rectangles. + this._atlasTextures = []; + for (let i = 0; i < TextureAtlas.maxAtlasPages; i++) { + const texture = throwIfFalsy(gl.createTexture()); + this.register(toDisposable(() => gl.deleteTexture(texture))); + gl.activeTexture(gl.TEXTURE0 + i); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([255, 0, 0, 255])); + this._atlasTextures[i] = texture; + } // Allow drawing of transparent texture gl.enable(gl.BLEND); @@ -188,7 +226,7 @@ export class GlyphRenderer extends Disposable { // Exit early if this is a null character, allow space character to continue as it may have // underline/strikethrough styles if (code === NULL_CELL_CODE || code === undefined/* This is used for the right side of wide chars */) { - fill(array, 0, $i, $i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES); + array.fill(0, $i, $i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES); return; } @@ -203,34 +241,38 @@ export class GlyphRenderer extends Disposable { $glyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext); } - $leftCellPadding = Math.floor((this._dimensions.scaledCellWidth - this._dimensions.scaledCharWidth) / 2); + $leftCellPadding = Math.floor((this._dimensions.device.cell.width - this._dimensions.device.char.width) / 2); if (bg !== lastBg && $glyph.offset.x > $leftCellPadding) { $clippedPixels = $glyph.offset.x - $leftCellPadding; // a_origin - array[$i ] = -($glyph.offset.x - $clippedPixels) + this._dimensions.scaledCharLeft; - array[$i + 1] = -$glyph.offset.y + this._dimensions.scaledCharTop; + array[$i ] = -($glyph.offset.x - $clippedPixels) + this._dimensions.device.char.left; + array[$i + 1] = -$glyph.offset.y + this._dimensions.device.char.top; // a_size - array[$i + 2] = ($glyph.size.x - $clippedPixels) / this._dimensions.scaledCanvasWidth; - array[$i + 3] = $glyph.size.y / this._dimensions.scaledCanvasHeight; + array[$i + 2] = ($glyph.size.x - $clippedPixels) / this._dimensions.device.canvas.width; + array[$i + 3] = $glyph.size.y / this._dimensions.device.canvas.height; + // a_texpage + array[$i + 4] = $glyph.texturePage; // a_texcoord - array[$i + 4] = $glyph.texturePositionClipSpace.x + $clippedPixels / this._atlas.cacheCanvas.width; - array[$i + 5] = $glyph.texturePositionClipSpace.y; + array[$i + 5] = $glyph.texturePositionClipSpace.x + $clippedPixels / this._atlas.pages[$glyph.texturePage].canvas.width; + array[$i + 6] = $glyph.texturePositionClipSpace.y; // a_texsize - array[$i + 6] = $glyph.sizeClipSpace.x - $clippedPixels / this._atlas.cacheCanvas.width; - array[$i + 7] = $glyph.sizeClipSpace.y; + array[$i + 7] = $glyph.sizeClipSpace.x - $clippedPixels / this._atlas.pages[$glyph.texturePage].canvas.width; + array[$i + 8] = $glyph.sizeClipSpace.y; } else { // a_origin - array[$i ] = -$glyph.offset.x + this._dimensions.scaledCharLeft; - array[$i + 1] = -$glyph.offset.y + this._dimensions.scaledCharTop; + array[$i ] = -$glyph.offset.x + this._dimensions.device.char.left; + array[$i + 1] = -$glyph.offset.y + this._dimensions.device.char.top; // a_size - array[$i + 2] = $glyph.size.x / this._dimensions.scaledCanvasWidth; - array[$i + 3] = $glyph.size.y / this._dimensions.scaledCanvasHeight; + array[$i + 2] = $glyph.size.x / this._dimensions.device.canvas.width; + array[$i + 3] = $glyph.size.y / this._dimensions.device.canvas.height; + // a_texpage + array[$i + 4] = $glyph.texturePage; // a_texcoord - array[$i + 4] = $glyph.texturePositionClipSpace.x; - array[$i + 5] = $glyph.texturePositionClipSpace.y; + array[$i + 5] = $glyph.texturePositionClipSpace.x; + array[$i + 6] = $glyph.texturePositionClipSpace.y; // a_texsize - array[$i + 6] = $glyph.sizeClipSpace.x; - array[$i + 7] = $glyph.sizeClipSpace.y; + array[$i + 7] = $glyph.sizeClipSpace.x; + array[$i + 8] = $glyph.sizeClipSpace.y; } // a_cellpos only changes on resize } @@ -245,7 +287,8 @@ export class GlyphRenderer extends Disposable { } else { this._vertices.attributes.fill(0); } - for (let i = 0; i < this._vertices.attributesBuffers.length; i++) { + let i = 0; + for (; i < this._vertices.attributesBuffers.length; i++) { if (this._vertices.count !== newCount) { this._vertices.attributesBuffers[i] = new Float32Array(newCount); } else { @@ -253,11 +296,11 @@ export class GlyphRenderer extends Disposable { } } this._vertices.count = newCount; - let i = 0; + i = 0; for (let y = 0; y < terminal.rows; y++) { for (let x = 0; x < terminal.cols; x++) { - this._vertices.attributes[i + 8] = x / terminal.cols; - this._vertices.attributes[i + 9] = y / terminal.rows; + this._vertices.attributes[i + 9] = x / terminal.cols; + this._vertices.attributes[i + 10] = y / terminal.rows; i += INDICES_PER_CELL; } } @@ -266,6 +309,7 @@ export class GlyphRenderer extends Disposable { public handleResize(): void { const gl = this._gl; gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + gl.uniform2f(this._resolutionLocation, gl.canvas.width, gl.canvas.height); this.clear(); } @@ -302,30 +346,31 @@ export class GlyphRenderer extends Disposable { gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer); gl.bufferData(gl.ARRAY_BUFFER, activeBuffer.subarray(0, bufferLength), gl.STREAM_DRAW); - // Bind the texture atlas if it's changed - if (this._atlas.hasCanvasChanged) { - this._atlas.hasCanvasChanged = false; - gl.uniform1i(this._textureLocation, 0); - gl.activeTexture(gl.TEXTURE0 + 0); - gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._atlas.cacheCanvas); - gl.generateMipmap(gl.TEXTURE_2D); + // Bind the atlas page texture if they have changed + for (let i = 0; i < this._atlas.pages.length; i++) { + if (this._atlas.pages[i].hasCanvasChanged) { + this._atlas.pages[i].hasCanvasChanged = false; + this._bindAtlasPageTexture(gl, this._atlas, i); + } } - // Set uniforms - gl.uniformMatrix4fv(this._projectionLocation, false, PROJECTION_MATRIX); - gl.uniform2f(this._resolutionLocation, gl.canvas.width, gl.canvas.height); - // Draw the viewport - gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, bufferLength / INDICES_PER_CELL); + gl.drawElementsInstanced(gl.TRIANGLE_STRIP, 4, gl.UNSIGNED_BYTE, 0, bufferLength / INDICES_PER_CELL); } public setAtlas(atlas: ITextureAtlas): void { - const gl = this._gl; this._atlas = atlas; + for (let i = 0; i < atlas.pages.length; i++) { + this._bindAtlasPageTexture(this._gl, atlas, i); + } + } - gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, atlas.cacheCanvas); + private _bindAtlasPageTexture(gl: IWebGL2RenderingContext, atlas: ITextureAtlas, i: number): void { + gl.activeTexture(gl.TEXTURE0 + i); + gl.bindTexture(gl.TEXTURE_2D, this._atlasTextures[i]); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, atlas.pages[i].canvas); gl.generateMipmap(gl.TEXTURE_2D); } diff --git a/addons/xterm-addon-webgl/src/RectangleRenderer.ts b/addons/xterm-addon-webgl/src/RectangleRenderer.ts index 04368711..f45ae3df 100644 --- a/addons/xterm-addon-webgl/src/RectangleRenderer.ts +++ b/addons/xterm-addon-webgl/src/RectangleRenderer.ts @@ -113,8 +113,9 @@ export class RectangleRenderer extends Disposable { gl.vertexAttribPointer(VertexAttribLocations.UNIT_QUAD, 2, this._gl.FLOAT, false, 0, 0); // Setup the unit quad element array buffer, this points to indices in - // unitQuadVertices to allow is to draw 2 triangles from the vertices - const unitQuadElementIndices = new Uint8Array([0, 1, 3, 0, 2, 3]); + // unitQuadVertices to allow is to draw 2 triangles from the vertices via a + // triangle strip + const unitQuadElementIndices = new Uint8Array([0, 1, 2, 3]); const elementIndicesBuffer = gl.createBuffer(); this.register(toDisposable(() => gl.deleteBuffer(elementIndicesBuffer))); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elementIndicesBuffer); @@ -153,7 +154,7 @@ export class RectangleRenderer extends Disposable { // Bind attributes buffer and draw gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer); gl.bufferData(gl.ARRAY_BUFFER, this._vertices.attributes, gl.DYNAMIC_DRAW); - gl.drawElementsInstanced(this._gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, this._vertices.count); + gl.drawElementsInstanced(this._gl.TRIANGLE_STRIP, 4, gl.UNSIGNED_BYTE, 0, this._vertices.count); } public handleResize(): void { @@ -175,8 +176,8 @@ export class RectangleRenderer extends Disposable { 0, 0, 0, - this._terminal.cols * this._dimensions.scaledCellWidth, - this._terminal.rows * this._dimensions.scaledCellHeight, + this._terminal.cols * this._dimensions.device.cell.width, + this._terminal.rows * this._dimensions.device.cell.height, this._bgFloat ); } @@ -264,21 +265,21 @@ export class RectangleRenderer extends Disposable { if (vertices.attributes.length < offset + 4) { vertices.attributes = expandFloat32Array(vertices.attributes, this._terminal.rows * this._terminal.cols * INDICES_PER_RECTANGLE); } - $x1 = startX * this._dimensions.scaledCellWidth; - $y1 = y * this._dimensions.scaledCellHeight; + $x1 = startX * this._dimensions.device.cell.width; + $y1 = y * this._dimensions.device.cell.height; $r = (($rgba >> 24) & 0xFF) / 255; $g = (($rgba >> 16) & 0xFF) / 255; $b = (($rgba >> 8 ) & 0xFF) / 255; $a = (!$isDefault && bg & BgFlags.DIM) ? DIM_OPACITY : 1; - this._addRectangle(vertices.attributes, offset, $x1, $y1, (endX - startX) * this._dimensions.scaledCellWidth, this._dimensions.scaledCellHeight, $r, $g, $b, $a); + this._addRectangle(vertices.attributes, offset, $x1, $y1, (endX - startX) * this._dimensions.device.cell.width, this._dimensions.device.cell.height, $r, $g, $b, $a); } private _addRectangle(array: Float32Array, offset: number, x1: number, y1: number, width: number, height: number, r: number, g: number, b: number, a: number): void { - array[offset ] = x1 / this._dimensions.scaledCanvasWidth; - array[offset + 1] = y1 / this._dimensions.scaledCanvasHeight; - array[offset + 2] = width / this._dimensions.scaledCanvasWidth; - array[offset + 3] = height / this._dimensions.scaledCanvasHeight; + array[offset ] = x1 / this._dimensions.device.canvas.width; + array[offset + 1] = y1 / this._dimensions.device.canvas.height; + array[offset + 2] = width / this._dimensions.device.canvas.width; + array[offset + 3] = height / this._dimensions.device.canvas.height; array[offset + 4] = r; array[offset + 5] = g; array[offset + 6] = b; @@ -286,10 +287,10 @@ export class RectangleRenderer extends Disposable { } private _addRectangleFloat(array: Float32Array, offset: number, x1: number, y1: number, width: number, height: number, color: Float32Array): void { - array[offset ] = x1 / this._dimensions.scaledCanvasWidth; - array[offset + 1] = y1 / this._dimensions.scaledCanvasHeight; - array[offset + 2] = width / this._dimensions.scaledCanvasWidth; - array[offset + 3] = height / this._dimensions.scaledCanvasHeight; + array[offset ] = x1 / this._dimensions.device.canvas.width; + array[offset + 1] = y1 / this._dimensions.device.canvas.height; + array[offset + 2] = width / this._dimensions.device.canvas.width; + array[offset + 3] = height / this._dimensions.device.canvas.height; array[offset + 4] = color[0]; array[offset + 5] = color[1]; array[offset + 6] = color[2]; diff --git a/addons/xterm-addon-webgl/src/RenderModel.ts b/addons/xterm-addon-webgl/src/RenderModel.ts index b1542d98..db364db4 100644 --- a/addons/xterm-addon-webgl/src/RenderModel.ts +++ b/addons/xterm-addon-webgl/src/RenderModel.ts @@ -4,7 +4,6 @@ */ import { IRenderModel } from './Types'; -import { fill } from 'common/TypedArrayUtils'; import { ISelectionRenderModel } from 'browser/renderer/shared/Types'; import { createSelectionRenderModel } from 'browser/renderer/shared/SelectionRenderModel'; @@ -35,7 +34,7 @@ export class RenderModel implements IRenderModel { } public clear(): void { - fill(this.cells, 0, 0); - fill(this.lineLengths, 0, 0); + this.cells.fill(0, 0); + this.lineLengths.fill(0, 0); } } diff --git a/addons/xterm-addon-webgl/src/WebglAddon.ts b/addons/xterm-addon-webgl/src/WebglAddon.ts index 71487315..9ae6df5a 100644 --- a/addons/xterm-addon-webgl/src/WebglAddon.ts +++ b/addons/xterm-addon-webgl/src/WebglAddon.ts @@ -3,21 +3,26 @@ * @license MIT */ -import { Terminal, ITerminalAddon, IEvent } from 'xterm'; -import { WebglRenderer } from './WebglRenderer'; -import { ICharacterJoinerService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services'; -import { IColorSet } from 'browser/Types'; +import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services'; +import { ITerminal } from 'browser/Types'; import { EventEmitter, forwardEvent } from 'common/EventEmitter'; -import { isSafari } from 'common/Platform'; -import { ICoreService, IDecorationService } from 'common/services/Services'; import { Disposable, toDisposable } from 'common/Lifecycle'; +import { isSafari } from 'common/Platform'; +import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; +import { ICoreTerminal } from 'common/Types'; +import { ITerminalAddon, Terminal } from 'xterm'; +import { WebglRenderer } from './WebglRenderer'; export class WebglAddon extends Disposable implements ITerminalAddon { private _terminal?: Terminal; private _renderer?: WebglRenderer; - private readonly _onChangeTextureAtlas = this.register(new EventEmitter()); + private readonly _onChangeTextureAtlas = this.register(new EventEmitter()); public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event; + private readonly _onAddTextureAtlasCanvas = this.register(new EventEmitter()); + public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event; + private readonly _onRemoveTextureAtlasCanvas = this.register(new EventEmitter()); + public readonly onRemoveTextureAtlasCanvas = this._onRemoveTextureAtlasCanvas.event; private readonly _onContextLoss = this.register(new EventEmitter()); public readonly onContextLoss = this._onContextLoss.event; @@ -31,21 +36,40 @@ export class WebglAddon extends Disposable implements ITerminalAddon { if (isSafari) { throw new Error('Webgl is not currently supported on Safari'); } - const core = (terminal as any)._core; + + const core = (terminal as any)._core as ITerminal; if (!terminal.element) { this.register(core.onWillOpen(() => this.activate(terminal))); return; } + this._terminal = terminal; - const renderService: IRenderService = core._renderService; - const characterJoinerService: ICharacterJoinerService = core._characterJoinerService; - const coreBrowserService: ICoreBrowserService = core._coreBrowserService; const coreService: ICoreService = core.coreService; - const decorationService: IDecorationService = core._decorationService; - const themeService: IThemeService = core._themeService; - this._renderer = this.register(new WebglRenderer(terminal, themeService, characterJoinerService, coreBrowserService, coreService, decorationService, this._preserveDrawingBuffer)); + const optionsService: IOptionsService = core.optionsService; + + const unsafeCore = core as any; + const renderService: IRenderService = unsafeCore._renderService; + const characterJoinerService: ICharacterJoinerService = unsafeCore._characterJoinerService; + const charSizeService: ICharSizeService = unsafeCore._charSizeService; + const coreBrowserService: ICoreBrowserService = unsafeCore._coreBrowserService; + const decorationService: IDecorationService = unsafeCore._decorationService; + const themeService: IThemeService = unsafeCore._themeService; + + this._renderer = this.register(new WebglRenderer( + terminal, + characterJoinerService, + charSizeService, + coreBrowserService, + coreService, + decorationService, + optionsService, + themeService, + this._preserveDrawingBuffer + )); this.register(forwardEvent(this._renderer.onContextLoss, this._onContextLoss)); this.register(forwardEvent(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas)); + this.register(forwardEvent(this._renderer.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas)); + this.register(forwardEvent(this._renderer.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas)); renderService.setRenderer(this._renderer); this.register(toDisposable(() => { diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index ed99a225..97e8dd7d 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -7,17 +7,19 @@ import { addDisposableDomListener } from 'browser/Lifecycle'; import { CellColorResolver } from 'browser/renderer/shared/CellColorResolver'; import { acquireTextureAtlas, removeTerminalFromCache } from 'browser/renderer/shared/CharAtlasCache'; import { observeDevicePixelDimensions } from 'browser/renderer/shared/DevicePixelObserver'; +import { createRenderDimensions, throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; +import { TextureAtlas } from 'browser/renderer/shared/TextureAtlas'; import { IRenderDimensions, IRenderer, IRequestRedrawEvent, ITextureAtlas } from 'browser/renderer/shared/Types'; -import { ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services'; -import { IColorSet, ITerminal, ReadonlyColorSet } from 'browser/Types'; +import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IThemeService } from 'browser/services/Services'; +import { ITerminal } from 'browser/Types'; import { AttributeData } from 'common/buffer/AttributeData'; import { CellData } from 'common/buffer/CellData'; import { Content, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants'; -import { EventEmitter } from 'common/EventEmitter'; -import { Disposable, toDisposable } from 'common/Lifecycle'; -import { ICoreService, IDecorationService } from 'common/services/Services'; +import { EventEmitter, forwardEvent } from 'common/EventEmitter'; +import { Disposable, getDisposeArrayDisposable, toDisposable } from 'common/Lifecycle'; +import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; import { CharData, IBufferLine, ICellData } from 'common/Types'; -import { Terminal } from 'xterm'; +import { IDisposable, Terminal } from 'xterm'; import { GlyphRenderer } from './GlyphRenderer'; import { RectangleRenderer } from './RectangleRenderer'; import { CursorRenderLayer } from './renderLayer/CursorRenderLayer'; @@ -28,6 +30,7 @@ import { IWebGL2RenderingContext } from './Types'; export class WebglRenderer extends Disposable implements IRenderer { private _renderLayers: IRenderLayer[]; + private _charAtlasDisposable: IDisposable | undefined; private _charAtlas: ITextureAtlas | undefined; private _devicePixelRatio: number; @@ -40,7 +43,7 @@ export class WebglRenderer extends Disposable implements IRenderer { private _rectangleRenderer!: RectangleRenderer; private _glyphRenderer!: GlyphRenderer; - public dimensions: IRenderDimensions; + public readonly dimensions: IRenderDimensions; private _core: ITerminal; private _isAttached: boolean; @@ -48,6 +51,10 @@ export class WebglRenderer extends Disposable implements IRenderer { private readonly _onChangeTextureAtlas = this.register(new EventEmitter()); public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event; + private readonly _onAddTextureAtlasCanvas = this.register(new EventEmitter()); + public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event; + private readonly _onRemoveTextureAtlasCanvas = this.register(new EventEmitter()); + public readonly onRemoveTextureAtlasCanvas = this._onRemoveTextureAtlasCanvas.event; private readonly _onRequestRedraw = this.register(new EventEmitter()); public readonly onRequestRedraw = this._onRequestRedraw.event; private readonly _onContextLoss = this.register(new EventEmitter()); @@ -55,11 +62,13 @@ export class WebglRenderer extends Disposable implements IRenderer { constructor( private _terminal: Terminal, - private readonly _themeService: IThemeService, private readonly _characterJoinerService: ICharacterJoinerService, + private readonly _charSizeService: ICharSizeService, private readonly _coreBrowserService: ICoreBrowserService, coreService: ICoreService, private readonly _decorationService: IDecorationService, + optionsService: IOptionsService, + private readonly _themeService: IThemeService, preserveDrawingBuffer?: boolean ) { super(); @@ -72,24 +81,12 @@ export class WebglRenderer extends Disposable implements IRenderer { this._renderLayers = [ new LinkRenderLayer(this._core.screenElement!, 2, this._terminal, this._core.linkifier2, this._coreBrowserService, this._themeService), - new CursorRenderLayer(_terminal, this._core.screenElement!, 3, this._onRequestRedraw, this._coreBrowserService, coreService, this._themeService) + new CursorRenderLayer(_terminal, this._core.screenElement!, 3, this._onRequestRedraw, this._coreBrowserService, coreService, this._themeService, optionsService) ]; - this.dimensions = { - scaledCharWidth: 0, - scaledCharHeight: 0, - scaledCellWidth: 0, - scaledCellHeight: 0, - scaledCharLeft: 0, - scaledCharTop: 0, - scaledCanvasWidth: 0, - scaledCanvasHeight: 0, - canvasWidth: 0, - canvasHeight: 0, - actualCellWidth: 0, - actualCellHeight: 0 - }; + this.dimensions = createRenderDimensions(); this._devicePixelRatio = this._coreBrowserService.dpr; this._updateDimensions(); + this.register(optionsService.onOptionChange(() => this._handleOptionsChanged())); this._canvas = document.createElement('canvas'); @@ -144,7 +141,7 @@ export class WebglRenderer extends Disposable implements IRenderer { } public get textureAtlas(): HTMLCanvasElement | undefined { - return this._charAtlas?.cacheCanvas; + return this._charAtlas?.pages[0].canvas; } private _handleColorChange(): void { @@ -175,14 +172,14 @@ export class WebglRenderer extends Disposable implements IRenderer { } // Resize the canvas - this._canvas.width = this.dimensions.scaledCanvasWidth; - this._canvas.height = this.dimensions.scaledCanvasHeight; - this._canvas.style.width = `${this.dimensions.canvasWidth}px`; - this._canvas.style.height = `${this.dimensions.canvasHeight}px`; + this._canvas.width = this.dimensions.device.canvas.width; + this._canvas.height = this.dimensions.device.canvas.height; + this._canvas.style.width = `${this.dimensions.css.canvas.width}px`; + this._canvas.style.height = `${this.dimensions.css.canvas.height}px`; // Resize the screen - this._core.screenElement!.style.width = `${this.dimensions.canvasWidth}px`; - this._core.screenElement!.style.height = `${this.dimensions.canvasHeight}px`; + this._core.screenElement!.style.width = `${this.dimensions.css.canvas.width}px`; + this._core.screenElement!.style.height = `${this.dimensions.css.canvas.height}px`; this._rectangleRenderer.setDimensions(this.dimensions); this._rectangleRenderer.handleResize(); @@ -230,10 +227,7 @@ export class WebglRenderer extends Disposable implements IRenderer { } } - public handleOptionsChanged(): void { - for (const l of this._renderLayers) { - l.handleOptionsChanged(this._terminal); - } + private _handleOptionsChanged(): void { this._updateDimensions(); this._refreshCharAtlas(); } @@ -255,19 +249,31 @@ export class WebglRenderer extends Disposable implements IRenderer { /** * Refreshes the char atlas, aquiring a new one if necessary. - * @param terminal The terminal. - * @param colorSet The color set to use for the char atlas. */ private _refreshCharAtlas(): void { - if (this.dimensions.scaledCharWidth <= 0 && this.dimensions.scaledCharHeight <= 0) { + if (this.dimensions.device.char.width <= 0 && this.dimensions.device.char.height <= 0) { // Mark as not attached so char atlas gets refreshed on next render this._isAttached = false; return; } - const atlas = acquireTextureAtlas(this._terminal, this._themeService.colors, this.dimensions.scaledCellWidth, this.dimensions.scaledCellHeight, this.dimensions.scaledCharWidth, this.dimensions.scaledCharHeight, this._coreBrowserService.dpr); + const atlas = acquireTextureAtlas( + this._terminal, + this._themeService.colors, + this.dimensions.device.cell.width, + this.dimensions.device.cell.height, + this.dimensions.device.char.width, + this.dimensions.device.char.height, + this._coreBrowserService.dpr + ); if (this._charAtlas !== atlas) { - this._onChangeTextureAtlas.fire(atlas.cacheCanvas); + + this._charAtlasDisposable?.dispose(); + this._onChangeTextureAtlas.fire(atlas.pages[0].canvas); + this._charAtlasDisposable = getDisposeArrayDisposable([ + forwardEvent(atlas.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas), + forwardEvent(atlas.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas) + ]); } this._charAtlas = atlas; this._charAtlas.warmUp(); @@ -309,7 +315,7 @@ export class WebglRenderer extends Disposable implements IRenderer { public renderRows(start: number, end: number): void { if (!this._isAttached) { - if (this._coreBrowserService.window.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._charSizeService.width && this._charSizeService.height) { this._updateDimensions(); this._refreshCharAtlas(); this._isAttached = true; @@ -326,7 +332,6 @@ export class WebglRenderer extends Disposable implements IRenderer { // Tell renderer the frame is beginning if (this._glyphRenderer.beginFrame()) { this._clearModel(true); - this._model.selection.clear(); } // Update model to reflect what's drawn @@ -447,64 +452,62 @@ export class WebglRenderer extends Disposable implements IRenderer { * Recalculates the character and canvas dimensions. */ private _updateDimensions(): void { - // TODO: Acquire CharSizeService properly - // Perform a new measure if the CharMeasure dimensions are not yet available - if (!(this._core as any)._charSizeService.width || !(this._core as any)._charSizeService.height) { + if (!this._charSizeService.width || !this._charSizeService.height) { return; } - // Calculate the scaled character width. Width is floored as it must be drawn to an integer grid + // Calculate the device character width. Width is floored as it must be drawn to an integer grid // in order for the char atlas glyphs to not be blurry. - this.dimensions.scaledCharWidth = Math.floor((this._core as any)._charSizeService.width * this._devicePixelRatio); + this.dimensions.device.char.width = Math.floor(this._charSizeService.width * this._devicePixelRatio); - // Calculate the scaled character height. Height is ceiled in case devicePixelRatio is a + // Calculate the device character height. Height is ceiled in case devicePixelRatio is a // floating point number in order to ensure there is enough space to draw the character to the // cell. - this.dimensions.scaledCharHeight = Math.ceil((this._core as any)._charSizeService.height * this._devicePixelRatio); + this.dimensions.device.char.height = Math.ceil(this._charSizeService.height * this._devicePixelRatio); - // Calculate the scaled cell height, if lineHeight is _not_ 1, the resulting value will be - // floored since lineHeight can never be lower then 1, this guarentees the scaled cell height - // will always be larger than scaled char height. - this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight); + // Calculate the device cell height, if lineHeight is _not_ 1, the resulting value will be + // floored since lineHeight can never be lower then 1, this guarentees the device cell height + // will always be larger than device char height. + this.dimensions.device.cell.height = Math.floor(this.dimensions.device.char.height * this._terminal.options.lineHeight); // Calculate the y offset within a cell that glyph should draw at in order for it to be centered // correctly within the cell. - this.dimensions.scaledCharTop = this._terminal.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2); + this.dimensions.device.char.top = this._terminal.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.device.cell.height - this.dimensions.device.char.height) / 2); - // Calculate the scaled cell width, taking the letterSpacing into account. - this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing); + // Calculate the device cell width, taking the letterSpacing into account. + this.dimensions.device.cell.width = this.dimensions.device.char.width + Math.round(this._terminal.options.letterSpacing); // Calculate the x offset with a cell that text should draw from in order for it to be centered // correctly within the cell. - this.dimensions.scaledCharLeft = Math.floor(this._terminal.options.letterSpacing / 2); + this.dimensions.device.char.left = Math.floor(this._terminal.options.letterSpacing / 2); - // Recalculate the canvas dimensions, the scaled dimensions define the actual number of pixel in + // Recalculate the canvas dimensions, the device dimensions define the actual number of pixel in // the canvas - this.dimensions.scaledCanvasHeight = this._terminal.rows * this.dimensions.scaledCellHeight; - this.dimensions.scaledCanvasWidth = this._terminal.cols * this.dimensions.scaledCellWidth; + this.dimensions.device.canvas.height = this._terminal.rows * this.dimensions.device.cell.height; + this.dimensions.device.canvas.width = this._terminal.cols * this.dimensions.device.cell.width; // The the size of the canvas on the page. It's important that this rounds to nearest integer // and not ceils as browsers often have floating point precision issues where // `window.devicePixelRatio` ends up being something like `1.100000023841858` for example, when // it's actually 1.1. Ceiling may causes blurriness as the backing canvas image is 1 pixel too // large for the canvas element size. - this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / this._devicePixelRatio); - this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / this._devicePixelRatio); + this.dimensions.css.canvas.height = Math.round(this.dimensions.device.canvas.height / this._devicePixelRatio); + this.dimensions.css.canvas.width = Math.round(this.dimensions.device.canvas.width / this._devicePixelRatio); // Get the CSS dimensions of an individual cell. This needs to be derived from the calculated // device pixel canvas value above. CharMeasure.width/height by itself is insufficient when the // page is not at 100% zoom level as CharMeasure is measured in CSS pixels, but the actual char // size on the canvas can differ. - this.dimensions.actualCellHeight = this.dimensions.scaledCellHeight / this._devicePixelRatio; - this.dimensions.actualCellWidth = this.dimensions.scaledCellWidth / this._devicePixelRatio; + this.dimensions.css.cell.height = this.dimensions.device.cell.height / this._devicePixelRatio; + this.dimensions.css.cell.width = this.dimensions.device.cell.width / this._devicePixelRatio; } private _setCanvasDevicePixelDimensions(width: number, height: number): void { if (this._canvas.width === width && this._canvas.height === height) { return; } - // While the actual canvas size has changed, keep scaledCanvasWidth/Height as the value before + // While the actual canvas size has changed, keep device canvas dimensions as the value before // the change as it's an exact multiple of the cell sizes. this._canvas.width = width; this._canvas.height = height; diff --git a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts index e99a1464..e30ef25b 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts @@ -17,12 +17,12 @@ import { Disposable, toDisposable } from 'common/Lifecycle'; export abstract class BaseRenderLayer extends Disposable implements IRenderLayer { private _canvas: HTMLCanvasElement; protected _ctx!: CanvasRenderingContext2D; - private _scaledCharWidth: number = 0; - private _scaledCharHeight: number = 0; - private _scaledCellWidth: number = 0; - private _scaledCellHeight: number = 0; - private _scaledCharLeft: number = 0; - private _scaledCharTop: number = 0; + private _deviceCharWidth: number = 0; + private _deviceCharHeight: number = 0; + private _deviceCellWidth: number = 0; + private _deviceCellHeight: number = 0; + private _deviceCharLeft: number = 0; + private _deviceCharTop: number = 0; protected _charAtlas: ITextureAtlas | undefined; @@ -59,7 +59,6 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer } } - public handleOptionsChanged(terminal: Terminal): void {} public handleBlur(terminal: Terminal): void {} public handleFocus(terminal: Terminal): void {} public handleCursorMove(terminal: Terminal): void {} @@ -91,24 +90,24 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer * @param colorSet The color set to use for the char atlas. */ private _refreshCharAtlas(terminal: Terminal, colorSet: ReadonlyColorSet): void { - if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) { + if (this._deviceCharWidth <= 0 && this._deviceCharHeight <= 0) { return; } - this._charAtlas = acquireTextureAtlas(terminal, colorSet, this._scaledCellWidth, this._scaledCellHeight, this._scaledCharWidth, this._scaledCharHeight, this._coreBrowserService.dpr); + this._charAtlas = acquireTextureAtlas(terminal, colorSet, this._deviceCellWidth, this._deviceCellHeight, this._deviceCharWidth, this._deviceCharHeight, this._coreBrowserService.dpr); this._charAtlas.warmUp(); } public resize(terminal: Terminal, dim: IRenderDimensions): void { - this._scaledCellWidth = dim.scaledCellWidth; - this._scaledCellHeight = dim.scaledCellHeight; - this._scaledCharWidth = dim.scaledCharWidth; - this._scaledCharHeight = dim.scaledCharHeight; - this._scaledCharLeft = dim.scaledCharLeft; - this._scaledCharTop = dim.scaledCharTop; - this._canvas.width = dim.scaledCanvasWidth; - this._canvas.height = dim.scaledCanvasHeight; - this._canvas.style.width = `${dim.canvasWidth}px`; - this._canvas.style.height = `${dim.canvasHeight}px`; + this._deviceCellWidth = dim.device.cell.width; + this._deviceCellHeight = dim.device.cell.height; + this._deviceCharWidth = dim.device.char.width; + this._deviceCharHeight = dim.device.char.height; + this._deviceCharLeft = dim.device.char.left; + this._deviceCharTop = dim.device.char.top; + this._canvas.width = dim.device.canvas.width; + this._canvas.height = dim.device.canvas.height; + this._canvas.style.width = `${dim.css.canvas.width}px`; + this._canvas.style.height = `${dim.css.canvas.height}px`; // Draw the background if this is an opaque layer if (!this._alpha) { @@ -129,10 +128,10 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer */ protected _fillCells(x: number, y: number, width: number, height: number): void { this._ctx.fillRect( - x * this._scaledCellWidth, - y * this._scaledCellHeight, - width * this._scaledCellWidth, - height * this._scaledCellHeight); + x * this._deviceCellWidth, + y * this._deviceCellHeight, + width * this._deviceCellWidth, + height * this._deviceCellHeight); } /** @@ -143,9 +142,9 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer */ protected _fillBottomLineAtCells(x: number, y: number, width: number = 1): void { this._ctx.fillRect( - x * this._scaledCellWidth, - (y + 1) * this._scaledCellHeight - this._coreBrowserService.dpr - 1 /* Ensure it's drawn within the cell */, - width * this._scaledCellWidth, + x * this._deviceCellWidth, + (y + 1) * this._deviceCellHeight - this._coreBrowserService.dpr - 1 /* Ensure it's drawn within the cell */, + width * this._deviceCellWidth, this._coreBrowserService.dpr); } @@ -157,10 +156,10 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer */ protected _fillLeftLineAtCell(x: number, y: number, width: number): void { this._ctx.fillRect( - x * this._scaledCellWidth, - y * this._scaledCellHeight, + x * this._deviceCellWidth, + y * this._deviceCellHeight, this._coreBrowserService.dpr * width, - this._scaledCellHeight); + this._deviceCellHeight); } /** @@ -172,10 +171,10 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer protected _strokeRectAtCell(x: number, y: number, width: number, height: number): void { this._ctx.lineWidth = this._coreBrowserService.dpr; this._ctx.strokeRect( - 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); + x * this._deviceCellWidth + this._coreBrowserService.dpr / 2, + y * this._deviceCellHeight + (this._coreBrowserService.dpr / 2), + width * this._deviceCellWidth - this._coreBrowserService.dpr, + (height * this._deviceCellHeight) - this._coreBrowserService.dpr); } /** @@ -200,17 +199,17 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer protected _clearCells(x: number, y: number, width: number, height: number): void { if (this._alpha) { this._ctx.clearRect( - x * this._scaledCellWidth, - y * this._scaledCellHeight, - width * this._scaledCellWidth, - height * this._scaledCellHeight); + x * this._deviceCellWidth, + y * this._deviceCellHeight, + width * this._deviceCellWidth, + height * this._deviceCellHeight); } else { this._ctx.fillStyle = this._themeService.colors.background.css; this._ctx.fillRect( - x * this._scaledCellWidth, - y * this._scaledCellHeight, - width * this._scaledCellWidth, - height * this._scaledCellHeight); + x * this._deviceCellWidth, + y * this._deviceCellHeight, + width * this._deviceCellWidth, + height * this._deviceCellHeight); } } @@ -222,7 +221,6 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer * @param cell The cell data for the character to draw. * @param x The column to draw at. * @param y The row to draw at. - * @param color The color of the character. */ protected _fillCharTrueColor(terminal: Terminal, cell: CellData, x: number, y: number): void { this._ctx.font = this._getFont(terminal, false, false); @@ -230,8 +228,8 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer this._clipCell(x, y, cell.getWidth()); this._ctx.fillText( cell.getChars(), - x * this._scaledCellWidth + this._scaledCharLeft, - y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight); + x * this._deviceCellWidth + this._deviceCharLeft, + y * this._deviceCellHeight + this._deviceCharTop + this._deviceCharHeight); } /** @@ -243,10 +241,10 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer private _clipCell(x: number, y: number, width: number): void { this._ctx.beginPath(); this._ctx.rect( - x * this._scaledCellWidth, - y * this._scaledCellHeight, - width * this._scaledCellWidth, - this._scaledCellHeight); + x * this._deviceCellWidth, + y * this._deviceCellHeight, + width * this._deviceCellWidth, + this._deviceCellHeight); this._ctx.clip(); } diff --git a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts index 74801f4e..cb288f24 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts @@ -7,11 +7,10 @@ import { Terminal } from 'xterm'; import { BaseRenderLayer } from './BaseRenderLayer'; import { ICellData } from 'common/Types'; import { CellData } from 'common/buffer/CellData'; -import { IColorSet, ReadonlyColorSet } from 'browser/Types'; import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/shared/Types'; import { IEventEmitter } from 'common/EventEmitter'; import { ICoreBrowserService, IThemeService } from 'browser/services/Services'; -import { ICoreService } from 'common/services/Services'; +import { ICoreService, IOptionsService } from 'common/services/Services'; import { toDisposable } from 'common/Lifecycle'; interface ICursorState { @@ -40,7 +39,8 @@ export class CursorRenderLayer extends BaseRenderLayer { private _onRequestRefreshRowsEvent: IEventEmitter, coreBrowserService: ICoreBrowserService, private readonly _coreService: ICoreService, - themeService: IThemeService + themeService: IThemeService, + optionsService: IOptionsService ) { super(terminal, container, 'cursor', zIndex, true, coreBrowserService, themeService); this._state = { @@ -55,7 +55,8 @@ export class CursorRenderLayer extends BaseRenderLayer { 'block': this._renderBlockCursor.bind(this), 'underline': this._renderUnderlineCursor.bind(this) }; - this.handleOptionsChanged(terminal); + this._handleOptionsChanged(terminal); + this.register(optionsService.onOptionChange(() => this._handleOptionsChanged(terminal))); this.register(toDisposable(() => { this._cursorBlinkStateManager?.dispose(); this._cursorBlinkStateManager = undefined; @@ -77,7 +78,7 @@ export class CursorRenderLayer extends BaseRenderLayer { public reset(terminal: Terminal): void { this._clearCursor(); this._cursorBlinkStateManager?.restartBlinkAnimation(terminal); - this.handleOptionsChanged(terminal); + this._handleOptionsChanged(terminal); } public handleBlur(terminal: Terminal): void { @@ -90,7 +91,7 @@ export class CursorRenderLayer extends BaseRenderLayer { this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.active.cursorY, end: terminal.buffer.active.cursorY }); } - public handleOptionsChanged(terminal: Terminal): void { + private _handleOptionsChanged(terminal: Terminal): void { if (terminal.options.cursorBlink) { if (!this._cursorBlinkStateManager) { this._cursorBlinkStateManager = new CursorBlinkStateManager(() => { diff --git a/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts index 2d2af192..77b02420 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts @@ -7,7 +7,7 @@ import { is256Color } from 'browser/renderer/shared/CharAtlasUtils'; import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants'; import { IRenderDimensions } from 'browser/renderer/shared/Types'; import { ICoreBrowserService, IThemeService } from 'browser/services/Services'; -import { ILinkifier2, ILinkifierEvent, ITerminal } from 'browser/Types'; +import { ILinkifier2, ILinkifierEvent } from 'browser/Types'; import { Terminal } from 'xterm'; import { BaseRenderLayer } from './BaseRenderLayer'; diff --git a/addons/xterm-addon-webgl/src/renderLayer/Types.ts b/addons/xterm-addon-webgl/src/renderLayer/Types.ts index 089680ca..3dbdfd9c 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/Types.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/Types.ts @@ -4,7 +4,6 @@ */ import { IDisposable, Terminal } from 'xterm'; -import { IColorSet, ReadonlyColorSet } from 'browser/Types'; import { IRenderDimensions } from 'browser/renderer/shared/Types'; export interface IRenderLayer extends IDisposable { @@ -14,7 +13,7 @@ export interface IRenderLayer extends IDisposable { handleBlur(terminal: Terminal): void; /** - * * Called when the terminal gets focus. + * Called when the terminal gets focus. */ handleFocus(terminal: Terminal): void; @@ -23,11 +22,6 @@ export interface IRenderLayer extends IDisposable { */ handleCursorMove(terminal: Terminal): void; - /** - * Called when options change. - */ - handleOptionsChanged(terminal: Terminal): void; - /** * Called when the data in the grid has changed (or needs to be rendered * again). diff --git a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts index 53073092..5e34aede 100644 --- a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts @@ -992,8 +992,8 @@ async function getCellColor(col: number, row: number): Promise { window.result = new Uint8Array(4); window.d = window.term._core._renderService.dimensions; window.gl.readPixels( - Math.floor((${col - 0.5}) * window.d.scaledCellWidth), - Math.floor(window.gl.drawingBufferHeight - 1 - (${row - 0.5}) * window.d.scaledCellHeight), + Math.floor((${col - 0.5}) * window.d.device.cell.width), + Math.floor(window.gl.drawingBufferHeight - 1 - (${row - 0.5}) * window.d.device.cell.height), 1, 1, window.gl.RGBA, window.gl.UNSIGNED_BYTE, window.result ); `); @@ -1003,12 +1003,12 @@ async function getCellColor(col: number, row: number): Promise { async function getCellPixels(col: number, row: number): Promise { await page.evaluate(` window.gl = window.term._core._renderService._renderer._gl; - window.result = new Uint8Array(window.d.scaledCellWidth * window.d.scaledCellHeight * 4); + window.result = new Uint8Array(window.d.device.cell.width * window.d.device.cell.height * 4); window.d = window.term._core._renderService.dimensions; window.gl.readPixels( - Math.floor(${col - 1} * window.d.scaledCellWidth), - Math.floor(window.gl.drawingBufferHeight - ${row} * window.d.scaledCellHeight), - window.d.scaledCellWidth, window.d.scaledCellHeight, window.gl.RGBA, window.gl.UNSIGNED_BYTE, window.result + Math.floor(${col - 1} * window.d.device.cell.width), + Math.floor(window.gl.drawingBufferHeight - ${row} * window.d.device.cell.height), + window.d.device.cell.width, window.d.device.cell.height, window.gl.RGBA, window.gl.UNSIGNED_BYTE, window.result ); `); return await page.evaluate(`Array.from(window.result)`); diff --git a/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts b/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts index 6865b6db..79108e0d 100644 --- a/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts +++ b/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts @@ -22,6 +22,11 @@ declare module 'xterm-addon-webgl' { */ public readonly onChangeTextureAtlas: IEvent; + /** + * An event that is fired when the a new page is added to the texture atlas. + */ + public readonly onAddTextureAtlasCanvas: IEvent; + constructor(preserveDrawingBuffer?: boolean); /** diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 1710922e..7b015064 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -9,7 +9,7 @@ trigger: jobs: - job: Linux pool: - vmImage: 'ubuntu-18.04' + vmImage: 'ubuntu-20.04' steps: - task: NodeTool@0 inputs: diff --git a/demo/client.ts b/demo/client.ts index 44a96eb3..38c4748a 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-restricted-syntax */ /** * Copyright (c) 2018 The xterm.js authors. All rights reserved. * @license MIT @@ -218,6 +219,8 @@ if (document.location.pathname === '/test') { document.getElementById('htmlserialize').addEventListener('click', htmlSerializeButtonHandler); document.getElementById('custom-glyph').addEventListener('click', writeCustomGlyphHandler); document.getElementById('load-test').addEventListener('click', loadTest); + document.getElementById('print-cjk').addEventListener('click', addCjk); + document.getElementById('print-cjk-sgr').addEventListener('click', addCjkRandomSgr); document.getElementById('powerline-symbol-test').addEventListener('click', powerlineSymbolTest); document.getElementById('underline-test').addEventListener('click', underlineTest); document.getElementById('ansi-colors').addEventListener('click', ansiColorsTest); @@ -248,8 +251,7 @@ function createTerminal(): void { addons.fit.instance = new FitAddon(); addons.unicode11.instance = new Unicode11Addon(); addons.webgl.instance = new WebglAddon(); - // TODO: Remove arguments when link provider API is the default - addons['web-links'].instance = new WebLinksAddon(undefined, undefined, true); + addons['web-links'].instance = new WebLinksAddon(); typedTerm.loadAddon(addons.fit.instance); typedTerm.loadAddon(addons.search.instance); typedTerm.loadAddon(addons.serialize.instance); @@ -274,8 +276,10 @@ function createTerminal(): void { typedTerm.loadAddon(addons.webgl.instance); setTimeout(() => { if (addons.webgl.instance !== undefined) { - addTextureAtlas(addons.webgl.instance.textureAtlas); - addons.webgl.instance.onChangeTextureAtlas(e => addTextureAtlas(e)); + setTextureAtlas(addons.webgl.instance.textureAtlas); + addons.webgl.instance.onChangeTextureAtlas(e => setTextureAtlas(e)); + addons.webgl.instance.onAddTextureAtlasCanvas(e => appendTextureAtlas(e)); + addons.webgl.instance.onRemoveTextureAtlasCanvas(e => removeTextureAtlas(e)); } }, 0); @@ -303,7 +307,7 @@ function createTerminal(): void { }); // fit is called within a setTimeout, cols and rows need this. - setTimeout(() => { + setTimeout(async () => { initOptions(term); // TODO: Clean this up, opt-cols/rows doesn't exist anymore (document.getElementById(`opt-cols`) as HTMLInputElement).value = term.cols; @@ -313,16 +317,14 @@ function createTerminal(): void { // Set terminal size again to set the specific dimensions on the demo updateTerminalSize(); - fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, { method: 'POST' }).then((res) => { - res.text().then((processId) => { - pid = processId; - socketURL += processId; - socket = new WebSocket(socketURL); - socket.onopen = runRealTerminal; - socket.onclose = runFakeTerminal; - socket.onerror = runFakeTerminal; - }); - }); + const res = await fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, { method: 'POST' }); + const processId = await res.text(); + pid = processId; + socketURL += processId; + socket = new WebSocket(socketURL); + socket.onopen = runRealTerminal; + socket.onclose = runFakeTerminal; + socket.onerror = runFakeTerminal; }, 0); } @@ -552,13 +554,15 @@ function initAddons(term: TerminalType): void { term.loadAddon(addon.instance); if (name === 'webgl') { setTimeout(() => { - addTextureAtlas(addons.webgl.instance.textureAtlas); - addons.webgl.instance.onChangeTextureAtlas(e => addTextureAtlas(e)); + setTextureAtlas(addons.webgl.instance.textureAtlas); + addons.webgl.instance.onChangeTextureAtlas(e => setTextureAtlas(e)); + addons.webgl.instance.onAddTextureAtlasCanvas(e => appendTextureAtlas(e)); }, 0); } else if (name === 'canvas') { setTimeout(() => { - addTextureAtlas(addons.canvas.instance.textureAtlas); - addons.canvas.instance.onChangeTextureAtlas(e => addTextureAtlas(e)); + setTextureAtlas(addons.canvas.instance.textureAtlas); + addons.canvas.instance.onChangeTextureAtlas(e => setTextureAtlas(e)); + addons.canvas.instance.onAddTextureAtlasCanvas(e => appendTextureAtlas(e)); }, 0); } else if (name === 'unicode11') { term.unicode.activeVersion = '11'; @@ -616,10 +620,8 @@ function addDomListener(element: HTMLElement, type: string, handler: (...args: a } function updateTerminalSize(): void { - const cols = parseInt((document.getElementById(`opt-cols`) as HTMLInputElement).value, 10); - const rows = parseInt((document.getElementById(`opt-rows`) as HTMLInputElement).value, 10); - const width = (cols * term._core._renderService.dimensions.actualCellWidth + term._core.viewport.scrollBarWidth).toString() + 'px'; - const height = (rows * term._core._renderService.dimensions.actualCellHeight).toString() + 'px'; + const width = (term._core._renderService.dimensions.css.canvas.width + term._core.viewport.scrollBarWidth).toString() + 'px'; + const height = (term._core._renderService.dimensions.css.canvas.height).toString() + 'px'; terminalContainer.style.width = width; terminalContainer.style.height = height; addons.fit.instance.fit(); @@ -651,9 +653,21 @@ function htmlSerializeButtonHandler(): void { document.getElementById('htmlserialize-output-result').innerText = 'Copied to clipboard'; } -function addTextureAtlas(e: HTMLCanvasElement): void { +function setTextureAtlas(e: HTMLCanvasElement): void { + styleAtlasPage(e); document.querySelector('#texture-atlas').replaceChildren(e); } +function appendTextureAtlas(e: HTMLCanvasElement): void { + styleAtlasPage(e); + document.querySelector('#texture-atlas').appendChild(e); +} +function removeTextureAtlas(e: HTMLCanvasElement): void { + e.remove(); +} +function styleAtlasPage(e: HTMLCanvasElement): void { + e.style.width = `${e.width / window.devicePixelRatio}px`; + e.style.height = `${e.height / window.devicePixelRatio}px`; +} function writeCustomGlyphHandler(): void { term.write('\n\r'); @@ -697,6 +711,7 @@ function writeCustomGlyphHandler(): void { term.write(' ║│╱ ╲│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╽ │┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▎\n\r'); term.write(' ║└─╥─┘║ │╚═╤═╝│ │╘═╪═╛│ │╙─╀─╜│ ┃└─╂─┘┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▏\n\r'); term.write(' ╚══╩══╝ └──┴──┘ ╰──┴──╯ ╰──┴──╯ ┗━━┻━━┛ └╌╌┘ ╎ ┗╍╍┛ ┋ ▁▂▃▄▅▆▇█\n\r'); + term.write('\x1b[0m'); window.scrollTo(0, 0); } @@ -968,6 +983,35 @@ function addAnsiHyperlink(): void { term.write('\x1b[3A\x1b[1C\x1b]8;;https://xtermjs.org\x07xter\x1b[B\x1b[4Dm.js\x1b]8;;\x07\x1b[2B\x1b[5D'); } +/** + * Prints the 20977 characters from the CJK Unified Ideographs unicode block. + */ +function addCjk(): void { + term.write('\n\n\r'); + for (let i = 0x4E00; i < 0x9FCC; i++) { + term.write(String.fromCharCode(i)); + } +} + +/** + * Prints the 20977 characters from the CJK Unified Ideographs unicode block with randomized styles. + */ +function addCjkRandomSgr(): void { + term.write('\n\n\r'); + for (let i = 0x4E00; i < 0x9FCC; i++) { + term.write(`\x1b[${getRandomSgr()}m${String.fromCharCode(i)}\x1b[0m`); + } +} +const randomSgrAttributes = [ + '1', '2', '3', '4', '5', '6', '7', '9', + '21', '22', '23', '24', '25', '26', '27', '28', '29', + '30', '31', '32', '33', '34', '35', '36', '37', '38', '39', + '40', '41', '42', '43', '44', '45', '46', '47', '48', '49' +]; +function getRandomSgr(): string { + return randomSgrAttributes[Math.floor(Math.random() * randomSgrAttributes.length)]; +} + function addDecoration(): void { term.options['overviewRulerWidth'] = 15; const marker = term.registerMarker(1); @@ -995,3 +1039,33 @@ function addOverviewRuler(): void { term.registerDecoration({ marker: term.registerMarker(10), overviewRulerOptions: { color: '#ffffff80', position: 'full' } }); } +(console as any).image = (source: ImageData | HTMLCanvasElement, scale: number = 1) => { + function getBox(width: number, height: number): any { + return { + string: '+', + style: 'font-size: 1px; padding: ' + Math.floor(height/2) + 'px ' + Math.floor(width/2) + 'px; line-height: ' + height + 'px;' + }; + } + if (source instanceof HTMLCanvasElement) { + source = source.getContext('2d')?.getImageData(0, 0, source.width, source.height)!; + } + const canvas = document.createElement('canvas'); + canvas.width = source.width; + canvas.height = source.height; + const ctx = canvas.getContext('2d')!; + ctx.putImageData(source, 0, 0); + + const sw = source.width * scale; + const sh = source.height * scale; + const dim = getBox(sw, sh); + console.log( + `Image: ${source.width} x ${source.height}\n%c${dim.string}`, + `${dim.style}background: url(${canvas.toDataURL()}); background-size: ${sw}px ${sh}px; background-repeat: no-repeat; color: transparent;` + ); + console.groupCollapsed('Zoomed'); + console.log( + `%c${dim.string}`, + `${getBox(sw * 10, sh * 10).style}background: url(${canvas.toDataURL()}); background-size: ${sw * 10}px ${sh * 10}px; background-repeat: no-repeat; color: transparent; image-rendering: pixelated;-ms-interpolation-mode: nearest-neighbor;` + ); + console.groupEnd(); +}; diff --git a/demo/index.html b/demo/index.html index a65b8bbc..0a7c8bb3 100644 --- a/demo/index.html +++ b/demo/index.html @@ -10,8 +10,6 @@ - -

xterm.js: A terminal for the web

@@ -74,6 +72,8 @@
Performance
+
+
Styles
@@ -91,6 +91,8 @@ + +