Merge pull request #4170 from Tyriar/shared_cache

Share texture atlas cache code between webgl and canvas renderers
This commit is contained in:
Daniel Imms
2022-10-05 10:19:17 -07:00
committed by GitHub
50 changed files with 820 additions and 1059 deletions
+1
View File
@@ -7,6 +7,7 @@
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": [
"demo/tsconfig.json",
"src/browser/tsconfig.json",
"src/common/tsconfig.json",
"src/headless/tsconfig.json",
+96 -287
View File
@@ -3,23 +3,22 @@
* @license MIT
*/
import { IRenderDimensions } from 'browser/renderer/Types';
import { IRenderLayer } from './Types';
import { ICellData, IColor } from 'common/Types';
import { DEFAULT_COLOR, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE, Attributes } from 'common/buffer/Constants';
import { IGlyphIdentifier } from './atlas/Types';
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, TEXT_BASELINE } from 'browser/renderer/Constants';
import { BaseCharAtlas } from './atlas/BaseCharAtlas';
import { acquireCharAtlas } from './atlas/CharAtlasCache';
import { AttributeData } from 'common/buffer/AttributeData';
import { removeElementFromParent } from 'browser/Dom';
import { acquireTextureAtlas } from 'browser/renderer/shared/CharAtlasCache';
import { TEXT_BASELINE } from 'browser/renderer/shared/Constants';
import { tryDrawCustomChar } from 'browser/renderer/shared/CustomGlyphs';
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
import { IRasterizedGlyph, IRenderDimensions, ISelectionRenderModel, ITextureAtlas } from 'browser/renderer/shared/Types';
import { createSelectionRenderModel } from 'browser/renderer/shared/SelectionRenderModel';
import { ICoreBrowserService } from 'browser/services/Services';
import { IColorSet } from 'browser/Types';
import { CellData } from 'common/buffer/CellData';
import { WHITESPACE_CELL_CODE } from 'common/buffer/Constants';
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
import { ICoreBrowserService } from 'browser/services/Services';
import { excludeFromContrastRatioDemands, throwIfFalsy } from 'browser/renderer/RendererUtils';
import { channels, color, rgba } from 'common/Color';
import { removeElementFromParent } from 'browser/Dom';
import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs';
import { ICellData } from 'common/Types';
import { Terminal } from 'xterm';
import { IRenderLayer } from './Types';
import { CellColorResolver } from 'browser/renderer/shared/CellColorResolver';
export abstract class BaseRenderLayer implements IRenderLayer {
private _canvas: HTMLCanvasElement;
@@ -31,44 +30,34 @@ export abstract class BaseRenderLayer implements IRenderLayer {
private _scaledCharLeft: number = 0;
private _scaledCharTop: number = 0;
protected _selectionStart: [number, number] | undefined;
protected _selectionEnd: [number, number] | undefined;
protected _columnSelectMode: boolean = false;
protected _selectionModel: ISelectionRenderModel = createSelectionRenderModel();
private _cellColorResolver: CellColorResolver;
private _bitmapGenerator?: BitmapGenerator;
protected _charAtlas: BaseCharAtlas | undefined;
/**
* An object that's reused when drawing glyphs in order to reduce GC.
*/
private _currentGlyphIdentifier: IGlyphIdentifier = {
chars: '',
code: 0,
bg: 0,
fg: 0,
bold: false,
dim: false,
italic: false
};
protected _charAtlas!: ITextureAtlas;
public get canvas(): HTMLCanvasElement { return this._canvas; }
public get cacheCanvas(): HTMLCanvasElement { return this._charAtlas?.cacheCanvas!; }
constructor(
private readonly _terminal: Terminal,
private _container: HTMLElement,
id: string,
zIndex: number,
private _alpha: boolean,
protected _colors: IColorSet,
private _rendererId: number,
protected readonly _bufferService: IBufferService,
protected readonly _optionsService: IOptionsService,
protected readonly _decorationService: IDecorationService,
protected readonly _coreBrowserService: ICoreBrowserService
) {
this._cellColorResolver = new CellColorResolver(this._terminal, this._colors, this._selectionModel, this._decorationService, this._coreBrowserService);
this._canvas = document.createElement('canvas');
this._canvas.classList.add(`xterm-${id}-layer`);
this._canvas.style.zIndex = zIndex.toString();
this._initCanvas();
this._container.appendChild(this._canvas);
this._refreshCharAtlas(this._colors);
}
public dispose(): void {
@@ -91,9 +80,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
public onGridChanged(startRow: number, endRow: number): void {}
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
this._selectionStart = start;
this._selectionEnd = end;
this._columnSelectMode = columnSelectMode;
this._selectionModel.update(this._terminal, start, end, columnSelectMode);
}
public setColors(colorSet: IColorSet): void {
@@ -127,8 +114,9 @@ export abstract class BaseRenderLayer implements IRenderLayer {
if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) {
return;
}
this._charAtlas = acquireCharAtlas(this._optionsService.rawOptions, this._rendererId, colorSet, this._scaledCharWidth, this._scaledCharHeight, this._coreBrowserService.dpr);
this._charAtlas = acquireTextureAtlas(this._terminal, colorSet, this._scaledCellWidth, this._scaledCellHeight, this._scaledCharWidth, this._scaledCharHeight, this._coreBrowserService.dpr);
this._charAtlas.warmUp();
this._bitmapGenerator = new BitmapGenerator(this._charAtlas.cacheCanvas);
}
public resize(dim: IRenderDimensions): void {
@@ -154,7 +142,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
public abstract reset(): void;
public clearTextureAtlas(): void {
this._charAtlas?.clear();
this._charAtlas?.clearTexture();
}
/**
@@ -365,134 +353,37 @@ export abstract class BaseRenderLayer implements IRenderLayer {
/**
* Draws one or more characters at a cell. If possible this will draw using
* the character atlas to reduce draw time.
* @param chars The character or characters.
* @param code The character code.
* @param width The width of the characters.
* @param x The column to draw at.
* @param y The row to draw at.
* @param fg The foreground color, in the format stored within the attributes.
* @param bg The background color, in the format stored within the attributes.
* This is used to validate whether a cached image can be used.
* @param bold Whether the text is bold.
*/
protected _drawChars(cell: ICellData, x: number, y: number): void {
const contrastColor = this._getContrastColor(cell, x, y);
// skip cache right away if we draw in RGB
// Note: to avoid bad runtime JoinedCellData will be skipped
// in the cache handler itself (atlasDidDraw == false) and
// fall through to uncached later down below
if (contrastColor || cell.isFgRGB() || cell.isBgRGB()) {
this._drawUncachedChars(cell, x, y, contrastColor);
return;
if (this._charAtlas.hasCanvasChanged) {
this._bitmapGenerator?.refresh();
this._charAtlas.hasCanvasChanged = false;
}
let fg;
let bg;
if (cell.isInverse()) {
fg = (cell.isBgDefault()) ? INVERTED_DEFAULT_COLOR : cell.getBgColor();
bg = (cell.isFgDefault()) ? INVERTED_DEFAULT_COLOR : cell.getFgColor();
const chars = cell.getChars();
this._cellColorResolver.resolve(cell, x, y);
let glyph: IRasterizedGlyph;
if (chars && chars.length > 1) {
glyph = this._charAtlas.getRasterizedGlyphCombinedChar(chars, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext);
} else {
bg = (cell.isBgDefault()) ? DEFAULT_COLOR : cell.getBgColor();
fg = (cell.isFgDefault()) ? DEFAULT_COLOR : cell.getFgColor();
glyph = this._charAtlas.getRasterizedGlyph(cell.getCode() || WHITESPACE_CELL_CODE, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext);
}
const drawInBrightColor = this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8;
fg += drawInBrightColor ? 8 : 0;
this._currentGlyphIdentifier.chars = cell.getChars() || WHITESPACE_CELL_CHAR;
this._currentGlyphIdentifier.code = cell.getCode() || WHITESPACE_CELL_CODE;
this._currentGlyphIdentifier.bg = bg;
this._currentGlyphIdentifier.fg = fg;
this._currentGlyphIdentifier.bold = !!cell.isBold();
this._currentGlyphIdentifier.dim = !!cell.isDim();
this._currentGlyphIdentifier.italic = !!cell.isItalic();
// Don't try cache the glyph if it uses any decoration foreground/background override.
let hasOverrides = false;
this._decorationService.forEachDecorationAtCell(x, y, undefined, d => {
if (d.backgroundColorRGB || d.foregroundColorRGB) {
hasOverrides = true;
}
});
const atlasDidDraw = hasOverrides ? false : this._charAtlas?.draw(this._ctx, this._currentGlyphIdentifier, x * this._scaledCellWidth + this._scaledCharLeft, y * this._scaledCellHeight + this._scaledCharTop);
if (!atlasDidDraw) {
this._drawUncachedChars(cell, x, y);
}
}
/**
* Draws one or more characters at one or more cells. The character(s) will be
* clipped to ensure that they fit with the cell(s), including the cell to the
* right if the last character is a wide character.
* @param chars The character.
* @param width The width of the character.
* @param fg The foreground color, in the format stored within the attributes.
* @param x The column to draw at.
* @param y The row to draw at.
*/
private _drawUncachedChars(cell: ICellData, x: number, y: number, fgOverride?: IColor): void {
this._ctx.save();
this._ctx.font = this._getFont(!!cell.isBold(), !!cell.isItalic());
this._ctx.textBaseline = TEXT_BASELINE;
if (cell.isInverse()) {
if (fgOverride) {
this._ctx.fillStyle = fgOverride.css;
} else if (cell.isBgDefault()) {
this._ctx.fillStyle = color.opaque(this._colors.background).css;
} else if (cell.isBgRGB()) {
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
} else {
let bg = cell.getBgColor();
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && bg < 8) {
bg += 8;
}
this._ctx.fillStyle = this._colors.ansi[bg].css;
}
} else {
if (fgOverride) {
this._ctx.fillStyle = fgOverride.css;
} else if (cell.isFgDefault()) {
this._ctx.fillStyle = this._colors.foreground.css;
} else if (cell.isFgRGB()) {
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
} else {
let fg = cell.getFgColor();
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
fg += 8;
}
this._ctx.fillStyle = this._colors.ansi[fg].css;
}
}
this._clipRow(y);
// Apply alpha to dim the character
if (cell.isDim()) {
this._ctx.globalAlpha = DIM_OPACITY;
}
// Draw custom characters if applicable
let drawSuccess = false;
if (this._optionsService.rawOptions.customGlyphs !== false) {
drawSuccess = tryDrawCustomChar(this._ctx, cell.getChars(), x * this._scaledCellWidth, y * this._scaledCellHeight, this._scaledCellWidth, this._scaledCellHeight, this._optionsService.rawOptions.fontSize, this._coreBrowserService.dpr);
}
// Draw the character
if (!drawSuccess) {
this._ctx.fillText(
cell.getChars(),
x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight);
}
// Draw the image, use the bitmap if it's available
this._ctx.drawImage(
this._bitmapGenerator?.bitmap || this._charAtlas!.cacheCanvas,
glyph.texturePosition.x,
glyph.texturePosition.y,
glyph.size.x,
glyph.size.y,
x * this._scaledCellWidth - glyph.offset.x,
y * this._scaledCellHeight - glyph.offset.y,
glyph.size.x,
glyph.size.y
);
this._ctx.restore();
}
/**
* Clips a row to ensure no pixels will be drawn outside the cells in the row.
* @param y The row to clip.
@@ -517,137 +408,55 @@ export abstract class BaseRenderLayer implements IRenderLayer {
return `${fontStyle} ${fontWeight} ${this._optionsService.rawOptions.fontSize * this._coreBrowserService.dpr}px ${this._optionsService.rawOptions.fontFamily}`;
}
private _getContrastColor(cell: CellData, x: number, y: number): IColor | undefined {
// Get any decoration foreground/background overrides, this must be fetched before the early
// exist but applied after inverse
let bgOverride: number | undefined;
let fgOverride: number | undefined;
let isTop = false;
this._decorationService.forEachDecorationAtCell(x, y, undefined, d => {
if (d.options.layer !== 'top' && isTop) {
return;
}
if (d.backgroundColorRGB) {
bgOverride = d.backgroundColorRGB.rgba;
}
if (d.foregroundColorRGB) {
fgOverride = d.foregroundColorRGB.rgba;
}
isTop = d.options.layer === 'top';
});
// Apply selection foreground if applicable
if (!isTop) {
if (this._colors.selectionForeground && this._isCellInSelection(x, y)) {
fgOverride = this._colors.selectionForeground.rgba;
}
}
if (!bgOverride && !fgOverride && (this._optionsService.rawOptions.minimumContrastRatio === 1 || excludeFromContrastRatioDemands(cell.getCode()))) {
return undefined;
}
if (!bgOverride && !fgOverride) {
// Try get from cache
const adjustedColor = this._colors.contrastCache.getColor(cell.bg, cell.fg);
if (adjustedColor !== undefined) {
return adjustedColor || undefined;
}
}
let fgColor = cell.getFgColor();
let fgColorMode = cell.getFgColorMode();
let bgColor = cell.getBgColor();
let bgColorMode = cell.getBgColorMode();
const isInverse = !!cell.isInverse();
const isBold = !!cell.isInverse();
if (isInverse) {
const temp = fgColor;
fgColor = bgColor;
bgColor = temp;
const temp2 = fgColorMode;
fgColorMode = bgColorMode;
bgColorMode = temp2;
}
const bgRgba = this._resolveBackgroundRgba(bgOverride !== undefined ? Attributes.CM_RGB : bgColorMode, bgOverride ?? bgColor, isInverse);
const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, isInverse, isBold);
let result = rgba.ensureContrastRatio(bgOverride ?? bgRgba, fgOverride ?? fgRgba, this._optionsService.rawOptions.minimumContrastRatio);
if (!result) {
if (!fgOverride) {
this._colors.contrastCache.setColor(cell.bg, cell.fg, null);
return undefined;
}
// If it was an override and there was no contrast change, set as the result
result = fgOverride;
}
const color: IColor = {
css: channels.toCss(
(result >> 24) & 0xFF,
(result >> 16) & 0xFF,
(result >> 8) & 0xFF
),
rgba: result
};
if (!bgOverride && !fgOverride) {
this._colors.contrastCache.setColor(cell.bg, cell.fg, color);
}
return color;
}
private _resolveBackgroundRgba(bgColorMode: number, bgColor: number, inverse: boolean): number {
switch (bgColorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
return this._colors.ansi[bgColor].rgba;
case Attributes.CM_RGB:
return bgColor << 8;
case Attributes.CM_DEFAULT:
default:
if (inverse) {
return this._colors.foreground.rgba;
}
return this._colors.background.rgba;
}
}
private _resolveForegroundRgba(fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): number {
switch (fgColorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && bold && fgColor < 8) {
fgColor += 8;
}
return this._colors.ansi[fgColor].rgba;
case Attributes.CM_RGB:
return fgColor << 8;
case Attributes.CM_DEFAULT:
default:
if (inverse) {
return this._colors.background.rgba;
}
return this._colors.foreground.rgba;
}
}
private _isCellInSelection(x: number, y: number): boolean {
const start = this._selectionStart;
const end = this._selectionEnd;
if (!start || !end) {
return false;
}
if (this._columnSelectMode) {
return x >= start[0] && y >= start[1] &&
x < end[0] && y < end[1];
}
return (y > start[1] && y < end[1]) ||
(start[1] === end[1] && y === start[1] && x >= start[0] && x < end[0]) ||
(start[1] < end[1] && y === end[1] && x < end[0]) ||
(start[1] < end[1] && y === start[1] && x >= start[0]);
}
}
/**
* The number of milliseconds to wait before generating the ImageBitmap, this is to debounce/batch
* the operation as window.createImageBitmap is asynchronous.
*/
const GLYPH_BITMAP_COMMIT_DELAY = 100;
const enum BitmapGeneratorState {
IDLE = 0,
GENERATING = 1,
GENERATING_INVALID = 2
}
class BitmapGenerator {
private _state: BitmapGeneratorState = BitmapGeneratorState.IDLE;
private _commitTimeout: number | undefined = undefined;
private _bitmap: ImageBitmap | undefined = undefined;
public get bitmap(): ImageBitmap | undefined { return this._bitmap; }
constructor(private readonly _canvas: HTMLCanvasElement) {
}
public refresh(): void {
// Clear the bitmap immediately as it's stale
this._bitmap = undefined;
if (this._commitTimeout === undefined) {
this._commitTimeout = window.setTimeout(() => this._generate(), GLYPH_BITMAP_COMMIT_DELAY);
}
if (this._state === BitmapGeneratorState.GENERATING) {
this._state = BitmapGeneratorState.GENERATING_INVALID;
}
}
private _generate(): void {
if (this._state === BitmapGeneratorState.IDLE) {
this._bitmap = undefined;
this._state = BitmapGeneratorState.GENERATING;
window.createImageBitmap(this._canvas).then(bitmap => {
if (this._state === BitmapGeneratorState.GENERATING_INVALID) {
this.refresh();
} else {
this._bitmap = bitmap;
}
this._state = BitmapGeneratorState.IDLE;
});
if (this._commitTimeout) {
this._commitTimeout = undefined;
}
}
}
}
+9 -1
View File
@@ -8,11 +8,14 @@ import { IColorSet } from 'browser/Types';
import { CanvasRenderer } from './CanvasRenderer';
import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { ITerminalAddon, Terminal } from 'xterm';
import { forwardEvent, initEvent } from 'common/EventEmitter';
export class CanvasAddon implements ITerminalAddon {
private _terminal?: Terminal;
private _renderer?: CanvasRenderer;
public readonly onChangeTextureAtlas = initEvent<HTMLCanvasElement>();
public activate(terminal: Terminal): void {
if (!terminal.element) {
throw new Error('Cannot activate CanvasAddon before Terminal.open');
@@ -29,7 +32,8 @@ export class CanvasAddon implements ITerminalAddon {
const colors: IColorSet = (terminal as any)._core._colorManager.colors;
const screenElement: HTMLElement = (terminal as any)._core.screenElement;
const linkifier = (terminal as any)._core.linkifier2;
this._renderer = new CanvasRenderer(colors, screenElement, linkifier, bufferService, charSizeService, optionsService, characterJoinerService, coreService, coreBrowserService, decorationService);
this._renderer = new CanvasRenderer(terminal, colors, screenElement, linkifier, bufferService, charSizeService, optionsService, characterJoinerService, coreService, coreBrowserService, decorationService);
forwardEvent(this._renderer.onChangeTextureAtlas, this.onChangeTextureAtlas);
renderService.setRenderer(this._renderer);
renderService.onResize(bufferService.cols, bufferService.rows);
}
@@ -44,4 +48,8 @@ export class CanvasAddon implements ITerminalAddon {
this._renderer?.dispose();
this._renderer = undefined;
}
public get textureAtlas(): HTMLCanvasElement | undefined {
return this._renderer?.textureAtlas;
}
}
+14 -11
View File
@@ -6,7 +6,7 @@
import { TextRenderLayer } from './TextRenderLayer';
import { SelectionRenderLayer } from './SelectionRenderLayer';
import { CursorRenderLayer } from './CursorRenderLayer';
import { IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types';
import { IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
import { IRenderLayer } from './Types';
import { LinkRenderLayer } from './LinkRenderLayer';
import { Disposable } from 'common/Lifecycle';
@@ -14,22 +14,21 @@ import { IColorSet, ILinkifier2 } from 'browser/Types';
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService } from 'browser/services/Services';
import { IBufferService, IOptionsService, IDecorationService, ICoreService } from 'common/services/Services';
import { removeTerminalFromCache } from './atlas/CharAtlasCache';
import { observeDevicePixelDimensions } from 'browser/renderer/shared/DevicePixelObserver';
import { Terminal } from 'xterm';
import { initEvent, EventEmitter, IEvent } from 'common/EventEmitter';
import { observeDevicePixelDimensions } from 'browser/renderer/DevicePixelObserver';
let nextRendererId = 1;
export class CanvasRenderer extends Disposable implements IRenderer {
private _id = nextRendererId++;
private _renderLayers: IRenderLayer[];
private _devicePixelRatio: number;
public dimensions: IRenderDimensions;
public readonly onRequestRedraw = initEvent<IRequestRedrawEvent>();
public readonly onChangeTextureAtlas = initEvent<HTMLCanvasElement>();
constructor(
private readonly _terminal: Terminal,
private _colors: IColorSet,
private readonly _screenElement: HTMLElement,
linkifier2: ILinkifier2,
@@ -44,10 +43,10 @@ export class CanvasRenderer extends Disposable implements IRenderer {
super();
const allowTransparency = this._optionsService.rawOptions.allowTransparency;
this._renderLayers = [
new TextRenderLayer(this._screenElement, 0, this._colors, allowTransparency, this._id, this._bufferService, this._optionsService, characterJoinerService, decorationService, this._coreBrowserService),
new SelectionRenderLayer(this._screenElement, 1, this._colors, this._id, this._bufferService, this._coreBrowserService, decorationService, this._optionsService),
new LinkRenderLayer(this._screenElement, 2, this._colors, this._id, linkifier2, this._bufferService, this._optionsService, decorationService, this._coreBrowserService),
new CursorRenderLayer(this._screenElement, 3, this._colors, this._id, this.onRequestRedraw, this._bufferService, this._optionsService, coreService, this._coreBrowserService, decorationService)
new TextRenderLayer(this._terminal, this._screenElement, 0, this._colors, allowTransparency, this._bufferService, this._optionsService, characterJoinerService, decorationService, this._coreBrowserService),
new SelectionRenderLayer(this._terminal, this._screenElement, 1, this._colors, this._bufferService, this._coreBrowserService, decorationService, this._optionsService),
new LinkRenderLayer(this._terminal, this._screenElement, 2, this._colors, linkifier2, this._bufferService, this._optionsService, decorationService, this._coreBrowserService),
new CursorRenderLayer(this._terminal, this._screenElement, 3, this._colors, this.onRequestRedraw, this._bufferService, this._optionsService, coreService, this._coreBrowserService, decorationService)
];
this.dimensions = {
scaledCharWidth: 0,
@@ -76,7 +75,11 @@ export class CanvasRenderer extends Disposable implements IRenderer {
l.dispose();
}
super.dispose();
removeTerminalFromCache(this._id);
removeTerminalFromCache(this._terminal);
}
public get textureAtlas(): HTMLCanvasElement | undefined {
return this._renderLayers[0].cacheCanvas;
}
public onDevicePixelRatioChange(): void {
@@ -3,7 +3,7 @@
* @license MIT
*/
import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types';
import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
import { BaseRenderLayer } from './BaseRenderLayer';
import { ICellData } from 'common/Types';
import { CellData } from 'common/buffer/CellData';
@@ -11,6 +11,7 @@ import { IColorSet } from 'browser/Types';
import { IBufferService, IOptionsService, ICoreService, IDecorationService } from 'common/services/Services';
import { IEventEmitter } from 'common/EventEmitter';
import { ICoreBrowserService } from 'browser/services/Services';
import { Terminal } from 'xterm';
interface ICursorState {
x: number;
@@ -32,10 +33,10 @@ export class CursorRenderLayer extends BaseRenderLayer {
private _cell: ICellData = new CellData();
constructor(
terminal: Terminal,
container: HTMLElement,
zIndex: number,
colors: IColorSet,
rendererId: number,
private readonly _onRequestRedraw: IEventEmitter<IRequestRedrawEvent>,
bufferService: IBufferService,
optionsService: IOptionsService,
@@ -43,7 +44,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
coreBrowserService: ICoreBrowserService,
decorationService: IDecorationService
) {
super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService, coreBrowserService);
super(terminal, container, 'cursor', zIndex, true, colors, bufferService, optionsService, decorationService, coreBrowserService);
this._state = {
x: 0,
y: 0,
@@ -3,29 +3,30 @@
* @license MIT
*/
import { IRenderDimensions } from 'browser/renderer/Types';
import { IRenderDimensions } from 'browser/renderer/shared/Types';
import { BaseRenderLayer } from './BaseRenderLayer';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
import { ICoreBrowserService } from 'browser/services/Services';
import { is256Color } from './atlas/CharAtlasUtils';
import { IColorSet, ILinkifierEvent, ILinkifier2 } from 'browser/Types';
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
import { is256Color } from 'browser/renderer/shared/CharAtlasUtils';
import { Terminal } from 'xterm';
export class LinkRenderLayer extends BaseRenderLayer {
private _state: ILinkifierEvent | undefined;
constructor(
terminal: Terminal,
container: HTMLElement,
zIndex: number,
colors: IColorSet,
rendererId: number,
linkifier2: ILinkifier2,
bufferService: IBufferService,
optionsService: IOptionsService,
decorationService: IDecorationService,
coreBrowserService: ICoreBrowserService
) {
super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService, coreBrowserService);
super(terminal, container, 'link', zIndex, true, colors, bufferService, optionsService, decorationService, coreBrowserService);
linkifier2.onShowLinkUnderline(e => this._onShowLinkUnderline(e));
linkifier2.onHideLinkUnderline(e => this._onHideLinkUnderline(e));
@@ -3,11 +3,12 @@
* @license MIT
*/
import { IRenderDimensions } from 'browser/renderer/Types';
import { IRenderDimensions } from 'browser/renderer/shared/Types';
import { BaseRenderLayer } from './BaseRenderLayer';
import { IColorSet } from 'browser/Types';
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
import { ICoreBrowserService } from 'browser/services/Services';
import { Terminal } from 'xterm';
interface ISelectionState {
start?: [number, number];
@@ -20,16 +21,16 @@ export class SelectionRenderLayer extends BaseRenderLayer {
private _state!: ISelectionState;
constructor(
terminal: Terminal,
container: HTMLElement,
zIndex: number,
colors: IColorSet,
rendererId: number,
bufferService: IBufferService,
coreBrowserService: ICoreBrowserService,
decorationService: IDecorationService,
optionsService: IOptionsService
) {
super(container, 'selection', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService, coreBrowserService);
super(terminal, container, 'selection', zIndex, true, colors, bufferService, optionsService, decorationService, coreBrowserService);
this._clearState();
}
@@ -46,8 +47,8 @@ export class SelectionRenderLayer extends BaseRenderLayer {
super.resize(dim);
// On resize use the base render layer's cached selection values since resize clears _state
// inside reset.
if (this._selectionStart && this._selectionEnd) {
this._redrawSelection(this._selectionStart, this._selectionEnd, this._columnSelectMode);
if (this._selectionModel.selectionStart && this._selectionModel.selectionEnd) {
this._redrawSelection(this._selectionModel.selectionStart, this._selectionModel.selectionEnd, this._selectionModel.columnSelectMode);
}
}
@@ -60,12 +61,12 @@ export class SelectionRenderLayer extends BaseRenderLayer {
public onBlur(): void {
this.reset();
this._redrawSelection(this._selectionStart, this._selectionEnd, this._columnSelectMode);
this._redrawSelection(this._selectionModel.selectionStart, this._selectionModel.selectionEnd, this._selectionModel.columnSelectMode);
}
public onFocus(): void {
this.reset();
this._redrawSelection(this._selectionStart, this._selectionEnd, this._columnSelectMode);
this._redrawSelection(this._selectionModel.selectionStart, this._selectionModel.selectionEnd, this._selectionModel.columnSelectMode);
}
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
@@ -3,7 +3,7 @@
* @license MIT
*/
import { IRenderDimensions } from 'browser/renderer/Types';
import { IRenderDimensions } from 'browser/renderer/shared/Types';
import { CharData, ICellData } from 'common/Types';
import { GridCache } from './GridCache';
import { BaseRenderLayer } from './BaseRenderLayer';
@@ -15,6 +15,7 @@ import { IOptionsService, IBufferService, IDecorationService } from 'common/serv
import { ICharacterJoinerService, ICoreBrowserService } from 'browser/services/Services';
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
import { color, css } from 'common/Color';
import { Terminal } from 'xterm';
/**
* This CharData looks like a null character, which will forc a clear and render
@@ -31,18 +32,18 @@ export class TextRenderLayer extends BaseRenderLayer {
private _workCell = new CellData();
constructor(
terminal: Terminal,
container: HTMLElement,
zIndex: number,
colors: IColorSet,
alpha: boolean,
rendererId: number,
bufferService: IBufferService,
optionsService: IOptionsService,
private readonly _characterJoinerService: ICharacterJoinerService,
decorationService: IDecorationService,
coreBrowserService: ICoreBrowserService
) {
super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService, decorationService, coreBrowserService);
super(terminal, container, 'text', zIndex, alpha, colors, bufferService, optionsService, decorationService, coreBrowserService);
this._state = new GridCache<CharData>();
}
@@ -232,78 +233,7 @@ export class TextRenderLayer extends BaseRenderLayer {
}
private _drawForeground(firstRow: number, lastRow: number): void {
this._forEachCell(firstRow, lastRow, (cell, x, y) => {
if (cell.isInvisible()) {
return;
}
this._drawChars(cell, x, y);
if (cell.isUnderline() || cell.isStrikethrough()) {
this._ctx.save();
if (cell.isInverse()) {
if (cell.isBgDefault()) {
this._ctx.fillStyle = this._colors.background.css;
} else if (cell.isBgRGB()) {
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
} else {
let bg = cell.getBgColor();
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && bg < 8) {
bg += 8;
}
this._ctx.fillStyle = this._colors.ansi[bg].css;
}
} else {
if (cell.isFgDefault()) {
this._ctx.fillStyle = this._colors.foreground.css;
} else if (cell.isFgRGB()) {
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
} else {
let fg = cell.getFgColor();
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
fg += 8;
}
this._ctx.fillStyle = this._colors.ansi[fg].css;
}
}
if (cell.isStrikethrough()) {
this._fillMiddleLineAtCells(x, y, cell.getWidth());
}
if (cell.isUnderline()) {
if (!cell.isUnderlineColorDefault()) {
if (cell.isUnderlineColorRGB()) {
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getUnderlineColor()).join(',')})`;
} else {
let fg = cell.getUnderlineColor();
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
fg += 8;
}
this._ctx.fillStyle = this._colors.ansi[fg].css;
}
}
switch (cell.extended.underlineStyle) {
case UnderlineStyle.DOUBLE:
this._fillBottomLineAtCells(x, y, cell.getWidth(), -this._coreBrowserService.dpr);
this._fillBottomLineAtCells(x, y, cell.getWidth(), this._coreBrowserService.dpr);
break;
case UnderlineStyle.CURLY:
this._curlyUnderlineAtCell(x, y, cell.getWidth());
break;
case UnderlineStyle.DOTTED:
this._dottedUnderlineAtCell(x, y, cell.getWidth());
break;
case UnderlineStyle.DASHED:
this._dashedUnderlineAtCell(x, y, cell.getWidth());
break;
case UnderlineStyle.SINGLE:
default:
this._fillBottomLineAtCells(x, y, cell.getWidth());
break;
}
}
this._ctx.restore();
}
});
this._forEachCell(firstRow, lastRow, (cell, x, y) => this._drawChars(cell, x, y));
}
public onGridChanged(firstRow: number, lastRow: number): void {
+1
View File
@@ -58,6 +58,7 @@ export interface IRenderer extends IDisposable {
export interface IRenderLayer extends IDisposable {
readonly canvas: HTMLCanvasElement;
readonly cacheCanvas: HTMLCanvasElement;
/**
* Called when the terminal loses focus.
@@ -1,58 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IGlyphIdentifier } from './Types';
import { IDisposable } from 'common/Types';
export abstract class BaseCharAtlas implements IDisposable {
private _didWarmUp: boolean = false;
public dispose(): void { }
/**
* Perform any work needed to warm the cache before it can be used. May be called multiple times.
* Implement _doWarmUp instead if you only want to get called once.
*/
public warmUp(): void {
if (!this._didWarmUp) {
this._doWarmUp();
this._didWarmUp = true;
}
}
/**
* Perform any work needed to warm the cache before it can be used. Used by the default
* implementation of warmUp(), and will only be called once.
*/
private _doWarmUp(): void { }
public clear(): void { }
/**
* Called when we start drawing a new frame.
*
* TODO: We rely on this getting called by TextRenderLayer. This should really be called by
* Renderer instead, but we need to make Renderer the source-of-truth for the char atlas, instead
* of BaseRenderLayer.
*/
public beginFrame(): void { }
/**
* May be called before warmUp finishes, however it is okay for the implementation to
* do nothing and return false in that case.
*
* @param ctx Where to draw the character onto.
* @param glyph Information about what to draw
* @param x The position on the context to start drawing at
* @param y The position on the context to start drawing at
* @returns The success state. True if we drew the character.
*/
public abstract draw(
ctx: CanvasRenderingContext2D,
glyph: IGlyphIdentifier,
x: number,
y: number
): boolean;
}
@@ -3,26 +3,21 @@
* @license MIT
*/
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, TEXT_BASELINE } from 'browser/renderer/Constants';
import { IGlyphIdentifier, ICharAtlasConfig } from './Types';
import { BaseCharAtlas } from './BaseCharAtlas';
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, TEXT_BASELINE } from 'browser/renderer/shared/Constants';
import { IGlyphIdentifier } from './Types';
import { DEFAULT_ANSI_COLORS } from 'browser/ColorManager';
import { LRUMap } from './LRUMap';
import { isFirefox, isSafari } from 'common/Platform';
import { IColor } from 'common/Types';
import { throwIfFalsy } from 'browser/renderer/RendererUtils';
import { color } from 'common/Color';
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
import { color, NULL_COLOR } from 'common/Color';
import { ICharAtlasConfig } from 'browser/renderer/shared/Types';
// In practice we're probably never going to exhaust a texture this large. For debugging purposes,
// however, it can be useful to set this to a really tiny value, to verify that LRU eviction works.
const TEXTURE_WIDTH = 1024;
const TEXTURE_HEIGHT = 1024;
const TRANSPARENT_COLOR = {
css: 'rgba(0, 0, 0, 0)',
rgba: 0
};
// Drawing to the cache is expensive: If we have to draw more than this number of glyphs to the
// cache in a single frame, give up on trying to cache anything else, and try to finish the current
// frame ASAP.
@@ -56,14 +51,16 @@ export function getGlyphCacheKey(glyph: IGlyphIdentifier): number {
return glyph.code << 21 | glyph.bg << 12 | glyph.fg << 3 | (glyph.bold ? 0 : 4) + (glyph.dim ? 0 : 2) + (glyph.italic ? 0 : 1);
}
export class DynamicCharAtlas extends BaseCharAtlas {
export class CanvasCharAtlas {
// An ordered map that we're using to keep track of where each glyph is in the atlas texture.
// It's ordered so that we can determine when to remove the old entries.
private _cacheMap: LRUMap<IGlyphCacheValue>;
// The texture that the atlas is drawn to
private _cacheCanvas: HTMLCanvasElement;
public get cacheCanvas(): HTMLCanvasElement { return this._cacheCanvas; }
private _cacheCtx: CanvasRenderingContext2D;
private _didWarmUp: boolean = false;
// A temporary context that glyphs are drawn to before being transfered to the atlas.
private _tmpCtx: CanvasRenderingContext2D;
@@ -84,7 +81,6 @@ export class DynamicCharAtlas extends BaseCharAtlas {
private _bitmap: ImageBitmap | null = null;
constructor(document: Document, private _config: ICharAtlasConfig) {
super();
this._cacheCanvas = document.createElement('canvas');
this._cacheCanvas.width = TEXTURE_WIDTH;
this._cacheCanvas.height = TEXTURE_HEIGHT;
@@ -118,6 +114,23 @@ export class DynamicCharAtlas extends BaseCharAtlas {
}
}
/**
* Perform any work needed to warm the cache before it can be used. May be called multiple times.
* Implement _doWarmUp instead if you only want to get called once.
*/
public warmUp(): void {
if (!this._didWarmUp) {
this._doWarmUp();
this._didWarmUp = true;
}
}
/**
* Perform any work needed to warm the cache before it can be used. Used by the default
* implementation of warmUp(), and will only be called once.
*/
private _doWarmUp(): void { }
public beginFrame(): void {
this._drawToCacheCount = 0;
}
@@ -226,7 +239,7 @@ export class DynamicCharAtlas extends BaseCharAtlas {
// The background color might have some transparency, so we need to render it as fully
// transparent in the atlas. Otherwise we'd end up drawing the transparent background twice
// around the anti-aliased edges of the glyph, and it would look too dark.
return TRANSPARENT_COLOR;
return NULL_COLOR;
}
let result: IColor;
if (glyph.bg === INVERTED_DEFAULT_COLOR) {
@@ -374,23 +387,6 @@ export class DynamicCharAtlas extends BaseCharAtlas {
}
}
// This is used for debugging the renderer, just swap out `new DynamicCharAtlas` with
// `new NoneCharAtlas`.
export class NoneCharAtlas extends BaseCharAtlas {
constructor(document: Document, config: ICharAtlasConfig) {
super();
}
public draw(
ctx: CanvasRenderingContext2D,
glyph: IGlyphIdentifier,
x: number,
y: number
): boolean {
return false;
}
}
/**
* Makes a particular rgb color and colors that are nearly the same in an ImageData completely
* transparent.
@@ -3,19 +3,18 @@
* @license MIT
*/
import { generateConfig, configEquals } from './CharAtlasUtils';
import { BaseCharAtlas } from './BaseCharAtlas';
import { DynamicCharAtlas } from './DynamicCharAtlas';
import { ICharAtlasConfig } from './Types';
import { generateConfig, configEquals } from 'browser/renderer/shared/CharAtlasUtils';
import { CanvasCharAtlas } from './CanvasCharAtlas';
import { IColorSet } from 'browser/Types';
import { ITerminalOptions } from 'xterm';
import { Terminal } from 'xterm';
import { ICharAtlasConfig } from 'browser/renderer/shared/Types';
interface ICharAtlasCacheEntry {
atlas: BaseCharAtlas;
atlas: CanvasCharAtlas;
config: ICharAtlasConfig;
// N.B. This implementation potentially holds onto copies of the terminal forever, so
// this may cause memory leaks.
ownedBy: number[];
ownedBy: Terminal[];
}
const charAtlasCache: ICharAtlasCacheEntry[] = [];
@@ -25,19 +24,20 @@ const charAtlasCache: ICharAtlasCacheEntry[] = [];
* one that is in use by another terminal.
*/
export function acquireCharAtlas(
options: Required<ITerminalOptions>,
rendererId: number,
terminal: Terminal,
colors: IColorSet,
scaledCellWidth: number,
scaledCellHeight: number,
scaledCharWidth: number,
scaledCharHeight: number,
devicePixelRatio: number
): BaseCharAtlas {
const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, options, colors, devicePixelRatio);
): CanvasCharAtlas {
const newConfig = generateConfig(scaledCellWidth, scaledCellHeight, scaledCharWidth, scaledCharHeight, terminal, colors, devicePixelRatio);
// Check to see if the renderer already owns this config
for (let i = 0; i < charAtlasCache.length; i++) {
const entry = charAtlasCache[i];
const ownedByIndex = entry.ownedBy.indexOf(rendererId);
const ownedByIndex = entry.ownedBy.indexOf(terminal);
if (ownedByIndex >= 0) {
if (configEquals(entry.config, newConfig)) {
return entry.atlas;
@@ -58,18 +58,18 @@ export function acquireCharAtlas(
const entry = charAtlasCache[i];
if (configEquals(entry.config, newConfig)) {
// Add the renderer to the cache entry and return
entry.ownedBy.push(rendererId);
entry.ownedBy.push(terminal);
return entry.atlas;
}
}
const newEntry: ICharAtlasCacheEntry = {
atlas: new DynamicCharAtlas(
atlas: new CanvasCharAtlas(
document,
newConfig
),
config: newConfig,
ownedBy: [rendererId]
ownedBy: [terminal]
};
charAtlasCache.push(newEntry);
return newEntry.atlas;
@@ -78,9 +78,9 @@ export function acquireCharAtlas(
/**
* Removes a terminal reference from the cache, allowing its memory to be freed.
*/
export function removeTerminalFromCache(rendererId: number): void {
export function removeTerminalFromCache(terminal: Terminal): void {
for (let i = 0; i < charAtlasCache.length; i++) {
const index = charAtlasCache[i].ownedBy.indexOf(rendererId);
const index = charAtlasCache[i].ownedBy.indexOf(terminal);
if (index !== -1) {
if (charAtlasCache[i].ownedBy.length === 1) {
// Remove the cache entry if it's the only renderer
@@ -1,54 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ICharAtlasConfig } from './Types';
import { DEFAULT_COLOR } from 'common/buffer/Constants';
import { IColorSet, IPartialColorSet } from 'browser/Types';
import { ITerminalOptions } from 'xterm';
export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, options: Required<ITerminalOptions>, colors: IColorSet, devicePixelRatio: number): ICharAtlasConfig {
// null out some fields that don't matter
const clonedColors: IPartialColorSet = {
foreground: colors.foreground,
background: colors.background,
cursor: undefined,
cursorAccent: undefined,
selectionBackground: undefined,
ansi: colors.ansi.slice()
};
return {
devicePixelRatio,
scaledCharWidth,
scaledCharHeight,
fontFamily: options.fontFamily,
fontSize: options.fontSize,
fontWeight: options.fontWeight,
fontWeightBold: options.fontWeightBold,
allowTransparency: options.allowTransparency,
colors: clonedColors
};
}
export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean {
for (let i = 0; i < a.colors.ansi.length; i++) {
if (a.colors.ansi[i].rgba !== b.colors.ansi[i].rgba) {
return false;
}
}
return a.devicePixelRatio === b.devicePixelRatio &&
a.fontFamily === b.fontFamily &&
a.fontSize === b.fontSize &&
a.fontWeight === b.fontWeight &&
a.fontWeightBold === b.fontWeightBold &&
a.allowTransparency === b.allowTransparency &&
a.scaledCharWidth === b.scaledCharWidth &&
a.scaledCharHeight === b.scaledCharHeight &&
a.colors.foreground.rgba === b.colors.foreground.rgba &&
a.colors.background.rgba === b.colors.background.rgba;
}
export function is256Color(colorCode: number): boolean {
return colorCode < DEFAULT_COLOR;
}
-12
View File
@@ -15,15 +15,3 @@ export interface IGlyphIdentifier {
dim: boolean;
italic: boolean;
}
export interface ICharAtlasConfig {
devicePixelRatio: number;
fontSize: number;
fontFamily: string;
fontWeight: FontWeight;
fontWeightBold: FontWeight;
scaledCharWidth: number;
scaledCharHeight: number;
allowTransparency: boolean;
colors: IPartialColorSet;
}
@@ -12,6 +12,11 @@ declare module 'xterm-addon-canvas' {
export class CanvasAddon implements ITerminalAddon {
public textureAtlas?: HTMLCanvasElement;
/**
* An event that is fired when the texture atlas of the renderer changes.
*/
public readonly onChangeTextureAtlas: IEvent<HTMLCanvasElement>;
constructor();
/**
@@ -3,15 +3,15 @@
* @license MIT
*/
import { createProgram, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils';
import { WebglCharAtlas } from './atlas/WebglCharAtlas';
import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel, IRasterizedGlyph } from './Types';
import { createProgram, PROJECTION_MATRIX } from './WebglUtils';
import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel } from './Types';
import { fill } from 'common/TypedArrayUtils';
import { NULL_CELL_CODE } from 'common/buffer/Constants';
import { Terminal } from 'xterm';
import { IColorSet } from 'browser/Types';
import { IRenderDimensions } from 'browser/renderer/Types';
import { IRasterizedGlyph, IRenderDimensions, ITextureAtlas } from 'browser/renderer/shared/Types';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
interface IVertices {
attributes: Float32Array;
@@ -77,7 +77,7 @@ let $leftCellPadding = 0;
let $clippedPixels = 0;
export class GlyphRenderer extends Disposable {
private _atlas: WebglCharAtlas | undefined;
private _atlas: ITextureAtlas | undefined;
private _program: WebGLProgram;
private _vertexArrayObject: IWebGLVertexArrayObject;
@@ -321,7 +321,7 @@ export class GlyphRenderer extends Disposable {
gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, bufferLength / INDICES_PER_CELL);
}
public setAtlas(atlas: WebglCharAtlas): void {
public setAtlas(atlas: ITextureAtlas): void {
const gl = this._gl;
this._atlas = atlas;
@@ -3,16 +3,17 @@
* @license MIT
*/
import { createProgram, expandFloat32Array, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils';
import { createProgram, expandFloat32Array, PROJECTION_MATRIX } from './WebglUtils';
import { IRenderModel, IWebGLVertexArrayObject, IWebGL2RenderingContext } from './Types';
import { Attributes, BgFlags, FgFlags } from 'common/buffer/Constants';
import { Terminal } from 'xterm';
import { IColor } from 'common/Types';
import { IColorSet } from 'browser/Types';
import { IRenderDimensions } from 'browser/renderer/Types';
import { IRenderDimensions } from 'browser/renderer/shared/Types';
import { RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { DIM_OPACITY } from 'browser/renderer/Constants';
import { DIM_OPACITY } from 'browser/renderer/shared/Constants';
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
const enum VertexAttribLocations {
POSITION = 0,
+4 -21
View File
@@ -3,8 +3,10 @@
* @license MIT
*/
import { IRenderModel, ISelectionRenderModel } from './Types';
import { IRenderModel } from './Types';
import { fill } from 'common/TypedArrayUtils';
import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
import { createSelectionRenderModel } from 'browser/renderer/shared/SelectionRenderModel';
export const RENDER_MODEL_INDICIES_PER_CELL = 4;
export const RENDER_MODEL_BG_OFFSET = 1;
@@ -21,16 +23,7 @@ export class RenderModel implements IRenderModel {
constructor() {
this.cells = new Uint32Array(0);
this.lineLengths = new Uint32Array(0);
this.selection = {
hasSelection: false,
columnSelectMode: false,
viewportStartRow: 0,
viewportEndRow: 0,
viewportCappedStartRow: 0,
viewportCappedEndRow: 0,
startCol: 0,
endCol: 0
};
this.selection = createSelectionRenderModel();
}
public resize(cols: number, rows: number): void {
@@ -45,14 +38,4 @@ export class RenderModel implements IRenderModel {
fill(this.cells, 0, 0);
fill(this.lineLengths, 0, 0);
}
public clearSelection(): void {
this.selection.hasSelection = false;
this.selection.viewportStartRow = 0;
this.selection.viewportEndRow = 0;
this.selection.viewportCappedStartRow = 0;
this.selection.viewportCappedEndRow = 0;
this.selection.startCol = 0;
this.selection.endCol = 0;
}
}
+1 -51
View File
@@ -3,46 +3,7 @@
* @license MIT
*/
/**
* Represents a rasterized glyph within a texture atlas. Some numbers are
* tracked in CSS pixels as well in order to reduce calculations during the
* render loop.
*/
export interface IRasterizedGlyph {
/**
* The x and y offset between the glyph's top/left and the top/left of a cell
* in pixels.
*/
offset: IVector;
/**
* the x and y position of the glyph in the texture in pixels.
*/
texturePosition: IVector;
/**
* the x and y position of the glyph in the texture in clip space coordinates.
*/
texturePositionClipSpace: IVector;
/**
* The width and height of the glyph in the texture in pixels.
*/
size: IVector;
/**
* The width and height of the glyph in the texture in clip space coordinates.
*/
sizeClipSpace: IVector;
}
export interface IVector {
x: number;
y: number;
}
export interface IBoundingBox {
top: number;
left: number;
right: number;
bottom: number;
}
import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
export interface IRenderModel {
cells: Uint32Array;
@@ -50,17 +11,6 @@ export interface IRenderModel {
selection: ISelectionRenderModel;
}
export interface ISelectionRenderModel {
hasSelection: boolean;
columnSelectMode: boolean;
viewportStartRow: number;
viewportEndRow: number;
viewportCappedStartRow: number;
viewportCappedEndRow: number;
startCol: number;
endCol: number;
}
export interface IWebGL2RenderingContext extends WebGLRenderingContext {
vertexAttribDivisor(index: number, divisor: number): void;
createVertexArray(): IWebGLVertexArrayObject;
+39 -193
View File
@@ -3,44 +3,37 @@
* @license MIT
*/
import { GlyphRenderer } from './GlyphRenderer';
import { LinkRenderLayer } from './renderLayer/LinkRenderLayer';
import { CursorRenderLayer } from './renderLayer/CursorRenderLayer';
import { acquireCharAtlas, removeTerminalFromCache } from './atlas/CharAtlasCache';
import { WebglCharAtlas } from './atlas/WebglCharAtlas';
import { RectangleRenderer } from './RectangleRenderer';
import { IWebGL2RenderingContext } from './Types';
import { RenderModel, COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_EXT_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
import { Disposable } from 'common/Lifecycle';
import { Attributes, BgFlags, Content, FgFlags, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
import { Terminal, IEvent } from 'xterm';
import { IRenderLayer } from './renderLayer/Types';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types';
import { observeDevicePixelDimensions } from 'browser/renderer/DevicePixelObserver';
import { ITerminal, IColorSet } from 'browser/Types';
import { EventEmitter, initEvent } from 'common/EventEmitter';
import { CellData } from 'common/buffer/CellData';
import { addDisposableDomListener } from 'browser/Lifecycle';
import { CellColorResolver } from 'browser/renderer/shared/CellColorResolver';
import { acquireTextureAtlas, removeTerminalFromCache } from 'browser/renderer/shared/CharAtlasCache';
import { observeDevicePixelDimensions } from 'browser/renderer/shared/DevicePixelObserver';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent, ITextureAtlas } from 'browser/renderer/shared/Types';
import { ICharacterJoinerService, ICoreBrowserService } from 'browser/services/Services';
import { CharData, IBufferLine, ICellData } from 'common/Types';
import { IColorSet, ITerminal } from 'browser/Types';
import { AttributeData } from 'common/buffer/AttributeData';
import { CellData } from 'common/buffer/CellData';
import { Content, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
import { initEvent } from 'common/EventEmitter';
import { Disposable } from 'common/Lifecycle';
import { ICoreService, IDecorationService } from 'common/services/Services';
// Work variables to avoid garbage collection
let $fg = 0;
let $bg = 0;
let $hasFg = false;
let $hasBg = false;
let $isSelected = false;
import { CharData, IBufferLine, ICellData } from 'common/Types';
import { Terminal } from 'xterm';
import { GlyphRenderer } from './GlyphRenderer';
import { RectangleRenderer } from './RectangleRenderer';
import { CursorRenderLayer } from './renderLayer/CursorRenderLayer';
import { LinkRenderLayer } from './renderLayer/LinkRenderLayer';
import { IRenderLayer } from './renderLayer/Types';
import { COMBINED_CHAR_BIT_MASK, RenderModel, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_EXT_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
import { IWebGL2RenderingContext } from './Types';
export class WebglRenderer extends Disposable implements IRenderer {
private _renderLayers: IRenderLayer[];
private _charAtlas: WebglCharAtlas | undefined;
private _charAtlas: ITextureAtlas | undefined;
private _devicePixelRatio: number;
private _model: RenderModel = new RenderModel();
private _workCell: CellData = new CellData();
private _workColors: { fg: number, bg: number, ext: number } = { fg: 0, bg: 0, ext: 0 };
private _cellColorResolver: CellColorResolver;
private _canvas: HTMLCanvasElement;
private _gl: IWebGL2RenderingContext;
@@ -68,6 +61,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
) {
super();
this._cellColorResolver = new CellColorResolver(this._terminal, this._colors, this._model.selection, this._decorationService, this._coreBrowserService);
this._core = (this._terminal as any)._core;
this._renderLayers = [
@@ -156,6 +151,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
l.reset(this._terminal);
}
this._cellColorResolver.setColors(colors);
this._rectangleRenderer.setColors();
this._refreshCharAtlas();
@@ -230,7 +226,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
for (const l of this._renderLayers) {
l.onSelectionChanged(this._terminal, start, end, columnSelectMode);
}
this._updateSelectionModel(start, end, columnSelectMode);
this._model.selection.update(this._terminal, start, end, columnSelectMode);
this._requestRedrawViewport();
}
@@ -275,10 +271,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
return;
}
const atlas = acquireCharAtlas(this._terminal, this._colors, this.dimensions.scaledCellWidth, this.dimensions.scaledCellHeight, this.dimensions.scaledCharWidth, this.dimensions.scaledCharHeight, this._coreBrowserService.dpr);
if (!('getRasterizedGlyph' in atlas)) {
throw new Error('The webgl renderer only works with the webgl char atlas');
}
const atlas = acquireTextureAtlas(this._terminal, this._colors, this.dimensions.scaledCellWidth, this.dimensions.scaledCellHeight, this.dimensions.scaledCharWidth, this.dimensions.scaledCharHeight, this._coreBrowserService.dpr);
if (this._charAtlas !== atlas) {
this.onChangeTextureAtlas.fire(atlas.cacheCanvas);
}
@@ -340,7 +333,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
// Tell renderer the frame is beginning
if (this._glyphRenderer.beginFrame()) {
this._clearModel(true);
this._updateSelectionModel(undefined, undefined);
this._model.selection.clear();
}
// Update model to reflect what's drawn
@@ -376,11 +369,11 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._model.lineLengths[y] = 0;
joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
for (x = 0; x < terminal.cols; x++) {
lastBg = this._workColors.bg;
lastBg = this._cellColorResolver.result.bg;
line.loadCell(x, cell);
if (x === 0) {
lastBg = this._workColors.bg;
lastBg = this._cellColorResolver.result.bg;
}
// If true, indicates that the current character(s) to draw were joined.
@@ -411,7 +404,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
// Load colors/resolve overrides into work colors
this._loadColorsForCell(x, row);
this._cellColorResolver.resolve(cell, x, row);
if (code !== NULL_CELL_CODE) {
this._model.lineLengths[y] = x + 1;
@@ -419,9 +412,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
// Nothing has changed, no updates needed
if (this._model.cells[i] === code &&
this._model.cells[i + RENDER_MODEL_BG_OFFSET] === this._workColors.bg &&
this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._workColors.fg &&
this._model.cells[i + RENDER_MODEL_EXT_OFFSET] === this._workColors.ext) {
this._model.cells[i + RENDER_MODEL_BG_OFFSET] === this._cellColorResolver.result.bg &&
this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._cellColorResolver.result.fg &&
this._model.cells[i + RENDER_MODEL_EXT_OFFSET] === this._cellColorResolver.result.ext) {
continue;
}
@@ -432,11 +425,11 @@ export class WebglRenderer extends Disposable implements IRenderer {
// Cache the results in the model
this._model.cells[i] = code;
this._model.cells[i + RENDER_MODEL_BG_OFFSET] = this._workColors.bg;
this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._workColors.fg;
this._model.cells[i + RENDER_MODEL_EXT_OFFSET] = this._workColors.ext;
this._model.cells[i + RENDER_MODEL_BG_OFFSET] = this._cellColorResolver.result.bg;
this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg;
this._model.cells[i + RENDER_MODEL_EXT_OFFSET] = this._cellColorResolver.result.ext;
this._glyphRenderer.updateCell(x, y, code, this._workColors.bg, this._workColors.fg, this._workColors.ext, chars, lastBg);
this._glyphRenderer.updateCell(x, y, code, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, chars, lastBg);
if (isJoined) {
// Restore work cell
@@ -447,9 +440,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
j = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
this._glyphRenderer.updateCell(x, y, NULL_CELL_CODE, 0, 0, 0, NULL_CELL_CHAR, 0);
this._model.cells[j] = NULL_CELL_CODE;
this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._workColors.bg;
this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._workColors.fg;
this._model.cells[j + RENDER_MODEL_EXT_OFFSET] = this._workColors.ext;
this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._cellColorResolver.result.bg;
this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg;
this._model.cells[j + RENDER_MODEL_EXT_OFFSET] = this._cellColorResolver.result.ext;
}
}
}
@@ -457,153 +450,6 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._rectangleRenderer.updateBackgrounds(this._model);
}
/**
* Loads colors for the cell into the work colors object. This resolves overrides/inverse if
* necessary which is why the work cell object is not used.
*/
private _loadColorsForCell(x: number, y: number): void {
this._workColors.bg = this._workCell.bg;
this._workColors.fg = this._workCell.fg;
this._workColors.ext = this._workCell.bg & BgFlags.HAS_EXTENDED ? this._workCell.extended.ext : 0;
// Get any foreground/background overrides, this happens on the model to avoid spreading
// override logic throughout the different sub-renderers
// Reset overrides work variables
$bg = 0;
$fg = 0;
$hasBg = false;
$hasFg = false;
$isSelected = false;
// Apply decorations on the bottom layer
this._decorationService.forEachDecorationAtCell(x, y, 'bottom', d => {
if (d.backgroundColorRGB) {
$bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
$hasBg = true;
}
if (d.foregroundColorRGB) {
$fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
$hasFg = true;
}
});
// Apply the selection color if needed
$isSelected = this._isCellSelected(x, y);
if ($isSelected) {
$bg = (this._coreBrowserService.isFocused ? this._colors.selectionBackgroundOpaque : this._colors.selectionInactiveBackgroundOpaque).rgba >> 8 & 0xFFFFFF;
$hasBg = true;
if (this._colors.selectionForeground) {
$fg = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
$hasFg = true;
}
}
// Apply decorations on the top layer
this._decorationService.forEachDecorationAtCell(x, y, 'top', d => {
if (d.backgroundColorRGB) {
$bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
$hasBg = true;
}
if (d.foregroundColorRGB) {
$fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
$hasFg = true;
}
});
// Convert any overrides from rgba to the fg/bg packed format. This resolves the inverse flag
// ahead of time in order to use the correct cache key
if ($hasBg) {
if ($isSelected) {
// Non-RGB attributes from model + force non-dim + override + force RGB color mode
$bg = (this._workCell.bg & ~Attributes.RGB_MASK & ~BgFlags.DIM) | $bg | Attributes.CM_RGB;
} else {
// Non-RGB attributes from model + override + force RGB color mode
$bg = (this._workCell.bg & ~Attributes.RGB_MASK) | $bg | Attributes.CM_RGB;
}
}
if ($hasFg) {
// Non-RGB attributes from model + force disable inverse + override + force RGB color mode
$fg = (this._workCell.fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | $fg | Attributes.CM_RGB;
}
// Handle case where inverse was specified by only one of bg override or fg override was set,
// resolving the other inverse color and setting the inverse flag if needed.
if (this._workColors.fg & FgFlags.INVERSE) {
if ($hasBg && !$hasFg) {
// Resolve bg color type (default color has a different meaning in fg vs bg)
if ((this._workColors.bg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
$fg = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | ((this._colors.background.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
} else {
$fg = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | this._workColors.bg & (Attributes.RGB_MASK | Attributes.CM_MASK);
}
$hasFg = true;
}
if (!$hasBg && $hasFg) {
// Resolve bg color type (default color has a different meaning in fg vs bg)
if ((this._workColors.fg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
$bg = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | ((this._colors.foreground.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
} else {
$bg = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | this._workColors.fg & (Attributes.RGB_MASK | Attributes.CM_MASK);
}
$hasBg = true;
}
}
// Use the override if it exists
this._workColors.bg = $hasBg ? $bg : this._workColors.bg;
this._workColors.fg = $hasFg ? $fg : this._workColors.fg;
}
private _isCellSelected(x: number, y: number): boolean {
if (!this._model.selection.hasSelection) {
return false;
}
y -= this._terminal.buffer.active.viewportY;
if (this._model.selection.columnSelectMode) {
if (this._model.selection.startCol <= this._model.selection.endCol) {
return x >= this._model.selection.startCol && y >= this._model.selection.viewportCappedStartRow &&
x < this._model.selection.endCol && y <= this._model.selection.viewportCappedEndRow;
}
return x < this._model.selection.startCol && y >= this._model.selection.viewportCappedStartRow &&
x >= this._model.selection.endCol && y <= this._model.selection.viewportCappedEndRow;
}
return (y > this._model.selection.viewportStartRow && y < this._model.selection.viewportEndRow) ||
(this._model.selection.viewportStartRow === this._model.selection.viewportEndRow && y === this._model.selection.viewportStartRow && x >= this._model.selection.startCol && x < this._model.selection.endCol) ||
(this._model.selection.viewportStartRow < this._model.selection.viewportEndRow && y === this._model.selection.viewportEndRow && x < this._model.selection.endCol) ||
(this._model.selection.viewportStartRow < this._model.selection.viewportEndRow && y === this._model.selection.viewportStartRow && x >= this._model.selection.startCol);
}
private _updateSelectionModel(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
const terminal = this._terminal;
// Selection does not exist
if (!start || !end || (start[0] === end[0] && start[1] === end[1])) {
this._model.clearSelection();
return;
}
// Translate from buffer position to viewport position
const viewportStartRow = start[1] - terminal.buffer.active.viewportY;
const viewportEndRow = end[1] - terminal.buffer.active.viewportY;
const viewportCappedStartRow = Math.max(viewportStartRow, 0);
const viewportCappedEndRow = Math.min(viewportEndRow, terminal.rows - 1);
// No need to draw the selection
if (viewportCappedStartRow >= terminal.rows || viewportCappedEndRow < 0) {
this._model.clearSelection();
return;
}
this._model.selection.hasSelection = true;
this._model.selection.columnSelectMode = columnSelectMode;
this._model.selection.viewportStartRow = viewportStartRow;
this._model.selection.viewportEndRow = viewportEndRow;
this._model.selection.viewportCappedStartRow = viewportCappedStartRow;
this._model.selection.viewportCappedEndRow = viewportCappedEndRow;
this._model.selection.startCol = start[0];
this._model.selection.endCol = end[0];
}
/**
* Recalculates the character and canvas dimensions.
*/
+2 -7
View File
@@ -3,6 +3,8 @@
* @license MIT
*/
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
/**
* A matrix that when multiplies will translate 0-1 coordinates (left to right,
* top to bottom) to clip space.
@@ -49,10 +51,3 @@ export function expandFloat32Array(source: Float32Array, max: number): Float32Ar
}
return newArray;
}
export function throwIfFalsy<T>(value: T | undefined | null): T {
if (!value) {
throw new Error('value must not be falsy');
}
return value;
}
-36
View File
@@ -1,36 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { FontWeight } from 'xterm';
import { IColorSet } from 'browser/Types';
export interface IGlyphIdentifier {
chars: string;
code: number;
bg: number;
fg: number;
bold: boolean;
dim: boolean;
italic: boolean;
}
export interface ICharAtlasConfig {
customGlyphs: boolean;
devicePixelRatio: number;
letterSpacing: number;
lineHeight: number;
fontSize: number;
fontFamily: string;
fontWeight: FontWeight;
fontWeightBold: FontWeight;
scaledCellWidth: number;
scaledCellHeight: number;
scaledCharWidth: number;
scaledCharHeight: number;
allowTransparency: boolean;
drawBoldTextInBrightColors: boolean;
minimumContrastRatio: number;
colors: IColorSet;
}
@@ -4,15 +4,14 @@
*/
import { IRenderLayer } from './Types';
import { acquireCharAtlas } from '../atlas/CharAtlasCache';
import { acquireTextureAtlas } from 'browser/renderer/shared/CharAtlasCache';
import { Terminal } from 'xterm';
import { IColorSet } from 'browser/Types';
import { TEXT_BASELINE } from 'browser/renderer/Constants';
import { TEXT_BASELINE } from 'browser/renderer/shared/Constants';
import { ICoreBrowserService } from 'browser/services/Services';
import { IRenderDimensions } from 'browser/renderer/Types';
import { IRenderDimensions, ITextureAtlas } from 'browser/renderer/shared/Types';
import { CellData } from 'common/buffer/CellData';
import { WebglCharAtlas } from 'atlas/WebglCharAtlas';
import { throwIfFalsy } from '../WebglUtils';
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
export abstract class BaseRenderLayer implements IRenderLayer {
private _canvas: HTMLCanvasElement;
@@ -24,7 +23,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
private _scaledCharLeft: number = 0;
private _scaledCharTop: number = 0;
protected _charAtlas: WebglCharAtlas | undefined;
protected _charAtlas: ITextureAtlas | undefined;
constructor(
private _container: HTMLElement,
@@ -95,7 +94,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) {
return;
}
this._charAtlas = acquireCharAtlas(terminal, colorSet, this._scaledCellWidth, this._scaledCellHeight, this._scaledCharWidth, this._scaledCharHeight, this._coreBrowserService.dpr);
this._charAtlas = acquireTextureAtlas(terminal, colorSet, this._scaledCellWidth, this._scaledCellHeight, this._scaledCharWidth, this._scaledCharHeight, this._coreBrowserService.dpr);
this._charAtlas.warmUp();
}
@@ -8,7 +8,7 @@ import { BaseRenderLayer } from './BaseRenderLayer';
import { ICellData } from 'common/Types';
import { CellData } from 'common/buffer/CellData';
import { IColorSet } from 'browser/Types';
import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types';
import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
import { IEventEmitter } from 'common/EventEmitter';
import { ICoreBrowserService } from 'browser/services/Services';
import { ICoreService } from 'common/services/Services';
@@ -5,11 +5,11 @@
import { Terminal } from 'xterm';
import { BaseRenderLayer } from './BaseRenderLayer';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants';
import { is256Color } from '../atlas/CharAtlasUtils';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
import { ITerminal, IColorSet, ILinkifierEvent } from 'browser/Types';
import { IRenderDimensions } from 'browser/renderer/Types';
import { IRenderDimensions } from 'browser/renderer/shared/Types';
import { ICoreBrowserService } from 'browser/services/Services';
import { is256Color } from 'browser/renderer/shared/CharAtlasUtils';
export class LinkRenderLayer extends BaseRenderLayer {
private _state: ILinkifierEvent | undefined;
@@ -5,7 +5,7 @@
import { IDisposable, Terminal } from 'xterm';
import { IColorSet } from 'browser/Types';
import { IRenderDimensions } from 'browser/renderer/Types';
import { IRenderDimensions } from 'browser/renderer/shared/Types';
export interface IRenderLayer extends IDisposable {
/**
+135 -63
View File
@@ -36,15 +36,15 @@ import { Terminal as TerminalType, ITerminalOptions } from 'xterm';
export interface IWindowWithTerminal extends Window {
term: TerminalType;
Terminal?: typeof TerminalType;
AttachAddon?: typeof AttachAddon;
FitAddon?: typeof FitAddon;
SearchAddon?: typeof SearchAddon;
SerializeAddon?: typeof SerializeAddon;
WebLinksAddon?: typeof WebLinksAddon;
WebglAddon?: typeof WebglAddon;
Unicode11Addon?: typeof Unicode11Addon;
LigaturesAddon?: typeof LigaturesAddon;
Terminal?: typeof TerminalType; // eslint-disable-line @typescript-eslint/naming-convention
AttachAddon?: typeof AttachAddon; // eslint-disable-line @typescript-eslint/naming-convention
FitAddon?: typeof FitAddon; // eslint-disable-line @typescript-eslint/naming-convention
SearchAddon?: typeof SearchAddon; // eslint-disable-line @typescript-eslint/naming-convention
SerializeAddon?: typeof SerializeAddon; // eslint-disable-line @typescript-eslint/naming-convention
WebLinksAddon?: typeof WebLinksAddon; // eslint-disable-line @typescript-eslint/naming-convention
WebglAddon?: typeof WebglAddon; // eslint-disable-line @typescript-eslint/naming-convention
Unicode11Addon?: typeof Unicode11Addon; // eslint-disable-line @typescript-eslint/naming-convention
LigaturesAddon?: typeof LigaturesAddon; // eslint-disable-line @typescript-eslint/naming-convention
}
declare let window: IWindowWithTerminal;
@@ -59,27 +59,29 @@ type AddonType = 'attach' | 'canvas' | 'fit' | 'search' | 'serialize' | 'unicode
interface IDemoAddon<T extends AddonType> {
name: T;
canChange: boolean;
ctor:
ctor: (
T extends 'attach' ? typeof AttachAddon :
T extends 'canvas' ? typeof CanvasAddon :
T extends 'fit' ? typeof FitAddon :
T extends 'search' ? typeof SearchAddon :
T extends 'serialize' ? typeof SerializeAddon :
T extends 'web-links' ? typeof WebLinksAddon :
T extends 'unicode11' ? typeof Unicode11Addon :
T extends 'ligatures' ? typeof LigaturesAddon :
typeof WebglAddon;
instance?:
T extends 'canvas' ? typeof CanvasAddon :
T extends 'fit' ? typeof FitAddon :
T extends 'search' ? typeof SearchAddon :
T extends 'serialize' ? typeof SerializeAddon :
T extends 'web-links' ? typeof WebLinksAddon :
T extends 'unicode11' ? typeof Unicode11Addon :
T extends 'ligatures' ? typeof LigaturesAddon :
typeof WebglAddon
);
instance?: (
T extends 'attach' ? AttachAddon :
T extends 'canvas' ? CanvasAddon :
T extends 'fit' ? FitAddon :
T extends 'search' ? SearchAddon :
T extends 'serialize' ? SerializeAddon :
T extends 'web-links' ? WebLinksAddon :
T extends 'webgl' ? WebglAddon :
T extends 'unicode11' ? typeof Unicode11Addon :
T extends 'ligatures' ? typeof LigaturesAddon :
never;
T extends 'canvas' ? CanvasAddon :
T extends 'fit' ? FitAddon :
T extends 'search' ? SearchAddon :
T extends 'serialize' ? SerializeAddon :
T extends 'web-links' ? WebLinksAddon :
T extends 'webgl' ? WebglAddon :
T extends 'unicode11' ? typeof Unicode11Addon :
T extends 'ligatures' ? typeof LigaturesAddon :
never
);
}
const addons: { [T in AddonType]: IDemoAddon<T> } = {
@@ -96,12 +98,12 @@ const addons: { [T in AddonType]: IDemoAddon<T> } = {
let terminalContainer = document.getElementById('terminal-container');
const actionElements = {
find: <HTMLInputElement>document.querySelector('#find'),
findNext: <HTMLInputElement>document.querySelector('#find-next'),
findPrevious: <HTMLInputElement>document.querySelector('#find-previous'),
find: document.querySelector('#find') as HTMLInputElement,
findNext: document.querySelector('#find-next') as HTMLInputElement,
findPrevious: document.querySelector('#find-previous') as HTMLInputElement,
findResults: document.querySelector('#find-results')
};
const paddingElement = <HTMLInputElement>document.getElementById('padding');
const paddingElement = document.getElementById('padding') as HTMLInputElement;
const xtermjsTheme = {
foreground: '#F8F8F8',
@@ -146,7 +148,7 @@ function getSearchOptions(e: KeyboardEvent): ISearchOptions {
};
}
const disposeRecreateButtonHandler = () => {
const disposeRecreateButtonHandler: () => void = () => {
// If the terminal exists dispose of it, otherwise recreate it
if (term) {
term.dispose();
@@ -169,7 +171,7 @@ const disposeRecreateButtonHandler = () => {
}
};
const createNewWindowButtonHandler = () => {
const createNewWindowButtonHandler: () => void = () => {
if (term) {
disposeRecreateButtonHandler();
}
@@ -196,7 +198,7 @@ const createNewWindowButtonHandler = () => {
}
});
}
}
};
if (document.location.pathname === '/test') {
window.Terminal = Terminal;
@@ -220,6 +222,7 @@ if (document.location.pathname === '/test') {
document.getElementById('underline-test').addEventListener('click', underlineTest);
document.getElementById('ansi-colors').addEventListener('click', ansiColorsTest);
document.getElementById('osc-hyperlinks').addEventListener('click', addAnsiHyperlink);
document.getElementById('sgr-test').addEventListener('click', sgrTest);
document.getElementById('add-decoration').addEventListener('click', addDecoration);
document.getElementById('add-overview-ruler').addEventListener('click', addOverviewRuler);
}
@@ -300,8 +303,8 @@ function createTerminal(): void {
setTimeout(() => {
initOptions(term);
// TODO: Clean this up, opt-cols/rows doesn't exist anymore
(<HTMLInputElement>document.getElementById(`opt-cols`)).value = term.cols;
(<HTMLInputElement>document.getElementById(`opt-rows`)).value = term.rows;
(document.getElementById(`opt-cols`) as HTMLInputElement).value = term.cols;
(document.getElementById(`opt-rows`) as HTMLInputElement).value = term.rows;
paddingElement.value = '0';
// Set terminal size again to set the specific dimensions on the demo
@@ -427,14 +430,14 @@ function initOptions(term: TerminalType): void {
// Attach listeners
booleanOptions.forEach(o => {
const input = <HTMLInputElement>document.getElementById(`opt-${o}`);
const input = document.getElementById(`opt-${o}`) as HTMLInputElement;
addDomListener(input, 'change', () => {
console.log('change', o, input.checked);
term.options[o] = input.checked;
});
});
numberOptions.forEach(o => {
const input = <HTMLInputElement>document.getElementById(`opt-${o}`);
const input = document.getElementById(`opt-${o}`) as HTMLInputElement;
addDomListener(input, 'change', () => {
console.log('change', o, input.value);
if (o === 'rows') {
@@ -456,7 +459,7 @@ function initOptions(term: TerminalType): void {
});
});
Object.keys(stringOptions).forEach(o => {
const input = <HTMLInputElement>document.getElementById(`opt-${o}`);
const input = document.getElementById(`opt-${o}`) as HTMLInputElement;
addDomListener(input, 'change', () => {
console.log('change', o, input.value);
let value: any = input.value;
@@ -489,7 +492,7 @@ function initOptions(term: TerminalType): void {
magenta: '#b168df',
red: '#da6771',
white: '#efefef',
yellow: '#fff099',
yellow: '#fff099'
};
break;
case 'light':
@@ -513,7 +516,7 @@ function initOptions(term: TerminalType): void {
magenta: '#bc05bc',
red: '#cd3131',
white: '#555555',
yellow: '#949800',
yellow: '#949800'
};
break;
}
@@ -545,7 +548,15 @@ function initAddons(term: TerminalType): void {
try {
term.loadAddon(addon.instance);
if (name === 'webgl') {
(addon.instance as WebglAddon).onChangeTextureAtlas(e => addTextureAtlas(e));
setTimeout(() => {
addTextureAtlas(addons.webgl.instance.textureAtlas);
addons.webgl.instance.onChangeTextureAtlas(e => addTextureAtlas(e));
}, 0);
} else if (name === 'canvas') {
setTimeout(() => {
addTextureAtlas(addons.canvas.instance.textureAtlas);
addons.canvas.instance.onChangeTextureAtlas(e => addTextureAtlas(e));
}, 0);
} else if (name === 'unicode11') {
term.unicode.activeVersion = '11';
} else if (name === 'search') {
@@ -559,7 +570,9 @@ function initAddons(term: TerminalType): void {
}
} else {
if (name === 'webgl') {
(addon.instance as WebglAddon).textureAtlas.remove();
addons.webgl.instance.textureAtlas.remove();
} else if (name === 'canvas') {
addons.canvas.instance.textureAtlas.remove();
} else if (name === 'unicode11') {
term.unicode.activeVersion = '6';
}
@@ -584,7 +597,7 @@ function initAddons(term: TerminalType): void {
container.appendChild(fragment);
}
function updateFindResults(e: { resultIndex: number, resultCount: number } | undefined) {
function updateFindResults(e: { resultIndex: number, resultCount: number } | undefined): void {
let content: string;
if (e === undefined) {
content = 'undefined';
@@ -600,8 +613,8 @@ function addDomListener(element: HTMLElement, type: string, handler: (...args: a
}
function updateTerminalSize(): void {
const cols = parseInt((<HTMLInputElement>document.getElementById(`opt-cols`)).value, 10);
const rows = parseInt((<HTMLInputElement>document.getElementById(`opt-rows`)).value, 10);
const cols = parseInt((document.getElementById(`opt-cols`) as HTMLInputElement).value, 10);
const rows = parseInt((document.getElementById(`opt-rows`) as HTMLInputElement).value, 10);
const width = (cols * term._core._renderService.dimensions.actualCellWidth + term._core.viewport.scrollBarWidth).toString() + 'px';
const height = (rows * term._core._renderService.dimensions.actualCellHeight).toString() + 'px';
terminalContainer.style.width = width;
@@ -625,21 +638,21 @@ function htmlSerializeButtonHandler(): void {
document.getElementById('htmlserialize-output').innerText = output;
// Deprecated, but the most supported for now.
function listener(e: any) {
e.clipboardData.setData("text/html", output);
function listener(e: any): void {
e.clipboardData.setData('text/html', output);
e.preventDefault();
}
document.addEventListener("copy", listener);
document.execCommand("copy");
document.removeEventListener("copy", listener);
document.getElementById("htmlserialize-output-result").innerText = "Copied to clipboard";
document.addEventListener('copy', listener);
document.execCommand('copy');
document.removeEventListener('copy', listener);
document.getElementById('htmlserialize-output-result').innerText = 'Copied to clipboard';
}
function addTextureAtlas(e: HTMLCanvasElement) {
function addTextureAtlas(e: HTMLCanvasElement): void {
document.querySelector('#texture-atlas').replaceChildren(e);
}
function writeCustomGlyphHandler() {
function writeCustomGlyphHandler(): void {
term.write('\n\r');
term.write('\n\r');
term.write('Box styles: ┎┰┒┍┯┑╓╥╖╒╤╕ ┏┳┓┌┲┓┌┬┐┏┱┐\n\r');
@@ -684,7 +697,7 @@ function writeCustomGlyphHandler() {
window.scrollTo(0, 0);
}
function loadTest() {
function loadTest(): void {
const rendererName = addons.webgl.instance ? 'webgl' : !!addons.canvas.instance ? 'canvas' : 'dom';
const testData = [];
let byteCount = 0;
@@ -717,7 +730,7 @@ function loadTest() {
});
}
function powerlineSymbolTest() {
function powerlineSymbolTest(): void {
function s(char: string): string {
return `${char} \x1b[7m${char}\x1b[0m `;
}
@@ -791,7 +804,7 @@ function powerlineSymbolTest() {
term.writeln('nf-mdi-github_face (\\uFbd9) \ufbd9');
}
function underlineTest() {
function underlineTest(): void {
function u(style: number): string {
return `\x1b[4:${style}m`;
}
@@ -801,7 +814,7 @@ function underlineTest() {
term.write('\n\n\r');
term.writeln('Underline styles:');
term.writeln('');
function showSequence(id: number, name: string) {
function showSequence(id: number, name: string): string {
let alphabet = '';
for (let i = 97; i < 123; i++) {
alphabet += String.fromCharCode(i);
@@ -848,7 +861,7 @@ function underlineTest() {
term.write('\x1b[0m\n\r');
}
function ansiColorsTest() {
function ansiColorsTest(): void {
term.writeln(`\x1b[0m\n\n\rStandard colors: Bright colors:`);
for (let i = 0; i < 16; i++) {
term.write(`\x1b[48;5;${i}m ${i.toString().padEnd(2, ' ').padStart(3, ' ')} \x1b[0m`);
@@ -873,7 +886,66 @@ function ansiColorsTest() {
}
}
function addAnsiHyperlink() {
function writeTestString(): string {
let alphabet = '';
for (let i = 97; i < 123; i++) {
alphabet += String.fromCharCode(i);
}
let numbers = '';
for (let i = 0; i < 10; i++) {
numbers += i.toString();
}
return `${alphabet} ${numbers} 汉语 한국어 👽`;
}
const testString = writeTestString();
function sgrTest(): void {
term.write('\n\n\r');
term.writeln(`Character Attributes (SGR, Select Graphic Rendition)`);
const entries: { ps: number, name: string }[] = [
{ ps: 0, name: 'Normal' },
{ ps: 1, name: 'Bold' },
{ ps: 2, name: 'Faint/dim' },
{ ps: 3, name: 'Italicized' },
{ ps: 4, name: 'Underlined' },
{ ps: 5, name: 'Blink' },
{ ps: 7, name: 'Inverse' },
{ ps: 8, name: 'Invisible' },
{ ps: 9, name: 'Crossed-out characters' },
{ ps: 21, name: 'Doubly-underlined' },
{ ps: 22, name: 'Normal' },
{ ps: 23, name: 'Not italicized' },
{ ps: 24, name: 'Not underlined' },
{ ps: 25, name: 'Steady (not blink)' },
{ ps: 27, name: 'Positive (not inverse)' },
{ ps: 28, name: 'Visible (not hidden)' },
{ ps: 29, name: 'Not crossed-out' },
{ ps: 30, name: 'Foreground Black' },
{ ps: 31, name: 'Foreground Red' },
{ ps: 32, name: 'Foreground Green' },
{ ps: 33, name: 'Foreground Yellow' },
{ ps: 34, name: 'Foreground Blue' },
{ ps: 35, name: 'Foreground Magenta' },
{ ps: 36, name: 'Foreground Cyan' },
{ ps: 37, name: 'Foreground White' },
{ ps: 39, name: 'Foreground default' },
{ ps: 40, name: 'Background Black' },
{ ps: 41, name: 'Background Red' },
{ ps: 42, name: 'Background Green' },
{ ps: 43, name: 'Background Yellow' },
{ ps: 44, name: 'Background Blue' },
{ ps: 45, name: 'Background Magenta' },
{ ps: 46, name: 'Background Cyan' },
{ ps: 47, name: 'Background White' },
{ ps: 49, name: 'Background default' }
];
const maxNameLength = entries.reduce<number>((p, c) => Math.max(c.name.length, p), 0);
for (const e of entries) {
term.writeln(`\x1b[0m\x1b[${e.ps}m ${e.ps.toString().padEnd(2, ' ')} ${e.name.padEnd(maxNameLength, ' ')} - ${testString}\x1b[0m`);
}
}
function addAnsiHyperlink(): void {
term.write('\n\n\r');
term.writeln(`Regular link with no id:`);
term.writeln('\x1b]8;;https://github.com\x07GitHub\x1b]8;;\x07');
@@ -893,7 +965,7 @@ function addAnsiHyperlink() {
term.write('\x1b[3A\x1b[1C\x1b]8;;https://xtermjs.org\x07xter\x1b[B\x1b[4Dm.js\x1b]8;;\x07\x1b[2B\x1b[5D');
}
function addDecoration() {
function addDecoration(): void {
term.options['overviewRulerWidth'] = 15;
const marker = term.registerMarker(1);
const decoration = term.registerDecoration({
@@ -908,7 +980,7 @@ function addDecoration() {
});
}
function addOverviewRuler() {
function addOverviewRuler(): void {
term.options['overviewRulerWidth'] = 15;
term.registerDecoration({ marker: term.registerMarker(1), overviewRulerOptions: { color: '#ef2929' } });
term.registerDecoration({ marker: term.registerMarker(3), overviewRulerOptions: { color: '#8ae234' } });
+1
View File
@@ -79,6 +79,7 @@
<dd><button id="custom-glyph" title="Write custom box drawing and block element characters to the terminal">Test custom glyphs</button></dd>
<dd><button id="powerline-symbol-test" title="Write powerline symbol characters to the terminal (\ue0a0+)">Powerline symbol test</button></dd>
<dd><button id="underline-test" title="Write text with Kitty's extended underline sequences">Underline test</button></dd>
<dd><button id="sgr-test" title="Write text with SGR attribute">SGR test</button></dd>
<dd><button id="ansi-colors" title="Write a wide range of ansi colors">Ansi colors test</button></dd>
<dd><button id="osc-hyperlinks" title="Write some OSC 8 hyperlinks">Ansi hyperlinks test</button></dd>
+3 -7
View File
@@ -5,7 +5,7 @@
import { IColorManager, IColorSet, IColorContrastCache } from 'browser/Types';
import { ITheme } from 'common/services/Services';
import { channels, color, css } from 'common/Color';
import { channels, color, css, NULL_COLOR } from 'common/Color';
import { ColorContrastCache } from 'browser/ColorContrastCache';
import { ColorIndex, IColor } from 'common/Types';
@@ -124,12 +124,8 @@ export class ColorManager implements IColorManager {
this.colors.selectionBackgroundOpaque = color.blend(this.colors.background, this.colors.selectionBackgroundTransparent);
this.colors.selectionInactiveBackgroundTransparent = this._parseColor(theme.selectionInactiveBackground, this.colors.selectionBackgroundTransparent);
this.colors.selectionInactiveBackgroundOpaque = color.blend(this.colors.background, this.colors.selectionInactiveBackgroundTransparent);
const nullColor: IColor = {
css: '',
rgba: 0
};
this.colors.selectionForeground = theme.selectionForeground ? this._parseColor(theme.selectionForeground, nullColor) : undefined;
if (this.colors.selectionForeground === nullColor) {
this.colors.selectionForeground = theme.selectionForeground ? this._parseColor(theme.selectionForeground, NULL_COLOR) : undefined;
if (this.colors.selectionForeground === NULL_COLOR) {
this.colors.selectionForeground = undefined;
}
+1 -1
View File
@@ -22,7 +22,7 @@
*/
import { ICompositionHelper, ITerminal, IBrowser, CustomKeyEventHandler, IViewport, ILinkifier2, CharacterJoinerHandler, IBufferRange } from 'browser/Types';
import { IRenderer } from 'browser/renderer/Types';
import { IRenderer } from 'browser/renderer/shared/Types';
import { CompositionHelper } from 'browser/input/CompositionHelper';
import { Viewport } from 'browser/Viewport';
import { rightClickHandler, moveTextAreaUnderMouseCursor, handlePasteEvent, copyHandler, paste } from 'browser/Clipboard';
+1 -1
View File
@@ -6,7 +6,7 @@
import { IDisposable, IMarker, ILinkProvider, IDecorationOptions, IDecoration } from 'xterm';
import { IEvent, EventEmitter, initEvent } from 'common/EventEmitter';
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
import { IColorSet, ITerminal, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper, CharacterJoinerHandler, IBufferRange } from 'browser/Types';
import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types';
import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, ITerminalOptions } from 'common/Types';
+1 -1
View File
@@ -9,7 +9,7 @@ import { IColorSet, IViewport } from 'browser/Types';
import { ICharSizeService, ICoreBrowserService, IRenderService } from 'browser/services/Services';
import { IBufferService, IOptionsService } from 'common/services/Services';
import { IBuffer } from 'common/buffer/Types';
import { IRenderDimensions } from 'browser/renderer/Types';
import { IRenderDimensions } from 'browser/renderer/shared/Types';
const FALLBACK_SCROLL_BAR_WIDTH = 15;
-56
View File
@@ -1,56 +0,0 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IDisposable } from 'common/Types';
import { IColorSet } from 'browser/Types';
import { IEvent } from 'common/EventEmitter';
export interface IRenderDimensions {
scaledCharWidth: number;
scaledCharHeight: number;
scaledCellWidth: number;
scaledCellHeight: number;
scaledCharLeft: number;
scaledCharTop: number;
scaledCanvasWidth: number;
scaledCanvasHeight: number;
canvasWidth: number;
canvasHeight: number;
actualCellWidth: number;
actualCellHeight: number;
}
export interface IRequestRedrawEvent {
start: number;
end: number;
}
/**
* Note that IRenderer implementations should emit the refresh event after
* rendering rows to the screen.
*/
export interface IRenderer extends IDisposable {
readonly dimensions: IRenderDimensions;
/**
* Fires when the renderer is requesting to be redrawn on the next animation
* frame but is _not_ a result of content changing (eg. selection changes).
*/
readonly onRequestRedraw: IEvent<IRequestRedrawEvent>;
dispose(): void;
setColors(colors: IColorSet): void;
onDevicePixelRatioChange(): void;
onResize(cols: number, rows: number): void;
onCharSizeChanged(): void;
onBlur(): void;
onFocus(): void;
onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
onCursorMove(): void;
onOptionsChanged(): void;
clear(): void;
renderRows(start: number, end: number): void;
clearTextureAtlas?(): void;
}
+2 -2
View File
@@ -3,9 +3,9 @@
* @license MIT
*/
import { IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types';
import { IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_BLINK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from 'browser/renderer/dom/DomRendererRowFactory';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
import { Disposable } from 'common/Lifecycle';
import { IColorSet, ILinkifierEvent, ILinkifier2 } from 'browser/Types';
import { ICharSizeService, ICoreBrowserService } from 'browser/services/Services';
@@ -4,7 +4,7 @@
*/
import { IBufferLine, ICellData, IColor } from 'common/Types';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants';
import { CellData } from 'common/buffer/CellData';
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
@@ -12,7 +12,7 @@ import { color, rgba } from 'common/Color';
import { IColorSet } from 'browser/Types';
import { ICharacterJoinerService, ICoreBrowserService } from 'browser/services/Services';
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
import { excludeFromContrastRatioDemands } from 'browser/renderer/RendererUtils';
import { excludeFromContrastRatioDemands } from 'browser/renderer/shared/RendererUtils';
import { AttributeData } from 'common/buffer/AttributeData';
export const BOLD_CLASS = 'xterm-bold';
@@ -0,0 +1,136 @@
import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
import { ICoreBrowserService } from 'browser/services/Services';
import { IColorSet } from 'browser/Types';
import { Attributes, BgFlags, FgFlags } from 'common/buffer/Constants';
import { IDecorationService } from 'common/services/Services';
import { ICellData } from 'common/Types';
import { Terminal } from 'xterm';
// Work variables to avoid garbage collection
let $fg = 0;
let $bg = 0;
let $hasFg = false;
let $hasBg = false;
let $isSelected = false;
export class CellColorResolver {
/**
* The shared result of the {@link resolve} call. This is only safe to use immediately after as
* any other calls will share object.
*/
public readonly result: { fg: number, bg: number, ext: number } = {
fg: 0,
bg: 0,
ext: 0
};
constructor(
private readonly _terminal: Terminal,
private _colors: IColorSet,
private readonly _selectionRenderModel: ISelectionRenderModel,
private readonly _decorationService: IDecorationService,
private readonly _coreBrowserService: ICoreBrowserService
) {
}
public setColors(colors: IColorSet): void {
this._colors = colors;
}
/**
* Resolves colors for the cell, putting the result into the shared {@link result}. This resolves
* overrides, inverse and selection for the cell which can then be used to feed into the renderer.
*/
public resolve(cell: ICellData, x: number, y: number): void {
this.result.bg = cell.bg;
this.result.fg = cell.fg;
this.result.ext = cell.bg & BgFlags.HAS_EXTENDED ? cell.extended.ext : 0;
// Get any foreground/background overrides, this happens on the model to avoid spreading
// override logic throughout the different sub-renderers
// Reset overrides work variables
$bg = 0;
$fg = 0;
$hasBg = false;
$hasFg = false;
$isSelected = false;
// Apply decorations on the bottom layer
this._decorationService.forEachDecorationAtCell(x, y, 'bottom', d => {
if (d.backgroundColorRGB) {
$bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
$hasBg = true;
}
if (d.foregroundColorRGB) {
$fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
$hasFg = true;
}
});
// Apply the selection color if needed
$isSelected = this._selectionRenderModel.isCellSelected(this._terminal, x, y);
if ($isSelected) {
$bg = (this._coreBrowserService.isFocused ? this._colors.selectionBackgroundOpaque : this._colors.selectionInactiveBackgroundOpaque).rgba >> 8 & 0xFFFFFF;
$hasBg = true;
if (this._colors.selectionForeground) {
$fg = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
$hasFg = true;
}
}
// Apply decorations on the top layer
this._decorationService.forEachDecorationAtCell(x, y, 'top', d => {
if (d.backgroundColorRGB) {
$bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
$hasBg = true;
}
if (d.foregroundColorRGB) {
$fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
$hasFg = true;
}
});
// Convert any overrides from rgba to the fg/bg packed format. This resolves the inverse flag
// ahead of time in order to use the correct cache key
if ($hasBg) {
if ($isSelected) {
// Non-RGB attributes from model + force non-dim + override + force RGB color mode
$bg = (cell.bg & ~Attributes.RGB_MASK & ~BgFlags.DIM) | $bg | Attributes.CM_RGB;
} else {
// Non-RGB attributes from model + override + force RGB color mode
$bg = (cell.bg & ~Attributes.RGB_MASK) | $bg | Attributes.CM_RGB;
}
}
if ($hasFg) {
// Non-RGB attributes from model + force disable inverse + override + force RGB color mode
$fg = (cell.fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | $fg | Attributes.CM_RGB;
}
// Handle case where inverse was specified by only one of bg override or fg override was set,
// resolving the other inverse color and setting the inverse flag if needed.
if (this.result.fg & FgFlags.INVERSE) {
if ($hasBg && !$hasFg) {
// Resolve bg color type (default color has a different meaning in fg vs bg)
if ((this.result.bg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
$fg = (this.result.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | ((this._colors.background.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
} else {
$fg = (this.result.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | this.result.bg & (Attributes.RGB_MASK | Attributes.CM_MASK);
}
$hasFg = true;
}
if (!$hasBg && $hasFg) {
// Resolve bg color type (default color has a different meaning in fg vs bg)
if ((this.result.fg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
$bg = (this.result.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | ((this._colors.foreground.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
} else {
$bg = (this.result.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | this.result.fg & (Attributes.RGB_MASK | Attributes.CM_MASK);
}
$hasBg = true;
}
}
// Use the override if it exists
this.result.bg = $hasBg ? $bg : this.result.bg;
this.result.fg = $hasFg ? $fg : this.result.fg;
}
}
@@ -3,21 +3,21 @@
* @license MIT
*/
import { generateConfig, configEquals } from './CharAtlasUtils';
import { WebglCharAtlas } from './WebglCharAtlas';
import { ICharAtlasConfig } from './Types';
import { TextureAtlas } from 'browser/renderer/shared/TextureAtlas';
import { Terminal } from 'xterm';
import { IColorSet, ITerminal } from 'browser/Types';
import { ICharAtlasConfig, ITextureAtlas } from 'browser/renderer/shared/Types';
import { generateConfig, configEquals } from 'browser/renderer/shared/CharAtlasUtils';
interface ICharAtlasCacheEntry {
atlas: WebglCharAtlas;
interface ITextureAtlasCacheEntry {
atlas: ITextureAtlas;
config: ICharAtlasConfig;
// N.B. This implementation potentially holds onto copies of the terminal forever, so
// this may cause memory leaks.
ownedBy: Terminal[];
}
const charAtlasCache: ICharAtlasCacheEntry[] = [];
const charAtlasCache: ITextureAtlasCacheEntry[] = [];
/**
* Acquires a char atlas, either generating a new one or returning an existing
@@ -25,7 +25,7 @@ const charAtlasCache: ICharAtlasCacheEntry[] = [];
* @param terminal The terminal.
* @param colors The colors to use.
*/
export function acquireCharAtlas(
export function acquireTextureAtlas(
terminal: Terminal,
colors: IColorSet,
scaledCellWidth: number,
@@ -33,7 +33,7 @@ export function acquireCharAtlas(
scaledCharWidth: number,
scaledCharHeight: number,
devicePixelRatio: number
): WebglCharAtlas {
): ITextureAtlas {
const newConfig = generateConfig(scaledCellWidth, scaledCellHeight, scaledCharWidth, scaledCharHeight, terminal, colors, devicePixelRatio);
// Check to see if the terminal already owns this config
@@ -66,8 +66,8 @@ export function acquireCharAtlas(
}
const core: ITerminal = (terminal as any)._core;
const newEntry: ICharAtlasCacheEntry = {
atlas: new WebglCharAtlas(document, newConfig, core.unicodeService),
const newEntry: ITextureAtlasCacheEntry = {
atlas: new TextureAtlas(document, newConfig, core.unicodeService),
config: newConfig,
ownedBy: [terminal]
};
@@ -5,14 +5,9 @@
import { ICharAtlasConfig } from './Types';
import { Attributes } from 'common/buffer/Constants';
import { Terminal, FontWeight } from 'xterm';
import { Terminal } from 'xterm';
import { IColorSet } from 'browser/Types';
import { IColor } from 'common/Types';
const NULL_COLOR: IColor = {
css: '',
rgba: 0
};
import { NULL_COLOR } from 'common/Color';
export function generateConfig(scaledCellWidth: number, scaledCellHeight: number, scaledCharWidth: number, scaledCharHeight: number, terminal: Terminal, colors: IColorSet, devicePixelRatio: number): ICharAtlasConfig {
// null out some fields that don't matter
@@ -3,7 +3,7 @@
* @license MIT
*/
import { throwIfFalsy } from 'browser/renderer/RendererUtils';
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
interface IBlockVector {
x: number;
+1
View File
@@ -0,0 +1 @@
This folder contains files that are shared between the renderer addons, but not necessarily bundled into the `xterm` module.
@@ -0,0 +1,91 @@
/**
* Copyright (c) 2022 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
import { Terminal } from 'xterm';
class SelectionRenderModel implements ISelectionRenderModel {
public hasSelection!: boolean;
public columnSelectMode!: boolean;
public viewportStartRow!: number;
public viewportEndRow!: number;
public viewportCappedStartRow!: number;
public viewportCappedEndRow!: number;
public startCol!: number;
public endCol!: number;
public selectionStart: [number, number] | undefined;
public selectionEnd: [number, number] | undefined;
constructor() {
this.clear();
}
public clear(): void {
this.hasSelection = false;
this.columnSelectMode = false;
this.viewportStartRow = 0;
this.viewportEndRow = 0;
this.viewportCappedStartRow = 0;
this.viewportCappedEndRow = 0;
this.startCol = 0;
this.endCol = 0;
this.selectionStart = undefined;
this.selectionEnd = undefined;
}
public update(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
this.selectionStart = start;
this.selectionEnd = end;
// Selection does not exist
if (!start || !end || (start[0] === end[0] && start[1] === end[1])) {
this.clear();
return;
}
// Translate from buffer position to viewport position
const viewportStartRow = start[1] - terminal.buffer.active.viewportY;
const viewportEndRow = end[1] - terminal.buffer.active.viewportY;
const viewportCappedStartRow = Math.max(viewportStartRow, 0);
const viewportCappedEndRow = Math.min(viewportEndRow, terminal.rows - 1);
// No need to draw the selection
if (viewportCappedStartRow >= terminal.rows || viewportCappedEndRow < 0) {
this.clear();
return;
}
this.hasSelection = true;
this.columnSelectMode = columnSelectMode;
this.viewportStartRow = viewportStartRow;
this.viewportEndRow = viewportEndRow;
this.viewportCappedStartRow = viewportCappedStartRow;
this.viewportCappedEndRow = viewportCappedEndRow;
this.startCol = start[0];
this.endCol = end[0];
}
public isCellSelected(terminal: Terminal, x: number, y: number): boolean {
if (!this.hasSelection) {
return false;
}
y -= terminal.buffer.active.viewportY;
if (this.columnSelectMode) {
if (this.startCol <= this.endCol) {
return x >= this.startCol && y >= this.viewportCappedStartRow &&
x < this.endCol && y <= this.viewportCappedEndRow;
}
return x < this.startCol && y >= this.viewportCappedStartRow &&
x >= this.endCol && y <= this.viewportCappedEndRow;
}
return (y > this.viewportStartRow && y < this.viewportEndRow) ||
(this.viewportStartRow === this.viewportEndRow && y === this.viewportStartRow && x >= this.startCol && x < this.endCol) ||
(this.viewportStartRow < this.viewportEndRow && y === this.viewportEndRow && x < this.endCol) ||
(this.viewportStartRow < this.viewportEndRow && y === this.viewportStartRow && x >= this.startCol);
}
}
export function createSelectionRenderModel(): ISelectionRenderModel {
return new SelectionRenderModel();
}
@@ -3,20 +3,17 @@
* @license MIT
*/
import { ICharAtlasConfig } from './Types';
import { DIM_OPACITY, TEXT_BASELINE } from 'browser/renderer/Constants';
import { IRasterizedGlyph, IBoundingBox } from '../Types';
import { DIM_OPACITY, TEXT_BASELINE } from 'browser/renderer/shared/Constants';
import { DEFAULT_COLOR, Attributes, DEFAULT_EXT, UnderlineStyle } from 'common/buffer/Constants';
import { throwIfFalsy } from '../WebglUtils';
import { IColor } from 'common/Types';
import { IDisposable } from 'xterm';
import { AttributeData } from 'common/buffer/AttributeData';
import { color, rgba } from 'common/Color';
import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs';
import { excludeFromContrastRatioDemands, isPowerlineGlyph, isRestrictedPowerlineGlyph } from 'browser/renderer/RendererUtils';
import { color, NULL_COLOR, rgba } from 'common/Color';
import { tryDrawCustomChar } from 'browser/renderer/shared/CustomGlyphs';
import { excludeFromContrastRatioDemands, isPowerlineGlyph, isRestrictedPowerlineGlyph, throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
import { IUnicodeService } from 'common/services/Services';
import { FourKeyMap } from 'common/MultiKeyMap';
import { IdleTaskQueue } from 'common/TaskQueue';
import { IBoundingBox, ICharAtlasConfig, IRasterizedGlyph, ITextureAtlas } from 'browser/renderer/shared/Types';
// For debugging purposes, it can be useful to set this to a really tiny value,
// to verify that LRU eviction works.
@@ -29,12 +26,6 @@ const TEXTURE_HEIGHT = 1024;
* this prevent juggling multiple textures in the GL context.
*/
const TEXTURE_CAPACITY = Math.floor(TEXTURE_HEIGHT * 0.8);
const TRANSPARENT_COLOR = {
css: 'rgba(0, 0, 0, 0)',
rgba: 0
};
/**
* A shared object which is used to draw nothing for a particular cell.
*/
@@ -57,7 +48,7 @@ interface ICharAtlasActiveRow {
// Work variables to avoid garbage collection
let $glyph = undefined;
export class WebglCharAtlas implements IDisposable {
export class TextureAtlas implements ITextureAtlas {
private _didWarmUp: boolean = false;
private _cacheMap: FourKeyMap<number, number, number, number, IRasterizedGlyph> = new FourKeyMap();
@@ -202,7 +193,7 @@ export class WebglCharAtlas implements IDisposable {
// The background color might have some transparency, so we need to render it as fully
// transparent in the atlas. Otherwise we'd end up drawing the transparent background twice
// around the anti-aliased edges of the glyph, and it would look too dark.
return TRANSPARENT_COLOR;
return NULL_COLOR;
}
let result: IColor;
@@ -345,6 +336,9 @@ export class WebglCharAtlas implements IDisposable {
private _drawToCache(codeOrChars: number | string, bg: number, fg: number, ext: number): IRasterizedGlyph {
const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars;
// Uncomment for debugging
// console.log(`draw to cache "${chars}"`, bg, fg, ext);
this.hasCanvasChanged = true;
// Allow 1 cell width per character, with a minimum of 2 (CJK), plus some padding. This is used
+156
View File
@@ -0,0 +1,156 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { FontWeight, Terminal } from 'xterm';
import { IColorSet } from 'browser/Types';
import { IDisposable } from 'common/Types';
import { IEvent } from 'common/EventEmitter';
export interface ICharAtlasConfig {
customGlyphs: boolean;
devicePixelRatio: number;
letterSpacing: number;
lineHeight: number;
fontSize: number;
fontFamily: string;
fontWeight: FontWeight;
fontWeightBold: FontWeight;
scaledCellWidth: number;
scaledCellHeight: number;
scaledCharWidth: number;
scaledCharHeight: number;
allowTransparency: boolean;
drawBoldTextInBrightColors: boolean;
minimumContrastRatio: number;
colors: IColorSet;
}
export interface IRenderDimensions {
scaledCharWidth: number;
scaledCharHeight: number;
scaledCellWidth: number;
scaledCellHeight: number;
scaledCharLeft: number;
scaledCharTop: number;
scaledCanvasWidth: number;
scaledCanvasHeight: number;
canvasWidth: number;
canvasHeight: number;
actualCellWidth: number;
actualCellHeight: number;
}
export interface IRequestRedrawEvent {
start: number;
end: number;
}
/**
* Note that IRenderer implementations should emit the refresh event after
* rendering rows to the screen.
*/
export interface IRenderer extends IDisposable {
readonly dimensions: IRenderDimensions;
/**
* Fires when the renderer is requesting to be redrawn on the next animation
* frame but is _not_ a result of content changing (eg. selection changes).
*/
readonly onRequestRedraw: IEvent<IRequestRedrawEvent>;
dispose(): void;
setColors(colors: IColorSet): void;
onDevicePixelRatioChange(): void;
onResize(cols: number, rows: number): void;
onCharSizeChanged(): void;
onBlur(): void;
onFocus(): void;
onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
onCursorMove(): void;
onOptionsChanged(): void;
clear(): void;
renderRows(start: number, end: number): void;
clearTextureAtlas?(): void;
}
export interface ITextureAtlas extends IDisposable {
readonly cacheCanvas: HTMLCanvasElement;
hasCanvasChanged: boolean;
/**
* Warm up the texture atlas, adding common glyphs to avoid slowing early frame.
*/
warmUp(): void;
/**
* Call when a frame is being drawn, this will return true if the atlas was cleared to make room
* for a new set of glyphs.
*/
beginFrame(): boolean;
/**
* Clear all glyphs from the texture atlas.
*/
clearTexture(): void;
getRasterizedGlyph(code: number, bg: number, fg: number, ext: number): IRasterizedGlyph;
getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number): IRasterizedGlyph;
}
/**
* Represents a rasterized glyph within a texture atlas. Some numbers are
* tracked in CSS pixels as well in order to reduce calculations during the
* render loop.
*/
export interface IRasterizedGlyph {
/**
* The x and y offset between the glyph's top/left and the top/left of a cell
* in pixels.
*/
offset: IVector;
/**
* the x and y position of the glyph in the texture in pixels.
*/
texturePosition: IVector;
/**
* the x and y position of the glyph in the texture in clip space coordinates.
*/
texturePositionClipSpace: IVector;
/**
* The width and height of the glyph in the texture in pixels.
*/
size: IVector;
/**
* The width and height of the glyph in the texture in clip space coordinates.
*/
sizeClipSpace: IVector;
}
export interface IVector {
x: number;
y: number;
}
export interface IBoundingBox {
top: number;
left: number;
right: number;
bottom: number;
}
export interface ISelectionRenderModel {
readonly hasSelection: boolean;
readonly columnSelectMode: boolean;
readonly viewportStartRow: number;
readonly viewportEndRow: number;
readonly viewportCappedStartRow: number;
readonly viewportCappedEndRow: number;
readonly startCol: number;
readonly endCol: number;
readonly selectionStart: [number, number] | undefined;
readonly selectionEnd: [number, number] | undefined;
clear(): void;
update(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode?: boolean): void;
isCellSelected(terminal: Terminal, x: number, y: number): boolean;
}
+1 -1
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { IRenderer, IRenderDimensions } from 'browser/renderer/Types';
import { IRenderer, IRenderDimensions } from 'browser/renderer/shared/Types';
import { RenderDebouncer } from 'browser/RenderDebouncer';
import { EventEmitter, IEvent, initEvent } from 'common/EventEmitter';
import { Disposable } from 'common/Lifecycle';
+1 -1
View File
@@ -4,7 +4,7 @@
*/
import { IEvent } from 'common/EventEmitter';
import { IRenderDimensions, IRenderer } from 'browser/renderer/Types';
import { IRenderDimensions, IRenderer } from 'browser/renderer/shared/Types';
import { IColorSet } from 'browser/Types';
import { ISelectionRedrawRequestEvent as ISelectionRequestRedrawEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
import { createDecorator } from 'common/services/ServiceRegistry';
+5
View File
@@ -11,6 +11,11 @@ let $g = 0;
let $b = 0;
let $a = 0;
export const NULL_COLOR: IColor = {
css: '#00000000',
rgba: 0
};
/**
* Helper functions where the source type is "channels" (individual color channels as numbers).
*/
+1 -1
View File
@@ -6,7 +6,7 @@
import { assert } from 'chai';
import { pollFor, openTerminal, getBrowserType, launchBrowser, writeSync } from './TestUtils';
import { Browser, Page } from 'playwright';
import { IRenderDimensions } from 'browser/renderer/Types';
import { IRenderDimensions } from 'browser/renderer/shared/Types';
const APP = 'http://127.0.0.1:3001/test';