diff options
Diffstat (limited to 'node_modules/xterm/src/browser/renderer/BaseRenderLayer.ts')
-rw-r--r-- | node_modules/xterm/src/browser/renderer/BaseRenderLayer.ts | 513 |
1 files changed, 513 insertions, 0 deletions
diff --git a/node_modules/xterm/src/browser/renderer/BaseRenderLayer.ts b/node_modules/xterm/src/browser/renderer/BaseRenderLayer.ts new file mode 100644 index 0000000..629e943 --- /dev/null +++ b/node_modules/xterm/src/browser/renderer/BaseRenderLayer.ts @@ -0,0 +1,513 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IRenderDimensions, IRenderLayer } from 'browser/renderer/Types'; +import { ICellData } from 'common/Types'; +import { DEFAULT_COLOR, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE, Attributes } from 'common/buffer/Constants'; +import { IGlyphIdentifier } from 'browser/renderer/atlas/Types'; +import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, TEXT_BASELINE } from 'browser/renderer/atlas/Constants'; +import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas'; +import { acquireCharAtlas } from 'browser/renderer/atlas/CharAtlasCache'; +import { AttributeData } from 'common/buffer/AttributeData'; +import { IColorSet, IColor } from 'browser/Types'; +import { CellData } from 'common/buffer/CellData'; +import { IBufferService, IOptionsService } from 'common/services/Services'; +import { throwIfFalsy } from 'browser/renderer/RendererUtils'; +import { channels, color, rgba } from 'browser/Color'; +import { removeElementFromParent } from 'browser/Dom'; +import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs'; + +export abstract class BaseRenderLayer implements IRenderLayer { + private _canvas: HTMLCanvasElement; + protected _ctx!: CanvasRenderingContext2D; + private _scaledCharWidth: number = 0; + private _scaledCharHeight: number = 0; + private _scaledCellWidth: number = 0; + private _scaledCellHeight: number = 0; + private _scaledCharLeft: number = 0; + private _scaledCharTop: number = 0; + + 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 + }; + + constructor( + private _container: HTMLElement, + id: string, + zIndex: number, + private _alpha: boolean, + protected _colors: IColorSet, + private _rendererId: number, + protected readonly _bufferService: IBufferService, + protected readonly _optionsService: IOptionsService + ) { + this._canvas = document.createElement('canvas'); + this._canvas.classList.add(`xterm-${id}-layer`); + this._canvas.style.zIndex = zIndex.toString(); + this._initCanvas(); + this._container.appendChild(this._canvas); + } + + public dispose(): void { + removeElementFromParent(this._canvas); + this._charAtlas?.dispose(); + } + + private _initCanvas(): void { + this._ctx = throwIfFalsy(this._canvas.getContext('2d', { alpha: this._alpha })); + // Draw the background if this is an opaque layer + if (!this._alpha) { + this._clearAll(); + } + } + + public onOptionsChanged(): void {} + public onBlur(): void {} + public onFocus(): void {} + public onCursorMove(): void {} + public onGridChanged(startRow: number, endRow: number): void {} + public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {} + + public setColors(colorSet: IColorSet): void { + this._refreshCharAtlas(colorSet); + } + + protected _setTransparency(alpha: boolean): void { + // Do nothing when alpha doesn't change + if (alpha === this._alpha) { + return; + } + + // Create new canvas and replace old one + const oldCanvas = this._canvas; + this._alpha = alpha; + // Cloning preserves properties + this._canvas = this._canvas.cloneNode() as HTMLCanvasElement; + this._initCanvas(); + this._container.replaceChild(this._canvas, oldCanvas); + + // Regenerate char atlas and force a full redraw + this._refreshCharAtlas(this._colors); + this.onGridChanged(0, this._bufferService.rows - 1); + } + + /** + * Refreshes the char atlas, aquiring a new one if necessary. + * @param colorSet The color set to use for the char atlas. + */ + private _refreshCharAtlas(colorSet: IColorSet): void { + if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) { + return; + } + this._charAtlas = acquireCharAtlas(this._optionsService.rawOptions, this._rendererId, colorSet, this._scaledCharWidth, this._scaledCharHeight); + this._charAtlas.warmUp(); + } + + public resize(dim: IRenderDimensions): void { + this._scaledCellWidth = dim.scaledCellWidth; + this._scaledCellHeight = dim.scaledCellHeight; + this._scaledCharWidth = dim.scaledCharWidth; + this._scaledCharHeight = dim.scaledCharHeight; + this._scaledCharLeft = dim.scaledCharLeft; + this._scaledCharTop = dim.scaledCharTop; + this._canvas.width = dim.scaledCanvasWidth; + this._canvas.height = dim.scaledCanvasHeight; + this._canvas.style.width = `${dim.canvasWidth}px`; + this._canvas.style.height = `${dim.canvasHeight}px`; + + // Draw the background if this is an opaque layer + if (!this._alpha) { + this._clearAll(); + } + + this._refreshCharAtlas(this._colors); + } + + public abstract reset(): void; + + public clearTextureAtlas(): void { + this._charAtlas?.clear(); + } + + /** + * Fills 1+ cells completely. This uses the existing fillStyle on the context. + * @param x The column to start at. + * @param y The row to start at + * @param width The number of columns to fill. + * @param height The number of rows to fill. + */ + protected _fillCells(x: number, y: number, width: number, height: number): void { + this._ctx.fillRect( + x * this._scaledCellWidth, + y * this._scaledCellHeight, + width * this._scaledCellWidth, + height * this._scaledCellHeight); + } + + /** + * Fills a 1px line (2px on HDPI) at the middle of the cell. This uses the + * existing fillStyle on the context. + * @param x The column to fill. + * @param y The row to fill. + */ + protected _fillMiddleLineAtCells(x: number, y: number, width: number = 1): void { + const cellOffset = Math.ceil(this._scaledCellHeight * 0.5); + this._ctx.fillRect( + x * this._scaledCellWidth, + (y + 1) * this._scaledCellHeight - cellOffset - window.devicePixelRatio, + width * this._scaledCellWidth, + window.devicePixelRatio); + } + + /** + * Fills a 1px line (2px on HDPI) at the bottom of the cell. This uses the + * existing fillStyle on the context. + * @param x The column to fill. + * @param y The row to fill. + */ + protected _fillBottomLineAtCells(x: number, y: number, width: number = 1): void { + this._ctx.fillRect( + x * this._scaledCellWidth, + (y + 1) * this._scaledCellHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */, + width * this._scaledCellWidth, + window.devicePixelRatio); + } + + /** + * Fills a 1px line (2px on HDPI) at the left of the cell. This uses the + * existing fillStyle on the context. + * @param x The column to fill. + * @param y The row to fill. + */ + protected _fillLeftLineAtCell(x: number, y: number, width: number): void { + this._ctx.fillRect( + x * this._scaledCellWidth, + y * this._scaledCellHeight, + window.devicePixelRatio * width, + this._scaledCellHeight); + } + + /** + * Strokes a 1px rectangle (2px on HDPI) around a cell. This uses the existing + * strokeStyle on the context. + * @param x The column to fill. + * @param y The row to fill. + */ + protected _strokeRectAtCell(x: number, y: number, width: number, height: number): void { + this._ctx.lineWidth = window.devicePixelRatio; + this._ctx.strokeRect( + x * this._scaledCellWidth + window.devicePixelRatio / 2, + y * this._scaledCellHeight + (window.devicePixelRatio / 2), + width * this._scaledCellWidth - window.devicePixelRatio, + (height * this._scaledCellHeight) - window.devicePixelRatio); + } + + /** + * Clears the entire canvas. + */ + protected _clearAll(): void { + if (this._alpha) { + this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); + } else { + this._ctx.fillStyle = this._colors.background.css; + this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height); + } + } + + /** + * Clears 1+ cells completely. + * @param x The column to start at. + * @param y The row to start at. + * @param width The number of columns to clear. + * @param height The number of rows to clear. + */ + protected _clearCells(x: number, y: number, width: number, height: number): void { + if (this._alpha) { + this._ctx.clearRect( + x * this._scaledCellWidth, + y * this._scaledCellHeight, + width * this._scaledCellWidth, + height * this._scaledCellHeight); + } else { + this._ctx.fillStyle = this._colors.background.css; + this._ctx.fillRect( + x * this._scaledCellWidth, + y * this._scaledCellHeight, + width * this._scaledCellWidth, + height * this._scaledCellHeight); + } + } + + /** + * Draws a truecolor character at the cell. The character will be clipped to + * ensure that it fits with the cell, including the cell to the right if it's + * a wide character. This uses the existing fillStyle on the context. + * @param cell The cell data for the character to draw. + * @param x The column to draw at. + * @param y The row to draw at. + * @param color The color of the character. + */ + protected _fillCharTrueColor(cell: CellData, x: number, y: number): void { + this._ctx.font = this._getFont(false, false); + this._ctx.textBaseline = TEXT_BASELINE; + this._clipRow(y); + + // 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); + } + + // Draw the character + if (!drawSuccess) { + this._ctx.fillText( + cell.getChars(), + x * this._scaledCellWidth + this._scaledCharLeft, + y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight); + } + } + + /** + * 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); + + // skip cache right away if we draw in RGB + // Note: to avoid bad runtime JoinedCellData will be skipped + // in the cache handler itself (atlasDidDraw == false) and + // fall through to uncached later down below + if (contrastColor || cell.isFgRGB() || cell.isBgRGB()) { + this._drawUncachedChars(cell, x, y, contrastColor); + return; + } + + let fg; + let bg; + if (cell.isInverse()) { + fg = (cell.isBgDefault()) ? INVERTED_DEFAULT_COLOR : cell.getBgColor(); + bg = (cell.isFgDefault()) ? INVERTED_DEFAULT_COLOR : cell.getFgColor(); + } else { + bg = (cell.isBgDefault()) ? DEFAULT_COLOR : cell.getBgColor(); + fg = (cell.isFgDefault()) ? DEFAULT_COLOR : cell.getFgColor(); + } + + 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(); + const atlasDidDraw = 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); + } + + // Draw the character + if (!drawSuccess) { + this._ctx.fillText( + cell.getChars(), + x * this._scaledCellWidth + this._scaledCharLeft, + y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight); + } + + 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. + */ + private _clipRow(y: number): void { + this._ctx.beginPath(); + this._ctx.rect( + 0, + y * this._scaledCellHeight, + this._bufferService.cols * this._scaledCellWidth, + this._scaledCellHeight); + this._ctx.clip(); + } + + /** + * Gets the current font. + * @param isBold If we should use the bold fontWeight. + */ + protected _getFont(isBold: boolean, isItalic: boolean): string { + const fontWeight = isBold ? this._optionsService.rawOptions.fontWeightBold : this._optionsService.rawOptions.fontWeight; + const fontStyle = isItalic ? 'italic' : ''; + + return `${fontStyle} ${fontWeight} ${this._optionsService.rawOptions.fontSize * window.devicePixelRatio}px ${this._optionsService.rawOptions.fontFamily}`; + } + + private _getContrastColor(cell: CellData): IColor | undefined { + if (this._optionsService.rawOptions.minimumContrastRatio === 1) { + return undefined; + } + + // Try get from cache first + 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(bgColorMode, bgColor, isInverse); + const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, isInverse, isBold); + const result = rgba.ensureContrastRatio(bgRgba, fgRgba, this._optionsService.rawOptions.minimumContrastRatio); + + if (!result) { + this._colors.contrastCache.setColor(cell.bg, cell.fg, null); + return undefined; + } + + const color: IColor = { + css: channels.toCss( + (result >> 24) & 0xFF, + (result >> 16) & 0xFF, + (result >> 8) & 0xFF + ), + rgba: result + }; + 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; + } + } +} + |