Merge remote-tracking branch 'upstream/master' into rounding

This commit is contained in:
Daniel Imms
2022-07-28 07:00:13 -07:00
81 changed files with 829 additions and 2119 deletions
-584
View File
@@ -1,584 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IRenderDimensions, IRenderLayer } from 'browser/renderer/Types';
import { ICellData, IColor } 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 } from 'browser/Types';
import { CellData } from 'common/buffer/CellData';
import { IBufferService, IDecorationService, IOptionsService } from 'common/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';
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;
private _selectionStart: [number, number] | undefined;
private _selectionEnd: [number, number] | undefined;
private _columnSelectMode: boolean = false;
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
};
public get canvas(): HTMLCanvasElement { return this._canvas; }
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,
protected readonly _decorationService: IDecorationService
) {
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._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, 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;
for (const d of this._decorationService.getDecorationsAtCell(x, y)) {
if (d.backgroundColorRGB || d.foregroundColorRGB) {
hasOverrides = true;
break;
}
}
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);
}
// 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, 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;
for (const d of this._decorationService.getDecorationsAtCell(x, y)) {
if (d.options.layer !== 'top' && isTop) {
continue;
}
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]);
}
}
@@ -6,10 +6,9 @@
import { isFirefox, isLegacyEdge } from 'common/Platform';
export const INVERTED_DEFAULT_COLOR = 257;
export const DIM_OPACITY = 0.5;
// The text baseline is set conditionally by browser. Using 'ideographic' for Firefox or Legacy Edge would
// result in truncated text (Issue 3353). Using 'bottom' for Chrome would result in slightly
// unaligned Powerline fonts (PR 3356#issuecomment-850928179).
export const TEXT_BASELINE: CanvasTextBaseline = isFirefox || isLegacyEdge ? 'bottom' : 'ideographic';
export const CHAR_ATLAS_CELL_SPACING = 1;
-378
View File
@@ -1,378 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types';
import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer';
import { ICellData } from 'common/Types';
import { CellData } from 'common/buffer/CellData';
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';
interface ICursorState {
x: number;
y: number;
isFocused: boolean;
style: string;
width: number;
}
/**
* The time between cursor blinks.
*/
const BLINK_INTERVAL = 600;
export class CursorRenderLayer extends BaseRenderLayer {
private _state: ICursorState;
private _cursorRenderers: {[key: string]: (x: number, y: number, cell: ICellData) => void};
private _cursorBlinkStateManager: CursorBlinkStateManager | undefined;
private _cell: ICellData = new CellData();
constructor(
container: HTMLElement,
zIndex: number,
colors: IColorSet,
rendererId: number,
private _onRequestRedraw: IEventEmitter<IRequestRedrawEvent>,
@IBufferService bufferService: IBufferService,
@IOptionsService optionsService: IOptionsService,
@ICoreService private readonly _coreService: ICoreService,
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService,
@IDecorationService decorationService: IDecorationService
) {
super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService);
this._state = {
x: 0,
y: 0,
isFocused: false,
style: '',
width: 0
};
this._cursorRenderers = {
'bar': this._renderBarCursor.bind(this),
'block': this._renderBlockCursor.bind(this),
'underline': this._renderUnderlineCursor.bind(this)
};
}
public dispose(): void {
if (this._cursorBlinkStateManager) {
this._cursorBlinkStateManager.dispose();
this._cursorBlinkStateManager = undefined;
}
super.dispose();
}
public resize(dim: IRenderDimensions): void {
super.resize(dim);
// Resizing the canvas discards the contents of the canvas so clear state
this._state = {
x: 0,
y: 0,
isFocused: false,
style: '',
width: 0
};
}
public reset(): void {
this._clearCursor();
this._cursorBlinkStateManager?.restartBlinkAnimation();
this.onOptionsChanged();
}
public onBlur(): void {
this._cursorBlinkStateManager?.pause();
this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
}
public onFocus(): void {
this._cursorBlinkStateManager?.resume();
this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
}
public onOptionsChanged(): void {
if (this._optionsService.rawOptions.cursorBlink) {
if (!this._cursorBlinkStateManager) {
this._cursorBlinkStateManager = new CursorBlinkStateManager(this._coreBrowserService.isFocused, () => {
this._render(true);
});
}
} else {
this._cursorBlinkStateManager?.dispose();
this._cursorBlinkStateManager = undefined;
}
// Request a refresh from the terminal as management of rendering is being
// moved back to the terminal
this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
}
public onCursorMove(): void {
this._cursorBlinkStateManager?.restartBlinkAnimation();
}
public onGridChanged(startRow: number, endRow: number): void {
if (!this._cursorBlinkStateManager || this._cursorBlinkStateManager.isPaused) {
this._render(false);
} else {
this._cursorBlinkStateManager.restartBlinkAnimation();
}
}
private _render(triggeredByAnimationFrame: boolean): void {
// Don't draw the cursor if it's hidden
if (!this._coreService.isCursorInitialized || this._coreService.isCursorHidden) {
this._clearCursor();
return;
}
const cursorY = this._bufferService.buffer.ybase + this._bufferService.buffer.y;
const viewportRelativeCursorY = cursorY - this._bufferService.buffer.ydisp;
// Don't draw the cursor if it's off-screen
if (viewportRelativeCursorY < 0 || viewportRelativeCursorY >= this._bufferService.rows) {
this._clearCursor();
return;
}
// in case cursor.x == cols adjust visual cursor to cols - 1
const cursorX = Math.min(this._bufferService.buffer.x, this._bufferService.cols - 1);
this._bufferService.buffer.lines.get(cursorY)!.loadCell(cursorX, this._cell);
if (this._cell.content === undefined) {
return;
}
if (!this._coreBrowserService.isFocused) {
this._clearCursor();
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor.css;
const cursorStyle = this._optionsService.rawOptions.cursorStyle;
if (cursorStyle && cursorStyle !== 'block') {
this._cursorRenderers[cursorStyle](cursorX, viewportRelativeCursorY, this._cell);
} else {
this._renderBlurCursor(cursorX, viewportRelativeCursorY, this._cell);
}
this._ctx.restore();
this._state.x = cursorX;
this._state.y = viewportRelativeCursorY;
this._state.isFocused = false;
this._state.style = cursorStyle;
this._state.width = this._cell.getWidth();
return;
}
// Don't draw the cursor if it's blinking
if (this._cursorBlinkStateManager && !this._cursorBlinkStateManager.isCursorVisible) {
this._clearCursor();
return;
}
if (this._state) {
// The cursor is already in the correct spot, don't redraw
if (this._state.x === cursorX &&
this._state.y === viewportRelativeCursorY &&
this._state.isFocused === this._coreBrowserService.isFocused &&
this._state.style === this._optionsService.rawOptions.cursorStyle &&
this._state.width === this._cell.getWidth()) {
return;
}
this._clearCursor();
}
this._ctx.save();
this._cursorRenderers[this._optionsService.rawOptions.cursorStyle || 'block'](cursorX, viewportRelativeCursorY, this._cell);
this._ctx.restore();
this._state.x = cursorX;
this._state.y = viewportRelativeCursorY;
this._state.isFocused = false;
this._state.style = this._optionsService.rawOptions.cursorStyle;
this._state.width = this._cell.getWidth();
}
private _clearCursor(): void {
if (this._state) {
// Avoid potential rounding errors when device pixel ratio is less than 1
if (window.devicePixelRatio < 1) {
this._clearAll();
} else {
this._clearCells(this._state.x, this._state.y, this._state.width, 1);
}
this._state = {
x: 0,
y: 0,
isFocused: false,
style: '',
width: 0
};
}
}
private _renderBarCursor(x: number, y: number, cell: ICellData): void {
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor.css;
this._fillLeftLineAtCell(x, y, this._optionsService.rawOptions.cursorWidth);
this._ctx.restore();
}
private _renderBlockCursor(x: number, y: number, cell: ICellData): void {
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor.css;
this._fillCells(x, y, cell.getWidth(), 1);
this._ctx.fillStyle = this._colors.cursorAccent.css;
this._fillCharTrueColor(cell, x, y);
this._ctx.restore();
}
private _renderUnderlineCursor(x: number, y: number, cell: ICellData): void {
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor.css;
this._fillBottomLineAtCells(x, y);
this._ctx.restore();
}
private _renderBlurCursor(x: number, y: number, cell: ICellData): void {
this._ctx.save();
this._ctx.strokeStyle = this._colors.cursor.css;
this._strokeRectAtCell(x, y, cell.getWidth(), 1);
this._ctx.restore();
}
}
class CursorBlinkStateManager {
public isCursorVisible: boolean;
private _animationFrame: number | undefined;
private _blinkStartTimeout: number | undefined;
private _blinkInterval: number | undefined;
/**
* The time at which the animation frame was restarted, this is used on the
* next render to restart the timers so they don't need to restart the timers
* multiple times over a short period.
*/
private _animationTimeRestarted: number | undefined;
constructor(
isFocused: boolean,
private _renderCallback: () => void
) {
this.isCursorVisible = true;
if (isFocused) {
this._restartInterval();
}
}
public get isPaused(): boolean { return !(this._blinkStartTimeout || this._blinkInterval); }
public dispose(): void {
if (this._blinkInterval) {
window.clearInterval(this._blinkInterval);
this._blinkInterval = undefined;
}
if (this._blinkStartTimeout) {
window.clearTimeout(this._blinkStartTimeout);
this._blinkStartTimeout = undefined;
}
if (this._animationFrame) {
window.cancelAnimationFrame(this._animationFrame);
this._animationFrame = undefined;
}
}
public restartBlinkAnimation(): void {
if (this.isPaused) {
return;
}
// Save a timestamp so that the restart can be done on the next interval
this._animationTimeRestarted = Date.now();
// Force a cursor render to ensure it's visible and in the correct position
this.isCursorVisible = true;
if (!this._animationFrame) {
this._animationFrame = window.requestAnimationFrame(() => {
this._renderCallback();
this._animationFrame = undefined;
});
}
}
private _restartInterval(timeToStart: number = BLINK_INTERVAL): void {
// Clear any existing interval
if (this._blinkInterval) {
window.clearInterval(this._blinkInterval);
this._blinkInterval = undefined;
}
// Setup the initial timeout which will hide the cursor, this is done before
// the regular interval is setup in order to support restarting the blink
// animation in a lightweight way (without thrashing clearInterval and
// setInterval).
this._blinkStartTimeout = window.setTimeout(() => {
// Check if another animation restart was requested while this was being
// started
if (this._animationTimeRestarted) {
const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted);
this._animationTimeRestarted = undefined;
if (time > 0) {
this._restartInterval(time);
return;
}
}
// Hide the cursor
this.isCursorVisible = false;
this._animationFrame = window.requestAnimationFrame(() => {
this._renderCallback();
this._animationFrame = undefined;
});
// Setup the blink interval
this._blinkInterval = window.setInterval(() => {
// Adjust the animation time if it was restarted
if (this._animationTimeRestarted) {
// calc time diff
// Make restart interval do a setTimeout initially?
const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted);
this._animationTimeRestarted = undefined;
this._restartInterval(time);
return;
}
// Invert visibility and render
this.isCursorVisible = !this.isCursorVisible;
this._animationFrame = window.requestAnimationFrame(() => {
this._renderCallback();
this._animationFrame = undefined;
});
}, BLINK_INTERVAL);
}, timeToStart);
}
public pause(): void {
this.isCursorVisible = true;
if (this._blinkInterval) {
window.clearInterval(this._blinkInterval);
this._blinkInterval = undefined;
}
if (this._blinkStartTimeout) {
window.clearTimeout(this._blinkStartTimeout);
this._blinkStartTimeout = undefined;
}
if (this._animationFrame) {
window.cancelAnimationFrame(this._animationFrame);
this._animationFrame = undefined;
}
}
public resume(): void {
// Clear out any existing timers just in case
this.pause();
this._animationTimeRestarted = undefined;
this._restartInterval();
this.restartBlinkAnimation();
}
}
-62
View File
@@ -1,62 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { GridCache } from 'browser/renderer/GridCache';
describe('GridCache', () => {
let grid: GridCache<number>;
beforeEach(() => {
grid = new GridCache<number>();
});
describe('constructor', () => {
it('should create an empty cache', () => {
assert.equal(grid.cache.length, 0);
});
});
describe('resize', () => {
it('should fill all new elements with null', () => {
grid.resize(2, 2);
assert.equal(grid.cache.length, 2);
assert.equal(grid.cache[0].length, 2);
assert.equal(grid.cache[0][0], null);
assert.equal(grid.cache[0][1], null);
assert.equal(grid.cache[1].length, 2);
assert.equal(grid.cache[1][0], null);
assert.equal(grid.cache[1][1], null);
grid.resize(3, 2);
assert.equal(grid.cache.length, 3);
assert.equal(grid.cache[2][0], null);
assert.equal(grid.cache[2][1], null);
});
it('should remove rows/cols from the cache when reduced', () => {
grid.resize(2, 2);
grid.resize(1, 1);
assert.equal(grid.cache.length, 1);
assert.equal(grid.cache[0].length, 1);
});
it('should not touch existing cache entries if they fit in the new cache', () => {
grid.resize(1, 1);
assert.equal(grid.cache[0][0], null);
grid.cache[0][0] = 1;
grid.resize(2, 1);
assert.equal(grid.cache[0][0], 1);
});
});
describe('clear', () => {
it('should make all entries null', () => {
grid.resize(1, 1);
grid.cache[0][0] = 1;
grid.clear();
assert.equal(grid.cache[0][0], null);
});
});
});
-33
View File
@@ -1,33 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
export class GridCache<T> {
public cache: (T | undefined)[][];
public constructor() {
this.cache = [];
}
public resize(width: number, height: number): void {
for (let x = 0; x < width; x++) {
if (this.cache.length <= x) {
this.cache.push([]);
}
for (let y = this.cache[x].length; y < height; y++) {
this.cache[x].push(undefined);
}
this.cache[x].length = height;
}
this.cache.length = width;
}
public clear(): void {
for (let x = 0; x < this.cache.length; x++) {
for (let y = 0; y < this.cache[x].length; y++) {
this.cache[x][y] = undefined;
}
}
}
}
-84
View File
@@ -1,84 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IRenderDimensions } from 'browser/renderer/Types';
import { BaseRenderLayer } from './BaseRenderLayer';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
import { is256Color } from 'browser/renderer/atlas/CharAtlasUtils';
import { IColorSet, ILinkifierEvent, ILinkifier, ILinkifier2 } from 'browser/Types';
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
export class LinkRenderLayer extends BaseRenderLayer {
private _state: ILinkifierEvent | undefined;
constructor(
container: HTMLElement,
zIndex: number,
colors: IColorSet,
rendererId: number,
linkifier: ILinkifier,
linkifier2: ILinkifier2,
@IBufferService bufferService: IBufferService,
@IOptionsService optionsService: IOptionsService,
@IDecorationService decorationService: IDecorationService
) {
super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService);
linkifier.onShowLinkUnderline(e => this._onShowLinkUnderline(e));
linkifier.onHideLinkUnderline(e => this._onHideLinkUnderline(e));
linkifier2.onShowLinkUnderline(e => this._onShowLinkUnderline(e));
linkifier2.onHideLinkUnderline(e => this._onHideLinkUnderline(e));
}
public resize(dim: IRenderDimensions): void {
super.resize(dim);
// Resizing the canvas discards the contents of the canvas so clear state
this._state = undefined;
}
public reset(): void {
this._clearCurrentLink();
}
private _clearCurrentLink(): void {
if (this._state) {
this._clearCells(this._state.x1, this._state.y1, this._state.cols - this._state.x1, 1);
const middleRowCount = this._state.y2 - this._state.y1 - 1;
if (middleRowCount > 0) {
this._clearCells(0, this._state.y1 + 1, this._state.cols, middleRowCount);
}
this._clearCells(0, this._state.y2, this._state.x2, 1);
this._state = undefined;
}
}
private _onShowLinkUnderline(e: ILinkifierEvent): void {
if (e.fg === INVERTED_DEFAULT_COLOR) {
this._ctx.fillStyle = this._colors.background.css;
} else if (e.fg && is256Color(e.fg)) {
// 256 color support
this._ctx.fillStyle = this._colors.ansi[e.fg].css;
} else {
this._ctx.fillStyle = this._colors.foreground.css;
}
if (e.y1 === e.y2) {
// Single line link
this._fillBottomLineAtCells(e.x1, e.y1, e.x2 - e.x1);
} else {
// Multi-line link
this._fillBottomLineAtCells(e.x1, e.y1, e.cols - e.x1);
for (let y = e.y1 + 1; y < e.y2; y++) {
this._fillBottomLineAtCells(0, y, e.cols);
}
this._fillBottomLineAtCells(0, e.y2, e.x2);
}
this._state = e;
}
private _onHideLinkUnderline(e: ILinkifierEvent): void {
this._clearCurrentLink();
}
}
-202
View File
@@ -1,202 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { TextRenderLayer } from 'browser/renderer/TextRenderLayer';
import { SelectionRenderLayer } from 'browser/renderer/SelectionRenderLayer';
import { CursorRenderLayer } from 'browser/renderer/CursorRenderLayer';
import { IRenderLayer, IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types';
import { LinkRenderLayer } from 'browser/renderer/LinkRenderLayer';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { IColorSet, ILinkifier, ILinkifier2 } from 'browser/Types';
import { ICharSizeService } from 'browser/services/Services';
import { IBufferService, IOptionsService, IInstantiationService } from 'common/services/Services';
import { removeTerminalFromCache } from 'browser/renderer/atlas/CharAtlasCache';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { observeDevicePixelDimensions } from 'browser/renderer/DevicePixelObserver';
let nextRendererId = 1;
export class Renderer extends Disposable implements IRenderer {
private _id = nextRendererId++;
private _renderLayers: IRenderLayer[];
private _devicePixelRatio: number;
public dimensions: IRenderDimensions;
private _onRequestRedraw = new EventEmitter<IRequestRedrawEvent>();
public get onRequestRedraw(): IEvent<IRequestRedrawEvent> { return this._onRequestRedraw.event; }
constructor(
private _colors: IColorSet,
private readonly _screenElement: HTMLElement,
linkifier: ILinkifier,
linkifier2: ILinkifier2,
@IInstantiationService instantiationService: IInstantiationService,
@IBufferService private readonly _bufferService: IBufferService,
@ICharSizeService private readonly _charSizeService: ICharSizeService,
@IOptionsService private readonly _optionsService: IOptionsService
) {
super();
const allowTransparency = this._optionsService.rawOptions.allowTransparency;
this._renderLayers = [
instantiationService.createInstance(TextRenderLayer, this._screenElement, 0, this._colors, allowTransparency, this._id),
instantiationService.createInstance(SelectionRenderLayer, this._screenElement, 1, this._colors, this._id),
instantiationService.createInstance(LinkRenderLayer, this._screenElement, 2, this._colors, this._id, linkifier, linkifier2),
instantiationService.createInstance(CursorRenderLayer, this._screenElement, 3, this._colors, this._id, this._onRequestRedraw)
];
this.dimensions = {
scaledCharWidth: 0,
scaledCharHeight: 0,
scaledCellWidth: 0,
scaledCellHeight: 0,
scaledCharLeft: 0,
scaledCharTop: 0,
scaledCanvasWidth: 0,
scaledCanvasHeight: 0,
canvasWidth: 0,
canvasHeight: 0,
actualCellWidth: 0,
actualCellHeight: 0
};
this._devicePixelRatio = window.devicePixelRatio;
this._updateDimensions();
this.register(observeDevicePixelDimensions(this._renderLayers[0].canvas, (w, h) => this._setCanvasDevicePixelDimensions(w, h)));
this.onOptionsChanged();
}
public dispose(): void {
for (const l of this._renderLayers) {
l.dispose();
}
super.dispose();
removeTerminalFromCache(this._id);
}
public onDevicePixelRatioChange(): void {
// If the device pixel ratio changed, the char atlas needs to be regenerated
// and the terminal needs to refreshed
if (this._devicePixelRatio !== window.devicePixelRatio) {
this._devicePixelRatio = window.devicePixelRatio;
this.onResize(this._bufferService.cols, this._bufferService.rows);
}
}
public setColors(colors: IColorSet): void {
this._colors = colors;
// Clear layers and force a full render
for (const l of this._renderLayers) {
l.setColors(this._colors);
l.reset();
}
}
public onResize(cols: number, rows: number): void {
// Update character and canvas dimensions
this._updateDimensions();
// Resize all render layers
for (const l of this._renderLayers) {
l.resize(this.dimensions);
}
// Resize the screen
this._screenElement.style.width = `${this.dimensions.canvasWidth}px`;
this._screenElement.style.height = `${this.dimensions.canvasHeight}px`;
}
public onCharSizeChanged(): void {
this.onResize(this._bufferService.cols, this._bufferService.rows);
}
public onBlur(): void {
this._runOperation(l => l.onBlur());
}
public onFocus(): void {
this._runOperation(l => l.onFocus());
}
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
this._runOperation(l => l.onSelectionChanged(start, end, columnSelectMode));
// Selection foreground requires a full re-render
if (this._colors.selectionForeground) {
this._onRequestRedraw.fire({ start: 0, end: this._bufferService.rows - 1 });
}
}
public onCursorMove(): void {
this._runOperation(l => l.onCursorMove());
}
public onOptionsChanged(): void {
this._runOperation(l => l.onOptionsChanged());
}
public clear(): void {
this._runOperation(l => l.reset());
}
private _runOperation(operation: (layer: IRenderLayer) => void): void {
for (const l of this._renderLayers) {
operation(l);
}
}
/**
* Performs the refresh loop callback, calling refresh only if a refresh is
* necessary before queueing up the next one.
*/
public renderRows(start: number, end: number): void {
for (const l of this._renderLayers) {
l.onGridChanged(start, end);
}
}
public clearTextureAtlas(): void {
for (const layer of this._renderLayers) {
layer.clearTextureAtlas();
}
}
/**
* Recalculates the character and canvas dimensions.
*/
private _updateDimensions(): void {
if (!this._charSizeService.hasValidSize) {
return;
}
// See the WebGL renderer for an explanation of this section.
this.dimensions.scaledCharWidth = Math.floor(this._charSizeService.width * window.devicePixelRatio);
this.dimensions.scaledCharHeight = Math.ceil(this._charSizeService.height * window.devicePixelRatio);
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._optionsService.rawOptions.lineHeight);
this.dimensions.scaledCharTop = this._optionsService.rawOptions.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2);
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._optionsService.rawOptions.letterSpacing);
this.dimensions.scaledCharLeft = Math.floor(this._optionsService.rawOptions.letterSpacing / 2);
this.dimensions.scaledCanvasHeight = this._bufferService.rows * this.dimensions.scaledCellHeight;
this.dimensions.scaledCanvasWidth = this._bufferService.cols * this.dimensions.scaledCellWidth;
this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / window.devicePixelRatio);
this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / window.devicePixelRatio);
this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._bufferService.rows;
this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._bufferService.cols;
}
private _setCanvasDevicePixelDimensions(width: number, height: number): void {
this.dimensions.scaledCanvasHeight = height;
this.dimensions.scaledCanvasWidth = width;
// Resize all render layers
for (const l of this._renderLayers) {
l.resize(this.dimensions);
}
this._requestRedrawViewport();
}
private _requestRedrawViewport(): void {
this._onRequestRedraw.fire({ start: 0, end: this._bufferService.rows - 1 });
}
}
@@ -1,131 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IRenderDimensions } from 'browser/renderer/Types';
import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer';
import { IColorSet } from 'browser/Types';
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
interface ISelectionState {
start?: [number, number];
end?: [number, number];
columnSelectMode?: boolean;
ydisp?: number;
}
export class SelectionRenderLayer extends BaseRenderLayer {
private _state!: ISelectionState;
constructor(
container: HTMLElement,
zIndex: number,
colors: IColorSet,
rendererId: number,
@IBufferService bufferService: IBufferService,
@IOptionsService optionsService: IOptionsService,
@IDecorationService decorationService: IDecorationService
) {
super(container, 'selection', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService);
this._clearState();
}
private _clearState(): void {
this._state = {
start: undefined,
end: undefined,
columnSelectMode: undefined,
ydisp: undefined
};
}
public resize(dim: IRenderDimensions): void {
super.resize(dim);
// Resizing the canvas discards the contents of the canvas so clear state
this._clearState();
}
public reset(): void {
if (this._state.start && this._state.end) {
this._clearState();
this._clearAll();
}
}
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
super.onSelectionChanged(start, end, columnSelectMode);
// Selection has not changed
if (!this._didStateChange(start, end, columnSelectMode, this._bufferService.buffer.ydisp)) {
return;
}
// Remove all selections
this._clearAll();
// Selection does not exist
if (!start || !end) {
this._clearState();
return;
}
// Translate from buffer position to viewport position
const viewportStartRow = start[1] - this._bufferService.buffer.ydisp;
const viewportEndRow = end[1] - this._bufferService.buffer.ydisp;
const viewportCappedStartRow = Math.max(viewportStartRow, 0);
const viewportCappedEndRow = Math.min(viewportEndRow, this._bufferService.rows - 1);
// No need to draw the selection
if (viewportCappedStartRow >= this._bufferService.rows || viewportCappedEndRow < 0) {
this._state.ydisp = this._bufferService.buffer.ydisp;
return;
}
this._ctx.fillStyle = this._colors.selectionTransparent.css;
if (columnSelectMode) {
const startCol = start[0];
const width = end[0] - startCol;
const height = viewportCappedEndRow - viewportCappedStartRow + 1;
this._fillCells(startCol, viewportCappedStartRow, width, height);
} else {
// Draw first row
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
const startRowEndCol = viewportCappedStartRow === viewportEndRow ? end[0] : this._bufferService.cols;
this._fillCells(startCol, viewportCappedStartRow, startRowEndCol - startCol, 1);
// Draw middle rows
const middleRowsCount = Math.max(viewportCappedEndRow - viewportCappedStartRow - 1, 0);
this._fillCells(0, viewportCappedStartRow + 1, this._bufferService.cols, middleRowsCount);
// Draw final row
if (viewportCappedStartRow !== viewportCappedEndRow) {
// Only draw viewportEndRow if it's not the same as viewportStartRow
const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._bufferService.cols;
this._fillCells(0, viewportCappedEndRow, endCol, 1);
}
}
// Save state for next render
this._state.start = [start[0], start[1]];
this._state.end = [end[0], end[1]];
this._state.columnSelectMode = columnSelectMode;
this._state.ydisp = this._bufferService.buffer.ydisp;
}
private _didStateChange(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean, ydisp: number): boolean {
return !this._areCoordinatesEqual(start, this._state.start) ||
!this._areCoordinatesEqual(end, this._state.end) ||
columnSelectMode !== this._state.columnSelectMode ||
ydisp !== this._state.ydisp;
}
private _areCoordinatesEqual(coord1: [number, number] | undefined, coord2: [number, number] | undefined): boolean {
if (!coord1 || !coord2) {
return false;
}
return coord1[0] === coord2[0] && coord1[1] === coord2[1];
}
}
-344
View File
@@ -1,344 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IRenderDimensions } from 'browser/renderer/Types';
import { CharData, ICellData } from 'common/Types';
import { GridCache } from 'browser/renderer/GridCache';
import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer';
import { AttributeData } from 'common/buffer/AttributeData';
import { NULL_CELL_CODE, Content } from 'common/buffer/Constants';
import { IColorSet } from 'browser/Types';
import { CellData } from 'common/buffer/CellData';
import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services';
import { ICharacterJoinerService } from 'browser/services/Services';
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
/**
* This CharData looks like a null character, which will forc a clear and render
* when the character changes (a regular space ' ' character may not as it's
* drawn state is a cleared cell).
*/
// const OVERLAP_OWNED_CHAR_DATA: CharData = [null, '', 0, -1];
export class TextRenderLayer extends BaseRenderLayer {
private _state: GridCache<CharData>;
private _characterWidth: number = 0;
private _characterFont: string = '';
private _characterOverlapCache: { [key: string]: boolean } = {};
private _workCell = new CellData();
constructor(
container: HTMLElement,
zIndex: number,
colors: IColorSet,
alpha: boolean,
rendererId: number,
@IBufferService bufferService: IBufferService,
@IOptionsService optionsService: IOptionsService,
@ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService,
@IDecorationService decorationService: IDecorationService
) {
super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService, decorationService);
this._state = new GridCache<CharData>();
}
public resize(dim: IRenderDimensions): void {
super.resize(dim);
// Clear the character width cache if the font or width has changed
const terminalFont = this._getFont(false, false);
if (this._characterWidth !== dim.scaledCharWidth || this._characterFont !== terminalFont) {
this._characterWidth = dim.scaledCharWidth;
this._characterFont = terminalFont;
this._characterOverlapCache = {};
}
// Resizing the canvas discards the contents of the canvas so clear state
this._state.clear();
this._state.resize(this._bufferService.cols, this._bufferService.rows);
}
public reset(): void {
this._state.clear();
this._clearAll();
}
private _forEachCell(
firstRow: number,
lastRow: number,
callback: (
cell: ICellData,
x: number,
y: number
) => void
): void {
for (let y = firstRow; y <= lastRow; y++) {
const row = y + this._bufferService.buffer.ydisp;
const line = this._bufferService.buffer.lines.get(row);
const joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
for (let x = 0; x < this._bufferService.cols; x++) {
line!.loadCell(x, this._workCell);
let cell = this._workCell;
// If true, indicates that the current character(s) to draw were joined.
let isJoined = false;
let lastCharX = x;
// The character to the left is a wide character, drawing is owned by
// the char at x-1
if (cell.getWidth() === 0) {
continue;
}
// Process any joined character ranges as needed. Because of how the
// ranges are produced, we know that they are valid for the characters
// and attributes of our input.
if (joinedRanges.length > 0 && x === joinedRanges[0][0]) {
isJoined = true;
const range = joinedRanges.shift()!;
// We already know the exact start and end column of the joined range,
// so we get the string and width representing it directly
cell = new JoinedCellData(
this._workCell,
line!.translateToString(true, range[0], range[1]),
range[1] - range[0]
);
// Skip over the cells occupied by this range in the loop
lastCharX = range[1] - 1;
}
// If the character is an overlapping char and the character to the
// right is a space, take ownership of the cell to the right. We skip
// this check for joined characters because their rendering likely won't
// yield the same result as rendering the last character individually.
if (!isJoined && this._isOverlapping(cell)) {
// If the character is overlapping, we want to force a re-render on every
// frame. This is specifically to work around the case where two
// overlaping chars `a` and `b` are adjacent, the cursor is moved to b and a
// space is added. Without this, the first half of `b` would never
// get removed, and `a` would not re-render because it thinks it's
// already in the correct state.
// this._state.cache[x][y] = OVERLAP_OWNED_CHAR_DATA;
if (lastCharX < line!.length - 1 && line!.getCodePoint(lastCharX + 1) === NULL_CELL_CODE) {
// patch width to 2
cell.content &= ~Content.WIDTH_MASK;
cell.content |= 2 << Content.WIDTH_SHIFT;
// this._clearChar(x + 1, y);
// The overlapping char's char data will force a clear and render when the
// overlapping char is no longer to the left of the character and also when
// the space changes to another character.
// this._state.cache[x + 1][y] = OVERLAP_OWNED_CHAR_DATA;
}
}
callback(
cell,
x,
y
);
x = lastCharX;
}
}
}
/**
* Draws the background for a specified range of columns. Tries to batch adjacent cells of the
* same color together to reduce draw calls.
*/
private _drawBackground(firstRow: number, lastRow: number): void {
const ctx = this._ctx;
const cols = this._bufferService.cols;
let startX: number = 0;
let startY: number = 0;
let prevFillStyle: string | null = null;
ctx.save();
this._forEachCell(firstRow, lastRow, (cell, x, y) => {
// libvte and xterm both draw the background (but not foreground) of invisible characters,
// so we should too.
let nextFillStyle = null; // null represents default background color
if (cell.isInverse()) {
if (cell.isFgDefault()) {
nextFillStyle = this._colors.foreground.css;
} else if (cell.isFgRGB()) {
nextFillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
} else {
nextFillStyle = this._colors.ansi[cell.getFgColor()].css;
}
} else if (cell.isBgRGB()) {
nextFillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
} else if (cell.isBgPalette()) {
nextFillStyle = this._colors.ansi[cell.getBgColor()].css;
}
// Get any decoration foreground/background overrides, this must be fetched before the early
// exist but applied after inverse
let isTop = false;
for (const d of this._decorationService.getDecorationsAtCell(x, this._bufferService.buffer.ydisp + y)) {
if (d.options.layer !== 'top' && isTop) {
continue;
}
if (d.backgroundColorRGB) {
nextFillStyle = d.backgroundColorRGB.css;
}
isTop = d.options.layer === 'top';
}
if (prevFillStyle === null) {
// This is either the first iteration, or the default background was set. Either way, we
// don't need to draw anything.
startX = x;
startY = y;
}
if (y !== startY) {
// our row changed, draw the previous row
ctx.fillStyle = prevFillStyle || '';
this._fillCells(startX, startY, cols - startX, 1);
startX = x;
startY = y;
} else if (prevFillStyle !== nextFillStyle) {
// our color changed, draw the previous characters in this row
ctx.fillStyle = prevFillStyle || '';
this._fillCells(startX, startY, x - startX, 1);
startX = x;
startY = y;
}
prevFillStyle = nextFillStyle;
});
// flush the last color we encountered
if (prevFillStyle !== null) {
ctx.fillStyle = prevFillStyle;
this._fillCells(startX, startY, cols - startX, 1);
}
ctx.restore();
}
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()) {
this._fillBottomLineAtCells(x, y, cell.getWidth());
}
this._ctx.restore();
}
});
}
public onGridChanged(firstRow: number, lastRow: number): void {
// Resize has not been called yet
if (this._state.cache.length === 0) {
return;
}
if (this._charAtlas) {
this._charAtlas.beginFrame();
}
this._clearCells(0, firstRow, this._bufferService.cols, lastRow - firstRow + 1);
this._drawBackground(firstRow, lastRow);
this._drawForeground(firstRow, lastRow);
}
public onOptionsChanged(): void {
this._setTransparency(this._optionsService.rawOptions.allowTransparency);
}
/**
* Whether a character is overlapping to the next cell.
*/
private _isOverlapping(cell: ICellData): boolean {
// Only single cell characters can be overlapping, rendering issues can
// occur without this check
if (cell.getWidth() !== 1) {
return false;
}
// We assume that any ascii character will not overlap
if (cell.getCode() < 256) {
return false;
}
const chars = cell.getChars();
// Deliver from cache if available
if (this._characterOverlapCache.hasOwnProperty(chars)) {
return this._characterOverlapCache[chars];
}
// Setup the font
this._ctx.save();
this._ctx.font = this._characterFont;
// Measure the width of the character, but Math.floor it
// because that is what the renderer does when it calculates
// the character dimensions we are comparing against
const overlaps = Math.floor(this._ctx.measureText(chars).width) > this._characterWidth;
// Restore the original context
this._ctx.restore();
// Cache and return
this._characterOverlapCache[chars] = overlaps;
return overlaps;
}
/**
* Clear the charcater at the cell specified.
* @param x The column of the char.
* @param y The row of the char.
*/
// private _clearChar(x: number, y: number): void {
// let colsToClear = 1;
// // Clear the adjacent character if it was wide
// const state = this._state.cache[x][y];
// if (state && state[CHAR_DATA_WIDTH_INDEX] === 2) {
// colsToClear = 2;
// }
// this.clearCells(x, y, colsToClear, 1);
// }
}
-55
View File
@@ -54,58 +54,3 @@ export interface IRenderer extends IDisposable {
renderRows(start: number, end: number): void;
clearTextureAtlas?(): void;
}
export interface IRenderLayer extends IDisposable {
readonly canvas: HTMLCanvasElement;
/**
* Called when the terminal loses focus.
*/
onBlur(): void;
/**
* * Called when the terminal gets focus.
*/
onFocus(): void;
/**
* Called when the cursor is moved.
*/
onCursorMove(): void;
/**
* Called when options change.
*/
onOptionsChanged(): void;
/**
* Called when the theme changes.
*/
setColors(colorSet: IColorSet): void;
/**
* Called when the data in the grid has changed (or needs to be rendered
* again).
*/
onGridChanged(startRow: number, endRow: number): void;
/**
* Calls when the selection changes.
*/
onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
/**
* Resize the render layer.
*/
resize(dim: IRenderDimensions): void;
/**
* Clear the state of the render layer.
*/
reset(): void;
/**
* Clears the texture atlas.
*/
clearTextureAtlas(): void;
}
@@ -1,58 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IGlyphIdentifier } from 'browser/renderer/atlas/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;
}
@@ -1,95 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { generateConfig, configEquals } from 'browser/renderer/atlas/CharAtlasUtils';
import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas';
import { DynamicCharAtlas } from 'browser/renderer/atlas/DynamicCharAtlas';
import { ICharAtlasConfig } from 'browser/renderer/atlas/Types';
import { IColorSet } from 'browser/Types';
import { ITerminalOptions } from 'common/services/Services';
interface ICharAtlasCacheEntry {
atlas: BaseCharAtlas;
config: ICharAtlasConfig;
// N.B. This implementation potentially holds onto copies of the terminal forever, so
// this may cause memory leaks.
ownedBy: number[];
}
const charAtlasCache: ICharAtlasCacheEntry[] = [];
/**
* Acquires a char atlas, either generating a new one or returning an existing
* one that is in use by another terminal.
*/
export function acquireCharAtlas(
options: ITerminalOptions,
rendererId: number,
colors: IColorSet,
scaledCharWidth: number,
scaledCharHeight: number
): BaseCharAtlas {
const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, options, colors);
// Check to see if the renderer already owns this config
for (let i = 0; i < charAtlasCache.length; i++) {
const entry = charAtlasCache[i];
const ownedByIndex = entry.ownedBy.indexOf(rendererId);
if (ownedByIndex >= 0) {
if (configEquals(entry.config, newConfig)) {
return entry.atlas;
}
// The configs differ, release the renderer from the entry
if (entry.ownedBy.length === 1) {
entry.atlas.dispose();
charAtlasCache.splice(i, 1);
} else {
entry.ownedBy.splice(ownedByIndex, 1);
}
break;
}
}
// Try match a char atlas from the cache
for (let i = 0; i < charAtlasCache.length; i++) {
const entry = charAtlasCache[i];
if (configEquals(entry.config, newConfig)) {
// Add the renderer to the cache entry and return
entry.ownedBy.push(rendererId);
return entry.atlas;
}
}
const newEntry: ICharAtlasCacheEntry = {
atlas: new DynamicCharAtlas(
document,
newConfig
),
config: newConfig,
ownedBy: [rendererId]
};
charAtlasCache.push(newEntry);
return newEntry.atlas;
}
/**
* Removes a terminal reference from the cache, allowing its memory to be freed.
*/
export function removeTerminalFromCache(rendererId: number): void {
for (let i = 0; i < charAtlasCache.length; i++) {
const index = charAtlasCache[i].ownedBy.indexOf(rendererId);
if (index !== -1) {
if (charAtlasCache[i].ownedBy.length === 1) {
// Remove the cache entry if it's the only renderer
charAtlasCache[i].atlas.dispose();
charAtlasCache.splice(i, 1);
} else {
// Remove the reference from the cache entry
charAtlasCache[i].ownedBy.splice(index, 1);
}
break;
}
}
}
@@ -1,54 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ICharAtlasConfig } from 'browser/renderer/atlas/Types';
import { DEFAULT_COLOR } from 'common/buffer/Constants';
import { IColorSet, IPartialColorSet } from 'browser/Types';
import { ITerminalOptions } from 'common/services/Services';
export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, options: ITerminalOptions, colors: IColorSet): ICharAtlasConfig {
// null out some fields that don't matter
const clonedColors: IPartialColorSet = {
foreground: colors.foreground,
background: colors.background,
cursor: undefined,
cursorAccent: undefined,
selection: undefined,
ansi: colors.ansi.slice()
};
return {
devicePixelRatio: window.devicePixelRatio,
scaledCharWidth,
scaledCharHeight,
fontFamily: options.fontFamily,
fontSize: options.fontSize,
fontWeight: options.fontWeight,
fontWeightBold: options.fontWeightBold,
allowTransparency: options.allowTransparency,
colors: clonedColors
};
}
export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean {
for (let i = 0; i < a.colors.ansi.length; i++) {
if (a.colors.ansi[i].rgba !== b.colors.ansi[i].rgba) {
return false;
}
}
return a.devicePixelRatio === b.devicePixelRatio &&
a.fontFamily === b.fontFamily &&
a.fontSize === b.fontSize &&
a.fontWeight === b.fontWeight &&
a.fontWeightBold === b.fontWeightBold &&
a.allowTransparency === b.allowTransparency &&
a.scaledCharWidth === b.scaledCharWidth &&
a.scaledCharHeight === b.scaledCharHeight &&
a.colors.foreground === b.colors.foreground &&
a.colors.background === b.colors.background;
}
export function is256Color(colorCode: number): boolean {
return colorCode < DEFAULT_COLOR;
}
@@ -1,405 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, TEXT_BASELINE } from 'browser/renderer/atlas/Constants';
import { IGlyphIdentifier, ICharAtlasConfig } from 'browser/renderer/atlas/Types';
import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas';
import { DEFAULT_ANSI_COLORS } from 'browser/ColorManager';
import { LRUMap } from 'browser/renderer/atlas/LRUMap';
import { isFirefox, isSafari } from 'common/Platform';
import { IColor } from 'common/Types';
import { throwIfFalsy } from 'browser/renderer/RendererUtils';
import { color } from 'common/Color';
// In practice we're probably never going to exhaust a texture this large. For debugging purposes,
// however, it can be useful to set this to a really tiny value, to verify that LRU eviction works.
const TEXTURE_WIDTH = 1024;
const TEXTURE_HEIGHT = 1024;
const TRANSPARENT_COLOR = {
css: 'rgba(0, 0, 0, 0)',
rgba: 0
};
// Drawing to the cache is expensive: If we have to draw more than this number of glyphs to the
// cache in a single frame, give up on trying to cache anything else, and try to finish the current
// frame ASAP.
//
// This helps to limit the amount of damage a program can do when it would otherwise thrash the
// cache.
const FRAME_CACHE_DRAW_LIMIT = 100;
/**
* The number of milliseconds to wait before generating the ImageBitmap, this is to debounce/batch
* the operation as window.createImageBitmap is asynchronous.
*/
const GLYPH_BITMAP_COMMIT_DELAY = 100;
interface IGlyphCacheValue {
index: number;
isEmpty: boolean;
inBitmap: boolean;
}
export function getGlyphCacheKey(glyph: IGlyphIdentifier): number {
// Note that this only returns a valid key when code < 256
// Layout:
// 0b00000000000000000000000000000001: italic (1)
// 0b00000000000000000000000000000010: dim (1)
// 0b00000000000000000000000000000100: bold (1)
// 0b00000000000000000000111111111000: fg (9)
// 0b00000000000111111111000000000000: bg (9)
// 0b00011111111000000000000000000000: code (8)
// 0b11100000000000000000000000000000: unused (3)
return glyph.code << 21 | glyph.bg << 12 | glyph.fg << 3 | (glyph.bold ? 0 : 4) + (glyph.dim ? 0 : 2) + (glyph.italic ? 0 : 1);
}
export class DynamicCharAtlas extends BaseCharAtlas {
// An ordered map that we're using to keep track of where each glyph is in the atlas texture.
// It's ordered so that we can determine when to remove the old entries.
private _cacheMap: LRUMap<IGlyphCacheValue>;
// The texture that the atlas is drawn to
private _cacheCanvas: HTMLCanvasElement;
private _cacheCtx: CanvasRenderingContext2D;
// A temporary context that glyphs are drawn to before being transfered to the atlas.
private _tmpCtx: CanvasRenderingContext2D;
// The number of characters stored in the atlas by width/height
private _width: number;
private _height: number;
private _drawToCacheCount: number = 0;
// An array of glyph keys that are waiting on the bitmap to be generated.
private _glyphsWaitingOnBitmap: IGlyphCacheValue[] = [];
// The timeout that is used to batch bitmap generation so it's not requested for every new glyph.
private _bitmapCommitTimeout: number | null = null;
// The bitmap to draw from, this is much faster on other browsers than others.
private _bitmap: ImageBitmap | null = null;
constructor(document: Document, private _config: ICharAtlasConfig) {
super();
this._cacheCanvas = document.createElement('canvas');
this._cacheCanvas.width = TEXTURE_WIDTH;
this._cacheCanvas.height = TEXTURE_HEIGHT;
// The canvas needs alpha because we use clearColor to convert the background color to alpha.
// It might also contain some characters with transparent backgrounds if allowTransparency is
// set.
this._cacheCtx = throwIfFalsy(this._cacheCanvas.getContext('2d', { alpha: true }));
const tmpCanvas = document.createElement('canvas');
tmpCanvas.width = this._config.scaledCharWidth;
tmpCanvas.height = this._config.scaledCharHeight;
this._tmpCtx = throwIfFalsy(tmpCanvas.getContext('2d', { alpha: this._config.allowTransparency }));
this._width = Math.floor(TEXTURE_WIDTH / this._config.scaledCharWidth);
this._height = Math.floor(TEXTURE_HEIGHT / this._config.scaledCharHeight);
const capacity = this._width * this._height;
this._cacheMap = new LRUMap(capacity);
this._cacheMap.prealloc(capacity);
// This is useful for debugging
// document.body.appendChild(this._cacheCanvas);
}
public dispose(): void {
if (this._bitmapCommitTimeout !== null) {
window.clearTimeout(this._bitmapCommitTimeout);
this._bitmapCommitTimeout = null;
}
}
public beginFrame(): void {
this._drawToCacheCount = 0;
}
public clear(): void {
if (this._cacheMap.size > 0) {
const capacity = this._width * this._height;
this._cacheMap = new LRUMap(capacity);
this._cacheMap.prealloc(capacity);
}
this._cacheCtx.clearRect(0, 0, TEXTURE_WIDTH, TEXTURE_HEIGHT);
this._tmpCtx.clearRect(0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight);
}
public draw(
ctx: CanvasRenderingContext2D,
glyph: IGlyphIdentifier,
x: number,
y: number
): boolean {
// Space is always an empty cell, special case this as it's so common
if (glyph.code === 32) {
return true;
}
// Exit early for uncachable glyphs
if (!this._canCache(glyph)) {
return false;
}
const glyphKey = getGlyphCacheKey(glyph);
const cacheValue = this._cacheMap.get(glyphKey);
if (cacheValue !== null && cacheValue !== undefined) {
this._drawFromCache(ctx, cacheValue, x, y);
return true;
}
if (this._drawToCacheCount < FRAME_CACHE_DRAW_LIMIT) {
let index;
if (this._cacheMap.size < this._cacheMap.capacity) {
index = this._cacheMap.size;
} else {
// we're out of space, so our call to set will delete this item
index = this._cacheMap.peek()!.index;
}
const cacheValue = this._drawToCache(glyph, index);
this._cacheMap.set(glyphKey, cacheValue);
this._drawFromCache(ctx, cacheValue, x, y);
return true;
}
return false;
}
private _canCache(glyph: IGlyphIdentifier): boolean {
// Only cache ascii and extended characters for now, to be safe. In the future, we could do
// something more complicated to determine the expected width of a character.
//
// If we switch the renderer over to webgl at some point, we may be able to use blending modes
// to draw overlapping glyphs from the atlas:
// https://github.com/servo/webrender/issues/464#issuecomment-255632875
// https://webglfundamentals.org/webgl/lessons/webgl-text-texture.html
return glyph.code < 256;
}
private _toCoordinateX(index: number): number {
return (index % this._width) * this._config.scaledCharWidth;
}
private _toCoordinateY(index: number): number {
return Math.floor(index / this._width) * this._config.scaledCharHeight;
}
private _drawFromCache(
ctx: CanvasRenderingContext2D,
cacheValue: IGlyphCacheValue,
x: number,
y: number
): void {
// We don't actually need to do anything if this is whitespace.
if (cacheValue.isEmpty) {
return;
}
const cacheX = this._toCoordinateX(cacheValue.index);
const cacheY = this._toCoordinateY(cacheValue.index);
ctx.drawImage(
cacheValue.inBitmap ? this._bitmap! : this._cacheCanvas,
cacheX,
cacheY,
this._config.scaledCharWidth,
this._config.scaledCharHeight,
x,
y,
this._config.scaledCharWidth,
this._config.scaledCharHeight
);
}
private _getColorFromAnsiIndex(idx: number): IColor {
if (idx < this._config.colors.ansi.length) {
return this._config.colors.ansi[idx];
}
return DEFAULT_ANSI_COLORS[idx];
}
private _getBackgroundColor(glyph: IGlyphIdentifier): IColor {
if (this._config.allowTransparency) {
// The background color might have some transparency, so we need to render it as fully
// transparent in the atlas. Otherwise we'd end up drawing the transparent background twice
// around the anti-aliased edges of the glyph, and it would look too dark.
return TRANSPARENT_COLOR;
}
if (glyph.bg === INVERTED_DEFAULT_COLOR) {
return this._config.colors.foreground;
}
if (glyph.bg < 256) {
return this._getColorFromAnsiIndex(glyph.bg);
}
return this._config.colors.background;
}
private _getForegroundColor(glyph: IGlyphIdentifier): IColor {
if (glyph.fg === INVERTED_DEFAULT_COLOR) {
return color.opaque(this._config.colors.background);
}
if (glyph.fg < 256) {
// 256 color support
return this._getColorFromAnsiIndex(glyph.fg);
}
return this._config.colors.foreground;
}
// TODO: We do this (or something similar) in multiple places. We should split this off
// into a shared function.
private _drawToCache(glyph: IGlyphIdentifier, index: number): IGlyphCacheValue {
this._drawToCacheCount++;
this._tmpCtx.save();
// draw the background
const backgroundColor = this._getBackgroundColor(glyph);
// Use a 'copy' composite operation to clear any existing glyph out of _tmpCtxWithAlpha, regardless of
// transparency in backgroundColor
this._tmpCtx.globalCompositeOperation = 'copy';
this._tmpCtx.fillStyle = backgroundColor.css;
this._tmpCtx.fillRect(0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight);
this._tmpCtx.globalCompositeOperation = 'source-over';
// draw the foreground/glyph
const fontWeight = glyph.bold ? this._config.fontWeightBold : this._config.fontWeight;
const fontStyle = glyph.italic ? 'italic' : '';
this._tmpCtx.font =
`${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`;
this._tmpCtx.textBaseline = TEXT_BASELINE;
this._tmpCtx.fillStyle = this._getForegroundColor(glyph).css;
// Apply alpha to dim the character
if (glyph.dim) {
this._tmpCtx.globalAlpha = DIM_OPACITY;
}
// Draw the character
this._tmpCtx.fillText(glyph.chars, 0, this._config.scaledCharHeight);
// clear the background from the character to avoid issues with drawing over the previous
// character if it extends past it's bounds
let imageData = this._tmpCtx.getImageData(
0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight
);
let isEmpty = false;
if (!this._config.allowTransparency) {
isEmpty = clearColor(imageData, backgroundColor);
}
// If this charcater is underscore and empty, shift it up until it is visible, try for a maximum
// of 5 pixels.
if (isEmpty && glyph.chars === '_' && !this._config.allowTransparency) {
for (let offset = 1; offset <= 5; offset++) {
// Draw the character
this._tmpCtx.fillText(glyph.chars, 0, this._config.scaledCharHeight - offset);
// clear the background from the character to avoid issues with drawing over the previous
// character if it extends past it's bounds
imageData = this._tmpCtx.getImageData(
0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight
);
isEmpty = clearColor(imageData, backgroundColor);
if (!isEmpty) {
break;
}
}
}
this._tmpCtx.restore();
// copy the data from imageData to _cacheCanvas
const x = this._toCoordinateX(index);
const y = this._toCoordinateY(index);
// putImageData doesn't do any blending, so it will overwrite any existing cache entry for us
this._cacheCtx.putImageData(imageData, x, y);
// Add the glyph and queue it to the bitmap (if the browser supports it)
const cacheValue = {
index,
isEmpty,
inBitmap: false
};
this._addGlyphToBitmap(cacheValue);
return cacheValue;
}
private _addGlyphToBitmap(cacheValue: IGlyphCacheValue): void {
// Support is patchy for createImageBitmap at the moment, pass a canvas back
// if support is lacking as drawImage works there too. Firefox is also
// included here as ImageBitmap appears both buggy and has horrible
// performance (tested on v55).
if (!('createImageBitmap' in window) || isFirefox || isSafari) {
return;
}
// Add the glyph to the queue
this._glyphsWaitingOnBitmap.push(cacheValue);
// Check if bitmap generation timeout already exists
if (this._bitmapCommitTimeout !== null) {
return;
}
this._bitmapCommitTimeout = window.setTimeout(() => this._generateBitmap(), GLYPH_BITMAP_COMMIT_DELAY);
}
private _generateBitmap(): void {
const glyphsMovingToBitmap = this._glyphsWaitingOnBitmap;
this._glyphsWaitingOnBitmap = [];
window.createImageBitmap(this._cacheCanvas).then(bitmap => {
// Set bitmap
this._bitmap = bitmap;
// Mark all new glyphs as in bitmap, excluding glyphs that came in after
// the bitmap was requested
for (let i = 0; i < glyphsMovingToBitmap.length; i++) {
const value = glyphsMovingToBitmap[i];
// It doesn't matter if the value was already evicted, it will be
// released from memory after this block if so.
value.inBitmap = true;
}
});
this._bitmapCommitTimeout = null;
}
}
// This is used for debugging the renderer, just swap out `new DynamicCharAtlas` with
// `new NoneCharAtlas`.
export class NoneCharAtlas extends BaseCharAtlas {
constructor(document: Document, config: ICharAtlasConfig) {
super();
}
public draw(
ctx: CanvasRenderingContext2D,
glyph: IGlyphIdentifier,
x: number,
y: number
): boolean {
return false;
}
}
/**
* Makes a particular rgb color and colors that are nearly the same in an ImageData completely
* transparent.
* @returns True if the result is "empty", meaning all pixels are fully transparent.
*/
function clearColor(imageData: ImageData, color: IColor): boolean {
let isEmpty = true;
const r = color.rgba >>> 24;
const g = color.rgba >>> 16 & 0xFF;
const b = color.rgba >>> 8 & 0xFF;
for (let offset = 0; offset < imageData.data.length; offset += 4) {
if (Math.abs(imageData.data[offset] - r) +
Math.abs(imageData.data[offset + 1] - g) +
Math.abs(imageData.data[offset + 2] - b) < 35) {
imageData.data[offset + 3] = 0;
} else {
isEmpty = false;
}
}
return isEmpty;
}
-65
View File
@@ -1,65 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { LRUMap } from 'browser/renderer/atlas/LRUMap';
describe('LRUMap', () => {
it('can be used to store and retrieve values', () => {
const map = new LRUMap(10);
map.set(1, 'valuea');
map.set(2, 'valueb');
map.set(3, 'valuec');
assert.strictEqual(map.get(1), 'valuea');
assert.strictEqual(map.get(2), 'valueb');
assert.strictEqual(map.get(3), 'valuec');
});
it('maintains a size from insertions', () => {
const map = new LRUMap(10);
assert.strictEqual(map.size, 0);
map.set(1, 'value');
assert.strictEqual(map.size, 1);
map.set(2, 'value');
assert.strictEqual(map.size, 2);
});
it('deletes the oldest entry when the capacity is exceeded', () => {
const map = new LRUMap(4);
map.set(1, 'value');
map.set(2, 'value');
map.set(3, 'value');
map.set(4, 'value');
map.set(5, 'value');
assert.isNull(map.get(1));
assert.isNotNull(map.get(2));
assert.isNotNull(map.get(3));
assert.isNotNull(map.get(4));
assert.isNotNull(map.get(5));
assert.strictEqual(map.size, 4);
});
it('prevents a recently accessed entry from getting deleted', () => {
const map = new LRUMap(2);
map.set(1, 'value');
map.set(2, 'value');
map.get(1);
// a would normally get deleted here, except that we called get()
map.set(3, 'value');
assert.isNotNull(map.get(1));
// b got deleted instead of a
assert.isNull(map.get(2));
assert.isNotNull(map.get(3));
});
it('supports mutation', () => {
const map = new LRUMap(10);
map.set(1, 'oldvalue');
map.set(1, 'newvalue');
// mutation doesn't change the size
assert.strictEqual(map.size, 1);
assert.strictEqual(map.get(1), 'newvalue');
});
});
-136
View File
@@ -1,136 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
interface ILinkedListNode<T> {
prev: ILinkedListNode<T> | null;
next: ILinkedListNode<T> | null;
key: number | null;
value: T | null;
}
export class LRUMap<T> {
private _map: { [key: number]: ILinkedListNode<T> } = {};
private _head: ILinkedListNode<T> | null = null;
private _tail: ILinkedListNode<T> | null = null;
private _nodePool: ILinkedListNode<T>[] = [];
public size: number = 0;
constructor(public capacity: number) { }
private _unlinkNode(node: ILinkedListNode<T>): void {
const prev = node.prev;
const next = node.next;
if (node === this._head) {
this._head = next;
}
if (node === this._tail) {
this._tail = prev;
}
if (prev !== null) {
prev.next = next;
}
if (next !== null) {
next.prev = prev;
}
}
private _appendNode(node: ILinkedListNode<T>): void {
const tail = this._tail;
if (tail !== null) {
tail.next = node;
}
node.prev = tail;
node.next = null;
this._tail = node;
if (this._head === null) {
this._head = node;
}
}
/**
* Preallocate a bunch of linked-list nodes. Allocating these nodes ahead of time means that
* they're more likely to live next to each other in memory, which seems to improve performance.
*
* Each empty object only consumes about 60 bytes of memory, so this is pretty cheap, even for
* large maps.
*/
public prealloc(count: number): void {
const nodePool = this._nodePool;
for (let i = 0; i < count; i++) {
nodePool.push({
prev: null,
next: null,
key: null,
value: null
});
}
}
public get(key: number): T | null {
// This is unsafe: We're assuming our keyspace doesn't overlap with Object.prototype. However,
// it's faster than calling hasOwnProperty, and in our case, it would never overlap.
const node = this._map[key];
if (node !== undefined) {
this._unlinkNode(node);
this._appendNode(node);
return node.value;
}
return null;
}
/**
* Gets a value from a key without marking it as the most recently used item.
*/
public peekValue(key: number): T | null {
const node = this._map[key];
if (node !== undefined) {
return node.value;
}
return null;
}
public peek(): T | null {
const head = this._head;
return head === null ? null : head.value;
}
public set(key: number, value: T): void {
// This is unsafe: See note above.
let node = this._map[key];
if (node !== undefined) {
// already exists, we just need to mutate it and move it to the end of the list
node = this._map[key];
this._unlinkNode(node);
node.value = value;
} else if (this.size >= this.capacity) {
// we're out of space: recycle the head node, move it to the tail
node = this._head!;
this._unlinkNode(node);
delete this._map[node.key!];
node.key = key;
node.value = value;
this._map[key] = node;
} else {
// make a new element
const nodePool = this._nodePool;
if (nodePool.length > 0) {
// use a preallocated node if we can
node = nodePool.pop()!;
node.key = key;
node.value = value;
} else {
node = {
prev: null,
next: null,
key,
value
};
}
this._map[key] = node;
this.size++;
}
this._appendNode(node);
}
}
-29
View File
@@ -1,29 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { FontWeight } from 'common/services/Services';
import { IPartialColorSet } from 'browser/Types';
export interface IGlyphIdentifier {
chars: string;
code: number;
bg: number;
fg: number;
bold: boolean;
dim: boolean;
italic: boolean;
}
export interface ICharAtlasConfig {
devicePixelRatio: number;
fontSize: number;
fontFamily: string;
fontWeight: FontWeight;
fontWeightBold: FontWeight;
scaledCharWidth: number;
scaledCharHeight: number;
allowTransparency: boolean;
colors: IPartialColorSet;
}
+2 -6
View File
@@ -5,9 +5,9 @@
import { IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/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/atlas/Constants';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants';
import { Disposable } from 'common/Lifecycle';
import { IColorSet, ILinkifierEvent, ILinkifier, ILinkifier2 } from 'browser/Types';
import { IColorSet, ILinkifierEvent, ILinkifier2 } from 'browser/Types';
import { ICharSizeService } from 'browser/services/Services';
import { IOptionsService, IBufferService, IInstantiationService, IDecorationService } from 'common/services/Services';
import { EventEmitter, IEvent } from 'common/EventEmitter';
@@ -47,7 +47,6 @@ export class DomRenderer extends Disposable implements IRenderer {
private readonly _element: HTMLElement,
private readonly _screenElement: HTMLElement,
private readonly _viewportElement: HTMLElement,
private readonly _linkifier: ILinkifier,
private readonly _linkifier2: ILinkifier2,
@IInstantiationService instantiationService: IInstantiationService,
@ICharSizeService private readonly _charSizeService: ICharSizeService,
@@ -87,9 +86,6 @@ export class DomRenderer extends Disposable implements IRenderer {
this._screenElement.appendChild(this._rowContainer);
this._screenElement.appendChild(this._selectionContainer);
this.register(this._linkifier.onShowLinkUnderline(e => this._onLinkHover(e)));
this.register(this._linkifier.onHideLinkUnderline(e => this._onLinkLeave(e)));
this.register(this._linkifier2.onShowLinkUnderline(e => this._onLinkHover(e)));
this.register(this._linkifier2.onHideLinkUnderline(e => this._onLinkLeave(e)));
}
@@ -4,13 +4,13 @@
*/
import { IBufferLine, ICellData, IColor } from 'common/Types';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants';
import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants';
import { CellData } from 'common/buffer/CellData';
import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { color, rgba } from 'common/Color';
import { IColorSet } from 'browser/Types';
import { ICharacterJoinerService, ISelectionService } from 'browser/services/Services';
import { ICharacterJoinerService } from 'browser/services/Services';
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
import { excludeFromContrastRatioDemands } from 'browser/renderer/RendererUtils';