diff --git a/.eslintrc.json b/.eslintrc.json index 9510fc6b..aa6e6417 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -7,6 +7,7 @@ "parser": "@typescript-eslint/parser", "parserOptions": { "project": [ + "demo/tsconfig.json", "src/browser/tsconfig.json", "src/common/tsconfig.json", "src/headless/tsconfig.json", diff --git a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts index f37ea1b8..2a9e09ba 100644 --- a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts @@ -3,23 +3,22 @@ * @license MIT */ -import { IRenderDimensions } from 'browser/renderer/Types'; -import { IRenderLayer } from './Types'; -import { ICellData, IColor } from 'common/Types'; -import { DEFAULT_COLOR, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE, Attributes } from 'common/buffer/Constants'; -import { IGlyphIdentifier } from './atlas/Types'; -import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, TEXT_BASELINE } from 'browser/renderer/Constants'; -import { BaseCharAtlas } from './atlas/BaseCharAtlas'; -import { acquireCharAtlas } from './atlas/CharAtlasCache'; -import { AttributeData } from 'common/buffer/AttributeData'; +import { removeElementFromParent } from 'browser/Dom'; +import { acquireTextureAtlas } from 'browser/renderer/shared/CharAtlasCache'; +import { TEXT_BASELINE } from 'browser/renderer/shared/Constants'; +import { tryDrawCustomChar } from 'browser/renderer/shared/CustomGlyphs'; +import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; +import { IRasterizedGlyph, IRenderDimensions, ISelectionRenderModel, ITextureAtlas } from 'browser/renderer/shared/Types'; +import { createSelectionRenderModel } from 'browser/renderer/shared/SelectionRenderModel'; +import { ICoreBrowserService } from 'browser/services/Services'; import { IColorSet } from 'browser/Types'; import { CellData } from 'common/buffer/CellData'; +import { WHITESPACE_CELL_CODE } from 'common/buffer/Constants'; import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; -import { ICoreBrowserService } from 'browser/services/Services'; -import { excludeFromContrastRatioDemands, throwIfFalsy } from 'browser/renderer/RendererUtils'; -import { channels, color, rgba } from 'common/Color'; -import { removeElementFromParent } from 'browser/Dom'; -import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs'; +import { ICellData } from 'common/Types'; +import { Terminal } from 'xterm'; +import { IRenderLayer } from './Types'; +import { CellColorResolver } from 'browser/renderer/shared/CellColorResolver'; export abstract class BaseRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; @@ -31,44 +30,34 @@ export abstract class BaseRenderLayer implements IRenderLayer { private _scaledCharLeft: number = 0; private _scaledCharTop: number = 0; - protected _selectionStart: [number, number] | undefined; - protected _selectionEnd: [number, number] | undefined; - protected _columnSelectMode: boolean = false; + protected _selectionModel: ISelectionRenderModel = createSelectionRenderModel(); + private _cellColorResolver: CellColorResolver; + private _bitmapGenerator?: BitmapGenerator; - protected _charAtlas: BaseCharAtlas | undefined; - - /** - * An object that's reused when drawing glyphs in order to reduce GC. - */ - private _currentGlyphIdentifier: IGlyphIdentifier = { - chars: '', - code: 0, - bg: 0, - fg: 0, - bold: false, - dim: false, - italic: false - }; + protected _charAtlas!: ITextureAtlas; public get canvas(): HTMLCanvasElement { return this._canvas; } + public get cacheCanvas(): HTMLCanvasElement { return this._charAtlas?.cacheCanvas!; } constructor( + private readonly _terminal: Terminal, private _container: HTMLElement, id: string, zIndex: number, private _alpha: boolean, protected _colors: IColorSet, - private _rendererId: number, protected readonly _bufferService: IBufferService, protected readonly _optionsService: IOptionsService, protected readonly _decorationService: IDecorationService, protected readonly _coreBrowserService: ICoreBrowserService ) { + this._cellColorResolver = new CellColorResolver(this._terminal, this._colors, this._selectionModel, this._decorationService, this._coreBrowserService); this._canvas = document.createElement('canvas'); this._canvas.classList.add(`xterm-${id}-layer`); this._canvas.style.zIndex = zIndex.toString(); this._initCanvas(); this._container.appendChild(this._canvas); + this._refreshCharAtlas(this._colors); } public dispose(): void { @@ -91,9 +80,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { public onGridChanged(startRow: number, endRow: number): void {} public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void { - this._selectionStart = start; - this._selectionEnd = end; - this._columnSelectMode = columnSelectMode; + this._selectionModel.update(this._terminal, start, end, columnSelectMode); } public setColors(colorSet: IColorSet): void { @@ -127,8 +114,9 @@ export abstract class BaseRenderLayer implements IRenderLayer { if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) { return; } - this._charAtlas = acquireCharAtlas(this._optionsService.rawOptions, this._rendererId, colorSet, this._scaledCharWidth, this._scaledCharHeight, this._coreBrowserService.dpr); + this._charAtlas = acquireTextureAtlas(this._terminal, colorSet, this._scaledCellWidth, this._scaledCellHeight, this._scaledCharWidth, this._scaledCharHeight, this._coreBrowserService.dpr); this._charAtlas.warmUp(); + this._bitmapGenerator = new BitmapGenerator(this._charAtlas.cacheCanvas); } public resize(dim: IRenderDimensions): void { @@ -154,7 +142,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { public abstract reset(): void; public clearTextureAtlas(): void { - this._charAtlas?.clear(); + this._charAtlas?.clearTexture(); } /** @@ -365,134 +353,37 @@ export abstract class BaseRenderLayer implements IRenderLayer { /** * Draws one or more characters at a cell. If possible this will draw using * the character atlas to reduce draw time. - * @param chars The character or characters. - * @param code The character code. - * @param width The width of the characters. - * @param x The column to draw at. - * @param y The row to draw at. - * @param fg The foreground color, in the format stored within the attributes. - * @param bg The background color, in the format stored within the attributes. - * This is used to validate whether a cached image can be used. - * @param bold Whether the text is bold. */ protected _drawChars(cell: ICellData, x: number, y: number): void { - const contrastColor = this._getContrastColor(cell, x, y); - - // skip cache right away if we draw in RGB - // Note: to avoid bad runtime JoinedCellData will be skipped - // in the cache handler itself (atlasDidDraw == false) and - // fall through to uncached later down below - if (contrastColor || cell.isFgRGB() || cell.isBgRGB()) { - this._drawUncachedChars(cell, x, y, contrastColor); - return; + if (this._charAtlas.hasCanvasChanged) { + this._bitmapGenerator?.refresh(); + this._charAtlas.hasCanvasChanged = false; } - - let fg; - let bg; - if (cell.isInverse()) { - fg = (cell.isBgDefault()) ? INVERTED_DEFAULT_COLOR : cell.getBgColor(); - bg = (cell.isFgDefault()) ? INVERTED_DEFAULT_COLOR : cell.getFgColor(); + const chars = cell.getChars(); + this._cellColorResolver.resolve(cell, x, y); + let glyph: IRasterizedGlyph; + if (chars && chars.length > 1) { + glyph = this._charAtlas.getRasterizedGlyphCombinedChar(chars, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext); } else { - bg = (cell.isBgDefault()) ? DEFAULT_COLOR : cell.getBgColor(); - fg = (cell.isFgDefault()) ? DEFAULT_COLOR : cell.getFgColor(); + glyph = this._charAtlas.getRasterizedGlyph(cell.getCode() || WHITESPACE_CELL_CODE, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext); } - - const drawInBrightColor = this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8; - - fg += drawInBrightColor ? 8 : 0; - this._currentGlyphIdentifier.chars = cell.getChars() || WHITESPACE_CELL_CHAR; - this._currentGlyphIdentifier.code = cell.getCode() || WHITESPACE_CELL_CODE; - this._currentGlyphIdentifier.bg = bg; - this._currentGlyphIdentifier.fg = fg; - this._currentGlyphIdentifier.bold = !!cell.isBold(); - this._currentGlyphIdentifier.dim = !!cell.isDim(); - this._currentGlyphIdentifier.italic = !!cell.isItalic(); - - // Don't try cache the glyph if it uses any decoration foreground/background override. - let hasOverrides = false; - this._decorationService.forEachDecorationAtCell(x, y, undefined, d => { - if (d.backgroundColorRGB || d.foregroundColorRGB) { - hasOverrides = true; - } - }); - - const atlasDidDraw = hasOverrides ? false : this._charAtlas?.draw(this._ctx, this._currentGlyphIdentifier, x * this._scaledCellWidth + this._scaledCharLeft, y * this._scaledCellHeight + this._scaledCharTop); - - if (!atlasDidDraw) { - this._drawUncachedChars(cell, x, y); - } - } - - /** - * Draws one or more characters at one or more cells. The character(s) will be - * clipped to ensure that they fit with the cell(s), including the cell to the - * right if the last character is a wide character. - * @param chars The character. - * @param width The width of the character. - * @param fg The foreground color, in the format stored within the attributes. - * @param x The column to draw at. - * @param y The row to draw at. - */ - private _drawUncachedChars(cell: ICellData, x: number, y: number, fgOverride?: IColor): void { this._ctx.save(); - this._ctx.font = this._getFont(!!cell.isBold(), !!cell.isItalic()); - this._ctx.textBaseline = TEXT_BASELINE; - - if (cell.isInverse()) { - if (fgOverride) { - this._ctx.fillStyle = fgOverride.css; - } else if (cell.isBgDefault()) { - this._ctx.fillStyle = color.opaque(this._colors.background).css; - } else if (cell.isBgRGB()) { - this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`; - } else { - let bg = cell.getBgColor(); - if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && bg < 8) { - bg += 8; - } - this._ctx.fillStyle = this._colors.ansi[bg].css; - } - } else { - if (fgOverride) { - this._ctx.fillStyle = fgOverride.css; - } else if (cell.isFgDefault()) { - this._ctx.fillStyle = this._colors.foreground.css; - } else if (cell.isFgRGB()) { - this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`; - } else { - let fg = cell.getFgColor(); - if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8) { - fg += 8; - } - this._ctx.fillStyle = this._colors.ansi[fg].css; - } - } - this._clipRow(y); - - // Apply alpha to dim the character - if (cell.isDim()) { - this._ctx.globalAlpha = DIM_OPACITY; - } - - // Draw custom characters if applicable - let drawSuccess = false; - if (this._optionsService.rawOptions.customGlyphs !== false) { - drawSuccess = tryDrawCustomChar(this._ctx, cell.getChars(), x * this._scaledCellWidth, y * this._scaledCellHeight, this._scaledCellWidth, this._scaledCellHeight, this._optionsService.rawOptions.fontSize, this._coreBrowserService.dpr); - } - - // Draw the character - if (!drawSuccess) { - this._ctx.fillText( - cell.getChars(), - x * this._scaledCellWidth + this._scaledCharLeft, - y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight); - } - + // Draw the image, use the bitmap if it's available + this._ctx.drawImage( + this._bitmapGenerator?.bitmap || this._charAtlas!.cacheCanvas, + glyph.texturePosition.x, + glyph.texturePosition.y, + glyph.size.x, + glyph.size.y, + x * this._scaledCellWidth - glyph.offset.x, + y * this._scaledCellHeight - glyph.offset.y, + glyph.size.x, + glyph.size.y + ); this._ctx.restore(); } - /** * Clips a row to ensure no pixels will be drawn outside the cells in the row. * @param y The row to clip. @@ -517,137 +408,55 @@ export abstract class BaseRenderLayer implements IRenderLayer { return `${fontStyle} ${fontWeight} ${this._optionsService.rawOptions.fontSize * this._coreBrowserService.dpr}px ${this._optionsService.rawOptions.fontFamily}`; } - - private _getContrastColor(cell: CellData, x: number, y: number): IColor | undefined { - // Get any decoration foreground/background overrides, this must be fetched before the early - // exist but applied after inverse - let bgOverride: number | undefined; - let fgOverride: number | undefined; - let isTop = false; - this._decorationService.forEachDecorationAtCell(x, y, undefined, d => { - if (d.options.layer !== 'top' && isTop) { - return; - } - if (d.backgroundColorRGB) { - bgOverride = d.backgroundColorRGB.rgba; - } - if (d.foregroundColorRGB) { - fgOverride = d.foregroundColorRGB.rgba; - } - isTop = d.options.layer === 'top'; - }); - - // Apply selection foreground if applicable - if (!isTop) { - if (this._colors.selectionForeground && this._isCellInSelection(x, y)) { - fgOverride = this._colors.selectionForeground.rgba; - } - } - - if (!bgOverride && !fgOverride && (this._optionsService.rawOptions.minimumContrastRatio === 1 || excludeFromContrastRatioDemands(cell.getCode()))) { - return undefined; - } - - if (!bgOverride && !fgOverride) { - // Try get from cache - const adjustedColor = this._colors.contrastCache.getColor(cell.bg, cell.fg); - if (adjustedColor !== undefined) { - return adjustedColor || undefined; - } - } - - let fgColor = cell.getFgColor(); - let fgColorMode = cell.getFgColorMode(); - let bgColor = cell.getBgColor(); - let bgColorMode = cell.getBgColorMode(); - const isInverse = !!cell.isInverse(); - const isBold = !!cell.isInverse(); - if (isInverse) { - const temp = fgColor; - fgColor = bgColor; - bgColor = temp; - const temp2 = fgColorMode; - fgColorMode = bgColorMode; - bgColorMode = temp2; - } - - const bgRgba = this._resolveBackgroundRgba(bgOverride !== undefined ? Attributes.CM_RGB : bgColorMode, bgOverride ?? bgColor, isInverse); - const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, isInverse, isBold); - let result = rgba.ensureContrastRatio(bgOverride ?? bgRgba, fgOverride ?? fgRgba, this._optionsService.rawOptions.minimumContrastRatio); - - if (!result) { - if (!fgOverride) { - this._colors.contrastCache.setColor(cell.bg, cell.fg, null); - return undefined; - } - // If it was an override and there was no contrast change, set as the result - result = fgOverride; - } - - const color: IColor = { - css: channels.toCss( - (result >> 24) & 0xFF, - (result >> 16) & 0xFF, - (result >> 8) & 0xFF - ), - rgba: result - }; - if (!bgOverride && !fgOverride) { - this._colors.contrastCache.setColor(cell.bg, cell.fg, color); - } - - return color; - } - - private _resolveBackgroundRgba(bgColorMode: number, bgColor: number, inverse: boolean): number { - switch (bgColorMode) { - case Attributes.CM_P16: - case Attributes.CM_P256: - return this._colors.ansi[bgColor].rgba; - case Attributes.CM_RGB: - return bgColor << 8; - case Attributes.CM_DEFAULT: - default: - if (inverse) { - return this._colors.foreground.rgba; - } - return this._colors.background.rgba; - } - } - - private _resolveForegroundRgba(fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): number { - switch (fgColorMode) { - case Attributes.CM_P16: - case Attributes.CM_P256: - if (this._optionsService.rawOptions.drawBoldTextInBrightColors && bold && fgColor < 8) { - fgColor += 8; - } - return this._colors.ansi[fgColor].rgba; - case Attributes.CM_RGB: - return fgColor << 8; - case Attributes.CM_DEFAULT: - default: - if (inverse) { - return this._colors.background.rgba; - } - return this._colors.foreground.rgba; - } - } - - private _isCellInSelection(x: number, y: number): boolean { - const start = this._selectionStart; - const end = this._selectionEnd; - if (!start || !end) { - return false; - } - if (this._columnSelectMode) { - return x >= start[0] && y >= start[1] && - x < end[0] && y < end[1]; - } - return (y > start[1] && y < end[1]) || - (start[1] === end[1] && y === start[1] && x >= start[0] && x < end[0]) || - (start[1] < end[1] && y === end[1] && x < end[0]) || - (start[1] < end[1] && y === start[1] && x >= start[0]); - } } +/** + * The number of milliseconds to wait before generating the ImageBitmap, this is to debounce/batch + * the operation as window.createImageBitmap is asynchronous. + */ +const GLYPH_BITMAP_COMMIT_DELAY = 100; + +const enum BitmapGeneratorState { + IDLE = 0, + GENERATING = 1, + GENERATING_INVALID = 2 +} + +class BitmapGenerator { + private _state: BitmapGeneratorState = BitmapGeneratorState.IDLE; + private _commitTimeout: number | undefined = undefined; + private _bitmap: ImageBitmap | undefined = undefined; + public get bitmap(): ImageBitmap | undefined { return this._bitmap; } + + constructor(private readonly _canvas: HTMLCanvasElement) { + } + + public refresh(): void { + // Clear the bitmap immediately as it's stale + this._bitmap = undefined; + if (this._commitTimeout === undefined) { + this._commitTimeout = window.setTimeout(() => this._generate(), GLYPH_BITMAP_COMMIT_DELAY); + } + if (this._state === BitmapGeneratorState.GENERATING) { + this._state = BitmapGeneratorState.GENERATING_INVALID; + } + } + + private _generate(): void { + if (this._state === BitmapGeneratorState.IDLE) { + this._bitmap = undefined; + this._state = BitmapGeneratorState.GENERATING; + window.createImageBitmap(this._canvas).then(bitmap => { + if (this._state === BitmapGeneratorState.GENERATING_INVALID) { + this.refresh(); + } else { + this._bitmap = bitmap; + } + this._state = BitmapGeneratorState.IDLE; + }); + if (this._commitTimeout) { + this._commitTimeout = undefined; + } + } + } +} diff --git a/addons/xterm-addon-canvas/src/CanvasAddon.ts b/addons/xterm-addon-canvas/src/CanvasAddon.ts index 9fca00a6..1f214767 100644 --- a/addons/xterm-addon-canvas/src/CanvasAddon.ts +++ b/addons/xterm-addon-canvas/src/CanvasAddon.ts @@ -8,11 +8,14 @@ import { IColorSet } from 'browser/Types'; import { CanvasRenderer } from './CanvasRenderer'; import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; import { ITerminalAddon, Terminal } from 'xterm'; +import { forwardEvent, initEvent } from 'common/EventEmitter'; export class CanvasAddon implements ITerminalAddon { private _terminal?: Terminal; private _renderer?: CanvasRenderer; + public readonly onChangeTextureAtlas = initEvent(); + public activate(terminal: Terminal): void { if (!terminal.element) { throw new Error('Cannot activate CanvasAddon before Terminal.open'); @@ -29,7 +32,8 @@ export class CanvasAddon implements ITerminalAddon { 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); + this._renderer = new CanvasRenderer(terminal, colors, screenElement, linkifier, bufferService, charSizeService, optionsService, characterJoinerService, coreService, coreBrowserService, decorationService); + forwardEvent(this._renderer.onChangeTextureAtlas, this.onChangeTextureAtlas); renderService.setRenderer(this._renderer); renderService.onResize(bufferService.cols, bufferService.rows); } @@ -44,4 +48,8 @@ export class CanvasAddon implements ITerminalAddon { this._renderer?.dispose(); this._renderer = undefined; } + + public get textureAtlas(): HTMLCanvasElement | undefined { + return this._renderer?.textureAtlas; + } } diff --git a/addons/xterm-addon-canvas/src/CanvasRenderer.ts b/addons/xterm-addon-canvas/src/CanvasRenderer.ts index 726ad080..b7abab76 100644 --- a/addons/xterm-addon-canvas/src/CanvasRenderer.ts +++ b/addons/xterm-addon-canvas/src/CanvasRenderer.ts @@ -6,7 +6,7 @@ import { TextRenderLayer } from './TextRenderLayer'; import { SelectionRenderLayer } from './SelectionRenderLayer'; import { CursorRenderLayer } from './CursorRenderLayer'; -import { IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types'; +import { IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/shared/Types'; import { IRenderLayer } from './Types'; import { LinkRenderLayer } from './LinkRenderLayer'; import { Disposable } from 'common/Lifecycle'; @@ -14,22 +14,21 @@ 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 { observeDevicePixelDimensions } from 'browser/renderer/shared/DevicePixelObserver'; +import { Terminal } from 'xterm'; import { initEvent, EventEmitter, IEvent } from 'common/EventEmitter'; -import { observeDevicePixelDimensions } from 'browser/renderer/DevicePixelObserver'; - -let nextRendererId = 1; export class CanvasRenderer extends Disposable implements IRenderer { - private _id = nextRendererId++; - private _renderLayers: IRenderLayer[]; private _devicePixelRatio: number; public dimensions: IRenderDimensions; public readonly onRequestRedraw = initEvent(); + public readonly onChangeTextureAtlas = initEvent(); constructor( + private readonly _terminal: Terminal, private _colors: IColorSet, private readonly _screenElement: HTMLElement, linkifier2: ILinkifier2, @@ -44,10 +43,10 @@ export class CanvasRenderer extends Disposable implements IRenderer { super(); const allowTransparency = this._optionsService.rawOptions.allowTransparency; this._renderLayers = [ - new TextRenderLayer(this._screenElement, 0, this._colors, allowTransparency, this._id, this._bufferService, this._optionsService, characterJoinerService, decorationService, this._coreBrowserService), - new SelectionRenderLayer(this._screenElement, 1, this._colors, this._id, this._bufferService, this._coreBrowserService, decorationService, this._optionsService), - new LinkRenderLayer(this._screenElement, 2, this._colors, this._id, linkifier2, this._bufferService, this._optionsService, decorationService, this._coreBrowserService), - new CursorRenderLayer(this._screenElement, 3, this._colors, this._id, this.onRequestRedraw, this._bufferService, this._optionsService, coreService, this._coreBrowserService, decorationService) + new TextRenderLayer(this._terminal, this._screenElement, 0, this._colors, allowTransparency, this._bufferService, this._optionsService, characterJoinerService, decorationService, this._coreBrowserService), + new SelectionRenderLayer(this._terminal, this._screenElement, 1, this._colors, this._bufferService, this._coreBrowserService, decorationService, this._optionsService), + new LinkRenderLayer(this._terminal, this._screenElement, 2, this._colors, linkifier2, this._bufferService, this._optionsService, decorationService, this._coreBrowserService), + new CursorRenderLayer(this._terminal, this._screenElement, 3, this._colors, this.onRequestRedraw, this._bufferService, this._optionsService, coreService, this._coreBrowserService, decorationService) ]; this.dimensions = { scaledCharWidth: 0, @@ -76,7 +75,11 @@ export class CanvasRenderer extends Disposable implements IRenderer { l.dispose(); } super.dispose(); - removeTerminalFromCache(this._id); + removeTerminalFromCache(this._terminal); + } + + public get textureAtlas(): HTMLCanvasElement | undefined { + return this._renderLayers[0].cacheCanvas; } public onDevicePixelRatioChange(): void { diff --git a/addons/xterm-addon-canvas/src/CursorRenderLayer.ts b/addons/xterm-addon-canvas/src/CursorRenderLayer.ts index 61e2d934..78dc5432 100644 --- a/addons/xterm-addon-canvas/src/CursorRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/CursorRenderLayer.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types'; +import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/shared/Types'; import { BaseRenderLayer } from './BaseRenderLayer'; import { ICellData } from 'common/Types'; import { CellData } from 'common/buffer/CellData'; @@ -11,6 +11,7 @@ import { IColorSet } from 'browser/Types'; import { IBufferService, IOptionsService, ICoreService, IDecorationService } from 'common/services/Services'; import { IEventEmitter } from 'common/EventEmitter'; import { ICoreBrowserService } from 'browser/services/Services'; +import { Terminal } from 'xterm'; interface ICursorState { x: number; @@ -32,10 +33,10 @@ export class CursorRenderLayer extends BaseRenderLayer { private _cell: ICellData = new CellData(); constructor( + terminal: Terminal, container: HTMLElement, zIndex: number, colors: IColorSet, - rendererId: number, private readonly _onRequestRedraw: IEventEmitter, bufferService: IBufferService, optionsService: IOptionsService, @@ -43,7 +44,7 @@ export class CursorRenderLayer extends BaseRenderLayer { coreBrowserService: ICoreBrowserService, decorationService: IDecorationService ) { - super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService, coreBrowserService); + super(terminal, container, 'cursor', zIndex, true, colors, bufferService, optionsService, decorationService, coreBrowserService); this._state = { x: 0, y: 0, diff --git a/addons/xterm-addon-canvas/src/LinkRenderLayer.ts b/addons/xterm-addon-canvas/src/LinkRenderLayer.ts index c07329fd..db622b3f 100644 --- a/addons/xterm-addon-canvas/src/LinkRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/LinkRenderLayer.ts @@ -3,29 +3,30 @@ * @license MIT */ -import { IRenderDimensions } from 'browser/renderer/Types'; +import { IRenderDimensions } from 'browser/renderer/shared/Types'; import { BaseRenderLayer } from './BaseRenderLayer'; -import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants'; +import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants'; import { ICoreBrowserService } from 'browser/services/Services'; -import { is256Color } from './atlas/CharAtlasUtils'; import { IColorSet, ILinkifierEvent, ILinkifier2 } from 'browser/Types'; import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; +import { is256Color } from 'browser/renderer/shared/CharAtlasUtils'; +import { Terminal } from 'xterm'; export class LinkRenderLayer extends BaseRenderLayer { private _state: ILinkifierEvent | undefined; constructor( + terminal: Terminal, container: HTMLElement, zIndex: number, colors: IColorSet, - rendererId: number, linkifier2: ILinkifier2, bufferService: IBufferService, optionsService: IOptionsService, decorationService: IDecorationService, coreBrowserService: ICoreBrowserService ) { - super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService, coreBrowserService); + super(terminal, container, 'link', zIndex, true, colors, bufferService, optionsService, decorationService, coreBrowserService); linkifier2.onShowLinkUnderline(e => this._onShowLinkUnderline(e)); linkifier2.onHideLinkUnderline(e => this._onHideLinkUnderline(e)); diff --git a/addons/xterm-addon-canvas/src/SelectionRenderLayer.ts b/addons/xterm-addon-canvas/src/SelectionRenderLayer.ts index e90007b7..759b0a12 100644 --- a/addons/xterm-addon-canvas/src/SelectionRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/SelectionRenderLayer.ts @@ -3,11 +3,12 @@ * @license MIT */ -import { IRenderDimensions } from 'browser/renderer/Types'; +import { IRenderDimensions } from 'browser/renderer/shared/Types'; import { BaseRenderLayer } from './BaseRenderLayer'; import { IColorSet } from 'browser/Types'; import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; import { ICoreBrowserService } from 'browser/services/Services'; +import { Terminal } from 'xterm'; interface ISelectionState { start?: [number, number]; @@ -20,16 +21,16 @@ export class SelectionRenderLayer extends BaseRenderLayer { private _state!: ISelectionState; constructor( + terminal: Terminal, container: HTMLElement, zIndex: number, colors: IColorSet, - rendererId: number, bufferService: IBufferService, coreBrowserService: ICoreBrowserService, decorationService: IDecorationService, optionsService: IOptionsService ) { - super(container, 'selection', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService, coreBrowserService); + super(terminal, container, 'selection', zIndex, true, colors, bufferService, optionsService, decorationService, coreBrowserService); this._clearState(); } @@ -46,8 +47,8 @@ export class SelectionRenderLayer extends BaseRenderLayer { super.resize(dim); // On resize use the base render layer's cached selection values since resize clears _state // inside reset. - if (this._selectionStart && this._selectionEnd) { - this._redrawSelection(this._selectionStart, this._selectionEnd, this._columnSelectMode); + if (this._selectionModel.selectionStart && this._selectionModel.selectionEnd) { + this._redrawSelection(this._selectionModel.selectionStart, this._selectionModel.selectionEnd, this._selectionModel.columnSelectMode); } } @@ -60,12 +61,12 @@ export class SelectionRenderLayer extends BaseRenderLayer { public onBlur(): void { this.reset(); - this._redrawSelection(this._selectionStart, this._selectionEnd, this._columnSelectMode); + this._redrawSelection(this._selectionModel.selectionStart, this._selectionModel.selectionEnd, this._selectionModel.columnSelectMode); } public onFocus(): void { this.reset(); - this._redrawSelection(this._selectionStart, this._selectionEnd, this._columnSelectMode); + this._redrawSelection(this._selectionModel.selectionStart, this._selectionModel.selectionEnd, this._selectionModel.columnSelectMode); } public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void { diff --git a/addons/xterm-addon-canvas/src/TextRenderLayer.ts b/addons/xterm-addon-canvas/src/TextRenderLayer.ts index 95f22fce..8d80514b 100644 --- a/addons/xterm-addon-canvas/src/TextRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/TextRenderLayer.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IRenderDimensions } from 'browser/renderer/Types'; +import { IRenderDimensions } from 'browser/renderer/shared/Types'; import { CharData, ICellData } from 'common/Types'; import { GridCache } from './GridCache'; import { BaseRenderLayer } from './BaseRenderLayer'; @@ -15,6 +15,7 @@ import { IOptionsService, IBufferService, IDecorationService } from 'common/serv import { ICharacterJoinerService, ICoreBrowserService } from 'browser/services/Services'; import { JoinedCellData } from 'browser/services/CharacterJoinerService'; import { color, css } from 'common/Color'; +import { Terminal } from 'xterm'; /** * This CharData looks like a null character, which will forc a clear and render @@ -31,18 +32,18 @@ export class TextRenderLayer extends BaseRenderLayer { private _workCell = new CellData(); constructor( + terminal: Terminal, container: HTMLElement, zIndex: number, colors: IColorSet, alpha: boolean, - rendererId: number, bufferService: IBufferService, optionsService: IOptionsService, private readonly _characterJoinerService: ICharacterJoinerService, decorationService: IDecorationService, coreBrowserService: ICoreBrowserService ) { - super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService, decorationService, coreBrowserService); + super(terminal, container, 'text', zIndex, alpha, colors, bufferService, optionsService, decorationService, coreBrowserService); this._state = new GridCache(); } @@ -232,78 +233,7 @@ export class TextRenderLayer extends BaseRenderLayer { } private _drawForeground(firstRow: number, lastRow: number): void { - this._forEachCell(firstRow, lastRow, (cell, x, y) => { - if (cell.isInvisible()) { - return; - } - this._drawChars(cell, x, y); - if (cell.isUnderline() || cell.isStrikethrough()) { - this._ctx.save(); - - if (cell.isInverse()) { - if (cell.isBgDefault()) { - this._ctx.fillStyle = this._colors.background.css; - } else if (cell.isBgRGB()) { - this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`; - } else { - let bg = cell.getBgColor(); - if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && bg < 8) { - bg += 8; - } - this._ctx.fillStyle = this._colors.ansi[bg].css; - } - } else { - if (cell.isFgDefault()) { - this._ctx.fillStyle = this._colors.foreground.css; - } else if (cell.isFgRGB()) { - this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`; - } else { - let fg = cell.getFgColor(); - if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8) { - fg += 8; - } - this._ctx.fillStyle = this._colors.ansi[fg].css; - } - } - - if (cell.isStrikethrough()) { - this._fillMiddleLineAtCells(x, y, cell.getWidth()); - } - if (cell.isUnderline()) { - if (!cell.isUnderlineColorDefault()) { - if (cell.isUnderlineColorRGB()) { - this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getUnderlineColor()).join(',')})`; - } else { - let fg = cell.getUnderlineColor(); - if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8) { - fg += 8; - } - this._ctx.fillStyle = this._colors.ansi[fg].css; - } - } - switch (cell.extended.underlineStyle) { - case UnderlineStyle.DOUBLE: - this._fillBottomLineAtCells(x, y, cell.getWidth(), -this._coreBrowserService.dpr); - this._fillBottomLineAtCells(x, y, cell.getWidth(), this._coreBrowserService.dpr); - break; - case UnderlineStyle.CURLY: - this._curlyUnderlineAtCell(x, y, cell.getWidth()); - break; - case UnderlineStyle.DOTTED: - this._dottedUnderlineAtCell(x, y, cell.getWidth()); - break; - case UnderlineStyle.DASHED: - this._dashedUnderlineAtCell(x, y, cell.getWidth()); - break; - case UnderlineStyle.SINGLE: - default: - this._fillBottomLineAtCells(x, y, cell.getWidth()); - break; - } - } - this._ctx.restore(); - } - }); + this._forEachCell(firstRow, lastRow, (cell, x, y) => this._drawChars(cell, x, y)); } public onGridChanged(firstRow: number, lastRow: number): void { diff --git a/addons/xterm-addon-canvas/src/Types.d.ts b/addons/xterm-addon-canvas/src/Types.d.ts index dda6052a..0527eae9 100644 --- a/addons/xterm-addon-canvas/src/Types.d.ts +++ b/addons/xterm-addon-canvas/src/Types.d.ts @@ -58,6 +58,7 @@ export interface IRenderer extends IDisposable { export interface IRenderLayer extends IDisposable { readonly canvas: HTMLCanvasElement; + readonly cacheCanvas: HTMLCanvasElement; /** * Called when the terminal loses focus. diff --git a/addons/xterm-addon-canvas/src/atlas/BaseCharAtlas.ts b/addons/xterm-addon-canvas/src/atlas/BaseCharAtlas.ts deleted file mode 100644 index 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/DynamicCharAtlas.ts b/addons/xterm-addon-canvas/src/atlas/CanvasCharAtlas.ts similarity index 93% rename from addons/xterm-addon-canvas/src/atlas/DynamicCharAtlas.ts rename to addons/xterm-addon-canvas/src/atlas/CanvasCharAtlas.ts index b809378e..a14ff5d7 100644 --- a/addons/xterm-addon-canvas/src/atlas/DynamicCharAtlas.ts +++ b/addons/xterm-addon-canvas/src/atlas/CanvasCharAtlas.ts @@ -3,26 +3,21 @@ * @license MIT */ -import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, TEXT_BASELINE } from 'browser/renderer/Constants'; -import { IGlyphIdentifier, ICharAtlasConfig } from './Types'; -import { BaseCharAtlas } from './BaseCharAtlas'; +import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, TEXT_BASELINE } from 'browser/renderer/shared/Constants'; +import { IGlyphIdentifier } from './Types'; import { DEFAULT_ANSI_COLORS } from 'browser/ColorManager'; import { LRUMap } from './LRUMap'; import { isFirefox, isSafari } from 'common/Platform'; import { IColor } from 'common/Types'; -import { throwIfFalsy } from 'browser/renderer/RendererUtils'; -import { color } from 'common/Color'; +import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; +import { color, NULL_COLOR } from 'common/Color'; +import { ICharAtlasConfig } from 'browser/renderer/shared/Types'; // In practice we're probably never going to exhaust a texture this large. For debugging purposes, // however, it can be useful to set this to a really tiny value, to verify that LRU eviction works. const TEXTURE_WIDTH = 1024; const TEXTURE_HEIGHT = 1024; -const TRANSPARENT_COLOR = { - css: 'rgba(0, 0, 0, 0)', - rgba: 0 -}; - // Drawing to the cache is expensive: If we have to draw more than this number of glyphs to the // cache in a single frame, give up on trying to cache anything else, and try to finish the current // frame ASAP. @@ -56,14 +51,16 @@ export function getGlyphCacheKey(glyph: IGlyphIdentifier): number { return glyph.code << 21 | glyph.bg << 12 | glyph.fg << 3 | (glyph.bold ? 0 : 4) + (glyph.dim ? 0 : 2) + (glyph.italic ? 0 : 1); } -export class DynamicCharAtlas extends BaseCharAtlas { +export class CanvasCharAtlas { // An ordered map that we're using to keep track of where each glyph is in the atlas texture. // It's ordered so that we can determine when to remove the old entries. private _cacheMap: LRUMap; // The texture that the atlas is drawn to private _cacheCanvas: HTMLCanvasElement; + public get cacheCanvas(): HTMLCanvasElement { return this._cacheCanvas; } private _cacheCtx: CanvasRenderingContext2D; + private _didWarmUp: boolean = false; // A temporary context that glyphs are drawn to before being transfered to the atlas. private _tmpCtx: CanvasRenderingContext2D; @@ -84,7 +81,6 @@ export class DynamicCharAtlas extends BaseCharAtlas { private _bitmap: ImageBitmap | null = null; constructor(document: Document, private _config: ICharAtlasConfig) { - super(); this._cacheCanvas = document.createElement('canvas'); this._cacheCanvas.width = TEXTURE_WIDTH; this._cacheCanvas.height = TEXTURE_HEIGHT; @@ -118,6 +114,23 @@ export class DynamicCharAtlas extends BaseCharAtlas { } } + /** + * Perform any work needed to warm the cache before it can be used. May be called multiple times. + * Implement _doWarmUp instead if you only want to get called once. + */ + public warmUp(): void { + if (!this._didWarmUp) { + this._doWarmUp(); + this._didWarmUp = true; + } + } + + /** + * Perform any work needed to warm the cache before it can be used. Used by the default + * implementation of warmUp(), and will only be called once. + */ + private _doWarmUp(): void { } + public beginFrame(): void { this._drawToCacheCount = 0; } @@ -226,7 +239,7 @@ export class DynamicCharAtlas extends BaseCharAtlas { // The background color might have some transparency, so we need to render it as fully // transparent in the atlas. Otherwise we'd end up drawing the transparent background twice // around the anti-aliased edges of the glyph, and it would look too dark. - return TRANSPARENT_COLOR; + return NULL_COLOR; } let result: IColor; if (glyph.bg === INVERTED_DEFAULT_COLOR) { @@ -374,23 +387,6 @@ export class DynamicCharAtlas extends BaseCharAtlas { } } -// This is used for debugging the renderer, just swap out `new DynamicCharAtlas` with -// `new NoneCharAtlas`. -export class NoneCharAtlas extends BaseCharAtlas { - constructor(document: Document, config: ICharAtlasConfig) { - super(); - } - - public draw( - ctx: CanvasRenderingContext2D, - glyph: IGlyphIdentifier, - x: number, - y: number - ): boolean { - return false; - } -} - /** * Makes a particular rgb color and colors that are nearly the same in an ImageData completely * transparent. diff --git a/addons/xterm-addon-canvas/src/atlas/CharAtlasCache.ts b/addons/xterm-addon-canvas/src/atlas/CharAtlasCache.ts index d9349286..f5995cf8 100644 --- a/addons/xterm-addon-canvas/src/atlas/CharAtlasCache.ts +++ b/addons/xterm-addon-canvas/src/atlas/CharAtlasCache.ts @@ -3,19 +3,18 @@ * @license MIT */ -import { generateConfig, configEquals } from './CharAtlasUtils'; -import { BaseCharAtlas } from './BaseCharAtlas'; -import { DynamicCharAtlas } from './DynamicCharAtlas'; -import { ICharAtlasConfig } from './Types'; +import { generateConfig, configEquals } from 'browser/renderer/shared/CharAtlasUtils'; +import { CanvasCharAtlas } from './CanvasCharAtlas'; import { IColorSet } from 'browser/Types'; -import { ITerminalOptions } from 'xterm'; +import { Terminal } from 'xterm'; +import { ICharAtlasConfig } from 'browser/renderer/shared/Types'; interface ICharAtlasCacheEntry { - atlas: BaseCharAtlas; + atlas: CanvasCharAtlas; config: ICharAtlasConfig; // N.B. This implementation potentially holds onto copies of the terminal forever, so // this may cause memory leaks. - ownedBy: number[]; + ownedBy: Terminal[]; } const charAtlasCache: ICharAtlasCacheEntry[] = []; @@ -25,19 +24,20 @@ const charAtlasCache: ICharAtlasCacheEntry[] = []; * one that is in use by another terminal. */ export function acquireCharAtlas( - options: Required, - rendererId: number, + terminal: Terminal, colors: IColorSet, + scaledCellWidth: number, + scaledCellHeight: number, scaledCharWidth: number, scaledCharHeight: number, devicePixelRatio: number -): BaseCharAtlas { - const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, options, colors, devicePixelRatio); +): CanvasCharAtlas { + const newConfig = generateConfig(scaledCellWidth, scaledCellHeight, scaledCharWidth, scaledCharHeight, terminal, colors, devicePixelRatio); // Check to see if the renderer already owns this config for (let i = 0; i < charAtlasCache.length; i++) { const entry = charAtlasCache[i]; - const ownedByIndex = entry.ownedBy.indexOf(rendererId); + const ownedByIndex = entry.ownedBy.indexOf(terminal); if (ownedByIndex >= 0) { if (configEquals(entry.config, newConfig)) { return entry.atlas; @@ -58,18 +58,18 @@ export function acquireCharAtlas( const entry = charAtlasCache[i]; if (configEquals(entry.config, newConfig)) { // Add the renderer to the cache entry and return - entry.ownedBy.push(rendererId); + entry.ownedBy.push(terminal); return entry.atlas; } } const newEntry: ICharAtlasCacheEntry = { - atlas: new DynamicCharAtlas( + atlas: new CanvasCharAtlas( document, newConfig ), config: newConfig, - ownedBy: [rendererId] + ownedBy: [terminal] }; charAtlasCache.push(newEntry); return newEntry.atlas; @@ -78,9 +78,9 @@ export function acquireCharAtlas( /** * Removes a terminal reference from the cache, allowing its memory to be freed. */ -export function removeTerminalFromCache(rendererId: number): void { +export function removeTerminalFromCache(terminal: Terminal): void { for (let i = 0; i < charAtlasCache.length; i++) { - const index = charAtlasCache[i].ownedBy.indexOf(rendererId); + const index = charAtlasCache[i].ownedBy.indexOf(terminal); if (index !== -1) { if (charAtlasCache[i].ownedBy.length === 1) { // Remove the cache entry if it's the only renderer diff --git a/addons/xterm-addon-canvas/src/atlas/CharAtlasUtils.ts b/addons/xterm-addon-canvas/src/atlas/CharAtlasUtils.ts deleted file mode 100644 index fa4be1e5..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.rgba === b.colors.foreground.rgba && - a.colors.background.rgba === b.colors.background.rgba; -} - -export function is256Color(colorCode: number): boolean { - return colorCode < DEFAULT_COLOR; -} diff --git a/addons/xterm-addon-canvas/src/atlas/Types.d.ts b/addons/xterm-addon-canvas/src/atlas/Types.d.ts index d8bc54c1..cde8abec 100644 --- a/addons/xterm-addon-canvas/src/atlas/Types.d.ts +++ b/addons/xterm-addon-canvas/src/atlas/Types.d.ts @@ -15,15 +15,3 @@ export interface IGlyphIdentifier { dim: boolean; italic: boolean; } - -export interface ICharAtlasConfig { - devicePixelRatio: number; - fontSize: number; - fontFamily: string; - fontWeight: FontWeight; - fontWeightBold: FontWeight; - scaledCharWidth: number; - scaledCharHeight: number; - allowTransparency: boolean; - colors: IPartialColorSet; -} diff --git a/addons/xterm-addon-canvas/typings/xterm-addon-canvas.d.ts b/addons/xterm-addon-canvas/typings/xterm-addon-canvas.d.ts index 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-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index 689b847a..f27ef29f 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; @@ -77,7 +77,7 @@ let $leftCellPadding = 0; let $clippedPixels = 0; export class GlyphRenderer extends Disposable { - private _atlas: WebglCharAtlas | undefined; + private _atlas: ITextureAtlas | undefined; private _program: WebGLProgram; private _vertexArrayObject: IWebGLVertexArrayObject; @@ -321,7 +321,7 @@ export class GlyphRenderer extends Disposable { gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, bufferLength / INDICES_PER_CELL); } - public setAtlas(atlas: WebglCharAtlas): void { + public setAtlas(atlas: ITextureAtlas): void { const gl = this._gl; this._atlas = atlas; diff --git a/addons/xterm-addon-webgl/src/RectangleRenderer.ts b/addons/xterm-addon-webgl/src/RectangleRenderer.ts index dccc7f6a..50a8cd58 100644 --- a/addons/xterm-addon-webgl/src/RectangleRenderer.ts +++ b/addons/xterm-addon-webgl/src/RectangleRenderer.ts @@ -3,16 +3,17 @@ * @license MIT */ -import { createProgram, expandFloat32Array, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils'; +import { createProgram, expandFloat32Array, PROJECTION_MATRIX } from './WebglUtils'; import { IRenderModel, IWebGLVertexArrayObject, IWebGL2RenderingContext } from './Types'; import { Attributes, BgFlags, FgFlags } from 'common/buffer/Constants'; import { Terminal } from 'xterm'; import { IColor } from 'common/Types'; import { IColorSet } from 'browser/Types'; -import { IRenderDimensions } from 'browser/renderer/Types'; +import { IRenderDimensions } from 'browser/renderer/shared/Types'; import { RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel'; import { Disposable, toDisposable } from 'common/Lifecycle'; -import { DIM_OPACITY } from 'browser/renderer/Constants'; +import { DIM_OPACITY } from 'browser/renderer/shared/Constants'; +import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; const enum VertexAttribLocations { POSITION = 0, 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/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 8d8b24f7..c7511946 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -3,44 +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, initEvent } from 'common/EventEmitter'; -import { CellData } from 'common/buffer/CellData'; import { addDisposableDomListener } from 'browser/Lifecycle'; +import { CellColorResolver } from 'browser/renderer/shared/CellColorResolver'; +import { acquireTextureAtlas, removeTerminalFromCache } from 'browser/renderer/shared/CharAtlasCache'; +import { observeDevicePixelDimensions } from 'browser/renderer/shared/DevicePixelObserver'; +import { IRenderDimensions, IRenderer, IRequestRedrawEvent, ITextureAtlas } from 'browser/renderer/shared/Types'; import { ICharacterJoinerService, ICoreBrowserService } from 'browser/services/Services'; -import { CharData, IBufferLine, ICellData } from 'common/Types'; +import { IColorSet, ITerminal } from 'browser/Types'; import { AttributeData } from 'common/buffer/AttributeData'; +import { CellData } from 'common/buffer/CellData'; +import { Content, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants'; +import { initEvent } from 'common/EventEmitter'; +import { Disposable } from 'common/Lifecycle'; import { ICoreService, IDecorationService } from 'common/services/Services'; - -// Work variables to avoid garbage collection -let $fg = 0; -let $bg = 0; -let $hasFg = false; -let $hasBg = false; -let $isSelected = false; +import { CharData, IBufferLine, ICellData } from 'common/Types'; +import { Terminal } from 'xterm'; +import { GlyphRenderer } from './GlyphRenderer'; +import { RectangleRenderer } from './RectangleRenderer'; +import { CursorRenderLayer } from './renderLayer/CursorRenderLayer'; +import { LinkRenderLayer } from './renderLayer/LinkRenderLayer'; +import { IRenderLayer } from './renderLayer/Types'; +import { COMBINED_CHAR_BIT_MASK, RenderModel, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_EXT_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel'; +import { IWebGL2RenderingContext } from './Types'; export class WebglRenderer extends Disposable implements IRenderer { private _renderLayers: IRenderLayer[]; - private _charAtlas: WebglCharAtlas | undefined; + private _charAtlas: ITextureAtlas | undefined; private _devicePixelRatio: number; private _model: RenderModel = new RenderModel(); private _workCell: CellData = new CellData(); - private _workColors: { fg: number, bg: number, ext: number } = { fg: 0, bg: 0, ext: 0 }; + private _cellColorResolver: CellColorResolver; private _canvas: HTMLCanvasElement; private _gl: IWebGL2RenderingContext; @@ -68,6 +61,8 @@ export class WebglRenderer extends Disposable implements IRenderer { ) { super(); + this._cellColorResolver = new CellColorResolver(this._terminal, this._colors, this._model.selection, this._decorationService, this._coreBrowserService); + this._core = (this._terminal as any)._core; this._renderLayers = [ @@ -156,6 +151,7 @@ export class WebglRenderer extends Disposable implements IRenderer { l.reset(this._terminal); } + this._cellColorResolver.setColors(colors); this._rectangleRenderer.setColors(); this._refreshCharAtlas(); @@ -230,7 +226,7 @@ export class WebglRenderer extends Disposable implements IRenderer { for (const l of this._renderLayers) { l.onSelectionChanged(this._terminal, start, end, columnSelectMode); } - this._updateSelectionModel(start, end, columnSelectMode); + this._model.selection.update(this._terminal, start, end, columnSelectMode); this._requestRedrawViewport(); } @@ -275,10 +271,7 @@ export class WebglRenderer extends Disposable implements IRenderer { return; } - const atlas = acquireCharAtlas(this._terminal, this._colors, this.dimensions.scaledCellWidth, this.dimensions.scaledCellHeight, this.dimensions.scaledCharWidth, this.dimensions.scaledCharHeight, this._coreBrowserService.dpr); - if (!('getRasterizedGlyph' in atlas)) { - throw new Error('The webgl renderer only works with the webgl char atlas'); - } + const atlas = acquireTextureAtlas(this._terminal, this._colors, this.dimensions.scaledCellWidth, this.dimensions.scaledCellHeight, this.dimensions.scaledCharWidth, this.dimensions.scaledCharHeight, this._coreBrowserService.dpr); if (this._charAtlas !== atlas) { this.onChangeTextureAtlas.fire(atlas.cacheCanvas); } @@ -340,7 +333,7 @@ export class WebglRenderer extends Disposable implements IRenderer { // Tell renderer the frame is beginning if (this._glyphRenderer.beginFrame()) { this._clearModel(true); - this._updateSelectionModel(undefined, undefined); + this._model.selection.clear(); } // Update model to reflect what's drawn @@ -376,11 +369,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. @@ -411,7 +404,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; @@ -419,9 +412,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; } @@ -432,11 +425,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 @@ -447,9 +440,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; } } } @@ -457,153 +450,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 - $bg = 0; - $fg = 0; - $hasBg = false; - $hasFg = false; - $isSelected = false; - - // Apply decorations on the bottom layer - this._decorationService.forEachDecorationAtCell(x, y, 'bottom', d => { - if (d.backgroundColorRGB) { - $bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF; - $hasBg = true; - } - if (d.foregroundColorRGB) { - $fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF; - $hasFg = true; - } - }); - - // Apply the selection color if needed - $isSelected = this._isCellSelected(x, y); - if ($isSelected) { - $bg = (this._coreBrowserService.isFocused ? this._colors.selectionBackgroundOpaque : this._colors.selectionInactiveBackgroundOpaque).rgba >> 8 & 0xFFFFFF; - $hasBg = true; - if (this._colors.selectionForeground) { - $fg = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF; - $hasFg = true; - } - } - - // Apply decorations on the top layer - this._decorationService.forEachDecorationAtCell(x, y, 'top', d => { - if (d.backgroundColorRGB) { - $bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF; - $hasBg = true; - } - if (d.foregroundColorRGB) { - $fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF; - $hasFg = true; - } - }); - - // Convert any overrides from rgba to the fg/bg packed format. This resolves the inverse flag - // ahead of time in order to use the correct cache key - if ($hasBg) { - if ($isSelected) { - // Non-RGB attributes from model + force non-dim + override + force RGB color mode - $bg = (this._workCell.bg & ~Attributes.RGB_MASK & ~BgFlags.DIM) | $bg | Attributes.CM_RGB; - } else { - // Non-RGB attributes from model + override + force RGB color mode - $bg = (this._workCell.bg & ~Attributes.RGB_MASK) | $bg | Attributes.CM_RGB; - } - } - if ($hasFg) { - // Non-RGB attributes from model + force disable inverse + override + force RGB color mode - $fg = (this._workCell.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._workColors.fg & FgFlags.INVERSE) { - if ($hasBg && !$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) { - $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 { - $fg = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | this._workColors.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._workColors.fg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) { - $bg = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | ((this._colors.foreground.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB; - } else { - $bg = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | this._workColors.fg & (Attributes.RGB_MASK | Attributes.CM_MASK); - } - $hasBg = true; - } - } - - // Use the override if it exists - this._workColors.bg = $hasBg ? $bg : this._workColors.bg; - this._workColors.fg = $hasFg ? $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..07eca0bf 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts @@ -4,15 +4,14 @@ */ import { IRenderLayer } from './Types'; -import { acquireCharAtlas } from '../atlas/CharAtlasCache'; +import { acquireTextureAtlas } from 'browser/renderer/shared/CharAtlasCache'; import { Terminal } from 'xterm'; import { IColorSet } from 'browser/Types'; -import { TEXT_BASELINE } from 'browser/renderer/Constants'; +import { TEXT_BASELINE } from 'browser/renderer/shared/Constants'; import { ICoreBrowserService } from 'browser/services/Services'; -import { IRenderDimensions } from 'browser/renderer/Types'; +import { IRenderDimensions, ITextureAtlas } from 'browser/renderer/shared/Types'; import { CellData } from 'common/buffer/CellData'; -import { WebglCharAtlas } from 'atlas/WebglCharAtlas'; -import { throwIfFalsy } from '../WebglUtils'; +import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; export abstract class BaseRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; @@ -24,7 +23,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { private _scaledCharLeft: number = 0; private _scaledCharTop: number = 0; - protected _charAtlas: WebglCharAtlas | undefined; + protected _charAtlas: ITextureAtlas | undefined; constructor( private _container: HTMLElement, @@ -95,7 +94,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) { return; } - this._charAtlas = acquireCharAtlas(terminal, colorSet, this._scaledCellWidth, this._scaledCellHeight, this._scaledCharWidth, this._scaledCharHeight, this._coreBrowserService.dpr); + this._charAtlas = acquireTextureAtlas(terminal, colorSet, this._scaledCellWidth, this._scaledCellHeight, this._scaledCharWidth, this._scaledCharHeight, this._coreBrowserService.dpr); this._charAtlas.warmUp(); } diff --git a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts index 2b274516..522f6fb6 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts @@ -8,7 +8,7 @@ import { BaseRenderLayer } from './BaseRenderLayer'; import { ICellData } from 'common/Types'; import { CellData } from 'common/buffer/CellData'; import { IColorSet } from 'browser/Types'; -import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types'; +import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/shared/Types'; import { IEventEmitter } from 'common/EventEmitter'; import { ICoreBrowserService } from 'browser/services/Services'; import { ICoreService } from 'common/services/Services'; diff --git a/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts index e1ff08d8..dbf2fdcf 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts @@ -5,11 +5,11 @@ import { Terminal } from 'xterm'; import { BaseRenderLayer } from './BaseRenderLayer'; -import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants'; -import { is256Color } from '../atlas/CharAtlasUtils'; +import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants'; import { ITerminal, IColorSet, ILinkifierEvent } from 'browser/Types'; -import { IRenderDimensions } from 'browser/renderer/Types'; +import { IRenderDimensions } from 'browser/renderer/shared/Types'; import { ICoreBrowserService } from 'browser/services/Services'; +import { is256Color } from 'browser/renderer/shared/CharAtlasUtils'; export class LinkRenderLayer extends BaseRenderLayer { private _state: ILinkifierEvent | undefined; diff --git a/addons/xterm-addon-webgl/src/renderLayer/Types.ts b/addons/xterm-addon-webgl/src/renderLayer/Types.ts index 70acff34..993304a2 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/Types.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/Types.ts @@ -5,7 +5,7 @@ import { IDisposable, Terminal } from 'xterm'; import { IColorSet } from 'browser/Types'; -import { IRenderDimensions } from 'browser/renderer/Types'; +import { IRenderDimensions } from 'browser/renderer/shared/Types'; export interface IRenderLayer extends IDisposable { /** diff --git a/demo/client.ts b/demo/client.ts index 00b10b57..593b0da6 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); } @@ -300,8 +303,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 +430,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 +459,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 +492,7 @@ function initOptions(term: TerminalType): void { magenta: '#b168df', red: '#da6771', white: '#efefef', - yellow: '#fff099', + yellow: '#fff099' }; break; case 'light': @@ -513,7 +516,7 @@ function initOptions(term: TerminalType): void { magenta: '#bc05bc', red: '#cd3131', white: '#555555', - yellow: '#949800', + yellow: '#949800' }; break; } @@ -545,7 +548,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 +570,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 +597,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 +613,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 +638,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 +697,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 +730,7 @@ function loadTest() { }); } -function powerlineSymbolTest() { +function powerlineSymbolTest(): void { function s(char: string): string { return `${char} \x1b[7m${char}\x1b[0m `; } @@ -791,7 +804,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 +814,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 +861,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 +886,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 +965,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 +980,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/src/browser/ColorManager.ts b/src/browser/ColorManager.ts index a22a423f..0eb726a6 100644 --- a/src/browser/ColorManager.ts +++ b/src/browser/ColorManager.ts @@ -5,7 +5,7 @@ import { IColorManager, IColorSet, IColorContrastCache } from 'browser/Types'; import { ITheme } from 'common/services/Services'; -import { channels, color, css } from 'common/Color'; +import { channels, color, css, NULL_COLOR } from 'common/Color'; import { ColorContrastCache } from 'browser/ColorContrastCache'; import { ColorIndex, IColor } from 'common/Types'; @@ -124,12 +124,8 @@ export class ColorManager implements IColorManager { this.colors.selectionBackgroundOpaque = color.blend(this.colors.background, this.colors.selectionBackgroundTransparent); this.colors.selectionInactiveBackgroundTransparent = this._parseColor(theme.selectionInactiveBackground, this.colors.selectionBackgroundTransparent); this.colors.selectionInactiveBackgroundOpaque = color.blend(this.colors.background, this.colors.selectionInactiveBackgroundTransparent); - const nullColor: IColor = { - css: '', - rgba: 0 - }; - this.colors.selectionForeground = theme.selectionForeground ? this._parseColor(theme.selectionForeground, nullColor) : undefined; - if (this.colors.selectionForeground === nullColor) { + this.colors.selectionForeground = theme.selectionForeground ? this._parseColor(theme.selectionForeground, NULL_COLOR) : undefined; + if (this.colors.selectionForeground === NULL_COLOR) { this.colors.selectionForeground = undefined; } diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index 90c1b134..bc8b19cf 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'; diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index 95920375..18c51864 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -6,7 +6,7 @@ import { IDisposable, IMarker, ILinkProvider, IDecorationOptions, IDecoration } from 'xterm'; import { IEvent, EventEmitter, initEvent } from 'common/EventEmitter'; import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services'; -import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types'; +import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types'; import { IColorSet, ITerminal, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper, CharacterJoinerHandler, IBufferRange } from 'browser/Types'; import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types'; import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, ITerminalOptions } from 'common/Types'; diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index f95e0bb4..45373353 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -9,7 +9,7 @@ import { IColorSet, IViewport } from 'browser/Types'; import { ICharSizeService, ICoreBrowserService, IRenderService } from 'browser/services/Services'; import { IBufferService, IOptionsService } from 'common/services/Services'; import { IBuffer } from 'common/buffer/Types'; -import { IRenderDimensions } from 'browser/renderer/Types'; +import { IRenderDimensions } from 'browser/renderer/shared/Types'; const FALLBACK_SCROLL_BAR_WIDTH = 15; diff --git a/src/browser/renderer/Types.d.ts b/src/browser/renderer/Types.d.ts deleted file mode 100644 index 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 8900a157..1a1f1013 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -3,9 +3,9 @@ * @license MIT */ -import { IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types'; +import { IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/shared/Types'; import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_BLINK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from 'browser/renderer/dom/DomRendererRowFactory'; -import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants'; +import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants'; import { Disposable } from 'common/Lifecycle'; import { IColorSet, ILinkifierEvent, ILinkifier2 } from 'browser/Types'; import { ICharSizeService, ICoreBrowserService } from 'browser/services/Services'; diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index b6ee7bf1..bce72ec3 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -4,7 +4,7 @@ */ import { IBufferLine, ICellData, IColor } from 'common/Types'; -import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants'; +import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants'; import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; @@ -12,7 +12,7 @@ import { color, rgba } from 'common/Color'; import { IColorSet } from 'browser/Types'; import { ICharacterJoinerService, ICoreBrowserService } from 'browser/services/Services'; import { JoinedCellData } from 'browser/services/CharacterJoinerService'; -import { excludeFromContrastRatioDemands } from 'browser/renderer/RendererUtils'; +import { excludeFromContrastRatioDemands } from 'browser/renderer/shared/RendererUtils'; import { AttributeData } from 'common/buffer/AttributeData'; export const BOLD_CLASS = 'xterm-bold'; diff --git a/src/browser/renderer/shared/CellColorResolver.ts b/src/browser/renderer/shared/CellColorResolver.ts new file mode 100644 index 00000000..b29475b9 --- /dev/null +++ b/src/browser/renderer/shared/CellColorResolver.ts @@ -0,0 +1,136 @@ +import { ISelectionRenderModel } from 'browser/renderer/shared/Types'; +import { ICoreBrowserService } from 'browser/services/Services'; +import { IColorSet } from 'browser/Types'; +import { Attributes, BgFlags, FgFlags } from 'common/buffer/Constants'; +import { IDecorationService } from 'common/services/Services'; +import { ICellData } from 'common/Types'; +import { Terminal } from 'xterm'; + +// Work variables to avoid garbage collection +let $fg = 0; +let $bg = 0; +let $hasFg = false; +let $hasBg = false; +let $isSelected = false; + +export class CellColorResolver { + /** + * The shared result of the {@link resolve} call. This is only safe to use immediately after as + * any other calls will share object. + */ + public readonly result: { fg: number, bg: number, ext: number } = { + fg: 0, + bg: 0, + ext: 0 + }; + + constructor( + private readonly _terminal: Terminal, + private _colors: IColorSet, + private readonly _selectionRenderModel: ISelectionRenderModel, + private readonly _decorationService: IDecorationService, + private readonly _coreBrowserService: ICoreBrowserService + ) { + } + + public setColors(colors: IColorSet): void { + this._colors = colors; + } + + /** + * Resolves colors for the cell, putting the result into the shared {@link result}. This resolves + * overrides, inverse and selection for the cell which can then be used to feed into the renderer. + */ + public resolve(cell: ICellData, x: number, y: number): void { + this.result.bg = cell.bg; + this.result.fg = cell.fg; + this.result.ext = cell.bg & BgFlags.HAS_EXTENDED ? cell.extended.ext : 0; + // Get any foreground/background overrides, this happens on the model to avoid spreading + // override logic throughout the different sub-renderers + + // Reset overrides work variables + $bg = 0; + $fg = 0; + $hasBg = false; + $hasFg = false; + $isSelected = false; + + // Apply decorations on the bottom layer + this._decorationService.forEachDecorationAtCell(x, y, 'bottom', d => { + if (d.backgroundColorRGB) { + $bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF; + $hasBg = true; + } + if (d.foregroundColorRGB) { + $fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF; + $hasFg = true; + } + }); + + // Apply the selection color if needed + $isSelected = this._selectionRenderModel.isCellSelected(this._terminal, x, y); + if ($isSelected) { + $bg = (this._coreBrowserService.isFocused ? this._colors.selectionBackgroundOpaque : this._colors.selectionInactiveBackgroundOpaque).rgba >> 8 & 0xFFFFFF; + $hasBg = true; + if (this._colors.selectionForeground) { + $fg = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF; + $hasFg = true; + } + } + + // Apply decorations on the top layer + this._decorationService.forEachDecorationAtCell(x, y, 'top', d => { + if (d.backgroundColorRGB) { + $bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF; + $hasBg = true; + } + if (d.foregroundColorRGB) { + $fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF; + $hasFg = true; + } + }); + + // Convert any overrides from rgba to the fg/bg packed format. This resolves the inverse flag + // ahead of time in order to use the correct cache key + if ($hasBg) { + if ($isSelected) { + // Non-RGB attributes from model + force non-dim + override + force RGB color mode + $bg = (cell.bg & ~Attributes.RGB_MASK & ~BgFlags.DIM) | $bg | Attributes.CM_RGB; + } else { + // Non-RGB attributes from model + override + force RGB color mode + $bg = (cell.bg & ~Attributes.RGB_MASK) | $bg | Attributes.CM_RGB; + } + } + if ($hasFg) { + // Non-RGB attributes from model + force disable inverse + override + force RGB color mode + $fg = (cell.fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | $fg | Attributes.CM_RGB; + } + + // Handle case where inverse was specified by only one of bg override or fg override was set, + // resolving the other inverse color and setting the inverse flag if needed. + if (this.result.fg & FgFlags.INVERSE) { + if ($hasBg && !$hasFg) { + // Resolve bg color type (default color has a different meaning in fg vs bg) + if ((this.result.bg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) { + $fg = (this.result.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | ((this._colors.background.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB; + } else { + $fg = (this.result.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | this.result.bg & (Attributes.RGB_MASK | Attributes.CM_MASK); + } + $hasFg = true; + } + if (!$hasBg && $hasFg) { + // Resolve bg color type (default color has a different meaning in fg vs bg) + if ((this.result.fg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) { + $bg = (this.result.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | ((this._colors.foreground.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB; + } else { + $bg = (this.result.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | this.result.fg & (Attributes.RGB_MASK | Attributes.CM_MASK); + } + $hasBg = true; + } + } + + // Use the override if it exists + this.result.bg = $hasBg ? $bg : this.result.bg; + this.result.fg = $hasFg ? $fg : this.result.fg; + } +} diff --git a/addons/xterm-addon-webgl/src/atlas/CharAtlasCache.ts b/src/browser/renderer/shared/CharAtlasCache.ts similarity index 83% rename from addons/xterm-addon-webgl/src/atlas/CharAtlasCache.ts rename to src/browser/renderer/shared/CharAtlasCache.ts index 893362c8..ee88c708 100644 --- a/addons/xterm-addon-webgl/src/atlas/CharAtlasCache.ts +++ b/src/browser/renderer/shared/CharAtlasCache.ts @@ -3,21 +3,21 @@ * @license MIT */ -import { generateConfig, configEquals } from './CharAtlasUtils'; -import { WebglCharAtlas } from './WebglCharAtlas'; -import { ICharAtlasConfig } from './Types'; +import { TextureAtlas } from 'browser/renderer/shared/TextureAtlas'; import { Terminal } from 'xterm'; import { IColorSet, ITerminal } from 'browser/Types'; +import { ICharAtlasConfig, ITextureAtlas } from 'browser/renderer/shared/Types'; +import { generateConfig, configEquals } from 'browser/renderer/shared/CharAtlasUtils'; -interface ICharAtlasCacheEntry { - atlas: WebglCharAtlas; +interface ITextureAtlasCacheEntry { + atlas: ITextureAtlas; config: ICharAtlasConfig; // N.B. This implementation potentially holds onto copies of the terminal forever, so // this may cause memory leaks. ownedBy: Terminal[]; } -const charAtlasCache: ICharAtlasCacheEntry[] = []; +const charAtlasCache: ITextureAtlasCacheEntry[] = []; /** * Acquires a char atlas, either generating a new one or returning an existing @@ -25,7 +25,7 @@ const charAtlasCache: ICharAtlasCacheEntry[] = []; * @param terminal The terminal. * @param colors The colors to use. */ -export function acquireCharAtlas( +export function acquireTextureAtlas( terminal: Terminal, colors: IColorSet, scaledCellWidth: number, @@ -33,7 +33,7 @@ export function acquireCharAtlas( scaledCharWidth: number, scaledCharHeight: number, devicePixelRatio: number -): WebglCharAtlas { +): ITextureAtlas { const newConfig = generateConfig(scaledCellWidth, scaledCellHeight, scaledCharWidth, scaledCharHeight, terminal, colors, devicePixelRatio); // Check to see if the terminal already owns this config @@ -66,8 +66,8 @@ export function acquireCharAtlas( } const core: ITerminal = (terminal as any)._core; - const newEntry: ICharAtlasCacheEntry = { - atlas: new WebglCharAtlas(document, newConfig, core.unicodeService), + const newEntry: ITextureAtlasCacheEntry = { + atlas: new TextureAtlas(document, newConfig, core.unicodeService), config: newConfig, ownedBy: [terminal] }; diff --git a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts b/src/browser/renderer/shared/CharAtlasUtils.ts similarity index 95% rename from addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts rename to src/browser/renderer/shared/CharAtlasUtils.ts index dc503d75..58c207e8 100644 --- a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts +++ b/src/browser/renderer/shared/CharAtlasUtils.ts @@ -5,14 +5,9 @@ import { ICharAtlasConfig } from './Types'; import { Attributes } from 'common/buffer/Constants'; -import { Terminal, FontWeight } from 'xterm'; +import { Terminal } from 'xterm'; import { IColorSet } from 'browser/Types'; -import { IColor } from 'common/Types'; - -const NULL_COLOR: IColor = { - css: '', - rgba: 0 -}; +import { NULL_COLOR } from 'common/Color'; export function generateConfig(scaledCellWidth: number, scaledCellHeight: number, scaledCharWidth: number, scaledCharHeight: number, terminal: Terminal, colors: IColorSet, devicePixelRatio: number): ICharAtlasConfig { // null out some fields that don't matter 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 98% rename from addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts rename to src/browser/renderer/shared/TextureAtlas.ts index dee1e02c..c4bffe8d 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. */ @@ -57,7 +48,7 @@ interface ICharAtlasActiveRow { // 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(); @@ -202,7 +193,7 @@ export class WebglCharAtlas implements IDisposable { // The background color might have some transparency, so we need to render it as fully // transparent in the atlas. Otherwise we'd end up drawing the transparent background twice // around the anti-aliased edges of the glyph, and it would look too dark. - return TRANSPARENT_COLOR; + return NULL_COLOR; } let result: IColor; @@ -345,6 +336,9 @@ export class WebglCharAtlas implements IDisposable { private _drawToCache(codeOrChars: number | string, bg: number, fg: number, ext: number): IRasterizedGlyph { const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars; + // Uncomment for debugging + // console.log(`draw to cache "${chars}"`, bg, fg, ext); + this.hasCanvasChanged = true; // Allow 1 cell width per character, with a minimum of 2 (CJK), plus some padding. This is used diff --git a/src/browser/renderer/shared/Types.d.ts b/src/browser/renderer/shared/Types.d.ts new file mode 100644 index 00000000..7e2badcb --- /dev/null +++ b/src/browser/renderer/shared/Types.d.ts @@ -0,0 +1,156 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { FontWeight, Terminal } from 'xterm'; +import { IColorSet } from 'browser/Types'; +import { IDisposable } from 'common/Types'; +import { IEvent } from 'common/EventEmitter'; + +export interface ICharAtlasConfig { + customGlyphs: boolean; + devicePixelRatio: number; + letterSpacing: number; + lineHeight: number; + fontSize: number; + fontFamily: string; + fontWeight: FontWeight; + fontWeightBold: FontWeight; + scaledCellWidth: number; + scaledCellHeight: number; + scaledCharWidth: number; + scaledCharHeight: number; + allowTransparency: boolean; + drawBoldTextInBrightColors: boolean; + minimumContrastRatio: number; + colors: IColorSet; +} + +export interface IRenderDimensions { + scaledCharWidth: number; + scaledCharHeight: number; + scaledCellWidth: number; + scaledCellHeight: number; + scaledCharLeft: number; + scaledCharTop: number; + scaledCanvasWidth: number; + scaledCanvasHeight: number; + canvasWidth: number; + canvasHeight: number; + actualCellWidth: number; + actualCellHeight: number; +} + +export interface IRequestRedrawEvent { + start: number; + end: number; +} + +/** + * Note that IRenderer implementations should emit the refresh event after + * rendering rows to the screen. + */ +export interface IRenderer extends IDisposable { + readonly dimensions: IRenderDimensions; + + /** + * Fires when the renderer is requesting to be redrawn on the next animation + * frame but is _not_ a result of content changing (eg. selection changes). + */ + readonly onRequestRedraw: IEvent; + + dispose(): void; + setColors(colors: IColorSet): void; + onDevicePixelRatioChange(): void; + onResize(cols: number, rows: number): void; + onCharSizeChanged(): void; + onBlur(): void; + onFocus(): void; + onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void; + onCursorMove(): void; + onOptionsChanged(): void; + clear(): void; + renderRows(start: number, end: number): void; + clearTextureAtlas?(): void; +} + +export interface ITextureAtlas extends IDisposable { + readonly cacheCanvas: HTMLCanvasElement; + hasCanvasChanged: boolean; + + /** + * Warm up the texture atlas, adding common glyphs to avoid slowing early frame. + */ + warmUp(): void; + + /** + * Call when a frame is being drawn, this will return true if the atlas was cleared to make room + * for a new set of glyphs. + */ + beginFrame(): boolean; + + /** + * Clear all glyphs from the texture atlas. + */ + clearTexture(): void; + getRasterizedGlyph(code: number, bg: number, fg: number, ext: number): IRasterizedGlyph; + getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number): IRasterizedGlyph; +} + +/** + * Represents a rasterized glyph within a texture atlas. Some numbers are + * tracked in CSS pixels as well in order to reduce calculations during the + * render loop. + */ +export interface IRasterizedGlyph { + /** + * The x and y offset between the glyph's top/left and the top/left of a cell + * in pixels. + */ + offset: IVector; + /** + * the x and y position of the glyph in the texture in pixels. + */ + texturePosition: IVector; + /** + * the x and y position of the glyph in the texture in clip space coordinates. + */ + texturePositionClipSpace: IVector; + /** + * The width and height of the glyph in the texture in pixels. + */ + size: IVector; + /** + * The width and height of the glyph in the texture in clip space coordinates. + */ + sizeClipSpace: IVector; +} + +export interface IVector { + x: number; + y: number; +} + +export interface IBoundingBox { + top: number; + left: number; + right: number; + bottom: number; +} + +export interface ISelectionRenderModel { + readonly hasSelection: boolean; + readonly columnSelectMode: boolean; + readonly viewportStartRow: number; + readonly viewportEndRow: number; + readonly viewportCappedStartRow: number; + readonly viewportCappedEndRow: number; + readonly startCol: number; + readonly endCol: number; + readonly selectionStart: [number, number] | undefined; + readonly selectionEnd: [number, number] | undefined; + clear(): void; + update(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode?: boolean): void; + isCellSelected(terminal: Terminal, x: number, y: number): boolean; +} diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index dfb4df80..73850bdb 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IRenderer, IRenderDimensions } from 'browser/renderer/Types'; +import { IRenderer, IRenderDimensions } from 'browser/renderer/shared/Types'; import { RenderDebouncer } from 'browser/RenderDebouncer'; import { EventEmitter, IEvent, initEvent } from 'common/EventEmitter'; import { Disposable } from 'common/Lifecycle'; diff --git a/src/browser/services/Services.ts b/src/browser/services/Services.ts index ab91f8a3..9eea5c76 100644 --- a/src/browser/services/Services.ts +++ b/src/browser/services/Services.ts @@ -4,7 +4,7 @@ */ import { IEvent } from 'common/EventEmitter'; -import { IRenderDimensions, IRenderer } from 'browser/renderer/Types'; +import { IRenderDimensions, IRenderer } from 'browser/renderer/shared/Types'; import { IColorSet } from 'browser/Types'; import { ISelectionRedrawRequestEvent as ISelectionRequestRedrawEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; diff --git a/src/common/Color.ts b/src/common/Color.ts index 00d8613c..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). */ 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';