diff --git a/.eslintrc.json b/.eslintrc.json index 9510fc6b..e6db42e2 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", @@ -95,7 +96,19 @@ { "selector": "enumMember", "format": ["UPPER_CASE"] }, // memberLike - Allow enum-like objects to use UPPER_CASE { "selector": "property", "modifiers": ["public"], "format": ["camelCase", "UPPER_CASE"] }, - { "selector": "method", "modifiers": ["public"], "format": ["camelCase", "UPPER_CASE"] }, + // restrict on* naming for events only + { "selector": "method", "modifiers": ["public"], "format": ["camelCase", "UPPER_CASE"], "custom": { + "regex": "^on[A-Z].+", + "match": false + } }, + { "selector": "method", "modifiers": ["private"], "format": ["camelCase"], "leadingUnderscore": "require", "custom": { + "regex": "^on[A-Z].+", + "match": false + } }, + { "selector": "method", "modifiers": ["protected"], "format": ["camelCase"], "leadingUnderscore": "require", "custom": { + "regex": "^on[A-Z].+", + "match": false + } }, // typeLike { "selector": "typeLike", "format": ["PascalCase"] }, { "selector": "interface", "format": ["PascalCase"], "prefix": ["I"] } diff --git a/README.md b/README.md index 5c4d2084..c78ae76e 100644 --- a/README.md +++ b/README.md @@ -221,6 +221,6 @@ Do you use xterm.js in your application as well? Please [open a Pull Request](ht 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 f37ea1b8..8f203400 100644 --- a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts @@ -3,25 +3,25 @@ * @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 { IColorSet } from 'browser/Types'; -import { CellData } from 'common/buffer/CellData'; -import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; -import { ICoreBrowserService } from 'browser/services/Services'; -import { excludeFromContrastRatioDemands, throwIfFalsy } from 'browser/renderer/RendererUtils'; -import { channels, color, rgba } from 'common/Color'; import { removeElementFromParent } from 'browser/Dom'; -import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs'; +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, IThemeService } from 'browser/services/Services'; +import { IColorSet, ReadonlyColorSet } from 'browser/Types'; +import { CellData } from 'common/buffer/CellData'; +import { WHITESPACE_CELL_CODE } from 'common/buffer/Constants'; +import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; +import { ICellData } from 'common/Types'; +import { Terminal } from 'xterm'; +import { IRenderLayer } from './Types'; +import { CellColorResolver } from 'browser/renderer/shared/CellColorResolver'; +import { Disposable, toDisposable } from 'common/Lifecycle'; -export abstract class BaseRenderLayer implements IRenderLayer { +export abstract class BaseRenderLayer extends Disposable implements IRenderLayer { private _canvas: HTMLCanvasElement; protected _ctx!: CanvasRenderingContext2D; private _scaledCharWidth: number = 0; @@ -31,49 +31,44 @@ 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 _themeService: IThemeService, protected readonly _bufferService: IBufferService, protected readonly _optionsService: IOptionsService, protected readonly _decorationService: IDecorationService, protected readonly _coreBrowserService: ICoreBrowserService ) { + super(); + this._cellColorResolver = new CellColorResolver(this._terminal, this._selectionModel, this._decorationService, this._coreBrowserService, this._themeService); 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._themeService.colors); + this.register(this._themeService.onChangeColors(e => { + this._refreshCharAtlas(e); + this.reset(); + })); - public dispose(): void { - removeElementFromParent(this._canvas); - this._charAtlas?.dispose(); + this.register(toDisposable(() => { + removeElementFromParent(this._canvas); + this._charAtlas?.dispose(); + })); } private _initCanvas(): void { @@ -84,20 +79,13 @@ export abstract class BaseRenderLayer implements IRenderLayer { } } - public onOptionsChanged(): void {} - public onBlur(): void {} - public onFocus(): void {} - public onCursorMove(): void {} - public onGridChanged(startRow: number, endRow: number): void {} + public handleBlur(): void {} + public handleFocus(): void {} + public handleCursorMove(): void {} + public handleGridChanged(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; - } - - public setColors(colorSet: IColorSet): void { - this._refreshCharAtlas(colorSet); + public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void { + this._selectionModel.update(this._terminal, start, end, columnSelectMode); } protected _setTransparency(alpha: boolean): void { @@ -115,20 +103,21 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._container.replaceChild(this._canvas, oldCanvas); // Regenerate char atlas and force a full redraw - this._refreshCharAtlas(this._colors); - this.onGridChanged(0, this._bufferService.rows - 1); + this._refreshCharAtlas(this._themeService.colors); + this.handleGridChanged(0, this._bufferService.rows - 1); } /** * Refreshes the char atlas, aquiring a new one if necessary. * @param colorSet The color set to use for the char atlas. */ - private _refreshCharAtlas(colorSet: IColorSet): void { + private _refreshCharAtlas(colorSet: ReadonlyColorSet): void { 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 { @@ -148,13 +137,13 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._clearAll(); } - this._refreshCharAtlas(this._colors); + this._refreshCharAtlas(this._themeService.colors); } public abstract reset(): void; public clearTextureAtlas(): void { - this._charAtlas?.clear(); + this._charAtlas?.clearTexture(); } /** @@ -304,7 +293,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { if (this._alpha) { this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); } else { - this._ctx.fillStyle = this._colors.background.css; + this._ctx.fillStyle = this._themeService.colors.background.css; this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height); } } @@ -324,7 +313,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { width * this._scaledCellWidth, height * this._scaledCellHeight); } else { - this._ctx.fillStyle = this._colors.background.css; + this._ctx.fillStyle = this._themeService.colors.background.css; this._ctx.fillRect( x * this._scaledCellWidth, y * this._scaledCellHeight, @@ -365,134 +354,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; - } - - 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 the image, use the bitmap if it's available + if (this._charAtlas.hasCanvasChanged) { + this._bitmapGenerator?.refresh(); + this._charAtlas.hasCanvasChanged = false; } - - // 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); - } - + 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 +409,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 9fca00a6..e39b56f1 100644 --- a/addons/xterm-addon-canvas/src/CanvasAddon.ts +++ b/addons/xterm-addon-canvas/src/CanvasAddon.ts @@ -3,45 +3,57 @@ * @license MIT */ -import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService } from 'browser/services/Services'; -import { IColorSet } from 'browser/Types'; +import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services'; +import { IColorSet, ITerminal } 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'; +import { Disposable, toDisposable } from 'common/Lifecycle'; -export class CanvasAddon implements ITerminalAddon { +export class CanvasAddon extends Disposable implements ITerminalAddon { private _terminal?: Terminal; private _renderer?: CanvasRenderer; - public activate(terminal: Terminal): void { - if (!terminal.element) { - throw new Error('Cannot activate CanvasAddon before Terminal.open'); - } - this._terminal = terminal; - const bufferService: IBufferService = (terminal as any)._core._bufferService; - const renderService: IRenderService = (terminal as any)._core._renderService; - const characterJoinerService: ICharacterJoinerService = (terminal as any)._core._characterJoinerService; - const charSizeService: ICharSizeService = (terminal as any)._core._charSizeService; - const coreService: ICoreService = (terminal as any)._core.coreService; - const coreBrowserService: ICoreBrowserService = (terminal as any)._core._coreBrowserService; - const decorationService: IDecorationService = (terminal as any)._core._decorationService; - const optionsService: IOptionsService = (terminal as any)._core.optionsService; - const colors: IColorSet = (terminal as any)._core._colorManager.colors; - const screenElement: HTMLElement = (terminal as any)._core.screenElement; - const linkifier = (terminal as any)._core.linkifier2; - this._renderer = new CanvasRenderer(colors, screenElement, linkifier, bufferService, charSizeService, optionsService, characterJoinerService, coreService, coreBrowserService, decorationService); - renderService.setRenderer(this._renderer); - renderService.onResize(bufferService.cols, bufferService.rows); + private readonly _onChangeTextureAtlas = this.register(new EventEmitter()); + public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event; + + public get textureAtlas(): HTMLCanvasElement | undefined { + return this._renderer?.textureAtlas; } - public dispose(): void { - if (!this._terminal) { - throw new Error('Cannot dispose CanvasAddon because it is activated'); + public activate(terminal: Terminal): void { + const core = (terminal as any)._core as ITerminal; + if (!terminal.element) { + this.register(core.onWillOpen(() => this.activate(terminal))); + return; } - const renderService: IRenderService = (this._terminal as any)._core._renderService; - renderService.setRenderer((this._terminal as any)._core._createRenderer()); - renderService.onResize(this._terminal.cols, this._terminal.rows); - this._renderer?.dispose(); - this._renderer = undefined; + + this._terminal = terminal; + const coreService = core.coreService; + const optionsService = core.optionsService; + const screenElement = core.screenElement!; + const linkifier = core.linkifier2; + + const unsafeCore = core as any; + const bufferService: IBufferService = unsafeCore._bufferService; + const renderService: IRenderService = unsafeCore._renderService; + const characterJoinerService: ICharacterJoinerService = unsafeCore._characterJoinerService; + const charSizeService: ICharSizeService = unsafeCore._charSizeService; + const coreBrowserService: ICoreBrowserService = unsafeCore._coreBrowserService; + const decorationService: IDecorationService = unsafeCore._decorationService; + const themeService: IThemeService = unsafeCore._themeService; + + this._renderer = new CanvasRenderer(terminal, screenElement, linkifier, bufferService, charSizeService, optionsService, characterJoinerService, coreService, coreBrowserService, decorationService, themeService); + this.register(forwardEvent(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas)); + renderService.setRenderer(this._renderer); + renderService.handleResize(bufferService.cols, bufferService.rows); + + this.register(toDisposable(() => { + renderService.setRenderer((this._terminal as any)._core._createRenderer()); + renderService.handleResize(terminal.cols, terminal.rows); + this._renderer?.dispose(); + this._renderer = undefined; + })); } } diff --git a/addons/xterm-addon-canvas/src/CanvasRenderer.ts b/addons/xterm-addon-canvas/src/CanvasRenderer.ts index b642efbc..b657270f 100644 --- a/addons/xterm-addon-canvas/src/CanvasRenderer.ts +++ b/addons/xterm-addon-canvas/src/CanvasRenderer.ts @@ -3,35 +3,34 @@ * @license MIT */ -import { TextRenderLayer } from './TextRenderLayer'; -import { SelectionRenderLayer } from './SelectionRenderLayer'; +import { removeTerminalFromCache } from 'browser/renderer/shared/CharAtlasCache'; +import { observeDevicePixelDimensions } from 'browser/renderer/shared/DevicePixelObserver'; +import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types'; +import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IThemeService } from 'browser/services/Services'; +import { IColorSet, ILinkifier2, ReadonlyColorSet } from 'browser/Types'; +import { EventEmitter } from 'common/EventEmitter'; +import { Disposable, toDisposable } from 'common/Lifecycle'; +import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; +import { Terminal } from 'xterm'; 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 { ICharacterJoinerService, ICharSizeService, ICoreBrowserService } from 'browser/services/Services'; -import { IBufferService, IOptionsService, IDecorationService, ICoreService } from 'common/services/Services'; -import { removeTerminalFromCache } from './atlas/CharAtlasCache'; -import { EventEmitter, IEvent } from 'common/EventEmitter'; -import { observeDevicePixelDimensions } from 'browser/renderer/DevicePixelObserver'; - -let nextRendererId = 1; +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 = this.register(new EventEmitter()); + public readonly onRequestRedraw = this._onRequestRedraw.event; + private readonly _onChangeTextureAtlas = this.register(new EventEmitter()); + public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event; constructor( - private _colors: IColorSet, + private readonly _terminal: Terminal, private readonly _screenElement: HTMLElement, linkifier2: ILinkifier2, private readonly _bufferService: IBufferService, @@ -40,15 +39,16 @@ export class CanvasRenderer extends Disposable implements IRenderer { characterJoinerService: ICharacterJoinerService, coreService: ICoreService, private readonly _coreBrowserService: ICoreBrowserService, - decorationService: IDecorationService + decorationService: IDecorationService, + private readonly _themeService: IThemeService ) { 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, allowTransparency, this._bufferService, this._optionsService, characterJoinerService, decorationService, this._coreBrowserService, _themeService), + new SelectionRenderLayer(this._terminal, this._screenElement, 1, this._bufferService, this._coreBrowserService, decorationService, this._optionsService, _themeService), + new LinkRenderLayer(this._terminal, this._screenElement, 2, linkifier2, this._bufferService, this._optionsService, decorationService, this._coreBrowserService, _themeService), + new CursorRenderLayer(this._terminal, this._screenElement, 3, this._onRequestRedraw, this._bufferService, this._optionsService, coreService, this._coreBrowserService, decorationService, _themeService) ]; this.dimensions = { scaledCharWidth: 0, @@ -68,37 +68,28 @@ export class CanvasRenderer extends Disposable implements IRenderer { this._updateDimensions(); this.register(observeDevicePixelDimensions(this._renderLayers[0].canvas, this._coreBrowserService.window, (w, h) => this._setCanvasDevicePixelDimensions(w, h))); - - this.onOptionsChanged(); + this.register(toDisposable(() => { + for (const l of this._renderLayers) { + l.dispose(); + } + removeTerminalFromCache(this._terminal); + })); } - public dispose(): void { - for (const l of this._renderLayers) { - l.dispose(); - } - super.dispose(); - removeTerminalFromCache(this._id); + public get textureAtlas(): HTMLCanvasElement | undefined { + return this._renderLayers[0].cacheCanvas; } - public onDevicePixelRatioChange(): void { + public handleDevicePixelRatioChange(): void { // If the device pixel ratio changed, the char atlas needs to be regenerated // and the terminal needs to refreshed if (this._devicePixelRatio !== this._coreBrowserService.dpr) { this._devicePixelRatio = this._coreBrowserService.dpr; - this.onResize(this._bufferService.cols, this._bufferService.rows); + this.handleResize(this._bufferService.cols, this._bufferService.rows); } } - public setColors(colors: IColorSet): void { - this._colors = colors; - // Clear layers and force a full render - for (const l of this._renderLayers) { - l.setColors(this._colors); - l.reset(); - } - } - - public onResize(cols: number, rows: number): void { + public handleResize(cols: number, rows: number): void { // Update character and canvas dimensions this._updateDimensions(); @@ -112,32 +103,28 @@ export class CanvasRenderer extends Disposable implements IRenderer { this._screenElement.style.height = `${this.dimensions.canvasHeight}px`; } - public onCharSizeChanged(): void { - this.onResize(this._bufferService.cols, this._bufferService.rows); + public handleCharSizeChanged(): void { + this.handleResize(this._bufferService.cols, this._bufferService.rows); } - public onBlur(): void { - this._runOperation(l => l.onBlur()); + public handleBlur(): void { + this._runOperation(l => l.handleBlur()); } - public onFocus(): void { - this._runOperation(l => l.onFocus()); + public handleFocus(): void { + this._runOperation(l => l.handleFocus()); } - public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void { - this._runOperation(l => l.onSelectionChanged(start, end, columnSelectMode)); + public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void { + this._runOperation(l => l.handleSelectionChanged(start, end, columnSelectMode)); // Selection foreground requires a full re-render - if (this._colors.selectionForeground) { + if (this._themeService.colors.selectionForeground) { this._onRequestRedraw.fire({ start: 0, end: this._bufferService.rows - 1 }); } } - public onCursorMove(): void { - this._runOperation(l => l.onCursorMove()); - } - - public onOptionsChanged(): void { - this._runOperation(l => l.onOptionsChanged()); + public handleCursorMove(): void { + this._runOperation(l => l.handleCursorMove()); } public clear(): void { @@ -156,7 +143,7 @@ export class CanvasRenderer extends Disposable implements IRenderer { */ public renderRows(start: number, end: number): void { for (const l of this._renderLayers) { - l.onGridChanged(start, end); + l.handleGridChanged(start, end); } } diff --git a/addons/xterm-addon-canvas/src/CursorRenderLayer.ts b/addons/xterm-addon-canvas/src/CursorRenderLayer.ts index 61e2d934..83806697 100644 --- a/addons/xterm-addon-canvas/src/CursorRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/CursorRenderLayer.ts @@ -3,14 +3,16 @@ * @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'; -import { IColorSet } from 'browser/Types'; +import { IColorSet, ReadonlyColorSet } from 'browser/Types'; import { IBufferService, IOptionsService, ICoreService, IDecorationService } from 'common/services/Services'; import { IEventEmitter } from 'common/EventEmitter'; -import { ICoreBrowserService } from 'browser/services/Services'; +import { ICoreBrowserService, IThemeService } from 'browser/services/Services'; +import { Terminal } from 'xterm'; +import { toDisposable } from 'common/Lifecycle'; interface ICursorState { x: number; @@ -32,18 +34,18 @@ 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, private readonly _coreService: ICoreService, coreBrowserService: ICoreBrowserService, - decorationService: IDecorationService + decorationService: IDecorationService, + themeService: IThemeService ) { - super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService, coreBrowserService); + super(terminal, container, 'cursor', zIndex, true, themeService, bufferService, optionsService, decorationService, coreBrowserService); this._state = { x: 0, y: 0, @@ -56,14 +58,11 @@ export class CursorRenderLayer extends BaseRenderLayer { 'block': this._renderBlockCursor.bind(this), 'underline': this._renderUnderlineCursor.bind(this) }; - } - - public dispose(): void { - if (this._cursorBlinkStateManager) { - this._cursorBlinkStateManager.dispose(); + this.register(optionsService.onOptionChange(() => this._handleOptionsChanged())); + this.register(toDisposable(() => { + this._cursorBlinkStateManager?.dispose(); this._cursorBlinkStateManager = undefined; - } - super.dispose(); + })); } public resize(dim: IRenderDimensions): void { @@ -81,20 +80,20 @@ export class CursorRenderLayer extends BaseRenderLayer { public reset(): void { this._clearCursor(); this._cursorBlinkStateManager?.restartBlinkAnimation(); - this.onOptionsChanged(); + this._handleOptionsChanged(); } - public onBlur(): void { + public handleBlur(): void { this._cursorBlinkStateManager?.pause(); this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y }); } - public onFocus(): void { + public handleFocus(): void { this._cursorBlinkStateManager?.resume(); this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y }); } - public onOptionsChanged(): void { + private _handleOptionsChanged(): void { if (this._optionsService.rawOptions.cursorBlink) { if (!this._cursorBlinkStateManager) { this._cursorBlinkStateManager = new CursorBlinkStateManager(this._coreBrowserService.isFocused, () => { @@ -110,11 +109,11 @@ export class CursorRenderLayer extends BaseRenderLayer { this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y }); } - public onCursorMove(): void { + public handleCursorMove(): void { this._cursorBlinkStateManager?.restartBlinkAnimation(); } - public onGridChanged(startRow: number, endRow: number): void { + public handleGridChanged(startRow: number, endRow: number): void { if (!this._cursorBlinkStateManager || this._cursorBlinkStateManager.isPaused) { this._render(false); } else { @@ -148,7 +147,7 @@ export class CursorRenderLayer extends BaseRenderLayer { if (!this._coreBrowserService.isFocused) { this._clearCursor(); this._ctx.save(); - this._ctx.fillStyle = this._colors.cursor.css; + this._ctx.fillStyle = this._themeService.colors.cursor.css; const cursorStyle = this._optionsService.rawOptions.cursorStyle; if (cursorStyle && cursorStyle !== 'block') { this._cursorRenderers[cursorStyle](cursorX, viewportRelativeCursorY, this._cell); @@ -213,30 +212,30 @@ export class CursorRenderLayer extends BaseRenderLayer { private _renderBarCursor(x: number, y: number, cell: ICellData): void { this._ctx.save(); - this._ctx.fillStyle = this._colors.cursor.css; + this._ctx.fillStyle = this._themeService.colors.cursor.css; this._fillLeftLineAtCell(x, y, this._optionsService.rawOptions.cursorWidth); this._ctx.restore(); } private _renderBlockCursor(x: number, y: number, cell: ICellData): void { this._ctx.save(); - this._ctx.fillStyle = this._colors.cursor.css; + this._ctx.fillStyle = this._themeService.colors.cursor.css; this._fillCells(x, y, cell.getWidth(), 1); - this._ctx.fillStyle = this._colors.cursorAccent.css; + this._ctx.fillStyle = this._themeService.colors.cursorAccent.css; this._fillCharTrueColor(cell, x, y); this._ctx.restore(); } private _renderUnderlineCursor(x: number, y: number, cell: ICellData): void { this._ctx.save(); - this._ctx.fillStyle = this._colors.cursor.css; + this._ctx.fillStyle = this._themeService.colors.cursor.css; this._fillBottomLineAtCells(x, y); this._ctx.restore(); } private _renderBlurCursor(x: number, y: number, cell: ICellData): void { this._ctx.save(); - this._ctx.strokeStyle = this._colors.cursor.css; + this._ctx.strokeStyle = this._themeService.colors.cursor.css; this._strokeRectAtCell(x, y, cell.getWidth(), 1); this._ctx.restore(); } diff --git a/addons/xterm-addon-canvas/src/LinkRenderLayer.ts b/addons/xterm-addon-canvas/src/LinkRenderLayer.ts index c07329fd..2d4c217c 100644 --- a/addons/xterm-addon-canvas/src/LinkRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/LinkRenderLayer.ts @@ -3,32 +3,33 @@ * @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 { ICoreBrowserService } from 'browser/services/Services'; -import { is256Color } from './atlas/CharAtlasUtils'; -import { IColorSet, ILinkifierEvent, ILinkifier2 } from 'browser/Types'; +import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants'; +import { ICoreBrowserService, IThemeService } from 'browser/services/Services'; +import { IColorSet, ILinkifierEvent, ILinkifier2, ReadonlyColorSet } 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 + coreBrowserService: ICoreBrowserService, + themeService: IThemeService ) { - super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService, coreBrowserService); + super(terminal, container, 'link', zIndex, true, themeService, bufferService, optionsService, decorationService, coreBrowserService); - linkifier2.onShowLinkUnderline(e => this._onShowLinkUnderline(e)); - linkifier2.onHideLinkUnderline(e => this._onHideLinkUnderline(e)); + this.register(linkifier2.onShowLinkUnderline(e => this._handleShowLinkUnderline(e))); + this.register(linkifier2.onHideLinkUnderline(e => this._handleHideLinkUnderline(e))); } public resize(dim: IRenderDimensions): void { @@ -53,14 +54,14 @@ export class LinkRenderLayer extends BaseRenderLayer { } } - private _onShowLinkUnderline(e: ILinkifierEvent): void { + private _handleShowLinkUnderline(e: ILinkifierEvent): void { if (e.fg === INVERTED_DEFAULT_COLOR) { - this._ctx.fillStyle = this._colors.background.css; + this._ctx.fillStyle = this._themeService.colors.background.css; } else if (e.fg && is256Color(e.fg)) { // 256 color support - this._ctx.fillStyle = this._colors.ansi[e.fg].css; + this._ctx.fillStyle = this._themeService.colors.ansi[e.fg].css; } else { - this._ctx.fillStyle = this._colors.foreground.css; + this._ctx.fillStyle = this._themeService.colors.foreground.css; } if (e.y1 === e.y2) { @@ -77,7 +78,7 @@ export class LinkRenderLayer extends BaseRenderLayer { this._state = e; } - private _onHideLinkUnderline(e: ILinkifierEvent): void { + private _handleHideLinkUnderline(e: ILinkifierEvent): void { this._clearCurrentLink(); } } diff --git a/addons/xterm-addon-canvas/src/SelectionRenderLayer.ts b/addons/xterm-addon-canvas/src/SelectionRenderLayer.ts index e90007b7..1c96dcc8 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 { IColorSet, ReadonlyColorSet } from 'browser/Types'; import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; -import { ICoreBrowserService } from 'browser/services/Services'; +import { ICoreBrowserService, IThemeService } 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 + optionsService: IOptionsService, + themeService: IThemeService ) { - super(container, 'selection', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService, coreBrowserService); + super(terminal, container, 'selection', zIndex, true, themeService, 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); } } @@ -58,18 +59,18 @@ export class SelectionRenderLayer extends BaseRenderLayer { } } - public onBlur(): void { + public handleBlur(): 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 { + public handleFocus(): 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 { - super.onSelectionChanged(start, end, columnSelectMode); + public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void { + super.handleSelectionChanged(start, end, columnSelectMode); this._redrawSelection(start, end, columnSelectMode); } @@ -101,8 +102,8 @@ export class SelectionRenderLayer extends BaseRenderLayer { } this._ctx.fillStyle = (this._coreBrowserService.isFocused - ? this._colors.selectionBackgroundTransparent - : this._colors.selectionInactiveBackgroundTransparent).css; + ? this._themeService.colors.selectionBackgroundTransparent + : this._themeService.colors.selectionInactiveBackgroundTransparent).css; if (columnSelectMode) { const startCol = start[0]; diff --git a/addons/xterm-addon-canvas/src/TextRenderLayer.ts b/addons/xterm-addon-canvas/src/TextRenderLayer.ts index 95f22fce..e2a35751 100644 --- a/addons/xterm-addon-canvas/src/TextRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/TextRenderLayer.ts @@ -3,18 +3,19 @@ * @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'; import { AttributeData } from 'common/buffer/AttributeData'; import { NULL_CELL_CODE, Content, UnderlineStyle } from 'common/buffer/Constants'; -import { IColorSet } from 'browser/Types'; +import { IColorSet, ReadonlyColorSet } from 'browser/Types'; import { CellData } from 'common/buffer/CellData'; import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services'; -import { ICharacterJoinerService, ICoreBrowserService } from 'browser/services/Services'; +import { ICharacterJoinerService, ICoreBrowserService, IThemeService } 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,19 +32,20 @@ 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 + coreBrowserService: ICoreBrowserService, + themeService: IThemeService ) { - super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService, decorationService, coreBrowserService); + super(terminal, container, 'text', zIndex, alpha, themeService, bufferService, optionsService, decorationService, coreBrowserService); this._state = new GridCache(); + this.register(optionsService.onSpecificOptionChange('allowTransparency', value => this._setTransparency(value))); } public resize(dim: IRenderDimensions): void { @@ -167,16 +169,16 @@ export class TextRenderLayer extends BaseRenderLayer { if (cell.isInverse()) { if (cell.isFgDefault()) { - nextFillStyle = this._colors.foreground.css; + nextFillStyle = this._themeService.colors.foreground.css; } else if (cell.isFgRGB()) { nextFillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`; } else { - nextFillStyle = this._colors.ansi[cell.getFgColor()].css; + nextFillStyle = this._themeService.colors.ansi[cell.getFgColor()].css; } } else if (cell.isBgRGB()) { nextFillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`; } else if (cell.isBgPalette()) { - nextFillStyle = this._colors.ansi[cell.getBgColor()].css; + nextFillStyle = this._themeService.colors.ansi[cell.getBgColor()].css; } // Apply dim to the background, this is relatively slow as the CSS is re-parsed but dim is @@ -232,81 +234,10 @@ 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 { + public handleGridChanged(firstRow: number, lastRow: number): void { // Resize has not been called yet if (this._state.cache.length === 0) { return; @@ -321,10 +252,6 @@ export class TextRenderLayer extends BaseRenderLayer { this._drawForeground(firstRow, lastRow); } - public onOptionsChanged(): void { - this._setTransparency(this._optionsService.rawOptions.allowTransparency); - } - /** * Whether a character is overlapping to the next cell. */ @@ -363,19 +290,4 @@ export class TextRenderLayer extends BaseRenderLayer { this._characterOverlapCache[chars] = overlaps; return overlaps; } - - /** - * Clear the charcater at the cell specified. - * @param x The column of the char. - * @param y The row of the char. - */ - // private _clearChar(x: number, y: number): void { - // let colsToClear = 1; - // // Clear the adjacent character if it was wide - // const state = this._state.cache[x][y]; - // if (state && state[CHAR_DATA_WIDTH_INDEX] === 2) { - // colsToClear = 2; - // } - // this.clearCells(x, y, colsToClear, 1); - // } } diff --git a/addons/xterm-addon-canvas/src/Types.d.ts b/addons/xterm-addon-canvas/src/Types.d.ts index dda6052a..753bd127 100644 --- a/addons/xterm-addon-canvas/src/Types.d.ts +++ b/addons/xterm-addon-canvas/src/Types.d.ts @@ -4,7 +4,7 @@ */ import { IDisposable } from 'common/Types'; -import { IColorSet } from 'browser/Types'; +import { IColorSet, ReadonlyColorSet } from 'browser/Types'; import { IEvent } from 'common/EventEmitter'; // TODO: Use core interfaces @@ -41,16 +41,14 @@ export interface IRenderer extends IDisposable { */ 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; + handleDevicePixelRatioChange(): void; + handleResize(cols: number, rows: number): void; + handleCharSizeChanged(): void; + handleBlur(): void; + handleFocus(): void; + handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void; + handleCursorMove(): void; + handleOptionsChanged(): void; clear(): void; renderRows(start: number, end: number): void; clearTextureAtlas?(): void; @@ -58,42 +56,33 @@ export interface IRenderer extends IDisposable { export interface IRenderLayer extends IDisposable { readonly canvas: HTMLCanvasElement; + readonly cacheCanvas: HTMLCanvasElement; /** * Called when the terminal loses focus. */ - onBlur(): void; + handleBlur(): void; /** * * Called when the terminal gets focus. */ - onFocus(): void; + handleFocus(): void; /** * Called when the cursor is moved. */ - onCursorMove(): void; - - /** - * Called when options change. - */ - onOptionsChanged(): void; - - /** - * Called when the theme changes. - */ - setColors(colorSet: IColorSet): void; + handleCursorMove(): void; /** * Called when the data in the grid has changed (or needs to be rendered * again). */ - onGridChanged(startRow: number, endRow: number): void; + handleGridChanged(startRow: number, endRow: number): void; /** * Calls when the selection changes. */ - onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void; + handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void; /** * Resize the render layer. 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 03cf0285..00000000 --- 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/CharAtlasCache.ts b/addons/xterm-addon-canvas/src/atlas/CharAtlasCache.ts deleted file mode 100644 index d9349286..00000000 --- a/addons/xterm-addon-canvas/src/atlas/CharAtlasCache.ts +++ /dev/null @@ -1,96 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { generateConfig, configEquals } from './CharAtlasUtils'; -import { BaseCharAtlas } from './BaseCharAtlas'; -import { DynamicCharAtlas } from './DynamicCharAtlas'; -import { ICharAtlasConfig } from './Types'; -import { IColorSet } from 'browser/Types'; -import { ITerminalOptions } from 'xterm'; - -interface ICharAtlasCacheEntry { - atlas: BaseCharAtlas; - config: ICharAtlasConfig; - // N.B. This implementation potentially holds onto copies of the terminal forever, so - // this may cause memory leaks. - ownedBy: number[]; -} - -const charAtlasCache: ICharAtlasCacheEntry[] = []; - -/** - * Acquires a char atlas, either generating a new one or returning an existing - * one that is in use by another terminal. - */ -export function acquireCharAtlas( - options: Required, - rendererId: number, - colors: IColorSet, - scaledCharWidth: number, - scaledCharHeight: number, - devicePixelRatio: number -): BaseCharAtlas { - const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, options, colors, devicePixelRatio); - - // Check to see if the renderer already owns this config - for (let i = 0; i < charAtlasCache.length; i++) { - const entry = charAtlasCache[i]; - const ownedByIndex = entry.ownedBy.indexOf(rendererId); - if (ownedByIndex >= 0) { - if (configEquals(entry.config, newConfig)) { - return entry.atlas; - } - // The configs differ, release the renderer from the entry - if (entry.ownedBy.length === 1) { - entry.atlas.dispose(); - charAtlasCache.splice(i, 1); - } else { - entry.ownedBy.splice(ownedByIndex, 1); - } - break; - } - } - - // Try match a char atlas from the cache - for (let i = 0; i < charAtlasCache.length; i++) { - const entry = charAtlasCache[i]; - if (configEquals(entry.config, newConfig)) { - // Add the renderer to the cache entry and return - entry.ownedBy.push(rendererId); - return entry.atlas; - } - } - - const newEntry: ICharAtlasCacheEntry = { - atlas: new DynamicCharAtlas( - document, - newConfig - ), - config: newConfig, - ownedBy: [rendererId] - }; - charAtlasCache.push(newEntry); - return newEntry.atlas; -} - -/** - * Removes a terminal reference from the cache, allowing its memory to be freed. - */ -export function removeTerminalFromCache(rendererId: number): void { - for (let i = 0; i < charAtlasCache.length; i++) { - const index = charAtlasCache[i].ownedBy.indexOf(rendererId); - if (index !== -1) { - if (charAtlasCache[i].ownedBy.length === 1) { - // Remove the cache entry if it's the only renderer - charAtlasCache[i].atlas.dispose(); - charAtlasCache.splice(i, 1); - } else { - // Remove the reference from the cache entry - charAtlasCache[i].ownedBy.splice(index, 1); - } - break; - } - } -} 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 b0151e30..00000000 --- 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/DynamicCharAtlas.ts b/addons/xterm-addon-canvas/src/atlas/DynamicCharAtlas.ts deleted file mode 100644 index 3098538d..00000000 --- a/addons/xterm-addon-canvas/src/atlas/DynamicCharAtlas.ts +++ /dev/null @@ -1,411 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, TEXT_BASELINE } from 'browser/renderer/Constants'; -import { IGlyphIdentifier, ICharAtlasConfig } from './Types'; -import { BaseCharAtlas } from './BaseCharAtlas'; -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'; - -// 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. -// -// This helps to limit the amount of damage a program can do when it would otherwise thrash the -// cache. -const FRAME_CACHE_DRAW_LIMIT = 100; - -/** - * 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; - -interface IGlyphCacheValue { - index: number; - isEmpty: boolean; - inBitmap: boolean; -} - -export function getGlyphCacheKey(glyph: IGlyphIdentifier): number { - // Note that this only returns a valid key when code < 256 - // Layout: - // 0b00000000000000000000000000000001: italic (1) - // 0b00000000000000000000000000000010: dim (1) - // 0b00000000000000000000000000000100: bold (1) - // 0b00000000000000000000111111111000: fg (9) - // 0b00000000000111111111000000000000: bg (9) - // 0b00011111111000000000000000000000: code (8) - // 0b11100000000000000000000000000000: unused (3) - 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 { - // 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; - private _cacheCtx: CanvasRenderingContext2D; - - // A temporary context that glyphs are drawn to before being transfered to the atlas. - private _tmpCtx: CanvasRenderingContext2D; - - // The number of characters stored in the atlas by width/height - private _width: number; - private _height: number; - - private _drawToCacheCount: number = 0; - - // An array of glyph keys that are waiting on the bitmap to be generated. - private _glyphsWaitingOnBitmap: IGlyphCacheValue[] = []; - - // The timeout that is used to batch bitmap generation so it's not requested for every new glyph. - private _bitmapCommitTimeout: number | null = null; - - // The bitmap to draw from, this is much faster on other browsers than others. - 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; - // The canvas needs alpha because we use clearColor to convert the background color to alpha. - // It might also contain some characters with transparent backgrounds if allowTransparency is - // set. - this._cacheCtx = throwIfFalsy(this._cacheCanvas.getContext('2d', { alpha: true })); - - 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._width = Math.floor(TEXTURE_WIDTH / this._config.scaledCharWidth); - this._height = Math.floor(TEXTURE_HEIGHT / this._config.scaledCharHeight); - const capacity = this._width * this._height; - this._cacheMap = new LRUMap(capacity); - this._cacheMap.prealloc(capacity); - - // This is useful for debugging - // document.body.appendChild(this._cacheCanvas); - } - - public dispose(): void { - if (this._bitmapCommitTimeout !== null) { - window.clearTimeout(this._bitmapCommitTimeout); - this._bitmapCommitTimeout = null; - } - } - - public beginFrame(): void { - this._drawToCacheCount = 0; - } - - public clear(): void { - if (this._cacheMap.size > 0) { - const capacity = this._width * this._height; - this._cacheMap = new LRUMap(capacity); - this._cacheMap.prealloc(capacity); - } - this._cacheCtx.clearRect(0, 0, TEXTURE_WIDTH, TEXTURE_HEIGHT); - this._tmpCtx.clearRect(0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight); - } - - public draw( - ctx: CanvasRenderingContext2D, - glyph: IGlyphIdentifier, - x: number, - y: number - ): boolean { - // Space is always an empty cell, special case this as it's so common - if (glyph.code === 32) { - return true; - } - - // Exit early for uncachable glyphs - if (!this._canCache(glyph)) { - return false; - } - - const glyphKey = getGlyphCacheKey(glyph); - const cacheValue = this._cacheMap.get(glyphKey); - if (cacheValue !== null && cacheValue !== undefined) { - this._drawFromCache(ctx, cacheValue, x, y); - return true; - } - if (this._drawToCacheCount < FRAME_CACHE_DRAW_LIMIT) { - let index; - if (this._cacheMap.size < this._cacheMap.capacity) { - index = this._cacheMap.size; - } else { - // we're out of space, so our call to set will delete this item - index = this._cacheMap.peek()!.index; - } - const cacheValue = this._drawToCache(glyph, index); - this._cacheMap.set(glyphKey, cacheValue); - this._drawFromCache(ctx, cacheValue, x, y); - return true; - } - return false; - } - - private _canCache(glyph: IGlyphIdentifier): boolean { - // Only cache ascii and extended characters for now, to be safe. In the future, we could do - // something more complicated to determine the expected width of a character. - // - // If we switch the renderer over to webgl at some point, we may be able to use blending modes - // to draw overlapping glyphs from the atlas: - // https://github.com/servo/webrender/issues/464#issuecomment-255632875 - // https://webglfundamentals.org/webgl/lessons/webgl-text-texture.html - return glyph.code < 256; - } - - private _toCoordinateX(index: number): number { - return (index % this._width) * this._config.scaledCharWidth; - } - - private _toCoordinateY(index: number): number { - return Math.floor(index / this._width) * this._config.scaledCharHeight; - } - - private _drawFromCache( - ctx: CanvasRenderingContext2D, - cacheValue: IGlyphCacheValue, - x: number, - y: number - ): void { - // We don't actually need to do anything if this is whitespace. - if (cacheValue.isEmpty) { - return; - } - const cacheX = this._toCoordinateX(cacheValue.index); - const cacheY = this._toCoordinateY(cacheValue.index); - ctx.drawImage( - cacheValue.inBitmap ? this._bitmap! : this._cacheCanvas, - cacheX, - cacheY, - this._config.scaledCharWidth, - this._config.scaledCharHeight, - x, - y, - this._config.scaledCharWidth, - this._config.scaledCharHeight - ); - } - - private _getColorFromAnsiIndex(idx: number): IColor { - if (idx < this._config.colors.ansi.length) { - return this._config.colors.ansi[idx]; - } - return DEFAULT_ANSI_COLORS[idx]; - } - - private _getBackgroundColor(glyph: IGlyphIdentifier): IColor { - if (this._config.allowTransparency) { - // 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; - } - let result: IColor; - if (glyph.bg === INVERTED_DEFAULT_COLOR) { - result = this._config.colors.foreground; - } else if (glyph.bg < 256) { - result = this._getColorFromAnsiIndex(glyph.bg); - } else { - result = this._config.colors.background; - } - if (glyph.dim) { - result = color.blend(this._config.colors.background, color.multiplyOpacity(result, 0.5)); - } - return result; - } - - private _getForegroundColor(glyph: IGlyphIdentifier): IColor { - if (glyph.fg === INVERTED_DEFAULT_COLOR) { - return color.opaque(this._config.colors.background); - } - if (glyph.fg < 256) { - // 256 color support - return this._getColorFromAnsiIndex(glyph.fg); - } - return this._config.colors.foreground; - } - - // TODO: We do this (or something similar) in multiple places. We should split this off - // into a shared function. - private _drawToCache(glyph: IGlyphIdentifier, index: number): IGlyphCacheValue { - this._drawToCacheCount++; - - this._tmpCtx.save(); - - // draw the background - const backgroundColor = this._getBackgroundColor(glyph); - // Use a 'copy' composite operation to clear any existing glyph out of _tmpCtxWithAlpha, regardless of - // transparency in backgroundColor - this._tmpCtx.globalCompositeOperation = 'copy'; - this._tmpCtx.fillStyle = backgroundColor.css; - this._tmpCtx.fillRect(0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight); - this._tmpCtx.globalCompositeOperation = 'source-over'; - - // draw the foreground/glyph - const fontWeight = glyph.bold ? this._config.fontWeightBold : this._config.fontWeight; - const fontStyle = glyph.italic ? 'italic' : ''; - this._tmpCtx.font = - `${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`; - this._tmpCtx.textBaseline = TEXT_BASELINE; - - this._tmpCtx.fillStyle = this._getForegroundColor(glyph).css; - - // Apply alpha to dim the character - if (glyph.dim) { - this._tmpCtx.globalAlpha = DIM_OPACITY; - } - - // Draw the character - this._tmpCtx.fillText(glyph.chars, 0, this._config.scaledCharHeight); - - // clear the background from the character to avoid issues with drawing over the previous - // character if it extends past it's bounds - let imageData = this._tmpCtx.getImageData( - 0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight - ); - let isEmpty = false; - if (!this._config.allowTransparency) { - isEmpty = clearColor(imageData, backgroundColor); - } - - // If this charcater is underscore and empty, shift it up until it is visible, try for a maximum - // of 5 pixels. - if (isEmpty && glyph.chars === '_' && !this._config.allowTransparency) { - for (let offset = 1; offset <= 5; offset++) { - // Draw the character - this._tmpCtx.fillText(glyph.chars, 0, this._config.scaledCharHeight - offset); - - // clear the background from the character to avoid issues with drawing over the previous - // character if it extends past it's bounds - imageData = this._tmpCtx.getImageData( - 0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight - ); - isEmpty = clearColor(imageData, backgroundColor); - if (!isEmpty) { - break; - } - } - } - - this._tmpCtx.restore(); - - // copy the data from imageData to _cacheCanvas - const x = this._toCoordinateX(index); - const y = this._toCoordinateY(index); - // putImageData doesn't do any blending, so it will overwrite any existing cache entry for us - this._cacheCtx.putImageData(imageData, x, y); - - // Add the glyph and queue it to the bitmap (if the browser supports it) - const cacheValue = { - index, - isEmpty, - inBitmap: false - }; - this._addGlyphToBitmap(cacheValue); - - return cacheValue; - } - - private _addGlyphToBitmap(cacheValue: IGlyphCacheValue): void { - // Support is patchy for createImageBitmap at the moment, pass a canvas back - // if support is lacking as drawImage works there too. Firefox is also - // included here as ImageBitmap appears both buggy and has horrible - // performance (tested on v55). - if (!('createImageBitmap' in window) || isFirefox || isSafari) { - return; - } - - // Add the glyph to the queue - this._glyphsWaitingOnBitmap.push(cacheValue); - - // Check if bitmap generation timeout already exists - if (this._bitmapCommitTimeout !== null) { - return; - } - - this._bitmapCommitTimeout = window.setTimeout(() => this._generateBitmap(), GLYPH_BITMAP_COMMIT_DELAY); - } - - private _generateBitmap(): void { - const glyphsMovingToBitmap = this._glyphsWaitingOnBitmap; - this._glyphsWaitingOnBitmap = []; - window.createImageBitmap(this._cacheCanvas).then(bitmap => { - // Set bitmap - this._bitmap = bitmap; - - // Mark all new glyphs as in bitmap, excluding glyphs that came in after - // the bitmap was requested - for (let i = 0; i < glyphsMovingToBitmap.length; i++) { - const value = glyphsMovingToBitmap[i]; - // It doesn't matter if the value was already evicted, it will be - // released from memory after this block if so. - value.inBitmap = true; - } - }); - this._bitmapCommitTimeout = null; - } -} - -// 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. - * @returns True if the result is "empty", meaning all pixels are fully transparent. - */ -function clearColor(imageData: ImageData, color: IColor): boolean { - let isEmpty = true; - const r = color.rgba >>> 24; - const g = color.rgba >>> 16 & 0xFF; - const b = color.rgba >>> 8 & 0xFF; - for (let offset = 0; offset < imageData.data.length; offset += 4) { - if (Math.abs(imageData.data[offset] - r) + - Math.abs(imageData.data[offset + 1] - g) + - Math.abs(imageData.data[offset + 2] - b) < 35) { - imageData.data[offset + 3] = 0; - } else { - isEmpty = false; - } - } - return isEmpty; -} diff --git a/addons/xterm-addon-canvas/src/atlas/LRUMap.test.ts b/addons/xterm-addon-canvas/src/atlas/LRUMap.test.ts deleted file mode 100644 index b24ad2af..00000000 --- a/addons/xterm-addon-canvas/src/atlas/LRUMap.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { assert } from 'chai'; -import { LRUMap } from './LRUMap'; - -describe('LRUMap', () => { - it('can be used to store and retrieve values', () => { - const map = new LRUMap(10); - map.set(1, 'valuea'); - map.set(2, 'valueb'); - map.set(3, 'valuec'); - assert.strictEqual(map.get(1), 'valuea'); - assert.strictEqual(map.get(2), 'valueb'); - assert.strictEqual(map.get(3), 'valuec'); - }); - - it('maintains a size from insertions', () => { - const map = new LRUMap(10); - assert.strictEqual(map.size, 0); - map.set(1, 'value'); - assert.strictEqual(map.size, 1); - map.set(2, 'value'); - assert.strictEqual(map.size, 2); - }); - - it('deletes the oldest entry when the capacity is exceeded', () => { - const map = new LRUMap(4); - map.set(1, 'value'); - map.set(2, 'value'); - map.set(3, 'value'); - map.set(4, 'value'); - map.set(5, 'value'); - assert.isNull(map.get(1)); - assert.isNotNull(map.get(2)); - assert.isNotNull(map.get(3)); - assert.isNotNull(map.get(4)); - assert.isNotNull(map.get(5)); - assert.strictEqual(map.size, 4); - }); - - it('prevents a recently accessed entry from getting deleted', () => { - const map = new LRUMap(2); - map.set(1, 'value'); - map.set(2, 'value'); - map.get(1); - // a would normally get deleted here, except that we called get() - map.set(3, 'value'); - assert.isNotNull(map.get(1)); - // b got deleted instead of a - assert.isNull(map.get(2)); - assert.isNotNull(map.get(3)); - }); - - it('supports mutation', () => { - const map = new LRUMap(10); - map.set(1, 'oldvalue'); - map.set(1, 'newvalue'); - // mutation doesn't change the size - assert.strictEqual(map.size, 1); - assert.strictEqual(map.get(1), 'newvalue'); - }); -}); diff --git a/addons/xterm-addon-canvas/src/atlas/LRUMap.ts b/addons/xterm-addon-canvas/src/atlas/LRUMap.ts deleted file mode 100644 index f70962fe..00000000 --- a/addons/xterm-addon-canvas/src/atlas/LRUMap.ts +++ /dev/null @@ -1,136 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -interface ILinkedListNode { - prev: ILinkedListNode | null; - next: ILinkedListNode | null; - key: number | null; - value: T | null; -} - -export class LRUMap { - private _map: { [key: number]: ILinkedListNode } = {}; - private _head: ILinkedListNode | null = null; - private _tail: ILinkedListNode | null = null; - private _nodePool: ILinkedListNode[] = []; - public size: number = 0; - - constructor(public capacity: number) { } - - private _unlinkNode(node: ILinkedListNode): void { - const prev = node.prev; - const next = node.next; - if (node === this._head) { - this._head = next; - } - if (node === this._tail) { - this._tail = prev; - } - if (prev !== null) { - prev.next = next; - } - if (next !== null) { - next.prev = prev; - } - } - - private _appendNode(node: ILinkedListNode): void { - const tail = this._tail; - if (tail !== null) { - tail.next = node; - } - node.prev = tail; - node.next = null; - this._tail = node; - if (this._head === null) { - this._head = node; - } - } - - /** - * Preallocate a bunch of linked-list nodes. Allocating these nodes ahead of time means that - * they're more likely to live next to each other in memory, which seems to improve performance. - * - * Each empty object only consumes about 60 bytes of memory, so this is pretty cheap, even for - * large maps. - */ - public prealloc(count: number): void { - const nodePool = this._nodePool; - for (let i = 0; i < count; i++) { - nodePool.push({ - prev: null, - next: null, - key: null, - value: null - }); - } - } - - public get(key: number): T | null { - // This is unsafe: We're assuming our keyspace doesn't overlap with Object.prototype. However, - // it's faster than calling hasOwnProperty, and in our case, it would never overlap. - const node = this._map[key]; - if (node !== undefined) { - this._unlinkNode(node); - this._appendNode(node); - return node.value; - } - return null; - } - - /** - * Gets a value from a key without marking it as the most recently used item. - */ - public peekValue(key: number): T | null { - const node = this._map[key]; - if (node !== undefined) { - return node.value; - } - return null; - } - - public peek(): T | null { - const head = this._head; - return head === null ? null : head.value; - } - - public set(key: number, value: T): void { - // This is unsafe: See note above. - let node = this._map[key]; - if (node !== undefined) { - // already exists, we just need to mutate it and move it to the end of the list - node = this._map[key]; - this._unlinkNode(node); - node.value = value; - } else if (this.size >= this.capacity) { - // we're out of space: recycle the head node, move it to the tail - node = this._head!; - this._unlinkNode(node); - delete this._map[node.key!]; - node.key = key; - node.value = value; - this._map[key] = node; - } else { - // make a new element - const nodePool = this._nodePool; - if (nodePool.length > 0) { - // use a preallocated node if we can - node = nodePool.pop()!; - node.key = key; - node.value = value; - } else { - node = { - prev: null, - next: null, - key, - value - }; - } - this._map[key] = node; - this.size++; - } - this._appendNode(node); - } -} diff --git a/addons/xterm-addon-canvas/src/atlas/Types.d.ts b/addons/xterm-addon-canvas/src/atlas/Types.d.ts deleted file mode 100644 index d8bc54c1..00000000 --- a/addons/xterm-addon-canvas/src/atlas/Types.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { FontWeight } from 'common/services/Services'; -import { IPartialColorSet } from 'browser/Types'; - -export interface IGlyphIdentifier { - chars: string; - code: number; - bg: number; - fg: number; - bold: boolean; - 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 7bbbd63d..6a2b98d4 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-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 689899ef..249dd594 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -5,6 +5,7 @@ import { Terminal, IDisposable, ITerminalAddon, IBufferRange, IDecoration } from 'xterm'; import { EventEmitter } from 'common/EventEmitter'; +import { Disposable, toDisposable } from 'common/Lifecycle'; export interface ISearchOptions { regex?: boolean; @@ -50,7 +51,7 @@ type LineCacheEntry = [ const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\\;:"\',./<>?'; const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs -export class SearchAddon implements ITerminalAddon { +export class SearchAddon extends Disposable implements ITerminalAddon { private _terminal: Terminal | undefined; private _cachedSearchTerm: string | undefined; private _selectedDecoration: IDecoration | undefined; @@ -72,13 +73,18 @@ export class SearchAddon implements ITerminalAddon { private _resultIndex: number | undefined; - private readonly _onDidChangeResults = new EventEmitter<{ resultIndex: number, resultCount: number } | undefined>(); + private readonly _onDidChangeResults = this.register(new EventEmitter<{ resultIndex: number, resultCount: number } | undefined>()); public readonly onDidChangeResults = this._onDidChangeResults.event; public activate(terminal: Terminal): void { this._terminal = terminal; - this._onDataDisposable = this._terminal.onWriteParsed(() => this._updateMatches()); - this._onResizeDisposable = this._terminal.onResize(() => this._updateMatches()); + this._onDataDisposable = this.register(this._terminal.onWriteParsed(() => this._updateMatches())); + this._onResizeDisposable = this.register(this._terminal.onResize(() => this._updateMatches())); + this.register(toDisposable(() => { + this.clearDecorations(); + this._onDataDisposable?.dispose(); + this._onResizeDisposable?.dispose(); + })); } private _updateMatches(): void { @@ -94,12 +100,6 @@ export class SearchAddon implements ITerminalAddon { } } - public dispose(): void { - this.clearDecorations(); - this._onDataDisposable?.dispose(); - this._onResizeDisposable?.dispose(); - } - public clearDecorations(retainCachedSearchTerm?: boolean): void { this._selectedDecoration?.dispose(); this._searchResults?.clear(); diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.test.ts b/addons/xterm-addon-serialize/src/SerializeAddon.test.ts index df4a72da..4c5dd645 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.test.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.test.ts @@ -7,9 +7,10 @@ import jsdom = require('jsdom'); import { assert } from 'chai'; import { SerializeAddon } from './SerializeAddon'; import { Terminal } from 'browser/public/Terminal'; -import { ColorManager } from 'browser/ColorManager'; import { SelectionModel } from 'browser/selection/SelectionModel'; import { IBufferService } from 'common/services/Services'; +import { OptionsService } from 'common/services/OptionsService'; +import { ThemeService } from 'browser/services/ThemeService'; function sgr(...seq: string[]): string { return `\x1b[${seq.join(';')}m`; @@ -44,14 +45,11 @@ class TestSelectionService { } describe('xterm-addon-serialize', () => { - let cm: ColorManager; let dom: jsdom.JSDOM; - let document: Document; let window: jsdom.DOMWindow; let serializeAddon: SerializeAddon; let terminal: Terminal; - let selectionService: any; before(() => { serializeAddon = new SerializeAddon(); @@ -60,7 +58,6 @@ describe('xterm-addon-serialize', () => { beforeEach(() => { dom = new jsdom.JSDOM(''); window = dom.window; - document = window.document; (window as any).HTMLCanvasElement.prototype.getContext = () => ({ createLinearGradient(): any { @@ -77,10 +74,8 @@ describe('xterm-addon-serialize', () => { terminal = new Terminal({ cols: 10, rows: 2, allowProposedApi: true }); terminal.loadAddon(serializeAddon); - selectionService = new TestSelectionService((terminal as any)._core._bufferService); - cm = new ColorManager(); - (terminal as any)._core._colorManager = cm; - (terminal as any)._core._selectionService = selectionService; + (terminal as any)._core._themeService = new ThemeService(new OptionsService({})); + (terminal as any)._core._selectionService = new TestSelectionService((terminal as any)._core._bufferService); }); describe('text', () => { diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 99967374..b83ff160 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -544,7 +544,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler { super(buffer); // https://github.com/xtermjs/xterm.js/issues/3601 - this._colors = (_terminal as any)._core._colorManager.colors; + this._colors = (_terminal as any)._core._themeService.colors; } private _padStart(target: string, targetLength: number, padString: string): string { diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index a7f4a700..511c9381 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; +export class GlyphRenderer extends Disposable { + private _atlas: ITextureAtlas | undefined; private _program: WebGLProgram; private _vertexArrayObject: IWebGLVertexArrayObject; @@ -101,7 +99,6 @@ export class GlyphRenderer extends Disposable { constructor( private _terminal: Terminal, - private _colors: IColorSet, private _gl: IWebGL2RenderingContext, private _dimensions: IRenderDimensions ) { @@ -170,7 +167,7 @@ export class GlyphRenderer extends Disposable { gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); // Set viewport - this.onResize(); + this.handleResize(); } public beginFrame(): boolean { @@ -186,12 +183,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 +198,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 } @@ -266,7 +263,7 @@ export class GlyphRenderer extends Disposable { } } - public onResize(): void { + public handleResize(): void { const gl = this._gl; gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); this.clear(); @@ -323,7 +320,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 ef08fb7c..04368711 100644 --- a/addons/xterm-addon-webgl/src/RectangleRenderer.ts +++ b/addons/xterm-addon-webgl/src/RectangleRenderer.ts @@ -3,16 +3,18 @@ * @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 { IColorSet, ReadonlyColorSet } from 'browser/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'; +import { IThemeService } from 'browser/services/Services'; const enum VertexAttribLocations { POSITION = 0, @@ -58,17 +60,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 { @@ -85,9 +85,9 @@ export class RectangleRenderer extends Disposable { constructor( private _terminal: Terminal, - private _colors: IColorSet, private _gl: IWebGL2RenderingContext, - private _dimensions: IRenderDimensions + private _dimensions: IRenderDimensions, + private readonly _themeService: IThemeService ) { super(); @@ -134,7 +134,11 @@ export class RectangleRenderer extends Disposable { gl.vertexAttribPointer(VertexAttribLocations.COLOR, 4, gl.FLOAT, false, BYTES_PER_RECTANGLE, 4 * Float32Array.BYTES_PER_ELEMENT); gl.vertexAttribDivisor(VertexAttribLocations.COLOR, 1); - this._updateCachedColors(); + this._updateCachedColors(_themeService.colors); + this.register(this._themeService.onChangeColors(e => { + this._updateCachedColors(e); + this._updateViewportRectangle(); + })); } public render(): void { @@ -152,12 +156,7 @@ export class RectangleRenderer extends Disposable { gl.drawElementsInstanced(this._gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, this._vertices.count); } - public onResize(): void { - this._updateViewportRectangle(); - } - - public setColors(): void { - this._updateCachedColors(); + public handleResize(): void { this._updateViewportRectangle(); } @@ -165,8 +164,8 @@ export class RectangleRenderer extends Disposable { this._dimensions = dimensions; } - private _updateCachedColors(): void { - this._bgFloat = this._colorToFloat32Array(this._colors.background); + private _updateCachedColors(colors: ReadonlyColorSet): void { + this._bgFloat = this._colorToFloat32Array(colors.background); } private _updateViewportRectangle(): void { @@ -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._themeService.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._themeService.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._themeService.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._themeService.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 2969a6d1..b1542d98 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 bcfa11c8..6e3a18fc 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 5b98a048..1f8e2616 100644 --- a/addons/xterm-addon-webgl/src/WebglAddon.ts +++ b/addons/xterm-addon-webgl/src/WebglAddon.ts @@ -3,56 +3,63 @@ * @license MIT */ -import { Terminal, ITerminalAddon, IEvent } from 'xterm'; -import { WebglRenderer } from './WebglRenderer'; -import { ICharacterJoinerService, ICoreBrowserService, IRenderService } from 'browser/services/Services'; -import { IColorSet } from 'browser/Types'; +import { ICharacterJoinerService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services'; +import { ITerminal } from 'browser/Types'; import { EventEmitter, forwardEvent } from 'common/EventEmitter'; +import { Disposable, toDisposable } from 'common/Lifecycle'; import { isSafari } from 'common/Platform'; -import { ICoreService, IDecorationService } from 'common/services/Services'; +import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; +import { ICoreTerminal } from 'common/Types'; +import { ITerminalAddon, Terminal } from 'xterm'; +import { WebglRenderer } from './WebglRenderer'; -export class WebglAddon implements ITerminalAddon { +export class WebglAddon extends Disposable 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 = this.register(new EventEmitter()); + public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event; + private readonly _onContextLoss = this.register(new EventEmitter()); + public readonly onContextLoss = this._onContextLoss.event; constructor( private _preserveDrawingBuffer?: boolean - ) {} + ) { + super(); + } public activate(terminal: Terminal): void { - if (!terminal.element) { - throw new Error('Cannot activate WebglAddon before Terminal.open'); - } if (isSafari) { throw new Error('Webgl is not currently supported on Safari'); } - this._terminal = terminal; - const renderService: IRenderService = (terminal as any)._core._renderService; - const characterJoinerService: ICharacterJoinerService = (terminal as any)._core._characterJoinerService; - const coreBrowserService: ICoreBrowserService = (terminal as any)._core._coreBrowserService; - const coreService: ICoreService = (terminal as any)._core.coreService; - const decorationService: IDecorationService = (terminal as any)._core._decorationService; - const colors: IColorSet = (terminal as any)._core._colorManager.colors; - this._renderer = new WebglRenderer(terminal, colors, characterJoinerService, coreBrowserService, coreService, decorationService, this._preserveDrawingBuffer); - forwardEvent(this._renderer.onContextLoss, this._onContextLoss); - forwardEvent(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas); - renderService.setRenderer(this._renderer); - } - public dispose(): void { - if (!this._terminal) { - throw new Error('Cannot dispose WebglAddon because it is activated'); + const core = (terminal as any)._core as ITerminal; + if (!terminal.element) { + this.register(core.onWillOpen(() => this.activate(terminal))); + return; } - const renderService: IRenderService = (this._terminal as any)._core._renderService; - renderService.setRenderer((this._terminal as any)._core._createRenderer()); - renderService.onResize(this._terminal.cols, this._terminal.rows); - this._renderer?.dispose(); - this._renderer = undefined; + + this._terminal = terminal; + const coreService: ICoreService = core.coreService; + const optionsService: IOptionsService = core.optionsService; + + const unsafeCore = core as any; + const renderService: IRenderService = unsafeCore._renderService; + const characterJoinerService: ICharacterJoinerService = unsafeCore._characterJoinerService; + const coreBrowserService: ICoreBrowserService = unsafeCore._coreBrowserService; + const decorationService: IDecorationService = unsafeCore._decorationService; + const themeService: IThemeService = unsafeCore._themeService; + + this._renderer = this.register(new WebglRenderer(terminal, themeService, characterJoinerService, coreBrowserService, optionsService, coreService, decorationService, this._preserveDrawingBuffer)); + this.register(forwardEvent(this._renderer.onContextLoss, this._onContextLoss)); + this.register(forwardEvent(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas)); + renderService.setRenderer(this._renderer); + + this.register(toDisposable(() => { + const renderService: IRenderService = (this._terminal as any)._core._renderService; + renderService.setRenderer((this._terminal as any)._core._createRenderer()); + renderService.handleResize(terminal.cols, terminal.rows); + })); } public get textureAtlas(): HTMLCanvasElement | undefined { @@ -60,6 +67,6 @@ export class WebglAddon implements ITerminalAddon { } public clearTextureAtlas(): void { - this._renderer?.clearCharAtlas(); + this._renderer?.clearTextureAtlas(); } } diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index d8daa0d1..fddeb99c 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 { ICharacterJoinerService, ICoreBrowserService } from 'browser/services/Services'; -import { CharData, IBufferLine, ICellData } from 'common/Types'; +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, IThemeService } from 'browser/services/Services'; +import { IColorSet, ITerminal, ReadonlyColorSet } from 'browser/Types'; import { AttributeData } from 'common/buffer/AttributeData'; -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 { CellData } from 'common/buffer/CellData'; +import { Content, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants'; +import { EventEmitter } from 'common/EventEmitter'; +import { Disposable, toDisposable } from 'common/Lifecycle'; +import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; +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,30 +46,34 @@ 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 = this.register(new EventEmitter()); + public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event; + private readonly _onRequestRedraw = this.register(new EventEmitter()); + public readonly onRequestRedraw = this._onRequestRedraw.event; + private readonly _onContextLoss = this.register(new EventEmitter()); + public readonly onContextLoss = this._onContextLoss.event; constructor( private _terminal: Terminal, - private _colors: IColorSet, + private readonly _themeService: IThemeService, private readonly _characterJoinerService: ICharacterJoinerService, private readonly _coreBrowserService: ICoreBrowserService, + optionsService: IOptionsService, coreService: ICoreService, private readonly _decorationService: IDecorationService, preserveDrawingBuffer?: boolean ) { super(); + this.register(this._themeService.onChangeColors(() => this._handleColorChange())); + + this._cellColorResolver = new CellColorResolver(this._terminal, this._model.selection, this._decorationService, this._coreBrowserService, this._themeService); + this._core = (this._terminal as any)._core; this._renderLayers = [ - new LinkRenderLayer(this._core.screenElement!, 2, this._colors, this._core, this._coreBrowserService), - new CursorRenderLayer(_terminal, this._core.screenElement!, 3, this._colors, this._onRequestRedraw, this._coreBrowserService, coreService) + new LinkRenderLayer(this._core.screenElement!, 2, this._terminal, this._core.linkifier2, this._coreBrowserService, this._themeService), + new CursorRenderLayer(_terminal, this._core.screenElement!, 3, this._onRequestRedraw, this._coreBrowserService, coreService, this._themeService, optionsService) ]; this.dimensions = { scaledCharWidth: 0, @@ -96,6 +91,7 @@ export class WebglRenderer extends Disposable implements IRenderer { }; this._devicePixelRatio = this._coreBrowserService.dpr; this._updateDimensions(); + this.register(optionsService.onOptionChange(() => this._handleOptionsChanged())); this._canvas = document.createElement('canvas'); @@ -139,47 +135,37 @@ export class WebglRenderer extends Disposable implements IRenderer { this._initializeWebGLState(); this._isAttached = this._coreBrowserService.window.document.body.contains(this._core.screenElement!); - } - public dispose(): void { - for (const l of this._renderLayers) { - l.dispose(); - } - this._canvas.parentElement?.removeChild(this._canvas); - removeTerminalFromCache(this._terminal); - super.dispose(); + this.register(toDisposable(() => { + for (const l of this._renderLayers) { + l.dispose(); + } + this._canvas.parentElement?.removeChild(this._canvas); + removeTerminalFromCache(this._terminal); + })); } public get textureAtlas(): HTMLCanvasElement | undefined { return this._charAtlas?.cacheCanvas; } - public setColors(colors: IColorSet): void { - this._colors = colors; - // Clear layers and force a full render - for (const l of this._renderLayers) { - l.setColors(this._terminal, this._colors); - l.reset(this._terminal); - } - - this._rectangleRenderer.setColors(); - + private _handleColorChange(): void { this._refreshCharAtlas(); // Force a full refresh this._clearModel(true); } - public onDevicePixelRatioChange(): void { + public handleDevicePixelRatioChange(): void { // If the device pixel ratio changed, the char atlas needs to be regenerated // and the terminal needs to refreshed if (this._devicePixelRatio !== this._coreBrowserService.dpr) { this._devicePixelRatio = this._coreBrowserService.dpr; - this.onResize(this._terminal.cols, this._terminal.rows); + this.handleResize(this._terminal.cols, this._terminal.rows); } } - public onResize(cols: number, rows: number): void { + public handleResize(cols: number, rows: number): void { // Update character and canvas dimensions this._updateDimensions(); @@ -201,9 +187,9 @@ export class WebglRenderer extends Disposable implements IRenderer { this._core.screenElement!.style.height = `${this.dimensions.canvasHeight}px`; this._rectangleRenderer.setDimensions(this.dimensions); - this._rectangleRenderer.onResize(); + this._rectangleRenderer.handleResize(); this._glyphRenderer.setDimensions(this.dimensions); - this._glyphRenderer.onResize(); + this._glyphRenderer.handleResize(); this._refreshCharAtlas(); @@ -212,44 +198,41 @@ export class WebglRenderer extends Disposable implements IRenderer { this._clearModel(false); } - public onCharSizeChanged(): void { - this.onResize(this._terminal.cols, this._terminal.rows); + public handleCharSizeChanged(): void { + this.handleResize(this._terminal.cols, this._terminal.rows); } - public onBlur(): void { + public handleBlur(): void { for (const l of this._renderLayers) { - l.onBlur(this._terminal); + l.handleBlur(this._terminal); } // Request a redraw for active/inactive selection background this._requestRedrawViewport(); } - public onFocus(): void { + public handleFocus(): void { for (const l of this._renderLayers) { - l.onFocus(this._terminal); + l.handleFocus(this._terminal); } // Request a redraw for active/inactive selection background this._requestRedrawViewport(); } - public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void { + public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void { for (const l of this._renderLayers) { - l.onSelectionChanged(this._terminal, start, end, columnSelectMode); + l.handleSelectionChanged(this._terminal, start, end, columnSelectMode); } - this._updateSelectionModel(start, end, columnSelectMode); + this._model.selection.update(this._terminal, start, end, columnSelectMode); this._requestRedrawViewport(); } - public onCursorMove(): void { + public handleCursorMove(): void { for (const l of this._renderLayers) { - l.onCursorMove(this._terminal); + l.handleCursorMove(this._terminal); } } - public onOptionsChanged(): void { - for (const l of this._renderLayers) { - l.onOptionsChanged(this._terminal); - } + private _handleOptionsChanged(): void { this._updateDimensions(); this._refreshCharAtlas(); } @@ -262,11 +245,11 @@ export class WebglRenderer extends Disposable implements IRenderer { this._rectangleRenderer?.dispose(); this._glyphRenderer?.dispose(); - this._rectangleRenderer = new RectangleRenderer(this._terminal, this._colors, this._gl, this.dimensions); - this._glyphRenderer = new GlyphRenderer(this._terminal, this._colors, this._gl, this.dimensions); + this._rectangleRenderer = this.register(new RectangleRenderer(this._terminal, this._gl, this.dimensions, this._themeService)); + this._glyphRenderer = this.register(new GlyphRenderer(this._terminal, this._gl, this.dimensions)); // Update dimensions and acquire char atlas - this.onCharSizeChanged(); + this.handleCharSizeChanged(); } /** @@ -281,10 +264,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._themeService.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); } @@ -305,10 +285,9 @@ export class WebglRenderer extends Disposable implements IRenderer { } } - public clearCharAtlas(): void { + public clearTextureAtlas(): void { this._charAtlas?.clearTexture(); this._clearModel(true); - this._updateModel(0, this._terminal.rows - 1); this._requestRedrawViewport(); } @@ -340,13 +319,13 @@ export class WebglRenderer extends Disposable implements IRenderer { // Update render layers for (const l of this._renderLayers) { - l.onGridChanged(this._terminal, start, end); + l.handleGridChanged(this._terminal, start, end); } // 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 +361,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 +396,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 +404,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 +417,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 +432,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 +442,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 841ad067..51a27377 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 8d2870cd..00000000 --- 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 e9c0cf41..8ed12d86 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts @@ -4,17 +4,17 @@ */ 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 { ICoreBrowserService } from 'browser/services/Services'; -import { IRenderDimensions } from 'browser/renderer/Types'; +import { IColorSet, ReadonlyColorSet } from 'browser/Types'; +import { TEXT_BASELINE } from 'browser/renderer/shared/Constants'; +import { ICoreBrowserService, IThemeService } from 'browser/services/Services'; +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'; +import { Disposable, toDisposable } from 'common/Lifecycle'; -export abstract class BaseRenderLayer implements IRenderLayer { +export abstract class BaseRenderLayer extends Disposable implements IRenderLayer { private _canvas: HTMLCanvasElement; protected _ctx!: CanvasRenderingContext2D; private _scaledCharWidth: number = 0; @@ -24,28 +24,31 @@ export abstract class BaseRenderLayer implements IRenderLayer { private _scaledCharLeft: number = 0; private _scaledCharTop: number = 0; - protected _charAtlas: WebglCharAtlas | undefined; + protected _charAtlas: ITextureAtlas | undefined; constructor( + terminal: Terminal, private _container: HTMLElement, id: string, zIndex: number, private _alpha: boolean, - protected _colors: IColorSet, - protected readonly _coreBrowserService: ICoreBrowserService + protected readonly _coreBrowserService: ICoreBrowserService, + protected readonly _themeService: IThemeService ) { + super(); 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); - } - - public dispose(): void { - this._canvas.remove(); - if (this._charAtlas) { - this._charAtlas.dispose(); - } + this.register(this._themeService.onChangeColors(e => { + this._refreshCharAtlas(terminal, e); + this.reset(terminal); + })); + this.register(toDisposable(() => { + this._canvas.remove(); + this._charAtlas?.dispose(); + })); } private _initCanvas(): void { @@ -56,16 +59,11 @@ export abstract class BaseRenderLayer implements IRenderLayer { } } - public onOptionsChanged(terminal: Terminal): void {} - public onBlur(terminal: Terminal): void {} - public onFocus(terminal: Terminal): void {} - public onCursorMove(terminal: Terminal): void {} - public onGridChanged(terminal: Terminal, startRow: number, endRow: number): void {} - public onSelectionChanged(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {} - - public setColors(terminal: Terminal, colorSet: IColorSet): void { - this._refreshCharAtlas(terminal, colorSet); - } + public handleBlur(terminal: Terminal): void {} + public handleFocus(terminal: Terminal): void {} + public handleCursorMove(terminal: Terminal): void {} + public handleGridChanged(terminal: Terminal, startRow: number, endRow: number): void {} + public handleSelectionChanged(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {} protected _setTransparency(terminal: Terminal, alpha: boolean): void { // Do nothing when alpha doesn't change @@ -82,8 +80,8 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._container.replaceChild(this._canvas, oldCanvas); // Regenerate char atlas and force a full redraw - this._refreshCharAtlas(terminal, this._colors); - this.onGridChanged(terminal, 0, terminal.rows - 1); + this._refreshCharAtlas(terminal, this._themeService.colors); + this.handleGridChanged(terminal, 0, terminal.rows - 1); } /** @@ -91,11 +89,11 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param terminal The terminal. * @param colorSet The color set to use for the char atlas. */ - private _refreshCharAtlas(terminal: Terminal, colorSet: IColorSet): void { + private _refreshCharAtlas(terminal: Terminal, colorSet: ReadonlyColorSet): void { 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(); } @@ -116,7 +114,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._clearAll(); } - this._refreshCharAtlas(terminal, this._colors); + this._refreshCharAtlas(terminal, this._themeService.colors); } public abstract reset(terminal: Terminal): void; @@ -186,7 +184,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { if (this._alpha) { this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); } else { - this._ctx.fillStyle = this._colors.background.css; + this._ctx.fillStyle = this._themeService.colors.background.css; this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height); } } @@ -206,7 +204,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { width * this._scaledCellWidth, height * this._scaledCellHeight); } else { - this._ctx.fillStyle = this._colors.background.css; + this._ctx.fillStyle = this._themeService.colors.background.css; this._ctx.fillRect( x * this._scaledCellWidth, y * this._scaledCellHeight, diff --git a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts index 2b274516..a6325dcb 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts @@ -7,11 +7,12 @@ import { Terminal } from 'xterm'; 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 { IColorSet, ReadonlyColorSet } from 'browser/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'; +import { ICoreBrowserService, IThemeService } from 'browser/services/Services'; +import { ICoreService, IOptionsService } from 'common/services/Services'; +import { toDisposable } from 'common/Lifecycle'; interface ICursorState { x: number; @@ -36,12 +37,13 @@ export class CursorRenderLayer extends BaseRenderLayer { terminal: Terminal, container: HTMLElement, zIndex: number, - colors: IColorSet, private _onRequestRefreshRowsEvent: IEventEmitter, coreBrowserService: ICoreBrowserService, - private readonly _coreService: ICoreService + private readonly _coreService: ICoreService, + themeService: IThemeService, + optionsService: IOptionsService ) { - super(container, 'cursor', zIndex, true, colors, coreBrowserService); + super(terminal, container, 'cursor', zIndex, true, coreBrowserService, themeService); this._state = { x: 0, y: 0, @@ -54,13 +56,12 @@ export class CursorRenderLayer extends BaseRenderLayer { 'block': this._renderBlockCursor.bind(this), 'underline': this._renderUnderlineCursor.bind(this) }; - this.onOptionsChanged(terminal); - } - - public override dispose(): void { - this._cursorBlinkStateManager?.dispose(); - this._cursorBlinkStateManager = undefined; - super.dispose(); + this._handleOptionsChanged(terminal); + this.register(optionsService.onOptionChange(() => this._handleOptionsChanged(terminal))); + this.register(toDisposable(() => { + this._cursorBlinkStateManager?.dispose(); + this._cursorBlinkStateManager = undefined; + })); } public resize(terminal: Terminal, dim: IRenderDimensions): void { @@ -78,20 +79,20 @@ export class CursorRenderLayer extends BaseRenderLayer { public reset(terminal: Terminal): void { this._clearCursor(); this._cursorBlinkStateManager?.restartBlinkAnimation(terminal); - this.onOptionsChanged(terminal); + this._handleOptionsChanged(terminal); } - public onBlur(terminal: Terminal): void { + public handleBlur(terminal: Terminal): void { this._cursorBlinkStateManager?.pause(); this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.active.cursorY, end: terminal.buffer.active.cursorY }); } - public onFocus(terminal: Terminal): void { + public handleFocus(terminal: Terminal): void { this._cursorBlinkStateManager?.resume(terminal); this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.active.cursorY, end: terminal.buffer.active.cursorY }); } - public onOptionsChanged(terminal: Terminal): void { + private _handleOptionsChanged(terminal: Terminal): void { if (terminal.options.cursorBlink) { if (!this._cursorBlinkStateManager) { this._cursorBlinkStateManager = new CursorBlinkStateManager(() => { @@ -107,11 +108,11 @@ export class CursorRenderLayer extends BaseRenderLayer { this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.active.cursorY, end: terminal.buffer.active.cursorY }); } - public onCursorMove(terminal: Terminal): void { + public handleCursorMove(terminal: Terminal): void { this._cursorBlinkStateManager?.restartBlinkAnimation(terminal); } - public onGridChanged(terminal: Terminal, startRow: number, endRow: number): void { + public handleGridChanged(terminal: Terminal, startRow: number, endRow: number): void { if (!this._cursorBlinkStateManager || this._cursorBlinkStateManager.isPaused) { this._render(terminal, false); } else { @@ -147,7 +148,7 @@ export class CursorRenderLayer extends BaseRenderLayer { if (!this._coreBrowserService.isFocused) { this._clearCursor(); this._ctx.save(); - this._ctx.fillStyle = this._colors.cursor.css; + this._ctx.fillStyle = this._themeService.colors.cursor.css; const cursorStyle = terminal.options.cursorStyle; if (cursorStyle && cursorStyle !== 'block') { this._cursorRenderers[cursorStyle](terminal, cursorX, viewportRelativeCursorY, this._cell); @@ -212,30 +213,30 @@ export class CursorRenderLayer extends BaseRenderLayer { private _renderBarCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void { this._ctx.save(); - this._ctx.fillStyle = this._colors.cursor.css; + this._ctx.fillStyle = this._themeService.colors.cursor.css; this._fillLeftLineAtCell(x, y, terminal.options.cursorWidth); this._ctx.restore(); } private _renderBlockCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void { this._ctx.save(); - this._ctx.fillStyle = this._colors.cursor.css; + this._ctx.fillStyle = this._themeService.colors.cursor.css; this._fillCells(x, y, cell.getWidth(), 1); - this._ctx.fillStyle = this._colors.cursorAccent.css; + this._ctx.fillStyle = this._themeService.colors.cursorAccent.css; this._fillCharTrueColor(terminal, cell, x, y); this._ctx.restore(); } private _renderUnderlineCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void { this._ctx.save(); - this._ctx.fillStyle = this._colors.cursor.css; + this._ctx.fillStyle = this._themeService.colors.cursor.css; this._fillBottomLineAtCells(x, y); this._ctx.restore(); } private _renderBlurCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void { this._ctx.save(); - this._ctx.strokeStyle = this._colors.cursor.css; + this._ctx.strokeStyle = this._themeService.colors.cursor.css; this._strokeRectAtCell(x, y, cell.getWidth(), 1); this._ctx.restore(); } diff --git a/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts index e1ff08d8..77b02420 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts @@ -3,13 +3,13 @@ * @license MIT */ +import { is256Color } from 'browser/renderer/shared/CharAtlasUtils'; +import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants'; +import { IRenderDimensions } from 'browser/renderer/shared/Types'; +import { ICoreBrowserService, IThemeService } from 'browser/services/Services'; +import { ILinkifier2, ILinkifierEvent } from 'browser/Types'; import { Terminal } from 'xterm'; import { BaseRenderLayer } from './BaseRenderLayer'; -import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants'; -import { is256Color } from '../atlas/CharAtlasUtils'; -import { ITerminal, IColorSet, ILinkifierEvent } from 'browser/Types'; -import { IRenderDimensions } from 'browser/renderer/Types'; -import { ICoreBrowserService } from 'browser/services/Services'; export class LinkRenderLayer extends BaseRenderLayer { private _state: ILinkifierEvent | undefined; @@ -17,14 +17,15 @@ export class LinkRenderLayer extends BaseRenderLayer { constructor( container: HTMLElement, zIndex: number, - colors: IColorSet, - terminal: ITerminal, - coreBrowserService: ICoreBrowserService + terminal: Terminal, + linkifier2: ILinkifier2, + coreBrowserService: ICoreBrowserService, + themeService: IThemeService ) { - super(container, 'link', zIndex, true, colors, coreBrowserService); + super(terminal, container, 'link', zIndex, true, coreBrowserService, themeService); - terminal.linkifier2.onShowLinkUnderline(e => this._onShowLinkUnderline(e)); - terminal.linkifier2.onHideLinkUnderline(e => this._onHideLinkUnderline(e)); + this.register(linkifier2.onShowLinkUnderline(e => this._handleShowLinkUnderline(e))); + this.register(linkifier2.onHideLinkUnderline(e => this._handleHideLinkUnderline(e))); } public resize(terminal: Terminal, dim: IRenderDimensions): void { @@ -49,14 +50,14 @@ export class LinkRenderLayer extends BaseRenderLayer { } } - private _onShowLinkUnderline(e: ILinkifierEvent): void { + private _handleShowLinkUnderline(e: ILinkifierEvent): void { if (e.fg === INVERTED_DEFAULT_COLOR) { - this._ctx.fillStyle = this._colors.background.css; + this._ctx.fillStyle = this._themeService.colors.background.css; } else if (e.fg !== undefined && is256Color(e.fg)) { // 256 color support - this._ctx.fillStyle = this._colors.ansi[e.fg!].css; + this._ctx.fillStyle = this._themeService.colors.ansi[e.fg!].css; } else { - this._ctx.fillStyle = this._colors.foreground.css; + this._ctx.fillStyle = this._themeService.colors.foreground.css; } if (e.y1 === e.y2) { @@ -73,7 +74,7 @@ export class LinkRenderLayer extends BaseRenderLayer { this._state = e; } - private _onHideLinkUnderline(e: ILinkifierEvent): void { + private _handleHideLinkUnderline(e: ILinkifierEvent): void { this._clearCurrentLink(); } } diff --git a/addons/xterm-addon-webgl/src/renderLayer/Types.ts b/addons/xterm-addon-webgl/src/renderLayer/Types.ts index 70acff34..bad56091 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/Types.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/Types.ts @@ -4,45 +4,34 @@ */ 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 { /** * Called when the terminal loses focus. */ - onBlur(terminal: Terminal): void; + handleBlur(terminal: Terminal): void; /** * * Called when the terminal gets focus. */ - onFocus(terminal: Terminal): void; + handleFocus(terminal: Terminal): void; /** * Called when the cursor is moved. */ - onCursorMove(terminal: Terminal): void; - - /** - * Called when options change. - */ - onOptionsChanged(terminal: Terminal): void; - - /** - * Called when the theme changes. - */ - setColors(terminal: Terminal, colorSet: IColorSet): void; + handleCursorMove(terminal: Terminal): void; /** * Called when the data in the grid has changed (or needs to be rendered * again). */ - onGridChanged(terminal: Terminal, startRow: number, endRow: number): void; + handleGridChanged(terminal: Terminal, startRow: number, endRow: number): void; /** * Calls when the selection changes. */ - onSelectionChanged(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void; + handleSelectionChanged(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void; /** * Registers a handler to join characters to render as a group 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 74aed0cc..6865b6db 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/azure-pipelines.yml b/azure-pipelines.yml index 1710922e..7b015064 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -9,7 +9,7 @@ trigger: jobs: - job: Linux pool: - vmImage: 'ubuntu-18.04' + vmImage: 'ubuntu-20.04' steps: - task: NodeTool@0 inputs: diff --git a/demo/client.ts b/demo/client.ts index 00b10b57..44a96eb3 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); } @@ -267,14 +270,17 @@ function createTerminal(): void { protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://'; socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/'; - term.open(terminalContainer); addons.fit.instance!.fit(); - try { - typedTerm.loadAddon(addons.webgl.instance); - setTimeout(() => { + typedTerm.loadAddon(addons.webgl.instance); + setTimeout(() => { + if (addons.webgl.instance !== undefined) { addTextureAtlas(addons.webgl.instance.textureAtlas); addons.webgl.instance.onChangeTextureAtlas(e => addTextureAtlas(e)); - }, 0); + } + }, 0); + + try { // try-catch to allow the demo to load if webgl is not supported + term.open(terminalContainer); } catch { addons.webgl.instance = undefined; @@ -300,8 +306,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 @@ -427,14 +433,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') { @@ -456,7 +462,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; @@ -489,7 +495,7 @@ function initOptions(term: TerminalType): void { magenta: '#b168df', red: '#da6771', white: '#efefef', - yellow: '#fff099', + yellow: '#fff099' }; break; case 'light': @@ -513,7 +519,7 @@ function initOptions(term: TerminalType): void { magenta: '#bc05bc', red: '#cd3131', white: '#555555', - yellow: '#949800', + yellow: '#949800' }; break; } @@ -545,7 +551,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') { @@ -559,7 +573,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'; } @@ -584,7 +600,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'; @@ -600,8 +616,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; @@ -625,21 +641,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'); @@ -684,7 +700,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; @@ -717,7 +733,7 @@ function loadTest() { }); } -function powerlineSymbolTest() { +function powerlineSymbolTest(): void { function s(char: string): string { return `${char} \x1b[7m${char}\x1b[0m `; } @@ -791,7 +807,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`; } @@ -801,7 +817,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); @@ -848,7 +864,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`); @@ -873,7 +889,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'); @@ -893,7 +968,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({ @@ -908,7 +983,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 41289221..a65b8bbc 100644 --- a/demo/index.html +++ b/demo/index.html @@ -79,6 +79,7 @@
+
diff --git a/demo/server.js b/demo/server.js index 8bb684a2..0e82f9e9 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,16 +72,26 @@ 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) { - sender = queueMicrotask(() => { + 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 = ''; sender = null; @@ -95,15 +100,24 @@ 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) { - sender = queueMicrotask(() => { + 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 = []; sender = null; @@ -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/AccessibilityManager.ts b/src/browser/AccessibilityManager.ts index eba283d5..d0c9f601 100644 --- a/src/browser/AccessibilityManager.ts +++ b/src/browser/AccessibilityManager.ts @@ -9,7 +9,7 @@ import { IBuffer } from 'common/buffer/Types'; import { isMac } from 'common/Platform'; import { TimeBasedDebouncer } from 'browser/TimeBasedDebouncer'; import { addDisposableDomListener } from 'browser/Lifecycle'; -import { Disposable } from 'common/Lifecycle'; +import { Disposable, toDisposable } from 'common/Lifecycle'; import { ScreenDprMonitor } from 'browser/ScreenDprMonitor'; import { IRenderService } from 'browser/services/Services'; import { removeElementFromParent } from 'browser/Dom'; @@ -65,8 +65,8 @@ export class AccessibilityManager extends Disposable { this._rowContainer.appendChild(this._rowElements[i]); } - this._topBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.TOP); - this._bottomBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.BOTTOM); + this._topBoundaryFocusListener = e => this._handleBoundaryFocus(e, BoundaryPosition.TOP); + this._bottomBoundaryFocusListener = e => this._handleBoundaryFocus(e, BoundaryPosition.BOTTOM); this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener); this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener); @@ -87,14 +87,14 @@ export class AccessibilityManager extends Disposable { this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot); this.register(this._renderRowsDebouncer); - this.register(this._terminal.onResize(e => this._onResize(e.rows))); + this.register(this._terminal.onResize(e => this._handleResize(e.rows))); this.register(this._terminal.onRender(e => this._refreshRows(e.start, e.end))); this.register(this._terminal.onScroll(() => this._refreshRows())); // Line feed is an issue as the prompt won't be read out after a command is run - this.register(this._terminal.onA11yChar(char => this._onChar(char))); - this.register(this._terminal.onLineFeed(() => this._onChar('\n'))); - this.register(this._terminal.onA11yTab(spaceCount => this._onTab(spaceCount))); - this.register(this._terminal.onKey(e => this._onKey(e.key))); + this.register(this._terminal.onA11yChar(char => this._handleChar(char))); + this.register(this._terminal.onLineFeed(() => this._handleChar('\n'))); + this.register(this._terminal.onA11yTab(spaceCount => this._handleTab(spaceCount))); + this.register(this._terminal.onKey(e => this._handleKey(e.key))); this.register(this._terminal.onBlur(() => this._clearLiveRegion())); this.register(this._renderService.onDimensionsChange(() => this._refreshRowsDimensions())); @@ -104,15 +104,13 @@ export class AccessibilityManager extends Disposable { // This shouldn't be needed on modern browsers but is present in case the // media query that drives the ScreenDprMonitor isn't supported this.register(addDisposableDomListener(window, 'resize', () => this._refreshRowsDimensions())); + this.register(toDisposable(() => { + removeElementFromParent(this._accessibilityTreeRoot); + this._rowElements.length = 0; + })); } - public dispose(): void { - super.dispose(); - removeElementFromParent(this._accessibilityTreeRoot); - this._rowElements.length = 0; - } - - private _onBoundaryFocus(e: FocusEvent, position: BoundaryPosition): void { + private _handleBoundaryFocus(e: FocusEvent, position: BoundaryPosition): void { const boundaryElement = e.target as HTMLElement; const beforeBoundaryElement = this._rowElements[position === BoundaryPosition.TOP ? 1 : this._rowElements.length - 2]; @@ -172,7 +170,7 @@ export class AccessibilityManager extends Disposable { e.stopImmediatePropagation(); } - private _onResize(rows: number): void { + private _handleResize(rows: number): void { // Remove bottom boundary listener this._rowElements[this._rowElements.length - 1].removeEventListener('focus', this._bottomBoundaryFocusListener); @@ -200,13 +198,13 @@ export class AccessibilityManager extends Disposable { return element; } - private _onTab(spaceCount: number): void { + private _handleTab(spaceCount: number): void { for (let i = 0; i < spaceCount; i++) { - this._onChar(' '); + this._handleChar(' '); } } - private _onChar(char: string): void { + private _handleChar(char: string): void { if (this._liveRegionLineCount < MAX_ROWS_TO_READ + 1) { if (this._charsToConsume.length > 0) { // Have the screen reader ignore the char if it was just input @@ -246,7 +244,7 @@ export class AccessibilityManager extends Disposable { } } - private _onKey(keyChar: string): void { + private _handleKey(keyChar: string): void { this._clearLiveRegion(); this._charsToConsume.push(keyChar); } @@ -280,7 +278,7 @@ export class AccessibilityManager extends Disposable { return; } if (this._rowElements.length !== this._terminal.rows) { - this._onResize(this._terminal.rows); + this._handleResize(this._terminal.rows); } for (let i = 0; i < this._terminal.rows; i++) { this._refreshRowDimensions(this._rowElements[i]); diff --git a/src/browser/ColorManager.test.ts b/src/browser/ColorManager.test.ts deleted file mode 100644 index cf60a1f5..00000000 --- a/src/browser/ColorManager.test.ts +++ /dev/null @@ -1,366 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import jsdom = require('jsdom'); -import { assert } from 'chai'; -import { ColorManager, DEFAULT_ANSI_COLORS } from 'browser/ColorManager'; - -describe('ColorManager', () => { - let cm: ColorManager; - let dom: jsdom.JSDOM; - let document: Document; - let window: jsdom.DOMWindow; - - beforeEach(() => { - dom = new jsdom.JSDOM(''); - window = dom.window; - document = window.document; - (window as any).HTMLCanvasElement.prototype.getContext = () => ({ - createLinearGradient(): any { - return null; - }, - - fillRect(): void { }, - - getImageData(): any { - return {data: [0, 0, 0, 0xFF]}; - } - }); - cm = new ColorManager(); - }); - - describe('constructor', () => { - it('should fill all colors with values', () => { - for (const key of Object.keys(cm.colors)) { - if (key !== 'ansi' && key !== 'contrastCache' && key !== 'selectionForeground') { - // A #rrggbb or rgba(...) - assert.ok((cm.colors as any)[key].css.length >= 7); - } - } - assert.equal(cm.colors.ansi.length, 256); - }); - - it('should fill 240 colors with expected values', () => { - assert.equal(cm.colors.ansi[16].css, '#000000'); - assert.equal(cm.colors.ansi[17].css, '#00005f'); - assert.equal(cm.colors.ansi[18].css, '#000087'); - assert.equal(cm.colors.ansi[19].css, '#0000af'); - assert.equal(cm.colors.ansi[20].css, '#0000d7'); - assert.equal(cm.colors.ansi[21].css, '#0000ff'); - assert.equal(cm.colors.ansi[22].css, '#005f00'); - assert.equal(cm.colors.ansi[23].css, '#005f5f'); - assert.equal(cm.colors.ansi[24].css, '#005f87'); - assert.equal(cm.colors.ansi[25].css, '#005faf'); - assert.equal(cm.colors.ansi[26].css, '#005fd7'); - assert.equal(cm.colors.ansi[27].css, '#005fff'); - assert.equal(cm.colors.ansi[28].css, '#008700'); - assert.equal(cm.colors.ansi[29].css, '#00875f'); - assert.equal(cm.colors.ansi[30].css, '#008787'); - assert.equal(cm.colors.ansi[31].css, '#0087af'); - assert.equal(cm.colors.ansi[32].css, '#0087d7'); - assert.equal(cm.colors.ansi[33].css, '#0087ff'); - assert.equal(cm.colors.ansi[34].css, '#00af00'); - assert.equal(cm.colors.ansi[35].css, '#00af5f'); - assert.equal(cm.colors.ansi[36].css, '#00af87'); - assert.equal(cm.colors.ansi[37].css, '#00afaf'); - assert.equal(cm.colors.ansi[38].css, '#00afd7'); - assert.equal(cm.colors.ansi[39].css, '#00afff'); - assert.equal(cm.colors.ansi[40].css, '#00d700'); - assert.equal(cm.colors.ansi[41].css, '#00d75f'); - assert.equal(cm.colors.ansi[42].css, '#00d787'); - assert.equal(cm.colors.ansi[43].css, '#00d7af'); - assert.equal(cm.colors.ansi[44].css, '#00d7d7'); - assert.equal(cm.colors.ansi[45].css, '#00d7ff'); - assert.equal(cm.colors.ansi[46].css, '#00ff00'); - assert.equal(cm.colors.ansi[47].css, '#00ff5f'); - assert.equal(cm.colors.ansi[48].css, '#00ff87'); - assert.equal(cm.colors.ansi[49].css, '#00ffaf'); - assert.equal(cm.colors.ansi[50].css, '#00ffd7'); - assert.equal(cm.colors.ansi[51].css, '#00ffff'); - assert.equal(cm.colors.ansi[52].css, '#5f0000'); - assert.equal(cm.colors.ansi[53].css, '#5f005f'); - assert.equal(cm.colors.ansi[54].css, '#5f0087'); - assert.equal(cm.colors.ansi[55].css, '#5f00af'); - assert.equal(cm.colors.ansi[56].css, '#5f00d7'); - assert.equal(cm.colors.ansi[57].css, '#5f00ff'); - assert.equal(cm.colors.ansi[58].css, '#5f5f00'); - assert.equal(cm.colors.ansi[59].css, '#5f5f5f'); - assert.equal(cm.colors.ansi[60].css, '#5f5f87'); - assert.equal(cm.colors.ansi[61].css, '#5f5faf'); - assert.equal(cm.colors.ansi[62].css, '#5f5fd7'); - assert.equal(cm.colors.ansi[63].css, '#5f5fff'); - assert.equal(cm.colors.ansi[64].css, '#5f8700'); - assert.equal(cm.colors.ansi[65].css, '#5f875f'); - assert.equal(cm.colors.ansi[66].css, '#5f8787'); - assert.equal(cm.colors.ansi[67].css, '#5f87af'); - assert.equal(cm.colors.ansi[68].css, '#5f87d7'); - assert.equal(cm.colors.ansi[69].css, '#5f87ff'); - assert.equal(cm.colors.ansi[70].css, '#5faf00'); - assert.equal(cm.colors.ansi[71].css, '#5faf5f'); - assert.equal(cm.colors.ansi[72].css, '#5faf87'); - assert.equal(cm.colors.ansi[73].css, '#5fafaf'); - assert.equal(cm.colors.ansi[74].css, '#5fafd7'); - assert.equal(cm.colors.ansi[75].css, '#5fafff'); - assert.equal(cm.colors.ansi[76].css, '#5fd700'); - assert.equal(cm.colors.ansi[77].css, '#5fd75f'); - assert.equal(cm.colors.ansi[78].css, '#5fd787'); - assert.equal(cm.colors.ansi[79].css, '#5fd7af'); - assert.equal(cm.colors.ansi[80].css, '#5fd7d7'); - assert.equal(cm.colors.ansi[81].css, '#5fd7ff'); - assert.equal(cm.colors.ansi[82].css, '#5fff00'); - assert.equal(cm.colors.ansi[83].css, '#5fff5f'); - assert.equal(cm.colors.ansi[84].css, '#5fff87'); - assert.equal(cm.colors.ansi[85].css, '#5fffaf'); - assert.equal(cm.colors.ansi[86].css, '#5fffd7'); - assert.equal(cm.colors.ansi[87].css, '#5fffff'); - assert.equal(cm.colors.ansi[88].css, '#870000'); - assert.equal(cm.colors.ansi[89].css, '#87005f'); - assert.equal(cm.colors.ansi[90].css, '#870087'); - assert.equal(cm.colors.ansi[91].css, '#8700af'); - assert.equal(cm.colors.ansi[92].css, '#8700d7'); - assert.equal(cm.colors.ansi[93].css, '#8700ff'); - assert.equal(cm.colors.ansi[94].css, '#875f00'); - assert.equal(cm.colors.ansi[95].css, '#875f5f'); - assert.equal(cm.colors.ansi[96].css, '#875f87'); - assert.equal(cm.colors.ansi[97].css, '#875faf'); - assert.equal(cm.colors.ansi[98].css, '#875fd7'); - assert.equal(cm.colors.ansi[99].css, '#875fff'); - assert.equal(cm.colors.ansi[100].css, '#878700'); - assert.equal(cm.colors.ansi[101].css, '#87875f'); - assert.equal(cm.colors.ansi[102].css, '#878787'); - assert.equal(cm.colors.ansi[103].css, '#8787af'); - assert.equal(cm.colors.ansi[104].css, '#8787d7'); - assert.equal(cm.colors.ansi[105].css, '#8787ff'); - assert.equal(cm.colors.ansi[106].css, '#87af00'); - assert.equal(cm.colors.ansi[107].css, '#87af5f'); - assert.equal(cm.colors.ansi[108].css, '#87af87'); - assert.equal(cm.colors.ansi[109].css, '#87afaf'); - assert.equal(cm.colors.ansi[110].css, '#87afd7'); - assert.equal(cm.colors.ansi[111].css, '#87afff'); - assert.equal(cm.colors.ansi[112].css, '#87d700'); - assert.equal(cm.colors.ansi[113].css, '#87d75f'); - assert.equal(cm.colors.ansi[114].css, '#87d787'); - assert.equal(cm.colors.ansi[115].css, '#87d7af'); - assert.equal(cm.colors.ansi[116].css, '#87d7d7'); - assert.equal(cm.colors.ansi[117].css, '#87d7ff'); - assert.equal(cm.colors.ansi[118].css, '#87ff00'); - assert.equal(cm.colors.ansi[119].css, '#87ff5f'); - assert.equal(cm.colors.ansi[120].css, '#87ff87'); - assert.equal(cm.colors.ansi[121].css, '#87ffaf'); - assert.equal(cm.colors.ansi[122].css, '#87ffd7'); - assert.equal(cm.colors.ansi[123].css, '#87ffff'); - assert.equal(cm.colors.ansi[124].css, '#af0000'); - assert.equal(cm.colors.ansi[125].css, '#af005f'); - assert.equal(cm.colors.ansi[126].css, '#af0087'); - assert.equal(cm.colors.ansi[127].css, '#af00af'); - assert.equal(cm.colors.ansi[128].css, '#af00d7'); - assert.equal(cm.colors.ansi[129].css, '#af00ff'); - assert.equal(cm.colors.ansi[130].css, '#af5f00'); - assert.equal(cm.colors.ansi[131].css, '#af5f5f'); - assert.equal(cm.colors.ansi[132].css, '#af5f87'); - assert.equal(cm.colors.ansi[133].css, '#af5faf'); - assert.equal(cm.colors.ansi[134].css, '#af5fd7'); - assert.equal(cm.colors.ansi[135].css, '#af5fff'); - assert.equal(cm.colors.ansi[136].css, '#af8700'); - assert.equal(cm.colors.ansi[137].css, '#af875f'); - assert.equal(cm.colors.ansi[138].css, '#af8787'); - assert.equal(cm.colors.ansi[139].css, '#af87af'); - assert.equal(cm.colors.ansi[140].css, '#af87d7'); - assert.equal(cm.colors.ansi[141].css, '#af87ff'); - assert.equal(cm.colors.ansi[142].css, '#afaf00'); - assert.equal(cm.colors.ansi[143].css, '#afaf5f'); - assert.equal(cm.colors.ansi[144].css, '#afaf87'); - assert.equal(cm.colors.ansi[145].css, '#afafaf'); - assert.equal(cm.colors.ansi[146].css, '#afafd7'); - assert.equal(cm.colors.ansi[147].css, '#afafff'); - assert.equal(cm.colors.ansi[148].css, '#afd700'); - assert.equal(cm.colors.ansi[149].css, '#afd75f'); - assert.equal(cm.colors.ansi[150].css, '#afd787'); - assert.equal(cm.colors.ansi[151].css, '#afd7af'); - assert.equal(cm.colors.ansi[152].css, '#afd7d7'); - assert.equal(cm.colors.ansi[153].css, '#afd7ff'); - assert.equal(cm.colors.ansi[154].css, '#afff00'); - assert.equal(cm.colors.ansi[155].css, '#afff5f'); - assert.equal(cm.colors.ansi[156].css, '#afff87'); - assert.equal(cm.colors.ansi[157].css, '#afffaf'); - assert.equal(cm.colors.ansi[158].css, '#afffd7'); - assert.equal(cm.colors.ansi[159].css, '#afffff'); - assert.equal(cm.colors.ansi[160].css, '#d70000'); - assert.equal(cm.colors.ansi[161].css, '#d7005f'); - assert.equal(cm.colors.ansi[162].css, '#d70087'); - assert.equal(cm.colors.ansi[163].css, '#d700af'); - assert.equal(cm.colors.ansi[164].css, '#d700d7'); - assert.equal(cm.colors.ansi[165].css, '#d700ff'); - assert.equal(cm.colors.ansi[166].css, '#d75f00'); - assert.equal(cm.colors.ansi[167].css, '#d75f5f'); - assert.equal(cm.colors.ansi[168].css, '#d75f87'); - assert.equal(cm.colors.ansi[169].css, '#d75faf'); - assert.equal(cm.colors.ansi[170].css, '#d75fd7'); - assert.equal(cm.colors.ansi[171].css, '#d75fff'); - assert.equal(cm.colors.ansi[172].css, '#d78700'); - assert.equal(cm.colors.ansi[173].css, '#d7875f'); - assert.equal(cm.colors.ansi[174].css, '#d78787'); - assert.equal(cm.colors.ansi[175].css, '#d787af'); - assert.equal(cm.colors.ansi[176].css, '#d787d7'); - assert.equal(cm.colors.ansi[177].css, '#d787ff'); - assert.equal(cm.colors.ansi[178].css, '#d7af00'); - assert.equal(cm.colors.ansi[179].css, '#d7af5f'); - assert.equal(cm.colors.ansi[180].css, '#d7af87'); - assert.equal(cm.colors.ansi[181].css, '#d7afaf'); - assert.equal(cm.colors.ansi[182].css, '#d7afd7'); - assert.equal(cm.colors.ansi[183].css, '#d7afff'); - assert.equal(cm.colors.ansi[184].css, '#d7d700'); - assert.equal(cm.colors.ansi[185].css, '#d7d75f'); - assert.equal(cm.colors.ansi[186].css, '#d7d787'); - assert.equal(cm.colors.ansi[187].css, '#d7d7af'); - assert.equal(cm.colors.ansi[188].css, '#d7d7d7'); - assert.equal(cm.colors.ansi[189].css, '#d7d7ff'); - assert.equal(cm.colors.ansi[190].css, '#d7ff00'); - assert.equal(cm.colors.ansi[191].css, '#d7ff5f'); - assert.equal(cm.colors.ansi[192].css, '#d7ff87'); - assert.equal(cm.colors.ansi[193].css, '#d7ffaf'); - assert.equal(cm.colors.ansi[194].css, '#d7ffd7'); - assert.equal(cm.colors.ansi[195].css, '#d7ffff'); - assert.equal(cm.colors.ansi[196].css, '#ff0000'); - assert.equal(cm.colors.ansi[197].css, '#ff005f'); - assert.equal(cm.colors.ansi[198].css, '#ff0087'); - assert.equal(cm.colors.ansi[199].css, '#ff00af'); - assert.equal(cm.colors.ansi[200].css, '#ff00d7'); - assert.equal(cm.colors.ansi[201].css, '#ff00ff'); - assert.equal(cm.colors.ansi[202].css, '#ff5f00'); - assert.equal(cm.colors.ansi[203].css, '#ff5f5f'); - assert.equal(cm.colors.ansi[204].css, '#ff5f87'); - assert.equal(cm.colors.ansi[205].css, '#ff5faf'); - assert.equal(cm.colors.ansi[206].css, '#ff5fd7'); - assert.equal(cm.colors.ansi[207].css, '#ff5fff'); - assert.equal(cm.colors.ansi[208].css, '#ff8700'); - assert.equal(cm.colors.ansi[209].css, '#ff875f'); - assert.equal(cm.colors.ansi[210].css, '#ff8787'); - assert.equal(cm.colors.ansi[211].css, '#ff87af'); - assert.equal(cm.colors.ansi[212].css, '#ff87d7'); - assert.equal(cm.colors.ansi[213].css, '#ff87ff'); - assert.equal(cm.colors.ansi[214].css, '#ffaf00'); - assert.equal(cm.colors.ansi[215].css, '#ffaf5f'); - assert.equal(cm.colors.ansi[216].css, '#ffaf87'); - assert.equal(cm.colors.ansi[217].css, '#ffafaf'); - assert.equal(cm.colors.ansi[218].css, '#ffafd7'); - assert.equal(cm.colors.ansi[219].css, '#ffafff'); - assert.equal(cm.colors.ansi[220].css, '#ffd700'); - assert.equal(cm.colors.ansi[221].css, '#ffd75f'); - assert.equal(cm.colors.ansi[222].css, '#ffd787'); - assert.equal(cm.colors.ansi[223].css, '#ffd7af'); - assert.equal(cm.colors.ansi[224].css, '#ffd7d7'); - assert.equal(cm.colors.ansi[225].css, '#ffd7ff'); - assert.equal(cm.colors.ansi[226].css, '#ffff00'); - assert.equal(cm.colors.ansi[227].css, '#ffff5f'); - assert.equal(cm.colors.ansi[228].css, '#ffff87'); - assert.equal(cm.colors.ansi[229].css, '#ffffaf'); - assert.equal(cm.colors.ansi[230].css, '#ffffd7'); - assert.equal(cm.colors.ansi[231].css, '#ffffff'); - assert.equal(cm.colors.ansi[232].css, '#080808'); - assert.equal(cm.colors.ansi[233].css, '#121212'); - assert.equal(cm.colors.ansi[234].css, '#1c1c1c'); - assert.equal(cm.colors.ansi[235].css, '#262626'); - assert.equal(cm.colors.ansi[236].css, '#303030'); - assert.equal(cm.colors.ansi[237].css, '#3a3a3a'); - assert.equal(cm.colors.ansi[238].css, '#444444'); - assert.equal(cm.colors.ansi[239].css, '#4e4e4e'); - assert.equal(cm.colors.ansi[240].css, '#585858'); - assert.equal(cm.colors.ansi[241].css, '#626262'); - assert.equal(cm.colors.ansi[242].css, '#6c6c6c'); - assert.equal(cm.colors.ansi[243].css, '#767676'); - assert.equal(cm.colors.ansi[244].css, '#808080'); - assert.equal(cm.colors.ansi[245].css, '#8a8a8a'); - assert.equal(cm.colors.ansi[246].css, '#949494'); - assert.equal(cm.colors.ansi[247].css, '#9e9e9e'); - assert.equal(cm.colors.ansi[248].css, '#a8a8a8'); - assert.equal(cm.colors.ansi[249].css, '#b2b2b2'); - assert.equal(cm.colors.ansi[250].css, '#bcbcbc'); - assert.equal(cm.colors.ansi[251].css, '#c6c6c6'); - assert.equal(cm.colors.ansi[252].css, '#d0d0d0'); - assert.equal(cm.colors.ansi[253].css, '#dadada'); - assert.equal(cm.colors.ansi[254].css, '#e4e4e4'); - assert.equal(cm.colors.ansi[255].css, '#eeeeee'); - }); - }); - - describe('setTheme', () => { - it('should not throw when not setting all colors', () => { - assert.doesNotThrow(() => { - cm.setTheme({}); - }); - }); - - it('should set a partial set of colors, using the default if not present', () => { - assert.equal(cm.colors.background.css, '#000000'); - assert.equal(cm.colors.foreground.css, '#ffffff'); - cm.setTheme({ - background: '#FF0000', - foreground: '#00FF00' - }); - assert.equal(cm.colors.background.css, '#FF0000'); - assert.equal(cm.colors.foreground.css, '#00FF00'); - cm.setTheme({ - background: '#0000FF' - }); - assert.equal(cm.colors.background.css, '#0000FF'); - // FG reverts back to default - assert.equal(cm.colors.foreground.css, '#ffffff'); - }); - - it('should set all extended ansi colors in reverse order', () => { - cm.setTheme({ - extendedAnsi: DEFAULT_ANSI_COLORS.map(a => a.css).slice().reverse() - }); - - for (let ansiColor = 16; ansiColor <= 255; ansiColor++) { - assert.equal(cm.colors.ansi[ansiColor].css, DEFAULT_ANSI_COLORS[255 + 16 - ansiColor].css); - } - }); - - it('should set one extended ansi color and keep the other default', () => { - cm.setTheme({ - extendedAnsi: ['#ffffff'] - }); - - assert.equal(cm.colors.ansi[16].css, '#ffffff'); - assert.equal(cm.colors.ansi[17].css, DEFAULT_ANSI_COLORS[17].css); - }); - - it('should set extended ansi colors to the default when they are unset', () => { - cm.setTheme({ - extendedAnsi: ['#ffffff'] - }); - assert.equal(cm.colors.ansi[16].css, '#ffffff'); - - cm.setTheme({ - extendedAnsi: [] - }); - assert.equal(cm.colors.ansi[16].css, DEFAULT_ANSI_COLORS[16].css); - - cm.setTheme({ - extendedAnsi: ['#ffffff'] - }); - assert.equal(cm.colors.ansi[16].css, '#ffffff'); - - cm.setTheme({}); - assert.equal(cm.colors.ansi[16].css, DEFAULT_ANSI_COLORS[16].css); - }); - - it('should set extended ansi colors to the default when they are partially unset', () => { - cm.setTheme({ - extendedAnsi: ['#ffffff', '#000000'] - }); - assert.equal(cm.colors.ansi[16].css, '#ffffff'); - assert.equal(cm.colors.ansi[17].css, '#000000'); - - cm.setTheme({ - extendedAnsi: ['#ffffff'] - }); - assert.equal(cm.colors.ansi[16].css, '#ffffff'); - assert.equal(cm.colors.ansi[17].css, DEFAULT_ANSI_COLORS[17].css); - }); - }); -}); diff --git a/src/browser/ColorManager.ts b/src/browser/ColorManager.ts deleted file mode 100644 index a22a423f..00000000 --- a/src/browser/ColorManager.ts +++ /dev/null @@ -1,221 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { IColorManager, IColorSet, IColorContrastCache } from 'browser/Types'; -import { ITheme } from 'common/services/Services'; -import { channels, color, css } from 'common/Color'; -import { ColorContrastCache } from 'browser/ColorContrastCache'; -import { ColorIndex, IColor } from 'common/Types'; - - -interface IRestoreColorSet { - foreground: IColor; - background: IColor; - cursor: IColor; - ansi: IColor[]; -} - - -const DEFAULT_FOREGROUND = css.toColor('#ffffff'); -const DEFAULT_BACKGROUND = css.toColor('#000000'); -const DEFAULT_CURSOR = css.toColor('#ffffff'); -const DEFAULT_CURSOR_ACCENT = css.toColor('#000000'); -const DEFAULT_SELECTION = { - css: 'rgba(255, 255, 255, 0.3)', - rgba: 0xFFFFFF4D -}; - -// An IIFE to generate DEFAULT_ANSI_COLORS. -export const DEFAULT_ANSI_COLORS = Object.freeze((() => { - const colors = [ - // dark: - css.toColor('#2e3436'), - css.toColor('#cc0000'), - css.toColor('#4e9a06'), - css.toColor('#c4a000'), - css.toColor('#3465a4'), - css.toColor('#75507b'), - css.toColor('#06989a'), - css.toColor('#d3d7cf'), - // bright: - css.toColor('#555753'), - css.toColor('#ef2929'), - css.toColor('#8ae234'), - css.toColor('#fce94f'), - css.toColor('#729fcf'), - css.toColor('#ad7fa8'), - css.toColor('#34e2e2'), - css.toColor('#eeeeec') - ]; - - // Fill in the remaining 240 ANSI colors. - // Generate colors (16-231) - const v = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]; - for (let i = 0; i < 216; i++) { - const r = v[(i / 36) % 6 | 0]; - const g = v[(i / 6) % 6 | 0]; - const b = v[i % 6]; - colors.push({ - css: channels.toCss(r, g, b), - rgba: channels.toRgba(r, g, b) - }); - } - - // Generate greys (232-255) - for (let i = 0; i < 24; i++) { - const c = 8 + i * 10; - colors.push({ - css: channels.toCss(c, c, c), - rgba: channels.toRgba(c, c, c) - }); - } - - return colors; -})()); - -/** - * Manages the source of truth for a terminal's colors. - */ -export class ColorManager implements IColorManager { - public colors: IColorSet; - - private _contrastCache: IColorContrastCache; - private _restoreColors!: IRestoreColorSet; - - constructor() { - this._contrastCache = new ColorContrastCache(); - this.colors = { - foreground: DEFAULT_FOREGROUND, - background: DEFAULT_BACKGROUND, - cursor: DEFAULT_CURSOR, - cursorAccent: DEFAULT_CURSOR_ACCENT, - selectionForeground: undefined, - selectionBackgroundTransparent: DEFAULT_SELECTION, - selectionBackgroundOpaque: color.blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION), - selectionInactiveBackgroundTransparent: DEFAULT_SELECTION, - selectionInactiveBackgroundOpaque: color.blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION), - ansi: DEFAULT_ANSI_COLORS.slice(), - contrastCache: this._contrastCache - }; - this._updateRestoreColors(); - } - - public onOptionsChange(key: string, value: any): void { - switch (key) { - case 'minimumContrastRatio': - this._contrastCache.clear(); - break; - } - } - - /** - * Sets the terminal's theme. - * @param theme The theme to use. If a partial theme is provided then default - * colors will be used where colors are not defined. - */ - public setTheme(theme: ITheme = {}): void { - this.colors.foreground = this._parseColor(theme.foreground, DEFAULT_FOREGROUND); - this.colors.background = this._parseColor(theme.background, DEFAULT_BACKGROUND); - this.colors.cursor = this._parseColor(theme.cursor, DEFAULT_CURSOR); - this.colors.cursorAccent = this._parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT); - this.colors.selectionBackgroundTransparent = this._parseColor(theme.selectionBackground, DEFAULT_SELECTION); - 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 = undefined; - } - - /** - * If selection color is opaque, blend it with background with 0.3 opacity - * Issue #2737 - */ - if (color.isOpaque(this.colors.selectionBackgroundTransparent)) { - const opacity = 0.3; - this.colors.selectionBackgroundTransparent = color.opacity(this.colors.selectionBackgroundTransparent, opacity); - } - if (color.isOpaque(this.colors.selectionInactiveBackgroundTransparent)) { - const opacity = 0.3; - this.colors.selectionInactiveBackgroundTransparent = color.opacity(this.colors.selectionInactiveBackgroundTransparent, opacity); - } - this.colors.ansi = DEFAULT_ANSI_COLORS.slice(); - this.colors.ansi[0] = this._parseColor(theme.black, DEFAULT_ANSI_COLORS[0]); - this.colors.ansi[1] = this._parseColor(theme.red, DEFAULT_ANSI_COLORS[1]); - this.colors.ansi[2] = this._parseColor(theme.green, DEFAULT_ANSI_COLORS[2]); - this.colors.ansi[3] = this._parseColor(theme.yellow, DEFAULT_ANSI_COLORS[3]); - this.colors.ansi[4] = this._parseColor(theme.blue, DEFAULT_ANSI_COLORS[4]); - this.colors.ansi[5] = this._parseColor(theme.magenta, DEFAULT_ANSI_COLORS[5]); - this.colors.ansi[6] = this._parseColor(theme.cyan, DEFAULT_ANSI_COLORS[6]); - this.colors.ansi[7] = this._parseColor(theme.white, DEFAULT_ANSI_COLORS[7]); - this.colors.ansi[8] = this._parseColor(theme.brightBlack, DEFAULT_ANSI_COLORS[8]); - this.colors.ansi[9] = this._parseColor(theme.brightRed, DEFAULT_ANSI_COLORS[9]); - this.colors.ansi[10] = this._parseColor(theme.brightGreen, DEFAULT_ANSI_COLORS[10]); - this.colors.ansi[11] = this._parseColor(theme.brightYellow, DEFAULT_ANSI_COLORS[11]); - this.colors.ansi[12] = this._parseColor(theme.brightBlue, DEFAULT_ANSI_COLORS[12]); - this.colors.ansi[13] = this._parseColor(theme.brightMagenta, DEFAULT_ANSI_COLORS[13]); - this.colors.ansi[14] = this._parseColor(theme.brightCyan, DEFAULT_ANSI_COLORS[14]); - this.colors.ansi[15] = this._parseColor(theme.brightWhite, DEFAULT_ANSI_COLORS[15]); - if (theme.extendedAnsi) { - const colorCount = Math.min(this.colors.ansi.length - 16, theme.extendedAnsi.length); - for (let i = 0; i < colorCount; i++) { - this.colors.ansi[i + 16] = this._parseColor(theme.extendedAnsi[i], DEFAULT_ANSI_COLORS[i + 16]); - } - } - // Clear our the cache - this._contrastCache.clear(); - this._updateRestoreColors(); - } - - public restoreColor(slot?: ColorIndex): void { - // unset slot restores all ansi colors - if (slot === undefined) { - for (let i = 0; i < this._restoreColors.ansi.length; ++i) { - this.colors.ansi[i] = this._restoreColors.ansi[i]; - } - return; - } - switch (slot) { - case ColorIndex.FOREGROUND: - this.colors.foreground = this._restoreColors.foreground; - break; - case ColorIndex.BACKGROUND: - this.colors.background = this._restoreColors.background; - break; - case ColorIndex.CURSOR: - this.colors.cursor = this._restoreColors.cursor; - break; - default: - this.colors.ansi[slot] = this._restoreColors.ansi[slot]; - } - } - - private _updateRestoreColors(): void { - this._restoreColors = { - foreground: this.colors.foreground, - background: this.colors.background, - cursor: this.colors.cursor, - ansi: this.colors.ansi.slice() - }; - } - - private _parseColor( - cssString: string | undefined, - fallback: IColor - ): IColor { - if (cssString !== undefined) { - try { - return css.toColor(cssString); - } catch { - // no-op - } - } - return fallback; - } -} diff --git a/src/browser/Linkifier2.ts b/src/browser/Linkifier2.ts index 9c978949..236efb22 100644 --- a/src/browser/Linkifier2.ts +++ b/src/browser/Linkifier2.ts @@ -8,7 +8,7 @@ import { IDisposable } from 'common/Types'; import { IMouseService, IRenderService } from './services/Services'; import { IBufferService } from 'common/services/Services'; import { EventEmitter, IEvent } from 'common/EventEmitter'; -import { Disposable, getDisposeArrayDisposable, disposeArray } from 'common/Lifecycle'; +import { Disposable, getDisposeArrayDisposable, disposeArray, toDisposable } from 'common/Lifecycle'; import { addDisposableDomListener } from 'browser/Lifecycle'; export class Linkifier2 extends Disposable implements ILinkifier2 { @@ -26,21 +26,19 @@ 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 ) { super(); this.register(getDisposeArrayDisposable(this._linkCacheDisposables)); - } - - public dispose(): void { - super.dispose(); - this._lastMouseEvent = undefined; + this.register(toDisposable(() => { + this._lastMouseEvent = undefined; + })); } public registerLinkProvider(linkProvider: ILinkProvider): IDisposable { @@ -66,12 +64,12 @@ export class Linkifier2 extends Disposable implements ILinkifier2 { this._isMouseOut = true; this._clearCurrentLink(); })); - this.register(addDisposableDomListener(this._element, 'mousemove', this._onMouseMove.bind(this))); + this.register(addDisposableDomListener(this._element, 'mousemove', this._handleMouseMove.bind(this))); this.register(addDisposableDomListener(this._element, 'mousedown', this._handleMouseDown.bind(this))); this.register(addDisposableDomListener(this._element, 'mouseup', this._handleMouseUp.bind(this))); } - private _onMouseMove(event: MouseEvent): void { + private _handleMouseMove(event: MouseEvent): void { this._lastMouseEvent = event; if (!this._element || !this._mouseService) { @@ -99,12 +97,12 @@ export class Linkifier2 extends Disposable implements ILinkifier2 { } if (!this._lastBufferCell || (position.x !== this._lastBufferCell.x || position.y !== this._lastBufferCell.y)) { - this._onHover(position); + this._handleHover(position); this._lastBufferCell = position; } } - private _onHover(position: IBufferCellPosition): void { + private _handleHover(position: IBufferCellPosition): void { // TODO: This currently does not cache link provider results across wrapped lines, activeLine should be something like `activeRange: {startY, endY}` // Check if we need to clear the link if (this._activeLine !== position.y) { diff --git a/src/browser/ScreenDprMonitor.ts b/src/browser/ScreenDprMonitor.ts index 8129da07..1c3f31b7 100644 --- a/src/browser/ScreenDprMonitor.ts +++ b/src/browser/ScreenDprMonitor.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Disposable } from 'common/Lifecycle'; +import { Disposable, toDisposable } from 'common/Lifecycle'; export type ScreenDprListener = (newDevicePixelRatio?: number, oldDevicePixelRatio?: number) => void; @@ -26,6 +26,9 @@ export class ScreenDprMonitor extends Disposable { constructor(private _parentWindow: Window) { super(); this._currentDevicePixelRatio = this._parentWindow.devicePixelRatio; + this.register(toDisposable(() => { + this.clearListener(); + })); } public setListener(listener: ScreenDprListener): void { @@ -43,11 +46,6 @@ export class ScreenDprMonitor extends Disposable { this._updateDpr(); } - public dispose(): void { - super.dispose(); - this.clearListener(); - } - private _updateDpr(): void { if (!this._outerListener) { return; diff --git a/src/browser/Terminal.test.ts b/src/browser/Terminal.test.ts index eaf420ed..a9d0b772 100644 --- a/src/browser/Terminal.test.ts +++ b/src/browser/Terminal.test.ts @@ -1378,7 +1378,6 @@ describe('Terminal', () => { assert.deepEqual(disposeStack, [markers[0], markers[1]]); // trimmed marker objs should be disposed assert.deepEqual(disposeStack.map(el => el.isDisposed), [true, true]); - assert.deepEqual(disposeStack.map(el => (el as any)._isDisposed), [true, true]); // trimmed markers should contain line -1 assert.deepEqual(disposeStack.map(el => el.line), [-1, -1]); }); diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index afdac748..9056cbe3 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'; @@ -39,9 +39,8 @@ import { KeyboardResultType, CoreMouseEventType, CoreMouseButton, CoreMouseActio import { evaluateKeyboardEvent } from 'common/input/Keyboard'; import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter'; import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; -import { ColorManager } from 'browser/ColorManager'; import { RenderService } from 'browser/services/RenderService'; -import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ICoreBrowserService, ICharacterJoinerService } from 'browser/services/Services'; +import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ICoreBrowserService, ICharacterJoinerService, IThemeService } from 'browser/services/Services'; import { CharSizeService } from 'browser/services/CharSizeService'; import { IBuffer } from 'common/buffer/Types'; import { MouseService } from 'browser/services/MouseService'; @@ -56,6 +55,8 @@ import { OverviewRulerRenderer } from 'browser/decorations/OverviewRulerRenderer import { DecorationService } from 'common/services/DecorationService'; import { IDecorationService } from 'common/services/Services'; import { OscLinkProvider } from 'browser/OscLinkProvider'; +import { toDisposable } from 'common/Lifecycle'; +import { ThemeService } from 'browser/services/ThemeService'; // Let it work inside Node.js for automated testing purposes. const document: Document = (typeof window !== 'undefined') ? window.document : null as any; @@ -85,6 +86,7 @@ export class Terminal extends CoreTerminal implements ITerminal { private _coreBrowserService: ICoreBrowserService | undefined; private _mouseService: IMouseService | undefined; private _renderService: IRenderService | undefined; + private _themeService: IThemeService | undefined; private _characterJoinerService: ICharacterJoinerService | undefined; private _selectionService: ISelectionService | undefined; @@ -119,30 +121,30 @@ export class Terminal extends CoreTerminal implements ITerminal { public viewport: IViewport | undefined; private _compositionHelper: ICompositionHelper | undefined; private _accessibilityManager: AccessibilityManager | undefined; - 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 = this.register(new EventEmitter()); + public readonly onCursorMove = this._onCursorMove.event; + private readonly _onKey = this.register(new EventEmitter<{ key: string, domEvent: KeyboardEvent }>()); + public readonly onKey = this._onKey.event; + private readonly _onRender = this.register(new EventEmitter<{ start: number, end: number }>()); + public readonly onRender = this._onRender.event; + private readonly _onSelectionChange = this.register(new EventEmitter()); + public readonly onSelectionChange = this._onSelectionChange.event; + private readonly _onTitleChange = this.register(new EventEmitter()); + public readonly onTitleChange = this._onTitleChange.event; + private readonly _onBell = this.register(new EventEmitter()); + public readonly onBell = this._onBell.event; - private _onFocus = new EventEmitter(); + private _onFocus = this.register(new EventEmitter()); public get onFocus(): IEvent { return this._onFocus.event; } - private _onBlur = new EventEmitter(); + private _onBlur = this.register(new EventEmitter()); public get onBlur(): IEvent { return this._onBlur.event; } - private _onA11yCharEmitter = new EventEmitter(); + private _onA11yCharEmitter = this.register(new EventEmitter()); public get onA11yChar(): IEvent { return this._onA11yCharEmitter.event; } - private _onA11yTabEmitter = new EventEmitter(); + private _onA11yTabEmitter = this.register(new EventEmitter()); public get onA11yTab(): IEvent { return this._onA11yTabEmitter.event; } + private _onWillOpen = this.register(new EventEmitter()); + public get onWillOpen(): IEvent { return this._onWillOpen.event; } /** * Creates a new `Terminal` object. @@ -182,6 +184,11 @@ export class Terminal extends CoreTerminal implements ITerminal { // Setup listeners this.register(this._bufferService.onResize(e => this._afterResize(e.cols, e.rows))); + + this.register(toDisposable(() => { + this._customKeyEventHandler = undefined; + this.element?.parentNode?.removeChild(this.element); + })); } /** @@ -191,9 +198,9 @@ export class Terminal extends CoreTerminal implements ITerminal { * while an event from OSC 10|110 | 11|111 | 12|112 always contains a single request. */ private _handleColorEvent(event: IColorEvent): void { - if (!this._colorManager) return; + if (!this._themeService) return; for (const req of event) { - let acc: 'foreground' | 'background' | 'cursor' | 'ansi' | undefined = undefined; + let acc: 'foreground' | 'background' | 'cursor' | 'ansi'; let ident = ''; switch (req.index) { case ColorIndex.FOREGROUND: // OSC 10 | 110 @@ -216,32 +223,23 @@ export class Terminal extends CoreTerminal implements ITerminal { switch (req.type) { case ColorRequestType.REPORT: const channels = color.toColorRGB(acc === 'ansi' - ? this._colorManager.colors.ansi[req.index] - : this._colorManager.colors[acc]); + ? this._themeService.colors.ansi[req.index] + : this._themeService.colors[acc]); this.coreService.triggerDataEvent(`${C0.ESC}]${ident};${toRgbString(channels)}${C1_ESCAPED.ST}`); break; case ColorRequestType.SET: - if (acc === 'ansi') this._colorManager.colors.ansi[req.index] = rgba.toColor(...req.color); - else this._colorManager.colors[acc] = rgba.toColor(...req.color); + if (acc === 'ansi') { + this._themeService.modifyColors(colors => colors.ansi[req.index] = rgba.toColor(...req.color)); + } else { + const narrowedAcc = acc; + this._themeService.modifyColors(colors => colors[narrowedAcc] = rgba.toColor(...req.color)); + } break; case ColorRequestType.RESTORE: - this._colorManager.restoreColor(req.index); + this._themeService.restoreColor(req.index); break; } } - this._renderService?.setColors(this._colorManager.colors); - this.viewport?.onThemeChange(this._colorManager.colors); - } - - public dispose(): void { - if (this._isDisposed) { - return; - } - super.dispose(); - this._renderService?.dispose(); - this._customKeyEventHandler = undefined; - this.write = () => { }; - this.element?.parentNode?.removeChild(this.element); } protected _setup(): void { @@ -266,60 +264,21 @@ export class Terminal extends CoreTerminal implements ITerminal { } } - protected _updateOptions(key: string): void { - super._updateOptions(key); - - // TODO: These listeners should be owned by individual components - switch (key) { - case 'fontFamily': - case 'fontSize': - // When the font changes the size of the cells may change which requires a renderer clear - this._renderService?.clear(); - this._charSizeService?.measure(); - break; - case 'cursorBlink': - case 'cursorStyle': - // The DOM renderer needs a row refresh to update the cursor styles - this.refresh(this.buffer.y, this.buffer.y); - break; - case 'customGlyphs': - case 'drawBoldTextInBrightColors': - case 'letterSpacing': - case 'lineHeight': - case 'fontWeight': - case 'fontWeightBold': - case 'minimumContrastRatio': - // When the font changes the size of the cells may change which requires a renderer clear - if (this._renderService) { - this._renderService.clear(); - this._renderService.onResize(this.cols, this.rows); - this.refresh(0, this.rows - 1); - } - break; - case 'scrollback': - this.viewport?.syncScrollArea(); - break; - case 'screenReaderMode': - if (this.optionsService.rawOptions.screenReaderMode) { - if (!this._accessibilityManager && this._renderService) { - this._accessibilityManager = new AccessibilityManager(this, this._renderService); - } - } else { - this._accessibilityManager?.dispose(); - this._accessibilityManager = undefined; - } - break; - case 'tabStopWidth': this.buffers.setupTabStops(); break; - case 'theme': - this._setTheme(this.optionsService.rawOptions.theme); - break; + private _handleScreenReaderModeOptionChange(value: boolean): void { + if (value) { + if (!this._accessibilityManager && this._renderService) { + this._accessibilityManager = new AccessibilityManager(this, this._renderService); + } + } else { + this._accessibilityManager?.dispose(); + this._accessibilityManager = undefined; } } /** * Binds the desired focus behavior on a given terminal object. */ - private _onTextAreaFocus(ev: KeyboardEvent): void { + private _handleTextAreaFocus(ev: KeyboardEvent): void { if (this.coreService.decPrivateModes.sendFocus) { this.coreService.triggerDataEvent(C0.ESC + '[I'); } @@ -340,7 +299,7 @@ export class Terminal extends CoreTerminal implements ITerminal { /** * Binds the desired blur behavior on a given terminal object. */ - private _onTextAreaBlur(): void { + private _handleTextAreaBlur(): void { // Text can safely be removed on blur. Doing it earlier could interfere with // screen readers reading it out. this.textarea!.value = ''; @@ -491,8 +450,8 @@ export class Terminal extends CoreTerminal implements ITerminal { this.textarea.setAttribute('autocapitalize', 'off'); this.textarea.setAttribute('spellcheck', 'false'); this.textarea.tabIndex = 0; - this.register(addDisposableDomListener(this.textarea, 'focus', (ev: KeyboardEvent) => this._onTextAreaFocus(ev))); - this.register(addDisposableDomListener(this.textarea, 'blur', () => this._onTextAreaBlur())); + this.register(addDisposableDomListener(this.textarea, 'focus', (ev: KeyboardEvent) => this._handleTextAreaFocus(ev))); + this.register(addDisposableDomListener(this.textarea, 'blur', () => this._handleTextAreaBlur())); this._helperContainer.appendChild(this.textarea); this._coreBrowserService = this._instantiationService.createInstance(CoreBrowserService, this.textarea, this._document.defaultView ?? window); @@ -501,16 +460,13 @@ export class Terminal extends CoreTerminal implements ITerminal { this._charSizeService = this._instantiationService.createInstance(CharSizeService, this._document, this._helperContainer); this._instantiationService.setService(ICharSizeService, this._charSizeService); - this._theme = this.options.theme || this._theme; - this._colorManager = new ColorManager(); - this.register(this.optionsService.onOptionChange(e => this._colorManager!.onOptionsChange(e, this.optionsService.rawOptions[e]))); - this._colorManager.setTheme(this._theme); + this._themeService = this._instantiationService.createInstance(ThemeService); + this._instantiationService.setService(IThemeService, this._themeService); this._characterJoinerService = this._instantiationService.createInstance(CharacterJoinerService); this._instantiationService.setService(ICharacterJoinerService, this._characterJoinerService); - const renderer = this._createRenderer(); - this._renderService = this.register(this._instantiationService.createInstance(RenderService, renderer, this.rows, this.screenElement)); + this._renderService = this.register(this._instantiationService.createInstance(RenderService, this.rows, this.screenElement)); this._instantiationService.setService(IRenderService, this._renderService); this.register(this._renderService.onRenderedViewportChange(e => this._onRender.fire(e))); this.onResize(e => this._renderService!.resize(e.cols, e.rows)); @@ -523,26 +479,32 @@ export class Terminal extends CoreTerminal implements ITerminal { // Performance: Add viewport and helper elements from the fragment this.element.appendChild(fragment); + try { + this._onWillOpen.fire(this.element); + } + catch { /* fails to load addon for some reason */ } + if (!this._renderService.hasRenderer()) { + this._renderService.setRenderer(this._createRenderer()); + } + this._mouseService = this._instantiationService.createInstance(MouseService); this._instantiationService.setService(IMouseService, this._mouseService); this.viewport = this._instantiationService.createInstance(Viewport, (amount: number) => this.scrollLines(amount, true, ScrollSource.VIEWPORT), this._viewportElement, - this._viewportScrollArea, - this.element + this._viewportScrollArea ); - this.viewport.onThemeChange(this._colorManager.colors); this.register(this._inputHandler.onRequestSyncScrollBar(() => this.viewport!.syncScrollArea())); this.register(this.viewport); this.register(this.onCursorMove(() => { - this._renderService!.onCursorMove(); + this._renderService!.handleCursorMove(); this._syncTextArea(); })); - this.register(this.onResize(() => this._renderService!.onResize(this.cols, this.rows))); - this.register(this.onBlur(() => this._renderService!.onBlur())); - this.register(this.onFocus(() => this._renderService!.onFocus())); + this.register(this.onResize(() => this._renderService!.handleResize(this.cols, this.rows))); + this.register(this.onBlur(() => this._renderService!.handleBlur())); + this.register(this.onFocus(() => this._renderService!.handleFocus())); this.register(this._renderService.onDimensionsChange(() => this.viewport!.syncScrollArea())); this._selectionService = this.register(this._instantiationService.createInstance(SelectionService, @@ -553,7 +515,7 @@ export class Terminal extends CoreTerminal implements ITerminal { this._instantiationService.setService(ISelectionService, this._selectionService); this.register(this._selectionService.onRequestScrollLines(e => this.scrollLines(e.amount, e.suppressScrollEvent))); this.register(this._selectionService.onSelectionChange(() => this._onSelectionChange.fire())); - this.register(this._selectionService.onRequestRedraw(e => this._renderService!.onSelectionChanged(e.start, e.end, e.columnSelectMode))); + this.register(this._selectionService.onRequestRedraw(e => this._renderService!.handleSelectionChanged(e.start, e.end, e.columnSelectMode))); this.register(this._selectionService.onLinuxMouseSelection(text => { // If there's a new selection, put it into the textarea, focus and select it // in order to register it as a selection on the OS. This event is fired @@ -570,7 +532,7 @@ export class Terminal extends CoreTerminal implements ITerminal { this.linkifier2.attachToDom(this.screenElement, this._mouseService, this._renderService); this.register(this._instantiationService.createInstance(BufferDecorationRenderer, this.screenElement)); - this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService!.onMouseDown(e))); + this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService!.handleMouseDown(e))); // apply mouse event classes set by escape codes before terminal was attached if (this.coreMouseService.areMouseEventsActive) { @@ -585,12 +547,13 @@ export class Terminal extends CoreTerminal implements ITerminal { // ensure the correct order of the dprchange event this._accessibilityManager = new AccessibilityManager(this, this._renderService); } + this.register(this.optionsService.onSpecificOptionChange('screenReaderMode', e => this._handleScreenReaderModeOptionChange(e))); if (this.options.overviewRulerWidth) { this._overviewRulerRenderer = this.register(this._instantiationService.createInstance(OverviewRulerRenderer, this._viewportElement, this.screenElement)); } - this.optionsService.onOptionChange(() => { - if (!this._overviewRulerRenderer && this.options.overviewRulerWidth && this._viewportElement && this.screenElement) { + this.optionsService.onSpecificOptionChange('overviewRulerWidth', value => { + if (!this._overviewRulerRenderer && value && this._viewportElement && this.screenElement) { this._overviewRulerRenderer = this.register(this._instantiationService.createInstance(OverviewRulerRenderer, this._viewportElement, this.screenElement)); } }); @@ -609,18 +572,7 @@ export class Terminal extends CoreTerminal implements ITerminal { } private _createRenderer(): IRenderer { - return this._instantiationService.createInstance(DomRenderer, this._colorManager!.colors, this.element!, this.screenElement!, this._viewportElement!, this.linkifier2); - } - - /** - * Sets the theme on the renderer. The renderer must have been initialized. - * @param theme The theme to set. - */ - private _setTheme(theme: ITheme): void { - this._theme = theme; - this._colorManager?.setTheme(theme); - this._renderService?.setColors(this._colorManager!.colors); - this.viewport?.onThemeChange(this._colorManager!.colors); + return this._instantiationService.createInstance(DomRenderer, this.element!, this.screenElement!, this._viewportElement!, this.linkifier2); } /** @@ -859,20 +811,20 @@ export class Terminal extends CoreTerminal implements ITerminal { // normal viewport scrolling // conditionally stop event, if the viewport still had rows to scroll within - if (this.viewport!.onWheel(ev)) { + if (this.viewport!.handleWheel(ev)) { return this.cancel(ev); } }, { passive: false })); this.register(addDisposableDomListener(el, 'touchstart', (ev: TouchEvent) => { if (this.coreMouseService.areMouseEventsActive) return; - this.viewport!.onTouchStart(ev); + this.viewport!.handleTouchStart(ev); return this.cancel(ev); }, { passive: true })); this.register(addDisposableDomListener(el, 'touchmove', (ev: TouchEvent) => { if (this.coreMouseService.areMouseEventsActive) return; - if (!this.viewport!.onTouchMove(ev)) { + if (!this.viewport!.handleTouchMove(ev)) { return this.cancel(ev); } }, { passive: false })); diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index 0b5e00c1..97ad90d8 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -5,11 +5,11 @@ 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 { IColorSet, ITerminal, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper, CharacterJoinerHandler, IBufferRange } from 'browser/Types'; +import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services'; +import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types'; +import { IColorSet, ITerminal, ILinkifier2, IBrowser, IViewport, ICompositionHelper, CharacterJoinerHandler, IBufferRange, ReadonlyColorSet } from 'browser/Types'; import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types'; -import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, ITerminalOptions } from 'common/Types'; +import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, ITerminalOptions, ColorIndex } from 'common/Types'; import { Buffer } from 'common/buffer/Buffer'; import * as Browser from 'common/Platform'; import { Terminal } from 'browser/Terminal'; @@ -17,6 +17,7 @@ import { IUnicodeService, IOptionsService, ICoreService, ICoreMouseService } fro import { IFunctionIdentifier, IParams } from 'common/parser/Types'; import { AttributeData } from 'common/buffer/AttributeData'; import { ISelectionRedrawRequestEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types'; +import { css } from 'common/Color'; export class TestTerminal extends Terminal { public get curAttrData(): IAttributeData { return (this as any)._inputHandler._curAttrData; } @@ -41,6 +42,7 @@ export class MockTerminal implements ITerminal { public onTitleChange!: IEvent; public onBell!: IEvent; public onScroll!: IEvent; + public onWillOpen!: IEvent; public onKey!: IEvent<{ key: string, domEvent: KeyboardEvent }>; public onRender!: IEvent<{ start: number, end: number }>; public onResize!: IEvent<{ cols: number, rows: number }>; @@ -264,7 +266,6 @@ export class MockRenderer implements IRenderer { public dispose(): void { throw new Error('Method not implemented.'); } - public colorManager!: IColorManager; public on(type: string, listener: XtermListener): void { throw new Error('Method not implemented.'); } @@ -278,20 +279,17 @@ export class MockRenderer implements IRenderer { throw new Error('Method not implemented.'); } public dimensions!: IRenderDimensions; - public setColors(colors: IColorSet): void { - throw new Error('Method not implemented.'); - } public registerDecoration(decorationOptions: IDecorationOptions): IDecoration { throw new Error('Method not implemented.'); } - public onResize(cols: number, rows: number): void { } - public onCharSizeChanged(): void { } - public onBlur(): void { } - public onFocus(): void { } - public onSelectionChanged(start: [number, number], end: [number, number]): void { } - public onCursorMove(): void { } - public onOptionsChanged(): void { } - public onDevicePixelRatioChange(): void { } + public handleResize(cols: number, rows: number): void { } + public handleCharSizeChanged(): void { } + public handleBlur(): void { } + public handleFocus(): void { } + public handleSelectionChanged(start: [number, number], end: [number, number]): void { } + public handleCursorMove(): void { } + public handleOptionsChanged(): void { } + public handleDevicePixelRatioChange(): void { } public clear(): void { } public renderRows(start: number, end: number): void { } } @@ -301,16 +299,16 @@ export class MockViewport implements IViewport { throw new Error('Method not implemented.'); } public scrollBarWidth: number = 0; - public onThemeChange(colors: IColorSet): void { + public handleThemeChange(colors: IColorSet): void { throw new Error('Method not implemented.'); } - public onWheel(ev: WheelEvent): boolean { + public handleWheel(ev: WheelEvent): boolean { throw new Error('Method not implemented.'); } - public onTouchStart(ev: TouchEvent): void { + public handleTouchStart(ev: TouchEvent): void { throw new Error('Method not implemented.'); } - public onTouchMove(ev: TouchEvent): boolean { + public handleTouchMove(ev: TouchEvent): boolean { throw new Error('Method not implemented.'); } public syncScrollArea(): void { } @@ -400,31 +398,31 @@ export class MockRenderService implements IRenderService { public resize(cols: number, rows: number): void { throw new Error('Method not implemented.'); } + public hasRenderer(): boolean { + throw new Error('Method not implemented.'); + } public setRenderer(renderer: IRenderer): void { throw new Error('Method not implemented.'); } - public setColors(colors: IColorSet): void { + public handleDevicePixelRatioChange(): void { throw new Error('Method not implemented.'); } - public onDevicePixelRatioChange(): void { + public handleResize(cols: number, rows: number): void { throw new Error('Method not implemented.'); } - public onResize(cols: number, rows: number): void { + public handleCharSizeChanged(): void { throw new Error('Method not implemented.'); } - public onCharSizeChanged(): void { + public handleBlur(): void { throw new Error('Method not implemented.'); } - public onBlur(): void { + public handleFocus(): void { throw new Error('Method not implemented.'); } - public onFocus(): void { + public handleSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean): void { throw new Error('Method not implemented.'); } - public onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean): void { - throw new Error('Method not implemented.'); - } - public onCursorMove(): void { + public handleCursorMove(): void { throw new Error('Method not implemented.'); } public clear(): void { @@ -494,10 +492,45 @@ export class MockSelectionService implements ISelectionService { public refresh(isLinuxMouseSelection?: boolean): void { throw new Error('Method not implemented.'); } - public onMouseDown(event: MouseEvent): void { + public handleMouseDown(event: MouseEvent): void { throw new Error('Method not implemented.'); } public isCellInSelection(x: number, y: number): boolean { return false; } } + +export class MockThemeService implements IThemeService{ + public serviceBrand: undefined; + public onChangeColors = new EventEmitter().event; + public restoreColor(slot?: ColorIndex | undefined): void { + throw new Error('Method not implemented.'); + } + public modifyColors(callback: (colors: IColorSet) => void): void { + throw new Error('Method not implemented.'); + } + public colors: ReadonlyColorSet = { + background: css.toColor('#010101'), + foreground: css.toColor('#020202'), + ansi: [ + // dark: + css.toColor('#2e3436'), + css.toColor('#cc0000'), + css.toColor('#4e9a06'), + css.toColor('#c4a000'), + css.toColor('#3465a4'), + css.toColor('#75507b'), + css.toColor('#06989a'), + css.toColor('#d3d7cf'), + // bright: + css.toColor('#555753'), + css.toColor('#ef2929'), + css.toColor('#8ae234'), + css.toColor('#fce94f'), + css.toColor('#729fcf'), + css.toColor('#ad7fa8'), + css.toColor('#34e2e2'), + css.toColor('#eeeeec') + ] + } as any; +} diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index 48461a5e..cfd7156f 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -23,6 +23,7 @@ export interface ITerminal extends IPublicTerminal, ICoreTerminal { onFocus: IEvent; onA11yChar: IEvent; onA11yTab: IEvent; + onWillOpen: IEvent; cancel(ev: Event, force?: boolean): boolean | void; } @@ -105,11 +106,6 @@ export interface IBrowser { isWindows: boolean; } -export interface IColorManager { - colors: IColorSet; - onOptionsChange(key: string, value: any): void; -} - export interface IColorSet { foreground: IColor; background: IColor; @@ -125,6 +121,8 @@ export interface IColorSet { contrastCache: IColorContrastCache; } +export type ReadonlyColorSet = Readonly> & { ansi: Readonly['ansi']> }; + export interface IColorContrastCache { clear(): void; setCss(bg: number, fg: number, value: string | null): void; @@ -146,10 +144,9 @@ export interface IViewport extends IDisposable { scrollBarWidth: number; syncScrollArea(immediate?: boolean): void; getLinesScrolled(ev: WheelEvent): number; - onWheel(ev: WheelEvent): boolean; - onTouchStart(ev: TouchEvent): void; - onTouchMove(ev: TouchEvent): boolean; - onThemeChange(colors: IColorSet): void; + handleWheel(ev: WheelEvent): boolean; + handleTouchStart(ev: TouchEvent): void; + handleTouchMove(ev: TouchEvent): boolean; } export interface ILinkifierEvent { diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index f95e0bb4..700c9e22 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -5,11 +5,11 @@ import { Disposable } from 'common/Lifecycle'; import { addDisposableDomListener } from 'browser/Lifecycle'; -import { IColorSet, IViewport } from 'browser/Types'; -import { ICharSizeService, ICoreBrowserService, IRenderService } from 'browser/services/Services'; +import { IColorSet, IViewport, ReadonlyColorSet } from 'browser/Types'; +import { ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } 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; @@ -52,12 +52,12 @@ export class Viewport extends Disposable implements IViewport { private readonly _scrollLines: (amount: number) => void, private readonly _viewportElement: HTMLElement, private readonly _scrollArea: HTMLElement, - private readonly _element: HTMLElement, @IBufferService private readonly _bufferService: IBufferService, @IOptionsService private readonly _optionsService: IOptionsService, @ICharSizeService private readonly _charSizeService: ICharSizeService, @IRenderService private readonly _renderService: IRenderService, - @ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService + @ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService, + @IThemeService themeService: IThemeService ) { super(); @@ -65,7 +65,7 @@ export class Viewport extends Disposable implements IViewport { // Unfortunately the overlay scrollbar would be hidden underneath the screen element in that case, // therefore we account for a standard amount to make it visible this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH; - this.register(addDisposableDomListener(this._viewportElement, 'scroll', this._onScroll.bind(this))); + this.register(addDisposableDomListener(this._viewportElement, 'scroll', this._handleScroll.bind(this))); // Track properties used in performance critical code manually to avoid using slow getters this._activeBuffer = this._bufferService.buffer; @@ -73,11 +73,15 @@ export class Viewport extends Disposable implements IViewport { this._renderDimensions = this._renderService.dimensions; this.register(this._renderService.onDimensionsChange(e => this._renderDimensions = e)); + this._handleThemeChange(themeService.colors); + this.register(themeService.onChangeColors(e => this._handleThemeChange(e))); + this.register(this._optionsService.onSpecificOptionChange('scrollback', () => this.syncScrollArea())); + // Perform this async to ensure the ICharSizeService is ready. setTimeout(() => this.syncScrollArea(), 0); } - public onThemeChange(colors: IColorSet): void { + private _handleThemeChange(colors: ReadonlyColorSet): void { this._viewportElement.style.backgroundColor = colors.background.css; } @@ -157,7 +161,7 @@ export class Viewport extends Disposable implements IViewport { * terminal to scroll to it. * @param ev The scroll event. */ - private _onScroll(ev: Event): void { + private _handleScroll(ev: Event): void { // Record current scroll top position this._lastScrollTop = this._viewportElement.scrollTop; @@ -234,7 +238,7 @@ export class Viewport extends Disposable implements IViewport { * `Viewport`. * @param ev The mouse wheel event. */ - public onWheel(ev: WheelEvent): boolean { + public handleWheel(ev: WheelEvent): boolean { const amount = this._getPixelsScrolled(ev); if (amount === 0) { return false; @@ -315,7 +319,7 @@ export class Viewport extends Disposable implements IViewport { * Handles the touchstart event, recording the touch occurred. * @param ev The touch event. */ - public onTouchStart(ev: TouchEvent): void { + public handleTouchStart(ev: TouchEvent): void { this._lastTouchY = ev.touches[0].pageY; } @@ -323,7 +327,7 @@ export class Viewport extends Disposable implements IViewport { * Handles the touchmove event, scrolling the viewport if the position shifted. * @param ev The touch event. */ - public onTouchMove(ev: TouchEvent): boolean { + public handleTouchMove(ev: TouchEvent): boolean { const deltaY = this._lastTouchY - ev.touches[0].pageY; this._lastTouchY = ev.touches[0].pageY; if (deltaY === 0) { diff --git a/src/browser/decorations/BufferDecorationRenderer.ts b/src/browser/decorations/BufferDecorationRenderer.ts index 7fcc5ea9..5836e266 100644 --- a/src/browser/decorations/BufferDecorationRenderer.ts +++ b/src/browser/decorations/BufferDecorationRenderer.ts @@ -5,7 +5,7 @@ import { addDisposableDomListener } from 'browser/Lifecycle'; import { IRenderService } from 'browser/services/Services'; -import { Disposable } from 'common/Lifecycle'; +import { Disposable, toDisposable } from 'common/Lifecycle'; import { IBufferService, IDecorationService, IInternalDecoration } from 'common/services/Services'; export class BufferDecorationRenderer extends Disposable { @@ -39,12 +39,10 @@ export class BufferDecorationRenderer extends Disposable { })); this.register(this._decorationService.onDecorationRegistered(() => this._queueRefresh())); this.register(this._decorationService.onDecorationRemoved(decoration => this._removeDecoration(decoration))); - } - - public override dispose(): void { - this._container.remove(); - this._decorationElements.clear(); - super.dispose(); + this.register(toDisposable(() => { + this._container.remove(); + this._decorationElements.clear(); + })); } private _queueRefresh(): void { diff --git a/src/browser/decorations/OverviewRulerRenderer.ts b/src/browser/decorations/OverviewRulerRenderer.ts index e7db50c5..90166960 100644 --- a/src/browser/decorations/OverviewRulerRenderer.ts +++ b/src/browser/decorations/OverviewRulerRenderer.ts @@ -6,7 +6,7 @@ import { ColorZoneStore, IColorZone, IColorZoneStore } from 'browser/decorations/ColorZoneStore'; import { addDisposableDomListener } from 'browser/Lifecycle'; import { ICoreBrowserService, IRenderService } from 'browser/services/Services'; -import { Disposable } from 'common/Lifecycle'; +import { Disposable, toDisposable } from 'common/Lifecycle'; import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; // Helper objects to avoid excessive calculation and garbage collection during rendering. These are @@ -68,6 +68,9 @@ export class OverviewRulerRenderer extends Disposable { this._registerDecorationListeners(); this._registerBufferChangeListeners(); this._registerDimensionChangeListeners(); + this.register(toDisposable(() => { + this._canvas?.remove(); + })); } /** @@ -107,24 +110,13 @@ export class OverviewRulerRenderer extends Disposable { } })); // overview ruler width changed - this.register(this._optionsService.onOptionChange(o => { - if (o === 'overviewRulerWidth') { - this._queueRefresh(true); - } - })); + this.register(this._optionsService.onSpecificOptionChange('overviewRulerWidth', () => this._queueRefresh(true))); // device pixel ratio changed - this.register(addDisposableDomListener(this._coreBrowseService.window, 'resize', () => { - this._queueRefresh(true); - })); + this.register(addDisposableDomListener(this._coreBrowseService.window, 'resize', () => this._queueRefresh(true))); // set the canvas dimensions this._queueRefresh(true); } - public override dispose(): void { - this._canvas?.remove(); - super.dispose(); - } - private _refreshDrawConstants(): void { // width const outerWidth = Math.floor(this._canvas.width / 3); diff --git a/src/browser/renderer/Types.d.ts b/src/browser/renderer/Types.d.ts deleted file mode 100644 index cb1a85b4..00000000 --- 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 8df6b302..02738b5b 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -3,12 +3,12 @@ * @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 { Disposable } from 'common/Lifecycle'; -import { IColorSet, ILinkifierEvent, ILinkifier2 } from 'browser/Types'; -import { ICharSizeService, ICoreBrowserService } from 'browser/services/Services'; +import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants'; +import { Disposable, toDisposable } from 'common/Lifecycle'; +import { IColorSet, ILinkifierEvent, ILinkifier2, ReadonlyColorSet } from 'browser/Types'; +import { ICharSizeService, ICoreBrowserService, IThemeService } from 'browser/services/Services'; import { IOptionsService, IBufferService, IInstantiationService } from 'common/services/Services'; import { EventEmitter, IEvent } from 'common/EventEmitter'; import { color } from 'common/Color'; @@ -40,10 +40,9 @@ export class DomRenderer extends Disposable implements IRenderer { public dimensions: IRenderDimensions; - public get onRequestRedraw(): IEvent { return new EventEmitter().event; } + public readonly onRequestRedraw = this.register(new EventEmitter()).event; constructor( - private _colors: IColorSet, private readonly _element: HTMLElement, private readonly _screenElement: HTMLElement, private readonly _viewportElement: HTMLElement, @@ -52,7 +51,8 @@ export class DomRenderer extends Disposable implements IRenderer { @ICharSizeService private readonly _charSizeService: ICharSizeService, @IOptionsService private readonly _optionsService: IOptionsService, @IBufferService private readonly _bufferService: IBufferService, - @ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService + @ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService, + @IThemeService themeService: IThemeService ) { super(); this._rowContainer = document.createElement('div'); @@ -79,26 +79,27 @@ export class DomRenderer extends Disposable implements IRenderer { actualCellHeight: 0 }; this._updateDimensions(); - this._injectCss(); + this.register(this._optionsService.onOptionChange(() => this._handleOptionsChanged())); - this._rowFactory = instantiationService.createInstance(DomRendererRowFactory, document, this._colors); + this.register(themeService.onChangeColors(e => this._injectCss(e))); + this._injectCss(themeService.colors); + + this._rowFactory = instantiationService.createInstance(DomRendererRowFactory, document); this._element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass); this._screenElement.appendChild(this._rowContainer); this._screenElement.appendChild(this._selectionContainer); - this.register(this._linkifier2.onShowLinkUnderline(e => this._onLinkHover(e))); - this.register(this._linkifier2.onHideLinkUnderline(e => this._onLinkLeave(e))); - } + this.register(this._linkifier2.onShowLinkUnderline(e => this._handleLinkHover(e))); + this.register(this._linkifier2.onHideLinkUnderline(e => this._handleLinkLeave(e))); - public dispose(): void { - this._element.classList.remove(TERMINAL_CLASS_PREFIX + this._terminalClass); + this.register(toDisposable(() => { + this._element.classList.remove(TERMINAL_CLASS_PREFIX + this._terminalClass); - // Outside influences such as React unmounts may manipulate the DOM before our disposal. - // https://github.com/xtermjs/xterm.js/issues/2960 - removeElementFromParent(this._rowContainer, this._selectionContainer, this._themeStyleElement, this._dimensionsStyleElement); - - super.dispose(); + // Outside influences such as React unmounts may manipulate the DOM before our disposal. + // https://github.com/xtermjs/xterm.js/issues/2960 + removeElementFromParent(this._rowContainer, this._selectionContainer, this._themeStyleElement, this._dimensionsStyleElement); + })); } private _updateDimensions(): void { @@ -144,12 +145,7 @@ export class DomRenderer extends Disposable implements IRenderer { this._screenElement.style.height = `${this.dimensions.canvasHeight}px`; } - public setColors(colors: IColorSet): void { - this._colors = colors; - this._injectCss(); - } - - private _injectCss(): void { + private _injectCss(colors: ReadonlyColorSet): void { if (!this._themeStyleElement) { this._themeStyleElement = document.createElement('style'); this._screenElement.appendChild(this._themeStyleElement); @@ -158,7 +154,7 @@ export class DomRenderer extends Disposable implements IRenderer { // Base CSS let styles = `${this._terminalSelector} .${ROW_CONTAINER_CLASS} {` + - ` color: ${this._colors.foreground.css};` + + ` color: ${colors.foreground.css};` + ` font-family: ${this._optionsService.rawOptions.fontFamily};` + ` font-size: ${this._optionsService.rawOptions.fontSize}px;` + `}`; @@ -183,18 +179,18 @@ export class DomRenderer extends Disposable implements IRenderer { styles += `@keyframes blink_block` + `_` + this._terminalClass + ` {` + ` 0% {` + - ` background-color: ${this._colors.cursor.css};` + - ` color: ${this._colors.cursorAccent.css};` + + ` background-color: ${colors.cursor.css};` + + ` color: ${colors.cursorAccent.css};` + ` }` + ` 50% {` + - ` background-color: ${this._colors.cursorAccent.css};` + - ` color: ${this._colors.cursor.css};` + + ` background-color: ${colors.cursorAccent.css};` + + ` color: ${colors.cursor.css};` + ` }` + `}`; // Cursor styles += `${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` + - ` outline: 1px solid ${this._colors.cursor.css};` + + ` outline: 1px solid ${colors.cursor.css};` + ` outline-offset: -1px;` + `}` + `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_BLINK_CLASS}:not(.${CURSOR_STYLE_BLOCK_CLASS}) {` + @@ -204,14 +200,14 @@ export class DomRenderer extends Disposable implements IRenderer { ` animation: blink_block` + `_` + this._terminalClass + ` 1s step-end infinite;` + `}` + `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` + - ` background-color: ${this._colors.cursor.css};` + - ` color: ${this._colors.cursorAccent.css};` + + ` background-color: ${colors.cursor.css};` + + ` color: ${colors.cursorAccent.css};` + `}` + `${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BAR_CLASS} {` + - ` box-shadow: ${this._optionsService.rawOptions.cursorWidth}px 0 0 ${this._colors.cursor.css} inset;` + + ` box-shadow: ${this._optionsService.rawOptions.cursorWidth}px 0 0 ${colors.cursor.css} inset;` + `}` + `${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_UNDERLINE_CLASS} {` + - ` box-shadow: 0 -1px 0 ${this._colors.cursor.css} inset;` + + ` box-shadow: 0 -1px 0 ${colors.cursor.css} inset;` + `}`; // Selection styles += @@ -224,26 +220,26 @@ export class DomRenderer extends Disposable implements IRenderer { `}` + `${this._terminalSelector}.focus .${SELECTION_CLASS} div {` + ` position: absolute;` + - ` background-color: ${this._colors.selectionBackgroundOpaque.css};` + + ` background-color: ${colors.selectionBackgroundOpaque.css};` + `}` + `${this._terminalSelector} .${SELECTION_CLASS} div {` + ` position: absolute;` + - ` background-color: ${this._colors.selectionInactiveBackgroundOpaque.css};` + + ` background-color: ${colors.selectionInactiveBackgroundOpaque.css};` + `}`; // Colors - this._colors.ansi.forEach((c, i) => { + for (const [i, c] of 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}; }`; + `${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { color: ${color.opaque(colors.background).css}; }` + + `${this._terminalSelector} .${BG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { background-color: ${colors.foreground.css}; }`; this._themeStyleElement.textContent = styles; } - public onDevicePixelRatioChange(): void { + public handleDevicePixelRatioChange(): void { this._updateDimensions(); } @@ -260,30 +256,30 @@ export class DomRenderer extends Disposable implements IRenderer { } } - public onResize(cols: number, rows: number): void { + public handleResize(cols: number, rows: number): void { this._refreshRowElements(cols, rows); this._updateDimensions(); } - public onCharSizeChanged(): void { + public handleCharSizeChanged(): void { this._updateDimensions(); } - public onBlur(): void { + public handleBlur(): void { this._rowContainer.classList.remove(FOCUS_CLASS); } - public onFocus(): void { + public handleFocus(): void { this._rowContainer.classList.add(FOCUS_CLASS); } - public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void { + public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void { // Remove all selections while (this._selectionContainer.children.length) { this._selectionContainer.removeChild(this._selectionContainer.children[0]); } - this._rowFactory.onSelectionChanged(start, end, columnSelectMode); + this._rowFactory.handleSelectionChanged(start, end, columnSelectMode); this.renderRows(0, this._bufferService.rows - 1); // Selection does not exist @@ -343,14 +339,13 @@ export class DomRenderer extends Disposable implements IRenderer { return element; } - public onCursorMove(): void { + public handleCursorMove(): void { // No-op, the cursor is drawn when rows are drawn } - public onOptionsChanged(): void { + private _handleOptionsChanged(): void { // Force a refresh this._updateDimensions(); - this._injectCss(); } public clear(): void { @@ -378,11 +373,11 @@ export class DomRenderer extends Disposable implements IRenderer { return `.${TERMINAL_CLASS_PREFIX}${this._terminalClass}`; } - private _onLinkHover(e: ILinkifierEvent): void { + private _handleLinkHover(e: ILinkifierEvent): void { this._setCellUnderline(e.x1, e.x2, e.y1, e.y2, e.cols, true); } - private _onLinkLeave(e: ILinkifierEvent): void { + private _handleLinkLeave(e: ILinkifierEvent): void { this._setCellUnderline(e.x1, e.x2, e.y1, e.y2, e.cols, false); } diff --git a/src/browser/renderer/dom/DomRendererRowFactory.test.ts b/src/browser/renderer/dom/DomRendererRowFactory.test.ts index 776ff701..fbb48536 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.test.ts @@ -12,7 +12,7 @@ import { IBufferLine } from 'common/Types'; import { CellData } from 'common/buffer/CellData'; import { MockCoreService, MockDecorationService, MockOptionsService } from 'common/TestUtils.test'; import { css } from 'common/Color'; -import { MockCharacterJoinerService, MockCoreBrowserService } from 'browser/TestUtils.test'; +import { MockCharacterJoinerService, MockCoreBrowserService, MockThemeService } from 'browser/TestUtils.test'; describe('DomRendererRowFactory', () => { let dom: jsdom.JSDOM; @@ -23,35 +23,12 @@ describe('DomRendererRowFactory', () => { dom = new jsdom.JSDOM(''); rowFactory = new DomRendererRowFactory( dom.window.document, - { - background: css.toColor('#010101'), - foreground: css.toColor('#020202'), - ansi: [ - // dark: - css.toColor('#2e3436'), - css.toColor('#cc0000'), - css.toColor('#4e9a06'), - css.toColor('#c4a000'), - css.toColor('#3465a4'), - css.toColor('#75507b'), - css.toColor('#06989a'), - css.toColor('#d3d7cf'), - // bright: - css.toColor('#555753'), - css.toColor('#ef2929'), - css.toColor('#8ae234'), - css.toColor('#fce94f'), - css.toColor('#729fcf'), - css.toColor('#ad7fa8'), - css.toColor('#34e2e2'), - css.toColor('#eeeeec') - ] - } as any, new MockCharacterJoinerService(), new MockOptionsService({ drawBoldTextInBrightColors: true }), new MockCoreBrowserService(), new MockCoreService(), - new MockDecorationService() + new MockDecorationService(), + new MockThemeService() ); lineData = createEmptyLineData(2); }); @@ -299,7 +276,7 @@ describe('DomRendererRowFactory', () => { it('should force selected cells with content to be rendered above the background', () => { lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)])); lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)])); - rowFactory.onSelectionChanged([1, 0], [2, 0], false); + rowFactory.handleSelectionChanged([1, 0], [2, 0], false); const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), 'ab' @@ -307,7 +284,7 @@ describe('DomRendererRowFactory', () => { }); it('should force whitespace cells to be rendered above the background', () => { lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)])); - rowFactory.onSelectionChanged([0, 0], [2, 0], false); + rowFactory.handleSelectionChanged([0, 0], [2, 0], false); const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), ' a' diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index b6ee7bf1..14b26c92 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -4,15 +4,15 @@ */ 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'; import { color, rgba } from 'common/Color'; -import { IColorSet } from 'browser/Types'; -import { ICharacterJoinerService, ICoreBrowserService } from 'browser/services/Services'; +import { IColorSet, ReadonlyColorSet } from 'browser/Types'; +import { ICharacterJoinerService, ICoreBrowserService, IThemeService } 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'; @@ -35,20 +35,16 @@ export class DomRendererRowFactory { constructor( private readonly _document: Document, - private _colors: IColorSet, @ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService, @IOptionsService private readonly _optionsService: IOptionsService, @ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService, @ICoreService private readonly _coreService: ICoreService, - @IDecorationService private readonly _decorationService: IDecorationService + @IDecorationService private readonly _decorationService: IDecorationService, + @IThemeService private readonly _themeService: IThemeService ) { } - public setColors(colors: IColorSet): void { - this._colors = colors; - } - - public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void { + public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void { this._selectionStart = start; this._selectionEnd = end; this._columnSelectMode = columnSelectMode; @@ -71,6 +67,8 @@ export class DomRendererRowFactory { } } + const colors = this._themeService.colors; + for (let x = 0; x < lineLength; x++) { lineData.loadCell(x, this._workCell); let width = this._workCell.getWidth(); @@ -176,7 +174,7 @@ export class DomRendererRowFactory { if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8) { fg += 8; } - charElement.style.textDecorationColor = this._colors.ansi[fg].css; + charElement.style.textDecorationColor = colors.ansi[fg].css; } } } @@ -224,17 +222,17 @@ export class DomRendererRowFactory { // Apply selection foreground if applicable const isInSelection = this._isCellInSelection(x, row); if (!isTop) { - if (this._colors.selectionForeground && isInSelection) { + if (colors.selectionForeground && isInSelection) { fgColorMode = Attributes.CM_RGB; - fg = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF; - fgOverride = this._colors.selectionForeground; + fg = colors.selectionForeground.rgba >> 8 & 0xFFFFFF; + fgOverride = colors.selectionForeground; } } // If in the selection, force the element to be above the selection to improve contrast and // support opaque selections if (isInSelection) { - bgOverride = this._coreBrowserService.isFocused ? this._colors.selectionBackgroundOpaque : this._colors.selectionInactiveBackgroundOpaque; + bgOverride = this._coreBrowserService.isFocused ? colors.selectionBackgroundOpaque : colors.selectionInactiveBackgroundOpaque; isTop = true; } @@ -248,7 +246,7 @@ export class DomRendererRowFactory { switch (bgColorMode) { case Attributes.CM_P16: case Attributes.CM_P256: - resolvedBg = this._colors.ansi[bg]; + resolvedBg = colors.ansi[bg]; charElement.classList.add(`xterm-bg-${bg}`); break; case Attributes.CM_RGB: @@ -258,10 +256,10 @@ export class DomRendererRowFactory { case Attributes.CM_DEFAULT: default: if (isInverse) { - resolvedBg = this._colors.foreground; + resolvedBg = colors.foreground; charElement.classList.add(`xterm-bg-${INVERTED_DEFAULT_COLOR}`); } else { - resolvedBg = this._colors.background; + resolvedBg = colors.background; } } @@ -279,7 +277,7 @@ export class DomRendererRowFactory { if (cell.isBold() && fg < 8 && this._optionsService.rawOptions.drawBoldTextInBrightColors) { fg += 8; } - if (!this._applyMinimumContrast(charElement, resolvedBg, this._colors.ansi[fg], cell, bgOverride, undefined)) { + if (!this._applyMinimumContrast(charElement, resolvedBg, colors.ansi[fg], cell, bgOverride, undefined)) { charElement.classList.add(`xterm-fg-${fg}`); } break; @@ -295,7 +293,7 @@ export class DomRendererRowFactory { break; case Attributes.CM_DEFAULT: default: - if (!this._applyMinimumContrast(charElement, resolvedBg, this._colors.foreground, cell, bgOverride, undefined)) { + if (!this._applyMinimumContrast(charElement, resolvedBg, colors.foreground, cell, bgOverride, undefined)) { if (isInverse) { charElement.classList.add(`xterm-fg-${INVERTED_DEFAULT_COLOR}`); } @@ -317,13 +315,13 @@ export class DomRendererRowFactory { // Try get from cache first, only use the cache when there are no decoration overrides let adjustedColor: IColor | undefined | null = undefined; if (!bgOverride && !fgOverride) { - adjustedColor = this._colors.contrastCache.getColor(bg.rgba, fg.rgba); + adjustedColor = this._themeService.colors.contrastCache.getColor(bg.rgba, fg.rgba); } // Calculate and store in cache if (adjustedColor === undefined) { adjustedColor = color.ensureContrastRatio(bgOverride || bg, fgOverride || fg, this._optionsService.rawOptions.minimumContrastRatio); - this._colors.contrastCache.setColor((bgOverride || bg).rgba, (fgOverride || fg).rgba, adjustedColor ?? null); + this._themeService.colors.contrastCache.setColor((bgOverride || bg).rgba, (fgOverride || fg).rgba, adjustedColor ?? null); } if (adjustedColor) { diff --git a/src/browser/renderer/shared/CellColorResolver.ts b/src/browser/renderer/shared/CellColorResolver.ts new file mode 100644 index 00000000..ae5019e2 --- /dev/null +++ b/src/browser/renderer/shared/CellColorResolver.ts @@ -0,0 +1,137 @@ +import { ISelectionRenderModel } from 'browser/renderer/shared/Types'; +import { ICoreBrowserService, IThemeService } from 'browser/services/Services'; +import { IColorSet, ReadonlyColorSet } 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; +let $colors: ReadonlyColorSet | undefined; + +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 readonly _selectionRenderModel: ISelectionRenderModel, + private readonly _decorationService: IDecorationService, + private readonly _coreBrowserService: ICoreBrowserService, + private readonly _themeService: IThemeService + ) { + } + + /** + * 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; + $colors = this._themeService.colors; + + // 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 ? $colors.selectionBackgroundOpaque : $colors.selectionInactiveBackgroundOpaque).rgba >> 8 & 0xFFFFFF; + $hasBg = true; + if ($colors.selectionForeground) { + $fg = $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)) | (($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)) | (($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; + } + } + + // Release object + $colors = undefined; + + // 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 80% rename from addons/xterm-addon-webgl/src/atlas/CharAtlasCache.ts rename to src/browser/renderer/shared/CharAtlasCache.ts index 893362c8..e953dc46 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 { ITerminal, ReadonlyColorSet } 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,15 +25,15 @@ const charAtlasCache: ICharAtlasCacheEntry[] = []; * @param terminal The terminal. * @param colors The colors to use. */ -export function acquireCharAtlas( +export function acquireTextureAtlas( terminal: Terminal, - colors: IColorSet, + colors: ReadonlyColorSet, scaledCellWidth: number, scaledCellHeight: number, 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 87% rename from addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts rename to src/browser/renderer/shared/CharAtlasUtils.ts index 83f82fa7..e443168e 100644 --- a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts +++ b/src/browser/renderer/shared/CharAtlasUtils.ts @@ -5,16 +5,11 @@ import { ICharAtlasConfig } from './Types'; import { Attributes } from 'common/buffer/Constants'; -import { Terminal, FontWeight } from 'xterm'; -import { IColorSet } from 'browser/Types'; -import { IColor } from 'common/Types'; +import { Terminal } from 'xterm'; +import { IColorSet, ReadonlyColorSet } from 'browser/Types'; +import { NULL_COLOR } from 'common/Color'; -const NULL_COLOR: IColor = { - css: '', - rgba: 0 -}; - -export function generateConfig(scaledCellWidth: number, scaledCellHeight: number, scaledCharWidth: number, scaledCharHeight: number, terminal: Terminal, colors: IColorSet, devicePixelRatio: number): ICharAtlasConfig { +export function generateConfig(scaledCellWidth: number, scaledCellHeight: number, scaledCharWidth: number, scaledCharHeight: number, terminal: Terminal, colors: ReadonlyColorSet, devicePixelRatio: number): ICharAtlasConfig { // null out some fields that don't matter const clonedColors: IColorSet = { foreground: colors.foreground, @@ -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 32256cf4..b8725685 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 00000000..58084235 --- /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 00000000..db375778 --- /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 4764ede4..d450cfdc 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/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(); @@ -164,6 +153,7 @@ export class WebglCharAtlas implements IDisposable { this._currentRow.height = 0; this._fixedRows.length = 0; this._didWarmUp = false; + this.hasCanvasChanged = true; } public getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number): IRasterizedGlyph { @@ -184,12 +174,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 +194,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 +337,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 00000000..61a90890 --- /dev/null +++ b/src/browser/renderer/shared/Types.d.ts @@ -0,0 +1,154 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { FontWeight, Terminal } from 'xterm'; +import { IColorSet, ReadonlyColorSet } 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; + handleDevicePixelRatioChange(): void; + handleResize(cols: number, rows: number): void; + handleCharSizeChanged(): void; + handleBlur(): void; + handleFocus(): void; + handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void; + handleCursorMove(): 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/selection/SelectionModel.test.ts b/src/browser/selection/SelectionModel.test.ts index 5ce3e316..79ff372b 100644 --- a/src/browser/selection/SelectionModel.test.ts +++ b/src/browser/selection/SelectionModel.test.ts @@ -50,17 +50,17 @@ describe('SelectionModel', () => { it('should trim a portion of the selection when a part of it is trimmed', () => { model.selectionStart = [0, 0]; model.selectionEnd = [10, 2]; - model.onTrim(1); + model.handleTrim(1); assert.deepEqual(model.finalSelectionStart, [0, 0]); assert.deepEqual(model.finalSelectionEnd, [10, 1]); - model.onTrim(1); + model.handleTrim(1); assert.deepEqual(model.finalSelectionStart, [0, 0]); assert.deepEqual(model.finalSelectionEnd, [10, 0]); }); it('should clear selection when it is trimmed in its entirety', () => { model.selectionStart = [0, 0]; model.selectionEnd = [10, 0]; - model.onTrim(1); + model.handleTrim(1); assert.deepEqual(model.finalSelectionStart, undefined); assert.deepEqual(model.finalSelectionEnd, undefined); }); diff --git a/src/browser/selection/SelectionModel.ts b/src/browser/selection/SelectionModel.ts index 6c8abbfd..041c7b24 100644 --- a/src/browser/selection/SelectionModel.ts +++ b/src/browser/selection/SelectionModel.ts @@ -120,7 +120,7 @@ export class SelectionModel { * @param amount The amount the buffer is being trimmed. * @return Whether a refresh is necessary. */ - public onTrim(amount: number): boolean { + public handleTrim(amount: number): boolean { // Adjust the selection position based on the trimmed amount. if (this.selectionStart) { this.selectionStart[1] -= amount; diff --git a/src/browser/services/CharSizeService.ts b/src/browser/services/CharSizeService.ts index b04e157f..45bbe840 100644 --- a/src/browser/services/CharSizeService.ts +++ b/src/browser/services/CharSizeService.ts @@ -4,10 +4,12 @@ */ import { IOptionsService } from 'common/services/Services'; -import { IEvent, EventEmitter } from 'common/EventEmitter'; +import { EventEmitter } from 'common/EventEmitter'; import { ICharSizeService } from 'browser/services/Services'; +import { Disposable } from 'common/Lifecycle'; +import { ITerminalOptions } from 'common/Types'; -export class CharSizeService implements ICharSizeService { +export class CharSizeService extends Disposable implements ICharSizeService { public serviceBrand: undefined; public width: number = 0; @@ -16,15 +18,17 @@ 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 = this.register(new EventEmitter()); + public readonly onCharSizeChange = this._onCharSizeChange.event; constructor( document: Document, parentElement: HTMLElement, @IOptionsService private readonly _optionsService: IOptionsService ) { + super(); this._measureStrategy = new DomMeasureStrategy(document, parentElement, this._optionsService); + this.register(this._optionsService.onMultipleOptionChange(['fontFamily', 'fontSize'], () => this.measure())); } public measure(): void { diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index 97258609..190967db 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -3,16 +3,17 @@ * @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'; import { ScreenDprMonitor } from 'browser/ScreenDprMonitor'; import { addDisposableDomListener } from 'browser/Lifecycle'; -import { IColorSet, IRenderDebouncerWithCallback } from 'browser/Types'; +import { IColorSet, IRenderDebouncerWithCallback, ReadonlyColorSet } from 'browser/Types'; import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services'; -import { ICharSizeService, ICoreBrowserService, IRenderService } from 'browser/services/Services'; +import { ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services'; import { DebouncedIdleTask } from 'common/TaskQueue'; +import { ThemeService } from 'browser/services/ThemeService'; interface ISelectionState { start: [number, number] | undefined; @@ -23,6 +24,7 @@ interface ISelectionState { export class RenderService extends Disposable implements IRenderService { public serviceBrand: undefined; + private _renderer: IRenderer | undefined; private _renderDebouncer: IRenderDebouncerWithCallback; private _screenDprMonitor: ScreenDprMonitor; private _pausedResizeTask = new DebouncedIdleTask(); @@ -39,42 +41,42 @@ 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 = this.register(new EventEmitter()); + public readonly onDimensionsChange = this._onDimensionsChange.event; + private readonly _onRenderedViewportChange = this.register(new EventEmitter<{ start: number, end: number }>()); + public readonly onRenderedViewportChange = this._onRenderedViewportChange.event; + private readonly _onRender = this.register(new EventEmitter<{ start: number, end: number }>()); + public readonly onRender = this._onRender.event; + private readonly _onRefreshRequest = this.register(new EventEmitter<{ start: number, end: number }>()); + public readonly onRefreshRequest = this._onRefreshRequest.event; - public get dimensions(): IRenderDimensions { return this._renderer.dimensions; } + public get dimensions(): IRenderDimensions { return this._renderer!.dimensions; } constructor( - private _renderer: IRenderer, private _rowCount: number, screenElement: HTMLElement, @IOptionsService optionsService: IOptionsService, @ICharSizeService private readonly _charSizeService: ICharSizeService, @IDecorationService decorationService: IDecorationService, @IBufferService bufferService: IBufferService, - @ICoreBrowserService coreBrowserService: ICoreBrowserService + @ICoreBrowserService coreBrowserService: ICoreBrowserService, + @IThemeService themeService: IThemeService ) { super(); - this.register({ dispose: () => this._renderer.dispose() }); + this.register({ dispose: () => this._renderer?.dispose() }); this._renderDebouncer = new RenderDebouncer(coreBrowserService.window, (start, end) => this._renderRows(start, end)); this.register(this._renderDebouncer); this._screenDprMonitor = new ScreenDprMonitor(coreBrowserService.window); - this._screenDprMonitor.setListener(() => this.onDevicePixelRatioChange()); + this._screenDprMonitor.setListener(() => this.handleDevicePixelRatioChange()); this.register(this._screenDprMonitor); this.register(bufferService.onResize(() => this._fullRefresh())); this.register(bufferService.buffers.onBufferActivate(() => this._renderer?.clear())); this.register(optionsService.onOptionChange(() => this._handleOptionsChanged())); - this.register(this._charSizeService.onCharSizeChange(() => this.onCharSizeChanged())); + this.register(this._charSizeService.onCharSizeChange(() => this.handleCharSizeChanged())); // Do a full refresh whenever any decoration is added or removed. This may not actually result // in changes but since decorations should be used sparingly or added/removed all in the same @@ -82,23 +84,45 @@ export class RenderService extends Disposable implements IRenderService { this.register(decorationService.onDecorationRegistered(() => this._fullRefresh())); this.register(decorationService.onDecorationRemoved(() => this._fullRefresh())); - // No need to register this as renderer is explicitly disposed in RenderService.dispose - this._renderer.onRequestRedraw(e => this.refreshRows(e.start, e.end, true)); + // Clear the renderer when the a change that could affect glyphs occurs + this.register(optionsService.onMultipleOptionChange([ + 'customGlyphs', + 'drawBoldTextInBrightColors', + 'letterSpacing', + 'lineHeight', + 'fontFamily', + 'fontSize', + 'fontWeight', + 'fontWeightBold', + 'minimumContrastRatio' + ], () => { + this.clear(); + this.handleResize(bufferService.cols, bufferService.rows); + this._fullRefresh(); + })); + + // Refresh the cursor line when the cursor changes + this.register(optionsService.onMultipleOptionChange([ + 'cursorBlink', + 'cursorStyle' + ], () => this.refreshRows(bufferService.buffer.y, bufferService.buffer.y, true))); // dprchange should handle this case, we need this as well for browsers that don't support the // matchMedia query. - this.register(addDisposableDomListener(coreBrowserService.window, 'resize', () => this.onDevicePixelRatioChange())); + this.register(addDisposableDomListener(coreBrowserService.window, 'resize', () => this.handleDevicePixelRatioChange())); + + this.register(themeService.onChangeColors(() => this._fullRefresh())); // Detect whether IntersectionObserver is detected and enable renderer pause // and resume based on terminal visibility if so if ('IntersectionObserver' in coreBrowserService.window) { - const observer = new coreBrowserService.window.IntersectionObserver(e => this._onIntersectionChange(e[e.length - 1]), { threshold: 0 }); + const observer = new coreBrowserService.window.IntersectionObserver(e => this._handleIntersectionChange(e[e.length - 1]), { threshold: 0 }); observer.observe(screenElement); this.register({ dispose: () => observer.disconnect() }); } } - private _onIntersectionChange(entry: IntersectionObserverEntry): void { + private _handleIntersectionChange(entry: IntersectionObserverEntry): void { this._isPaused = entry.isIntersecting === undefined ? (entry.intersectionRatio === 0) : !entry.isIntersecting; // Terminal was hidden on open @@ -125,11 +149,14 @@ export class RenderService extends Disposable implements IRenderService { } private _renderRows(start: number, end: number): void { + if (!this._renderer) { + return; + } this._renderer.renderRows(start, end); // Update selection if needed if (this._needsSelectionRefresh) { - this._renderer.onSelectionChanged(this._selectionState.start, this._selectionState.end, this._selectionState.columnSelectMode); + this._renderer.handleSelectionChanged(this._selectionState.start, this._selectionState.end, this._selectionState.columnSelectMode); this._needsSelectionRefresh = false; } @@ -147,12 +174,17 @@ export class RenderService extends Disposable implements IRenderService { } private _handleOptionsChanged(): void { - this._renderer.onOptionsChanged(); + if (!this._renderer) { + return; + } this.refreshRows(0, this._rowCount - 1); this._fireOnCanvasResize(); } private _fireOnCanvasResize(): void { + if (!this._renderer) { + return; + } // Don't fire the event if the dimensions haven't changed if (this._renderer.dimensions.canvasWidth === this._canvasWidth && this._renderer.dimensions.canvasHeight === this._canvasHeight) { return; @@ -160,13 +192,13 @@ export class RenderService extends Disposable implements IRenderService { this._onDimensionsChange.fire(this._renderer.dimensions); } - public dispose(): void { - super.dispose(); + public hasRenderer(): boolean { + return !!this._renderer; } public setRenderer(renderer: IRenderer): void { // TODO: RenderService should be the only one to dispose the renderer - this._renderer.dispose(); + this._renderer?.dispose(); this._renderer = renderer; this._renderer.onRequestRedraw(e => this.refreshRows(e.start, e.end, true)); @@ -188,58 +220,62 @@ export class RenderService extends Disposable implements IRenderService { } public clearTextureAtlas(): void { - this._renderer?.clearTextureAtlas?.(); + if (!this._renderer) { + return; + } + this._renderer.clearTextureAtlas?.(); this._fullRefresh(); } - public setColors(colors: IColorSet): void { - this._renderer.setColors(colors); - this._fullRefresh(); - } - - public onDevicePixelRatioChange(): void { + public handleDevicePixelRatioChange(): void { // Force char size measurement as DomMeasureStrategy(getBoundingClientRect) is not stable // when devicePixelRatio changes this._charSizeService.measure(); - this._renderer.onDevicePixelRatioChange(); + if (!this._renderer) { + return; + } + this._renderer.handleDevicePixelRatioChange(); this.refreshRows(0, this._rowCount - 1); } - public onResize(cols: number, rows: number): void { + public handleResize(cols: number, rows: number): void { + if (!this._renderer) { + return; + } if (this._isPaused) { - this._pausedResizeTask.set(() => this._renderer.onResize(cols, rows)); + this._pausedResizeTask.set(() => this._renderer!.handleResize(cols, rows)); } else { - this._renderer.onResize(cols, rows); + this._renderer.handleResize(cols, rows); } this._fullRefresh(); } // TODO: Is this useful when we have onResize? - public onCharSizeChanged(): void { - this._renderer.onCharSizeChanged(); + public handleCharSizeChanged(): void { + this._renderer?.handleCharSizeChanged(); } - public onBlur(): void { - this._renderer.onBlur(); + public handleBlur(): void { + this._renderer?.handleBlur(); } - public onFocus(): void { - this._renderer.onFocus(); + public handleFocus(): void { + this._renderer?.handleFocus(); } - public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void { + public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void { this._selectionState.start = start; this._selectionState.end = end; this._selectionState.columnSelectMode = columnSelectMode; - this._renderer.onSelectionChanged(start, end, columnSelectMode); + this._renderer?.handleSelectionChanged(start, end, columnSelectMode); } - public onCursorMove(): void { - this._renderer.onCursorMove(); + public handleCursorMove(): void { + this._renderer?.handleCursorMove(); } public clear(): void { - this._renderer.clear(); + this._renderer?.clear(); } } diff --git a/src/browser/services/SelectionService.ts b/src/browser/services/SelectionService.ts index 4ee1ffa1..868fadda 100644 --- a/src/browser/services/SelectionService.ts +++ b/src/browser/services/SelectionService.ts @@ -15,7 +15,7 @@ import { IBufferRange, ILinkifier2 } from 'browser/Types'; import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services'; import { getCoordsRelativeToElement } from 'browser/input/Mouse'; import { moveToCellSequence } from 'browser/input/MoveToCell'; -import { Disposable } from 'common/Lifecycle'; +import { Disposable, toDisposable } from 'common/Lifecycle'; import { getRangeLength } from 'common/buffer/BufferRange'; /** @@ -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, @@ -134,24 +134,24 @@ export class SelectionService extends Disposable implements ISelectionService { super(); // Init listeners - this._mouseMoveListener = event => this._onMouseMove(event as MouseEvent); - this._mouseUpListener = event => this._onMouseUp(event as MouseEvent); + this._mouseMoveListener = event => this._handleMouseMove(event as MouseEvent); + this._mouseUpListener = event => this._handleMouseUp(event as MouseEvent); this._coreService.onUserInput(() => { if (this.hasSelection) { this.clearSelection(); } }); - this._trimListener = this._bufferService.buffer.lines.onTrim(amount => this._onTrim(amount)); - this.register(this._bufferService.buffers.onBufferActivate(e => this._onBufferActivate(e))); + this._trimListener = this._bufferService.buffer.lines.onTrim(amount => this._handleTrim(amount)); + this.register(this._bufferService.buffers.onBufferActivate(e => this._handleBufferActivate(e))); this.enable(); this._model = new SelectionModel(this._bufferService); this._activeSelectionMode = SelectionMode.NORMAL; - } - public dispose(): void { - this._removeMouseDownListeners(); + this.register(toDisposable(() => { + this._removeMouseDownListeners(); + })); } public reset(): void { @@ -375,8 +375,8 @@ export class SelectionService extends Disposable implements ISelectionService { * Handle the buffer being trimmed, adjust the selection position. * @param amount The amount the buffer is being trimmed. */ - private _onTrim(amount: number): void { - const needsRefresh = this._model.onTrim(amount); + private _handleTrim(amount: number): void { + const needsRefresh = this._model.handleTrim(amount); if (needsRefresh) { this.refresh(); } @@ -438,7 +438,7 @@ export class SelectionService extends Disposable implements ISelectionService { * Handles te mousedown event, setting up for a new selection. * @param event The mousedown event. */ - public onMouseDown(event: MouseEvent): void { + public handleMouseDown(event: MouseEvent): void { this._mouseDownTimeStamp = event.timeStamp; // If we have selection, we want the context menu on right click even if the // terminal is in mouse mode. @@ -468,14 +468,14 @@ export class SelectionService extends Disposable implements ISelectionService { this._dragScrollAmount = 0; if (this._enabled && event.shiftKey) { - this._onIncrementalClick(event); + this._handleIncrementalClick(event); } else { if (event.detail === 1) { - this._onSingleClick(event); + this._handleSingleClick(event); } else if (event.detail === 2) { - this._onDoubleClick(event); + this._handleDoubleClick(event); } else if (event.detail === 3) { - this._onTripleClick(event); + this._handleTripleClick(event); } } @@ -512,7 +512,7 @@ export class SelectionService extends Disposable implements ISelectionService { * position. * @param event The mouse event. */ - private _onIncrementalClick(event: MouseEvent): void { + private _handleIncrementalClick(event: MouseEvent): void { if (this._model.selectionStart) { this._model.selectionEnd = this._getMouseBufferCoords(event); } @@ -523,7 +523,7 @@ export class SelectionService extends Disposable implements ISelectionService { * start position. * @param event The mouse event. */ - private _onSingleClick(event: MouseEvent): void { + private _handleSingleClick(event: MouseEvent): void { this._model.selectionStartLength = 0; this._model.isSelectAllActive = false; this._activeSelectionMode = this.shouldColumnSelect(event) ? SelectionMode.COLUMN : SelectionMode.NORMAL; @@ -557,7 +557,7 @@ export class SelectionService extends Disposable implements ISelectionService { * Performs a double click, selecting the current word. * @param event The mouse event. */ - private _onDoubleClick(event: MouseEvent): void { + private _handleDoubleClick(event: MouseEvent): void { if (this._selectWordAtCursor(event, true)) { this._activeSelectionMode = SelectionMode.WORD; } @@ -568,7 +568,7 @@ export class SelectionService extends Disposable implements ISelectionService { * select mode. * @param event The mouse event. */ - private _onTripleClick(event: MouseEvent): void { + private _handleTripleClick(event: MouseEvent): void { const coords = this._getMouseBufferCoords(event); if (coords) { this._activeSelectionMode = SelectionMode.LINE; @@ -589,7 +589,7 @@ export class SelectionService extends Disposable implements ISelectionService { * end of the selection and refreshing the selection. * @param event The mousemove event. */ - private _onMouseMove(event: MouseEvent): void { + private _handleMouseMove(event: MouseEvent): void { // If the mousemove listener is active it means that a selection is // currently being made, we should stop propagation to prevent mouse events // to be sent to the pty. @@ -690,7 +690,7 @@ export class SelectionService extends Disposable implements ISelectionService { * Handles the mouseup event, removing the mousedown listeners. * @param event The mouseup event. */ - private _onMouseUp(event: MouseEvent): void { + private _handleMouseUp(event: MouseEvent): void { const timeElapsed = event.timeStamp - this._mouseDownTimeStamp; this._removeMouseDownListeners(); @@ -746,14 +746,14 @@ export class SelectionService extends Disposable implements ISelectionService { this._onSelectionChange.fire(); } - private _onBufferActivate(e: {activeBuffer: IBuffer, inactiveBuffer: IBuffer}): void { + private _handleBufferActivate(e: {activeBuffer: IBuffer, inactiveBuffer: IBuffer}): void { this.clearSelection(); // Only adjust the selection on trim, shiftElements is rarely used (only in // reverseIndex) and delete in a splice is only ever used when the same // number of elements was just added. Given this is could actually be // beneficial to leave the selection as is for these cases. this._trimListener.dispose(); - this._trimListener = e.activeBuffer.lines.onTrim(amount => this._onTrim(amount)); + this._trimListener = e.activeBuffer.lines.onTrim(amount => this._handleTrim(amount)); } /** diff --git a/src/browser/services/Services.ts b/src/browser/services/Services.ts index ab91f8a3..76a0f7df 100644 --- a/src/browser/services/Services.ts +++ b/src/browser/services/Services.ts @@ -4,11 +4,12 @@ */ import { IEvent } from 'common/EventEmitter'; -import { IRenderDimensions, IRenderer } from 'browser/renderer/Types'; -import { IColorSet } from 'browser/Types'; +import { IRenderDimensions, IRenderer } from 'browser/renderer/shared/Types'; +import { IColorSet, ReadonlyColorSet } from 'browser/Types'; import { ISelectionRedrawRequestEvent as ISelectionRequestRedrawEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; -import { IDisposable } from 'common/Types'; +import { ColorIndex, IDisposable } from 'common/Types'; +import { ITheme } from 'common/services/Services'; export const ICharSizeService = createDecorator('CharSizeService'); export interface ICharSizeService { @@ -71,16 +72,15 @@ export interface IRenderService extends IDisposable { refreshRows(start: number, end: number): void; clearTextureAtlas(): void; resize(cols: number, rows: number): void; + hasRenderer(): boolean; setRenderer(renderer: IRenderer): void; - setColors(colors: IColorSet): void; - onDevicePixelRatioChange(): void; - onResize(cols: number, rows: number): void; - // TODO: Is this useful when we have onResize? - onCharSizeChanged(): void; - onBlur(): void; - onFocus(): void; - onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void; - onCursorMove(): void; + handleDevicePixelRatioChange(): void; + handleResize(cols: number, rows: number): void; + handleCharSizeChanged(): void; + handleBlur(): void; + handleFocus(): void; + handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void; + handleCursorMove(): void; clear(): void; } @@ -109,7 +109,7 @@ export interface ISelectionService { shouldColumnSelect(event: KeyboardEvent | MouseEvent): boolean; shouldForceSelection(event: MouseEvent): boolean; refresh(isLinuxMouseSelection?: boolean): void; - onMouseDown(event: MouseEvent): void; + handleMouseDown(event: MouseEvent): void; isCellInSelection(x: number, y: number): boolean; } @@ -121,3 +121,19 @@ export interface ICharacterJoinerService { deregister(joinerId: number): boolean; getJoinedCharacters(row: number): [number, number][]; } + +export const IThemeService = createDecorator('ThemeService'); +export interface IThemeService { + serviceBrand: undefined; + + readonly colors: ReadonlyColorSet; + + readonly onChangeColors: IEvent; + + restoreColor(slot?: ColorIndex): void; + /** + * Allows external modifying of colors in the theme, this is used instead of {@link colors} to + * prevent accidental writes. + */ + modifyColors(callback: (colors: IColorSet) => void): void; +} diff --git a/src/browser/services/ThemeService.test.ts b/src/browser/services/ThemeService.test.ts new file mode 100644 index 00000000..f2b2def3 --- /dev/null +++ b/src/browser/services/ThemeService.test.ts @@ -0,0 +1,367 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import jsdom = require('jsdom'); +import { assert } from 'chai'; +import { ThemeService, DEFAULT_ANSI_COLORS } from 'browser/services/ThemeService'; +import { OptionsService } from 'common/services/OptionsService'; + +describe('ThemeService', () => { + let themeService: ThemeService; + let dom: jsdom.JSDOM; + let window: jsdom.DOMWindow; + let optionsService: OptionsService; + + beforeEach(() => { + dom = new jsdom.JSDOM(''); + window = dom.window; + (window as any).HTMLCanvasElement.prototype.getContext = () => ({ + createLinearGradient(): any { + return null; + }, + + fillRect(): void { }, + + getImageData(): any { + return {data: [0, 0, 0, 0xFF]}; + } + }); + optionsService = new OptionsService({}); + themeService = new ThemeService(optionsService); + }); + + describe('constructor', () => { + it('should fill all colors with values', () => { + for (const key of Object.keys(themeService.colors)) { + if (key !== 'ansi' && key !== 'contrastCache' && key !== 'selectionForeground') { + // A #rrggbb or rgba(...) + assert.ok((themeService.colors as any)[key].css.length >= 7); + } + } + assert.equal(themeService.colors.ansi.length, 256); + }); + + it('should fill 240 colors with expected values', () => { + assert.equal(themeService.colors.ansi[16].css, '#000000'); + assert.equal(themeService.colors.ansi[17].css, '#00005f'); + assert.equal(themeService.colors.ansi[18].css, '#000087'); + assert.equal(themeService.colors.ansi[19].css, '#0000af'); + assert.equal(themeService.colors.ansi[20].css, '#0000d7'); + assert.equal(themeService.colors.ansi[21].css, '#0000ff'); + assert.equal(themeService.colors.ansi[22].css, '#005f00'); + assert.equal(themeService.colors.ansi[23].css, '#005f5f'); + assert.equal(themeService.colors.ansi[24].css, '#005f87'); + assert.equal(themeService.colors.ansi[25].css, '#005faf'); + assert.equal(themeService.colors.ansi[26].css, '#005fd7'); + assert.equal(themeService.colors.ansi[27].css, '#005fff'); + assert.equal(themeService.colors.ansi[28].css, '#008700'); + assert.equal(themeService.colors.ansi[29].css, '#00875f'); + assert.equal(themeService.colors.ansi[30].css, '#008787'); + assert.equal(themeService.colors.ansi[31].css, '#0087af'); + assert.equal(themeService.colors.ansi[32].css, '#0087d7'); + assert.equal(themeService.colors.ansi[33].css, '#0087ff'); + assert.equal(themeService.colors.ansi[34].css, '#00af00'); + assert.equal(themeService.colors.ansi[35].css, '#00af5f'); + assert.equal(themeService.colors.ansi[36].css, '#00af87'); + assert.equal(themeService.colors.ansi[37].css, '#00afaf'); + assert.equal(themeService.colors.ansi[38].css, '#00afd7'); + assert.equal(themeService.colors.ansi[39].css, '#00afff'); + assert.equal(themeService.colors.ansi[40].css, '#00d700'); + assert.equal(themeService.colors.ansi[41].css, '#00d75f'); + assert.equal(themeService.colors.ansi[42].css, '#00d787'); + assert.equal(themeService.colors.ansi[43].css, '#00d7af'); + assert.equal(themeService.colors.ansi[44].css, '#00d7d7'); + assert.equal(themeService.colors.ansi[45].css, '#00d7ff'); + assert.equal(themeService.colors.ansi[46].css, '#00ff00'); + assert.equal(themeService.colors.ansi[47].css, '#00ff5f'); + assert.equal(themeService.colors.ansi[48].css, '#00ff87'); + assert.equal(themeService.colors.ansi[49].css, '#00ffaf'); + assert.equal(themeService.colors.ansi[50].css, '#00ffd7'); + assert.equal(themeService.colors.ansi[51].css, '#00ffff'); + assert.equal(themeService.colors.ansi[52].css, '#5f0000'); + assert.equal(themeService.colors.ansi[53].css, '#5f005f'); + assert.equal(themeService.colors.ansi[54].css, '#5f0087'); + assert.equal(themeService.colors.ansi[55].css, '#5f00af'); + assert.equal(themeService.colors.ansi[56].css, '#5f00d7'); + assert.equal(themeService.colors.ansi[57].css, '#5f00ff'); + assert.equal(themeService.colors.ansi[58].css, '#5f5f00'); + assert.equal(themeService.colors.ansi[59].css, '#5f5f5f'); + assert.equal(themeService.colors.ansi[60].css, '#5f5f87'); + assert.equal(themeService.colors.ansi[61].css, '#5f5faf'); + assert.equal(themeService.colors.ansi[62].css, '#5f5fd7'); + assert.equal(themeService.colors.ansi[63].css, '#5f5fff'); + assert.equal(themeService.colors.ansi[64].css, '#5f8700'); + assert.equal(themeService.colors.ansi[65].css, '#5f875f'); + assert.equal(themeService.colors.ansi[66].css, '#5f8787'); + assert.equal(themeService.colors.ansi[67].css, '#5f87af'); + assert.equal(themeService.colors.ansi[68].css, '#5f87d7'); + assert.equal(themeService.colors.ansi[69].css, '#5f87ff'); + assert.equal(themeService.colors.ansi[70].css, '#5faf00'); + assert.equal(themeService.colors.ansi[71].css, '#5faf5f'); + assert.equal(themeService.colors.ansi[72].css, '#5faf87'); + assert.equal(themeService.colors.ansi[73].css, '#5fafaf'); + assert.equal(themeService.colors.ansi[74].css, '#5fafd7'); + assert.equal(themeService.colors.ansi[75].css, '#5fafff'); + assert.equal(themeService.colors.ansi[76].css, '#5fd700'); + assert.equal(themeService.colors.ansi[77].css, '#5fd75f'); + assert.equal(themeService.colors.ansi[78].css, '#5fd787'); + assert.equal(themeService.colors.ansi[79].css, '#5fd7af'); + assert.equal(themeService.colors.ansi[80].css, '#5fd7d7'); + assert.equal(themeService.colors.ansi[81].css, '#5fd7ff'); + assert.equal(themeService.colors.ansi[82].css, '#5fff00'); + assert.equal(themeService.colors.ansi[83].css, '#5fff5f'); + assert.equal(themeService.colors.ansi[84].css, '#5fff87'); + assert.equal(themeService.colors.ansi[85].css, '#5fffaf'); + assert.equal(themeService.colors.ansi[86].css, '#5fffd7'); + assert.equal(themeService.colors.ansi[87].css, '#5fffff'); + assert.equal(themeService.colors.ansi[88].css, '#870000'); + assert.equal(themeService.colors.ansi[89].css, '#87005f'); + assert.equal(themeService.colors.ansi[90].css, '#870087'); + assert.equal(themeService.colors.ansi[91].css, '#8700af'); + assert.equal(themeService.colors.ansi[92].css, '#8700d7'); + assert.equal(themeService.colors.ansi[93].css, '#8700ff'); + assert.equal(themeService.colors.ansi[94].css, '#875f00'); + assert.equal(themeService.colors.ansi[95].css, '#875f5f'); + assert.equal(themeService.colors.ansi[96].css, '#875f87'); + assert.equal(themeService.colors.ansi[97].css, '#875faf'); + assert.equal(themeService.colors.ansi[98].css, '#875fd7'); + assert.equal(themeService.colors.ansi[99].css, '#875fff'); + assert.equal(themeService.colors.ansi[100].css, '#878700'); + assert.equal(themeService.colors.ansi[101].css, '#87875f'); + assert.equal(themeService.colors.ansi[102].css, '#878787'); + assert.equal(themeService.colors.ansi[103].css, '#8787af'); + assert.equal(themeService.colors.ansi[104].css, '#8787d7'); + assert.equal(themeService.colors.ansi[105].css, '#8787ff'); + assert.equal(themeService.colors.ansi[106].css, '#87af00'); + assert.equal(themeService.colors.ansi[107].css, '#87af5f'); + assert.equal(themeService.colors.ansi[108].css, '#87af87'); + assert.equal(themeService.colors.ansi[109].css, '#87afaf'); + assert.equal(themeService.colors.ansi[110].css, '#87afd7'); + assert.equal(themeService.colors.ansi[111].css, '#87afff'); + assert.equal(themeService.colors.ansi[112].css, '#87d700'); + assert.equal(themeService.colors.ansi[113].css, '#87d75f'); + assert.equal(themeService.colors.ansi[114].css, '#87d787'); + assert.equal(themeService.colors.ansi[115].css, '#87d7af'); + assert.equal(themeService.colors.ansi[116].css, '#87d7d7'); + assert.equal(themeService.colors.ansi[117].css, '#87d7ff'); + assert.equal(themeService.colors.ansi[118].css, '#87ff00'); + assert.equal(themeService.colors.ansi[119].css, '#87ff5f'); + assert.equal(themeService.colors.ansi[120].css, '#87ff87'); + assert.equal(themeService.colors.ansi[121].css, '#87ffaf'); + assert.equal(themeService.colors.ansi[122].css, '#87ffd7'); + assert.equal(themeService.colors.ansi[123].css, '#87ffff'); + assert.equal(themeService.colors.ansi[124].css, '#af0000'); + assert.equal(themeService.colors.ansi[125].css, '#af005f'); + assert.equal(themeService.colors.ansi[126].css, '#af0087'); + assert.equal(themeService.colors.ansi[127].css, '#af00af'); + assert.equal(themeService.colors.ansi[128].css, '#af00d7'); + assert.equal(themeService.colors.ansi[129].css, '#af00ff'); + assert.equal(themeService.colors.ansi[130].css, '#af5f00'); + assert.equal(themeService.colors.ansi[131].css, '#af5f5f'); + assert.equal(themeService.colors.ansi[132].css, '#af5f87'); + assert.equal(themeService.colors.ansi[133].css, '#af5faf'); + assert.equal(themeService.colors.ansi[134].css, '#af5fd7'); + assert.equal(themeService.colors.ansi[135].css, '#af5fff'); + assert.equal(themeService.colors.ansi[136].css, '#af8700'); + assert.equal(themeService.colors.ansi[137].css, '#af875f'); + assert.equal(themeService.colors.ansi[138].css, '#af8787'); + assert.equal(themeService.colors.ansi[139].css, '#af87af'); + assert.equal(themeService.colors.ansi[140].css, '#af87d7'); + assert.equal(themeService.colors.ansi[141].css, '#af87ff'); + assert.equal(themeService.colors.ansi[142].css, '#afaf00'); + assert.equal(themeService.colors.ansi[143].css, '#afaf5f'); + assert.equal(themeService.colors.ansi[144].css, '#afaf87'); + assert.equal(themeService.colors.ansi[145].css, '#afafaf'); + assert.equal(themeService.colors.ansi[146].css, '#afafd7'); + assert.equal(themeService.colors.ansi[147].css, '#afafff'); + assert.equal(themeService.colors.ansi[148].css, '#afd700'); + assert.equal(themeService.colors.ansi[149].css, '#afd75f'); + assert.equal(themeService.colors.ansi[150].css, '#afd787'); + assert.equal(themeService.colors.ansi[151].css, '#afd7af'); + assert.equal(themeService.colors.ansi[152].css, '#afd7d7'); + assert.equal(themeService.colors.ansi[153].css, '#afd7ff'); + assert.equal(themeService.colors.ansi[154].css, '#afff00'); + assert.equal(themeService.colors.ansi[155].css, '#afff5f'); + assert.equal(themeService.colors.ansi[156].css, '#afff87'); + assert.equal(themeService.colors.ansi[157].css, '#afffaf'); + assert.equal(themeService.colors.ansi[158].css, '#afffd7'); + assert.equal(themeService.colors.ansi[159].css, '#afffff'); + assert.equal(themeService.colors.ansi[160].css, '#d70000'); + assert.equal(themeService.colors.ansi[161].css, '#d7005f'); + assert.equal(themeService.colors.ansi[162].css, '#d70087'); + assert.equal(themeService.colors.ansi[163].css, '#d700af'); + assert.equal(themeService.colors.ansi[164].css, '#d700d7'); + assert.equal(themeService.colors.ansi[165].css, '#d700ff'); + assert.equal(themeService.colors.ansi[166].css, '#d75f00'); + assert.equal(themeService.colors.ansi[167].css, '#d75f5f'); + assert.equal(themeService.colors.ansi[168].css, '#d75f87'); + assert.equal(themeService.colors.ansi[169].css, '#d75faf'); + assert.equal(themeService.colors.ansi[170].css, '#d75fd7'); + assert.equal(themeService.colors.ansi[171].css, '#d75fff'); + assert.equal(themeService.colors.ansi[172].css, '#d78700'); + assert.equal(themeService.colors.ansi[173].css, '#d7875f'); + assert.equal(themeService.colors.ansi[174].css, '#d78787'); + assert.equal(themeService.colors.ansi[175].css, '#d787af'); + assert.equal(themeService.colors.ansi[176].css, '#d787d7'); + assert.equal(themeService.colors.ansi[177].css, '#d787ff'); + assert.equal(themeService.colors.ansi[178].css, '#d7af00'); + assert.equal(themeService.colors.ansi[179].css, '#d7af5f'); + assert.equal(themeService.colors.ansi[180].css, '#d7af87'); + assert.equal(themeService.colors.ansi[181].css, '#d7afaf'); + assert.equal(themeService.colors.ansi[182].css, '#d7afd7'); + assert.equal(themeService.colors.ansi[183].css, '#d7afff'); + assert.equal(themeService.colors.ansi[184].css, '#d7d700'); + assert.equal(themeService.colors.ansi[185].css, '#d7d75f'); + assert.equal(themeService.colors.ansi[186].css, '#d7d787'); + assert.equal(themeService.colors.ansi[187].css, '#d7d7af'); + assert.equal(themeService.colors.ansi[188].css, '#d7d7d7'); + assert.equal(themeService.colors.ansi[189].css, '#d7d7ff'); + assert.equal(themeService.colors.ansi[190].css, '#d7ff00'); + assert.equal(themeService.colors.ansi[191].css, '#d7ff5f'); + assert.equal(themeService.colors.ansi[192].css, '#d7ff87'); + assert.equal(themeService.colors.ansi[193].css, '#d7ffaf'); + assert.equal(themeService.colors.ansi[194].css, '#d7ffd7'); + assert.equal(themeService.colors.ansi[195].css, '#d7ffff'); + assert.equal(themeService.colors.ansi[196].css, '#ff0000'); + assert.equal(themeService.colors.ansi[197].css, '#ff005f'); + assert.equal(themeService.colors.ansi[198].css, '#ff0087'); + assert.equal(themeService.colors.ansi[199].css, '#ff00af'); + assert.equal(themeService.colors.ansi[200].css, '#ff00d7'); + assert.equal(themeService.colors.ansi[201].css, '#ff00ff'); + assert.equal(themeService.colors.ansi[202].css, '#ff5f00'); + assert.equal(themeService.colors.ansi[203].css, '#ff5f5f'); + assert.equal(themeService.colors.ansi[204].css, '#ff5f87'); + assert.equal(themeService.colors.ansi[205].css, '#ff5faf'); + assert.equal(themeService.colors.ansi[206].css, '#ff5fd7'); + assert.equal(themeService.colors.ansi[207].css, '#ff5fff'); + assert.equal(themeService.colors.ansi[208].css, '#ff8700'); + assert.equal(themeService.colors.ansi[209].css, '#ff875f'); + assert.equal(themeService.colors.ansi[210].css, '#ff8787'); + assert.equal(themeService.colors.ansi[211].css, '#ff87af'); + assert.equal(themeService.colors.ansi[212].css, '#ff87d7'); + assert.equal(themeService.colors.ansi[213].css, '#ff87ff'); + assert.equal(themeService.colors.ansi[214].css, '#ffaf00'); + assert.equal(themeService.colors.ansi[215].css, '#ffaf5f'); + assert.equal(themeService.colors.ansi[216].css, '#ffaf87'); + assert.equal(themeService.colors.ansi[217].css, '#ffafaf'); + assert.equal(themeService.colors.ansi[218].css, '#ffafd7'); + assert.equal(themeService.colors.ansi[219].css, '#ffafff'); + assert.equal(themeService.colors.ansi[220].css, '#ffd700'); + assert.equal(themeService.colors.ansi[221].css, '#ffd75f'); + assert.equal(themeService.colors.ansi[222].css, '#ffd787'); + assert.equal(themeService.colors.ansi[223].css, '#ffd7af'); + assert.equal(themeService.colors.ansi[224].css, '#ffd7d7'); + assert.equal(themeService.colors.ansi[225].css, '#ffd7ff'); + assert.equal(themeService.colors.ansi[226].css, '#ffff00'); + assert.equal(themeService.colors.ansi[227].css, '#ffff5f'); + assert.equal(themeService.colors.ansi[228].css, '#ffff87'); + assert.equal(themeService.colors.ansi[229].css, '#ffffaf'); + assert.equal(themeService.colors.ansi[230].css, '#ffffd7'); + assert.equal(themeService.colors.ansi[231].css, '#ffffff'); + assert.equal(themeService.colors.ansi[232].css, '#080808'); + assert.equal(themeService.colors.ansi[233].css, '#121212'); + assert.equal(themeService.colors.ansi[234].css, '#1c1c1c'); + assert.equal(themeService.colors.ansi[235].css, '#262626'); + assert.equal(themeService.colors.ansi[236].css, '#303030'); + assert.equal(themeService.colors.ansi[237].css, '#3a3a3a'); + assert.equal(themeService.colors.ansi[238].css, '#444444'); + assert.equal(themeService.colors.ansi[239].css, '#4e4e4e'); + assert.equal(themeService.colors.ansi[240].css, '#585858'); + assert.equal(themeService.colors.ansi[241].css, '#626262'); + assert.equal(themeService.colors.ansi[242].css, '#6c6c6c'); + assert.equal(themeService.colors.ansi[243].css, '#767676'); + assert.equal(themeService.colors.ansi[244].css, '#808080'); + assert.equal(themeService.colors.ansi[245].css, '#8a8a8a'); + assert.equal(themeService.colors.ansi[246].css, '#949494'); + assert.equal(themeService.colors.ansi[247].css, '#9e9e9e'); + assert.equal(themeService.colors.ansi[248].css, '#a8a8a8'); + assert.equal(themeService.colors.ansi[249].css, '#b2b2b2'); + assert.equal(themeService.colors.ansi[250].css, '#bcbcbc'); + assert.equal(themeService.colors.ansi[251].css, '#c6c6c6'); + assert.equal(themeService.colors.ansi[252].css, '#d0d0d0'); + assert.equal(themeService.colors.ansi[253].css, '#dadada'); + assert.equal(themeService.colors.ansi[254].css, '#e4e4e4'); + assert.equal(themeService.colors.ansi[255].css, '#eeeeee'); + }); + }); + + describe('setTheme', () => { + it('should not throw when not setting all colors', () => { + assert.doesNotThrow(() => { + optionsService.options.theme = {}; + }); + }); + + it('should set a partial set of colors, using the default if not present', () => { + assert.equal(themeService.colors.background.css, '#000000'); + assert.equal(themeService.colors.foreground.css, '#ffffff'); + optionsService.options.theme = { + background: '#FF0000', + foreground: '#00FF00' + }; + assert.equal(themeService.colors.background.css, '#FF0000'); + assert.equal(themeService.colors.foreground.css, '#00FF00'); + optionsService.options.theme = { + background: '#0000FF' + }; + assert.equal(themeService.colors.background.css, '#0000FF'); + // FG reverts back to default + assert.equal(themeService.colors.foreground.css, '#ffffff'); + }); + + it('should set all extended ansi colors in reverse order', () => { + optionsService.options.theme = { + extendedAnsi: DEFAULT_ANSI_COLORS.map(a => a.css).slice().reverse() + }; + + for (let ansiColor = 16; ansiColor <= 255; ansiColor++) { + assert.equal(themeService.colors.ansi[ansiColor].css, DEFAULT_ANSI_COLORS[255 + 16 - ansiColor].css); + } + }); + + it('should set one extended ansi color and keep the other default', () => { + optionsService.options.theme = { + extendedAnsi: ['#ffffff'] + }; + + assert.equal(themeService.colors.ansi[16].css, '#ffffff'); + assert.equal(themeService.colors.ansi[17].css, DEFAULT_ANSI_COLORS[17].css); + }); + + it('should set extended ansi colors to the default when they are unset', () => { + optionsService.options.theme = { + extendedAnsi: ['#ffffff'] + }; + assert.equal(themeService.colors.ansi[16].css, '#ffffff'); + + optionsService.options.theme = { + extendedAnsi: [] + }; + assert.equal(themeService.colors.ansi[16].css, DEFAULT_ANSI_COLORS[16].css); + + optionsService.options.theme = { + extendedAnsi: ['#ffffff'] + }; + assert.equal(themeService.colors.ansi[16].css, '#ffffff'); + + optionsService.options.theme = {}; + assert.equal(themeService.colors.ansi[16].css, DEFAULT_ANSI_COLORS[16].css); + }); + + it('should set extended ansi colors to the default when they are partially unset', () => { + optionsService.options.theme = { + extendedAnsi: ['#ffffff', '#000000'] + }; + assert.equal(themeService.colors.ansi[16].css, '#ffffff'); + assert.equal(themeService.colors.ansi[17].css, '#000000'); + + optionsService.options.theme = { + extendedAnsi: ['#ffffff'] + }; + assert.equal(themeService.colors.ansi[16].css, '#ffffff'); + assert.equal(themeService.colors.ansi[17].css, DEFAULT_ANSI_COLORS[17].css); + }); + }); +}); diff --git a/src/browser/services/ThemeService.ts b/src/browser/services/ThemeService.ts new file mode 100644 index 00000000..ac0104d5 --- /dev/null +++ b/src/browser/services/ThemeService.ts @@ -0,0 +1,235 @@ +/** + * Copyright (c) 2022 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ColorContrastCache } from 'browser/ColorContrastCache'; +import { IThemeService } from 'browser/services/Services'; +import { IColorContrastCache, IColorSet, ReadonlyColorSet } from 'browser/Types'; +import { channels, color, css, NULL_COLOR } from 'common/Color'; +import { EventEmitter } from 'common/EventEmitter'; +import { Disposable } from 'common/Lifecycle'; +import { IOptionsService, ITheme } from 'common/services/Services'; +import { ColorIndex, IColor } from 'common/Types'; + +interface IRestoreColorSet { + foreground: IColor; + background: IColor; + cursor: IColor; + ansi: IColor[]; +} + + +const DEFAULT_FOREGROUND = css.toColor('#ffffff'); +const DEFAULT_BACKGROUND = css.toColor('#000000'); +const DEFAULT_CURSOR = css.toColor('#ffffff'); +const DEFAULT_CURSOR_ACCENT = css.toColor('#000000'); +const DEFAULT_SELECTION = { + css: 'rgba(255, 255, 255, 0.3)', + rgba: 0xFFFFFF4D +}; + +// An IIFE to generate DEFAULT_ANSI_COLORS. +export const DEFAULT_ANSI_COLORS = Object.freeze((() => { + const colors = [ + // dark: + css.toColor('#2e3436'), + css.toColor('#cc0000'), + css.toColor('#4e9a06'), + css.toColor('#c4a000'), + css.toColor('#3465a4'), + css.toColor('#75507b'), + css.toColor('#06989a'), + css.toColor('#d3d7cf'), + // bright: + css.toColor('#555753'), + css.toColor('#ef2929'), + css.toColor('#8ae234'), + css.toColor('#fce94f'), + css.toColor('#729fcf'), + css.toColor('#ad7fa8'), + css.toColor('#34e2e2'), + css.toColor('#eeeeec') + ]; + + // Fill in the remaining 240 ANSI colors. + // Generate colors (16-231) + const v = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]; + for (let i = 0; i < 216; i++) { + const r = v[(i / 36) % 6 | 0]; + const g = v[(i / 6) % 6 | 0]; + const b = v[i % 6]; + colors.push({ + css: channels.toCss(r, g, b), + rgba: channels.toRgba(r, g, b) + }); + } + + // Generate greys (232-255) + for (let i = 0; i < 24; i++) { + const c = 8 + i * 10; + colors.push({ + css: channels.toCss(c, c, c), + rgba: channels.toRgba(c, c, c) + }); + } + + return colors; +})()); + +export class ThemeService extends Disposable implements IThemeService { + public serviceBrand: undefined; + + private _colors: IColorSet; + private _contrastCache: IColorContrastCache; + private _restoreColors!: IRestoreColorSet; + + public get colors(): ReadonlyColorSet { return this._colors; } + + private readonly _onChangeColors = this.register(new EventEmitter()); + public readonly onChangeColors = this._onChangeColors.event; + + constructor( + @IOptionsService private readonly _optionsService: IOptionsService + ) { + super(); + + this._contrastCache = new ColorContrastCache(); + this._colors = { + foreground: DEFAULT_FOREGROUND, + background: DEFAULT_BACKGROUND, + cursor: DEFAULT_CURSOR, + cursorAccent: DEFAULT_CURSOR_ACCENT, + selectionForeground: undefined, + selectionBackgroundTransparent: DEFAULT_SELECTION, + selectionBackgroundOpaque: color.blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION), + selectionInactiveBackgroundTransparent: DEFAULT_SELECTION, + selectionInactiveBackgroundOpaque: color.blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION), + ansi: DEFAULT_ANSI_COLORS.slice(), + contrastCache: this._contrastCache + }; + this._updateRestoreColors(); + this._setTheme(this._optionsService.rawOptions.theme); + + this.register(this._optionsService.onSpecificOptionChange('minimumContrastRatio', () => this._contrastCache.clear())); + this.register(this._optionsService.onSpecificOptionChange('theme', () => this._setTheme(this._optionsService.rawOptions.theme))); + } + + /** + * Sets the terminal's theme. + * @param theme The theme to use. If a partial theme is provided then default + * colors will be used where colors are not defined. + */ + private _setTheme(theme: ITheme = {}): void { + const colors = this._colors; + colors.foreground = parseColor(theme.foreground, DEFAULT_FOREGROUND); + colors.background = parseColor(theme.background, DEFAULT_BACKGROUND); + colors.cursor = parseColor(theme.cursor, DEFAULT_CURSOR); + colors.cursorAccent = parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT); + colors.selectionBackgroundTransparent = parseColor(theme.selectionBackground, DEFAULT_SELECTION); + colors.selectionBackgroundOpaque = color.blend(colors.background, colors.selectionBackgroundTransparent); + colors.selectionInactiveBackgroundTransparent = parseColor(theme.selectionInactiveBackground, colors.selectionBackgroundTransparent); + colors.selectionInactiveBackgroundOpaque = color.blend(colors.background, colors.selectionInactiveBackgroundTransparent); + colors.selectionForeground = theme.selectionForeground ? parseColor(theme.selectionForeground, NULL_COLOR) : undefined; + if (colors.selectionForeground === NULL_COLOR) { + colors.selectionForeground = undefined; + } + + /** + * If selection color is opaque, blend it with background with 0.3 opacity + * Issue #2737 + */ + if (color.isOpaque(colors.selectionBackgroundTransparent)) { + const opacity = 0.3; + colors.selectionBackgroundTransparent = color.opacity(colors.selectionBackgroundTransparent, opacity); + } + if (color.isOpaque(colors.selectionInactiveBackgroundTransparent)) { + const opacity = 0.3; + colors.selectionInactiveBackgroundTransparent = color.opacity(colors.selectionInactiveBackgroundTransparent, opacity); + } + colors.ansi = DEFAULT_ANSI_COLORS.slice(); + colors.ansi[0] = parseColor(theme.black, DEFAULT_ANSI_COLORS[0]); + colors.ansi[1] = parseColor(theme.red, DEFAULT_ANSI_COLORS[1]); + colors.ansi[2] = parseColor(theme.green, DEFAULT_ANSI_COLORS[2]); + colors.ansi[3] = parseColor(theme.yellow, DEFAULT_ANSI_COLORS[3]); + colors.ansi[4] = parseColor(theme.blue, DEFAULT_ANSI_COLORS[4]); + colors.ansi[5] = parseColor(theme.magenta, DEFAULT_ANSI_COLORS[5]); + colors.ansi[6] = parseColor(theme.cyan, DEFAULT_ANSI_COLORS[6]); + colors.ansi[7] = parseColor(theme.white, DEFAULT_ANSI_COLORS[7]); + colors.ansi[8] = parseColor(theme.brightBlack, DEFAULT_ANSI_COLORS[8]); + colors.ansi[9] = parseColor(theme.brightRed, DEFAULT_ANSI_COLORS[9]); + colors.ansi[10] = parseColor(theme.brightGreen, DEFAULT_ANSI_COLORS[10]); + colors.ansi[11] = parseColor(theme.brightYellow, DEFAULT_ANSI_COLORS[11]); + colors.ansi[12] = parseColor(theme.brightBlue, DEFAULT_ANSI_COLORS[12]); + colors.ansi[13] = parseColor(theme.brightMagenta, DEFAULT_ANSI_COLORS[13]); + colors.ansi[14] = parseColor(theme.brightCyan, DEFAULT_ANSI_COLORS[14]); + colors.ansi[15] = parseColor(theme.brightWhite, DEFAULT_ANSI_COLORS[15]); + if (theme.extendedAnsi) { + const colorCount = Math.min(colors.ansi.length - 16, theme.extendedAnsi.length); + for (let i = 0; i < colorCount; i++) { + colors.ansi[i + 16] = parseColor(theme.extendedAnsi[i], DEFAULT_ANSI_COLORS[i + 16]); + } + } + // Clear our the cache + this._contrastCache.clear(); + this._updateRestoreColors(); + this._onChangeColors.fire(this.colors); + } + + public restoreColor(slot?: ColorIndex): void { + this._restoreColor(slot); + this._onChangeColors.fire(this.colors); + } + + private _restoreColor(slot: ColorIndex | undefined): void { + // unset slot restores all ansi colors + if (slot === undefined) { + for (let i = 0; i < this._restoreColors.ansi.length; ++i) { + this._colors.ansi[i] = this._restoreColors.ansi[i]; + } + return; + } + switch (slot) { + case ColorIndex.FOREGROUND: + this._colors.foreground = this._restoreColors.foreground; + break; + case ColorIndex.BACKGROUND: + this._colors.background = this._restoreColors.background; + break; + case ColorIndex.CURSOR: + this._colors.cursor = this._restoreColors.cursor; + break; + default: + this._colors.ansi[slot] = this._restoreColors.ansi[slot]; + } + } + + public modifyColors(callback: (colors: IColorSet) => void): void { + callback(this._colors); + // Assume the change happened + this._onChangeColors.fire(this.colors); + } + + private _updateRestoreColors(): void { + this._restoreColors = { + foreground: this._colors.foreground, + background: this._colors.background, + cursor: this._colors.cursor, + ansi: this._colors.ansi.slice() + }; + } +} + +function parseColor( + cssString: string | undefined, + fallback: IColor +): IColor { + if (cssString !== undefined) { + try { + return css.toColor(cssString); + } catch { + // no-op + } + } + return fallback; +} diff --git a/src/common/CircularList.ts b/src/common/CircularList.ts index 4d2c04ec..b7e1e075 100644 --- a/src/common/CircularList.ts +++ b/src/common/CircularList.ts @@ -5,6 +5,7 @@ import { ICircularList } from 'common/Types'; import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { Disposable } from 'common/Lifecycle'; export interface IInsertEvent { index: number; @@ -20,21 +21,22 @@ export interface IDeleteEvent { * Represents a circular list; a list with a maximum size that wraps around when push is called, * overriding values at the start of the list. */ -export class CircularList implements ICircularList { +export class CircularList extends Disposable implements ICircularList { protected _array: (T | undefined)[]; 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 = this.register(new EventEmitter()); + public readonly onDelete = this.onDeleteEmitter.event; + public readonly onInsertEmitter = this.register(new EventEmitter()); + public readonly onInsert = this.onInsertEmitter.event; + public readonly onTrimEmitter = this.register(new EventEmitter()); + public readonly onTrim = this.onTrimEmitter.event; constructor( private _maxLength: number ) { + super(); this._array = new Array(this._maxLength); this._startIndex = 0; this._length = 0; diff --git a/src/common/Color.ts b/src/common/Color.ts index 6e70671b..0d7bffe0 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 d7cb0f7e..f4a7d301 100644 --- a/src/common/CoreTerminal.ts +++ b/src/common/CoreTerminal.ts @@ -21,8 +21,8 @@ * http://linux.die.net/man/7/urxvt */ -import { Disposable } from 'common/Lifecycle'; -import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, ICoreMouseService, IUnicodeService, IDirtyRowService, LogLevelEnum, ITerminalOptions, IOscLinkService } from 'common/services/Services'; +import { Disposable, toDisposable } from 'common/Lifecycle'; +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,28 +59,29 @@ 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; } - 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(); + private readonly _onBinary = this.register(new EventEmitter()); + public readonly onBinary = this._onBinary.event; + private readonly _onData = this.register(new EventEmitter()); + public readonly onData = this._onData.event; + protected _onLineFeed = this.register(new EventEmitter()); + public readonly onLineFeed = this._onLineFeed.event; + private readonly _onResize = this.register(new EventEmitter<{ cols: number, rows: number }>()); + public readonly onResize = this._onResize.event; + protected readonly _onWriteParsed = this.register(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 = this.register(new EventEmitter()); public get onScroll(): IEvent { if (!this._onScrollApi) { - this._onScrollApi = new EventEmitter(); - this.register(this._onScroll.event(ev => { + this._onScrollApi = this.register(new EventEmitter()); + this._onScroll.event(ev => { this._onScrollApi?.fire(ev.position); - })); + }); } return this._onScrollApi.event; } @@ -104,19 +103,17 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { // Setup and initialize services this._instantiationService = new InstantiationService(); - this.optionsService = new OptionsService(options); + this.optionsService = this.register(new OptionsService(options)); this._instantiationService.setService(IOptionsService, this.optionsService); this._bufferService = this.register(this._instantiationService.createInstance(BufferService)); this._instantiationService.setService(IBufferService, this._bufferService); - this._logService = this._instantiationService.createInstance(LogService); + this._logService = this.register(this._instantiationService.createInstance(LogService)); this._instantiationService.setService(ILogService, this._logService); this.coreService = this.register(this._instantiationService.createInstance(CoreService, () => this.scrollToBottom())); this._instantiationService.setService(ICoreService, this.coreService); - this.coreMouseService = this._instantiationService.createInstance(CoreMouseService); + this.coreMouseService = this.register(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.unicodeService = this.register(this._instantiationService.createInstance(UnicodeService)); this._instantiationService.setService(IUnicodeService, this.unicodeService); this._charsetService = this._instantiationService.createInstance(CharsetService); this._instantiationService.setService(ICharsetService, this._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 = this.register(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,28 +129,25 @@ 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.optionsService.onOptionChange(key => this._updateOptions(key))); + this.register(this.coreService.onUserInput(() => this._writeBuffer.handleUserInput())); + this.register(this.optionsService.onSpecificOptionChange('windowsMode', e => this._handleWindowsModeOptionChange(e))); 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 - this._writeBuffer = new WriteBuffer((data, promiseResult) => this._inputHandler.parse(data, promiseResult)); + this._writeBuffer = this.register(new WriteBuffer((data, promiseResult) => this._inputHandler.parse(data, promiseResult))); this.register(forwardEvent(this._writeBuffer.onWriteParsed, this._onWriteParsed)); - } - public dispose(): void { - if (this._isDisposed) { - return; - } - super.dispose(); - this._windowsMode?.dispose(); - this._windowsMode = undefined; + this.register(toDisposable(() => { + this._windowsMode?.dispose(); + this._windowsMode = undefined; + })); } public write(data: string | Uint8Array, callback?: () => void): void { @@ -267,20 +261,12 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { this.coreMouseService.reset(); } - protected _updateOptions(key: string): void { - // TODO: These listeners should be owned by individual components - switch (key) { - case 'scrollback': - this.buffers.resize(this.cols, this.rows); - break; - case 'windowsMode': - if (this.optionsService.rawOptions.windowsMode) { - this._enableWindowsMode(); - } else { - this._windowsMode?.dispose(); - this._windowsMode = undefined; - } - break; + private _handleWindowsModeOptionChange(value: boolean): void { + if (value) { + this._enableWindowsMode(); + } else { + this._windowsMode?.dispose(); + this._windowsMode = undefined; } } diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts index f734b002..d9127e7b 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 b599bb7e..b91b446c 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 = this.register(new EventEmitter()); + public readonly onRequestBell = this._onRequestBell.event; + private readonly _onRequestRefreshRows = this.register(new EventEmitter()); + public readonly onRequestRefreshRows = this._onRequestRefreshRows.event; + private readonly _onRequestReset = this.register(new EventEmitter()); + public readonly onRequestReset = this._onRequestReset.event; + private readonly _onRequestSendFocus = this.register(new EventEmitter()); + public readonly onRequestSendFocus = this._onRequestSendFocus.event; + private readonly _onRequestSyncScrollBar = this.register(new EventEmitter()); + public readonly onRequestSyncScrollBar = this._onRequestSyncScrollBar.event; + private readonly _onRequestWindowsOptionsReport = this.register(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 = this.register(new EventEmitter()); + public readonly onA11yChar = this._onA11yChar.event; + private readonly _onA11yTab = this.register(new EventEmitter()); + public readonly onA11yTab = this._onA11yTab.event; + private readonly _onCursorMove = this.register(new EventEmitter()); + public readonly onCursorMove = this._onCursorMove.event; + private readonly _onLineFeed = this.register(new EventEmitter()); + public readonly onLineFeed = this._onLineFeed.event; + private readonly _onScroll = this.register(new EventEmitter()); + public readonly onScroll = this._onScroll.event; + private readonly _onTitleChange = this.register(new EventEmitter()); + public readonly onTitleChange = this._onTitleChange.event; + private readonly _onColor = this.register(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; @@ -379,10 +382,6 @@ export class InputHandler extends Disposable implements IInputHandler { this._parser.registerDcsHandler({ intermediates: '$', final: 'q' }, new DcsHandler((data, params) => this.requestStatusString(data, params))); } - public dispose(): void { - super.dispose(); - } - /** * Async parse support. */ @@ -459,7 +458,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 +492,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 +506,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 +634,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 +698,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 +713,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 +841,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 +857,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 +1177,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 +1196,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 +1256,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 +1288,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 +1322,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 +1348,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 +1374,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 +1394,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 +1410,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 +1442,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 +1475,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 +1498,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 +1521,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 +1543,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 +3219,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 +3292,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 +3343,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/Lifecycle.ts b/src/common/Lifecycle.ts index b3a7cc21..7ccc8aa7 100644 --- a/src/common/Lifecycle.ts +++ b/src/common/Lifecycle.ts @@ -17,15 +17,18 @@ export abstract class Disposable implements IDisposable { } /** - * Disposes the object, triggering the `dispose` method on all registered IDisposables. + * Disposes the object, triggering the `dispose` method on all registered IDisposables. This is a + * readonly property instead of a method to prevent subclasses overriding it which is an easy + * mistake that can introduce memory leaks. If a class extends Disposable, all dispose calls + * should be done via {@link register}. */ - public dispose(): void { + public readonly dispose = (): void => { this._isDisposed = true; for (const d of this._disposables) { d.dispose(); } this._disposables.length = 0; - } + }; /** * Registers a disposable object. diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index 8fb71a5d..3aa0f694 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -3,13 +3,13 @@ * @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'; import { IBufferSet, IBuffer } from 'common/buffer/Types'; import { BufferSet } from 'common/buffer/BufferSet'; -import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharset, IModes, IAttributeData, IOscLinkData } from 'common/Types'; +import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharset, IModes, IAttributeData, IOscLinkData, IDisposable } from 'common/Types'; import { UnicodeV6 } from 'common/input/UnicodeV6'; import { IDecorationOptions, IDecoration } from 'xterm'; @@ -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; @@ -123,7 +113,7 @@ export class MockOptionsService implements IOptionsService { public serviceBrand: any; public readonly rawOptions: Required = clone(DEFAULT_OPTIONS); public options: Required = this.rawOptions; - public onOptionChange: IEvent = new EventEmitter().event; + public onOptionChange: IEvent = new EventEmitter().event; constructor(testOptions?: Partial) { if (testOptions) { for (const key of Object.keys(testOptions)) { @@ -131,6 +121,22 @@ export class MockOptionsService implements IOptionsService { } } } + // eslint-disable-next-line @typescript-eslint/naming-convention + public onSpecificOptionChange(key: T, listener: (arg1: ITerminalOptions[T]) => any): IDisposable { + return this.onOptionChange(eventKey => { + if (eventKey === key) { + listener(this.rawOptions[key]); + } + }); + } + // eslint-disable-next-line @typescript-eslint/naming-convention + public onMultipleOptionChange(keys: (keyof ITerminalOptions)[], listener: () => any): IDisposable { + return this.onOptionChange(eventKey => { + if (keys.indexOf(eventKey) !== -1) { + listener(); + } + }); + } public setOptions(options: ITerminalOptions): void { for (const key of Object.keys(options)) { this.options[key] = options[key]; diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index 83866304..d53a9466 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -168,6 +168,7 @@ export class Buffer implements IBuffer { // Deal with columns increasing (reducing needs to happen after reflow) if (this._cols < newCols) { for (let i = 0; i < this.lines.length; i++) { + // +boolean for fast 0 or 1 conversion needsCleanup |= +this.lines.get(i)!.resize(newCols, nullCell); } } @@ -247,6 +248,7 @@ export class Buffer implements IBuffer { // Trim the end of the line off if cols shrunk if (this._cols > newCols) { for (let i = 0; i < this.lines.length; i++) { + // +boolean for fast 0 or 1 conversion needsCleanup |= +this.lines.get(i)!.resize(newCols, nullCell); } } diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 2e4fbca9..2971d309 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; /** Factor when to cleanup underlying array buffer after shrinking. */ const CLEANUP_THRESHOLD = 2; @@ -181,10 +179,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 f940bb8f..bc7aa58e 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. @@ -32,6 +32,8 @@ export class BufferSet extends Disposable implements IBufferSet { ) { super(); this.reset(); + this.register(this._optionsService.onSpecificOptionChange('scrollback', () => this.resize(this._bufferService.cols, this._bufferService.rows))); + this.register(this._optionsService.onSpecificOptionChange('tabStopWidth', () => this.setupTabStops())); } public reset(): void { @@ -119,6 +121,7 @@ export class BufferSet extends Disposable implements IBufferSet { public resize(newCols: number, newRows: number): void { this._normal.resize(newCols, newRows); this._alt.resize(newCols, newRows); + this.setupTabStops(newCols); } /** diff --git a/src/common/buffer/Marker.ts b/src/common/buffer/Marker.ts index 72c4085c..0629e26a 100644 --- a/src/common/buffer/Marker.ts +++ b/src/common/buffer/Marker.ts @@ -3,25 +3,25 @@ * @license MIT */ -import { EventEmitter, IEvent } from 'common/EventEmitter'; -import { Disposable } from 'common/Lifecycle'; -import { IMarker } from 'common/Types'; +import { EventEmitter } from 'common/EventEmitter'; +import { disposeArray } from 'common/Lifecycle'; +import { IDisposable, IMarker } from 'common/Types'; -export class Marker extends Disposable implements IMarker { +export class Marker implements IMarker { private static _nextId = 1; - private _id: number = Marker._nextId++; public isDisposed: boolean = false; + private _disposables: IDisposable[] = []; + private _id: number = Marker._nextId++; public get id(): number { return this._id; } - private _onDispose = new EventEmitter(); - public get onDispose(): IEvent { return this._onDispose.event; } + private readonly _onDispose = this.register(new EventEmitter()); + public readonly onDispose = this._onDispose.event; constructor( public line: number ) { - super(); } public dispose(): void { @@ -32,6 +32,12 @@ export class Marker extends Disposable implements IMarker { this.line = -1; // Emit before super.dispose such that dispose listeners get a change to react this._onDispose.fire(); - super.dispose(); + disposeArray(this._disposables); + this._disposables.length = 0; + } + + public register(disposable: T): T { + this._disposables.push(disposable); + return disposable; } } diff --git a/src/common/input/TextDecoder.test.ts b/src/common/input/TextDecoder.test.ts index da1760a2..b8db8a03 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 2cdf4e3c..68dbc6f7 100644 --- a/src/common/input/WriteBuffer.ts +++ b/src/common/input/WriteBuffer.ts @@ -5,6 +5,7 @@ */ import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { Disposable } from 'common/Lifecycle'; declare const setTimeout: (handler: () => void, timeout?: number) => void; @@ -33,17 +34,25 @@ const WRITE_TIMEOUT_MS = 12; */ const WRITE_BUFFER_LENGTH_THRESHOLD = 50; -export class WriteBuffer { +export class WriteBuffer extends Disposable { private _writeBuffer: (string | Uint8Array)[] = []; private _callbacks: ((() => void) | undefined)[] = []; private _pendingData = 0; private _bufferOffset = 0; private _isSyncWriting = false; private _syncCalls = 0; - public get onWriteParsed(): IEvent { return this._onWriteParsed.event; } - private _onWriteParsed = new EventEmitter(); + private _didUserInput = false; - constructor(private _action: (data: string | Uint8Array, promiseResult?: boolean) => void | Promise) { } + private readonly _onWriteParsed = this.register(new EventEmitter()); + public readonly onWriteParsed = this._onWriteParsed.event; + + constructor(private _action: (data: string | Uint8Array, promiseResult?: boolean) => void | Promise) { + super(); + } + + public handleUserInput(): void { + this._didUserInput = true; + } /** * @deprecated Unreliable, to be removed soon. @@ -99,7 +108,20 @@ export class WriteBuffer { // schedule chunk processing for next event loop run if (!this._writeBuffer.length) { this._bufferOffset = 0; - queueMicrotask(() => this._innerWrite()); + + // 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()); } this._pendingData += data.length; diff --git a/src/common/parser/EscapeSequenceParser.test.ts b/src/common/parser/EscapeSequenceParser.test.ts index 4b23fa00..76e88dea 100644 --- a/src/common/parser/EscapeSequenceParser.test.ts +++ b/src/common/parser/EscapeSequenceParser.test.ts @@ -87,7 +87,7 @@ class TestEscapeSequenceParser extends EscapeSequenceParser { } } public mockOscParser(): void { - this._oscParser = oscPutParser; + (this as any)._oscParser = oscPutParser; } public identifier(id: IFunctionIdentifier): number { return this._identifier(id); diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index f20a7e91..5bcd2dd3 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -5,7 +5,7 @@ import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandlerType, OscFallbackHandlerType, IOscParser, EscHandlerType, IDcsParser, DcsFallbackHandlerType, IFunctionIdentifier, ExecuteFallbackHandlerType, CsiFallbackHandlerType, EscFallbackHandlerType, PrintHandlerType, PrintFallbackHandlerType, ExecuteHandlerType, IParserStackState, ParserStackType, ResumableHandlersType } from 'common/parser/Types'; import { ParserState, ParserAction } from 'common/parser/Constants'; -import { Disposable } from 'common/Lifecycle'; +import { Disposable, toDisposable } from 'common/Lifecycle'; import { IDisposable } from 'common/Types'; import { fill } from 'common/TypedArrayUtils'; import { Params } from 'common/parser/Params'; @@ -242,8 +242,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP protected _executeHandlers: { [flag: number]: ExecuteHandlerType }; protected _csiHandlers: IHandlerCollection; protected _escHandlers: IHandlerCollection; - protected _oscParser: IOscParser; - protected _dcsParser: IDcsParser; + protected readonly _oscParser: IOscParser; + protected readonly _dcsParser: IDcsParser; protected _errorHandler: (state: IParsingState) => IParsingState; // fallback handlers @@ -284,8 +284,13 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._executeHandlers = Object.create(null); this._csiHandlers = Object.create(null); this._escHandlers = Object.create(null); - this._oscParser = new OscParser(); - this._dcsParser = new DcsParser(); + this.register(toDisposable(() => { + this._csiHandlers = Object.create(null); + this._executeHandlers = Object.create(null); + this._escHandlers = Object.create(null); + })); + this._oscParser = this.register(new OscParser()); + this._dcsParser = this.register(new DcsParser()); this._errorHandler = this._errorHandlerFb; // swallow 7bit ST (ESC+\) @@ -338,14 +343,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP return res.reverse().join(''); } - public dispose(): void { - this._csiHandlers = Object.create(null); - this._executeHandlers = Object.create(null); - this._escHandlers = Object.create(null); - this._oscParser.dispose(); - this._dcsParser.dispose(); - } - public setPrintHandler(handler: PrintHandlerType): void { this._printHandler = handler; } diff --git a/src/common/public/BufferNamespaceApi.ts b/src/common/public/BufferNamespaceApi.ts index d86f6bf5..9e49ce2b 100644 --- a/src/common/public/BufferNamespaceApi.ts +++ b/src/common/public/BufferNamespaceApi.ts @@ -5,14 +5,15 @@ import { IBuffer as IBufferApi, IBufferNamespace as IBufferNamespaceApi } from 'xterm'; import { BufferApiView } from 'common/public/BufferApiView'; -import { IEvent, EventEmitter } from 'common/EventEmitter'; +import { EventEmitter } from 'common/EventEmitter'; 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 e3b7dcd8..3f15f242 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 = this.register(new EventEmitter<{ cols: number, rows: number }>()); + public readonly onResize = this._onResize.event; + private readonly _onScroll = this.register(new EventEmitter()); + public readonly onScroll = this._onScroll.event; public get buffer(): IBuffer { return this.buffers.active; } @@ -36,19 +36,14 @@ export class BufferService extends Disposable implements IBufferService { super(); this.cols = Math.max(optionsService.rawOptions.cols || 0, MINIMUM_COLS); this.rows = Math.max(optionsService.rawOptions.rows || 0, MINIMUM_ROWS); - this.buffers = new BufferSet(optionsService, this); - } - - public dispose(): void { - super.dispose(); - this.buffers.dispose(); + this.buffers = this.register(new BufferSet(optionsService, this)); } public resize(cols: number, rows: number): void { this.cols = cols; this.rows = rows; this.buffers.resize(cols, rows); - this.buffers.setupTabStops(this.cols); + // TODO: This doesn't fire when scrollback changes - add a resize event to BufferSet and forward event this._onResize.fire({ cols, rows }); } diff --git a/src/common/services/CoreMouseService.ts b/src/common/services/CoreMouseService.ts index 54e991f8..d955370e 100644 --- a/src/common/services/CoreMouseService.ts +++ b/src/common/services/CoreMouseService.ts @@ -5,6 +5,7 @@ import { IBufferService, ICoreService, ICoreMouseService } from 'common/services/Services'; import { EventEmitter, IEvent } from 'common/EventEmitter'; import { ICoreMouseProtocol, ICoreMouseEvent, CoreMouseEncoding, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types'; +import { Disposable } from 'common/Lifecycle'; /** * Supported default protocols. @@ -165,18 +166,21 @@ const DEFAULT_ENCODINGS: { [key: string]: CoreMouseEncoding } = { * a tracking report to the backend based on protocol and encoding limitations. * To send a mouse event call `triggerMouseEvent`. */ -export class CoreMouseService implements ICoreMouseService { +export class CoreMouseService extends Disposable implements ICoreMouseService { private _protocols: { [name: string]: ICoreMouseProtocol } = {}; private _encodings: { [name: string]: CoreMouseEncoding } = {}; private _activeProtocol: string = ''; private _activeEncoding: string = ''; - private _onProtocolChange = new EventEmitter(); private _lastEvent: ICoreMouseEvent | null = null; + private readonly _onProtocolChange = this.register(new EventEmitter()); + public readonly onProtocolChange = this._onProtocolChange.event; + constructor( @IBufferService private readonly _bufferService: IBufferService, @ICoreService private readonly _coreService: ICoreService ) { + super(); // register default protocols and encodings for (const name of Object.keys(DEFAULT_PROTOCOLS)) this.addProtocol(name, DEFAULT_PROTOCOLS[name]); for (const name of Object.keys(DEFAULT_ENCODINGS)) this.addEncoding(name, DEFAULT_ENCODINGS[name]); @@ -225,13 +229,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 20a34603..9282197b 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 e5d115a1..c27e7b2f 100644 --- a/src/common/services/DecorationService.ts +++ b/src/common/services/DecorationService.ts @@ -5,17 +5,15 @@ import { css } from 'common/Color'; import { EventEmitter } from 'common/EventEmitter'; -import { Disposable } from 'common/Lifecycle'; +import { Disposable, toDisposable } from 'common/Lifecycle'; import { IDecorationService, IInternalDecoration } from 'common/services/Services'; 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,13 +25,23 @@ 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(); } + constructor() { + super(); + + this.register(toDisposable(() => { + for (const d of this._decorations.values()) { + this._onDecorationRemoved.fire(d); + } + this.reset(); + })); + } public registerDecoration(options: IDecorationOptions): IDecoration | undefined { if (options.marker.isDisposed) { return undefined; @@ -76,32 +84,26 @@ 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); } }); } - - public dispose(): void { - for (const d of this._decorations.values()) { - this._onDecorationRemoved.fire(d); - } - this.reset(); - } } class Decoration extends Disposable implements IInternalDecoration { public readonly marker: IMarker; public element: HTMLElement | undefined; - public isDisposed: boolean = false; 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; + public get isDisposed(): boolean { return this._isDisposed; } + private _cachedBg: IColor | undefined | null = null; public get backgroundColorRGB(): IColor | undefined { if (this._cachedBg === null) { @@ -134,14 +136,12 @@ class Decoration extends Disposable implements IInternalDecoration { if (this.options.overviewRulerOptions && !this.options.overviewRulerOptions.position) { this.options.overviewRulerOptions.position = 'full'; } - } - public override dispose(): void { - if (this._isDisposed) { - return; - } - this._isDisposed = true; - this._onDispose.fire(); - super.dispose(); + this.register(toDisposable(() => { + if (this._isDisposed) { + return; + } + this._onDispose.fire(); + })); } } diff --git a/src/common/services/DirtyRowService.ts b/src/common/services/DirtyRowService.ts deleted file mode 100644 index 1c43b67e..00000000 --- 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 8280948a..375e442d 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/LogService.ts b/src/common/services/LogService.ts index d3566567..4b56a097 100644 --- a/src/common/services/LogService.ts +++ b/src/common/services/LogService.ts @@ -3,6 +3,7 @@ * @license MIT */ +import { Disposable } from 'common/Lifecycle'; import { ILogService, IOptionsService, LogLevelEnum } from 'common/services/Services'; type LogType = (message?: any, ...optionalParams: any[]) => void; @@ -29,7 +30,7 @@ const optionsKeyToLogLevel: { [key: string]: LogLevelEnum } = { const LOG_PREFIX = 'xterm.js: '; -export class LogService implements ILogService { +export class LogService extends Disposable implements ILogService { public serviceBrand: any; public logLevel: LogLevelEnum = LogLevelEnum.OFF; @@ -37,12 +38,9 @@ export class LogService implements ILogService { constructor( @IOptionsService private readonly _optionsService: IOptionsService ) { + super(); this._updateLogLevel(); - this._optionsService.onOptionChange(key => { - if (key === 'logLevel') { - this._updateLogLevel(); - } - }); + this.register(this._optionsService.onSpecificOptionChange('logLevel', () => this._updateLogLevel())); } private _updateLogLevel(): void { diff --git a/src/common/services/OptionsService.test.ts b/src/common/services/OptionsService.test.ts index a65cb986..004f7316 100644 --- a/src/common/services/OptionsService.test.ts +++ b/src/common/services/OptionsService.test.ts @@ -5,6 +5,7 @@ import { assert } from 'chai'; import { OptionsService, DEFAULT_OPTIONS } from 'common/services/OptionsService'; +import { IDisposable } from 'common/Types'; describe('OptionsService', () => { describe('constructor', () => { @@ -59,7 +60,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; @@ -71,4 +72,79 @@ describe('OptionsService', () => { assert.equal(service.options.fontWeight, DEFAULT_OPTIONS.fontWeight, 'Wrong string literals should be reset to default'); }); }); + describe('onOptionChange', () => { + let service: OptionsService; + beforeEach(() => { + service = new OptionsService({}); + }); + it('should fire on any option change', async () => { + let disposable: IDisposable; + await new Promise(r => { + disposable = service.onOptionChange(e => { + assert.strictEqual(e, 'cursorWidth'); + r(); + }); + service.options.cursorWidth = 10; + }); + disposable!.dispose(); + await new Promise(r => { + service.onOptionChange(e => { + assert.strictEqual(e, 'scrollback'); + r(); + }); + service.options.scrollback = 20; + }); + }); + }); + describe('onSpecificOptionChange', () => { + let service: OptionsService; + beforeEach(() => { + service = new OptionsService({}); + }); + it('should fire only on a specific option change', async () => { + await new Promise(r => { + service.onSpecificOptionChange('scrollback', e => { + assert.strictEqual(e, 20); + r(); + }); + service.options.cursorWidth = 10; + service.options.scrollback = 20; + }); + }); + }); + describe('onSpecificOptionChange', () => { + let service: OptionsService; + beforeEach(() => { + service = new OptionsService({}); + }); + it('should fire only on a specific option change', async () => { + await new Promise(r => { + service.onSpecificOptionChange('scrollback', e => { + assert.strictEqual(e, 20); + r(); + }); + service.options.cursorWidth = 10; + service.options.scrollback = 20; + }); + }); + }); + describe('onMultipleOptionChange', () => { + let service: OptionsService; + beforeEach(() => { + service = new OptionsService({}); + }); + it('should fire only for specific options', async () => { + await new Promise(r => { + let called = false; + service.onMultipleOptionChange(['scrollback'], () => { + called = true; + }); + service.options.cursorWidth = 10; + assert.notOk(called); + service.options.scrollback = 20; + assert.ok(called); + r(); + }); + }); + }); }); diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index c7e8d294..976cdf8d 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -6,7 +6,8 @@ import { IOptionsService, ITerminalOptions, FontWeight } from 'common/services/Services'; import { EventEmitter, IEvent } from 'common/EventEmitter'; import { isMac } from 'common/Platform'; -import { CursorStyle } from 'common/Types'; +import { CursorStyle, IDisposable } from 'common/Types'; +import { Disposable } from 'common/Lifecycle'; export const DEFAULT_OPTIONS: Readonly> = { cols: 80, @@ -51,16 +52,17 @@ export const DEFAULT_OPTIONS: Readonly> = { const FONT_WEIGHT_OPTIONS: Extract[] = ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900']; -export class OptionsService implements IOptionsService { +export class OptionsService extends Disposable implements IOptionsService { public serviceBrand: any; public readonly rawOptions: Required; public options: Required; - private _onOptionChange = new EventEmitter(); - public get onOptionChange(): IEvent { return this._onOptionChange.event; } + private readonly _onOptionChange = this.register(new EventEmitter()); + public readonly onOptionChange = this._onOptionChange.event; constructor(options: Partial) { + super(); // set the default value of each option const defaultOptions = { ...DEFAULT_OPTIONS }; for (const key in options) { @@ -80,6 +82,24 @@ export class OptionsService implements IOptionsService { this._setupOptions(); } + // eslint-disable-next-line @typescript-eslint/naming-convention + public onSpecificOptionChange(key: T, listener: (value: ITerminalOptions[T]) => any): IDisposable { + return this.onOptionChange(eventKey => { + if (eventKey === key) { + listener(this.rawOptions[key]); + } + }); + } + + // eslint-disable-next-line @typescript-eslint/naming-convention + public onMultipleOptionChange(keys: (keyof ITerminalOptions)[], listener: () => any): IDisposable { + return this.onOptionChange(eventKey => { + if (keys.indexOf(eventKey) !== -1) { + listener(); + } + }); + } + private _setupOptions(): void { const getter = (propName: string): any => { if (!(propName in DEFAULT_OPTIONS)) { diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 22edad1d..cc388063 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; @@ -195,9 +182,33 @@ export interface IOptionsService { * internally. */ readonly rawOptions: Required; + + /** + * Options as exposed through the public API, this property uses getters and setters with + * validation which makes it safer but slower. {@link rawOptions} should be used for pretty much + * all internal usage for performance reasons. + */ readonly options: Required; - readonly onOptionChange: IEvent; + /** + * Adds an event listener for when any option changes. + */ + readonly onOptionChange: IEvent; + + /** + * Adds an event listener for when a specific option changes, this is a convenience method that is + * preferred over {@link onOptionChange} when only a single option is being listened to. + */ + // eslint-disable-next-line @typescript-eslint/naming-convention + onSpecificOptionChange(key: T, listener: (arg1: Required[T]) => any): IDisposable; + + /** + * Adds an event listener for when a set of specific options change, this is a convenience method + * that is preferred over {@link onOptionChange} when multiple options are being listened to and + * handled the same way. + */ + // eslint-disable-next-line @typescript-eslint/naming-convention + onMultipleOptionChange(keys: (keyof ITerminalOptions)[], listener: () => any): IDisposable; } export type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' | number; diff --git a/src/common/services/UnicodeService.ts b/src/common/services/UnicodeService.ts index e96b7579..5c5b74f6 100644 --- a/src/common/services/UnicodeService.ts +++ b/src/common/services/UnicodeService.ts @@ -6,15 +6,15 @@ import { IUnicodeService, IUnicodeVersionProvider } from 'common/services/Servic import { EventEmitter, IEvent } from 'common/EventEmitter'; import { UnicodeV6 } from 'common/input/UnicodeV6'; - export class UnicodeService implements IUnicodeService { public serviceBrand: any; 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(); @@ -23,6 +23,10 @@ export class UnicodeService implements IUnicodeService { this._activeProvider = defaultProvider; } + public dispose(): void { + this._onChange.dispose(); + } + public get versions(): string[] { return Object.keys(this._providers); } diff --git a/src/headless/Terminal.ts b/src/headless/Terminal.ts index 1cad0ee2..2c244f21 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 = this.register(new EventEmitter()); + public readonly onBell = this._onBell.event; + private readonly _onCursorMove = this.register(new EventEmitter()); + public readonly onCursorMove = this._onCursorMove.event; + private readonly _onTitleChange = this.register(new EventEmitter()); + public readonly onTitleChange = this._onTitleChange.event; + private readonly _onA11yCharEmitter = this.register(new EventEmitter()); + public readonly onA11yChar = this._onA11yCharEmitter.event; + private readonly _onA11yTabEmitter = this.register(new EventEmitter()); + public readonly onA11yTab = this._onA11yTabEmitter.event; /** * Creates a new `Terminal` object. @@ -72,14 +71,6 @@ export class Terminal extends CoreTerminal { this.register(forwardEvent(this._inputHandler.onA11yTab, this._onA11yTabEmitter)); } - public dispose(): void { - if (this._isDisposed) { - return; - } - super.dispose(); - this.write = () => { }; - } - /** * Convenience property to active buffer. */ @@ -87,15 +78,6 @@ export class Terminal extends CoreTerminal { return this.buffers.active; } - protected _updateOptions(key: string): void { - super._updateOptions(key); - - // TODO: These listeners should be owned by individual components - switch (key) { - case 'tabStopWidth': this.buffers.setupTabStops(); break; - } - } - // TODO: Support paste here? public get markers(): IMarker[] { diff --git a/test/api/InputHandler.api.ts b/test/api/InputHandler.api.ts index c8cbbf66..8192bfed 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 d240738f..1b00fee5 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 } } } ]); });