Merge branch 'master' into dynamic-char-atlas
I did a bunch of testing of various ansi escape codes (including italics) with all three char atlas implementations (none, static, dynamic), with and without `drawBoldTextInBrightColors`, and with and without `enableBold` to test all of the edge cases that I could.
This commit is contained in:
@@ -214,7 +214,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
* @param color The color of the character.
|
||||
*/
|
||||
protected fillCharTrueColor(terminal: ITerminal, charData: CharData, x: number, y: number): void {
|
||||
this._ctx.font = this._getFont(terminal, false);
|
||||
this._ctx.font = this._getFont(terminal, false, false);
|
||||
this._ctx.textBaseline = 'top';
|
||||
this._clipRow(terminal, y);
|
||||
this._ctx.fillText(
|
||||
@@ -237,16 +237,18 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
* This is used to validate whether a cached image can be used.
|
||||
* @param bold Whether the text is bold.
|
||||
*/
|
||||
protected drawChar(terminal: ITerminal, char: string, code: number, width: number, x: number, y: number, fg: number, bg: number, bold: boolean, dim: boolean): void {
|
||||
protected drawChar(terminal: ITerminal, char: string, code: number, width: number, x: number, y: number, fg: number, bg: number, bold: boolean, dim: boolean, italic: boolean): void {
|
||||
const drawInBrightColor = terminal.options.drawBoldTextInBrightColors && bold && fg < 8;
|
||||
fg += drawInBrightColor ? 8 : 0;
|
||||
const atlasDidDraw = this._charAtlas && this._charAtlas.draw(
|
||||
this._ctx,
|
||||
{char, code, bg, fg, bold: bold && terminal.options.enableBold, dim},
|
||||
{char, code, bg, fg, bold: bold && terminal.options.enableBold, dim, italic},
|
||||
x * this._scaledCellWidth + this._scaledCharLeft,
|
||||
y * this._scaledCellHeight + this._scaledCharTop
|
||||
);
|
||||
|
||||
if (!atlasDidDraw) {
|
||||
this._drawUncachedChar(terminal, char, width, fg, x, y, bold && terminal.options.enableBold, dim);
|
||||
this._drawUncachedChar(terminal, char, width, fg, x, y, bold && terminal.options.enableBold, dim, italic);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,9 +263,9 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
* @param x The column to draw at.
|
||||
* @param y The row to draw at.
|
||||
*/
|
||||
private _drawUncachedChar(terminal: ITerminal, char: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean): void {
|
||||
private _drawUncachedChar(terminal: ITerminal, char: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean, italic: boolean): void {
|
||||
this._ctx.save();
|
||||
this._ctx.font = this._getFont(terminal, bold);
|
||||
this._ctx.font = this._getFont(terminal, bold, italic);
|
||||
this._ctx.textBaseline = 'top';
|
||||
|
||||
if (fg === INVERTED_DEFAULT_COLOR) {
|
||||
@@ -309,10 +311,11 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
* @param terminal The terminal.
|
||||
* @param isBold If we should use the bold fontWeight.
|
||||
*/
|
||||
protected _getFont(terminal: ITerminal, isBold: boolean): string {
|
||||
protected _getFont(terminal: ITerminal, isBold: boolean, isItalic: boolean): string {
|
||||
const fontWeight = isBold ? terminal.options.fontWeightBold : terminal.options.fontWeight;
|
||||
const fontStyle = isItalic ? 'italic' : '';
|
||||
|
||||
return `${fontWeight} ${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
|
||||
return `${fontStyle} ${fontWeight} ${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
super.resize(terminal, dim);
|
||||
|
||||
// Clear the character width cache if the font or width has changed
|
||||
const terminalFont = this._getFont(terminal, false);
|
||||
const terminalFont = this._getFont(terminal, false, false);
|
||||
if (this._characterWidth !== dim.scaledCharWidth || this._characterFont !== terminalFont) {
|
||||
this._characterWidth = dim.scaledCharWidth;
|
||||
this._characterFont = terminalFont;
|
||||
@@ -116,30 +116,63 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
}
|
||||
|
||||
if (flags & FLAGS.BOLD) {
|
||||
// Convert the FG color to the bold variant
|
||||
if (fg < 8) {
|
||||
fg += 8;
|
||||
}
|
||||
}
|
||||
|
||||
callback(code, char, width, x, y, fg, bg, flags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the background for a specified range of columns. Tries to batch adjacent cells of the
|
||||
* same color together to reduce draw calls.
|
||||
*/
|
||||
private _drawBackground(terminal: ITerminal, firstRow: number, lastRow: number): void {
|
||||
const ctx = this._ctx;
|
||||
const cols = terminal.cols;
|
||||
let startX: number = 0;
|
||||
let startY: number = 0;
|
||||
let prevFillStyle: string | null = null;
|
||||
|
||||
ctx.save();
|
||||
|
||||
this._forEachCell(terminal, firstRow, lastRow, (code, char, width, x, y, fg, bg, flags) => {
|
||||
// libvte and xterm both draw the background (but not foreground) of invisible characters,
|
||||
// so we should too.
|
||||
const isDefaultBackground = bg >= 256;
|
||||
if (!isDefaultBackground) {
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = (bg === INVERTED_DEFAULT_COLOR ? this._colors.foreground.css : this._colors.ansi[bg].css);
|
||||
this.fillCells(x, y, width, 1);
|
||||
this._ctx.restore();
|
||||
let nextFillStyle = null; // null represents default background color
|
||||
if (bg === INVERTED_DEFAULT_COLOR) {
|
||||
nextFillStyle = this._colors.foreground.css;
|
||||
} else if (bg < 256) {
|
||||
nextFillStyle = this._colors.ansi[bg].css;
|
||||
}
|
||||
|
||||
if (prevFillStyle === null) {
|
||||
// This is either the first iteration, or the default background was set. Either way, we
|
||||
// don't need to draw anything.
|
||||
startX = x;
|
||||
startY = y;
|
||||
} if (y !== startY) {
|
||||
// our row changed, draw the previous row
|
||||
ctx.fillStyle = prevFillStyle;
|
||||
this.fillCells(startX, startY, cols - startX, 1);
|
||||
startX = x;
|
||||
startY = y;
|
||||
} else if (prevFillStyle !== nextFillStyle) {
|
||||
// our color changed, draw the previous characters in this row
|
||||
ctx.fillStyle = prevFillStyle;
|
||||
this.fillCells(startX, startY, x - startX, 1);
|
||||
startX = x;
|
||||
startY = y;
|
||||
}
|
||||
|
||||
prevFillStyle = nextFillStyle;
|
||||
});
|
||||
|
||||
// flush the last color we encountered
|
||||
if (prevFillStyle !== null) {
|
||||
ctx.fillStyle = prevFillStyle;
|
||||
this.fillCells(startX, startY, cols - startX, 1);
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
private _drawForeground(terminal: ITerminal, firstRow: number, lastRow: number): void {
|
||||
@@ -164,7 +197,7 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
terminal, char, code,
|
||||
width, x, y,
|
||||
fg, bg,
|
||||
!!(flags & FLAGS.BOLD), !!(flags & FLAGS.DIM)
|
||||
!!(flags & FLAGS.BOLD), !!(flags & FLAGS.DIM), !!(flags & FLAGS.ITALIC)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,13 +10,14 @@ import { IColorSet } from '../shared/Types';
|
||||
/**
|
||||
* Flags used to render terminal text properly.
|
||||
*/
|
||||
export enum FLAGS {
|
||||
export const enum FLAGS {
|
||||
BOLD = 1,
|
||||
UNDERLINE = 2,
|
||||
BLINK = 4,
|
||||
INVERSE = 8,
|
||||
INVISIBLE = 16,
|
||||
DIM = 32
|
||||
DIM = 32,
|
||||
ITALIC = 64
|
||||
}
|
||||
|
||||
export interface IRenderer extends IEventEmitter {
|
||||
|
||||
@@ -48,6 +48,6 @@ export default abstract class BaseCharAtlas {
|
||||
ctx: CanvasRenderingContext2D,
|
||||
glyph: IGlyphIdentifier,
|
||||
x: number,
|
||||
y: number,
|
||||
y: number
|
||||
): boolean;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import StaticCharAtlas from './StaticCharAtlas';
|
||||
const charAtlasImplementations = {
|
||||
'none': NoneCharAtlas,
|
||||
'static': StaticCharAtlas,
|
||||
'dynamic': DynamicCharAtlas,
|
||||
'dynamic': DynamicCharAtlas
|
||||
};
|
||||
|
||||
interface ICharAtlasCacheEntry {
|
||||
@@ -38,7 +38,7 @@ export function acquireCharAtlas(
|
||||
terminal: ITerminal,
|
||||
colors: IColorSet,
|
||||
scaledCharWidth: number,
|
||||
scaledCharHeight: number,
|
||||
scaledCharHeight: number
|
||||
): BaseCharAtlas {
|
||||
const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, terminal, colors);
|
||||
|
||||
@@ -51,15 +51,14 @@ export function acquireCharAtlas(
|
||||
if (ownedByIndex >= 0) {
|
||||
if (configEquals(entry.config, newConfig)) {
|
||||
return entry.atlas;
|
||||
} else {
|
||||
// The configs differ, release the terminal from the entry
|
||||
if (entry.ownedBy.length === 1) {
|
||||
charAtlasCache.splice(i, 1);
|
||||
} else {
|
||||
entry.ownedBy.splice(ownedByIndex, 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
// The configs differ, release the terminal from the entry
|
||||
if (entry.ownedBy.length === 1) {
|
||||
charAtlasCache.splice(i, 1);
|
||||
} else {
|
||||
entry.ownedBy.splice(ownedByIndex, 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,10 +75,10 @@ export function acquireCharAtlas(
|
||||
const newEntry: ICharAtlasCacheEntry = {
|
||||
atlas: new charAtlasImplementations[terminal.options.experimentalCharAtlas](
|
||||
document,
|
||||
newConfig,
|
||||
newConfig
|
||||
),
|
||||
config: newConfig,
|
||||
ownedBy: [terminal],
|
||||
ownedBy: [terminal]
|
||||
};
|
||||
charAtlasCache.push(newEntry);
|
||||
return newEntry.atlas;
|
||||
|
||||
@@ -17,7 +17,7 @@ export function generateConfig(scaledCharWidth: number, scaledCharHeight: number
|
||||
selection: null,
|
||||
// For the static char atlas, we only use the first 16 colors, but we need all 256 for the
|
||||
// dynamic character atlas.
|
||||
ansi: colors.ansi.slice(0, 16),
|
||||
ansi: colors.ansi.slice(0, 16)
|
||||
};
|
||||
return {
|
||||
type: terminal.options.experimentalCharAtlas,
|
||||
|
||||
@@ -18,8 +18,8 @@ const TEXTURE_HEIGHT = 1024;
|
||||
|
||||
const TRANSPARENT_COLOR = {
|
||||
css: 'rgba(0, 0, 0, 0)',
|
||||
rgba: 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
|
||||
@@ -35,7 +35,8 @@ interface IGlyphCacheValue {
|
||||
}
|
||||
|
||||
function getGlyphCacheKey(glyph: IGlyphIdentifier): string {
|
||||
return `${glyph.bg}_${glyph.fg}_${glyph.bold ? 0 : 1}${glyph.dim ? 0 : 1}${glyph.char}`;
|
||||
const styleFlags = (glyph.bold ? 0 : 4) + (glyph.dim ? 0 : 2) + (glyph.italic ? 0 : 1);
|
||||
return `${glyph.bg}_${glyph.fg}_${styleFlags}${glyph.char}`;
|
||||
}
|
||||
|
||||
export default class DynamicCharAtlas extends BaseCharAtlas {
|
||||
@@ -89,7 +90,7 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
|
||||
ctx: CanvasRenderingContext2D,
|
||||
glyph: IGlyphIdentifier,
|
||||
x: number,
|
||||
y: number,
|
||||
y: number
|
||||
): boolean {
|
||||
const glyphKey = getGlyphCacheKey(glyph);
|
||||
const cacheValue = this._cacheMap.get(glyphKey);
|
||||
@@ -108,9 +109,8 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
|
||||
this._cacheMap.set(glyphKey, cacheValue);
|
||||
this._drawFromCache(ctx, cacheValue, x, y);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private _canCache(glyph: IGlyphIdentifier): boolean {
|
||||
@@ -151,7 +151,7 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
|
||||
x,
|
||||
y,
|
||||
this._config.scaledCharWidth,
|
||||
this._config.scaledCharHeight,
|
||||
this._config.scaledCharHeight
|
||||
);
|
||||
}
|
||||
|
||||
@@ -172,9 +172,8 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
|
||||
return this._config.colors.foreground;
|
||||
} else if (glyph.bg < 256) {
|
||||
return this._getColorFromAnsiIndex(glyph.bg);
|
||||
} else {
|
||||
return this._config.colors.background;
|
||||
}
|
||||
return this._config.colors.background;
|
||||
}
|
||||
|
||||
private _getForegroundColor(glyph: IGlyphIdentifier): IColor {
|
||||
@@ -183,9 +182,8 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
|
||||
} else if (glyph.fg < 256) {
|
||||
// 256 color support
|
||||
return this._getColorFromAnsiIndex(glyph.fg);
|
||||
} else {
|
||||
return this._config.colors.foreground;
|
||||
}
|
||||
return this._config.colors.foreground;
|
||||
}
|
||||
|
||||
// TODO: We do this (or something similar) in multiple places. We should split this off
|
||||
@@ -206,11 +204,9 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
|
||||
|
||||
// draw the foreground/glyph
|
||||
const fontWeight = glyph.bold ? this._config.fontWeightBold : this._config.fontWeight;
|
||||
const fontStyle = glyph.italic ? 'italic' : '';
|
||||
this._tmpCtx.font =
|
||||
`${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`;
|
||||
if (glyph.bold) {
|
||||
this._tmpCtx.font = `bold ${this._tmpCtx.font}`;
|
||||
}
|
||||
`${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`;
|
||||
this._tmpCtx.textBaseline = 'top';
|
||||
|
||||
this._tmpCtx.fillStyle = this._getForegroundColor(glyph).css;
|
||||
@@ -226,7 +222,7 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
|
||||
// clear the background from the character to avoid issues with drawing over the previous
|
||||
// character if it extends past it's bounds
|
||||
const imageData = this._tmpCtx.getImageData(
|
||||
0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight,
|
||||
0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight
|
||||
);
|
||||
let isEmpty = false;
|
||||
if (!this._config.allowTransparency) {
|
||||
@@ -240,7 +236,7 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
|
||||
|
||||
return {
|
||||
index,
|
||||
isEmpty,
|
||||
isEmpty
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
*/
|
||||
|
||||
interface ILinkedListNode<T> {
|
||||
prev: ILinkedListNode<T>,
|
||||
next: ILinkedListNode<T>,
|
||||
key: string,
|
||||
value: T,
|
||||
prev: ILinkedListNode<T>;
|
||||
next: ILinkedListNode<T>;
|
||||
key: string;
|
||||
value: T;
|
||||
}
|
||||
|
||||
export default class LRUMap<T> {
|
||||
@@ -56,14 +56,14 @@ export default class LRUMap<T> {
|
||||
* Each empty object only consumes about 60 bytes of memory, so this is pretty cheap, even for
|
||||
* large maps.
|
||||
*/
|
||||
public prealloc(count: number) {
|
||||
public prealloc(count: number): void {
|
||||
const nodePool = this._nodePool;
|
||||
for (let i = 0; i < count; i++) {
|
||||
nodePool.push({
|
||||
prev: null,
|
||||
next: null,
|
||||
key: null,
|
||||
value: null,
|
||||
value: null
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -114,7 +114,7 @@ export default class LRUMap<T> {
|
||||
prev: null,
|
||||
next: null,
|
||||
key,
|
||||
value,
|
||||
value
|
||||
};
|
||||
}
|
||||
this._map[key] = node;
|
||||
|
||||
@@ -22,33 +22,31 @@ export default class StaticCharAtlas extends BaseCharAtlas {
|
||||
return canvas;
|
||||
}
|
||||
|
||||
public _doWarmUp() {
|
||||
public _doWarmUp(): void {
|
||||
const result = generateStaticCharAtlasTexture(window, this._canvasFactory, this._config);
|
||||
if (result instanceof HTMLCanvasElement) {
|
||||
this._texture = result;
|
||||
} else {
|
||||
result.then(texture => {
|
||||
this._texture = texture
|
||||
this._texture = texture;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _isCached(glyph: IGlyphIdentifier, colorIndex: number): boolean {
|
||||
const isAscii = glyph.code < 256;
|
||||
// A color is basic if it is one of the standard normal or bold weight
|
||||
// colors of the characters held in the char atlas. Note that this excludes
|
||||
// the normal weight _light_ color characters.
|
||||
const isBasicColor = (colorIndex > 1 && glyph.fg < 16) && (glyph.fg < 8 || glyph.bold);
|
||||
// A color is basic if it is one of the 4 bit ANSI colors.
|
||||
const isBasicColor = glyph.fg < 16;
|
||||
const isDefaultColor = glyph.fg >= 256;
|
||||
const isDefaultBackground = glyph.bg >= 256;
|
||||
return isAscii && (isBasicColor || isDefaultColor) && isDefaultBackground;
|
||||
return isAscii && (isBasicColor || isDefaultColor) && isDefaultBackground && !glyph.italic;
|
||||
}
|
||||
|
||||
public draw(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
glyph: IGlyphIdentifier,
|
||||
x: number,
|
||||
y: number,
|
||||
y: number
|
||||
): boolean {
|
||||
// we're not warmed up yet
|
||||
if (this._texture == null) {
|
||||
@@ -57,7 +55,7 @@ export default class StaticCharAtlas extends BaseCharAtlas {
|
||||
|
||||
let colorIndex = 0;
|
||||
if (glyph.fg < 256) {
|
||||
colorIndex = glyph.fg + 2;
|
||||
colorIndex = 2 + glyph.fg + (glyph.bold ? 16 : 0);
|
||||
} else {
|
||||
// If default color and bold
|
||||
if (glyph.bold) {
|
||||
@@ -76,14 +74,6 @@ export default class StaticCharAtlas extends BaseCharAtlas {
|
||||
ctx.globalAlpha = DIM_OPACITY;
|
||||
}
|
||||
|
||||
// Draw the non-bold version of the same color if bold is not enabled
|
||||
/*if (glyph.bold && !terminal.options.enableBold) {
|
||||
// Ignore default color as it's not touched above
|
||||
if (colorIndex > 1) {
|
||||
colorIndex -= 8;
|
||||
}
|
||||
}*/
|
||||
|
||||
ctx.drawImage(
|
||||
this._texture,
|
||||
glyph.code * charAtlasCellWidth,
|
||||
|
||||
@@ -13,4 +13,5 @@ export interface IGlyphIdentifier {
|
||||
fg: number;
|
||||
bold: boolean;
|
||||
dim: boolean;
|
||||
italic: boolean;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user