656 lines
23 KiB
TypeScript
656 lines
23 KiB
TypeScript
/**
|
|
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
|
* @license MIT
|
|
*/
|
|
|
|
import { IRenderDimensions } from 'browser/renderer/shared/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/shared/Constants';
|
|
import { acquireCharAtlas } from './atlas/CharAtlasCache';
|
|
import { AttributeData } from 'common/buffer/AttributeData';
|
|
import { IColorSet } from 'browser/Types';
|
|
import { CellData } from 'common/buffer/CellData';
|
|
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
|
|
import { ICoreBrowserService } from 'browser/services/Services';
|
|
import { excludeFromContrastRatioDemands, throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
|
|
import { channels, color, rgba } from 'common/Color';
|
|
import { removeElementFromParent } from 'browser/Dom';
|
|
import { tryDrawCustomChar } from 'browser/renderer/shared/CustomGlyphs';
|
|
import { Terminal } from 'xterm';
|
|
import { CanvasCharAtlas } from 'atlas/CanvasCharAtlas';
|
|
|
|
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 _selectionStart: [number, number] | undefined;
|
|
protected _selectionEnd: [number, number] | undefined;
|
|
protected _columnSelectMode: boolean = false;
|
|
|
|
protected _charAtlas: CanvasCharAtlas | 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
|
|
};
|
|
|
|
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,
|
|
protected readonly _bufferService: IBufferService,
|
|
protected readonly _optionsService: IOptionsService,
|
|
protected readonly _decorationService: IDecorationService,
|
|
protected readonly _coreBrowserService: ICoreBrowserService
|
|
) {
|
|
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 {
|
|
this._selectionStart = start;
|
|
this._selectionEnd = end;
|
|
this._columnSelectMode = columnSelectMode;
|
|
}
|
|
|
|
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._terminal, colorSet, this._scaledCellWidth, this._scaledCellHeight, this._scaledCharWidth, this._scaledCharHeight, this._coreBrowserService.dpr);
|
|
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 - this._coreBrowserService.dpr,
|
|
width * this._scaledCellWidth,
|
|
this._coreBrowserService.dpr);
|
|
}
|
|
|
|
/**
|
|
* 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, pixelOffset: number = 0): void {
|
|
this._ctx.fillRect(
|
|
x * this._scaledCellWidth,
|
|
(y + 1) * this._scaledCellHeight + pixelOffset - this._coreBrowserService.dpr - 1 /* Ensure it's drawn within the cell */,
|
|
width * this._scaledCellWidth,
|
|
this._coreBrowserService.dpr);
|
|
}
|
|
|
|
protected _curlyUnderlineAtCell(x: number, y: number, width: number = 1): void {
|
|
this._ctx.save();
|
|
this._ctx.beginPath();
|
|
this._ctx.strokeStyle = this._ctx.fillStyle;
|
|
const lineWidth = this._coreBrowserService.dpr;
|
|
this._ctx.lineWidth = lineWidth;
|
|
for (let xOffset = 0; xOffset < width; xOffset++) {
|
|
const xLeft = (x + xOffset) * this._scaledCellWidth;
|
|
const xMid = (x + xOffset + 0.5) * this._scaledCellWidth;
|
|
const xRight = (x + xOffset + 1) * this._scaledCellWidth;
|
|
const yMid = (y + 1) * this._scaledCellHeight - lineWidth - 1;
|
|
const yMidBot = yMid - lineWidth;
|
|
const yMidTop = yMid + lineWidth;
|
|
this._ctx.moveTo(xLeft, yMid);
|
|
this._ctx.bezierCurveTo(
|
|
xLeft, yMidBot,
|
|
xMid, yMidBot,
|
|
xMid, yMid
|
|
);
|
|
this._ctx.bezierCurveTo(
|
|
xMid, yMidTop,
|
|
xRight, yMidTop,
|
|
xRight, yMid
|
|
);
|
|
}
|
|
this._ctx.stroke();
|
|
this._ctx.restore();
|
|
}
|
|
|
|
protected _dottedUnderlineAtCell(x: number, y: number, width: number = 1): void {
|
|
this._ctx.save();
|
|
this._ctx.beginPath();
|
|
this._ctx.strokeStyle = this._ctx.fillStyle;
|
|
const lineWidth = this._coreBrowserService.dpr;
|
|
this._ctx.lineWidth = lineWidth;
|
|
this._ctx.setLineDash([lineWidth * 2, lineWidth]);
|
|
const xLeft = x * this._scaledCellWidth;
|
|
const yMid = (y + 1) * this._scaledCellHeight - lineWidth - 1;
|
|
this._ctx.moveTo(xLeft, yMid);
|
|
for (let xOffset = 0; xOffset < width; xOffset++) {
|
|
// const xLeft = x * this._scaledCellWidth;
|
|
const xRight = (x + width + xOffset) * this._scaledCellWidth;
|
|
this._ctx.lineTo(xRight, yMid);
|
|
}
|
|
this._ctx.stroke();
|
|
this._ctx.closePath();
|
|
this._ctx.restore();
|
|
}
|
|
|
|
protected _dashedUnderlineAtCell(x: number, y: number, width: number = 1): void {
|
|
this._ctx.save();
|
|
this._ctx.beginPath();
|
|
this._ctx.strokeStyle = this._ctx.fillStyle;
|
|
const lineWidth = this._coreBrowserService.dpr;
|
|
this._ctx.lineWidth = lineWidth;
|
|
this._ctx.setLineDash([lineWidth * 4, lineWidth * 3]);
|
|
const xLeft = x * this._scaledCellWidth;
|
|
const xRight = (x + width) * this._scaledCellWidth;
|
|
const yMid = (y + 1) * this._scaledCellHeight - lineWidth - 1;
|
|
this._ctx.moveTo(xLeft, yMid);
|
|
this._ctx.lineTo(xRight, yMid);
|
|
this._ctx.stroke();
|
|
this._ctx.closePath();
|
|
this._ctx.restore();
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
this._coreBrowserService.dpr * 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 {
|
|
const lineWidth = this._coreBrowserService.dpr;
|
|
this._ctx.lineWidth = lineWidth;
|
|
this._ctx.strokeRect(
|
|
x * this._scaledCellWidth + lineWidth / 2,
|
|
y * this._scaledCellHeight + (lineWidth / 2),
|
|
width * this._scaledCellWidth - lineWidth,
|
|
(height * this._scaledCellHeight) - lineWidth);
|
|
}
|
|
|
|
/**
|
|
* 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, 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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Draws one or more characters at a cell. If possible this will draw using
|
|
* the character atlas to reduce draw time.
|
|
* @param chars The character or characters.
|
|
* @param code The character code.
|
|
* @param width The width of the characters.
|
|
* @param x The column to draw at.
|
|
* @param y The row to draw at.
|
|
* @param fg The foreground color, in the format stored within the attributes.
|
|
* @param bg The background color, in the format stored within the attributes.
|
|
* This is used to validate whether a cached image can be used.
|
|
* @param bold Whether the text is bold.
|
|
*/
|
|
protected _drawChars(cell: ICellData, x: number, y: number): void {
|
|
const contrastColor = this._getContrastColor(cell, x, y);
|
|
|
|
// skip cache right away if we draw in RGB
|
|
// Note: to avoid bad runtime JoinedCellData will be skipped
|
|
// in the cache handler itself (atlasDidDraw == false) and
|
|
// fall through to uncached later down below
|
|
if (contrastColor || cell.isFgRGB() || cell.isBgRGB()) {
|
|
this._drawUncachedChars(cell, x, y, contrastColor);
|
|
return;
|
|
}
|
|
|
|
let fg;
|
|
let bg;
|
|
if (cell.isInverse()) {
|
|
fg = (cell.isBgDefault()) ? INVERTED_DEFAULT_COLOR : cell.getBgColor();
|
|
bg = (cell.isFgDefault()) ? INVERTED_DEFAULT_COLOR : cell.getFgColor();
|
|
} 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();
|
|
|
|
// 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);
|
|
}
|
|
|
|
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 * 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]);
|
|
}
|
|
}
|
|
|