aboutsummaryrefslogtreecommitdiffstats
path: root/node_modules/xterm/src/browser/renderer/BaseRenderLayer.ts
diff options
context:
space:
mode:
Diffstat (limited to 'node_modules/xterm/src/browser/renderer/BaseRenderLayer.ts')
-rw-r--r--node_modules/xterm/src/browser/renderer/BaseRenderLayer.ts513
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;
+ }
+ }
+}
+