diff --git a/.eslintrc.json b/.eslintrc.json index 9510fc6b9..aa6e6417c 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -7,6 +7,7 @@ "parser": "@typescript-eslint/parser", "parserOptions": { "project": [ + "demo/tsconfig.json", "src/browser/tsconfig.json", "src/common/tsconfig.json", "src/headless/tsconfig.json", diff --git a/README.md b/README.md index 2c450e424..c78ae76ed 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,26 @@ The full API for xterm.js is contained within the [TypeScript declaration file]( Note that some APIs are marked *experimental*, these are added to enable experimentation with new ideas without committing to support it like a normal [semver](https://semver.org/) API. Note that these APIs can change radically between versions, so be sure to read release notes if you plan on using experimental APIs. +## Releases + +Xterm.js follows a monthly release cycle roughly. + +All current and past releases are available on this repo's [Releases page](https://github.com/sourcelair/xterm.js/releases), you can view the [high-level roadmap on the wiki](https://github.com/xtermjs/xterm.js/wiki/Roadmap) and see what we're working on now by looking through [Milestones](https://github.com/sourcelair/xterm.js/milestones). + +### Beta builds + +Our CI releases beta builds to npm for every change that goes into master. Install the latest beta build with: + +```bash +npm install -S xterm@beta +``` + +These should generally be stable, but some bugs may slip in. We recommend using the beta build primarily to test out new features and to verify bug fixes. + +## Contributing + +You can read the [guide on the wiki](https://github.com/xtermjs/xterm.js/wiki/Contributing) to learn how to contribute and set up xterm.js for development. + ## Real-world uses Xterm.js is used in several world-class applications to provide great terminal experiences. @@ -197,30 +217,10 @@ Xterm.js is used in several world-class applications to provide great terminal e Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it on our list. Note: Please add any new contributions to the end of the list only. -## Releases - -Xterm.js follows a monthly release cycle roughly. - -All current and past releases are available on this repo's [Releases page](https://github.com/sourcelair/xterm.js/releases), you can view the [high-level roadmap on the wiki](https://github.com/xtermjs/xterm.js/wiki/Roadmap) and see what we're working on now by looking through [Milestones](https://github.com/sourcelair/xterm.js/milestones). - -### Beta builds - -Our CI releases beta builds to npm for every change that goes into master. Install the latest beta build with: - -```bash -npm install -S xterm@beta -``` - -These should generally be stable, but some bugs may slip in. We recommend using the beta build primarily to test out new features and to verify bug fixes. - -## Contributing - -You can read the [guide on the wiki](https://github.com/xtermjs/xterm.js/wiki/Contributing) to learn how to contribute and set up xterm.js for development. - ## License Agreement If you contribute code to this project, you implicitly allow your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work. -Copyright (c) 2017-2019, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (MIT License)
+Copyright (c) 2017-2022, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (MIT License)
Copyright (c) 2014-2017, SourceLair, Private Company ([www.sourcelair.com](https://www.sourcelair.com/home)) (MIT License)
Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) diff --git a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts index f37ea1b85..2a9e09bac 100644 --- a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts @@ -3,23 +3,22 @@ * @license MIT */ -import { IRenderDimensions } from 'browser/renderer/Types'; -import { IRenderLayer } from './Types'; -import { ICellData, IColor } from 'common/Types'; -import { DEFAULT_COLOR, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE, Attributes } from 'common/buffer/Constants'; -import { IGlyphIdentifier } from './atlas/Types'; -import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, TEXT_BASELINE } from 'browser/renderer/Constants'; -import { BaseCharAtlas } from './atlas/BaseCharAtlas'; -import { acquireCharAtlas } from './atlas/CharAtlasCache'; -import { AttributeData } from 'common/buffer/AttributeData'; +import { removeElementFromParent } from 'browser/Dom'; +import { acquireTextureAtlas } from 'browser/renderer/shared/CharAtlasCache'; +import { TEXT_BASELINE } from 'browser/renderer/shared/Constants'; +import { tryDrawCustomChar } from 'browser/renderer/shared/CustomGlyphs'; +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 } from 'browser/services/Services'; import { IColorSet } 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 { ICoreBrowserService } from 'browser/services/Services'; -import { excludeFromContrastRatioDemands, throwIfFalsy } from 'browser/renderer/RendererUtils'; -import { channels, color, rgba } from 'common/Color'; -import { removeElementFromParent } from 'browser/Dom'; -import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs'; +import { ICellData } from 'common/Types'; +import { Terminal } from 'xterm'; +import { IRenderLayer } from './Types'; +import { CellColorResolver } from 'browser/renderer/shared/CellColorResolver'; export abstract class BaseRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; @@ -31,44 +30,34 @@ export abstract class BaseRenderLayer implements IRenderLayer { private _scaledCharLeft: number = 0; private _scaledCharTop: number = 0; - protected _selectionStart: [number, number] | undefined; - protected _selectionEnd: [number, number] | undefined; - protected _columnSelectMode: boolean = false; + protected _selectionModel: ISelectionRenderModel = createSelectionRenderModel(); + private _cellColorResolver: CellColorResolver; + private _bitmapGenerator?: BitmapGenerator; - protected _charAtlas: BaseCharAtlas | undefined; - - /** - * An object that's reused when drawing glyphs in order to reduce GC. - */ - private _currentGlyphIdentifier: IGlyphIdentifier = { - chars: '', - code: 0, - bg: 0, - fg: 0, - bold: false, - dim: false, - italic: false - }; + protected _charAtlas!: ITextureAtlas; public get canvas(): HTMLCanvasElement { return this._canvas; } + public get cacheCanvas(): HTMLCanvasElement { return this._charAtlas?.cacheCanvas!; } constructor( + private readonly _terminal: Terminal, private _container: HTMLElement, id: string, zIndex: number, private _alpha: boolean, protected _colors: IColorSet, - private _rendererId: number, protected readonly _bufferService: IBufferService, protected readonly _optionsService: IOptionsService, protected readonly _decorationService: IDecorationService, protected readonly _coreBrowserService: ICoreBrowserService ) { + this._cellColorResolver = new CellColorResolver(this._terminal, this._colors, this._selectionModel, this._decorationService, this._coreBrowserService); this._canvas = document.createElement('canvas'); this._canvas.classList.add(`xterm-${id}-layer`); this._canvas.style.zIndex = zIndex.toString(); this._initCanvas(); this._container.appendChild(this._canvas); + this._refreshCharAtlas(this._colors); } public dispose(): void { @@ -91,9 +80,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { public onGridChanged(startRow: number, endRow: number): void {} public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void { - this._selectionStart = start; - this._selectionEnd = end; - this._columnSelectMode = columnSelectMode; + this._selectionModel.update(this._terminal, start, end, columnSelectMode); } public setColors(colorSet: IColorSet): void { @@ -127,8 +114,9 @@ export abstract class BaseRenderLayer implements IRenderLayer { if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) { return; } - this._charAtlas = acquireCharAtlas(this._optionsService.rawOptions, this._rendererId, colorSet, this._scaledCharWidth, this._scaledCharHeight, this._coreBrowserService.dpr); + this._charAtlas = acquireTextureAtlas(this._terminal, colorSet, this._scaledCellWidth, this._scaledCellHeight, this._scaledCharWidth, this._scaledCharHeight, this._coreBrowserService.dpr); this._charAtlas.warmUp(); + this._bitmapGenerator = new BitmapGenerator(this._charAtlas.cacheCanvas); } public resize(dim: IRenderDimensions): void { @@ -154,7 +142,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { public abstract reset(): void; public clearTextureAtlas(): void { - this._charAtlas?.clear(); + this._charAtlas?.clearTexture(); } /** @@ -365,134 +353,37 @@ export abstract class BaseRenderLayer implements IRenderLayer { /** * Draws one or more characters at a cell. If possible this will draw using * the character atlas to reduce draw time. - * @param chars The character or characters. - * @param code The character code. - * @param width The width of the characters. - * @param x The column to draw at. - * @param y The row to draw at. - * @param fg The foreground color, in the format stored within the attributes. - * @param bg The background color, in the format stored within the attributes. - * This is used to validate whether a cached image can be used. - * @param bold Whether the text is bold. */ protected _drawChars(cell: ICellData, x: number, y: number): void { - const contrastColor = this._getContrastColor(cell, x, y); - - // skip cache right away if we draw in RGB - // Note: to avoid bad runtime JoinedCellData will be skipped - // in the cache handler itself (atlasDidDraw == false) and - // fall through to uncached later down below - if (contrastColor || cell.isFgRGB() || cell.isBgRGB()) { - this._drawUncachedChars(cell, x, y, contrastColor); - return; + if (this._charAtlas.hasCanvasChanged) { + this._bitmapGenerator?.refresh(); + this._charAtlas.hasCanvasChanged = false; } - - let fg; - let bg; - if (cell.isInverse()) { - fg = (cell.isBgDefault()) ? INVERTED_DEFAULT_COLOR : cell.getBgColor(); - bg = (cell.isFgDefault()) ? INVERTED_DEFAULT_COLOR : cell.getFgColor(); + const chars = cell.getChars(); + this._cellColorResolver.resolve(cell, x, 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); } else { - bg = (cell.isBgDefault()) ? DEFAULT_COLOR : cell.getBgColor(); - fg = (cell.isFgDefault()) ? DEFAULT_COLOR : cell.getFgColor(); + glyph = this._charAtlas.getRasterizedGlyph(cell.getCode() || WHITESPACE_CELL_CODE, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext); } - - const drawInBrightColor = this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8; - - fg += drawInBrightColor ? 8 : 0; - this._currentGlyphIdentifier.chars = cell.getChars() || WHITESPACE_CELL_CHAR; - this._currentGlyphIdentifier.code = cell.getCode() || WHITESPACE_CELL_CODE; - this._currentGlyphIdentifier.bg = bg; - this._currentGlyphIdentifier.fg = fg; - this._currentGlyphIdentifier.bold = !!cell.isBold(); - this._currentGlyphIdentifier.dim = !!cell.isDim(); - this._currentGlyphIdentifier.italic = !!cell.isItalic(); - - // Don't try cache the glyph if it uses any decoration foreground/background override. - let hasOverrides = false; - this._decorationService.forEachDecorationAtCell(x, y, undefined, d => { - if (d.backgroundColorRGB || d.foregroundColorRGB) { - hasOverrides = true; - } - }); - - const atlasDidDraw = hasOverrides ? false : this._charAtlas?.draw(this._ctx, this._currentGlyphIdentifier, x * this._scaledCellWidth + this._scaledCharLeft, y * this._scaledCellHeight + this._scaledCharTop); - - if (!atlasDidDraw) { - this._drawUncachedChars(cell, x, y); - } - } - - /** - * Draws one or more characters at one or more cells. The character(s) will be - * clipped to ensure that they fit with the cell(s), including the cell to the - * right if the last character is a wide character. - * @param chars The character. - * @param width The width of the character. - * @param fg The foreground color, in the format stored within the attributes. - * @param x The column to draw at. - * @param y The row to draw at. - */ - private _drawUncachedChars(cell: ICellData, x: number, y: number, fgOverride?: IColor): void { this._ctx.save(); - this._ctx.font = this._getFont(!!cell.isBold(), !!cell.isItalic()); - this._ctx.textBaseline = TEXT_BASELINE; - - if (cell.isInverse()) { - if (fgOverride) { - this._ctx.fillStyle = fgOverride.css; - } else if (cell.isBgDefault()) { - this._ctx.fillStyle = color.opaque(this._colors.background).css; - } else if (cell.isBgRGB()) { - this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`; - } else { - let bg = cell.getBgColor(); - if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && bg < 8) { - bg += 8; - } - this._ctx.fillStyle = this._colors.ansi[bg].css; - } - } else { - if (fgOverride) { - this._ctx.fillStyle = fgOverride.css; - } else if (cell.isFgDefault()) { - this._ctx.fillStyle = this._colors.foreground.css; - } else if (cell.isFgRGB()) { - this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`; - } else { - let fg = cell.getFgColor(); - if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8) { - fg += 8; - } - this._ctx.fillStyle = this._colors.ansi[fg].css; - } - } - this._clipRow(y); - - // Apply alpha to dim the character - if (cell.isDim()) { - this._ctx.globalAlpha = DIM_OPACITY; - } - - // 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); - } - - // Draw the character - if (!drawSuccess) { - this._ctx.fillText( - cell.getChars(), - x * this._scaledCellWidth + this._scaledCharLeft, - y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight); - } - + // Draw the image, use the bitmap if it's available + this._ctx.drawImage( + this._bitmapGenerator?.bitmap || this._charAtlas!.cacheCanvas, + glyph.texturePosition.x, + glyph.texturePosition.y, + glyph.size.x, + glyph.size.y, + x * this._scaledCellWidth - glyph.offset.x, + y * this._scaledCellHeight - glyph.offset.y, + glyph.size.x, + glyph.size.y + ); this._ctx.restore(); } - /** * Clips a row to ensure no pixels will be drawn outside the cells in the row. * @param y The row to clip. @@ -517,137 +408,55 @@ export abstract class BaseRenderLayer implements IRenderLayer { return `${fontStyle} ${fontWeight} ${this._optionsService.rawOptions.fontSize * this._coreBrowserService.dpr}px ${this._optionsService.rawOptions.fontFamily}`; } - - private _getContrastColor(cell: CellData, x: number, y: number): IColor | undefined { - // Get any decoration foreground/background overrides, this must be fetched before the early - // exist but applied after inverse - let bgOverride: number | undefined; - let fgOverride: number | undefined; - let isTop = false; - this._decorationService.forEachDecorationAtCell(x, y, undefined, d => { - if (d.options.layer !== 'top' && isTop) { - return; - } - if (d.backgroundColorRGB) { - bgOverride = d.backgroundColorRGB.rgba; - } - if (d.foregroundColorRGB) { - fgOverride = d.foregroundColorRGB.rgba; - } - isTop = d.options.layer === 'top'; - }); - - // Apply selection foreground if applicable - if (!isTop) { - if (this._colors.selectionForeground && this._isCellInSelection(x, y)) { - fgOverride = this._colors.selectionForeground.rgba; - } - } - - if (!bgOverride && !fgOverride && (this._optionsService.rawOptions.minimumContrastRatio === 1 || excludeFromContrastRatioDemands(cell.getCode()))) { - return undefined; - } - - if (!bgOverride && !fgOverride) { - // Try get from cache - const adjustedColor = this._colors.contrastCache.getColor(cell.bg, cell.fg); - if (adjustedColor !== undefined) { - return adjustedColor || undefined; - } - } - - let fgColor = cell.getFgColor(); - let fgColorMode = cell.getFgColorMode(); - let bgColor = cell.getBgColor(); - let bgColorMode = cell.getBgColorMode(); - const isInverse = !!cell.isInverse(); - const isBold = !!cell.isInverse(); - if (isInverse) { - const temp = fgColor; - fgColor = bgColor; - bgColor = temp; - const temp2 = fgColorMode; - fgColorMode = bgColorMode; - bgColorMode = temp2; - } - - const bgRgba = this._resolveBackgroundRgba(bgOverride !== undefined ? Attributes.CM_RGB : bgColorMode, bgOverride ?? bgColor, isInverse); - const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, isInverse, isBold); - let result = rgba.ensureContrastRatio(bgOverride ?? bgRgba, fgOverride ?? fgRgba, this._optionsService.rawOptions.minimumContrastRatio); - - if (!result) { - if (!fgOverride) { - this._colors.contrastCache.setColor(cell.bg, cell.fg, null); - return undefined; - } - // If it was an override and there was no contrast change, set as the result - result = fgOverride; - } - - const color: IColor = { - css: channels.toCss( - (result >> 24) & 0xFF, - (result >> 16) & 0xFF, - (result >> 8) & 0xFF - ), - rgba: result - }; - if (!bgOverride && !fgOverride) { - this._colors.contrastCache.setColor(cell.bg, cell.fg, color); - } - - return color; - } - - private _resolveBackgroundRgba(bgColorMode: number, bgColor: number, inverse: boolean): number { - switch (bgColorMode) { - case Attributes.CM_P16: - case Attributes.CM_P256: - return this._colors.ansi[bgColor].rgba; - case Attributes.CM_RGB: - return bgColor << 8; - case Attributes.CM_DEFAULT: - default: - if (inverse) { - return this._colors.foreground.rgba; - } - return this._colors.background.rgba; - } - } - - private _resolveForegroundRgba(fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): number { - switch (fgColorMode) { - case Attributes.CM_P16: - case Attributes.CM_P256: - if (this._optionsService.rawOptions.drawBoldTextInBrightColors && bold && fgColor < 8) { - fgColor += 8; - } - return this._colors.ansi[fgColor].rgba; - case Attributes.CM_RGB: - return fgColor << 8; - case Attributes.CM_DEFAULT: - default: - if (inverse) { - return this._colors.background.rgba; - } - return this._colors.foreground.rgba; - } - } - - private _isCellInSelection(x: number, y: number): boolean { - const start = this._selectionStart; - const end = this._selectionEnd; - if (!start || !end) { - return false; - } - if (this._columnSelectMode) { - return x >= start[0] && y >= start[1] && - x < end[0] && y < end[1]; - } - return (y > start[1] && y < end[1]) || - (start[1] === end[1] && y === start[1] && x >= start[0] && x < end[0]) || - (start[1] < end[1] && y === end[1] && x < end[0]) || - (start[1] < end[1] && y === start[1] && x >= start[0]); - } } +/** + * The number of milliseconds to wait before generating the ImageBitmap, this is to debounce/batch + * the operation as window.createImageBitmap is asynchronous. + */ +const GLYPH_BITMAP_COMMIT_DELAY = 100; + +const enum BitmapGeneratorState { + IDLE = 0, + GENERATING = 1, + GENERATING_INVALID = 2 +} + +class BitmapGenerator { + private _state: BitmapGeneratorState = BitmapGeneratorState.IDLE; + private _commitTimeout: number | undefined = undefined; + private _bitmap: ImageBitmap | undefined = undefined; + public get bitmap(): ImageBitmap | undefined { return this._bitmap; } + + constructor(private readonly _canvas: HTMLCanvasElement) { + } + + public refresh(): void { + // Clear the bitmap immediately as it's stale + this._bitmap = undefined; + if (this._commitTimeout === undefined) { + this._commitTimeout = window.setTimeout(() => this._generate(), GLYPH_BITMAP_COMMIT_DELAY); + } + if (this._state === BitmapGeneratorState.GENERATING) { + this._state = BitmapGeneratorState.GENERATING_INVALID; + } + } + + private _generate(): void { + if (this._state === BitmapGeneratorState.IDLE) { + this._bitmap = undefined; + this._state = BitmapGeneratorState.GENERATING; + window.createImageBitmap(this._canvas).then(bitmap => { + if (this._state === BitmapGeneratorState.GENERATING_INVALID) { + this.refresh(); + } else { + this._bitmap = bitmap; + } + this._state = BitmapGeneratorState.IDLE; + }); + if (this._commitTimeout) { + this._commitTimeout = undefined; + } + } + } +} diff --git a/addons/xterm-addon-canvas/src/CanvasAddon.ts b/addons/xterm-addon-canvas/src/CanvasAddon.ts index 32fe242e2..f90dad155 100644 --- a/addons/xterm-addon-canvas/src/CanvasAddon.ts +++ b/addons/xterm-addon-canvas/src/CanvasAddon.ts @@ -8,11 +8,15 @@ import { IColorSet } from 'browser/Types'; import { CanvasRenderer } from './CanvasRenderer'; import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; import { ITerminalAddon, Terminal } from 'xterm'; +import { EventEmitter, forwardEvent } from 'common/EventEmitter'; export class CanvasAddon implements ITerminalAddon { private _terminal?: Terminal; private _renderer?: CanvasRenderer; + private readonly _onChangeTextureAtlas = new EventEmitter(); + public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event; + public activate(terminal: Terminal): void { const core = (terminal as any)._core; if (!terminal.element) { @@ -31,7 +35,8 @@ export class CanvasAddon implements ITerminalAddon { const colors: IColorSet = core._colorManager.colors; const screenElement: HTMLElement = core.screenElement; const linkifier = core.linkifier2; - this._renderer = new CanvasRenderer(colors, screenElement, linkifier, bufferService, charSizeService, optionsService, characterJoinerService, coreService, coreBrowserService, decorationService); + this._renderer = new CanvasRenderer(terminal, colors, screenElement, linkifier, bufferService, charSizeService, optionsService, characterJoinerService, coreService, coreBrowserService, decorationService); + forwardEvent(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas); renderService.setRenderer(this._renderer); renderService.onResize(bufferService.cols, bufferService.rows); } @@ -46,4 +51,8 @@ export class CanvasAddon implements ITerminalAddon { this._renderer?.dispose(); this._renderer = undefined; } + + public get textureAtlas(): HTMLCanvasElement | undefined { + return this._renderer?.textureAtlas; + } } diff --git a/addons/xterm-addon-canvas/src/CanvasRenderer.ts b/addons/xterm-addon-canvas/src/CanvasRenderer.ts index b642efbca..34e75c016 100644 --- a/addons/xterm-addon-canvas/src/CanvasRenderer.ts +++ b/addons/xterm-addon-canvas/src/CanvasRenderer.ts @@ -3,34 +3,34 @@ * @license MIT */ -import { TextRenderLayer } from './TextRenderLayer'; -import { SelectionRenderLayer } from './SelectionRenderLayer'; -import { CursorRenderLayer } from './CursorRenderLayer'; -import { IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types'; -import { IRenderLayer } from './Types'; -import { LinkRenderLayer } from './LinkRenderLayer'; -import { Disposable } from 'common/Lifecycle'; -import { IColorSet, ILinkifier2 } from 'browser/Types'; +import { observeDevicePixelDimensions } from 'browser/renderer/shared/DevicePixelObserver'; +import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types'; import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService } from 'browser/services/Services'; -import { IBufferService, IOptionsService, IDecorationService, ICoreService } from 'common/services/Services'; +import { IColorSet, ILinkifier2 } from 'browser/Types'; +import { EventEmitter } from 'common/EventEmitter'; +import { Disposable } from 'common/Lifecycle'; +import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; +import { Terminal } from 'xterm'; import { removeTerminalFromCache } from './atlas/CharAtlasCache'; -import { EventEmitter, IEvent } from 'common/EventEmitter'; -import { observeDevicePixelDimensions } from 'browser/renderer/DevicePixelObserver'; - -let nextRendererId = 1; +import { CursorRenderLayer } from './CursorRenderLayer'; +import { LinkRenderLayer } from './LinkRenderLayer'; +import { SelectionRenderLayer } from './SelectionRenderLayer'; +import { TextRenderLayer } from './TextRenderLayer'; +import { IRenderLayer } from './Types'; export class CanvasRenderer extends Disposable implements IRenderer { - private _id = nextRendererId++; - private _renderLayers: IRenderLayer[]; private _devicePixelRatio: number; public dimensions: IRenderDimensions; - private _onRequestRedraw = new EventEmitter(); - public get onRequestRedraw(): IEvent { return this._onRequestRedraw.event; } + private readonly _onRequestRedraw = new EventEmitter(); + public readonly onRequestRedraw = this._onRequestRedraw.event; + private readonly _onChangeTextureAtlas = new EventEmitter(); + public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event; constructor( + private readonly _terminal: Terminal, private _colors: IColorSet, private readonly _screenElement: HTMLElement, linkifier2: ILinkifier2, @@ -45,10 +45,10 @@ export class CanvasRenderer extends Disposable implements IRenderer { super(); const allowTransparency = this._optionsService.rawOptions.allowTransparency; this._renderLayers = [ - new TextRenderLayer(this._screenElement, 0, this._colors, allowTransparency, this._id, this._bufferService, this._optionsService, characterJoinerService, decorationService, this._coreBrowserService), - new SelectionRenderLayer(this._screenElement, 1, this._colors, this._id, this._bufferService, this._coreBrowserService, decorationService, this._optionsService), - new LinkRenderLayer(this._screenElement, 2, this._colors, this._id, linkifier2, this._bufferService, this._optionsService, decorationService, this._coreBrowserService), - new CursorRenderLayer(this._screenElement, 3, this._colors, this._id, this._onRequestRedraw, this._bufferService, this._optionsService, coreService, this._coreBrowserService, decorationService) + new TextRenderLayer(this._terminal, this._screenElement, 0, this._colors, allowTransparency, this._bufferService, this._optionsService, characterJoinerService, decorationService, this._coreBrowserService), + new SelectionRenderLayer(this._terminal, this._screenElement, 1, this._colors, this._bufferService, this._coreBrowserService, decorationService, this._optionsService), + new LinkRenderLayer(this._terminal, this._screenElement, 2, this._colors, linkifier2, this._bufferService, this._optionsService, decorationService, this._coreBrowserService), + new CursorRenderLayer(this._terminal, this._screenElement, 3, this._colors, this._onRequestRedraw, this._bufferService, this._optionsService, coreService, this._coreBrowserService, decorationService) ]; this.dimensions = { scaledCharWidth: 0, @@ -77,7 +77,11 @@ export class CanvasRenderer extends Disposable implements IRenderer { l.dispose(); } super.dispose(); - removeTerminalFromCache(this._id); + removeTerminalFromCache(this._terminal); + } + + public get textureAtlas(): HTMLCanvasElement | undefined { + return this._renderLayers[0].cacheCanvas; } public onDevicePixelRatioChange(): void { diff --git a/addons/xterm-addon-canvas/src/CursorRenderLayer.ts b/addons/xterm-addon-canvas/src/CursorRenderLayer.ts index 61e2d9341..78dc54322 100644 --- a/addons/xterm-addon-canvas/src/CursorRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/CursorRenderLayer.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types'; +import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/shared/Types'; import { BaseRenderLayer } from './BaseRenderLayer'; import { ICellData } from 'common/Types'; import { CellData } from 'common/buffer/CellData'; @@ -11,6 +11,7 @@ import { IColorSet } from 'browser/Types'; import { IBufferService, IOptionsService, ICoreService, IDecorationService } from 'common/services/Services'; import { IEventEmitter } from 'common/EventEmitter'; import { ICoreBrowserService } from 'browser/services/Services'; +import { Terminal } from 'xterm'; interface ICursorState { x: number; @@ -32,10 +33,10 @@ export class CursorRenderLayer extends BaseRenderLayer { private _cell: ICellData = new CellData(); constructor( + terminal: Terminal, container: HTMLElement, zIndex: number, colors: IColorSet, - rendererId: number, private readonly _onRequestRedraw: IEventEmitter, bufferService: IBufferService, optionsService: IOptionsService, @@ -43,7 +44,7 @@ export class CursorRenderLayer extends BaseRenderLayer { coreBrowserService: ICoreBrowserService, decorationService: IDecorationService ) { - super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService, coreBrowserService); + super(terminal, container, 'cursor', zIndex, true, colors, bufferService, optionsService, decorationService, coreBrowserService); this._state = { x: 0, y: 0, diff --git a/addons/xterm-addon-canvas/src/LinkRenderLayer.ts b/addons/xterm-addon-canvas/src/LinkRenderLayer.ts index c07329fd6..db622b3fe 100644 --- a/addons/xterm-addon-canvas/src/LinkRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/LinkRenderLayer.ts @@ -3,29 +3,30 @@ * @license MIT */ -import { IRenderDimensions } from 'browser/renderer/Types'; +import { IRenderDimensions } from 'browser/renderer/shared/Types'; import { BaseRenderLayer } from './BaseRenderLayer'; -import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants'; +import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants'; import { ICoreBrowserService } from 'browser/services/Services'; -import { is256Color } from './atlas/CharAtlasUtils'; import { IColorSet, ILinkifierEvent, ILinkifier2 } from 'browser/Types'; import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; +import { is256Color } from 'browser/renderer/shared/CharAtlasUtils'; +import { Terminal } from 'xterm'; export class LinkRenderLayer extends BaseRenderLayer { private _state: ILinkifierEvent | undefined; constructor( + terminal: Terminal, container: HTMLElement, zIndex: number, colors: IColorSet, - rendererId: number, linkifier2: ILinkifier2, bufferService: IBufferService, optionsService: IOptionsService, decorationService: IDecorationService, coreBrowserService: ICoreBrowserService ) { - super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService, coreBrowserService); + super(terminal, container, 'link', zIndex, true, colors, bufferService, optionsService, decorationService, coreBrowserService); linkifier2.onShowLinkUnderline(e => this._onShowLinkUnderline(e)); linkifier2.onHideLinkUnderline(e => this._onHideLinkUnderline(e)); diff --git a/addons/xterm-addon-canvas/src/SelectionRenderLayer.ts b/addons/xterm-addon-canvas/src/SelectionRenderLayer.ts index e90007b72..759b0a123 100644 --- a/addons/xterm-addon-canvas/src/SelectionRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/SelectionRenderLayer.ts @@ -3,11 +3,12 @@ * @license MIT */ -import { IRenderDimensions } from 'browser/renderer/Types'; +import { IRenderDimensions } from 'browser/renderer/shared/Types'; import { BaseRenderLayer } from './BaseRenderLayer'; import { IColorSet } from 'browser/Types'; import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; import { ICoreBrowserService } from 'browser/services/Services'; +import { Terminal } from 'xterm'; interface ISelectionState { start?: [number, number]; @@ -20,16 +21,16 @@ export class SelectionRenderLayer extends BaseRenderLayer { private _state!: ISelectionState; constructor( + terminal: Terminal, container: HTMLElement, zIndex: number, colors: IColorSet, - rendererId: number, bufferService: IBufferService, coreBrowserService: ICoreBrowserService, decorationService: IDecorationService, optionsService: IOptionsService ) { - super(container, 'selection', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService, coreBrowserService); + super(terminal, container, 'selection', zIndex, true, colors, bufferService, optionsService, decorationService, coreBrowserService); this._clearState(); } @@ -46,8 +47,8 @@ export class SelectionRenderLayer extends BaseRenderLayer { super.resize(dim); // On resize use the base render layer's cached selection values since resize clears _state // inside reset. - if (this._selectionStart && this._selectionEnd) { - this._redrawSelection(this._selectionStart, this._selectionEnd, this._columnSelectMode); + if (this._selectionModel.selectionStart && this._selectionModel.selectionEnd) { + this._redrawSelection(this._selectionModel.selectionStart, this._selectionModel.selectionEnd, this._selectionModel.columnSelectMode); } } @@ -60,12 +61,12 @@ export class SelectionRenderLayer extends BaseRenderLayer { public onBlur(): void { this.reset(); - this._redrawSelection(this._selectionStart, this._selectionEnd, this._columnSelectMode); + this._redrawSelection(this._selectionModel.selectionStart, this._selectionModel.selectionEnd, this._selectionModel.columnSelectMode); } public onFocus(): void { this.reset(); - this._redrawSelection(this._selectionStart, this._selectionEnd, this._columnSelectMode); + this._redrawSelection(this._selectionModel.selectionStart, this._selectionModel.selectionEnd, this._selectionModel.columnSelectMode); } public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void { diff --git a/addons/xterm-addon-canvas/src/TextRenderLayer.ts b/addons/xterm-addon-canvas/src/TextRenderLayer.ts index 95f22fce7..8d80514b4 100644 --- a/addons/xterm-addon-canvas/src/TextRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/TextRenderLayer.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IRenderDimensions } from 'browser/renderer/Types'; +import { IRenderDimensions } from 'browser/renderer/shared/Types'; import { CharData, ICellData } from 'common/Types'; import { GridCache } from './GridCache'; import { BaseRenderLayer } from './BaseRenderLayer'; @@ -15,6 +15,7 @@ import { IOptionsService, IBufferService, IDecorationService } from 'common/serv import { ICharacterJoinerService, ICoreBrowserService } from 'browser/services/Services'; import { JoinedCellData } from 'browser/services/CharacterJoinerService'; import { color, css } from 'common/Color'; +import { Terminal } from 'xterm'; /** * This CharData looks like a null character, which will forc a clear and render @@ -31,18 +32,18 @@ export class TextRenderLayer extends BaseRenderLayer { private _workCell = new CellData(); constructor( + terminal: Terminal, container: HTMLElement, zIndex: number, colors: IColorSet, alpha: boolean, - rendererId: number, bufferService: IBufferService, optionsService: IOptionsService, private readonly _characterJoinerService: ICharacterJoinerService, decorationService: IDecorationService, coreBrowserService: ICoreBrowserService ) { - super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService, decorationService, coreBrowserService); + super(terminal, container, 'text', zIndex, alpha, colors, bufferService, optionsService, decorationService, coreBrowserService); this._state = new GridCache(); } @@ -232,78 +233,7 @@ export class TextRenderLayer extends BaseRenderLayer { } private _drawForeground(firstRow: number, lastRow: number): void { - this._forEachCell(firstRow, lastRow, (cell, x, y) => { - if (cell.isInvisible()) { - return; - } - this._drawChars(cell, x, y); - if (cell.isUnderline() || cell.isStrikethrough()) { - this._ctx.save(); - - if (cell.isInverse()) { - if (cell.isBgDefault()) { - this._ctx.fillStyle = this._colors.background.css; - } else if (cell.isBgRGB()) { - this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`; - } else { - let bg = cell.getBgColor(); - if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && bg < 8) { - bg += 8; - } - this._ctx.fillStyle = this._colors.ansi[bg].css; - } - } else { - if (cell.isFgDefault()) { - this._ctx.fillStyle = this._colors.foreground.css; - } else if (cell.isFgRGB()) { - this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`; - } else { - let fg = cell.getFgColor(); - if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8) { - fg += 8; - } - this._ctx.fillStyle = this._colors.ansi[fg].css; - } - } - - if (cell.isStrikethrough()) { - this._fillMiddleLineAtCells(x, y, cell.getWidth()); - } - if (cell.isUnderline()) { - if (!cell.isUnderlineColorDefault()) { - if (cell.isUnderlineColorRGB()) { - this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getUnderlineColor()).join(',')})`; - } else { - let fg = cell.getUnderlineColor(); - if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8) { - fg += 8; - } - this._ctx.fillStyle = this._colors.ansi[fg].css; - } - } - switch (cell.extended.underlineStyle) { - case UnderlineStyle.DOUBLE: - this._fillBottomLineAtCells(x, y, cell.getWidth(), -this._coreBrowserService.dpr); - this._fillBottomLineAtCells(x, y, cell.getWidth(), this._coreBrowserService.dpr); - break; - case UnderlineStyle.CURLY: - this._curlyUnderlineAtCell(x, y, cell.getWidth()); - break; - case UnderlineStyle.DOTTED: - this._dottedUnderlineAtCell(x, y, cell.getWidth()); - break; - case UnderlineStyle.DASHED: - this._dashedUnderlineAtCell(x, y, cell.getWidth()); - break; - case UnderlineStyle.SINGLE: - default: - this._fillBottomLineAtCells(x, y, cell.getWidth()); - break; - } - } - this._ctx.restore(); - } - }); + this._forEachCell(firstRow, lastRow, (cell, x, y) => this._drawChars(cell, x, y)); } public onGridChanged(firstRow: number, lastRow: number): void { diff --git a/addons/xterm-addon-canvas/src/Types.d.ts b/addons/xterm-addon-canvas/src/Types.d.ts index dda6052ae..0527eae9d 100644 --- a/addons/xterm-addon-canvas/src/Types.d.ts +++ b/addons/xterm-addon-canvas/src/Types.d.ts @@ -58,6 +58,7 @@ export interface IRenderer extends IDisposable { export interface IRenderLayer extends IDisposable { readonly canvas: HTMLCanvasElement; + readonly cacheCanvas: HTMLCanvasElement; /** * Called when the terminal loses focus. diff --git a/addons/xterm-addon-canvas/src/atlas/BaseCharAtlas.ts b/addons/xterm-addon-canvas/src/atlas/BaseCharAtlas.ts deleted file mode 100644 index 03cf0285f..000000000 --- a/addons/xterm-addon-canvas/src/atlas/BaseCharAtlas.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { IGlyphIdentifier } from './Types'; -import { IDisposable } from 'common/Types'; - -export abstract class BaseCharAtlas implements IDisposable { - private _didWarmUp: boolean = false; - - public dispose(): void { } - - /** - * Perform any work needed to warm the cache before it can be used. May be called multiple times. - * Implement _doWarmUp instead if you only want to get called once. - */ - public warmUp(): void { - if (!this._didWarmUp) { - this._doWarmUp(); - this._didWarmUp = true; - } - } - - /** - * Perform any work needed to warm the cache before it can be used. Used by the default - * implementation of warmUp(), and will only be called once. - */ - private _doWarmUp(): void { } - - public clear(): void { } - - /** - * Called when we start drawing a new frame. - * - * TODO: We rely on this getting called by TextRenderLayer. This should really be called by - * Renderer instead, but we need to make Renderer the source-of-truth for the char atlas, instead - * of BaseRenderLayer. - */ - public beginFrame(): void { } - - /** - * May be called before warmUp finishes, however it is okay for the implementation to - * do nothing and return false in that case. - * - * @param ctx Where to draw the character onto. - * @param glyph Information about what to draw - * @param x The position on the context to start drawing at - * @param y The position on the context to start drawing at - * @returns The success state. True if we drew the character. - */ - public abstract draw( - ctx: CanvasRenderingContext2D, - glyph: IGlyphIdentifier, - x: number, - y: number - ): boolean; -} diff --git a/addons/xterm-addon-canvas/src/atlas/DynamicCharAtlas.ts b/addons/xterm-addon-canvas/src/atlas/CanvasCharAtlas.ts similarity index 92% rename from addons/xterm-addon-canvas/src/atlas/DynamicCharAtlas.ts rename to addons/xterm-addon-canvas/src/atlas/CanvasCharAtlas.ts index 3098538dd..a14ff5d71 100644 --- a/addons/xterm-addon-canvas/src/atlas/DynamicCharAtlas.ts +++ b/addons/xterm-addon-canvas/src/atlas/CanvasCharAtlas.ts @@ -3,26 +3,21 @@ * @license MIT */ -import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, TEXT_BASELINE } from 'browser/renderer/Constants'; -import { IGlyphIdentifier, ICharAtlasConfig } from './Types'; -import { BaseCharAtlas } from './BaseCharAtlas'; +import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, TEXT_BASELINE } from 'browser/renderer/shared/Constants'; +import { IGlyphIdentifier } from './Types'; import { DEFAULT_ANSI_COLORS } from 'browser/ColorManager'; import { LRUMap } from './LRUMap'; import { isFirefox, isSafari } from 'common/Platform'; import { IColor } from 'common/Types'; -import { throwIfFalsy } from 'browser/renderer/RendererUtils'; -import { color } from 'common/Color'; +import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; +import { color, NULL_COLOR } from 'common/Color'; +import { ICharAtlasConfig } from 'browser/renderer/shared/Types'; // In practice we're probably never going to exhaust a texture this large. For debugging purposes, // however, it can be useful to set this to a really tiny value, to verify that LRU eviction works. const TEXTURE_WIDTH = 1024; const TEXTURE_HEIGHT = 1024; -const TRANSPARENT_COLOR = { - css: 'rgba(0, 0, 0, 0)', - rgba: 0 -}; - // Drawing to the cache is expensive: If we have to draw more than this number of glyphs to the // cache in a single frame, give up on trying to cache anything else, and try to finish the current // frame ASAP. @@ -56,14 +51,16 @@ export function getGlyphCacheKey(glyph: IGlyphIdentifier): number { return glyph.code << 21 | glyph.bg << 12 | glyph.fg << 3 | (glyph.bold ? 0 : 4) + (glyph.dim ? 0 : 2) + (glyph.italic ? 0 : 1); } -export class DynamicCharAtlas extends BaseCharAtlas { +export class CanvasCharAtlas { // An ordered map that we're using to keep track of where each glyph is in the atlas texture. // It's ordered so that we can determine when to remove the old entries. private _cacheMap: LRUMap; // The texture that the atlas is drawn to private _cacheCanvas: HTMLCanvasElement; + public get cacheCanvas(): HTMLCanvasElement { return this._cacheCanvas; } private _cacheCtx: CanvasRenderingContext2D; + private _didWarmUp: boolean = false; // A temporary context that glyphs are drawn to before being transfered to the atlas. private _tmpCtx: CanvasRenderingContext2D; @@ -84,7 +81,6 @@ export class DynamicCharAtlas extends BaseCharAtlas { private _bitmap: ImageBitmap | null = null; constructor(document: Document, private _config: ICharAtlasConfig) { - super(); this._cacheCanvas = document.createElement('canvas'); this._cacheCanvas.width = TEXTURE_WIDTH; this._cacheCanvas.height = TEXTURE_HEIGHT; @@ -96,7 +92,10 @@ export class DynamicCharAtlas extends BaseCharAtlas { const tmpCanvas = document.createElement('canvas'); tmpCanvas.width = this._config.scaledCharWidth; tmpCanvas.height = this._config.scaledCharHeight; - this._tmpCtx = throwIfFalsy(tmpCanvas.getContext('2d', { alpha: this._config.allowTransparency })); + this._tmpCtx = throwIfFalsy(tmpCanvas.getContext('2d', { + alpha: this._config.allowTransparency, + willReadFrequently: true + })); this._width = Math.floor(TEXTURE_WIDTH / this._config.scaledCharWidth); this._height = Math.floor(TEXTURE_HEIGHT / this._config.scaledCharHeight); @@ -115,6 +114,23 @@ export class DynamicCharAtlas extends BaseCharAtlas { } } + /** + * Perform any work needed to warm the cache before it can be used. May be called multiple times. + * Implement _doWarmUp instead if you only want to get called once. + */ + public warmUp(): void { + if (!this._didWarmUp) { + this._doWarmUp(); + this._didWarmUp = true; + } + } + + /** + * Perform any work needed to warm the cache before it can be used. Used by the default + * implementation of warmUp(), and will only be called once. + */ + private _doWarmUp(): void { } + public beginFrame(): void { this._drawToCacheCount = 0; } @@ -223,7 +239,7 @@ export class DynamicCharAtlas extends BaseCharAtlas { // The background color might have some transparency, so we need to render it as fully // transparent in the atlas. Otherwise we'd end up drawing the transparent background twice // around the anti-aliased edges of the glyph, and it would look too dark. - return TRANSPARENT_COLOR; + return NULL_COLOR; } let result: IColor; if (glyph.bg === INVERTED_DEFAULT_COLOR) { @@ -371,23 +387,6 @@ export class DynamicCharAtlas extends BaseCharAtlas { } } -// This is used for debugging the renderer, just swap out `new DynamicCharAtlas` with -// `new NoneCharAtlas`. -export class NoneCharAtlas extends BaseCharAtlas { - constructor(document: Document, config: ICharAtlasConfig) { - super(); - } - - public draw( - ctx: CanvasRenderingContext2D, - glyph: IGlyphIdentifier, - x: number, - y: number - ): boolean { - return false; - } -} - /** * Makes a particular rgb color and colors that are nearly the same in an ImageData completely * transparent. diff --git a/addons/xterm-addon-canvas/src/atlas/CharAtlasCache.ts b/addons/xterm-addon-canvas/src/atlas/CharAtlasCache.ts index d9349286f..f5995cf8e 100644 --- a/addons/xterm-addon-canvas/src/atlas/CharAtlasCache.ts +++ b/addons/xterm-addon-canvas/src/atlas/CharAtlasCache.ts @@ -3,19 +3,18 @@ * @license MIT */ -import { generateConfig, configEquals } from './CharAtlasUtils'; -import { BaseCharAtlas } from './BaseCharAtlas'; -import { DynamicCharAtlas } from './DynamicCharAtlas'; -import { ICharAtlasConfig } from './Types'; +import { generateConfig, configEquals } from 'browser/renderer/shared/CharAtlasUtils'; +import { CanvasCharAtlas } from './CanvasCharAtlas'; import { IColorSet } from 'browser/Types'; -import { ITerminalOptions } from 'xterm'; +import { Terminal } from 'xterm'; +import { ICharAtlasConfig } from 'browser/renderer/shared/Types'; interface ICharAtlasCacheEntry { - atlas: BaseCharAtlas; + atlas: CanvasCharAtlas; config: ICharAtlasConfig; // N.B. This implementation potentially holds onto copies of the terminal forever, so // this may cause memory leaks. - ownedBy: number[]; + ownedBy: Terminal[]; } const charAtlasCache: ICharAtlasCacheEntry[] = []; @@ -25,19 +24,20 @@ const charAtlasCache: ICharAtlasCacheEntry[] = []; * one that is in use by another terminal. */ export function acquireCharAtlas( - options: Required, - rendererId: number, + terminal: Terminal, colors: IColorSet, + scaledCellWidth: number, + scaledCellHeight: number, scaledCharWidth: number, scaledCharHeight: number, devicePixelRatio: number -): BaseCharAtlas { - const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, options, colors, devicePixelRatio); +): CanvasCharAtlas { + const newConfig = generateConfig(scaledCellWidth, scaledCellHeight, scaledCharWidth, scaledCharHeight, terminal, colors, devicePixelRatio); // Check to see if the renderer already owns this config for (let i = 0; i < charAtlasCache.length; i++) { const entry = charAtlasCache[i]; - const ownedByIndex = entry.ownedBy.indexOf(rendererId); + const ownedByIndex = entry.ownedBy.indexOf(terminal); if (ownedByIndex >= 0) { if (configEquals(entry.config, newConfig)) { return entry.atlas; @@ -58,18 +58,18 @@ export function acquireCharAtlas( const entry = charAtlasCache[i]; if (configEquals(entry.config, newConfig)) { // Add the renderer to the cache entry and return - entry.ownedBy.push(rendererId); + entry.ownedBy.push(terminal); return entry.atlas; } } const newEntry: ICharAtlasCacheEntry = { - atlas: new DynamicCharAtlas( + atlas: new CanvasCharAtlas( document, newConfig ), config: newConfig, - ownedBy: [rendererId] + ownedBy: [terminal] }; charAtlasCache.push(newEntry); return newEntry.atlas; @@ -78,9 +78,9 @@ export function acquireCharAtlas( /** * Removes a terminal reference from the cache, allowing its memory to be freed. */ -export function removeTerminalFromCache(rendererId: number): void { +export function removeTerminalFromCache(terminal: Terminal): void { for (let i = 0; i < charAtlasCache.length; i++) { - const index = charAtlasCache[i].ownedBy.indexOf(rendererId); + const index = charAtlasCache[i].ownedBy.indexOf(terminal); if (index !== -1) { if (charAtlasCache[i].ownedBy.length === 1) { // Remove the cache entry if it's the only renderer diff --git a/addons/xterm-addon-canvas/src/atlas/CharAtlasUtils.ts b/addons/xterm-addon-canvas/src/atlas/CharAtlasUtils.ts deleted file mode 100644 index b0151e30d..000000000 --- a/addons/xterm-addon-canvas/src/atlas/CharAtlasUtils.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { ICharAtlasConfig } from './Types'; -import { DEFAULT_COLOR } from 'common/buffer/Constants'; -import { IColorSet, IPartialColorSet } from 'browser/Types'; -import { ITerminalOptions } from 'xterm'; - -export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, options: Required, colors: IColorSet, devicePixelRatio: number): ICharAtlasConfig { - // null out some fields that don't matter - const clonedColors: IPartialColorSet = { - foreground: colors.foreground, - background: colors.background, - cursor: undefined, - cursorAccent: undefined, - selectionBackground: undefined, - ansi: colors.ansi.slice() - }; - return { - devicePixelRatio, - scaledCharWidth, - scaledCharHeight, - fontFamily: options.fontFamily, - fontSize: options.fontSize, - fontWeight: options.fontWeight, - fontWeightBold: options.fontWeightBold, - allowTransparency: options.allowTransparency, - colors: clonedColors - }; -} - -export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean { - for (let i = 0; i < a.colors.ansi.length; i++) { - if (a.colors.ansi[i].rgba !== b.colors.ansi[i].rgba) { - return false; - } - } - return a.devicePixelRatio === b.devicePixelRatio && - a.fontFamily === b.fontFamily && - a.fontSize === b.fontSize && - a.fontWeight === b.fontWeight && - a.fontWeightBold === b.fontWeightBold && - a.allowTransparency === b.allowTransparency && - a.scaledCharWidth === b.scaledCharWidth && - a.scaledCharHeight === b.scaledCharHeight && - a.colors.foreground === b.colors.foreground && - a.colors.background === b.colors.background; -} - -export function is256Color(colorCode: number): boolean { - return colorCode < DEFAULT_COLOR; -} diff --git a/addons/xterm-addon-canvas/src/atlas/Types.d.ts b/addons/xterm-addon-canvas/src/atlas/Types.d.ts index d8bc54c19..cde8abec3 100644 --- a/addons/xterm-addon-canvas/src/atlas/Types.d.ts +++ b/addons/xterm-addon-canvas/src/atlas/Types.d.ts @@ -15,15 +15,3 @@ export interface IGlyphIdentifier { dim: boolean; italic: boolean; } - -export interface ICharAtlasConfig { - devicePixelRatio: number; - fontSize: number; - fontFamily: string; - fontWeight: FontWeight; - fontWeightBold: FontWeight; - scaledCharWidth: number; - scaledCharHeight: number; - allowTransparency: boolean; - colors: IPartialColorSet; -} 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 7bbbd63db..6a2b98d4a 100644 --- a/addons/xterm-addon-canvas/typings/xterm-addon-canvas.d.ts +++ b/addons/xterm-addon-canvas/typings/xterm-addon-canvas.d.ts @@ -12,6 +12,11 @@ declare module 'xterm-addon-canvas' { export class CanvasAddon implements ITerminalAddon { public textureAtlas?: HTMLCanvasElement; + /** + * An event that is fired when the texture atlas of the renderer changes. + */ + public readonly onChangeTextureAtlas: IEvent; + constructor(); /** diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index a7f4a700f..f27ef29ff 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -3,15 +3,15 @@ * @license MIT */ -import { createProgram, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils'; -import { WebglCharAtlas } from './atlas/WebglCharAtlas'; -import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel, IRasterizedGlyph } from './Types'; +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 { IRenderDimensions } from 'browser/renderer/Types'; +import { IRasterizedGlyph, IRenderDimensions, ITextureAtlas } from 'browser/renderer/shared/Types'; import { Disposable, toDisposable } from 'common/Lifecycle'; +import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; interface IVertices { attributes: Float32Array; @@ -70,16 +70,14 @@ const INDICES_PER_CELL = 10; const BYTES_PER_CELL = INDICES_PER_CELL * Float32Array.BYTES_PER_ELEMENT; const CELL_POSITION_INDICES = 2; -/** Work variables to avoid garbage collection. */ -const w: { i: number, glyph: IRasterizedGlyph | undefined, leftCellPadding: number, clippedPixels: number } = { - i: 0, - glyph: undefined, - leftCellPadding: 0, - clippedPixels: 0 -}; +// Work variables to avoid garbage collection +let $i = 0; +let $glyph: IRasterizedGlyph | undefined = undefined; +let $leftCellPadding = 0; +let $clippedPixels = 0; export class GlyphRenderer extends Disposable { - private _atlas: WebglCharAtlas | undefined; + private _atlas: ITextureAtlas | undefined; private _program: WebGLProgram; private _vertexArrayObject: IWebGLVertexArrayObject; @@ -186,12 +184,12 @@ export class GlyphRenderer extends Disposable { } private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, bg: number, fg: number, ext: number, chars: string, lastBg: number): void { - w.i = (y * this._terminal.cols + x) * INDICES_PER_CELL; + $i = (y * this._terminal.cols + x) * INDICES_PER_CELL; // 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, w.i, w.i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES); + fill(array, 0, $i, $i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES); return; } @@ -201,39 +199,39 @@ export class GlyphRenderer extends Disposable { // Get the glyph if (chars && chars.length > 1) { - w.glyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext); + $glyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext); } else { - w.glyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext); + $glyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext); } - w.leftCellPadding = Math.floor((this._dimensions.scaledCellWidth - this._dimensions.scaledCharWidth) / 2); - if (bg !== lastBg && w.glyph.offset.x > w.leftCellPadding) { - w.clippedPixels = w.glyph.offset.x - w.leftCellPadding; + $leftCellPadding = Math.floor((this._dimensions.scaledCellWidth - this._dimensions.scaledCharWidth) / 2); + if (bg !== lastBg && $glyph.offset.x > $leftCellPadding) { + $clippedPixels = $glyph.offset.x - $leftCellPadding; // a_origin - array[w.i ] = -(w.glyph.offset.x - w.clippedPixels) + this._dimensions.scaledCharLeft; - array[w.i + 1] = -w.glyph.offset.y + this._dimensions.scaledCharTop; + array[$i ] = -($glyph.offset.x - $clippedPixels) + this._dimensions.scaledCharLeft; + array[$i + 1] = -$glyph.offset.y + this._dimensions.scaledCharTop; // a_size - array[w.i + 2] = (w.glyph.size.x - w.clippedPixels) / this._dimensions.scaledCanvasWidth; - array[w.i + 3] = w.glyph.size.y / this._dimensions.scaledCanvasHeight; + array[$i + 2] = ($glyph.size.x - $clippedPixels) / this._dimensions.scaledCanvasWidth; + array[$i + 3] = $glyph.size.y / this._dimensions.scaledCanvasHeight; // a_texcoord - array[w.i + 4] = w.glyph.texturePositionClipSpace.x + w.clippedPixels / this._atlas.cacheCanvas.width; - array[w.i + 5] = w.glyph.texturePositionClipSpace.y; + array[$i + 4] = $glyph.texturePositionClipSpace.x + $clippedPixels / this._atlas.cacheCanvas.width; + array[$i + 5] = $glyph.texturePositionClipSpace.y; // a_texsize - array[w.i + 6] = w.glyph.sizeClipSpace.x - w.clippedPixels / this._atlas.cacheCanvas.width; - array[w.i + 7] = w.glyph.sizeClipSpace.y; + array[$i + 6] = $glyph.sizeClipSpace.x - $clippedPixels / this._atlas.cacheCanvas.width; + array[$i + 7] = $glyph.sizeClipSpace.y; } else { // a_origin - array[w.i ] = -w.glyph.offset.x + this._dimensions.scaledCharLeft; - array[w.i + 1] = -w.glyph.offset.y + this._dimensions.scaledCharTop; + array[$i ] = -$glyph.offset.x + this._dimensions.scaledCharLeft; + array[$i + 1] = -$glyph.offset.y + this._dimensions.scaledCharTop; // a_size - array[w.i + 2] = w.glyph.size.x / this._dimensions.scaledCanvasWidth; - array[w.i + 3] = w.glyph.size.y / this._dimensions.scaledCanvasHeight; + array[$i + 2] = $glyph.size.x / this._dimensions.scaledCanvasWidth; + array[$i + 3] = $glyph.size.y / this._dimensions.scaledCanvasHeight; // a_texcoord - array[w.i + 4] = w.glyph.texturePositionClipSpace.x; - array[w.i + 5] = w.glyph.texturePositionClipSpace.y; + array[$i + 4] = $glyph.texturePositionClipSpace.x; + array[$i + 5] = $glyph.texturePositionClipSpace.y; // a_texsize - array[w.i + 6] = w.glyph.sizeClipSpace.x; - array[w.i + 7] = w.glyph.sizeClipSpace.y; + array[$i + 6] = $glyph.sizeClipSpace.x; + array[$i + 7] = $glyph.sizeClipSpace.y; } // a_cellpos only changes on resize } @@ -323,7 +321,7 @@ export class GlyphRenderer extends Disposable { gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, bufferLength / INDICES_PER_CELL); } - public setAtlas(atlas: WebglCharAtlas): void { + public setAtlas(atlas: ITextureAtlas): void { const gl = this._gl; this._atlas = atlas; diff --git a/addons/xterm-addon-webgl/src/RectangleRenderer.ts b/addons/xterm-addon-webgl/src/RectangleRenderer.ts index ef08fb7c2..50a8cd58b 100644 --- a/addons/xterm-addon-webgl/src/RectangleRenderer.ts +++ b/addons/xterm-addon-webgl/src/RectangleRenderer.ts @@ -3,16 +3,17 @@ * @license MIT */ -import { createProgram, expandFloat32Array, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils'; +import { createProgram, expandFloat32Array, PROJECTION_MATRIX } from './WebglUtils'; import { IRenderModel, IWebGLVertexArrayObject, IWebGL2RenderingContext } from './Types'; import { Attributes, BgFlags, FgFlags } from 'common/buffer/Constants'; import { Terminal } from 'xterm'; import { IColor } from 'common/Types'; import { IColorSet } from 'browser/Types'; -import { IRenderDimensions } from 'browser/renderer/Types'; +import { IRenderDimensions } from 'browser/renderer/shared/Types'; import { RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel'; import { Disposable, toDisposable } from 'common/Lifecycle'; -import { DIM_OPACITY } from 'browser/renderer/Constants'; +import { DIM_OPACITY } from 'browser/renderer/shared/Constants'; +import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; const enum VertexAttribLocations { POSITION = 0, @@ -58,17 +59,15 @@ const BYTES_PER_RECTANGLE = INDICES_PER_RECTANGLE * Float32Array.BYTES_PER_ELEME const INITIAL_BUFFER_RECTANGLE_CAPACITY = 20 * INDICES_PER_RECTANGLE; -/** Work variables to avoid garbage collection. */ -const w: { rgba: number, isDefault: boolean, x1: number, y1: number, r: number, g: number, b: number, a: number } = { - rgba: 0, - isDefault: false, - x1: 0, - y1: 0, - r: 0, - g: 0, - b: 0, - a: 0 -}; +// Work variables to avoid garbage collection +let $rgba = 0; +let $isDefault = false; +let $x1 = 0; +let $y1 = 0; +let $r = 0; +let $g = 0; +let $b = 0; +let $a = 0; export class RectangleRenderer extends Disposable { @@ -232,47 +231,47 @@ export class RectangleRenderer extends Disposable { } private _updateRectangle(vertices: IVertices, offset: number, fg: number, bg: number, startX: number, endX: number, y: number): void { - w.isDefault = false; + $isDefault = false; if (fg & FgFlags.INVERSE) { switch (fg & Attributes.CM_MASK) { case Attributes.CM_P16: case Attributes.CM_P256: - w.rgba = this._colors.ansi[fg & Attributes.PCOLOR_MASK].rgba; + $rgba = this._colors.ansi[fg & Attributes.PCOLOR_MASK].rgba; break; case Attributes.CM_RGB: - w.rgba = (fg & Attributes.RGB_MASK) << 8; + $rgba = (fg & Attributes.RGB_MASK) << 8; break; case Attributes.CM_DEFAULT: default: - w.rgba = this._colors.foreground.rgba; + $rgba = this._colors.foreground.rgba; } } else { switch (bg & Attributes.CM_MASK) { case Attributes.CM_P16: case Attributes.CM_P256: - w.rgba = this._colors.ansi[bg & Attributes.PCOLOR_MASK].rgba; + $rgba = this._colors.ansi[bg & Attributes.PCOLOR_MASK].rgba; break; case Attributes.CM_RGB: - w.rgba = (bg & Attributes.RGB_MASK) << 8; + $rgba = (bg & Attributes.RGB_MASK) << 8; break; case Attributes.CM_DEFAULT: default: - w.rgba = this._colors.background.rgba; - w.isDefault = true; + $rgba = this._colors.background.rgba; + $isDefault = true; } } if (vertices.attributes.length < offset + 4) { vertices.attributes = expandFloat32Array(vertices.attributes, this._terminal.rows * this._terminal.cols * INDICES_PER_RECTANGLE); } - w.x1 = startX * this._dimensions.scaledCellWidth; - w.y1 = y * this._dimensions.scaledCellHeight; - w.r = ((w.rgba >> 24) & 0xFF) / 255; - w.g = ((w.rgba >> 16) & 0xFF) / 255; - w.b = ((w.rgba >> 8 ) & 0xFF) / 255; - w.a = (!w.isDefault && bg & BgFlags.DIM) ? DIM_OPACITY : 1; + $x1 = startX * this._dimensions.scaledCellWidth; + $y1 = y * this._dimensions.scaledCellHeight; + $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, w.x1, w.y1, (endX - startX) * this._dimensions.scaledCellWidth, this._dimensions.scaledCellHeight, w.r, w.g, w.b, w.a); + this._addRectangle(vertices.attributes, offset, $x1, $y1, (endX - startX) * this._dimensions.scaledCellWidth, this._dimensions.scaledCellHeight, $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 { diff --git a/addons/xterm-addon-webgl/src/RenderModel.ts b/addons/xterm-addon-webgl/src/RenderModel.ts index 2969a6d17..b1542d984 100644 --- a/addons/xterm-addon-webgl/src/RenderModel.ts +++ b/addons/xterm-addon-webgl/src/RenderModel.ts @@ -3,8 +3,10 @@ * @license MIT */ -import { IRenderModel, ISelectionRenderModel } from './Types'; +import { IRenderModel } from './Types'; import { fill } from 'common/TypedArrayUtils'; +import { ISelectionRenderModel } from 'browser/renderer/shared/Types'; +import { createSelectionRenderModel } from 'browser/renderer/shared/SelectionRenderModel'; export const RENDER_MODEL_INDICIES_PER_CELL = 4; export const RENDER_MODEL_BG_OFFSET = 1; @@ -21,16 +23,7 @@ export class RenderModel implements IRenderModel { constructor() { this.cells = new Uint32Array(0); this.lineLengths = new Uint32Array(0); - this.selection = { - hasSelection: false, - columnSelectMode: false, - viewportStartRow: 0, - viewportEndRow: 0, - viewportCappedStartRow: 0, - viewportCappedEndRow: 0, - startCol: 0, - endCol: 0 - }; + this.selection = createSelectionRenderModel(); } public resize(cols: number, rows: number): void { @@ -45,14 +38,4 @@ export class RenderModel implements IRenderModel { fill(this.cells, 0, 0); fill(this.lineLengths, 0, 0); } - - public clearSelection(): void { - this.selection.hasSelection = false; - this.selection.viewportStartRow = 0; - this.selection.viewportEndRow = 0; - this.selection.viewportCappedStartRow = 0; - this.selection.viewportCappedEndRow = 0; - this.selection.startCol = 0; - this.selection.endCol = 0; - } } diff --git a/addons/xterm-addon-webgl/src/Types.d.ts b/addons/xterm-addon-webgl/src/Types.d.ts index bcfa11c8d..6e3a18fc5 100644 --- a/addons/xterm-addon-webgl/src/Types.d.ts +++ b/addons/xterm-addon-webgl/src/Types.d.ts @@ -3,46 +3,7 @@ * @license MIT */ -/** - * Represents a rasterized glyph within a texture atlas. Some numbers are - * tracked in CSS pixels as well in order to reduce calculations during the - * render loop. - */ -export interface IRasterizedGlyph { - /** - * The x and y offset between the glyph's top/left and the top/left of a cell - * in pixels. - */ - offset: IVector; - /** - * the x and y position of the glyph in the texture in pixels. - */ - texturePosition: IVector; - /** - * the x and y position of the glyph in the texture in clip space coordinates. - */ - texturePositionClipSpace: IVector; - /** - * The width and height of the glyph in the texture in pixels. - */ - size: IVector; - /** - * The width and height of the glyph in the texture in clip space coordinates. - */ - sizeClipSpace: IVector; -} - -export interface IVector { - x: number; - y: number; -} - -export interface IBoundingBox { - top: number; - left: number; - right: number; - bottom: number; -} +import { ISelectionRenderModel } from 'browser/renderer/shared/Types'; export interface IRenderModel { cells: Uint32Array; @@ -50,17 +11,6 @@ export interface IRenderModel { selection: ISelectionRenderModel; } -export interface ISelectionRenderModel { - hasSelection: boolean; - columnSelectMode: boolean; - viewportStartRow: number; - viewportEndRow: number; - viewportCappedStartRow: number; - viewportCappedEndRow: number; - startCol: number; - endCol: number; -} - export interface IWebGL2RenderingContext extends WebGLRenderingContext { vertexAttribDivisor(index: number, divisor: number): void; createVertexArray(): IWebGLVertexArrayObject; diff --git a/addons/xterm-addon-webgl/src/WebglAddon.ts b/addons/xterm-addon-webgl/src/WebglAddon.ts index e8fa87998..ac2d2cf4c 100644 --- a/addons/xterm-addon-webgl/src/WebglAddon.ts +++ b/addons/xterm-addon-webgl/src/WebglAddon.ts @@ -15,10 +15,10 @@ export class WebglAddon implements ITerminalAddon { private _terminal?: Terminal; private _renderer?: WebglRenderer; - private _onChangeTextureAtlas = new EventEmitter(); - public get onChangeTextureAtlas(): IEvent { return this._onChangeTextureAtlas.event; } - private _onContextLoss = new EventEmitter(); - public get onContextLoss(): IEvent { return this._onContextLoss.event; } + private readonly _onChangeTextureAtlas = new EventEmitter(); + public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event; + private readonly _onContextLoss = new EventEmitter(); + public readonly onContextLoss = this._onContextLoss.event; constructor( private _preserveDrawingBuffer?: boolean diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index d8daa0d18..b22f3d5e1 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -3,46 +3,37 @@ * @license MIT */ -import { GlyphRenderer } from './GlyphRenderer'; -import { LinkRenderLayer } from './renderLayer/LinkRenderLayer'; -import { CursorRenderLayer } from './renderLayer/CursorRenderLayer'; -import { acquireCharAtlas, removeTerminalFromCache } from './atlas/CharAtlasCache'; -import { WebglCharAtlas } from './atlas/WebglCharAtlas'; -import { RectangleRenderer } from './RectangleRenderer'; -import { IWebGL2RenderingContext } from './Types'; -import { RenderModel, COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_EXT_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel'; -import { Disposable } from 'common/Lifecycle'; -import { Attributes, BgFlags, Content, FgFlags, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants'; -import { Terminal, IEvent } from 'xterm'; -import { IRenderLayer } from './renderLayer/Types'; -import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types'; -import { observeDevicePixelDimensions } from 'browser/renderer/DevicePixelObserver'; -import { ITerminal, IColorSet } from 'browser/Types'; -import { EventEmitter } from 'common/EventEmitter'; -import { CellData } from 'common/buffer/CellData'; 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 { IRenderDimensions, IRenderer, IRequestRedrawEvent, ITextureAtlas } from 'browser/renderer/shared/Types'; import { ICharacterJoinerService, ICoreBrowserService } from 'browser/services/Services'; -import { CharData, IBufferLine, ICellData } from 'common/Types'; +import { IColorSet, 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 } from 'common/Lifecycle'; import { ICoreService, IDecorationService } from 'common/services/Services'; - -/** Work variables to avoid garbage collection. */ -const w: { fg: number, bg: number, hasFg: boolean, hasBg: boolean, isSelected: boolean } = { - fg: 0, - bg: 0, - hasFg: false, - hasBg: false, - isSelected: false -}; +import { CharData, IBufferLine, ICellData } from 'common/Types'; +import { Terminal } from 'xterm'; +import { GlyphRenderer } from './GlyphRenderer'; +import { RectangleRenderer } from './RectangleRenderer'; +import { CursorRenderLayer } from './renderLayer/CursorRenderLayer'; +import { LinkRenderLayer } from './renderLayer/LinkRenderLayer'; +import { IRenderLayer } from './renderLayer/Types'; +import { COMBINED_CHAR_BIT_MASK, RenderModel, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_EXT_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel'; +import { IWebGL2RenderingContext } from './Types'; export class WebglRenderer extends Disposable implements IRenderer { private _renderLayers: IRenderLayer[]; - private _charAtlas: WebglCharAtlas | undefined; + private _charAtlas: ITextureAtlas | undefined; private _devicePixelRatio: number; private _model: RenderModel = new RenderModel(); private _workCell: CellData = new CellData(); - private _workColors: { fg: number, bg: number, ext: number } = { fg: 0, bg: 0, ext: 0 }; + private _cellColorResolver: CellColorResolver; private _canvas: HTMLCanvasElement; private _gl: IWebGL2RenderingContext; @@ -55,13 +46,12 @@ export class WebglRenderer extends Disposable implements IRenderer { private _isAttached: boolean; private _contextRestorationTimeout: number | undefined; - private _onChangeTextureAtlas = new EventEmitter(); - public get onChangeTextureAtlas(): IEvent { return this._onChangeTextureAtlas.event; } - private _onRequestRedraw = new EventEmitter(); - public get onRequestRedraw(): IEvent { return this._onRequestRedraw.event; } - - private _onContextLoss = new EventEmitter(); - public get onContextLoss(): IEvent { return this._onContextLoss.event; } + private readonly _onChangeTextureAtlas = new EventEmitter(); + public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event; + private readonly _onRequestRedraw = new EventEmitter(); + public readonly onRequestRedraw = this._onRequestRedraw.event; + private readonly _onContextLoss = new EventEmitter(); + public readonly onContextLoss = this._onContextLoss.event; constructor( private _terminal: Terminal, @@ -74,6 +64,8 @@ export class WebglRenderer extends Disposable implements IRenderer { ) { super(); + this._cellColorResolver = new CellColorResolver(this._terminal, this._colors, this._model.selection, this._decorationService, this._coreBrowserService); + this._core = (this._terminal as any)._core; this._renderLayers = [ @@ -162,6 +154,7 @@ export class WebglRenderer extends Disposable implements IRenderer { l.reset(this._terminal); } + this._cellColorResolver.setColors(colors); this._rectangleRenderer.setColors(); this._refreshCharAtlas(); @@ -236,7 +229,7 @@ export class WebglRenderer extends Disposable implements IRenderer { for (const l of this._renderLayers) { l.onSelectionChanged(this._terminal, start, end, columnSelectMode); } - this._updateSelectionModel(start, end, columnSelectMode); + this._model.selection.update(this._terminal, start, end, columnSelectMode); this._requestRedrawViewport(); } @@ -281,10 +274,7 @@ export class WebglRenderer extends Disposable implements IRenderer { return; } - const atlas = acquireCharAtlas(this._terminal, this._colors, this.dimensions.scaledCellWidth, this.dimensions.scaledCellHeight, this.dimensions.scaledCharWidth, this.dimensions.scaledCharHeight, this._coreBrowserService.dpr); - if (!('getRasterizedGlyph' in atlas)) { - throw new Error('The webgl renderer only works with the webgl char atlas'); - } + const atlas = acquireTextureAtlas(this._terminal, this._colors, this.dimensions.scaledCellWidth, this.dimensions.scaledCellHeight, this.dimensions.scaledCharWidth, this.dimensions.scaledCharHeight, this._coreBrowserService.dpr); if (this._charAtlas !== atlas) { this._onChangeTextureAtlas.fire(atlas.cacheCanvas); } @@ -346,7 +336,7 @@ export class WebglRenderer extends Disposable implements IRenderer { // Tell renderer the frame is beginning if (this._glyphRenderer.beginFrame()) { this._clearModel(true); - this._updateSelectionModel(undefined, undefined); + this._model.selection.clear(); } // Update model to reflect what's drawn @@ -382,11 +372,11 @@ export class WebglRenderer extends Disposable implements IRenderer { this._model.lineLengths[y] = 0; joinedRanges = this._characterJoinerService.getJoinedCharacters(row); for (x = 0; x < terminal.cols; x++) { - lastBg = this._workColors.bg; + lastBg = this._cellColorResolver.result.bg; line.loadCell(x, cell); if (x === 0) { - lastBg = this._workColors.bg; + lastBg = this._cellColorResolver.result.bg; } // If true, indicates that the current character(s) to draw were joined. @@ -417,7 +407,7 @@ export class WebglRenderer extends Disposable implements IRenderer { i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL; // Load colors/resolve overrides into work colors - this._loadColorsForCell(x, row); + this._cellColorResolver.resolve(cell, x, row); if (code !== NULL_CELL_CODE) { this._model.lineLengths[y] = x + 1; @@ -425,9 +415,9 @@ export class WebglRenderer extends Disposable implements IRenderer { // Nothing has changed, no updates needed if (this._model.cells[i] === code && - this._model.cells[i + RENDER_MODEL_BG_OFFSET] === this._workColors.bg && - this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._workColors.fg && - this._model.cells[i + RENDER_MODEL_EXT_OFFSET] === this._workColors.ext) { + this._model.cells[i + RENDER_MODEL_BG_OFFSET] === this._cellColorResolver.result.bg && + this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._cellColorResolver.result.fg && + this._model.cells[i + RENDER_MODEL_EXT_OFFSET] === this._cellColorResolver.result.ext) { continue; } @@ -438,11 +428,11 @@ export class WebglRenderer extends Disposable implements IRenderer { // Cache the results in the model this._model.cells[i] = code; - this._model.cells[i + RENDER_MODEL_BG_OFFSET] = this._workColors.bg; - this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._workColors.fg; - this._model.cells[i + RENDER_MODEL_EXT_OFFSET] = this._workColors.ext; + this._model.cells[i + RENDER_MODEL_BG_OFFSET] = this._cellColorResolver.result.bg; + this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg; + this._model.cells[i + RENDER_MODEL_EXT_OFFSET] = this._cellColorResolver.result.ext; - this._glyphRenderer.updateCell(x, y, code, this._workColors.bg, this._workColors.fg, this._workColors.ext, chars, lastBg); + this._glyphRenderer.updateCell(x, y, code, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, chars, lastBg); if (isJoined) { // Restore work cell @@ -453,9 +443,9 @@ export class WebglRenderer extends Disposable implements IRenderer { j = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL; this._glyphRenderer.updateCell(x, y, NULL_CELL_CODE, 0, 0, 0, NULL_CELL_CHAR, 0); this._model.cells[j] = NULL_CELL_CODE; - this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._workColors.bg; - this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._workColors.fg; - this._model.cells[j + RENDER_MODEL_EXT_OFFSET] = this._workColors.ext; + this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._cellColorResolver.result.bg; + this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg; + this._model.cells[j + RENDER_MODEL_EXT_OFFSET] = this._cellColorResolver.result.ext; } } } @@ -463,153 +453,6 @@ export class WebglRenderer extends Disposable implements IRenderer { this._rectangleRenderer.updateBackgrounds(this._model); } - /** - * Loads colors for the cell into the work colors object. This resolves overrides/inverse if - * necessary which is why the work cell object is not used. - */ - private _loadColorsForCell(x: number, y: number): void { - this._workColors.bg = this._workCell.bg; - this._workColors.fg = this._workCell.fg; - this._workColors.ext = this._workCell.bg & BgFlags.HAS_EXTENDED ? this._workCell.extended.ext : 0; - // Get any foreground/background overrides, this happens on the model to avoid spreading - // override logic throughout the different sub-renderers - - // Reset overrides work variables - w.bg = 0; - w.fg = 0; - w.hasBg = false; - w.hasFg = false; - w.isSelected = false; - - // Apply decorations on the bottom layer - this._decorationService.forEachDecorationAtCell(x, y, 'bottom', d => { - if (d.backgroundColorRGB) { - w.bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF; - w.hasBg = true; - } - if (d.foregroundColorRGB) { - w.fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF; - w.hasFg = true; - } - }); - - // Apply the selection color if needed - w.isSelected = this._isCellSelected(x, y); - if (w.isSelected) { - w.bg = (this._coreBrowserService.isFocused ? this._colors.selectionBackgroundOpaque : this._colors.selectionInactiveBackgroundOpaque).rgba >> 8 & 0xFFFFFF; - w.hasBg = true; - if (this._colors.selectionForeground) { - w.fg = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF; - w.hasFg = true; - } - } - - // Apply decorations on the top layer - this._decorationService.forEachDecorationAtCell(x, y, 'top', d => { - if (d.backgroundColorRGB) { - w.bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF; - w.hasBg = true; - } - if (d.foregroundColorRGB) { - w.fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF; - w.hasFg = true; - } - }); - - // Convert any overrides from rgba to the fg/bg packed format. This resolves the inverse flag - // ahead of time in order to use the correct cache key - if (w.hasBg) { - if (w.isSelected) { - // Non-RGB attributes from model + force non-dim + override + force RGB color mode - w.bg = (this._workCell.bg & ~Attributes.RGB_MASK & ~BgFlags.DIM) | w.bg | Attributes.CM_RGB; - } else { - // Non-RGB attributes from model + override + force RGB color mode - w.bg = (this._workCell.bg & ~Attributes.RGB_MASK) | w.bg | Attributes.CM_RGB; - } - } - if (w.hasFg) { - // Non-RGB attributes from model + force disable inverse + override + force RGB color mode - w.fg = (this._workCell.fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | w.fg | Attributes.CM_RGB; - } - - // Handle case where inverse was specified by only one of bg override or fg override was set, - // resolving the other inverse color and setting the inverse flag if needed. - if (this._workColors.fg & FgFlags.INVERSE) { - if (w.hasBg && !w.hasFg) { - // Resolve bg color type (default color has a different meaning in fg vs bg) - if ((this._workColors.bg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) { - w.fg = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | ((this._colors.background.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB; - } else { - w.fg = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | this._workColors.bg & (Attributes.RGB_MASK | Attributes.CM_MASK); - } - w.hasFg = true; - } - if (!w.hasBg && w.hasFg) { - // Resolve bg color type (default color has a different meaning in fg vs bg) - if ((this._workColors.fg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) { - w.bg = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | ((this._colors.foreground.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB; - } else { - w.bg = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | this._workColors.fg & (Attributes.RGB_MASK | Attributes.CM_MASK); - } - w.hasBg = true; - } - } - - // Use the override if it exists - this._workColors.bg = w.hasBg ? w.bg : this._workColors.bg; - this._workColors.fg = w.hasFg ? w.fg : this._workColors.fg; - } - - private _isCellSelected(x: number, y: number): boolean { - if (!this._model.selection.hasSelection) { - return false; - } - y -= this._terminal.buffer.active.viewportY; - if (this._model.selection.columnSelectMode) { - if (this._model.selection.startCol <= this._model.selection.endCol) { - return x >= this._model.selection.startCol && y >= this._model.selection.viewportCappedStartRow && - x < this._model.selection.endCol && y <= this._model.selection.viewportCappedEndRow; - } - return x < this._model.selection.startCol && y >= this._model.selection.viewportCappedStartRow && - x >= this._model.selection.endCol && y <= this._model.selection.viewportCappedEndRow; - } - return (y > this._model.selection.viewportStartRow && y < this._model.selection.viewportEndRow) || - (this._model.selection.viewportStartRow === this._model.selection.viewportEndRow && y === this._model.selection.viewportStartRow && x >= this._model.selection.startCol && x < this._model.selection.endCol) || - (this._model.selection.viewportStartRow < this._model.selection.viewportEndRow && y === this._model.selection.viewportEndRow && x < this._model.selection.endCol) || - (this._model.selection.viewportStartRow < this._model.selection.viewportEndRow && y === this._model.selection.viewportStartRow && x >= this._model.selection.startCol); - } - - private _updateSelectionModel(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void { - const terminal = this._terminal; - - // Selection does not exist - if (!start || !end || (start[0] === end[0] && start[1] === end[1])) { - this._model.clearSelection(); - return; - } - - // Translate from buffer position to viewport position - const viewportStartRow = start[1] - terminal.buffer.active.viewportY; - const viewportEndRow = end[1] - terminal.buffer.active.viewportY; - const viewportCappedStartRow = Math.max(viewportStartRow, 0); - const viewportCappedEndRow = Math.min(viewportEndRow, terminal.rows - 1); - - // No need to draw the selection - if (viewportCappedStartRow >= terminal.rows || viewportCappedEndRow < 0) { - this._model.clearSelection(); - return; - } - - this._model.selection.hasSelection = true; - this._model.selection.columnSelectMode = columnSelectMode; - this._model.selection.viewportStartRow = viewportStartRow; - this._model.selection.viewportEndRow = viewportEndRow; - this._model.selection.viewportCappedStartRow = viewportCappedStartRow; - this._model.selection.viewportCappedEndRow = viewportCappedEndRow; - this._model.selection.startCol = start[0]; - this._model.selection.endCol = end[0]; - } - /** * Recalculates the character and canvas dimensions. */ diff --git a/addons/xterm-addon-webgl/src/WebglUtils.ts b/addons/xterm-addon-webgl/src/WebglUtils.ts index 841ad067b..51a273777 100644 --- a/addons/xterm-addon-webgl/src/WebglUtils.ts +++ b/addons/xterm-addon-webgl/src/WebglUtils.ts @@ -3,6 +3,8 @@ * @license MIT */ +import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; + /** * A matrix that when multiplies will translate 0-1 coordinates (left to right, * top to bottom) to clip space. @@ -49,10 +51,3 @@ export function expandFloat32Array(source: Float32Array, max: number): Float32Ar } return newArray; } - -export function throwIfFalsy(value: T | undefined | null): T { - if (!value) { - throw new Error('value must not be falsy'); - } - return value; -} diff --git a/addons/xterm-addon-webgl/src/atlas/Types.d.ts b/addons/xterm-addon-webgl/src/atlas/Types.d.ts deleted file mode 100644 index 8d2870cde..000000000 --- a/addons/xterm-addon-webgl/src/atlas/Types.d.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { FontWeight } from 'xterm'; -import { IColorSet } from 'browser/Types'; - -export interface IGlyphIdentifier { - chars: string; - code: number; - bg: number; - fg: number; - bold: boolean; - dim: boolean; - italic: boolean; -} - -export interface ICharAtlasConfig { - customGlyphs: boolean; - devicePixelRatio: number; - letterSpacing: number; - lineHeight: number; - fontSize: number; - fontFamily: string; - fontWeight: FontWeight; - fontWeightBold: FontWeight; - scaledCellWidth: number; - scaledCellHeight: number; - scaledCharWidth: number; - scaledCharHeight: number; - allowTransparency: boolean; - drawBoldTextInBrightColors: boolean; - minimumContrastRatio: number; - colors: IColorSet; -} diff --git a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts index e9c0cf413..07eca0bfb 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts @@ -4,15 +4,14 @@ */ import { IRenderLayer } from './Types'; -import { acquireCharAtlas } from '../atlas/CharAtlasCache'; +import { acquireTextureAtlas } from 'browser/renderer/shared/CharAtlasCache'; import { Terminal } from 'xterm'; import { IColorSet } from 'browser/Types'; -import { TEXT_BASELINE } from 'browser/renderer/Constants'; +import { TEXT_BASELINE } from 'browser/renderer/shared/Constants'; import { ICoreBrowserService } from 'browser/services/Services'; -import { IRenderDimensions } from 'browser/renderer/Types'; +import { IRenderDimensions, ITextureAtlas } from 'browser/renderer/shared/Types'; import { CellData } from 'common/buffer/CellData'; -import { WebglCharAtlas } from 'atlas/WebglCharAtlas'; -import { throwIfFalsy } from '../WebglUtils'; +import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; export abstract class BaseRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; @@ -24,7 +23,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { private _scaledCharLeft: number = 0; private _scaledCharTop: number = 0; - protected _charAtlas: WebglCharAtlas | undefined; + protected _charAtlas: ITextureAtlas | undefined; constructor( private _container: HTMLElement, @@ -95,7 +94,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) { return; } - this._charAtlas = acquireCharAtlas(terminal, colorSet, this._scaledCellWidth, this._scaledCellHeight, this._scaledCharWidth, this._scaledCharHeight, this._coreBrowserService.dpr); + this._charAtlas = acquireTextureAtlas(terminal, colorSet, this._scaledCellWidth, this._scaledCellHeight, this._scaledCharWidth, this._scaledCharHeight, this._coreBrowserService.dpr); this._charAtlas.warmUp(); } diff --git a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts index 2b274516d..522f6fb62 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts @@ -8,7 +8,7 @@ import { BaseRenderLayer } from './BaseRenderLayer'; import { ICellData } from 'common/Types'; import { CellData } from 'common/buffer/CellData'; import { IColorSet } from 'browser/Types'; -import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types'; +import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/shared/Types'; import { IEventEmitter } from 'common/EventEmitter'; import { ICoreBrowserService } from 'browser/services/Services'; import { ICoreService } from 'common/services/Services'; diff --git a/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts index e1ff08d81..dbf2fdcff 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts @@ -5,11 +5,11 @@ import { Terminal } from 'xterm'; import { BaseRenderLayer } from './BaseRenderLayer'; -import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants'; -import { is256Color } from '../atlas/CharAtlasUtils'; +import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants'; import { ITerminal, IColorSet, ILinkifierEvent } from 'browser/Types'; -import { IRenderDimensions } from 'browser/renderer/Types'; +import { IRenderDimensions } from 'browser/renderer/shared/Types'; import { ICoreBrowserService } from 'browser/services/Services'; +import { is256Color } from 'browser/renderer/shared/CharAtlasUtils'; export class LinkRenderLayer extends BaseRenderLayer { private _state: ILinkifierEvent | undefined; diff --git a/addons/xterm-addon-webgl/src/renderLayer/Types.ts b/addons/xterm-addon-webgl/src/renderLayer/Types.ts index 70acff340..993304a2f 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/Types.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/Types.ts @@ -5,7 +5,7 @@ import { IDisposable, Terminal } from 'xterm'; import { IColorSet } from 'browser/Types'; -import { IRenderDimensions } from 'browser/renderer/Types'; +import { IRenderDimensions } from 'browser/renderer/shared/Types'; export interface IRenderLayer extends IDisposable { /** 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 74aed0cc7..6865b6dbe 100644 --- a/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts +++ b/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts @@ -15,12 +15,12 @@ declare module 'xterm-addon-webgl' { /** * An event that is fired when the renderer loses its canvas context. */ - public get onContextLoss(): IEvent; + public readonly onContextLoss: IEvent; /** * An event that is fired when the texture atlas of the renderer changes. */ - public get onChangeTextureAtlas(): IEvent; + public readonly onChangeTextureAtlas: IEvent; constructor(preserveDrawingBuffer?: boolean); diff --git a/demo/client.ts b/demo/client.ts index 31a3347cd..ea55d6d93 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -36,15 +36,15 @@ import { Terminal as TerminalType, ITerminalOptions } from 'xterm'; export interface IWindowWithTerminal extends Window { term: TerminalType; - Terminal?: typeof TerminalType; - AttachAddon?: typeof AttachAddon; - FitAddon?: typeof FitAddon; - SearchAddon?: typeof SearchAddon; - SerializeAddon?: typeof SerializeAddon; - WebLinksAddon?: typeof WebLinksAddon; - WebglAddon?: typeof WebglAddon; - Unicode11Addon?: typeof Unicode11Addon; - LigaturesAddon?: typeof LigaturesAddon; + Terminal?: typeof TerminalType; // eslint-disable-line @typescript-eslint/naming-convention + AttachAddon?: typeof AttachAddon; // eslint-disable-line @typescript-eslint/naming-convention + FitAddon?: typeof FitAddon; // eslint-disable-line @typescript-eslint/naming-convention + SearchAddon?: typeof SearchAddon; // eslint-disable-line @typescript-eslint/naming-convention + SerializeAddon?: typeof SerializeAddon; // eslint-disable-line @typescript-eslint/naming-convention + WebLinksAddon?: typeof WebLinksAddon; // eslint-disable-line @typescript-eslint/naming-convention + WebglAddon?: typeof WebglAddon; // eslint-disable-line @typescript-eslint/naming-convention + Unicode11Addon?: typeof Unicode11Addon; // eslint-disable-line @typescript-eslint/naming-convention + LigaturesAddon?: typeof LigaturesAddon; // eslint-disable-line @typescript-eslint/naming-convention } declare let window: IWindowWithTerminal; @@ -59,27 +59,29 @@ type AddonType = 'attach' | 'canvas' | 'fit' | 'search' | 'serialize' | 'unicode interface IDemoAddon { name: T; canChange: boolean; - ctor: + ctor: ( T extends 'attach' ? typeof AttachAddon : - T extends 'canvas' ? typeof CanvasAddon : - T extends 'fit' ? typeof FitAddon : - T extends 'search' ? typeof SearchAddon : - T extends 'serialize' ? typeof SerializeAddon : - T extends 'web-links' ? typeof WebLinksAddon : - T extends 'unicode11' ? typeof Unicode11Addon : - T extends 'ligatures' ? typeof LigaturesAddon : - typeof WebglAddon; - instance?: + T extends 'canvas' ? typeof CanvasAddon : + T extends 'fit' ? typeof FitAddon : + T extends 'search' ? typeof SearchAddon : + T extends 'serialize' ? typeof SerializeAddon : + T extends 'web-links' ? typeof WebLinksAddon : + T extends 'unicode11' ? typeof Unicode11Addon : + T extends 'ligatures' ? typeof LigaturesAddon : + typeof WebglAddon + ); + instance?: ( T extends 'attach' ? AttachAddon : - T extends 'canvas' ? CanvasAddon : - T extends 'fit' ? FitAddon : - T extends 'search' ? SearchAddon : - T extends 'serialize' ? SerializeAddon : - T extends 'web-links' ? WebLinksAddon : - T extends 'webgl' ? WebglAddon : - T extends 'unicode11' ? typeof Unicode11Addon : - T extends 'ligatures' ? typeof LigaturesAddon : - never; + T extends 'canvas' ? CanvasAddon : + T extends 'fit' ? FitAddon : + T extends 'search' ? SearchAddon : + T extends 'serialize' ? SerializeAddon : + T extends 'web-links' ? WebLinksAddon : + T extends 'webgl' ? WebglAddon : + T extends 'unicode11' ? typeof Unicode11Addon : + T extends 'ligatures' ? typeof LigaturesAddon : + never + ); } const addons: { [T in AddonType]: IDemoAddon } = { @@ -96,12 +98,12 @@ const addons: { [T in AddonType]: IDemoAddon } = { let terminalContainer = document.getElementById('terminal-container'); const actionElements = { - find: document.querySelector('#find'), - findNext: document.querySelector('#find-next'), - findPrevious: document.querySelector('#find-previous'), + find: document.querySelector('#find') as HTMLInputElement, + findNext: document.querySelector('#find-next') as HTMLInputElement, + findPrevious: document.querySelector('#find-previous') as HTMLInputElement, findResults: document.querySelector('#find-results') }; -const paddingElement = document.getElementById('padding'); +const paddingElement = document.getElementById('padding') as HTMLInputElement; const xtermjsTheme = { foreground: '#F8F8F8', @@ -146,7 +148,7 @@ function getSearchOptions(e: KeyboardEvent): ISearchOptions { }; } -const disposeRecreateButtonHandler = () => { +const disposeRecreateButtonHandler: () => void = () => { // If the terminal exists dispose of it, otherwise recreate it if (term) { term.dispose(); @@ -169,7 +171,7 @@ const disposeRecreateButtonHandler = () => { } }; -const createNewWindowButtonHandler = () => { +const createNewWindowButtonHandler: () => void = () => { if (term) { disposeRecreateButtonHandler(); } @@ -196,7 +198,7 @@ const createNewWindowButtonHandler = () => { } }); } -} +}; if (document.location.pathname === '/test') { window.Terminal = Terminal; @@ -220,6 +222,7 @@ if (document.location.pathname === '/test') { document.getElementById('underline-test').addEventListener('click', underlineTest); document.getElementById('ansi-colors').addEventListener('click', ansiColorsTest); document.getElementById('osc-hyperlinks').addEventListener('click', addAnsiHyperlink); + document.getElementById('sgr-test').addEventListener('click', sgrTest); document.getElementById('add-decoration').addEventListener('click', addDecoration); document.getElementById('add-overview-ruler').addEventListener('click', addOverviewRuler); } @@ -296,8 +299,8 @@ function createTerminal(): void { setTimeout(() => { initOptions(term); // TODO: Clean this up, opt-cols/rows doesn't exist anymore - (document.getElementById(`opt-cols`)).value = term.cols; - (document.getElementById(`opt-rows`)).value = term.rows; + (document.getElementById(`opt-cols`) as HTMLInputElement).value = term.cols; + (document.getElementById(`opt-rows`) as HTMLInputElement).value = term.rows; paddingElement.value = '0'; // Set terminal size again to set the specific dimensions on the demo @@ -423,14 +426,14 @@ function initOptions(term: TerminalType): void { // Attach listeners booleanOptions.forEach(o => { - const input = document.getElementById(`opt-${o}`); + const input = document.getElementById(`opt-${o}`) as HTMLInputElement; addDomListener(input, 'change', () => { console.log('change', o, input.checked); term.options[o] = input.checked; }); }); numberOptions.forEach(o => { - const input = document.getElementById(`opt-${o}`); + const input = document.getElementById(`opt-${o}`) as HTMLInputElement; addDomListener(input, 'change', () => { console.log('change', o, input.value); if (o === 'rows') { @@ -452,7 +455,7 @@ function initOptions(term: TerminalType): void { }); }); Object.keys(stringOptions).forEach(o => { - const input = document.getElementById(`opt-${o}`); + const input = document.getElementById(`opt-${o}`) as HTMLInputElement; addDomListener(input, 'change', () => { console.log('change', o, input.value); let value: any = input.value; @@ -485,7 +488,7 @@ function initOptions(term: TerminalType): void { magenta: '#b168df', red: '#da6771', white: '#efefef', - yellow: '#fff099', + yellow: '#fff099' }; break; case 'light': @@ -509,7 +512,7 @@ function initOptions(term: TerminalType): void { magenta: '#bc05bc', red: '#cd3131', white: '#555555', - yellow: '#949800', + yellow: '#949800' }; break; } @@ -541,7 +544,15 @@ function initAddons(term: TerminalType): void { try { term.loadAddon(addon.instance); if (name === 'webgl') { - (addon.instance as WebglAddon).onChangeTextureAtlas(e => addTextureAtlas(e)); + setTimeout(() => { + addTextureAtlas(addons.webgl.instance.textureAtlas); + addons.webgl.instance.onChangeTextureAtlas(e => addTextureAtlas(e)); + }, 0); + } else if (name === 'canvas') { + setTimeout(() => { + addTextureAtlas(addons.canvas.instance.textureAtlas); + addons.canvas.instance.onChangeTextureAtlas(e => addTextureAtlas(e)); + }, 0); } else if (name === 'unicode11') { term.unicode.activeVersion = '11'; } else if (name === 'search') { @@ -555,7 +566,9 @@ function initAddons(term: TerminalType): void { } } else { if (name === 'webgl') { - (addon.instance as WebglAddon).textureAtlas.remove(); + addons.webgl.instance.textureAtlas.remove(); + } else if (name === 'canvas') { + addons.canvas.instance.textureAtlas.remove(); } else if (name === 'unicode11') { term.unicode.activeVersion = '6'; } @@ -580,7 +593,7 @@ function initAddons(term: TerminalType): void { container.appendChild(fragment); } -function updateFindResults(e: { resultIndex: number, resultCount: number } | undefined) { +function updateFindResults(e: { resultIndex: number, resultCount: number } | undefined): void { let content: string; if (e === undefined) { content = 'undefined'; @@ -596,8 +609,8 @@ function addDomListener(element: HTMLElement, type: string, handler: (...args: a } function updateTerminalSize(): void { - const cols = parseInt((document.getElementById(`opt-cols`)).value, 10); - const rows = parseInt((document.getElementById(`opt-rows`)).value, 10); + 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'; terminalContainer.style.width = width; @@ -621,21 +634,21 @@ function htmlSerializeButtonHandler(): void { document.getElementById('htmlserialize-output').innerText = output; // Deprecated, but the most supported for now. - function listener(e: any) { - e.clipboardData.setData("text/html", output); + function listener(e: any): void { + e.clipboardData.setData('text/html', output); e.preventDefault(); } - document.addEventListener("copy", listener); - document.execCommand("copy"); - document.removeEventListener("copy", listener); - document.getElementById("htmlserialize-output-result").innerText = "Copied to clipboard"; + document.addEventListener('copy', listener); + document.execCommand('copy'); + document.removeEventListener('copy', listener); + document.getElementById('htmlserialize-output-result').innerText = 'Copied to clipboard'; } -function addTextureAtlas(e: HTMLCanvasElement) { +function addTextureAtlas(e: HTMLCanvasElement): void { document.querySelector('#texture-atlas').replaceChildren(e); } -function writeCustomGlyphHandler() { +function writeCustomGlyphHandler(): void { term.write('\n\r'); term.write('\n\r'); term.write('Box styles: ┎┰┒┍┯┑╓╥╖╒╤╕ ┏┳┓┌┲┓┌┬┐┏┱┐\n\r'); @@ -680,7 +693,7 @@ function writeCustomGlyphHandler() { window.scrollTo(0, 0); } -function loadTest() { +function loadTest(): void { const rendererName = addons.webgl.instance ? 'webgl' : !!addons.canvas.instance ? 'canvas' : 'dom'; const testData = []; let byteCount = 0; @@ -713,7 +726,7 @@ function loadTest() { }); } -function powerlineSymbolTest() { +function powerlineSymbolTest(): void { function s(char: string): string { return `${char} \x1b[7m${char}\x1b[0m `; } @@ -787,7 +800,7 @@ function powerlineSymbolTest() { term.writeln('nf-mdi-github_face (\\uFbd9) \ufbd9'); } -function underlineTest() { +function underlineTest(): void { function u(style: number): string { return `\x1b[4:${style}m`; } @@ -797,7 +810,7 @@ function underlineTest() { term.write('\n\n\r'); term.writeln('Underline styles:'); term.writeln(''); - function showSequence(id: number, name: string) { + function showSequence(id: number, name: string): string { let alphabet = ''; for (let i = 97; i < 123; i++) { alphabet += String.fromCharCode(i); @@ -844,7 +857,7 @@ function underlineTest() { term.write('\x1b[0m\n\r'); } -function ansiColorsTest() { +function ansiColorsTest(): void { term.writeln(`\x1b[0m\n\n\rStandard colors: Bright colors:`); for (let i = 0; i < 16; i++) { term.write(`\x1b[48;5;${i}m ${i.toString().padEnd(2, ' ').padStart(3, ' ')} \x1b[0m`); @@ -869,7 +882,66 @@ function ansiColorsTest() { } } -function addAnsiHyperlink() { +function writeTestString(): string { + let alphabet = ''; + for (let i = 97; i < 123; i++) { + alphabet += String.fromCharCode(i); + } + let numbers = ''; + for (let i = 0; i < 10; i++) { + numbers += i.toString(); + } + return `${alphabet} ${numbers} 汉语 한국어 👽`; +} +const testString = writeTestString(); + +function sgrTest(): void { + term.write('\n\n\r'); + term.writeln(`Character Attributes (SGR, Select Graphic Rendition)`); + const entries: { ps: number, name: string }[] = [ + { ps: 0, name: 'Normal' }, + { ps: 1, name: 'Bold' }, + { ps: 2, name: 'Faint/dim' }, + { ps: 3, name: 'Italicized' }, + { ps: 4, name: 'Underlined' }, + { ps: 5, name: 'Blink' }, + { ps: 7, name: 'Inverse' }, + { ps: 8, name: 'Invisible' }, + { ps: 9, name: 'Crossed-out characters' }, + { ps: 21, name: 'Doubly-underlined' }, + { ps: 22, name: 'Normal' }, + { ps: 23, name: 'Not italicized' }, + { ps: 24, name: 'Not underlined' }, + { ps: 25, name: 'Steady (not blink)' }, + { ps: 27, name: 'Positive (not inverse)' }, + { ps: 28, name: 'Visible (not hidden)' }, + { ps: 29, name: 'Not crossed-out' }, + { ps: 30, name: 'Foreground Black' }, + { ps: 31, name: 'Foreground Red' }, + { ps: 32, name: 'Foreground Green' }, + { ps: 33, name: 'Foreground Yellow' }, + { ps: 34, name: 'Foreground Blue' }, + { ps: 35, name: 'Foreground Magenta' }, + { ps: 36, name: 'Foreground Cyan' }, + { ps: 37, name: 'Foreground White' }, + { ps: 39, name: 'Foreground default' }, + { ps: 40, name: 'Background Black' }, + { ps: 41, name: 'Background Red' }, + { ps: 42, name: 'Background Green' }, + { ps: 43, name: 'Background Yellow' }, + { ps: 44, name: 'Background Blue' }, + { ps: 45, name: 'Background Magenta' }, + { ps: 46, name: 'Background Cyan' }, + { ps: 47, name: 'Background White' }, + { ps: 49, name: 'Background default' } + ]; + const maxNameLength = entries.reduce((p, c) => Math.max(c.name.length, p), 0); + for (const e of entries) { + term.writeln(`\x1b[0m\x1b[${e.ps}m ${e.ps.toString().padEnd(2, ' ')} ${e.name.padEnd(maxNameLength, ' ')} - ${testString}\x1b[0m`); + } +} + +function addAnsiHyperlink(): void { term.write('\n\n\r'); term.writeln(`Regular link with no id:`); term.writeln('\x1b]8;;https://github.com\x07GitHub\x1b]8;;\x07'); @@ -889,7 +961,7 @@ function addAnsiHyperlink() { term.write('\x1b[3A\x1b[1C\x1b]8;;https://xtermjs.org\x07xter\x1b[B\x1b[4Dm.js\x1b]8;;\x07\x1b[2B\x1b[5D'); } -function addDecoration() { +function addDecoration(): void { term.options['overviewRulerWidth'] = 15; const marker = term.registerMarker(1); const decoration = term.registerDecoration({ @@ -904,7 +976,7 @@ function addDecoration() { }); } -function addOverviewRuler() { +function addOverviewRuler(): void { term.options['overviewRulerWidth'] = 15; term.registerDecoration({ marker: term.registerMarker(1), overviewRulerOptions: { color: '#ef2929' } }); term.registerDecoration({ marker: term.registerMarker(3), overviewRulerOptions: { color: '#8ae234' } }); diff --git a/demo/index.html b/demo/index.html index 412892210..a65b8bbc2 100644 --- a/demo/index.html +++ b/demo/index.html @@ -79,6 +79,7 @@
+
diff --git a/demo/server.js b/demo/server.js index 8d295942f..0e82f9e96 100644 --- a/demo/server.js +++ b/demo/server.js @@ -16,8 +16,7 @@ function startServer() { var app = express(); expressWs(app); - var terminals = {}, - logs = {}; + var terminals = {}; app.use('/xterm.css', express.static(__dirname + '/../css/xterm.css')); app.get('/logo.png', (req, res) => { @@ -55,10 +54,6 @@ function startServer() { console.log('Created terminal with PID: ' + term.pid); terminals[term.pid] = term; - logs[term.pid] = ''; - term.on('data', function(data) { - logs[term.pid] += data; - }); res.send(term.pid.toString()); res.end(); }); @@ -77,15 +72,25 @@ function startServer() { app.ws('/terminals/:pid', function (ws, req) { var term = terminals[parseInt(req.params.pid)]; console.log('Connected to terminal ' + term.pid); - ws.send(logs[term.pid]); + + // unbuffered delivery after user input + let userInput = false; // string message buffering - function buffer(socket, timeout) { + function buffer(socket, timeout, maxSize) { let s = ''; let sender = null; return (data) => { s += data; - if (!sender) { + if (s.length > maxSize || userInput) { + userInput = false; + socket.send(s); + s = ''; + if (sender) { + clearTimeout(sender); + sender = null; + } + } else if (!sender) { sender = setTimeout(() => { socket.send(s); s = ''; @@ -95,14 +100,23 @@ function startServer() { }; } // binary message buffering - function bufferUtf8(socket, timeout) { + function bufferUtf8(socket, timeout, maxSize) { let buffer = []; let sender = null; let length = 0; return (data) => { buffer.push(data); length += data.length; - if (!sender) { + if (length > maxSize || userInput) { + userInput = false; + socket.send(Buffer.concat(buffer, length)); + buffer = []; + length = 0; + if (sender) { + clearTimeout(sender); + sender = null; + } + } else if (!sender) { sender = setTimeout(() => { socket.send(Buffer.concat(buffer, length)); buffer = []; @@ -112,27 +126,23 @@ function startServer() { } }; } - const send = USE_BINARY ? bufferUtf8(ws, 5) : buffer(ws, 5); + const send = (USE_BINARY ? bufferUtf8 : buffer)(ws, 5, 262144); // WARNING: This is a naive implementation that will not throttle the flow of data. This means // it could flood the communication channel and make the terminal unresponsive. Learn more about // the problem and how to implement flow control at https://xtermjs.org/docs/guides/flowcontrol/ term.on('data', function(data) { - try { - send(data); - } catch (ex) { - // The WebSocket is not open, ignore - } + send(data); }); ws.on('message', function(msg) { term.write(msg); + userInput = true; }); ws.on('close', function () { term.kill(); console.log('Closed terminal ' + term.pid); // Clean things up delete terminals[term.pid]; - delete logs[term.pid]; }); }); diff --git a/src/browser/ColorManager.ts b/src/browser/ColorManager.ts index a22a423fd..0eb726a65 100644 --- a/src/browser/ColorManager.ts +++ b/src/browser/ColorManager.ts @@ -5,7 +5,7 @@ import { IColorManager, IColorSet, IColorContrastCache } from 'browser/Types'; import { ITheme } from 'common/services/Services'; -import { channels, color, css } from 'common/Color'; +import { channels, color, css, NULL_COLOR } from 'common/Color'; import { ColorContrastCache } from 'browser/ColorContrastCache'; import { ColorIndex, IColor } from 'common/Types'; @@ -124,12 +124,8 @@ export class ColorManager implements IColorManager { this.colors.selectionBackgroundOpaque = color.blend(this.colors.background, this.colors.selectionBackgroundTransparent); this.colors.selectionInactiveBackgroundTransparent = this._parseColor(theme.selectionInactiveBackground, this.colors.selectionBackgroundTransparent); this.colors.selectionInactiveBackgroundOpaque = color.blend(this.colors.background, this.colors.selectionInactiveBackgroundTransparent); - const nullColor: IColor = { - css: '', - rgba: 0 - }; - this.colors.selectionForeground = theme.selectionForeground ? this._parseColor(theme.selectionForeground, nullColor) : undefined; - if (this.colors.selectionForeground === nullColor) { + this.colors.selectionForeground = theme.selectionForeground ? this._parseColor(theme.selectionForeground, NULL_COLOR) : undefined; + if (this.colors.selectionForeground === NULL_COLOR) { this.colors.selectionForeground = undefined; } diff --git a/src/browser/Linkifier2.ts b/src/browser/Linkifier2.ts index 9c978949b..cf5b9dd2e 100644 --- a/src/browser/Linkifier2.ts +++ b/src/browser/Linkifier2.ts @@ -26,10 +26,10 @@ export class Linkifier2 extends Disposable implements ILinkifier2 { private _activeProviderReplies: Map | undefined; private _activeLine: number = -1; - private _onShowLinkUnderline = this.register(new EventEmitter()); - public get onShowLinkUnderline(): IEvent { return this._onShowLinkUnderline.event; } - private _onHideLinkUnderline = this.register(new EventEmitter()); - public get onHideLinkUnderline(): IEvent { return this._onHideLinkUnderline.event; } + private readonly _onShowLinkUnderline = this.register(new EventEmitter()); + public readonly onShowLinkUnderline = this._onShowLinkUnderline.event; + private readonly _onHideLinkUnderline = this.register(new EventEmitter()); + public readonly onHideLinkUnderline = this._onHideLinkUnderline.event; constructor( @IBufferService private readonly _bufferService: IBufferService diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index aae5cdefd..14dbca381 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -22,7 +22,7 @@ */ import { ICompositionHelper, ITerminal, IBrowser, CustomKeyEventHandler, IViewport, ILinkifier2, CharacterJoinerHandler, IBufferRange } from 'browser/Types'; -import { IRenderer } from 'browser/renderer/Types'; +import { IRenderer } from 'browser/renderer/shared/Types'; import { CompositionHelper } from 'browser/input/CompositionHelper'; import { Viewport } from 'browser/Viewport'; import { rightClickHandler, moveTextAreaUnderMouseCursor, handlePasteEvent, copyHandler, paste } from 'browser/Clipboard'; @@ -122,18 +122,18 @@ export class Terminal extends CoreTerminal implements ITerminal { private _colorManager: ColorManager | undefined; private _theme: ITheme | undefined; - private _onCursorMove = new EventEmitter(); - public get onCursorMove(): IEvent { return this._onCursorMove.event; } - private _onKey = new EventEmitter<{ key: string, domEvent: KeyboardEvent }>(); - public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._onKey.event; } - private _onRender = new EventEmitter<{ start: number, end: number }>(); - public get onRender(): IEvent<{ start: number, end: number }> { return this._onRender.event; } - private _onSelectionChange = new EventEmitter(); - public get onSelectionChange(): IEvent { return this._onSelectionChange.event; } - private _onTitleChange = new EventEmitter(); - public get onTitleChange(): IEvent { return this._onTitleChange.event; } - private _onBell = new EventEmitter(); - public get onBell(): IEvent { return this._onBell.event; } + private readonly _onCursorMove = new EventEmitter(); + public readonly onCursorMove = this._onCursorMove.event; + private readonly _onKey = new EventEmitter<{ key: string, domEvent: KeyboardEvent }>(); + public readonly onKey = this._onKey.event; + private readonly _onRender = new EventEmitter<{ start: number, end: number }>(); + public readonly onRender = this._onRender.event; + private readonly _onSelectionChange = new EventEmitter(); + public readonly onSelectionChange = this._onSelectionChange.event; + private readonly _onTitleChange = new EventEmitter(); + public readonly onTitleChange = this._onTitleChange.event; + private readonly _onBell = new EventEmitter(); + public readonly onBell = this._onBell.event; private _onFocus = new EventEmitter(); public get onFocus(): IEvent { return this._onFocus.event; } @@ -146,7 +146,6 @@ export class Terminal extends CoreTerminal implements ITerminal { private _onWillOpen = new EventEmitter(); public get onWillOpen(): IEvent { return this._onWillOpen.event; } - /** * Creates a new `Terminal` object. * diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index fb0988505..d72f1446b 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -6,7 +6,7 @@ import { IDisposable, IMarker, ILinkProvider, IDecorationOptions, IDecoration } from 'xterm'; import { IEvent, EventEmitter } from 'common/EventEmitter'; import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services'; -import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types'; +import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types'; import { IColorSet, ITerminal, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper, CharacterJoinerHandler, IBufferRange } from 'browser/Types'; import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types'; import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, ITerminalOptions } from 'common/Types'; diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index f95e0bb48..453733533 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -9,7 +9,7 @@ import { IColorSet, IViewport } from 'browser/Types'; import { ICharSizeService, ICoreBrowserService, IRenderService } from 'browser/services/Services'; import { IBufferService, IOptionsService } from 'common/services/Services'; import { IBuffer } from 'common/buffer/Types'; -import { IRenderDimensions } from 'browser/renderer/Types'; +import { IRenderDimensions } from 'browser/renderer/shared/Types'; const FALLBACK_SCROLL_BAR_WIDTH = 15; diff --git a/src/browser/renderer/Types.d.ts b/src/browser/renderer/Types.d.ts deleted file mode 100644 index cb1a85b46..000000000 --- a/src/browser/renderer/Types.d.ts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Copyright (c) 2019 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { IDisposable } from 'common/Types'; -import { IColorSet } from 'browser/Types'; -import { IEvent } from 'common/EventEmitter'; - -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; -} - -export interface IRequestRedrawEvent { - start: number; - end: number; -} - -/** - * Note that IRenderer implementations should emit the refresh event after - * rendering rows to the screen. - */ -export interface IRenderer extends IDisposable { - readonly dimensions: IRenderDimensions; - - /** - * Fires when the renderer is requesting to be redrawn on the next animation - * frame but is _not_ a result of content changing (eg. selection changes). - */ - readonly onRequestRedraw: IEvent; - - dispose(): void; - setColors(colors: IColorSet): void; - onDevicePixelRatioChange(): void; - onResize(cols: number, rows: number): void; - onCharSizeChanged(): void; - onBlur(): void; - onFocus(): void; - onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void; - onCursorMove(): void; - onOptionsChanged(): void; - clear(): void; - renderRows(start: number, end: number): void; - clearTextureAtlas?(): void; -} diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index 8df6b3028..6a70ccec4 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -3,9 +3,9 @@ * @license MIT */ -import { IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types'; +import { IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/shared/Types'; import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_BLINK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from 'browser/renderer/dom/DomRendererRowFactory'; -import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants'; +import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants'; import { Disposable } from 'common/Lifecycle'; import { IColorSet, ILinkifierEvent, ILinkifier2 } from 'browser/Types'; import { ICharSizeService, ICoreBrowserService } from 'browser/services/Services'; @@ -40,7 +40,7 @@ export class DomRenderer extends Disposable implements IRenderer { public dimensions: IRenderDimensions; - public get onRequestRedraw(): IEvent { return new EventEmitter().event; } + public readonly onRequestRedraw = new EventEmitter().event; constructor( private _colors: IColorSet, @@ -231,11 +231,11 @@ export class DomRenderer extends Disposable implements IRenderer { ` background-color: ${this._colors.selectionInactiveBackgroundOpaque.css};` + `}`; // Colors - this._colors.ansi.forEach((c, i) => { + for (const [i, c] of this._colors.ansi.entries()) { styles += `${this._terminalSelector} .${FG_CLASS_PREFIX}${i} { color: ${c.css}; }` + `${this._terminalSelector} .${BG_CLASS_PREFIX}${i} { background-color: ${c.css}; }`; - }); + } styles += `${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { color: ${color.opaque(this._colors.background).css}; }` + `${this._terminalSelector} .${BG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { background-color: ${this._colors.foreground.css}; }`; diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index d3eb9e8e6..bce72ec35 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -4,7 +4,7 @@ */ import { IBufferLine, ICellData, IColor } from 'common/Types'; -import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants'; +import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants'; import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; @@ -12,7 +12,7 @@ import { color, rgba } from 'common/Color'; import { IColorSet } from 'browser/Types'; import { ICharacterJoinerService, ICoreBrowserService } from 'browser/services/Services'; import { JoinedCellData } from 'browser/services/CharacterJoinerService'; -import { excludeFromContrastRatioDemands } from 'browser/renderer/RendererUtils'; +import { excludeFromContrastRatioDemands } from 'browser/renderer/shared/RendererUtils'; import { AttributeData } from 'common/buffer/AttributeData'; export const BOLD_CLASS = 'xterm-bold'; @@ -166,7 +166,7 @@ export class DomRendererRowFactory { if (cell.isUnderline()) { charElement.classList.add(`${UNDERLINE_CLASS}-${cell.extended.underlineStyle}`); if (charElement.textContent === ' ') { - charElement.innerHTML = ' '; + charElement.textContent = '\xa0'; // =   } if (!cell.isUnderlineColorDefault()) { if (cell.isUnderlineColorRGB()) { diff --git a/src/browser/renderer/shared/CellColorResolver.ts b/src/browser/renderer/shared/CellColorResolver.ts new file mode 100644 index 000000000..b29475b94 --- /dev/null +++ b/src/browser/renderer/shared/CellColorResolver.ts @@ -0,0 +1,136 @@ +import { ISelectionRenderModel } from 'browser/renderer/shared/Types'; +import { ICoreBrowserService } from 'browser/services/Services'; +import { IColorSet } from 'browser/Types'; +import { Attributes, BgFlags, FgFlags } from 'common/buffer/Constants'; +import { IDecorationService } from 'common/services/Services'; +import { ICellData } from 'common/Types'; +import { Terminal } from 'xterm'; + +// Work variables to avoid garbage collection +let $fg = 0; +let $bg = 0; +let $hasFg = false; +let $hasBg = false; +let $isSelected = false; + +export class CellColorResolver { + /** + * The shared result of the {@link resolve} call. This is only safe to use immediately after as + * any other calls will share object. + */ + public readonly result: { fg: number, bg: number, ext: number } = { + fg: 0, + bg: 0, + ext: 0 + }; + + constructor( + private readonly _terminal: Terminal, + private _colors: IColorSet, + private readonly _selectionRenderModel: ISelectionRenderModel, + private readonly _decorationService: IDecorationService, + private readonly _coreBrowserService: ICoreBrowserService + ) { + } + + public setColors(colors: IColorSet): void { + this._colors = colors; + } + + /** + * Resolves colors for the cell, putting the result into the shared {@link result}. This resolves + * overrides, inverse and selection for the cell which can then be used to feed into the renderer. + */ + public resolve(cell: ICellData, x: number, y: number): void { + this.result.bg = cell.bg; + this.result.fg = cell.fg; + this.result.ext = cell.bg & BgFlags.HAS_EXTENDED ? cell.extended.ext : 0; + // Get any foreground/background overrides, this happens on the model to avoid spreading + // override logic throughout the different sub-renderers + + // Reset overrides work variables + $bg = 0; + $fg = 0; + $hasBg = false; + $hasFg = false; + $isSelected = false; + + // Apply decorations on the bottom layer + this._decorationService.forEachDecorationAtCell(x, y, 'bottom', d => { + if (d.backgroundColorRGB) { + $bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF; + $hasBg = true; + } + if (d.foregroundColorRGB) { + $fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF; + $hasFg = true; + } + }); + + // Apply the selection color if needed + $isSelected = this._selectionRenderModel.isCellSelected(this._terminal, x, y); + if ($isSelected) { + $bg = (this._coreBrowserService.isFocused ? this._colors.selectionBackgroundOpaque : this._colors.selectionInactiveBackgroundOpaque).rgba >> 8 & 0xFFFFFF; + $hasBg = true; + if (this._colors.selectionForeground) { + $fg = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF; + $hasFg = true; + } + } + + // Apply decorations on the top layer + this._decorationService.forEachDecorationAtCell(x, y, 'top', d => { + if (d.backgroundColorRGB) { + $bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF; + $hasBg = true; + } + if (d.foregroundColorRGB) { + $fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF; + $hasFg = true; + } + }); + + // Convert any overrides from rgba to the fg/bg packed format. This resolves the inverse flag + // ahead of time in order to use the correct cache key + if ($hasBg) { + if ($isSelected) { + // Non-RGB attributes from model + force non-dim + override + force RGB color mode + $bg = (cell.bg & ~Attributes.RGB_MASK & ~BgFlags.DIM) | $bg | Attributes.CM_RGB; + } else { + // Non-RGB attributes from model + override + force RGB color mode + $bg = (cell.bg & ~Attributes.RGB_MASK) | $bg | Attributes.CM_RGB; + } + } + if ($hasFg) { + // Non-RGB attributes from model + force disable inverse + override + force RGB color mode + $fg = (cell.fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | $fg | Attributes.CM_RGB; + } + + // Handle case where inverse was specified by only one of bg override or fg override was set, + // resolving the other inverse color and setting the inverse flag if needed. + if (this.result.fg & FgFlags.INVERSE) { + if ($hasBg && !$hasFg) { + // Resolve bg color type (default color has a different meaning in fg vs bg) + if ((this.result.bg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) { + $fg = (this.result.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | ((this._colors.background.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB; + } else { + $fg = (this.result.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | this.result.bg & (Attributes.RGB_MASK | Attributes.CM_MASK); + } + $hasFg = true; + } + if (!$hasBg && $hasFg) { + // Resolve bg color type (default color has a different meaning in fg vs bg) + if ((this.result.fg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) { + $bg = (this.result.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | ((this._colors.foreground.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB; + } else { + $bg = (this.result.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | this.result.fg & (Attributes.RGB_MASK | Attributes.CM_MASK); + } + $hasBg = true; + } + } + + // Use the override if it exists + this.result.bg = $hasBg ? $bg : this.result.bg; + this.result.fg = $hasFg ? $fg : this.result.fg; + } +} diff --git a/addons/xterm-addon-webgl/src/atlas/CharAtlasCache.ts b/src/browser/renderer/shared/CharAtlasCache.ts similarity index 83% rename from addons/xterm-addon-webgl/src/atlas/CharAtlasCache.ts rename to src/browser/renderer/shared/CharAtlasCache.ts index 893362c86..ee88c7089 100644 --- a/addons/xterm-addon-webgl/src/atlas/CharAtlasCache.ts +++ b/src/browser/renderer/shared/CharAtlasCache.ts @@ -3,21 +3,21 @@ * @license MIT */ -import { generateConfig, configEquals } from './CharAtlasUtils'; -import { WebglCharAtlas } from './WebglCharAtlas'; -import { ICharAtlasConfig } from './Types'; +import { TextureAtlas } from 'browser/renderer/shared/TextureAtlas'; import { Terminal } from 'xterm'; import { IColorSet, ITerminal } from 'browser/Types'; +import { ICharAtlasConfig, ITextureAtlas } from 'browser/renderer/shared/Types'; +import { generateConfig, configEquals } from 'browser/renderer/shared/CharAtlasUtils'; -interface ICharAtlasCacheEntry { - atlas: WebglCharAtlas; +interface ITextureAtlasCacheEntry { + atlas: ITextureAtlas; config: ICharAtlasConfig; // N.B. This implementation potentially holds onto copies of the terminal forever, so // this may cause memory leaks. ownedBy: Terminal[]; } -const charAtlasCache: ICharAtlasCacheEntry[] = []; +const charAtlasCache: ITextureAtlasCacheEntry[] = []; /** * Acquires a char atlas, either generating a new one or returning an existing @@ -25,7 +25,7 @@ const charAtlasCache: ICharAtlasCacheEntry[] = []; * @param terminal The terminal. * @param colors The colors to use. */ -export function acquireCharAtlas( +export function acquireTextureAtlas( terminal: Terminal, colors: IColorSet, scaledCellWidth: number, @@ -33,7 +33,7 @@ export function acquireCharAtlas( scaledCharWidth: number, scaledCharHeight: number, devicePixelRatio: number -): WebglCharAtlas { +): ITextureAtlas { const newConfig = generateConfig(scaledCellWidth, scaledCellHeight, scaledCharWidth, scaledCharHeight, terminal, colors, devicePixelRatio); // Check to see if the terminal already owns this config @@ -66,8 +66,8 @@ export function acquireCharAtlas( } const core: ITerminal = (terminal as any)._core; - const newEntry: ICharAtlasCacheEntry = { - atlas: new WebglCharAtlas(document, newConfig, core.unicodeService), + const newEntry: ITextureAtlasCacheEntry = { + atlas: new TextureAtlas(document, newConfig, core.unicodeService), config: newConfig, ownedBy: [terminal] }; diff --git a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts b/src/browser/renderer/shared/CharAtlasUtils.ts similarity index 92% rename from addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts rename to src/browser/renderer/shared/CharAtlasUtils.ts index 83f82fa76..58c207e88 100644 --- a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts +++ b/src/browser/renderer/shared/CharAtlasUtils.ts @@ -5,14 +5,9 @@ import { ICharAtlasConfig } from './Types'; import { Attributes } from 'common/buffer/Constants'; -import { Terminal, FontWeight } from 'xterm'; +import { Terminal } from 'xterm'; import { IColorSet } from 'browser/Types'; -import { IColor } from 'common/Types'; - -const NULL_COLOR: IColor = { - css: '', - rgba: 0 -}; +import { NULL_COLOR } from 'common/Color'; export function generateConfig(scaledCellWidth: number, scaledCellHeight: number, scaledCharWidth: number, scaledCharHeight: number, terminal: Terminal, colors: IColorSet, devicePixelRatio: number): ICharAtlasConfig { // null out some fields that don't matter @@ -70,8 +65,8 @@ export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean a.scaledCharHeight === b.scaledCharHeight && a.drawBoldTextInBrightColors === b.drawBoldTextInBrightColors && a.minimumContrastRatio === b.minimumContrastRatio && - a.colors.foreground === b.colors.foreground && - a.colors.background === b.colors.background; + a.colors.foreground.rgba === b.colors.foreground.rgba && + a.colors.background.rgba === b.colors.background.rgba; } export function is256Color(colorCode: number): boolean { diff --git a/src/browser/renderer/Constants.ts b/src/browser/renderer/shared/Constants.ts similarity index 100% rename from src/browser/renderer/Constants.ts rename to src/browser/renderer/shared/Constants.ts diff --git a/src/browser/renderer/CustomGlyphs.ts b/src/browser/renderer/shared/CustomGlyphs.ts similarity index 99% rename from src/browser/renderer/CustomGlyphs.ts rename to src/browser/renderer/shared/CustomGlyphs.ts index 32256cf4b..b87256855 100644 --- a/src/browser/renderer/CustomGlyphs.ts +++ b/src/browser/renderer/shared/CustomGlyphs.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { throwIfFalsy } from 'browser/renderer/RendererUtils'; +import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; interface IBlockVector { x: number; diff --git a/src/browser/renderer/DevicePixelObserver.ts b/src/browser/renderer/shared/DevicePixelObserver.ts similarity index 100% rename from src/browser/renderer/DevicePixelObserver.ts rename to src/browser/renderer/shared/DevicePixelObserver.ts diff --git a/src/browser/renderer/shared/README.md b/src/browser/renderer/shared/README.md new file mode 100644 index 000000000..580842356 --- /dev/null +++ b/src/browser/renderer/shared/README.md @@ -0,0 +1 @@ +This folder contains files that are shared between the renderer addons, but not necessarily bundled into the `xterm` module. diff --git a/src/browser/renderer/RendererUtils.ts b/src/browser/renderer/shared/RendererUtils.ts similarity index 100% rename from src/browser/renderer/RendererUtils.ts rename to src/browser/renderer/shared/RendererUtils.ts diff --git a/src/browser/renderer/shared/SelectionRenderModel.ts b/src/browser/renderer/shared/SelectionRenderModel.ts new file mode 100644 index 000000000..db3757788 --- /dev/null +++ b/src/browser/renderer/shared/SelectionRenderModel.ts @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2022 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ISelectionRenderModel } from 'browser/renderer/shared/Types'; +import { Terminal } from 'xterm'; + +class SelectionRenderModel implements ISelectionRenderModel { + public hasSelection!: boolean; + public columnSelectMode!: boolean; + public viewportStartRow!: number; + public viewportEndRow!: number; + public viewportCappedStartRow!: number; + public viewportCappedEndRow!: number; + public startCol!: number; + public endCol!: number; + public selectionStart: [number, number] | undefined; + public selectionEnd: [number, number] | undefined; + + constructor() { + this.clear(); + } + + public clear(): void { + this.hasSelection = false; + this.columnSelectMode = false; + this.viewportStartRow = 0; + this.viewportEndRow = 0; + this.viewportCappedStartRow = 0; + this.viewportCappedEndRow = 0; + this.startCol = 0; + this.endCol = 0; + this.selectionStart = undefined; + this.selectionEnd = undefined; + } + + public update(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void { + this.selectionStart = start; + this.selectionEnd = end; + // Selection does not exist + if (!start || !end || (start[0] === end[0] && start[1] === end[1])) { + this.clear(); + return; + } + + // Translate from buffer position to viewport position + const viewportStartRow = start[1] - terminal.buffer.active.viewportY; + const viewportEndRow = end[1] - terminal.buffer.active.viewportY; + const viewportCappedStartRow = Math.max(viewportStartRow, 0); + const viewportCappedEndRow = Math.min(viewportEndRow, terminal.rows - 1); + + // No need to draw the selection + if (viewportCappedStartRow >= terminal.rows || viewportCappedEndRow < 0) { + this.clear(); + return; + } + + this.hasSelection = true; + this.columnSelectMode = columnSelectMode; + this.viewportStartRow = viewportStartRow; + this.viewportEndRow = viewportEndRow; + this.viewportCappedStartRow = viewportCappedStartRow; + this.viewportCappedEndRow = viewportCappedEndRow; + this.startCol = start[0]; + this.endCol = end[0]; + } + + public isCellSelected(terminal: Terminal, x: number, y: number): boolean { + if (!this.hasSelection) { + return false; + } + y -= terminal.buffer.active.viewportY; + if (this.columnSelectMode) { + if (this.startCol <= this.endCol) { + return x >= this.startCol && y >= this.viewportCappedStartRow && + x < this.endCol && y <= this.viewportCappedEndRow; + } + return x < this.startCol && y >= this.viewportCappedStartRow && + x >= this.endCol && y <= this.viewportCappedEndRow; + } + return (y > this.viewportStartRow && y < this.viewportEndRow) || + (this.viewportStartRow === this.viewportEndRow && y === this.viewportStartRow && x >= this.startCol && x < this.endCol) || + (this.viewportStartRow < this.viewportEndRow && y === this.viewportEndRow && x < this.endCol) || + (this.viewportStartRow < this.viewportEndRow && y === this.viewportStartRow && x >= this.startCol); + } +} + +export function createSelectionRenderModel(): ISelectionRenderModel { + return new SelectionRenderModel(); +} diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/src/browser/renderer/shared/TextureAtlas.ts similarity index 97% rename from addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts rename to src/browser/renderer/shared/TextureAtlas.ts index dfcede38a..c4bffe8d3 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/src/browser/renderer/shared/TextureAtlas.ts @@ -3,20 +3,17 @@ * @license MIT */ -import { ICharAtlasConfig } from './Types'; -import { DIM_OPACITY, TEXT_BASELINE } from 'browser/renderer/Constants'; -import { IRasterizedGlyph, IBoundingBox } from '../Types'; +import { DIM_OPACITY, TEXT_BASELINE } from 'browser/renderer/shared/Constants'; import { DEFAULT_COLOR, Attributes, DEFAULT_EXT, UnderlineStyle } from 'common/buffer/Constants'; -import { throwIfFalsy } from '../WebglUtils'; import { IColor } from 'common/Types'; -import { IDisposable } from 'xterm'; import { AttributeData } from 'common/buffer/AttributeData'; -import { color, rgba } from 'common/Color'; -import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs'; -import { excludeFromContrastRatioDemands, isPowerlineGlyph, isRestrictedPowerlineGlyph } from 'browser/renderer/RendererUtils'; +import { color, NULL_COLOR, rgba } from 'common/Color'; +import { tryDrawCustomChar } from 'browser/renderer/shared/CustomGlyphs'; +import { excludeFromContrastRatioDemands, isPowerlineGlyph, isRestrictedPowerlineGlyph, throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; import { IUnicodeService } from 'common/services/Services'; import { FourKeyMap } from 'common/MultiKeyMap'; -import { IdleTaskQueue } from 'common/Idle'; +import { IdleTaskQueue } from 'common/TaskQueue'; +import { IBoundingBox, ICharAtlasConfig, IRasterizedGlyph, ITextureAtlas } from 'browser/renderer/shared/Types'; // For debugging purposes, it can be useful to set this to a really tiny value, // to verify that LRU eviction works. @@ -29,12 +26,6 @@ const TEXTURE_HEIGHT = 1024; * this prevent juggling multiple textures in the GL context. */ const TEXTURE_CAPACITY = Math.floor(TEXTURE_HEIGHT * 0.8); - -const TRANSPARENT_COLOR = { - css: 'rgba(0, 0, 0, 0)', - rgba: 0 -}; - /** * A shared object which is used to draw nothing for a particular cell. */ @@ -54,12 +45,10 @@ interface ICharAtlasActiveRow { height: number; } -/** Work variables to avoid garbage collection. */ -const w: { glyph: IRasterizedGlyph | undefined } = { - glyph: undefined -}; +// Work variables to avoid garbage collection +let $glyph = undefined; -export class WebglCharAtlas implements IDisposable { +export class TextureAtlas implements ITextureAtlas { private _didWarmUp: boolean = false; private _cacheMap: FourKeyMap = new FourKeyMap(); @@ -184,12 +173,12 @@ export class WebglCharAtlas implements IDisposable { fg: number, ext: number ): IRasterizedGlyph { - w.glyph = cacheMap.get(key, bg, fg, ext); - if (!w.glyph) { - w.glyph = this._drawToCache(key, bg, fg, ext); - cacheMap.set(key, bg, fg, ext, w.glyph); + $glyph = cacheMap.get(key, bg, fg, ext); + if (!$glyph) { + $glyph = this._drawToCache(key, bg, fg, ext); + cacheMap.set(key, bg, fg, ext, $glyph); } - return w.glyph; + return $glyph; } private _getColorFromAnsiIndex(idx: number): IColor { @@ -204,7 +193,7 @@ export class WebglCharAtlas implements IDisposable { // The background color might have some transparency, so we need to render it as fully // transparent in the atlas. Otherwise we'd end up drawing the transparent background twice // around the anti-aliased edges of the glyph, and it would look too dark. - return TRANSPARENT_COLOR; + return NULL_COLOR; } let result: IColor; @@ -347,6 +336,9 @@ export class WebglCharAtlas implements IDisposable { private _drawToCache(codeOrChars: number | string, bg: number, fg: number, ext: number): IRasterizedGlyph { const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars; + // Uncomment for debugging + // console.log(`draw to cache "${chars}"`, bg, fg, ext); + this.hasCanvasChanged = true; // Allow 1 cell width per character, with a minimum of 2 (CJK), plus some padding. This is used diff --git a/src/browser/renderer/shared/Types.d.ts b/src/browser/renderer/shared/Types.d.ts new file mode 100644 index 000000000..7e2badcbc --- /dev/null +++ b/src/browser/renderer/shared/Types.d.ts @@ -0,0 +1,156 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { FontWeight, Terminal } from 'xterm'; +import { IColorSet } from 'browser/Types'; +import { IDisposable } from 'common/Types'; +import { IEvent } from 'common/EventEmitter'; + +export interface ICharAtlasConfig { + customGlyphs: boolean; + devicePixelRatio: number; + letterSpacing: number; + lineHeight: number; + fontSize: number; + fontFamily: string; + fontWeight: FontWeight; + fontWeightBold: FontWeight; + scaledCellWidth: number; + scaledCellHeight: number; + scaledCharWidth: number; + scaledCharHeight: number; + allowTransparency: boolean; + drawBoldTextInBrightColors: boolean; + minimumContrastRatio: number; + colors: IColorSet; +} + +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; +} + +export interface IRequestRedrawEvent { + start: number; + end: number; +} + +/** + * Note that IRenderer implementations should emit the refresh event after + * rendering rows to the screen. + */ +export interface IRenderer extends IDisposable { + readonly dimensions: IRenderDimensions; + + /** + * Fires when the renderer is requesting to be redrawn on the next animation + * frame but is _not_ a result of content changing (eg. selection changes). + */ + readonly onRequestRedraw: IEvent; + + dispose(): void; + setColors(colors: IColorSet): void; + onDevicePixelRatioChange(): void; + onResize(cols: number, rows: number): void; + onCharSizeChanged(): void; + onBlur(): void; + onFocus(): void; + onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void; + onCursorMove(): void; + onOptionsChanged(): void; + clear(): void; + renderRows(start: number, end: number): void; + clearTextureAtlas?(): void; +} + +export interface ITextureAtlas extends IDisposable { + readonly cacheCanvas: HTMLCanvasElement; + hasCanvasChanged: boolean; + + /** + * Warm up the texture atlas, adding common glyphs to avoid slowing early frame. + */ + warmUp(): void; + + /** + * Call when a frame is being drawn, this will return true if the atlas was cleared to make room + * for a new set of glyphs. + */ + beginFrame(): boolean; + + /** + * Clear all glyphs from the texture atlas. + */ + clearTexture(): void; + getRasterizedGlyph(code: number, bg: number, fg: number, ext: number): IRasterizedGlyph; + getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number): IRasterizedGlyph; +} + +/** + * Represents a rasterized glyph within a texture atlas. Some numbers are + * tracked in CSS pixels as well in order to reduce calculations during the + * render loop. + */ +export interface IRasterizedGlyph { + /** + * The x and y offset between the glyph's top/left and the top/left of a cell + * in pixels. + */ + offset: IVector; + /** + * the x and y position of the glyph in the texture in pixels. + */ + texturePosition: IVector; + /** + * the x and y position of the glyph in the texture in clip space coordinates. + */ + texturePositionClipSpace: IVector; + /** + * The width and height of the glyph in the texture in pixels. + */ + size: IVector; + /** + * The width and height of the glyph in the texture in clip space coordinates. + */ + sizeClipSpace: IVector; +} + +export interface IVector { + x: number; + y: number; +} + +export interface IBoundingBox { + top: number; + left: number; + right: number; + bottom: number; +} + +export interface ISelectionRenderModel { + readonly hasSelection: boolean; + readonly columnSelectMode: boolean; + readonly viewportStartRow: number; + readonly viewportEndRow: number; + readonly viewportCappedStartRow: number; + readonly viewportCappedEndRow: number; + readonly startCol: number; + readonly endCol: number; + readonly selectionStart: [number, number] | undefined; + readonly selectionEnd: [number, number] | undefined; + clear(): void; + update(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode?: boolean): void; + isCellSelected(terminal: Terminal, x: number, y: number): boolean; +} diff --git a/src/browser/services/CharSizeService.ts b/src/browser/services/CharSizeService.ts index b04e157f6..7062deece 100644 --- a/src/browser/services/CharSizeService.ts +++ b/src/browser/services/CharSizeService.ts @@ -16,8 +16,8 @@ export class CharSizeService implements ICharSizeService { public get hasValidSize(): boolean { return this.width > 0 && this.height > 0; } - private _onCharSizeChange = new EventEmitter(); - public get onCharSizeChange(): IEvent { return this._onCharSizeChange.event; } + private readonly _onCharSizeChange = new EventEmitter(); + public readonly onCharSizeChange = this._onCharSizeChange.event; constructor( document: Document, diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index 6c5737c76..3a48281c7 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IRenderer, IRenderDimensions } from 'browser/renderer/Types'; +import { IRenderer, IRenderDimensions } from 'browser/renderer/shared/Types'; import { RenderDebouncer } from 'browser/RenderDebouncer'; import { EventEmitter, IEvent } from 'common/EventEmitter'; import { Disposable } from 'common/Lifecycle'; @@ -12,7 +12,7 @@ import { addDisposableDomListener } from 'browser/Lifecycle'; import { IColorSet, IRenderDebouncerWithCallback } from 'browser/Types'; import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services'; import { ICharSizeService, ICoreBrowserService, IRenderService } from 'browser/services/Services'; -import { DebouncedIdleTask } from 'common/Idle'; +import { DebouncedIdleTask } from 'common/TaskQueue'; interface ISelectionState { start: [number, number] | undefined; @@ -40,14 +40,14 @@ export class RenderService extends Disposable implements IRenderService { columnSelectMode: false }; - private _onDimensionsChange = new EventEmitter(); - public get onDimensionsChange(): IEvent { return this._onDimensionsChange.event; } - private _onRenderedViewportChange = new EventEmitter<{ start: number, end: number }>(); - public get onRenderedViewportChange(): IEvent<{ start: number, end: number }> { return this._onRenderedViewportChange.event; } - private _onRender = new EventEmitter<{ start: number, end: number }>(); - public get onRender(): IEvent<{ start: number, end: number }> { return this._onRender.event; } - private _onRefreshRequest = new EventEmitter<{ start: number, end: number }>(); - public get onRefreshRequest(): IEvent<{ start: number, end: number }> { return this._onRefreshRequest.event; } + private readonly _onDimensionsChange = new EventEmitter(); + public readonly onDimensionsChange = this._onDimensionsChange.event; + private readonly _onRenderedViewportChange = new EventEmitter<{ start: number, end: number }>(); + public readonly onRenderedViewportChange = this._onRenderedViewportChange.event; + private readonly _onRender = new EventEmitter<{ start: number, end: number }>(); + public readonly onRender = this._onRender.event; + private readonly _onRefreshRequest = new EventEmitter<{ start: number, end: number }>(); + public readonly onRefreshRequest = this._onRefreshRequest.event; public get dimensions(): IRenderDimensions { return this._renderer!.dimensions; } diff --git a/src/browser/services/SelectionService.ts b/src/browser/services/SelectionService.ts index 4ee1ffa1f..3780c7ee8 100644 --- a/src/browser/services/SelectionService.ts +++ b/src/browser/services/SelectionService.ts @@ -111,14 +111,14 @@ export class SelectionService extends Disposable implements ISelectionService { private _oldSelectionStart: [number, number] | undefined = undefined; private _oldSelectionEnd: [number, number] | undefined = undefined; - private _onLinuxMouseSelection = this.register(new EventEmitter()); - public get onLinuxMouseSelection(): IEvent { return this._onLinuxMouseSelection.event; } - private _onRedrawRequest = this.register(new EventEmitter()); - public get onRequestRedraw(): IEvent { return this._onRedrawRequest.event; } - private _onSelectionChange = this.register(new EventEmitter()); - public get onSelectionChange(): IEvent { return this._onSelectionChange.event; } - private _onRequestScrollLines = this.register(new EventEmitter()); - public get onRequestScrollLines(): IEvent { return this._onRequestScrollLines.event; } + private readonly _onLinuxMouseSelection = this.register(new EventEmitter()); + public readonly onLinuxMouseSelection = this._onLinuxMouseSelection.event; + private readonly _onRedrawRequest = this.register(new EventEmitter()); + public readonly onRequestRedraw = this._onRedrawRequest.event; + private readonly _onSelectionChange = this.register(new EventEmitter()); + public readonly onSelectionChange = this._onSelectionChange.event; + private readonly _onRequestScrollLines = this.register(new EventEmitter()); + public readonly onRequestScrollLines = this._onRequestScrollLines.event; constructor( private readonly _element: HTMLElement, diff --git a/src/browser/services/Services.ts b/src/browser/services/Services.ts index 3b52501b9..4343c450b 100644 --- a/src/browser/services/Services.ts +++ b/src/browser/services/Services.ts @@ -4,7 +4,7 @@ */ import { IEvent } from 'common/EventEmitter'; -import { IRenderDimensions, IRenderer } from 'browser/renderer/Types'; +import { IRenderDimensions, IRenderer } from 'browser/renderer/shared/Types'; import { IColorSet } from 'browser/Types'; import { ISelectionRedrawRequestEvent as ISelectionRequestRedrawEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; diff --git a/src/common/CircularList.ts b/src/common/CircularList.ts index 4d2c04eca..599db1aba 100644 --- a/src/common/CircularList.ts +++ b/src/common/CircularList.ts @@ -25,12 +25,12 @@ export class CircularList implements ICircularList { private _startIndex: number; private _length: number; - public onDeleteEmitter = new EventEmitter(); - public get onDelete(): IEvent { return this.onDeleteEmitter.event; } - public onInsertEmitter = new EventEmitter(); - public get onInsert(): IEvent { return this.onInsertEmitter.event; } - public onTrimEmitter = new EventEmitter(); - public get onTrim(): IEvent { return this.onTrimEmitter.event; } + public readonly onDeleteEmitter = new EventEmitter(); + public readonly onDelete = this.onDeleteEmitter.event; + public readonly onInsertEmitter = new EventEmitter(); + public readonly onInsert = this.onInsertEmitter.event; + public readonly onTrimEmitter = new EventEmitter(); + public readonly onTrim = this.onTrimEmitter.event; constructor( private _maxLength: number diff --git a/src/common/Color.ts b/src/common/Color.ts index 6e70671b5..0d7bffe04 100644 --- a/src/common/Color.ts +++ b/src/common/Color.ts @@ -11,6 +11,11 @@ let $g = 0; let $b = 0; let $a = 0; +export const NULL_COLOR: IColor = { + css: '#00000000', + rgba: 0 +}; + /** * Helper functions where the source type is "channels" (individual color channels as numbers). */ @@ -129,7 +134,7 @@ export namespace css { */ export function toColor(css: string): IColor { // Formats: #rgb[a] and #rrggbb[aa] - if (css.match(/#[0-9a-f]{3,8}/i)) { + if (css.match(/#[\da-f]{3,8}/i)) { switch (css.length) { case 4: { // #rgb $r = parseInt(css.slice(1, 2).repeat(2), 16); diff --git a/src/common/CoreTerminal.ts b/src/common/CoreTerminal.ts index d7cb0f7eb..2ad6f7351 100644 --- a/src/common/CoreTerminal.ts +++ b/src/common/CoreTerminal.ts @@ -22,7 +22,7 @@ */ import { Disposable } from 'common/Lifecycle'; -import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, ICoreMouseService, IUnicodeService, IDirtyRowService, LogLevelEnum, ITerminalOptions, IOscLinkService } from 'common/services/Services'; +import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, ICoreMouseService, IUnicodeService, LogLevelEnum, ITerminalOptions, IOscLinkService } from 'common/services/Services'; import { InstantiationService } from 'common/services/InstantiationService'; import { LogService } from 'common/services/LogService'; import { BufferService, MINIMUM_COLS, MINIMUM_ROWS } from 'common/services/BufferService'; @@ -31,7 +31,6 @@ import { IDisposable, IAttributeData, ICoreTerminal, IScrollEvent, ScrollSource import { CoreService } from 'common/services/CoreService'; import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter'; import { CoreMouseService } from 'common/services/CoreMouseService'; -import { DirtyRowService } from 'common/services/DirtyRowService'; import { UnicodeService } from 'common/services/UnicodeService'; import { CharsetService } from 'common/services/CharsetService'; import { updateWindowsModeWrappedState } from 'common/WindowsMode'; @@ -49,7 +48,6 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { protected readonly _bufferService: IBufferService; protected readonly _logService: ILogService; protected readonly _charsetService: ICharsetService; - protected readonly _dirtyRowService: IDirtyRowService; protected readonly _oscLinkService: IOscLinkService; public readonly coreMouseService: ICoreMouseService; @@ -61,22 +59,23 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { private _writeBuffer: WriteBuffer; private _windowsMode: IDisposable | undefined; - private _onBinary = new EventEmitter(); - public get onBinary(): IEvent { return this._onBinary.event; } - private _onData = new EventEmitter(); - public get onData(): IEvent { return this._onData.event; } + private readonly _onBinary = new EventEmitter(); + public readonly onBinary = this._onBinary.event; + private readonly _onData = new EventEmitter(); + public readonly onData = this._onData.event; protected _onLineFeed = new EventEmitter(); - public get onLineFeed(): IEvent { return this._onLineFeed.event; } - private _onResize = new EventEmitter<{ cols: number, rows: number }>(); - public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; } - protected _onScroll = new EventEmitter(); - public get onWriteParsed(): IEvent { return this._onWriteParsed.event; } - protected _onWriteParsed = new EventEmitter(); + public readonly onLineFeed = this._onLineFeed.event; + private readonly _onResize = new EventEmitter<{ cols: number, rows: number }>(); + public readonly onResize = this._onResize.event; + protected readonly _onWriteParsed = new EventEmitter(); + public readonly onWriteParsed = this._onWriteParsed.event; + /** * Internally we track the source of the scroll but this is meaningless outside the library so * it's filtered out. */ protected _onScrollApi?: EventEmitter; + protected _onScroll = new EventEmitter(); public get onScroll(): IEvent { if (!this._onScrollApi) { this._onScrollApi = new EventEmitter(); @@ -114,8 +113,6 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { this._instantiationService.setService(ICoreService, this.coreService); this.coreMouseService = this._instantiationService.createInstance(CoreMouseService); this._instantiationService.setService(ICoreMouseService, this.coreMouseService); - this._dirtyRowService = this._instantiationService.createInstance(DirtyRowService); - this._instantiationService.setService(IDirtyRowService, this._dirtyRowService); this.unicodeService = this._instantiationService.createInstance(UnicodeService); this._instantiationService.setService(IUnicodeService, this.unicodeService); this._charsetService = this._instantiationService.createInstance(CharsetService); @@ -124,7 +121,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { this._instantiationService.setService(IOscLinkService, this._oscLinkService); // Register input handler and handle/forward events - this._inputHandler = new InputHandler(this._bufferService, this._charsetService, this.coreService, this._dirtyRowService, this._logService, this.optionsService, this._oscLinkService, this.coreMouseService, this.unicodeService); + this._inputHandler = new InputHandler(this._bufferService, this._charsetService, this.coreService, this._logService, this.optionsService, this._oscLinkService, this.coreMouseService, this.unicodeService); this.register(forwardEvent(this._inputHandler.onLineFeed, this._onLineFeed)); this.register(this._inputHandler); @@ -132,14 +129,15 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { this.register(forwardEvent(this._bufferService.onResize, this._onResize)); this.register(forwardEvent(this.coreService.onData, this._onData)); this.register(forwardEvent(this.coreService.onBinary, this._onBinary)); + this.register(this.coreService.onUserInput(() => this._writeBuffer.handleUserInput())); this.register(this.optionsService.onOptionChange(key => this._updateOptions(key))); this.register(this._bufferService.onScroll(event => { this._onScroll.fire({ position: this._bufferService.buffer.ydisp, source: ScrollSource.TERMINAL }); - this._dirtyRowService.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom); + this._inputHandler.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom); })); this.register(this._inputHandler.onScroll(event => { this._onScroll.fire({ position: this._bufferService.buffer.ydisp, source: ScrollSource.TERMINAL }); - this._dirtyRowService.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom); + this._inputHandler.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom); })); // Setup WriteBuffer diff --git a/src/common/Idle.ts b/src/common/Idle.ts deleted file mode 100644 index 529d38a4e..000000000 --- a/src/common/Idle.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Copyright (c) 2022 The xterm.js authors. All rights reserved. - * @license MIT - */ - -/** - * A queue of that runs tasks over several idle callbacks, trying to maintain the specified - * frame rate. The tasks will run in the order they are enqueued, but they will run some time later, - * and care should be taken to ensure they're non-urgent and will not introduce race conditions. - */ -export class IdleTaskQueue { - private _tasks: (() => void)[] = []; - private _idleCallback?: number; - private _maxTaskDuration: number; - private _i = 0; - - /** - * @param targetFps The target frame rate. - */ - constructor(targetFps: number = 240) { - this._maxTaskDuration = 1000 / targetFps; - } - - /** - * Adds a task to the queue which will run in a future idle callback. - */ - public enqueue(task: () => void): void { - this._tasks.push(task); - this._start(); - } - - /** - * Flushes the queue, running all remaining tasks synchronously. - */ - public flush(): void { - while (this._i < this._tasks.length) { - this._tasks[this._i++](); - } - this.clear(); - } - - /** - * Clears any remaining tasks from the queue, these will not be run. - */ - public clear(): void { - if (this._idleCallback) { - cancelIdleCallback(this._idleCallback); - this._idleCallback = undefined; - } - this._i = 0; - this._tasks.length = 0; - } - - private _start(): void { - if (!this._idleCallback) { - this._idleCallback = requestIdleCallback(() => this._process()); - } - } - - private _process(): void { - const start = performance.now(); - this._idleCallback = undefined; - while (this._i < this._tasks.length) { - this._tasks[this._i++](); - if (performance.now() - start > this._maxTaskDuration) { - this._start(); - return; - } - } - this.clear(); - } -} - -export class DebouncedIdleTask { - private _queue: IdleTaskQueue; - - /** - * @param targetFps The target frame rate. - */ - constructor(targetFps: number = 240) { - this._queue = new IdleTaskQueue(targetFps); - } - - public set(task: () => void): void { - this._queue.clear(); - this._queue.enqueue(task); - } - - public flush(): void { - this._queue.flush(); - } -} diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts index f734b002c..d9127e7b2 100644 --- a/src/common/InputHandler.test.ts +++ b/src/common/InputHandler.test.ts @@ -11,7 +11,7 @@ import { CellData } from 'common/buffer/CellData'; import { Attributes, UnderlineStyle } from 'common/buffer/Constants'; import { AttributeData } from 'common/buffer/AttributeData'; import { Params } from 'common/parser/Params'; -import { MockCoreService, MockBufferService, MockDirtyRowService, MockOptionsService, MockLogService, MockCoreMouseService, MockCharsetService, MockUnicodeService, MockOscLinkService } from 'common/TestUtils.test'; +import { MockCoreService, MockBufferService, MockOptionsService, MockLogService, MockCoreMouseService, MockCharsetService, MockUnicodeService, MockOscLinkService } from 'common/TestUtils.test'; import { IBufferService, ICoreService } from 'common/services/Services'; import { DEFAULT_OPTIONS } from 'common/services/OptionsService'; import { clone } from 'common/Clone'; @@ -67,7 +67,7 @@ describe('InputHandler', () => { bufferService.resize(80, 30); coreService = new CoreService(() => { }, bufferService, new MockLogService(), optionsService); - inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); + inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); }); describe('SL/SR/DECIC/DECDC', () => { @@ -236,7 +236,7 @@ describe('InputHandler', () => { describe('setMode', () => { it('should toggle bracketedPasteMode', () => { const coreService = new MockCoreService(); - const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); + const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); // Set bracketed paste mode inputHandler.setModePrivate(Params.fromArray([2004])); assert.equal(coreService.decPrivateModes.bracketedPasteMode, true); @@ -258,7 +258,6 @@ describe('InputHandler', () => { bufferService, new MockCharsetService(), new MockCoreService(), - new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), @@ -305,7 +304,6 @@ describe('InputHandler', () => { bufferService, new MockCharsetService(), new MockCoreService(), - new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), @@ -356,7 +354,6 @@ describe('InputHandler', () => { bufferService, new MockCharsetService(), new MockCoreService(), - new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), @@ -394,7 +391,6 @@ describe('InputHandler', () => { bufferService, new MockCharsetService(), new MockCoreService(), - new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), @@ -445,7 +441,6 @@ describe('InputHandler', () => { bufferService, new MockCharsetService(), new MockCoreService(), - new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), @@ -572,7 +567,6 @@ describe('InputHandler', () => { new MockBufferService(80, 30), new MockCharsetService(), new MockCoreService(), - new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), @@ -599,7 +593,7 @@ describe('InputHandler', () => { beforeEach(() => { bufferService = new MockBufferService(80, 30); - handler = new TestInputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); + handler = new TestInputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); }); it('should handle DECSET/DECRST 47 (alt screen buffer)', async () => { await handler.parseP('\x1b[?47h\r\n\x1b[31mJUNK\x1b[?47lTEST'); @@ -796,7 +790,7 @@ describe('InputHandler', () => { describe('colon notation', () => { let inputHandler2: TestInputHandler; beforeEach(() => { - inputHandler2 = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); + inputHandler2 = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); }); describe('should equal to semicolon', () => { it('CSI 38:2::50:100:150 m', async () => { @@ -2278,7 +2272,7 @@ describe('InputHandler - async handlers', () => { coreService = new CoreService(() => { }, bufferService, new MockLogService(), optionsService); coreService.onData(data => { console.log(data); }); - inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); + inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); }); it('async CUP with CPR check', async () => { diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index b599bb7e4..7f0879efc 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -12,11 +12,11 @@ import { Disposable } from 'common/Lifecycle'; import { StringToUtf32, stringFromCodePoint, Utf8ToUtf32 } from 'common/input/TextDecoder'; import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { EventEmitter, IEvent } from 'common/EventEmitter'; -import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IFunctionIdentifier } from 'common/parser/Types'; +import { IParsingState, IEscapeSequenceParser, IParams, IFunctionIdentifier } from 'common/parser/Types'; import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content, UnderlineStyle } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; import { AttributeData } from 'common/buffer/AttributeData'; -import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, LogLevelEnum, IOscLinkService } from 'common/services/Services'; +import { ICoreService, IBufferService, IOptionsService, ILogService, ICoreMouseService, ICharsetService, IUnicodeService, LogLevelEnum, IOscLinkService } from 'common/services/Services'; import { OscHandler } from 'common/parser/OscParser'; import { DcsHandler } from 'common/parser/DcsParser'; import { IBuffer } from 'common/buffer/Types'; @@ -104,6 +104,8 @@ export enum WindowsOptionsReportType { // create a warning log if an async handler takes longer than the limit (in ms) const SLOW_ASYNC_LIMIT = 5000; +// Work variables to avoid garbage collection +let $temp = 0; /** * The terminal's standard implementation of IInputHandler, this handles all @@ -120,6 +122,7 @@ export class InputHandler extends Disposable implements IInputHandler { private _windowTitle = ''; private _iconName = ''; private _currentLinkId?: number; + private _dirtyRowTracker: IDirtyRowTracker; protected _windowTitleStack: string[] = []; protected _iconNameStack: string[] = []; @@ -129,33 +132,33 @@ export class InputHandler extends Disposable implements IInputHandler { private _activeBuffer: IBuffer; - private _onRequestBell = new EventEmitter(); - public get onRequestBell(): IEvent { return this._onRequestBell.event; } - private _onRequestRefreshRows = new EventEmitter(); - public get onRequestRefreshRows(): IEvent { return this._onRequestRefreshRows.event; } - private _onRequestReset = new EventEmitter(); - public get onRequestReset(): IEvent { return this._onRequestReset.event; } - private _onRequestSendFocus = new EventEmitter(); - public get onRequestSendFocus(): IEvent { return this._onRequestSendFocus.event; } - private _onRequestSyncScrollBar = new EventEmitter(); - public get onRequestSyncScrollBar(): IEvent { return this._onRequestSyncScrollBar.event; } - private _onRequestWindowsOptionsReport = new EventEmitter(); - public get onRequestWindowsOptionsReport(): IEvent { return this._onRequestWindowsOptionsReport.event; } + private readonly _onRequestBell = new EventEmitter(); + public readonly onRequestBell = this._onRequestBell.event; + private readonly _onRequestRefreshRows = new EventEmitter(); + public readonly onRequestRefreshRows = this._onRequestRefreshRows.event; + private readonly _onRequestReset = new EventEmitter(); + public readonly onRequestReset = this._onRequestReset.event; + private readonly _onRequestSendFocus = new EventEmitter(); + public readonly onRequestSendFocus = this._onRequestSendFocus.event; + private readonly _onRequestSyncScrollBar = new EventEmitter(); + public readonly onRequestSyncScrollBar = this._onRequestSyncScrollBar.event; + private readonly _onRequestWindowsOptionsReport = new EventEmitter(); + public readonly onRequestWindowsOptionsReport = this._onRequestWindowsOptionsReport.event; - private _onA11yChar = new EventEmitter(); - public get onA11yChar(): IEvent { return this._onA11yChar.event; } - private _onA11yTab = new EventEmitter(); - public get onA11yTab(): IEvent { return this._onA11yTab.event; } - private _onCursorMove = new EventEmitter(); - public get onCursorMove(): IEvent { return this._onCursorMove.event; } - private _onLineFeed = new EventEmitter(); - public get onLineFeed(): IEvent { return this._onLineFeed.event; } - private _onScroll = new EventEmitter(); - public get onScroll(): IEvent { return this._onScroll.event; } - private _onTitleChange = new EventEmitter(); - public get onTitleChange(): IEvent { return this._onTitleChange.event; } - private _onColor = new EventEmitter(); - public get onColor(): IEvent { return this._onColor.event; } + private readonly _onA11yChar = new EventEmitter(); + public readonly onA11yChar = this._onA11yChar.event; + private readonly _onA11yTab = new EventEmitter(); + public readonly onA11yTab = this._onA11yTab.event; + private readonly _onCursorMove = new EventEmitter(); + public readonly onCursorMove = this._onCursorMove.event; + private readonly _onLineFeed = new EventEmitter(); + public readonly onLineFeed = this._onLineFeed.event; + private readonly _onScroll = new EventEmitter(); + public readonly onScroll = this._onScroll.event; + private readonly _onTitleChange = new EventEmitter(); + public readonly onTitleChange = this._onTitleChange.event; + private readonly _onColor = new EventEmitter(); + public readonly onColor = this._onColor.event; private _parseStack: IParseStack = { paused: false, @@ -169,7 +172,6 @@ export class InputHandler extends Disposable implements IInputHandler { private readonly _bufferService: IBufferService, private readonly _charsetService: ICharsetService, private readonly _coreService: ICoreService, - private readonly _dirtyRowService: IDirtyRowService, private readonly _logService: ILogService, private readonly _optionsService: IOptionsService, private readonly _oscLinkService: IOscLinkService, @@ -179,6 +181,7 @@ export class InputHandler extends Disposable implements IInputHandler { ) { super(); this.register(this._parser); + this._dirtyRowTracker = new DirtyRowTracker(this._bufferService); // Track properties used in performance critical code manually to avoid using slow getters this._activeBuffer = this._bufferService.buffer; @@ -459,7 +462,7 @@ export class InputHandler extends Disposable implements IInputHandler { // Clear the dirty row service so we know which lines changed as a result of parsing // Important: do not clear between async calls, otherwise we lost pending update information. if (!wasPaused) { - this._dirtyRowService.clearRange(); + this._dirtyRowTracker.clearRange(); } // process big data in smaller chunks @@ -493,7 +496,7 @@ export class InputHandler extends Disposable implements IInputHandler { } // Refresh any dirty rows accumulated as part of parsing - this._onRequestRefreshRows.fire(this._dirtyRowService.start, this._dirtyRowService.end); + this._onRequestRefreshRows.fire(this._dirtyRowTracker.start, this._dirtyRowTracker.end); } public print(data: Uint32Array, start: number, end: number): void { @@ -507,7 +510,7 @@ export class InputHandler extends Disposable implements IInputHandler { const curAttr = this._curAttrData; let bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!; - this._dirtyRowService.markDirty(this._activeBuffer.y); + this._dirtyRowTracker.markDirty(this._activeBuffer.y); // handle wide chars: reset start_cell-1 if we would overwrite the second cell of a wide char if (this._activeBuffer.x && end - start > 0 && bufferRow.getWidth(this._activeBuffer.x - 1) === 2) { @@ -635,7 +638,7 @@ export class InputHandler extends Disposable implements IInputHandler { bufferRow.setCellFromCodePoint(this._activeBuffer.x, 0, 1, curAttr.fg, curAttr.bg, curAttr.extended); } - this._dirtyRowService.markDirty(this._activeBuffer.y); + this._dirtyRowTracker.markDirty(this._activeBuffer.y); } /** @@ -699,7 +702,7 @@ export class InputHandler extends Disposable implements IInputHandler { * @vt: #Y C0 FF "Form Feed" "\f, \x0C" "Treated as LF." */ public lineFeed(): boolean { - this._dirtyRowService.markDirty(this._activeBuffer.y); + this._dirtyRowTracker.markDirty(this._activeBuffer.y); if (this._optionsService.rawOptions.convertEol) { this._activeBuffer.x = 0; } @@ -714,7 +717,7 @@ export class InputHandler extends Disposable implements IInputHandler { if (this._activeBuffer.x >= this._bufferService.cols) { this._activeBuffer.x--; } - this._dirtyRowService.markDirty(this._activeBuffer.y); + this._dirtyRowTracker.markDirty(this._activeBuffer.y); this._onLineFeed.fire(); return true; @@ -842,14 +845,14 @@ export class InputHandler extends Disposable implements IInputHandler { this._activeBuffer.y = this._coreService.decPrivateModes.origin ? Math.min(this._activeBuffer.scrollBottom, Math.max(this._activeBuffer.scrollTop, this._activeBuffer.y)) : Math.min(this._bufferService.rows - 1, Math.max(0, this._activeBuffer.y)); - this._dirtyRowService.markDirty(this._activeBuffer.y); + this._dirtyRowTracker.markDirty(this._activeBuffer.y); } /** * Set absolute cursor position. */ private _setCursor(x: number, y: number): void { - this._dirtyRowService.markDirty(this._activeBuffer.y); + this._dirtyRowTracker.markDirty(this._activeBuffer.y); if (this._coreService.decPrivateModes.origin) { this._activeBuffer.x = x; this._activeBuffer.y = this._activeBuffer.scrollTop + y; @@ -858,7 +861,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._activeBuffer.y = y; } this._restrictCursor(); - this._dirtyRowService.markDirty(this._activeBuffer.y); + this._dirtyRowTracker.markDirty(this._activeBuffer.y); } /** @@ -1178,16 +1181,16 @@ export class InputHandler extends Disposable implements IInputHandler { switch (params.params[0]) { case 0: j = this._activeBuffer.y; - this._dirtyRowService.markDirty(j); + this._dirtyRowTracker.markDirty(j); this._eraseInBufferLine(j++, this._activeBuffer.x, this._bufferService.cols, this._activeBuffer.x === 0, respectProtect); for (; j < this._bufferService.rows; j++) { this._resetBufferLine(j, respectProtect); } - this._dirtyRowService.markDirty(j); + this._dirtyRowTracker.markDirty(j); break; case 1: j = this._activeBuffer.y; - this._dirtyRowService.markDirty(j); + this._dirtyRowTracker.markDirty(j); // Deleted front part of line and everything before. This line will no longer be wrapped. this._eraseInBufferLine(j, 0, this._activeBuffer.x + 1, true, respectProtect); if (this._activeBuffer.x + 1 >= this._bufferService.cols) { @@ -1197,15 +1200,15 @@ export class InputHandler extends Disposable implements IInputHandler { while (j--) { this._resetBufferLine(j, respectProtect); } - this._dirtyRowService.markDirty(0); + this._dirtyRowTracker.markDirty(0); break; case 2: j = this._bufferService.rows; - this._dirtyRowService.markDirty(j - 1); + this._dirtyRowTracker.markDirty(j - 1); while (j--) { this._resetBufferLine(j, respectProtect); } - this._dirtyRowService.markDirty(0); + this._dirtyRowTracker.markDirty(0); break; case 3: // Clear scrollback (everything not in viewport) @@ -1257,7 +1260,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._eraseInBufferLine(this._activeBuffer.y, 0, this._bufferService.cols, true, respectProtect); break; } - this._dirtyRowService.markDirty(this._activeBuffer.y); + this._dirtyRowTracker.markDirty(this._activeBuffer.y); return true; } @@ -1289,7 +1292,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._activeBuffer.lines.splice(row, 0, this._activeBuffer.getBlankLine(this._eraseAttrData())); } - this._dirtyRowService.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom); + this._dirtyRowTracker.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom); this._activeBuffer.x = 0; // see https://vt100.net/docs/vt220-rm/chapter4.html - vt220 only? return true; } @@ -1323,7 +1326,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._activeBuffer.lines.splice(j, 0, this._activeBuffer.getBlankLine(this._eraseAttrData())); } - this._dirtyRowService.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom); + this._dirtyRowTracker.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom); this._activeBuffer.x = 0; // see https://vt100.net/docs/vt220-rm/chapter4.html - vt220 only? return true; } @@ -1349,7 +1352,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData() ); - this._dirtyRowService.markDirty(this._activeBuffer.y); + this._dirtyRowTracker.markDirty(this._activeBuffer.y); } return true; } @@ -1375,7 +1378,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData() ); - this._dirtyRowService.markDirty(this._activeBuffer.y); + this._dirtyRowTracker.markDirty(this._activeBuffer.y); } return true; } @@ -1395,7 +1398,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollTop, 1); this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollBottom, 0, this._activeBuffer.getBlankLine(this._eraseAttrData())); } - this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); + this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); return true; } @@ -1411,7 +1414,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollBottom, 1); this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollTop, 0, this._activeBuffer.getBlankLine(DEFAULT_ATTR_DATA)); } - this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); + this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); return true; } @@ -1443,7 +1446,7 @@ export class InputHandler extends Disposable implements IInputHandler { line.deleteCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); line.isWrapped = false; } - this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); + this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); return true; } @@ -1476,7 +1479,7 @@ export class InputHandler extends Disposable implements IInputHandler { line.insertCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); line.isWrapped = false; } - this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); + this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); return true; } @@ -1499,7 +1502,7 @@ export class InputHandler extends Disposable implements IInputHandler { line.insertCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); line.isWrapped = false; } - this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); + this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); return true; } @@ -1522,7 +1525,7 @@ export class InputHandler extends Disposable implements IInputHandler { line.deleteCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); line.isWrapped = false; } - this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); + this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); return true; } @@ -1544,7 +1547,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData() ); - this._dirtyRowService.markDirty(this._activeBuffer.y); + this._dirtyRowTracker.markDirty(this._activeBuffer.y); } return true; } @@ -3220,7 +3223,7 @@ export class InputHandler extends Disposable implements IInputHandler { const scrollRegionHeight = this._activeBuffer.scrollBottom - this._activeBuffer.scrollTop; this._activeBuffer.lines.shiftElements(this._activeBuffer.ybase + this._activeBuffer.y, scrollRegionHeight, 1); this._activeBuffer.lines.set(this._activeBuffer.ybase + this._activeBuffer.y, this._activeBuffer.getBlankLine(this._eraseAttrData())); - this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); + this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); } else { this._activeBuffer.y--; this._restrictCursor(); // quickfix to not run out of bounds @@ -3293,7 +3296,7 @@ export class InputHandler extends Disposable implements IInputHandler { line.isWrapped = false; } } - this._dirtyRowService.markAllDirty(); + this._dirtyRowTracker.markAllDirty(); this._setCursor(0, 0); return true; } @@ -3344,4 +3347,60 @@ export class InputHandler extends Disposable implements IInputHandler { if (data === ' q') return f(`P1$r${STYLES[opts.cursorStyle] - (opts.cursorBlink ? 1 : 0)} q`); return f(`P0$r`); } + + public markRangeDirty(y1: number, y2: number): void { + this._dirtyRowTracker.markRangeDirty(y1, y2); + } +} + +export interface IDirtyRowTracker { + readonly start: number; + readonly end: number; + + clearRange(): void; + markDirty(y: number): void; + markRangeDirty(y1: number, y2: number): void; + markAllDirty(): void; +} + +class DirtyRowTracker implements IDirtyRowTracker { + public start!: number; + public end!: number; + + constructor( + @IBufferService private readonly _bufferService: IBufferService + ) { + this.clearRange(); + } + + public clearRange(): void { + this.start = this._bufferService.buffer.y; + this.end = this._bufferService.buffer.y; + } + + public markDirty(y: number): void { + if (y < this.start) { + this.start = y; + } else if (y > this.end) { + this.end = y; + } + } + + public markRangeDirty(y1: number, y2: number): void { + if (y1 > y2) { + $temp = y1; + y1 = y2; + y2 = $temp; + } + if (y1 < this.start) { + this.start = y1; + } + if (y2 > this.end) { + this.end = y2; + } + } + + public markAllDirty(): void { + this.markRangeDirty(0, this._bufferService.rows - 1); + } } diff --git a/src/common/TaskQueue.ts b/src/common/TaskQueue.ts new file mode 100644 index 000000000..94c5c53b3 --- /dev/null +++ b/src/common/TaskQueue.ts @@ -0,0 +1,147 @@ +/** + * Copyright (c) 2022 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { isNode } from 'common/Platform'; + +interface ITaskQueue { + /** + * Adds a task to the queue which will run in a future idle callback. + */ + enqueue(task: () => void): void; + + /** + * Flushes the queue, running all remaining tasks synchronously. + */ + flush(): void; + + /** + * Clears any remaining tasks from the queue, these will not be run. + */ + clear(): void; +} + +interface ITaskDeadline { + timeRemaining(): number; +} +type CallbackWithDeadline = (deadline: ITaskDeadline) => void; + +abstract class TaskQueue implements ITaskQueue { + private _tasks: (() => void)[] = []; + private _idleCallback?: number; + private _i = 0; + + protected abstract _requestCallback(callback: CallbackWithDeadline): number; + protected abstract _cancelCallback(identifier: number): void; + + public enqueue(task: () => void): void { + this._tasks.push(task); + this._start(); + } + + public flush(): void { + while (this._i < this._tasks.length) { + this._tasks[this._i++](); + } + this.clear(); + } + + public clear(): void { + if (this._idleCallback) { + this._cancelCallback(this._idleCallback); + this._idleCallback = undefined; + } + this._i = 0; + this._tasks.length = 0; + } + + private _start(): void { + if (!this._idleCallback) { + this._idleCallback = this._requestCallback(this._process.bind(this)); + } + } + + private _process(deadline: ITaskDeadline): void { + this._idleCallback = undefined; + let taskDuration = 0; + let longestTask = 0; + while (this._i < this._tasks.length) { + taskDuration = performance.now(); + this._tasks[this._i++](); + taskDuration = performance.now() - taskDuration; + longestTask = Math.max(taskDuration, longestTask); + // Guess the following task will take a similar time to the longest task in this batch, allow + // additional room to try avoid exceeding the deadline + if (longestTask * 1.5 > deadline.timeRemaining()) { + this._start(); + return; + } + } + this.clear(); + } +} + +/** + * A queue of that runs tasks over several tasks via setTimeout, trying to maintain above 60 frames + * per second. The tasks will run in the order they are enqueued, but they will run some time later, + * and care should be taken to ensure they're non-urgent and will not introduce race conditions. + */ +export class PriorityTaskQueue extends TaskQueue { + protected _requestCallback(callback: CallbackWithDeadline): number { + return setTimeout(() => callback(this._createDeadline(16))); + } + + protected _cancelCallback(identifier: number): void { + clearTimeout(identifier); + } + + private _createDeadline(duration: number): ITaskDeadline { + const end = performance.now() + duration; + return { + timeRemaining: () => Math.max(0, end - performance.now()) + }; + } +} + +class IdleTaskQueueInternal extends TaskQueue { + protected _requestCallback(callback: IdleRequestCallback): number { + return requestIdleCallback(callback); + } + + protected _cancelCallback(identifier: number): void { + cancelIdleCallback(identifier); + } +} + +/** + * A queue of that runs tasks over several idle callbacks, trying to respect the idle callback's + * deadline given by the environment. The tasks will run in the order they are enqueued, but they + * will run some time later, and care should be taken to ensure they're non-urgent and will not + * introduce race conditions. + * + * This reverts to a {@link PriorityTaskQueue} if the environment does not support idle callbacks. + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export const IdleTaskQueue = (!isNode && 'requestIdleCallback' in window) ? IdleTaskQueueInternal : PriorityTaskQueue; + +/** + * An object that tracks a single debounced task that will run on the next idle frame. When called + * multiple times, only the last set task will run. + */ +export class DebouncedIdleTask { + private _queue: ITaskQueue; + + constructor() { + this._queue = new IdleTaskQueue(); + } + + public set(task: () => void): void { + this._queue.clear(); + this._queue.enqueue(task); + } + + public flush(): void { + this._queue.flush(); + } +} diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index 8fb71a5d5..e302d3e4b 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum, IDecorationService, IInternalDecoration, IOscLinkService } from 'common/services/Services'; +import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, ICoreMouseService, ICharsetService, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum, IDecorationService, IInternalDecoration, IOscLinkService } from 'common/services/Services'; import { IEvent, EventEmitter } from 'common/EventEmitter'; import { clone } from 'common/Clone'; import { DEFAULT_OPTIONS } from 'common/services/OptionsService'; @@ -100,16 +100,6 @@ export class MockCoreService implements ICoreService { public triggerBinaryEvent(data: string): void { } } -export class MockDirtyRowService implements IDirtyRowService { - public serviceBrand: any; - public start: number = 0; - public end: number = 0; - public clearRange(): void { } - public markDirty(y: number): void { } - public markRangeDirty(y1: number, y2: number): void { } - public markAllDirty(): void { } -} - export class MockLogService implements ILogService { public serviceBrand: any; public logLevel = LogLevelEnum.DEBUG; diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 43e898392..875ac6c97 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -37,10 +37,8 @@ const enum Cell { export const DEFAULT_ATTR_DATA = Object.freeze(new AttributeData()); -/** Work variables to avoid garbage collection. */ -const w: { startIndex: number } = { - startIndex: 0 -}; +// Work variables to avoid garbage collection +let $startIndex = 0; /** * Typed array based bufferline implementation. @@ -178,10 +176,10 @@ export class BufferLine implements IBufferLine { * to GC as it significantly reduced the amount of new objects/references needed. */ public loadCell(index: number, cell: ICellData): ICellData { - w.startIndex = index * CELL_SIZE; - cell.content = this._data[w.startIndex + Cell.CONTENT]; - cell.fg = this._data[w.startIndex + Cell.FG]; - cell.bg = this._data[w.startIndex + Cell.BG]; + $startIndex = index * CELL_SIZE; + cell.content = this._data[$startIndex + Cell.CONTENT]; + cell.fg = this._data[$startIndex + Cell.FG]; + cell.bg = this._data[$startIndex + Cell.BG]; if (cell.content & Content.IS_COMBINED_MASK) { cell.combinedData = this._combined[index]; } diff --git a/src/common/buffer/BufferSet.ts b/src/common/buffer/BufferSet.ts index f940bb8f3..46fcb0979 100644 --- a/src/common/buffer/BufferSet.ts +++ b/src/common/buffer/BufferSet.ts @@ -19,8 +19,8 @@ export class BufferSet extends Disposable implements IBufferSet { private _alt!: Buffer; private _activeBuffer!: Buffer; - private _onBufferActivate = this.register(new EventEmitter<{activeBuffer: IBuffer, inactiveBuffer: IBuffer}>()); - public get onBufferActivate(): IEvent<{activeBuffer: IBuffer, inactiveBuffer: IBuffer}> { return this._onBufferActivate.event; } + private readonly _onBufferActivate = this.register(new EventEmitter<{activeBuffer: IBuffer, inactiveBuffer: IBuffer}>()); + public readonly onBufferActivate = this._onBufferActivate.event; /** * Create a new BufferSet for the given terminal. diff --git a/src/common/buffer/Marker.ts b/src/common/buffer/Marker.ts index 72c4085ca..56d64a726 100644 --- a/src/common/buffer/Marker.ts +++ b/src/common/buffer/Marker.ts @@ -15,8 +15,8 @@ export class Marker extends Disposable implements IMarker { public get id(): number { return this._id; } - private _onDispose = new EventEmitter(); - public get onDispose(): IEvent { return this._onDispose.event; } + private readonly _onDispose = new EventEmitter(); + public readonly onDispose = this._onDispose.event; constructor( public line: number diff --git a/src/common/input/TextDecoder.test.ts b/src/common/input/TextDecoder.test.ts index da1760a24..b8db8a035 100644 --- a/src/common/input/TextDecoder.test.ts +++ b/src/common/input/TextDecoder.test.ts @@ -121,35 +121,42 @@ describe('text encodings', () => { describe('Utf8ToUtf32 decoder', () => { describe('full codepoint test', () => { - - it('0..65535 (1/2/3 byte sequences)', () => { - const decoder = new Utf8ToUtf32(); - const target = new Uint32Array(5); - for (let i = 0; i < 65536; ++i) { - // skip surrogate pairs and a BOM - if ((i >= 0xD800 && i <= 0xDFFF) || i === 0xFEFF) { - continue; + function formatRange(min: number, max: number): string { + return `${min}..${max} (0x${min.toString(16).toUpperCase()}..0x${max.toString(16).toUpperCase()})`; + } + for (let min = 0; min < 65535; min += 10000) { + const max = Math.min(min + 10000, 65536); + it(`${formatRange(min, max)} (1/2/3 byte sequences)`, () => { + const decoder = new Utf8ToUtf32(); + const target = new Uint32Array(5); + for (let i = min; i < max; ++i) { + // skip surrogate pairs and a BOM + if ((i >= 0xD800 && i <= 0xDFFF) || i === 0xFEFF) { + continue; + } + const utf8Data = fromByteString(encode(String.fromCharCode(i))); + const length = decoder.decode(utf8Data, target); + assert.equal(length, 1); + assert.equal(toString(target, length), String.fromCharCode(i)); + decoder.clear(); } - const utf8Data = fromByteString(encode(String.fromCharCode(i))); - const length = decoder.decode(utf8Data, target); - assert.equal(length, 1); - assert.equal(toString(target, length), String.fromCharCode(i)); - decoder.clear(); - } - }); - - it('65536..0x10FFFF (4 byte sequences)', function (): void { - this.timeout(20000); - const decoder = new Utf8ToUtf32(); - const target = new Uint32Array(5); - for (let i = 65536; i < 0x10FFFF; ++i) { - const utf8Data = fromByteString(encode(stringFromCodePoint(i))); - const length = decoder.decode(utf8Data, target); - assert.equal(length, 1); - assert.equal(target[0], i); - decoder.clear(); - } - }); + }); + } + for (let minRaw = 60000; minRaw < 0x10FFFF; minRaw += 10000) { + const min = Math.max(minRaw, 65536); + const max = Math.min(minRaw + 10000, 0x10FFFF); + it(`${formatRange(min, max)} (4 byte sequences)`, function (): void { + const decoder = new Utf8ToUtf32(); + const target = new Uint32Array(5); + for (let i = min; i < max; ++i) { + const utf8Data = fromByteString(encode(stringFromCodePoint(i))); + const length = decoder.decode(utf8Data, target); + assert.equal(length, 1); + assert.equal(target[0], i); + decoder.clear(); + } + }); + } it('0xFEFF(BOM)', () => { const decoder = new Utf8ToUtf32(); diff --git a/src/common/input/WriteBuffer.ts b/src/common/input/WriteBuffer.ts index 67fd751e9..cb40ffa1f 100644 --- a/src/common/input/WriteBuffer.ts +++ b/src/common/input/WriteBuffer.ts @@ -33,12 +33,6 @@ const WRITE_TIMEOUT_MS = 12; */ const WRITE_BUFFER_LENGTH_THRESHOLD = 50; -// queueMicrotask polyfill for nodejs < v11 -const qmt: (cb: () => void) => void = (typeof queueMicrotask === 'undefined') - ? (cb: () => void) => { Promise.resolve().then(cb); } - : queueMicrotask; - - export class WriteBuffer { private _writeBuffer: (string | Uint8Array)[] = []; private _callbacks: ((() => void) | undefined)[] = []; @@ -46,11 +40,17 @@ export class WriteBuffer { private _bufferOffset = 0; private _isSyncWriting = false; private _syncCalls = 0; - public get onWriteParsed(): IEvent { return this._onWriteParsed.event; } - private _onWriteParsed = new EventEmitter(); + private _didUserInput = false; + + private readonly _onWriteParsed = new EventEmitter(); + public readonly onWriteParsed = this._onWriteParsed.event; constructor(private _action: (data: string | Uint8Array, promiseResult?: boolean) => void | Promise) { } + public handleUserInput(): void { + this._didUserInput = true; + } + /** * @deprecated Unreliable, to be removed soon. */ @@ -105,6 +105,19 @@ export class WriteBuffer { // schedule chunk processing for next event loop run if (!this._writeBuffer.length) { this._bufferOffset = 0; + + // If this is the first write call after the user has done some input, + // parse it immediately to minimize input latency, + // otherwise schedule for the next event + if (this._didUserInput) { + this._didUserInput = false; + this._pendingData += data.length; + this._writeBuffer.push(data); + this._callbacks.push(callback); + this._innerWrite(); + return; + } + setTimeout(() => this._innerWrite()); } @@ -194,7 +207,7 @@ export class WriteBuffer { // 2. spawn a promise immediately resolving to `true` // (executed on the same queue, thus properly aligned before continuation happens) result.catch(err => { - qmt(() => {throw err;}); + queueMicrotask(() => {throw err;}); return Promise.resolve(false); }).then(continuation); return; diff --git a/src/common/public/BufferNamespaceApi.ts b/src/common/public/BufferNamespaceApi.ts index d86f6bf5a..033f5955d 100644 --- a/src/common/public/BufferNamespaceApi.ts +++ b/src/common/public/BufferNamespaceApi.ts @@ -11,8 +11,9 @@ import { ICoreTerminal } from 'common/Types'; export class BufferNamespaceApi implements IBufferNamespaceApi { private _normal: BufferApiView; private _alternate: BufferApiView; - private _onBufferChange = new EventEmitter(); - public get onBufferChange(): IEvent { return this._onBufferChange.event; } + + private readonly _onBufferChange = new EventEmitter(); + public readonly onBufferChange = this._onBufferChange.event; constructor(private _core: ICoreTerminal) { this._normal = new BufferApiView(this._core.buffers.normal, 'normal'); diff --git a/src/common/services/BufferService.ts b/src/common/services/BufferService.ts index e3b7dcd87..1bc93041c 100644 --- a/src/common/services/BufferService.ts +++ b/src/common/services/BufferService.ts @@ -6,7 +6,7 @@ import { IBufferService, IOptionsService } from 'common/services/Services'; import { BufferSet } from 'common/buffer/BufferSet'; import { IBufferSet, IBuffer } from 'common/buffer/Types'; -import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { EventEmitter, IEventEmitter, IEvent } from 'common/EventEmitter'; import { Disposable } from 'common/Lifecycle'; import { IAttributeData, IBufferLine, ScrollSource } from 'common/Types'; @@ -22,10 +22,10 @@ export class BufferService extends Disposable implements IBufferService { /** Whether the user is scrolling (locks the scroll position) */ public isUserScrolling: boolean = false; - private _onResize = new EventEmitter<{ cols: number, rows: number }>(); - public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; } - private _onScroll = new EventEmitter(); - public get onScroll(): IEvent { return this._onScroll.event; } + private readonly _onResize = new EventEmitter<{ cols: number, rows: number }>(); + public readonly onResize = this._onResize.event; + private readonly _onScroll = new EventEmitter(); + public readonly onScroll = this._onScroll.event; public get buffer(): IBuffer { return this.buffers.active; } diff --git a/src/common/services/CoreMouseService.ts b/src/common/services/CoreMouseService.ts index 54e991f8c..8c2a24def 100644 --- a/src/common/services/CoreMouseService.ts +++ b/src/common/services/CoreMouseService.ts @@ -170,9 +170,11 @@ export class CoreMouseService implements ICoreMouseService { private _encodings: { [name: string]: CoreMouseEncoding } = {}; private _activeProtocol: string = ''; private _activeEncoding: string = ''; - private _onProtocolChange = new EventEmitter(); private _lastEvent: ICoreMouseEvent | null = null; + private readonly _onProtocolChange = new EventEmitter(); + public readonly onProtocolChange = this._onProtocolChange.event; + constructor( @IBufferService private readonly _bufferService: IBufferService, @ICoreService private readonly _coreService: ICoreService @@ -225,13 +227,6 @@ export class CoreMouseService implements ICoreMouseService { this._lastEvent = null; } - /** - * Event to announce changes in mouse tracking. - */ - public get onProtocolChange(): IEvent { - return this._onProtocolChange.event; - } - /** * Triggers a mouse event to be sent. * diff --git a/src/common/services/CoreService.ts b/src/common/services/CoreService.ts index 20a346031..9282197b3 100644 --- a/src/common/services/CoreService.ts +++ b/src/common/services/CoreService.ts @@ -34,12 +34,12 @@ export class CoreService extends Disposable implements ICoreService { // Circular dependency, this must be unset or memory will leak after Terminal.dispose private _scrollToBottom: (() => void) | undefined; - private _onData = this.register(new EventEmitter()); - public get onData(): IEvent { return this._onData.event; } - private _onUserInput = this.register(new EventEmitter()); - public get onUserInput(): IEvent { return this._onUserInput.event; } - private _onBinary = this.register(new EventEmitter()); - public get onBinary(): IEvent { return this._onBinary.event; } + private readonly _onData = this.register(new EventEmitter()); + public readonly onData = this._onData.event; + private readonly _onUserInput = this.register(new EventEmitter()); + public readonly onUserInput = this._onUserInput.event; + private readonly _onBinary = this.register(new EventEmitter()); + public readonly onBinary = this._onBinary.event; constructor( // TODO: Move this into a service diff --git a/src/common/services/DecorationService.ts b/src/common/services/DecorationService.ts index e5d115a14..522b04dea 100644 --- a/src/common/services/DecorationService.ts +++ b/src/common/services/DecorationService.ts @@ -11,11 +11,9 @@ import { SortedList } from 'common/SortedList'; import { IColor } from 'common/Types'; import { IDecorationOptions, IDecoration, IMarker, IEvent } from 'xterm'; -/** Work variables to avoid garbage collection. */ -const w = { - xmin: 0, - xmax: 0 -}; +// Work variables to avoid garbage collection +let $xmin = 0; +let $xmax = 0; export class DecorationService extends Disposable implements IDecorationService { public serviceBrand: any; @@ -27,10 +25,10 @@ export class DecorationService extends Disposable implements IDecorationService */ private readonly _decorations: SortedList = new SortedList(e => e?.marker.line); - private _onDecorationRegistered = this.register(new EventEmitter()); - public get onDecorationRegistered(): IEvent { return this._onDecorationRegistered.event; } - private _onDecorationRemoved = this.register(new EventEmitter()); - public get onDecorationRemoved(): IEvent { return this._onDecorationRemoved.event; } + private readonly _onDecorationRegistered = this.register(new EventEmitter()); + public readonly onDecorationRegistered = this._onDecorationRegistered.event; + private readonly _onDecorationRemoved = this.register(new EventEmitter()); + public readonly onDecorationRemoved = this._onDecorationRemoved.event; public get decorations(): IterableIterator { return this._decorations.values(); } @@ -76,9 +74,9 @@ export class DecorationService extends Disposable implements IDecorationService public forEachDecorationAtCell(x: number, line: number, layer: 'bottom' | 'top' | undefined, callback: (decoration: IInternalDecoration) => void): void { this._decorations.forEachByKey(line, d => { - w.xmin = d.options.x ?? 0; - w.xmax = w.xmin + (d.options.width ?? 1); - if (x >= w.xmin && x < w.xmax && (!layer || (d.options.layer ?? 'bottom') === layer)) { + $xmin = d.options.x ?? 0; + $xmax = $xmin + (d.options.width ?? 1); + if (x >= $xmin && x < $xmax && (!layer || (d.options.layer ?? 'bottom') === layer)) { callback(d); } }); @@ -99,7 +97,7 @@ class Decoration extends Disposable implements IInternalDecoration { public readonly onRenderEmitter = this.register(new EventEmitter()); public readonly onRender = this.onRenderEmitter.event; - private _onDispose = this.register(new EventEmitter()); + private readonly _onDispose = this.register(new EventEmitter()); public readonly onDispose = this._onDispose.event; private _cachedBg: IColor | undefined | null = null; diff --git a/src/common/services/DirtyRowService.ts b/src/common/services/DirtyRowService.ts deleted file mode 100644 index 1c43b67ef..000000000 --- a/src/common/services/DirtyRowService.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright (c) 2019 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { IBufferService, IDirtyRowService } from 'common/services/Services'; - -export class DirtyRowService implements IDirtyRowService { - public serviceBrand: any; - - private _start!: number; - private _end!: number; - - public get start(): number { return this._start; } - public get end(): number { return this._end; } - - constructor( - @IBufferService private readonly _bufferService: IBufferService - ) { - this.clearRange(); - } - - public clearRange(): void { - this._start = this._bufferService.buffer.y; - this._end = this._bufferService.buffer.y; - } - - public markDirty(y: number): void { - if (y < this._start) { - this._start = y; - } else if (y > this._end) { - this._end = y; - } - } - - public markRangeDirty(y1: number, y2: number): void { - if (y1 > y2) { - const temp = y1; - y1 = y2; - y2 = temp; - } - if (y1 < this._start) { - this._start = y1; - } - if (y2 > this._end) { - this._end = y2; - } - } - - public markAllDirty(): void { - this.markRangeDirty(0, this._bufferService.rows - 1); - } -} diff --git a/src/common/services/InstantiationService.ts b/src/common/services/InstantiationService.ts index 8280948ac..375e442de 100644 --- a/src/common/services/InstantiationService.ts +++ b/src/common/services/InstantiationService.ts @@ -29,7 +29,9 @@ export class ServiceCollection { } public forEach(callback: (id: IServiceIdentifier, instance: any) => any): void { - this._entries.forEach((value, key) => callback(key, value)); + for (const [key, value] of this._entries.entries()) { + callback(key, value); + } } public has(id: IServiceIdentifier): boolean { diff --git a/src/common/services/OptionsService.test.ts b/src/common/services/OptionsService.test.ts index a65cb986b..3ff22ac09 100644 --- a/src/common/services/OptionsService.test.ts +++ b/src/common/services/OptionsService.test.ts @@ -59,7 +59,7 @@ describe('OptionsService', () => { }); it('normalizes invalid fontWeight option values', () => { service.options.fontWeight = 350; - assert.doesNotThrow(() => service.options.fontWeight = 10000), 'fontWeight should be normalized instead of throwing'; + assert.doesNotThrow(() => service.options.fontWeight = 10000, 'fontWeight should be normalized instead of throwing'); assert.equal(service.options.fontWeight, DEFAULT_OPTIONS.fontWeight, 'Values greater than 1000 should be reset to default'); service.options.fontWeight = 350; diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index c7e8d294f..33aa3ee23 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -57,8 +57,8 @@ export class OptionsService implements IOptionsService { public readonly rawOptions: Required; public options: Required; - private _onOptionChange = new EventEmitter(); - public get onOptionChange(): IEvent { return this._onOptionChange.event; } + private readonly _onOptionChange = new EventEmitter(); + public readonly onOptionChange = this._onOptionChange.event; constructor(options: Partial) { // set the default value of each option diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 22edad1dd..e2b517cde 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -122,19 +122,6 @@ export interface ICharsetService { setgCharset(g: number, charset: ICharset | undefined): void; } -export const IDirtyRowService = createDecorator('DirtyRowService'); -export interface IDirtyRowService { - serviceBrand: undefined; - - readonly start: number; - readonly end: number; - - clearRange(): void; - markDirty(y: number): void; - markRangeDirty(y1: number, y2: number): void; - markAllDirty(): void; -} - export interface IServiceIdentifier { (...args: any[]): void; type: T; diff --git a/src/common/services/UnicodeService.ts b/src/common/services/UnicodeService.ts index e96b75793..239f4d622 100644 --- a/src/common/services/UnicodeService.ts +++ b/src/common/services/UnicodeService.ts @@ -13,8 +13,9 @@ export class UnicodeService implements IUnicodeService { private _providers: {[key: string]: IUnicodeVersionProvider} = Object.create(null); private _active: string = ''; private _activeProvider: IUnicodeVersionProvider; - private _onChange = new EventEmitter(); - public get onChange(): IEvent { return this._onChange.event; } + + private readonly _onChange = new EventEmitter(); + public readonly onChange = this._onChange.event; constructor() { const defaultProvider = new UnicodeV6(); diff --git a/src/headless/Terminal.ts b/src/headless/Terminal.ts index 1cad0ee2d..639988ebd 100644 --- a/src/headless/Terminal.ts +++ b/src/headless/Terminal.ts @@ -32,17 +32,16 @@ export class Terminal extends CoreTerminal { // TODO: We should remove options once components adopt optionsService public get options(): Required { return this.optionsService.options; } - private _onBell = new EventEmitter(); - public get onBell(): IEvent { return this._onBell.event; } - private _onCursorMove = new EventEmitter(); - public get onCursorMove(): IEvent { return this._onCursorMove.event; } - private _onTitleChange = new EventEmitter(); - public get onTitleChange(): IEvent { return this._onTitleChange.event; } - - private _onA11yCharEmitter = new EventEmitter(); - public get onA11yChar(): IEvent { return this._onA11yCharEmitter.event; } - private _onA11yTabEmitter = new EventEmitter(); - public get onA11yTab(): IEvent { return this._onA11yTabEmitter.event; } + private readonly _onBell = new EventEmitter(); + public readonly onBell = this._onBell.event; + private readonly _onCursorMove = new EventEmitter(); + public readonly onCursorMove = this._onCursorMove.event; + private readonly _onTitleChange = new EventEmitter(); + public readonly onTitleChange = this._onTitleChange.event; + private readonly _onA11yCharEmitter = new EventEmitter(); + public readonly onA11yChar = this._onA11yCharEmitter.event; + private readonly _onA11yTabEmitter = new EventEmitter(); + public readonly onA11yTab = this._onA11yTabEmitter.event; /** * Creates a new `Terminal` object. diff --git a/test/api/InputHandler.api.ts b/test/api/InputHandler.api.ts index c8cbbf665..8192bfed3 100644 --- a/test/api/InputHandler.api.ts +++ b/test/api/InputHandler.api.ts @@ -6,7 +6,7 @@ import { assert } from 'chai'; import { pollFor, openTerminal, getBrowserType, launchBrowser, writeSync } from './TestUtils'; import { Browser, Page } from 'playwright'; -import { IRenderDimensions } from 'browser/renderer/Types'; +import { IRenderDimensions } from 'browser/renderer/shared/Types'; const APP = 'http://127.0.0.1:3001/test'; diff --git a/test/api/MouseTracking.api.ts b/test/api/MouseTracking.api.ts index d240738f1..1b00fee5f 100644 --- a/test/api/MouseTracking.api.ts +++ b/test/api/MouseTracking.api.ts @@ -572,7 +572,7 @@ describe('Mouse Tracking Tests', async () => { await page.keyboard.up('Control'); await pollFor(page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: true, shift: false, meta: false } } }, - { col: 45, row: 25, state: { action: 'release', button: '', modifier: { control: true, shift: false, meta: false } } }, + { col: 45, row: 25, state: { action: 'release', button: '', modifier: { control: true, shift: false, meta: false } } } // { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: false } } } ]); @@ -587,7 +587,7 @@ describe('Mouse Tracking Tests', async () => { await page.keyboard.up('Alt'); await pollFor(page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: true } } }, - { col: 45, row: 25, state: { action: 'release', button: '', modifier: { control: false, shift: false, meta: true } } }, + { col: 45, row: 25, state: { action: 'release', button: '', modifier: { control: false, shift: false, meta: true } } } // { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: true } } } ]); @@ -630,7 +630,7 @@ describe('Mouse Tracking Tests', async () => { // await page.keyboard.up('Shift'); await pollFor(page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: true, shift: false, meta: true } } }, - { col: 45, row: 25, state: { action: 'release', button: '', modifier: { control: true, shift: false, meta: true } } }, + { col: 45, row: 25, state: { action: 'release', button: '', modifier: { control: true, shift: false, meta: true } } } // { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: true } } } ]); }); @@ -722,7 +722,7 @@ describe('Mouse Tracking Tests', async () => { await page.keyboard.up('Control'); await pollFor(page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: true, shift: false, meta: false } } }, - { col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: true, shift: false, meta: false } } }, + { col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: true, shift: false, meta: false } } } // { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: false } } } ]); @@ -737,7 +737,7 @@ describe('Mouse Tracking Tests', async () => { await page.keyboard.up('Alt'); await pollFor(page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: true } } }, - { col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: false, shift: false, meta: true } } }, + { col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: false, shift: false, meta: true } } } // { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: true } } } ]); @@ -779,7 +779,7 @@ describe('Mouse Tracking Tests', async () => { // await page.keyboard.up('Shift'); await pollFor(page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: true, shift: false, meta: true } } }, - { col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: true, shift: false, meta: true } } }, + { col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: true, shift: false, meta: true } } } // { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: true } } } ]); }); @@ -884,7 +884,7 @@ describe('Mouse Tracking Tests', async () => { await pollFor(page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: true, shift: false, meta: false } } }, { col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: true, shift: false, meta: false } } }, - { col: 45, row: 25, state: { action: 'release', button: '', modifier: { control: true, shift: false, meta: false } } }, + { col: 45, row: 25, state: { action: 'release', button: '', modifier: { control: true, shift: false, meta: false } } } // { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: false } } } ]); @@ -900,7 +900,7 @@ describe('Mouse Tracking Tests', async () => { await pollFor(page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: true } } }, { col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: false, shift: false, meta: true } } }, - { col: 45, row: 25, state: { action: 'release', button: '', modifier: { control: false, shift: false, meta: true } } }, + { col: 45, row: 25, state: { action: 'release', button: '', modifier: { control: false, shift: false, meta: true } } } // { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: true } } } ]); @@ -944,7 +944,7 @@ describe('Mouse Tracking Tests', async () => { await pollFor(page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: true, shift: false, meta: true } } }, { col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: true, shift: false, meta: true } } }, - { col: 45, row: 25, state: { action: 'release', button: '', modifier: { control: true, shift: false, meta: true } } }, + { col: 45, row: 25, state: { action: 'release', button: '', modifier: { control: true, shift: false, meta: true } } } // { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: true } } } ]); }); @@ -1040,7 +1040,7 @@ describe('Mouse Tracking Tests', async () => { await pollFor(page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: true, shift: false, meta: false } } }, { col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: true, shift: false, meta: false } } }, - { col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: true, shift: false, meta: false } } }, + { col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: true, shift: false, meta: false } } } // { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: false } } } ]); @@ -1056,7 +1056,7 @@ describe('Mouse Tracking Tests', async () => { await pollFor(page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: true } } }, { col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: false, shift: false, meta: true } } }, - { col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: false, shift: false, meta: true } } }, + { col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: false, shift: false, meta: true } } } // { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: true } } } ]); @@ -1100,7 +1100,7 @@ describe('Mouse Tracking Tests', async () => { await pollFor(page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: true, shift: false, meta: true } } }, { col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: true, shift: false, meta: true } } }, - { col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: true, shift: false, meta: true } } }, + { col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: true, shift: false, meta: true } } } // { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: true } } } ]); }); @@ -1205,7 +1205,7 @@ describe('Mouse Tracking Tests', async () => { { col: 44, row: 25, state: { action: 'move', button: '', modifier: { control: true, shift: false, meta: false } } }, { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: true, shift: false, meta: false } } }, { col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: true, shift: false, meta: false } } }, - { col: 45, row: 25, state: { action: 'release', button: '', modifier: { control: true, shift: false, meta: false } } }, + { col: 45, row: 25, state: { action: 'release', button: '', modifier: { control: true, shift: false, meta: false } } } // { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: false } } } ]); @@ -1221,7 +1221,7 @@ describe('Mouse Tracking Tests', async () => { { col: 44, row: 25, state: { action: 'move', button: '', modifier: { control: false, shift: false, meta: true } } }, { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: true } } }, { col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: false, shift: false, meta: true } } }, - { col: 45, row: 25, state: { action: 'release', button: '', modifier: { control: false, shift: false, meta: true } } }, + { col: 45, row: 25, state: { action: 'release', button: '', modifier: { control: false, shift: false, meta: true } } } // { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: true } } } ]); @@ -1266,7 +1266,7 @@ describe('Mouse Tracking Tests', async () => { { col: 44, row: 25, state: { action: 'move', button: '', modifier: { control: true, shift: false, meta: true } } }, { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: true, shift: false, meta: true } } }, { col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: true, shift: false, meta: true } } }, - { col: 45, row: 25, state: { action: 'release', button: '', modifier: { control: true, shift: false, meta: true } } }, + { col: 45, row: 25, state: { action: 'release', button: '', modifier: { control: true, shift: false, meta: true } } } // { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: true } } } ]); }); @@ -1366,7 +1366,7 @@ describe('Mouse Tracking Tests', async () => { { col: 44, row: 25, state: { action: 'move', button: '', modifier: { control: true, shift: false, meta: false } } }, { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: true, shift: false, meta: false } } }, { col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: true, shift: false, meta: false } } }, - { col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: true, shift: false, meta: false } } }, + { col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: true, shift: false, meta: false } } } // { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: false } } } ]); @@ -1382,7 +1382,7 @@ describe('Mouse Tracking Tests', async () => { { col: 44, row: 25, state: { action: 'move', button: '', modifier: { control: false, shift: false, meta: true } } }, { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: true } } }, { col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: false, shift: false, meta: true } } }, - { col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: false, shift: false, meta: true } } }, + { col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: false, shift: false, meta: true } } } // { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: true } } } ]); @@ -1427,7 +1427,7 @@ describe('Mouse Tracking Tests', async () => { { col: 44, row: 25, state: { action: 'move', button: '', modifier: { control: true, shift: false, meta: true } } }, { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: true, shift: false, meta: true } } }, { col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: true, shift: false, meta: true } } }, - { col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: true, shift: false, meta: true } } }, + { col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: true, shift: false, meta: true } } } // { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: true } } } ]); });