mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge branch 'master' into handle-delete-composition-helper
This commit is contained in:
@@ -344,7 +344,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
// 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);
|
||||
drawSuccess = tryDrawCustomChar(this._ctx, cell.getChars(), x * this._scaledCellWidth, y * this._scaledCellHeight, this._scaledCellWidth, this._scaledCellHeight, this._optionsService.rawOptions.fontSize);
|
||||
}
|
||||
|
||||
// Draw the character
|
||||
@@ -404,12 +404,11 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
|
||||
// Don't try cache the glyph if it uses any decoration foreground/background override.
|
||||
let hasOverrides = false;
|
||||
for (const d of this._decorationService.getDecorationsAtCell(x, y)) {
|
||||
this._decorationService.forEachDecorationAtCell(x, y, undefined, d => {
|
||||
if (d.backgroundColorRGB || d.foregroundColorRGB) {
|
||||
hasOverrides = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const atlasDidDraw = hasOverrides ? false : this._charAtlas?.draw(this._ctx, this._currentGlyphIdentifier, x * this._scaledCellWidth + this._scaledCharLeft, y * this._scaledCellHeight + this._scaledCharTop);
|
||||
|
||||
@@ -473,7 +472,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
// 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);
|
||||
drawSuccess = tryDrawCustomChar(this._ctx, cell.getChars(), x * this._scaledCellWidth, y * this._scaledCellHeight, this._scaledCellWidth, this._scaledCellHeight, this._optionsService.rawOptions.fontSize);
|
||||
}
|
||||
|
||||
// Draw the character
|
||||
@@ -519,9 +518,9 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
let bgOverride: number | undefined;
|
||||
let fgOverride: number | undefined;
|
||||
let isTop = false;
|
||||
for (const d of this._decorationService.getDecorationsAtCell(x, y)) {
|
||||
this._decorationService.forEachDecorationAtCell(x, y, undefined, d => {
|
||||
if (d.options.layer !== 'top' && isTop) {
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
if (d.backgroundColorRGB) {
|
||||
bgOverride = d.backgroundColorRGB.rgba;
|
||||
@@ -530,7 +529,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
fgOverride = d.foregroundColorRGB.rgba;
|
||||
}
|
||||
isTop = d.options.layer === 'top';
|
||||
}
|
||||
});
|
||||
|
||||
// Apply selection foreground if applicable
|
||||
if (!isTop) {
|
||||
|
||||
@@ -187,15 +187,15 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
// Get any decoration foreground/background overrides, this must be fetched before the early
|
||||
// exist but applied after inverse
|
||||
let isTop = false;
|
||||
for (const d of this._decorationService.getDecorationsAtCell(x, this._bufferService.buffer.ydisp + y)) {
|
||||
this._decorationService.forEachDecorationAtCell(x, this._bufferService.buffer.ydisp + y, undefined, d => {
|
||||
if (d.options.layer !== 'top' && isTop) {
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
if (d.backgroundColorRGB) {
|
||||
nextFillStyle = d.backgroundColorRGB.css;
|
||||
}
|
||||
isTop = d.options.layer === 'top';
|
||||
}
|
||||
});
|
||||
|
||||
if (prevFillStyle === null) {
|
||||
// This is either the first iteration, or the default background was set. Either way, we
|
||||
|
||||
@@ -70,6 +70,14 @@ const INDICES_PER_CELL = 10;
|
||||
const BYTES_PER_CELL = INDICES_PER_CELL * Float32Array.BYTES_PER_ELEMENT;
|
||||
const CELL_POSITION_INDICES = 2;
|
||||
|
||||
/** Work variables to avoid garbage collection. */
|
||||
const w: { i: number, glyph: IRasterizedGlyph | undefined, leftCellPadding: number, clippedPixels: number } = {
|
||||
i: 0,
|
||||
glyph: undefined,
|
||||
leftCellPadding: 0,
|
||||
clippedPixels: 0
|
||||
};
|
||||
|
||||
export class GlyphRenderer extends Disposable {
|
||||
private _atlas: WebglCharAtlas | undefined;
|
||||
|
||||
@@ -122,7 +130,7 @@ export class GlyphRenderer extends Disposable {
|
||||
gl.vertexAttribPointer(VertexAttribLocations.UNIT_QUAD, 2, this._gl.FLOAT, false, 0, 0);
|
||||
|
||||
// Setup the unit quad element array buffer, this points to indices in
|
||||
// unitQuadVertuces to allow is to draw 2 triangles from the vertices
|
||||
// unitQuadVertices to allow is to draw 2 triangles from the vertices
|
||||
const unitQuadElementIndices = new Uint8Array([0, 1, 3, 0, 2, 3]);
|
||||
const elementIndicesBuffer = gl.createBuffer();
|
||||
this.register(toDisposable(() => gl.deleteBuffer(elementIndicesBuffer)));
|
||||
@@ -170,18 +178,20 @@ export class GlyphRenderer extends Disposable {
|
||||
}
|
||||
|
||||
public updateCell(x: number, y: number, code: number, bg: number, fg: number, ext: number, chars: string, lastBg: number): void {
|
||||
// Since this function is called for every cell (`rows*cols`), it must be very optimized. It
|
||||
// should not instantiate any variables unless a new glyph is drawn to the cache where the
|
||||
// slight slowdown is acceptable for the developer ergonomics provided as it's a once of for
|
||||
// each glyph.
|
||||
this._updateCell(this._vertices.attributes, x, y, code, bg, fg, ext, chars, lastBg);
|
||||
}
|
||||
|
||||
private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, bg: number, fg: number, ext: number, chars: string, lastBg: number): void {
|
||||
const terminal = this._terminal;
|
||||
|
||||
const i = (y * terminal.cols + x) * INDICES_PER_CELL;
|
||||
w.i = (y * this._terminal.cols + x) * INDICES_PER_CELL;
|
||||
|
||||
// Exit early if this is a null character, allow space character to continue as it may have
|
||||
// underline/strikethrough styles
|
||||
if (code === NULL_CELL_CODE || code === undefined/* This is used for the right side of wide chars */) {
|
||||
fill(array, 0, i, i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES);
|
||||
fill(array, 0, w.i, w.i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -190,47 +200,40 @@ export class GlyphRenderer extends Disposable {
|
||||
}
|
||||
|
||||
// Get the glyph
|
||||
let rasterizedGlyph: IRasterizedGlyph;
|
||||
if (chars && chars.length > 1) {
|
||||
rasterizedGlyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext);
|
||||
w.glyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext);
|
||||
} else {
|
||||
rasterizedGlyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext);
|
||||
w.glyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext);
|
||||
}
|
||||
|
||||
// Fill empty if no glyph was found
|
||||
if (!rasterizedGlyph) {
|
||||
fill(array, 0, i, i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES);
|
||||
return;
|
||||
}
|
||||
|
||||
const leftCellPadding = Math.floor((this._dimensions.scaledCellWidth - this._dimensions.scaledCharWidth) / 2);
|
||||
if (bg !== lastBg && rasterizedGlyph.offset.x > leftCellPadding) {
|
||||
const clippedPixels = rasterizedGlyph.offset.x - leftCellPadding;
|
||||
w.leftCellPadding = Math.floor((this._dimensions.scaledCellWidth - this._dimensions.scaledCharWidth) / 2);
|
||||
if (bg !== lastBg && w.glyph.offset.x > w.leftCellPadding) {
|
||||
w.clippedPixels = w.glyph.offset.x - w.leftCellPadding;
|
||||
// a_origin
|
||||
array[i ] = -(rasterizedGlyph.offset.x - clippedPixels) + this._dimensions.scaledCharLeft;
|
||||
array[i + 1] = -rasterizedGlyph.offset.y + this._dimensions.scaledCharTop;
|
||||
array[w.i ] = -(w.glyph.offset.x - w.clippedPixels) + this._dimensions.scaledCharLeft;
|
||||
array[w.i + 1] = -w.glyph.offset.y + this._dimensions.scaledCharTop;
|
||||
// a_size
|
||||
array[i + 2] = (rasterizedGlyph.size.x - clippedPixels) / this._dimensions.scaledCanvasWidth;
|
||||
array[i + 3] = rasterizedGlyph.size.y / this._dimensions.scaledCanvasHeight;
|
||||
array[w.i + 2] = (w.glyph.size.x - w.clippedPixels) / this._dimensions.scaledCanvasWidth;
|
||||
array[w.i + 3] = w.glyph.size.y / this._dimensions.scaledCanvasHeight;
|
||||
// a_texcoord
|
||||
array[i + 4] = rasterizedGlyph.texturePositionClipSpace.x + clippedPixels / this._atlas.cacheCanvas.width;
|
||||
array[i + 5] = rasterizedGlyph.texturePositionClipSpace.y;
|
||||
array[w.i + 4] = w.glyph.texturePositionClipSpace.x + w.clippedPixels / this._atlas.cacheCanvas.width;
|
||||
array[w.i + 5] = w.glyph.texturePositionClipSpace.y;
|
||||
// a_texsize
|
||||
array[i + 6] = rasterizedGlyph.sizeClipSpace.x - clippedPixels / this._atlas.cacheCanvas.width;
|
||||
array[i + 7] = rasterizedGlyph.sizeClipSpace.y;
|
||||
array[w.i + 6] = w.glyph.sizeClipSpace.x - w.clippedPixels / this._atlas.cacheCanvas.width;
|
||||
array[w.i + 7] = w.glyph.sizeClipSpace.y;
|
||||
} else {
|
||||
// a_origin
|
||||
array[i ] = -rasterizedGlyph.offset.x + this._dimensions.scaledCharLeft;
|
||||
array[i + 1] = -rasterizedGlyph.offset.y + this._dimensions.scaledCharTop;
|
||||
array[w.i ] = -w.glyph.offset.x + this._dimensions.scaledCharLeft;
|
||||
array[w.i + 1] = -w.glyph.offset.y + this._dimensions.scaledCharTop;
|
||||
// a_size
|
||||
array[i + 2] = rasterizedGlyph.size.x / this._dimensions.scaledCanvasWidth;
|
||||
array[i + 3] = rasterizedGlyph.size.y / this._dimensions.scaledCanvasHeight;
|
||||
array[w.i + 2] = w.glyph.size.x / this._dimensions.scaledCanvasWidth;
|
||||
array[w.i + 3] = w.glyph.size.y / this._dimensions.scaledCanvasHeight;
|
||||
// a_texcoord
|
||||
array[i + 4] = rasterizedGlyph.texturePositionClipSpace.x;
|
||||
array[i + 5] = rasterizedGlyph.texturePositionClipSpace.y;
|
||||
array[w.i + 4] = w.glyph.texturePositionClipSpace.x;
|
||||
array[w.i + 5] = w.glyph.texturePositionClipSpace.y;
|
||||
// a_texsize
|
||||
array[i + 6] = rasterizedGlyph.sizeClipSpace.x;
|
||||
array[i + 7] = rasterizedGlyph.sizeClipSpace.y;
|
||||
array[w.i + 6] = w.glyph.sizeClipSpace.x;
|
||||
array[w.i + 7] = w.glyph.sizeClipSpace.y;
|
||||
}
|
||||
// a_cellpos only changes on resize
|
||||
}
|
||||
|
||||
@@ -28,12 +28,11 @@ layout (location = ${VertexAttribLocations.COLOR}) in vec4 a_color;
|
||||
layout (location = ${VertexAttribLocations.UNIT_QUAD}) in vec2 a_unitquad;
|
||||
|
||||
uniform mat4 u_projection;
|
||||
uniform vec2 u_resolution;
|
||||
|
||||
out vec4 v_color;
|
||||
|
||||
void main() {
|
||||
vec2 zeroToOne = (a_position + (a_unitquad * a_size)) / u_resolution;
|
||||
vec2 zeroToOne = a_position + (a_unitquad * a_size);
|
||||
gl_Position = u_projection * vec4(zeroToOne, 0.0, 1.0);
|
||||
v_color = a_color;
|
||||
}`;
|
||||
@@ -59,11 +58,22 @@ const BYTES_PER_RECTANGLE = INDICES_PER_RECTANGLE * Float32Array.BYTES_PER_ELEME
|
||||
|
||||
const INITIAL_BUFFER_RECTANGLE_CAPACITY = 20 * INDICES_PER_RECTANGLE;
|
||||
|
||||
/** Work variables to avoid garbage collection. */
|
||||
const w: { rgba: number, isDefault: boolean, x1: number, y1: number, r: number, g: number, b: number, a: number } = {
|
||||
rgba: 0,
|
||||
isDefault: false,
|
||||
x1: 0,
|
||||
y1: 0,
|
||||
r: 0,
|
||||
g: 0,
|
||||
b: 0,
|
||||
a: 0
|
||||
};
|
||||
|
||||
export class RectangleRenderer extends Disposable {
|
||||
|
||||
private _program: WebGLProgram;
|
||||
private _vertexArrayObject: IWebGLVertexArrayObject;
|
||||
private _resolutionLocation: WebGLUniformLocation;
|
||||
private _attributesBuffer: WebGLBuffer;
|
||||
private _projectionLocation: WebGLUniformLocation;
|
||||
private _bgFloat!: Float32Array;
|
||||
@@ -87,7 +97,6 @@ export class RectangleRenderer extends Disposable {
|
||||
this.register(toDisposable(() => gl.deleteProgram(this._program)));
|
||||
|
||||
// Uniform locations
|
||||
this._resolutionLocation = throwIfFalsy(gl.getUniformLocation(this._program, 'u_resolution'));
|
||||
this._projectionLocation = throwIfFalsy(gl.getUniformLocation(this._program, 'u_projection'));
|
||||
|
||||
// Create and set the vertex array object
|
||||
@@ -104,7 +113,7 @@ export class RectangleRenderer extends Disposable {
|
||||
gl.vertexAttribPointer(VertexAttribLocations.UNIT_QUAD, 2, this._gl.FLOAT, false, 0, 0);
|
||||
|
||||
// Setup the unit quad element array buffer, this points to indices in
|
||||
// unitQuadVertuces to allow is to draw 2 triangles from the vertices
|
||||
// unitQuadVertices to allow is to draw 2 triangles from the vertices
|
||||
const unitQuadElementIndices = new Uint8Array([0, 1, 3, 0, 2, 3]);
|
||||
const elementIndicesBuffer = gl.createBuffer();
|
||||
this.register(toDisposable(() => gl.deleteBuffer(elementIndicesBuffer)));
|
||||
@@ -136,7 +145,6 @@ export class RectangleRenderer extends Disposable {
|
||||
gl.bindVertexArray(this._vertexArrayObject);
|
||||
|
||||
gl.uniformMatrix4fv(this._projectionLocation, false, PROJECTION_MATRIX);
|
||||
gl.uniform2f(this._resolutionLocation, gl.canvas.width, gl.canvas.height);
|
||||
|
||||
// Bind attributes buffer and draw
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
|
||||
@@ -153,6 +161,10 @@ export class RectangleRenderer extends Disposable {
|
||||
this._updateViewportRectangle();
|
||||
}
|
||||
|
||||
public setDimensions(dimensions: IRenderDimensions): void {
|
||||
this._dimensions = dimensions;
|
||||
}
|
||||
|
||||
private _updateCachedColors(): void {
|
||||
this._bgFloat = this._colorToFloat32Array(this._colors.background);
|
||||
}
|
||||
@@ -174,22 +186,34 @@ export class RectangleRenderer extends Disposable {
|
||||
const terminal = this._terminal;
|
||||
const vertices = this._vertices;
|
||||
|
||||
// Declare variable ahead of time to avoid garbage collection
|
||||
let rectangleCount = 1;
|
||||
let y: number;
|
||||
let x: number;
|
||||
let currentStartX: number;
|
||||
let currentBg: number;
|
||||
let currentFg: number;
|
||||
let currentInverse: boolean;
|
||||
let modelIndex: number;
|
||||
let bg: number;
|
||||
let fg: number;
|
||||
let inverse: boolean;
|
||||
let offset: number;
|
||||
|
||||
for (let y = 0; y < terminal.rows; y++) {
|
||||
let currentStartX = -1;
|
||||
let currentBg = 0;
|
||||
let currentFg = 0;
|
||||
let currentInverse = false;
|
||||
for (let x = 0; x < terminal.cols; x++) {
|
||||
const modelIndex = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
|
||||
const bg = model.cells[modelIndex + RENDER_MODEL_BG_OFFSET];
|
||||
const fg = model.cells[modelIndex + RENDER_MODEL_FG_OFFSET];
|
||||
const inverse = !!(fg & FgFlags.INVERSE);
|
||||
for (y = 0; y < terminal.rows; y++) {
|
||||
currentStartX = -1;
|
||||
currentBg = 0;
|
||||
currentFg = 0;
|
||||
currentInverse = false;
|
||||
for (x = 0; x < terminal.cols; x++) {
|
||||
modelIndex = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
|
||||
bg = model.cells[modelIndex + RENDER_MODEL_BG_OFFSET];
|
||||
fg = model.cells[modelIndex + RENDER_MODEL_FG_OFFSET];
|
||||
inverse = !!(fg & FgFlags.INVERSE);
|
||||
if (bg !== currentBg || (fg !== currentFg && (currentInverse || inverse))) {
|
||||
// A rectangle needs to be drawn if going from non-default to another color
|
||||
if (currentBg !== 0 || (currentInverse && currentFg !== 0)) {
|
||||
const offset = rectangleCount++ * INDICES_PER_RECTANGLE;
|
||||
offset = rectangleCount++ * INDICES_PER_RECTANGLE;
|
||||
this._updateRectangle(vertices, offset, currentFg, currentBg, currentStartX, x, y);
|
||||
}
|
||||
currentStartX = x;
|
||||
@@ -200,7 +224,7 @@ export class RectangleRenderer extends Disposable {
|
||||
}
|
||||
// Finish rectangle if it's still going
|
||||
if (currentBg !== 0 || (currentInverse && currentFg !== 0)) {
|
||||
const offset = rectangleCount++ * INDICES_PER_RECTANGLE;
|
||||
offset = rectangleCount++ * INDICES_PER_RECTANGLE;
|
||||
this._updateRectangle(vertices, offset, currentFg, currentBg, currentStartX, terminal.cols, y);
|
||||
}
|
||||
}
|
||||
@@ -208,55 +232,54 @@ export class RectangleRenderer extends Disposable {
|
||||
}
|
||||
|
||||
private _updateRectangle(vertices: IVertices, offset: number, fg: number, bg: number, startX: number, endX: number, y: number): void {
|
||||
let rgba: number | undefined;
|
||||
let isDefault = false;
|
||||
w.isDefault = false;
|
||||
if (fg & FgFlags.INVERSE) {
|
||||
switch (fg & Attributes.CM_MASK) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256:
|
||||
rgba = this._colors.ansi[fg & Attributes.PCOLOR_MASK].rgba;
|
||||
w.rgba = this._colors.ansi[fg & Attributes.PCOLOR_MASK].rgba;
|
||||
break;
|
||||
case Attributes.CM_RGB:
|
||||
rgba = (fg & Attributes.RGB_MASK) << 8;
|
||||
w.rgba = (fg & Attributes.RGB_MASK) << 8;
|
||||
break;
|
||||
case Attributes.CM_DEFAULT:
|
||||
default:
|
||||
rgba = this._colors.foreground.rgba;
|
||||
w.rgba = this._colors.foreground.rgba;
|
||||
}
|
||||
} else {
|
||||
switch (bg & Attributes.CM_MASK) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256:
|
||||
rgba = this._colors.ansi[bg & Attributes.PCOLOR_MASK].rgba;
|
||||
w.rgba = this._colors.ansi[bg & Attributes.PCOLOR_MASK].rgba;
|
||||
break;
|
||||
case Attributes.CM_RGB:
|
||||
rgba = (bg & Attributes.RGB_MASK) << 8;
|
||||
w.rgba = (bg & Attributes.RGB_MASK) << 8;
|
||||
break;
|
||||
case Attributes.CM_DEFAULT:
|
||||
default:
|
||||
rgba = this._colors.background.rgba;
|
||||
isDefault = true;
|
||||
w.rgba = this._colors.background.rgba;
|
||||
w.isDefault = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (vertices.attributes.length < offset + 4) {
|
||||
vertices.attributes = expandFloat32Array(vertices.attributes, this._terminal.rows * this._terminal.cols * INDICES_PER_RECTANGLE);
|
||||
}
|
||||
const x1 = startX * this._dimensions.scaledCellWidth;
|
||||
const y1 = y * this._dimensions.scaledCellHeight;
|
||||
const r = ((rgba >> 24) & 0xFF) / 255;
|
||||
const g = ((rgba >> 16) & 0xFF) / 255;
|
||||
const b = ((rgba >> 8 ) & 0xFF) / 255;
|
||||
const a = (!isDefault && bg & BgFlags.DIM) ? DIM_OPACITY : 1;
|
||||
w.x1 = startX * this._dimensions.scaledCellWidth;
|
||||
w.y1 = y * this._dimensions.scaledCellHeight;
|
||||
w.r = ((w.rgba >> 24) & 0xFF) / 255;
|
||||
w.g = ((w.rgba >> 16) & 0xFF) / 255;
|
||||
w.b = ((w.rgba >> 8 ) & 0xFF) / 255;
|
||||
w.a = (!w.isDefault && bg & BgFlags.DIM) ? DIM_OPACITY : 1;
|
||||
|
||||
this._addRectangle(vertices.attributes, offset, x1, y1, (endX - startX) * this._dimensions.scaledCellWidth, this._dimensions.scaledCellHeight, r, g, b, a);
|
||||
this._addRectangle(vertices.attributes, offset, w.x1, w.y1, (endX - startX) * this._dimensions.scaledCellWidth, this._dimensions.scaledCellHeight, w.r, w.g, w.b, w.a);
|
||||
}
|
||||
|
||||
private _addRectangle(array: Float32Array, offset: number, x1: number, y1: number, width: number, height: number, r: number, g: number, b: number, a: number): void {
|
||||
array[offset ] = x1;
|
||||
array[offset + 1] = y1;
|
||||
array[offset + 2] = width;
|
||||
array[offset + 3] = height;
|
||||
array[offset ] = x1 / this._dimensions.scaledCanvasWidth;
|
||||
array[offset + 1] = y1 / this._dimensions.scaledCanvasHeight;
|
||||
array[offset + 2] = width / this._dimensions.scaledCanvasWidth;
|
||||
array[offset + 3] = height / this._dimensions.scaledCanvasHeight;
|
||||
array[offset + 4] = r;
|
||||
array[offset + 5] = g;
|
||||
array[offset + 6] = b;
|
||||
@@ -264,10 +287,10 @@ export class RectangleRenderer extends Disposable {
|
||||
}
|
||||
|
||||
private _addRectangleFloat(array: Float32Array, offset: number, x1: number, y1: number, width: number, height: number, color: Float32Array): void {
|
||||
array[offset ] = x1;
|
||||
array[offset + 1] = y1;
|
||||
array[offset + 2] = width;
|
||||
array[offset + 3] = height;
|
||||
array[offset ] = x1 / this._dimensions.scaledCanvasWidth;
|
||||
array[offset + 1] = y1 / this._dimensions.scaledCanvasHeight;
|
||||
array[offset + 2] = width / this._dimensions.scaledCanvasWidth;
|
||||
array[offset + 3] = height / this._dimensions.scaledCanvasHeight;
|
||||
array[offset + 4] = color[0];
|
||||
array[offset + 5] = color[1];
|
||||
array[offset + 6] = color[2];
|
||||
|
||||
@@ -11,7 +11,7 @@ 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, toDisposable } from 'common/Lifecycle';
|
||||
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';
|
||||
@@ -22,10 +22,19 @@ import { EventEmitter } from 'common/EventEmitter';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
import { ICharacterJoinerService, ICoreBrowserService } from 'browser/services/Services';
|
||||
import { CharData, ICellData } from 'common/Types';
|
||||
import { CharData, IBufferLine, ICellData } from 'common/Types';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { ICoreService, IDecorationService } from 'common/services/Services';
|
||||
|
||||
/** Work variables to avoid garbage collection. */
|
||||
const w: { fg: number, bg: number, hasFg: boolean, hasBg: boolean, isSelected: boolean } = {
|
||||
fg: 0,
|
||||
bg: 0,
|
||||
hasFg: false,
|
||||
hasBg: false,
|
||||
isSelected: false
|
||||
};
|
||||
|
||||
export class WebglRenderer extends Disposable implements IRenderer {
|
||||
private _renderLayers: IRenderLayer[];
|
||||
private _charAtlas: WebglCharAtlas | undefined;
|
||||
@@ -173,6 +182,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
this._core.screenElement!.style.width = `${this.dimensions.canvasWidth}px`;
|
||||
this._core.screenElement!.style.height = `${this.dimensions.canvasHeight}px`;
|
||||
|
||||
this._rectangleRenderer.setDimensions(this.dimensions);
|
||||
this._rectangleRenderer.onResize();
|
||||
this._glyphRenderer.setDimensions(this.dimensions);
|
||||
this._glyphRenderer.onResize();
|
||||
@@ -305,14 +315,28 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
private _updateModel(start: number, end: number): void {
|
||||
const terminal = this._core;
|
||||
let cell: ICellData = this._workCell;
|
||||
let lastBg: number = 0;
|
||||
|
||||
for (let y = start; y <= end; y++) {
|
||||
const row = y + terminal.buffer.ydisp;
|
||||
const line = terminal.buffer.lines.get(row)!;
|
||||
// Declare variable ahead of time to avoid garbage collection
|
||||
let lastBg: number;
|
||||
let y: number;
|
||||
let row: number;
|
||||
let line: IBufferLine;
|
||||
let joinedRanges: [number, number][];
|
||||
let isJoined: boolean;
|
||||
let lastCharX: number;
|
||||
let range: [number, number];
|
||||
let chars: string;
|
||||
let code: number;
|
||||
let i: number;
|
||||
let x: number;
|
||||
let j: number;
|
||||
|
||||
for (y = start; y <= end; y++) {
|
||||
row = y + terminal.buffer.ydisp;
|
||||
line = terminal.buffer.lines.get(row)!;
|
||||
this._model.lineLengths[y] = 0;
|
||||
const joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
|
||||
for (let x = 0; x < terminal.cols; x++) {
|
||||
joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
|
||||
for (x = 0; x < terminal.cols; x++) {
|
||||
lastBg = this._workColors.bg;
|
||||
line.loadCell(x, cell);
|
||||
|
||||
@@ -321,15 +345,15 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
}
|
||||
|
||||
// If true, indicates that the current character(s) to draw were joined.
|
||||
let isJoined = false;
|
||||
let lastCharX = x;
|
||||
isJoined = false;
|
||||
lastCharX = x;
|
||||
|
||||
// Process any joined character ranges as needed. Because of how the
|
||||
// ranges are produced, we know that they are valid for the characters
|
||||
// and attributes of our input.
|
||||
if (joinedRanges.length > 0 && x === joinedRanges[0][0]) {
|
||||
isJoined = true;
|
||||
const range = joinedRanges.shift()!;
|
||||
range = joinedRanges.shift()!;
|
||||
|
||||
// We already know the exact start and end column of the joined range,
|
||||
// so we get the string and width representing it directly.
|
||||
@@ -343,9 +367,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
lastCharX = range[1] - 1;
|
||||
}
|
||||
|
||||
const chars = cell.getChars();
|
||||
let code = cell.getCode();
|
||||
const i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
|
||||
chars = cell.getChars();
|
||||
code = cell.getCode();
|
||||
i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
|
||||
|
||||
// Load colors/resolve overrides into work colors
|
||||
this._loadColorsForCell(x, row);
|
||||
@@ -381,7 +405,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
|
||||
// Null out non-first cells
|
||||
for (x++; x < lastCharX; x++) {
|
||||
const j = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
|
||||
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;
|
||||
@@ -404,79 +428,91 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
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
|
||||
let bgOverride: number | undefined;
|
||||
let fgOverride: number | undefined;
|
||||
let isSelected: boolean = false;
|
||||
|
||||
// Reset overrides work variables
|
||||
w.bg = 0;
|
||||
w.fg = 0;
|
||||
w.hasBg = false;
|
||||
w.hasFg = false;
|
||||
w.isSelected = false;
|
||||
|
||||
// Apply decorations on the bottom layer
|
||||
for (const d of this._decorationService.getDecorationsAtCell(x, y, 'bottom')) {
|
||||
this._decorationService.forEachDecorationAtCell(x, y, 'bottom', d => {
|
||||
if (d.backgroundColorRGB) {
|
||||
bgOverride = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
w.bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
w.hasBg = true;
|
||||
}
|
||||
if (d.foregroundColorRGB) {
|
||||
fgOverride = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
w.fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
w.hasFg = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Apply the selection color if needed
|
||||
isSelected = this._isCellSelected(x, y);
|
||||
if (isSelected) {
|
||||
bgOverride = (this._coreBrowserService.isFocused ? this._colors.selectionBackgroundOpaque : this._colors.selectionInactiveBackgroundOpaque).rgba >> 8 & 0xFFFFFF;
|
||||
w.isSelected = this._isCellSelected(x, y);
|
||||
if (w.isSelected) {
|
||||
w.bg = (this._coreBrowserService.isFocused ? this._colors.selectionBackgroundOpaque : this._colors.selectionInactiveBackgroundOpaque).rgba >> 8 & 0xFFFFFF;
|
||||
w.hasBg = true;
|
||||
if (this._colors.selectionForeground) {
|
||||
fgOverride = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
|
||||
w.fg = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
|
||||
w.hasFg = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply decorations on the top layer
|
||||
for (const d of this._decorationService.getDecorationsAtCell(x, y, 'top')) {
|
||||
this._decorationService.forEachDecorationAtCell(x, y, 'top', d => {
|
||||
if (d.backgroundColorRGB) {
|
||||
bgOverride = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
w.bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
w.hasBg = true;
|
||||
}
|
||||
if (d.foregroundColorRGB) {
|
||||
fgOverride = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
w.fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
w.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 (bgOverride !== undefined) {
|
||||
if (isSelected) {
|
||||
if (w.hasBg) {
|
||||
if (w.isSelected) {
|
||||
// Non-RGB attributes from model + force non-dim + override + force RGB color mode
|
||||
bgOverride = (this._workCell.bg & ~Attributes.RGB_MASK & ~BgFlags.DIM) | bgOverride | Attributes.CM_RGB;
|
||||
w.bg = (this._workCell.bg & ~Attributes.RGB_MASK & ~BgFlags.DIM) | w.bg | Attributes.CM_RGB;
|
||||
} else {
|
||||
// Non-RGB attributes from model + override + force RGB color mode
|
||||
bgOverride = (this._workCell.bg & ~Attributes.RGB_MASK) | bgOverride | Attributes.CM_RGB;
|
||||
w.bg = (this._workCell.bg & ~Attributes.RGB_MASK) | w.bg | Attributes.CM_RGB;
|
||||
}
|
||||
}
|
||||
if (fgOverride !== undefined) {
|
||||
if (w.hasFg) {
|
||||
// Non-RGB attributes from model + force disable inverse + override + force RGB color mode
|
||||
fgOverride = (this._workCell.fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | fgOverride | Attributes.CM_RGB;
|
||||
w.fg = (this._workCell.fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | w.fg | Attributes.CM_RGB;
|
||||
}
|
||||
|
||||
// Handle case where inverse was specified by only one of bgOverride or fgOverride was set,
|
||||
// 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 (bgOverride !== undefined && fgOverride === undefined) {
|
||||
if (w.hasBg && !w.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) {
|
||||
fgOverride = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | ((this._colors.background.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
|
||||
w.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 {
|
||||
fgOverride = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | this._workColors.bg & (Attributes.RGB_MASK | Attributes.CM_MASK);
|
||||
w.fg = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | this._workColors.bg & (Attributes.RGB_MASK | Attributes.CM_MASK);
|
||||
}
|
||||
w.hasFg = true;
|
||||
}
|
||||
if (bgOverride === undefined && fgOverride !== undefined) {
|
||||
if (!w.hasBg && w.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) {
|
||||
bgOverride = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | ((this._colors.foreground.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
|
||||
w.bg = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | ((this._colors.foreground.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
|
||||
} else {
|
||||
bgOverride = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | this._workColors.fg & (Attributes.RGB_MASK | Attributes.CM_MASK);
|
||||
w.bg = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | this._workColors.fg & (Attributes.RGB_MASK | Attributes.CM_MASK);
|
||||
}
|
||||
w.hasBg = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Use the override if it exists
|
||||
this._workColors.bg = bgOverride ?? this._workColors.bg;
|
||||
this._workColors.fg = fgOverride ?? this._workColors.fg;
|
||||
this._workColors.bg = w.hasBg ? w.bg : this._workColors.bg;
|
||||
this._workColors.fg = w.hasFg ? w.fg : this._workColors.fg;
|
||||
}
|
||||
|
||||
private _isCellSelected(x: number, y: number): boolean {
|
||||
@@ -587,11 +623,11 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
}
|
||||
|
||||
private _setCanvasDevicePixelDimensions(width: number, height: number): void {
|
||||
if (this.dimensions.scaledCanvasWidth === width && this.dimensions.scaledCanvasHeight === height) {
|
||||
if (this._canvas.width === width && this._canvas.height === height) {
|
||||
return;
|
||||
}
|
||||
this.dimensions.scaledCanvasWidth = width;
|
||||
this.dimensions.scaledCanvasHeight = height;
|
||||
// While the actual canvas size has changed, keep scaledCanvasWidth/Height as the value before
|
||||
// the change as it's an exact multiple of the cell sizes.
|
||||
this._canvas.width = width;
|
||||
this._canvas.height = height;
|
||||
this._requestRedrawViewport();
|
||||
|
||||
@@ -7,7 +7,7 @@ import { generateConfig, configEquals } from './CharAtlasUtils';
|
||||
import { WebglCharAtlas } from './WebglCharAtlas';
|
||||
import { ICharAtlasConfig } from './Types';
|
||||
import { Terminal } from 'xterm';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IColorSet, ITerminal } from 'browser/Types';
|
||||
|
||||
interface ICharAtlasCacheEntry {
|
||||
atlas: WebglCharAtlas;
|
||||
@@ -64,8 +64,9 @@ export function acquireCharAtlas(
|
||||
}
|
||||
}
|
||||
|
||||
const core: ITerminal = (terminal as any)._core;
|
||||
const newEntry: ICharAtlasCacheEntry = {
|
||||
atlas: new WebglCharAtlas(document, newConfig),
|
||||
atlas: new WebglCharAtlas(document, newConfig, core.unicodeService),
|
||||
config: newConfig,
|
||||
ownedBy: [terminal]
|
||||
};
|
||||
|
||||
@@ -13,7 +13,9 @@ 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 } from 'browser/renderer/RendererUtils';
|
||||
import { excludeFromContrastRatioDemands, isPowerlineGlyph, isRestrictedPowerlineGlyph } from 'browser/renderer/RendererUtils';
|
||||
import { IUnicodeService } from 'common/services/Services';
|
||||
import { FourKeyMap } from 'common/MultiKeyMap';
|
||||
|
||||
// For debugging purposes, it can be useful to set this to a really tiny value,
|
||||
// to verify that LRU eviction works.
|
||||
@@ -51,11 +53,16 @@ interface ICharAtlasActiveRow {
|
||||
height: number;
|
||||
}
|
||||
|
||||
/** Work variables to avoid garbage collection. */
|
||||
const w: { glyph: IRasterizedGlyph | undefined } = {
|
||||
glyph: undefined
|
||||
};
|
||||
|
||||
export class WebglCharAtlas implements IDisposable {
|
||||
private _didWarmUp: boolean = false;
|
||||
|
||||
private _cacheMap: { [code: number]: IRasterizedGlyphSet } = {};
|
||||
private _cacheMapCombined: { [chars: string]: IRasterizedGlyphSet } = {};
|
||||
private _cacheMap: FourKeyMap<number, number, number, number, IRasterizedGlyph> = new FourKeyMap();
|
||||
private _cacheMapCombined: FourKeyMap<string, number, number, number, IRasterizedGlyph> = new FourKeyMap();
|
||||
|
||||
// The texture that the atlas is drawn to
|
||||
public cacheCanvas: HTMLCanvasElement;
|
||||
@@ -89,7 +96,8 @@ export class WebglCharAtlas implements IDisposable {
|
||||
|
||||
constructor(
|
||||
document: Document,
|
||||
private _config: ICharAtlasConfig
|
||||
private readonly _config: ICharAtlasConfig,
|
||||
private readonly _unicodeService: IUnicodeService
|
||||
) {
|
||||
this.cacheCanvas = document.createElement('canvas');
|
||||
this.cacheCanvas.width = TEXTURE_WIDTH;
|
||||
@@ -122,13 +130,7 @@ export class WebglCharAtlas implements IDisposable {
|
||||
// Pre-fill with ASCII 33-126
|
||||
for (let i = 33; i < 126; i++) {
|
||||
const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT);
|
||||
this._cacheMap[i] = {
|
||||
[DEFAULT_COLOR]: {
|
||||
[DEFAULT_COLOR]: {
|
||||
[DEFAULT_EXT]: rasterizedGlyph
|
||||
}
|
||||
}
|
||||
};
|
||||
this._cacheMap.set(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT, rasterizedGlyph);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,8 +148,8 @@ export class WebglCharAtlas implements IDisposable {
|
||||
return;
|
||||
}
|
||||
this._cacheCtx.clearRect(0, 0, TEXTURE_WIDTH, TEXTURE_HEIGHT);
|
||||
this._cacheMap = {};
|
||||
this._cacheMapCombined = {};
|
||||
this._cacheMap.clear();
|
||||
this._cacheMapCombined.clear();
|
||||
this._currentRow.x = 0;
|
||||
this._currentRow.y = 0;
|
||||
this._currentRow.height = 0;
|
||||
@@ -167,39 +169,18 @@ export class WebglCharAtlas implements IDisposable {
|
||||
* Gets the glyphs texture coords, drawing the texture if it's not already
|
||||
*/
|
||||
private _getFromCacheMap(
|
||||
cacheMap: { [key: string | number]: IRasterizedGlyphSet },
|
||||
cacheMap: FourKeyMap<string | number, number, number, number, IRasterizedGlyph>,
|
||||
key: string | number,
|
||||
bg: number,
|
||||
fg: number,
|
||||
ext: number
|
||||
): IRasterizedGlyph {
|
||||
let rasterizedGlyphSet = cacheMap[key];
|
||||
if (!rasterizedGlyphSet) {
|
||||
rasterizedGlyphSet = {};
|
||||
cacheMap[key] = rasterizedGlyphSet;
|
||||
w.glyph = cacheMap.get(key, bg, fg, ext);
|
||||
if (!w.glyph) {
|
||||
w.glyph = this._drawToCache(key, bg, fg, ext);
|
||||
cacheMap.set(key, bg, fg, ext, w.glyph);
|
||||
}
|
||||
|
||||
let rasterizedGlyphSetBg = rasterizedGlyphSet[bg];
|
||||
if (!rasterizedGlyphSetBg) {
|
||||
rasterizedGlyphSetBg = {};
|
||||
rasterizedGlyphSet[bg] = rasterizedGlyphSetBg;
|
||||
}
|
||||
|
||||
let rasterizedGlyph: IRasterizedGlyph | undefined;
|
||||
let rasterizedGlyphSetFg = rasterizedGlyphSetBg[fg];
|
||||
if (!rasterizedGlyphSetFg) {
|
||||
rasterizedGlyphSetFg = {};
|
||||
rasterizedGlyphSetBg[fg] = rasterizedGlyphSetFg;
|
||||
} else {
|
||||
rasterizedGlyph = rasterizedGlyphSetFg[ext];
|
||||
}
|
||||
|
||||
if (!rasterizedGlyph) {
|
||||
rasterizedGlyph = this._drawToCache(key, bg, fg, ext);
|
||||
rasterizedGlyphSetFg[ext] = rasterizedGlyph;
|
||||
}
|
||||
|
||||
return rasterizedGlyph;
|
||||
return w.glyph;
|
||||
}
|
||||
|
||||
private _getColorFromAnsiIndex(idx: number): IColor {
|
||||
@@ -418,6 +399,7 @@ export class WebglCharAtlas implements IDisposable {
|
||||
this._tmpCtx.textBaseline = TEXT_BASELINE;
|
||||
|
||||
const powerlineGlyph = chars.length === 1 && isPowerlineGlyph(chars.charCodeAt(0));
|
||||
const restrictedPowerlineGlyph = chars.length === 1 && isRestrictedPowerlineGlyph(chars.charCodeAt(0));
|
||||
const foregroundColor = this._getForegroundColor(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, dim, bold, excludeFromContrastRatioDemands(chars.charCodeAt(0)));
|
||||
this._tmpCtx.fillStyle = foregroundColor.css;
|
||||
|
||||
@@ -427,7 +409,7 @@ export class WebglCharAtlas implements IDisposable {
|
||||
// Draw custom characters if applicable
|
||||
let customGlyph = false;
|
||||
if (this._config.customGlyphs !== false) {
|
||||
customGlyph = tryDrawCustomChar(this._tmpCtx, chars, padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight);
|
||||
customGlyph = tryDrawCustomChar(this._tmpCtx, chars, padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight, this._config.fontSize);
|
||||
}
|
||||
|
||||
// Whether to clear pixels based on a threshold difference between the glyph color and the
|
||||
@@ -435,6 +417,13 @@ export class WebglCharAtlas implements IDisposable {
|
||||
// underline colors to prevent important colors could get cleared.
|
||||
let enableClearThresholdCheck = !powerlineGlyph;
|
||||
|
||||
let chWidth: number;
|
||||
if (typeof codeOrChars === 'number') {
|
||||
chWidth = this._unicodeService.wcwidth(codeOrChars);
|
||||
} else {
|
||||
chWidth = this._unicodeService.getStringCellWidth(codeOrChars);
|
||||
}
|
||||
|
||||
// Draw underline
|
||||
if (underline) {
|
||||
this._tmpCtx.save();
|
||||
@@ -461,69 +450,76 @@ export class WebglCharAtlas implements IDisposable {
|
||||
// Underline style/stroke
|
||||
this._tmpCtx.beginPath();
|
||||
const xLeft = padding;
|
||||
const xRight = padding + this._config.scaledCellWidth;
|
||||
const xRight = padding + this._config.scaledCellWidth * chWidth;
|
||||
const yTop = Math.ceil(padding + this._config.scaledCharHeight) - yOffset;
|
||||
const yMid = padding + this._config.scaledCharHeight + lineWidth - yOffset;
|
||||
const yBot = Math.ceil(padding + this._config.scaledCharHeight + lineWidth * 2) - yOffset;
|
||||
switch (this._workAttributeData.extended.underlineStyle) {
|
||||
case UnderlineStyle.DOUBLE:
|
||||
this._tmpCtx.moveTo(xLeft, yTop);
|
||||
this._tmpCtx.lineTo(xRight, yTop);
|
||||
this._tmpCtx.moveTo(xLeft, yBot);
|
||||
this._tmpCtx.lineTo(xRight, yBot);
|
||||
break;
|
||||
case UnderlineStyle.CURLY:
|
||||
const xMid = padding + this._config.scaledCellWidth / 2;
|
||||
// Choose the bezier top and bottom based on the device pixel ratio, the curly line is
|
||||
// made taller when the line width is as otherwise it's not very clear otherwise.
|
||||
const yCurlyBot = lineWidth <= 1 ? yBot : Math.ceil(padding + this._config.scaledCharHeight - lineWidth / 2) - yOffset;
|
||||
const yCurlyTop = lineWidth <= 1 ? yTop : Math.ceil(padding + this._config.scaledCharHeight + lineWidth / 2) - yOffset;
|
||||
// Clip the left and right edges of the underline such that it can be drawn just outside
|
||||
// the edge of the cell to ensure a continuous stroke when there are multiple underlined
|
||||
// glyphs adjacent to one another.
|
||||
const clipRegion = new Path2D();
|
||||
clipRegion.rect(xLeft, yTop, this._config.scaledCellWidth, yBot - yTop);
|
||||
this._tmpCtx.clip(clipRegion);
|
||||
// Start 1/2 cell before and end 1/2 cells after to ensure a smooth curve with other cells
|
||||
this._tmpCtx.moveTo(xLeft - this._config.scaledCellWidth / 2, yMid);
|
||||
this._tmpCtx.bezierCurveTo(
|
||||
xLeft - this._config.scaledCellWidth / 2, yCurlyTop,
|
||||
xLeft, yCurlyTop,
|
||||
xLeft, yMid
|
||||
);
|
||||
this._tmpCtx.bezierCurveTo(
|
||||
xLeft, yCurlyBot,
|
||||
xMid, yCurlyBot,
|
||||
xMid, yMid
|
||||
);
|
||||
this._tmpCtx.bezierCurveTo(
|
||||
xMid, yCurlyTop,
|
||||
xRight, yCurlyTop,
|
||||
xRight, yMid
|
||||
);
|
||||
this._tmpCtx.bezierCurveTo(
|
||||
xRight, yCurlyBot,
|
||||
xRight + this._config.scaledCellWidth / 2, yCurlyBot,
|
||||
xRight + this._config.scaledCellWidth / 2, yMid
|
||||
);
|
||||
break;
|
||||
case UnderlineStyle.DOTTED:
|
||||
this._tmpCtx.setLineDash([window.devicePixelRatio * 2, window.devicePixelRatio]);
|
||||
this._tmpCtx.moveTo(xLeft, yTop);
|
||||
this._tmpCtx.lineTo(xRight, yTop);
|
||||
break;
|
||||
case UnderlineStyle.DASHED:
|
||||
this._tmpCtx.setLineDash([window.devicePixelRatio * 4, window.devicePixelRatio * 3]);
|
||||
this._tmpCtx.moveTo(xLeft, yTop);
|
||||
this._tmpCtx.lineTo(xRight, yTop);
|
||||
break;
|
||||
case UnderlineStyle.SINGLE:
|
||||
default:
|
||||
this._tmpCtx.moveTo(xLeft, yTop);
|
||||
this._tmpCtx.lineTo(xRight, yTop);
|
||||
break;
|
||||
|
||||
for (let i = 0; i < chWidth; i++) {
|
||||
this._tmpCtx.save();
|
||||
const xChLeft = xLeft + i * this._config.scaledCellWidth;
|
||||
const xChRight = xLeft + (i + 1) * this._config.scaledCellWidth;
|
||||
const xChMid = xChLeft + this._config.scaledCellWidth / 2;
|
||||
switch (this._workAttributeData.extended.underlineStyle) {
|
||||
case UnderlineStyle.DOUBLE:
|
||||
this._tmpCtx.moveTo(xChLeft, yTop);
|
||||
this._tmpCtx.lineTo(xChRight, yTop);
|
||||
this._tmpCtx.moveTo(xChLeft, yBot);
|
||||
this._tmpCtx.lineTo(xChRight, yBot);
|
||||
break;
|
||||
case UnderlineStyle.CURLY:
|
||||
// Choose the bezier top and bottom based on the device pixel ratio, the curly line is
|
||||
// made taller when the line width is as otherwise it's not very clear otherwise.
|
||||
const yCurlyBot = lineWidth <= 1 ? yBot : Math.ceil(padding + this._config.scaledCharHeight - lineWidth / 2) - yOffset;
|
||||
const yCurlyTop = lineWidth <= 1 ? yTop : Math.ceil(padding + this._config.scaledCharHeight + lineWidth / 2) - yOffset;
|
||||
// Clip the left and right edges of the underline such that it can be drawn just outside
|
||||
// the edge of the cell to ensure a continuous stroke when there are multiple underlined
|
||||
// glyphs adjacent to one another.
|
||||
const clipRegion = new Path2D();
|
||||
clipRegion.rect(xChLeft, yTop, this._config.scaledCellWidth, yBot - yTop);
|
||||
this._tmpCtx.clip(clipRegion);
|
||||
// Start 1/2 cell before and end 1/2 cells after to ensure a smooth curve with other cells
|
||||
this._tmpCtx.moveTo(xChLeft - this._config.scaledCellWidth / 2, yMid);
|
||||
this._tmpCtx.bezierCurveTo(
|
||||
xChLeft - this._config.scaledCellWidth / 2, yCurlyTop,
|
||||
xChLeft, yCurlyTop,
|
||||
xChLeft, yMid
|
||||
);
|
||||
this._tmpCtx.bezierCurveTo(
|
||||
xChLeft, yCurlyBot,
|
||||
xChMid, yCurlyBot,
|
||||
xChMid, yMid
|
||||
);
|
||||
this._tmpCtx.bezierCurveTo(
|
||||
xChMid, yCurlyTop,
|
||||
xChRight, yCurlyTop,
|
||||
xChRight, yMid
|
||||
);
|
||||
this._tmpCtx.bezierCurveTo(
|
||||
xChRight, yCurlyBot,
|
||||
xChRight + this._config.scaledCellWidth / 2, yCurlyBot,
|
||||
xChRight + this._config.scaledCellWidth / 2, yMid
|
||||
);
|
||||
break;
|
||||
case UnderlineStyle.DOTTED:
|
||||
this._tmpCtx.setLineDash([window.devicePixelRatio * 2, window.devicePixelRatio]);
|
||||
this._tmpCtx.moveTo(xChLeft, yTop);
|
||||
this._tmpCtx.lineTo(xChRight, yTop);
|
||||
break;
|
||||
case UnderlineStyle.DASHED:
|
||||
this._tmpCtx.setLineDash([window.devicePixelRatio * 4, window.devicePixelRatio * 3]);
|
||||
this._tmpCtx.moveTo(xChLeft, yTop);
|
||||
this._tmpCtx.lineTo(xChRight, yTop);
|
||||
break;
|
||||
case UnderlineStyle.SINGLE:
|
||||
default:
|
||||
this._tmpCtx.moveTo(xChLeft, yTop);
|
||||
this._tmpCtx.lineTo(xChRight, yTop);
|
||||
break;
|
||||
}
|
||||
this._tmpCtx.stroke();
|
||||
this._tmpCtx.restore();
|
||||
}
|
||||
this._tmpCtx.stroke();
|
||||
this._tmpCtx.restore();
|
||||
|
||||
// Draw stroke in the background color for non custom characters in order to give an outline
|
||||
@@ -533,18 +529,26 @@ export class WebglCharAtlas implements IDisposable {
|
||||
// This only works when transparency is disabled because it's not clear how to clear stroked
|
||||
// text
|
||||
if (!this._config.allowTransparency && chars !== ' ') {
|
||||
// This translates to 1/2 the line width in either direction
|
||||
// Measure the text, only draw the stroke if there is a descent beyond an alphabetic text
|
||||
// baseline
|
||||
this._tmpCtx.save();
|
||||
// Clip the region to only draw in valid pixels near the underline to avoid a slight
|
||||
// outline around the whole glyph, as well as additional pixels in the glyph at the top
|
||||
// which would increase GPU memory demands
|
||||
const clipRegion = new Path2D();
|
||||
clipRegion.rect(xLeft, yTop - Math.ceil(lineWidth / 2), this._config.scaledCellWidth, yBot - yTop + Math.ceil(lineWidth / 2));
|
||||
this._tmpCtx.clip(clipRegion);
|
||||
this._tmpCtx.lineWidth = window.devicePixelRatio * 3;
|
||||
this._tmpCtx.strokeStyle = backgroundColor.css;
|
||||
this._tmpCtx.strokeText(chars, padding, padding + this._config.scaledCharHeight);
|
||||
this._tmpCtx.textBaseline = 'alphabetic';
|
||||
const metrics = this._tmpCtx.measureText(chars);
|
||||
this._tmpCtx.restore();
|
||||
if ('actualBoundingBoxDescent' in metrics && metrics.actualBoundingBoxDescent > 0) {
|
||||
// This translates to 1/2 the line width in either direction
|
||||
this._tmpCtx.save();
|
||||
// Clip the region to only draw in valid pixels near the underline to avoid a slight
|
||||
// outline around the whole glyph, as well as additional pixels in the glyph at the top
|
||||
// which would increase GPU memory demands
|
||||
const clipRegion = new Path2D();
|
||||
clipRegion.rect(xLeft, yTop - Math.ceil(lineWidth / 2), this._config.scaledCellWidth, yBot - yTop + Math.ceil(lineWidth / 2));
|
||||
this._tmpCtx.clip(clipRegion);
|
||||
this._tmpCtx.lineWidth = window.devicePixelRatio * 3;
|
||||
this._tmpCtx.strokeStyle = backgroundColor.css;
|
||||
this._tmpCtx.strokeText(chars, padding, padding + this._config.scaledCharHeight);
|
||||
this._tmpCtx.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -581,7 +585,7 @@ export class WebglCharAtlas implements IDisposable {
|
||||
this._tmpCtx.strokeStyle = this._tmpCtx.fillStyle;
|
||||
this._tmpCtx.beginPath();
|
||||
this._tmpCtx.moveTo(padding, padding + Math.floor(this._config.scaledCharHeight / 2) - yOffset);
|
||||
this._tmpCtx.lineTo(padding + this._config.scaledCharWidth, padding + Math.floor(this._config.scaledCharHeight / 2) - yOffset);
|
||||
this._tmpCtx.lineTo(padding + this._config.scaledCharWidth * chWidth, padding + Math.floor(this._config.scaledCharHeight / 2) - yOffset);
|
||||
this._tmpCtx.stroke();
|
||||
}
|
||||
|
||||
@@ -606,7 +610,7 @@ export class WebglCharAtlas implements IDisposable {
|
||||
return NULL_RASTERIZED_GLYPH;
|
||||
}
|
||||
|
||||
const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, allowedWidth, powerlineGlyph, customGlyph, padding);
|
||||
const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, allowedWidth, restrictedPowerlineGlyph, customGlyph, padding);
|
||||
const clippedImageData = this._clipImageData(imageData, this._workBoundingBox);
|
||||
|
||||
// Find the best atlas row to use
|
||||
|
||||
+23
-8
@@ -203,7 +203,7 @@ function createTerminal(): void {
|
||||
term = new Terminal({
|
||||
allowProposedApi: true,
|
||||
windowsMode: isWindows,
|
||||
fontFamily: 'Fira Code, courier-new, courier, monospace',
|
||||
fontFamily: '"Fira Code", courier-new, courier, monospace, "Powerline Extra Symbols"',
|
||||
theme: xtermjsTheme
|
||||
} as ITerminalOptions);
|
||||
|
||||
@@ -406,7 +406,11 @@ function initOptions(term: TerminalType): void {
|
||||
const input = <HTMLInputElement>document.getElementById(`opt-${o}`);
|
||||
addDomListener(input, 'change', () => {
|
||||
console.log('change', o, input.value);
|
||||
if (o === 'lineHeight') {
|
||||
if (o === 'rows') {
|
||||
term.resize(term.cols, parseInt(input.value));
|
||||
} else if (o === 'cols') {
|
||||
term.resize(parseInt(input.value), term.rows);
|
||||
} else if (o === 'lineHeight') {
|
||||
term.options.lineHeight = parseFloat(input.value);
|
||||
} else if (o === 'scrollSensitivity') {
|
||||
term.options.scrollSensitivity = parseFloat(input.value);
|
||||
@@ -766,12 +770,23 @@ function underlineTest() {
|
||||
term.write('\n\n\r');
|
||||
term.writeln('Underline styles:');
|
||||
term.writeln('');
|
||||
term.writeln(`${u(0)}4:0m - No underline`);
|
||||
term.writeln(`${u(1)}4:1m - Straight`);
|
||||
term.writeln(`${u(2)}4:2m - Double`);
|
||||
term.writeln(`${u(3)}4:3m - Curly`);
|
||||
term.writeln(`${u(4)}4:4m - Dotted`);
|
||||
term.writeln(`${u(5)}4:5m - Dashed\x1b[0m`);
|
||||
function showSequence(id: number, name: 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 `${u(id)}4:${id}m - ${name}\x1b[4:0m`.padEnd(33, ' ') + `${u(id)}${alphabet} ${numbers} 汉语 한국어 👽\x1b[4:0m`;
|
||||
}
|
||||
term.writeln(showSequence(0, 'No underline'));
|
||||
term.writeln(showSequence(1, 'Straight'));
|
||||
term.writeln(showSequence(2, 'Double'));
|
||||
term.writeln(showSequence(3, 'Curly'));
|
||||
term.writeln(showSequence(4, 'Dotted'));
|
||||
term.writeln(showSequence(5, 'Dashed'));
|
||||
term.writeln('');
|
||||
term.writeln(`Underline colors (256 color mode):`);
|
||||
term.writeln('');
|
||||
|
||||
@@ -5,35 +5,30 @@
|
||||
|
||||
import { IColorContrastCache } from 'browser/Types';
|
||||
import { IColor } from 'common/Types';
|
||||
import { TwoKeyMap } from 'common/MultiKeyMap';
|
||||
|
||||
export class ColorContrastCache implements IColorContrastCache {
|
||||
private _color: { [bg: number]: { [fg: number]: IColor | null | undefined } | undefined } = {};
|
||||
private _rgba: { [bg: number]: { [fg: number]: string | null | undefined } | undefined } = {};
|
||||
|
||||
public clear(): void {
|
||||
this._color = {};
|
||||
this._rgba = {};
|
||||
}
|
||||
private _color: TwoKeyMap</* bg */number, /* fg */number, IColor | null> = new TwoKeyMap();
|
||||
private _css: TwoKeyMap</* bg */number, /* fg */number, string | null> = new TwoKeyMap();
|
||||
|
||||
public setCss(bg: number, fg: number, value: string | null): void {
|
||||
if (!this._rgba[bg]) {
|
||||
this._rgba[bg] = {};
|
||||
}
|
||||
this._rgba[bg]![fg] = value;
|
||||
this._css.set(bg, fg, value);
|
||||
}
|
||||
|
||||
public getCss(bg: number, fg: number): string | null | undefined {
|
||||
return this._rgba[bg] ? this._rgba[bg]![fg] : undefined;
|
||||
return this._css.get(bg, fg);
|
||||
}
|
||||
|
||||
public setColor(bg: number, fg: number, value: IColor | null): void {
|
||||
if (!this._color[bg]) {
|
||||
this._color[bg] = {};
|
||||
}
|
||||
this._color[bg]![fg] = value;
|
||||
this._color.set(bg, fg, value);
|
||||
}
|
||||
|
||||
public getColor(bg: number, fg: number): IColor | null | undefined {
|
||||
return this._color[bg] ? this._color[bg]![fg] : undefined;
|
||||
return this._color.get(bg, fg);
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._color.clear();
|
||||
this._css.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,25 +349,27 @@ const enum VectorType {
|
||||
* not been patched with powerline characters and also to get pixel perfect rendering as rendering
|
||||
* issues can occur around AA/SPAA.
|
||||
*
|
||||
* The line variants draw beyond the cell and get clipped to ensure the end of the line is not visible.
|
||||
*
|
||||
* Original symbols defined in https://github.com/powerline/fontpatcher
|
||||
*/
|
||||
export const powerlineDefinitions: { [index: string]: IVectorShape } = {
|
||||
// Right triangle solid
|
||||
'\u{E0B0}': { d: 'M0,0 L1,.5 L0,1', type: VectorType.FILL },
|
||||
'\u{E0B0}': { d: 'M0,0 L1,.5 L0,1', type: VectorType.FILL, rightPadding: 2 },
|
||||
// Right triangle line
|
||||
'\u{E0B1}': { d: 'M0,0 L1,.5 L0,1', type: VectorType.STROKE, leftPadding: window.devicePixelRatio / 2, rightPadding: window.devicePixelRatio / 2 },
|
||||
'\u{E0B1}': { d: 'M-1,-.5 L1,.5 L-1,1.5', type: VectorType.STROKE, leftPadding: 1, rightPadding: 1 },
|
||||
// Left triangle solid
|
||||
'\u{E0B2}': { d: 'M1,0 L0,.5 L1,1', type: VectorType.FILL },
|
||||
'\u{E0B2}': { d: 'M1,0 L0,.5 L1,1', type: VectorType.FILL, leftPadding: 2 },
|
||||
// Left triangle line
|
||||
'\u{E0B3}': { d: 'M1,0 L0,.5 L1,1', type: VectorType.STROKE, leftPadding: window.devicePixelRatio / 2, rightPadding: window.devicePixelRatio / 2 },
|
||||
'\u{E0B3}': { d: 'M2,-.5 L0,.5 L2,1.5', type: VectorType.STROKE, leftPadding: 1, rightPadding: 1 },
|
||||
// Right semi-circle solid,
|
||||
'\u{E0B4}': { d: 'M0,0 L0,1 C0.552,1,1,0.776,1,.5 C1,0.224,0.552,0,0,0', type: VectorType.FILL },
|
||||
'\u{E0B4}': { d: 'M0,0 L0,1 C0.552,1,1,0.776,1,.5 C1,0.224,0.552,0,0,0', type: VectorType.FILL, rightPadding: 1 },
|
||||
// Right semi-circle line,
|
||||
'\u{E0B5}': { d: 'M0,1 C0.552,1,1,0.776,1,.5 C1,0.224,0.552,0,0,0', type: VectorType.STROKE, rightPadding: window.devicePixelRatio / 2 },
|
||||
'\u{E0B5}': { d: 'M0,1 C0.552,1,1,0.776,1,.5 C1,0.224,0.552,0,0,0', type: VectorType.STROKE, rightPadding: 1 },
|
||||
// Left semi-circle solid,
|
||||
'\u{E0B6}': { d: 'M1,0 L1,1 C0.448,1,0,0.776,0,.5 C0,0.224,0.448,0,1,0', type: VectorType.FILL },
|
||||
'\u{E0B6}': { d: 'M1,0 L1,1 C0.448,1,0,0.776,0,.5 C0,0.224,0.448,0,1,0', type: VectorType.FILL, leftPadding: 1 },
|
||||
// Left semi-circle line,
|
||||
'\u{E0B7}': { d: 'M1,1 C0.448,1,0,0.776,0,.5 C0,0.224,0.448,0,1,0', type: VectorType.STROKE, leftPadding: window.devicePixelRatio / 2 }
|
||||
'\u{E0B7}': { d: 'M1,1 C0.448,1,0,0.776,0,.5 C0,0.224,0.448,0,1,0', type: VectorType.STROKE, leftPadding: 1 }
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -380,7 +382,8 @@ export function tryDrawCustomChar(
|
||||
xOffset: number,
|
||||
yOffset: number,
|
||||
scaledCellWidth: number,
|
||||
scaledCellHeight: number
|
||||
scaledCellHeight: number,
|
||||
fontSize: number
|
||||
): boolean {
|
||||
const blockElementDefinition = blockElementDefinitions[c];
|
||||
if (blockElementDefinition) {
|
||||
@@ -402,7 +405,7 @@ export function tryDrawCustomChar(
|
||||
|
||||
const powerlineDefinition = powerlineDefinitions[c];
|
||||
if (powerlineDefinition) {
|
||||
drawPowerlineChar(ctx, powerlineDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight);
|
||||
drawPowerlineChar(ctx, powerlineDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight, fontSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -562,7 +565,7 @@ function drawBoxDrawingChar(
|
||||
if (!args[0] || !args[1]) {
|
||||
continue;
|
||||
}
|
||||
f(ctx, translateArgs(args, scaledCellWidth, scaledCellHeight, xOffset, yOffset));
|
||||
f(ctx, translateArgs(args, scaledCellWidth, scaledCellHeight, xOffset, yOffset, true));
|
||||
}
|
||||
ctx.stroke();
|
||||
ctx.closePath();
|
||||
@@ -575,10 +578,13 @@ function drawPowerlineChar(
|
||||
xOffset: number,
|
||||
yOffset: number,
|
||||
scaledCellWidth: number,
|
||||
scaledCellHeight: number
|
||||
scaledCellHeight: number,
|
||||
fontSize: number
|
||||
): void {
|
||||
ctx.beginPath();
|
||||
ctx.lineWidth = window.devicePixelRatio;
|
||||
// Scale the stroke with DPR and font size
|
||||
const cssLineWidth = fontSize / 12;
|
||||
ctx.lineWidth = window.devicePixelRatio * cssLineWidth;
|
||||
for (const instruction of charDefinition.d.split(' ')) {
|
||||
const type = instruction[0];
|
||||
const f = svgToCanvasInstructionMap[type];
|
||||
@@ -590,7 +596,16 @@ function drawPowerlineChar(
|
||||
if (!args[0] || !args[1]) {
|
||||
continue;
|
||||
}
|
||||
f(ctx, translateArgs(args, scaledCellWidth, scaledCellHeight, xOffset, yOffset, charDefinition.leftPadding, charDefinition.rightPadding));
|
||||
f(ctx, translateArgs(
|
||||
args,
|
||||
scaledCellWidth,
|
||||
scaledCellHeight,
|
||||
xOffset,
|
||||
yOffset,
|
||||
false,
|
||||
(charDefinition.leftPadding ?? 0) * (cssLineWidth / 2),
|
||||
(charDefinition.rightPadding ?? 0) * (cssLineWidth / 2)
|
||||
));
|
||||
}
|
||||
if (charDefinition.type === VectorType.STROKE) {
|
||||
ctx.strokeStyle = ctx.fillStyle;
|
||||
@@ -611,7 +626,7 @@ const svgToCanvasInstructionMap: { [index: string]: any } = {
|
||||
'M': (ctx: CanvasRenderingContext2D, args: number[]) => ctx.moveTo(args[0], args[1])
|
||||
};
|
||||
|
||||
function translateArgs(args: string[], cellWidth: number, cellHeight: number, xOffset: number, yOffset: number, leftPadding: number = 0, rightPadding: number = 0): number[] {
|
||||
function translateArgs(args: string[], cellWidth: number, cellHeight: number, xOffset: number, yOffset: number, doClamp: boolean, leftPadding: number = 0, rightPadding: number = 0): number[] {
|
||||
const result = args.map(e => parseFloat(e) || parseInt(e));
|
||||
|
||||
if (result.length < 2) {
|
||||
@@ -623,7 +638,7 @@ function translateArgs(args: string[], cellWidth: number, cellHeight: number, xO
|
||||
result[x] *= cellWidth - (leftPadding * window.devicePixelRatio) - (rightPadding * window.devicePixelRatio);
|
||||
// Ensure coordinate doesn't escape cell bounds and round to the nearest 0.5 to ensure a crisp
|
||||
// line at 100% devicePixelRatio
|
||||
if (result[x] !== 0) {
|
||||
if (doClamp && result[x] !== 0) {
|
||||
result[x] = clamp(Math.round(result[x] + 0.5) - 0.5, cellWidth, 0);
|
||||
}
|
||||
// Apply the cell's offset (ie. x*cellWidth)
|
||||
@@ -635,7 +650,7 @@ function translateArgs(args: string[], cellWidth: number, cellHeight: number, xO
|
||||
result[y] *= cellHeight;
|
||||
// Ensure coordinate doesn't escape cell bounds and round to the nearest 0.5 to ensure a crisp
|
||||
// line at 100% devicePixelRatio
|
||||
if (result[y] !== 0) {
|
||||
if (doClamp && result[y] !== 0) {
|
||||
result[y] = clamp(Math.round(result[y] + 0.5) - 0.5, cellHeight, 0);
|
||||
}
|
||||
// Apply the cell's offset (ie. x*cellHeight)
|
||||
|
||||
@@ -14,11 +14,15 @@ export function isPowerlineGlyph(codepoint: number): boolean {
|
||||
// Only return true for Powerline symbols which require
|
||||
// different padding and should be excluded from minimum contrast
|
||||
// ratio standards
|
||||
return 0xE0A4 <= codepoint && codepoint <= 0xE0D6;
|
||||
return 0xE0A4 <= codepoint && codepoint <= 0xE0D6;
|
||||
}
|
||||
|
||||
export function isRestrictedPowerlineGlyph(codepoint: number): boolean {
|
||||
return 0xE0B0 <= codepoint && codepoint <= 0xE0B7;
|
||||
}
|
||||
|
||||
function isBoxOrBlockGlyph(codepoint: number): boolean {
|
||||
return (0x2500 <= codepoint && codepoint <= 0x259F);
|
||||
return 0x2500 <= codepoint && codepoint <= 0x259F;
|
||||
}
|
||||
|
||||
export function excludeFromContrastRatioDemands(codepoint: number): boolean {
|
||||
|
||||
@@ -204,9 +204,9 @@ export class DomRendererRowFactory {
|
||||
let bgOverride: IColor | undefined;
|
||||
let fgOverride: IColor | undefined;
|
||||
let isTop = false;
|
||||
for (const d of this._decorationService.getDecorationsAtCell(x, row)) {
|
||||
this._decorationService.forEachDecorationAtCell(x, row, undefined, d => {
|
||||
if (d.options.layer !== 'top' && isTop) {
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
if (d.backgroundColorRGB) {
|
||||
bgColorMode = Attributes.CM_RGB;
|
||||
@@ -219,7 +219,7 @@ export class DomRendererRowFactory {
|
||||
fgOverride = d.foregroundColorRGB;
|
||||
}
|
||||
isTop = d.options.layer === 'top';
|
||||
}
|
||||
});
|
||||
|
||||
// Apply selection foreground if applicable
|
||||
const isInSelection = this._isCellInSelection(x, row);
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { FourKeyMap, TwoKeyMap } from 'common/MultiKeyMap';
|
||||
|
||||
const strictEqual = assert.strictEqual;
|
||||
|
||||
describe('TwoKeyMap', () => {
|
||||
let map: TwoKeyMap<number | string, number | string, string>;
|
||||
|
||||
beforeEach(() => {
|
||||
map = new TwoKeyMap();
|
||||
});
|
||||
|
||||
it('set, get', () => {
|
||||
strictEqual(map.get(1, 2), undefined);
|
||||
map.set(1, 2, 'foo');
|
||||
strictEqual(map.get(1, 2), 'foo');
|
||||
map.set(1, 3, 'bar');
|
||||
strictEqual(map.get(1, 2), 'foo');
|
||||
strictEqual(map.get(1, 3), 'bar');
|
||||
map.set(2, 2, 'foo2');
|
||||
map.set(2, 3, 'bar2');
|
||||
strictEqual(map.get(1, 2), 'foo');
|
||||
strictEqual(map.get(1, 3), 'bar');
|
||||
strictEqual(map.get(2, 2), 'foo2');
|
||||
strictEqual(map.get(2, 3), 'bar2');
|
||||
});
|
||||
it('clear', () => {
|
||||
strictEqual(map.get(1, 2), undefined);
|
||||
map.set(1, 2, 'foo');
|
||||
strictEqual(map.get(1, 2), 'foo');
|
||||
map.clear();
|
||||
strictEqual(map.get(1, 2), undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('FourKeyMap', () => {
|
||||
let map: FourKeyMap<number | string, number | string, number | string, number | string, string>;
|
||||
|
||||
beforeEach(() => {
|
||||
map = new FourKeyMap();
|
||||
});
|
||||
|
||||
it('set, get', () => {
|
||||
strictEqual(map.get(1, 2, 3, 4), undefined);
|
||||
map.set(1, 2, 3, 4, 'foo');
|
||||
strictEqual(map.get(1, 2, 3, 4), 'foo');
|
||||
map.set(1, 3, 3, 4, 'bar');
|
||||
strictEqual(map.get(1, 2, 3, 4), 'foo');
|
||||
strictEqual(map.get(1, 3, 3, 4), 'bar');
|
||||
map.set(2, 2, 3, 4, 'foo2');
|
||||
map.set(2, 3, 3, 4, 'bar2');
|
||||
strictEqual(map.get(1, 2, 3, 4), 'foo');
|
||||
strictEqual(map.get(1, 3, 3, 4), 'bar');
|
||||
strictEqual(map.get(2, 2, 3, 4), 'foo2');
|
||||
strictEqual(map.get(2, 3, 3, 4), 'bar2');
|
||||
});
|
||||
it('clear', () => {
|
||||
strictEqual(map.get(1, 2, 3, 4), undefined);
|
||||
map.set(1, 2, 3, 4, 'foo');
|
||||
strictEqual(map.get(1, 2, 3, 4), 'foo');
|
||||
map.clear();
|
||||
strictEqual(map.get(1, 2, 3, 4), undefined);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) 2022 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
export class TwoKeyMap<TFirst extends string | number, TSecond extends string | number, TValue> {
|
||||
private _data: { [bg: string | number]: { [fg: string | number]: TValue | undefined } | undefined } = {};
|
||||
|
||||
public set(first: TFirst, second: TSecond, value: TValue): void {
|
||||
if (!this._data[first]) {
|
||||
this._data[first] = {};
|
||||
}
|
||||
this._data[first as string | number]![second] = value;
|
||||
}
|
||||
|
||||
public get(first: TFirst, second: TSecond): TValue | undefined {
|
||||
return this._data[first as string | number] ? this._data[first as string | number]![second] : undefined;
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._data = {};
|
||||
}
|
||||
}
|
||||
|
||||
export class FourKeyMap<TFirst extends string | number, TSecond extends string | number, TThird extends string | number, TFourth extends string | number, TValue> {
|
||||
private _data: TwoKeyMap<TFirst, TSecond, TwoKeyMap<TThird, TFourth, TValue>> = new TwoKeyMap();
|
||||
|
||||
public set(first: TFirst, second: TSecond, third: TThird, fourth: TFourth, value: TValue): void {
|
||||
if (!this._data.get(first, second)) {
|
||||
this._data.set(first, second, new TwoKeyMap());
|
||||
}
|
||||
this._data.get(first, second)!.set(third, fourth, value);
|
||||
}
|
||||
|
||||
public get(first: TFirst, second: TSecond, third: TThird, fourth: TFourth): TValue | undefined {
|
||||
return this._data.get(first, second)?.get(third, fourth);
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._data.clear();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,9 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
// Work variables to avoid garbage collection.
|
||||
let i = 0;
|
||||
|
||||
/**
|
||||
* A generic list that is maintained in sorted order and allows values with duplicate keys. This
|
||||
* list is based on binary search and as such locating a key will take O(log n) amortized, this
|
||||
@@ -25,7 +28,7 @@ export class SortedList<T> {
|
||||
this._array.push(value);
|
||||
return;
|
||||
}
|
||||
const i = this._search(this._getKey(value), 0, this._array.length - 1);
|
||||
i = this._search(this._getKey(value), 0, this._array.length - 1);
|
||||
this._array.splice(i, 0, value);
|
||||
}
|
||||
|
||||
@@ -37,7 +40,7 @@ export class SortedList<T> {
|
||||
if (key === undefined) {
|
||||
return false;
|
||||
}
|
||||
let i = this._search(key, 0, this._array.length - 1);
|
||||
i = this._search(key, 0, this._array.length - 1);
|
||||
if (i === -1) {
|
||||
return false;
|
||||
}
|
||||
@@ -57,7 +60,7 @@ export class SortedList<T> {
|
||||
if (this._array.length === 0) {
|
||||
return;
|
||||
}
|
||||
let i = this._search(key, 0, this._array.length - 1);
|
||||
i = this._search(key, 0, this._array.length - 1);
|
||||
if (i < 0 || i >= this._array.length) {
|
||||
return;
|
||||
}
|
||||
@@ -69,6 +72,22 @@ export class SortedList<T> {
|
||||
} while (++i < this._array.length && this._getKey(this._array[i]) === key);
|
||||
}
|
||||
|
||||
public forEachByKey(key: number, callback: (value: T) => void): void {
|
||||
if (this._array.length === 0) {
|
||||
return;
|
||||
}
|
||||
i = this._search(key, 0, this._array.length - 1);
|
||||
if (i < 0 || i >= this._array.length) {
|
||||
return;
|
||||
}
|
||||
if (this._getKey(this._array[i]) !== key) {
|
||||
return;
|
||||
}
|
||||
do {
|
||||
callback(this._array[i]);
|
||||
} while (++i < this._array.length && this._getKey(this._array[i]) === key);
|
||||
}
|
||||
|
||||
public values(): IterableIterator<T> {
|
||||
return this._array.values();
|
||||
}
|
||||
|
||||
@@ -161,7 +161,6 @@ export class MockDecorationService implements IDecorationService {
|
||||
public onDecorationRemoved = new EventEmitter<IInternalDecoration>().event;
|
||||
public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined { return undefined; }
|
||||
public reset(): void { }
|
||||
public *getDecorationsAtLine(line: number): IterableIterator<IInternalDecoration> { }
|
||||
public *getDecorationsAtCell(x: number, line: number): IterableIterator<IInternalDecoration> { }
|
||||
public forEachDecorationAtCell(x: number, line: number, layer: 'bottom' | 'top' | undefined, callback: (decoration: IInternalDecoration) => void): void { }
|
||||
public dispose(): void { }
|
||||
}
|
||||
|
||||
@@ -37,6 +37,11 @@ const enum Cell {
|
||||
|
||||
export const DEFAULT_ATTR_DATA = Object.freeze(new AttributeData());
|
||||
|
||||
/** Work variables to avoid garbage collection. */
|
||||
const w: { startIndex: number } = {
|
||||
startIndex: 0
|
||||
};
|
||||
|
||||
/**
|
||||
* Typed array based bufferline implementation.
|
||||
*
|
||||
@@ -168,10 +173,10 @@ export class BufferLine implements IBufferLine {
|
||||
* to GC as it significantly reduced the amount of new objects/references needed.
|
||||
*/
|
||||
public loadCell(index: number, cell: ICellData): ICellData {
|
||||
const startIndex = index * CELL_SIZE;
|
||||
cell.content = this._data[startIndex + Cell.CONTENT];
|
||||
cell.fg = this._data[startIndex + Cell.FG];
|
||||
cell.bg = this._data[startIndex + Cell.BG];
|
||||
w.startIndex = index * CELL_SIZE;
|
||||
cell.content = this._data[w.startIndex + Cell.CONTENT];
|
||||
cell.fg = this._data[w.startIndex + Cell.FG];
|
||||
cell.bg = this._data[w.startIndex + Cell.BG];
|
||||
if (cell.content & Content.IS_COMBINED_MASK) {
|
||||
cell.combinedData = this._combined[index];
|
||||
}
|
||||
|
||||
@@ -11,6 +11,12 @@ import { SortedList } from 'common/SortedList';
|
||||
import { IColor } from 'common/Types';
|
||||
import { IDecorationOptions, IDecoration, IMarker, IEvent } from 'xterm';
|
||||
|
||||
/** Work variables to avoid garbage collection. */
|
||||
const w = {
|
||||
xmin: 0,
|
||||
xmax: 0
|
||||
};
|
||||
|
||||
export class DecorationService extends Disposable implements IDecorationService {
|
||||
public serviceBrand: any;
|
||||
|
||||
@@ -56,10 +62,6 @@ export class DecorationService extends Disposable implements IDecorationService
|
||||
this._decorations.clear();
|
||||
}
|
||||
|
||||
public *getDecorationsAtLine(line: number): IterableIterator<IInternalDecoration> {
|
||||
return this._decorations.getKeyIterator(line);
|
||||
}
|
||||
|
||||
public *getDecorationsAtCell(x: number, line: number, layer?: 'bottom' | 'top'): IterableIterator<IInternalDecoration> {
|
||||
let xmin = 0;
|
||||
let xmax = 0;
|
||||
@@ -72,6 +74,16 @@ export class DecorationService extends Disposable implements IDecorationService
|
||||
}
|
||||
}
|
||||
|
||||
public forEachDecorationAtCell(x: number, line: number, layer: 'bottom' | 'top' | undefined, callback: (decoration: IInternalDecoration) => void): void {
|
||||
this._decorations.forEachByKey(line, d => {
|
||||
w.xmin = d.options.x ?? 0;
|
||||
w.xmax = w.xmin + (d.options.width ?? 1);
|
||||
if (x >= w.xmin && x < w.xmax && (!layer || (d.options.layer ?? 'bottom') === layer)) {
|
||||
callback(d);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
for (const d of this._decorations.values()) {
|
||||
this._onDecorationRemoved.fire(d);
|
||||
|
||||
@@ -304,10 +304,11 @@ export interface IDecorationService extends IDisposable {
|
||||
readonly onDecorationRemoved: IEvent<IInternalDecoration>;
|
||||
registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined;
|
||||
reset(): void;
|
||||
/** Iterates over the decorations at a line (in no particular order). */
|
||||
getDecorationsAtLine(line: number): IterableIterator<IInternalDecoration>;
|
||||
/** Iterates over the decorations at a cell (in no particular order). */
|
||||
getDecorationsAtCell(x: number, line: number, layer?: 'bottom' | 'top'): IterableIterator<IInternalDecoration>;
|
||||
/**
|
||||
* Trigger a callback over the decoration at a cell (in no particular order). This uses a callback
|
||||
* instead of an iterator as it's typically used in hot code paths.
|
||||
*/
|
||||
forEachDecorationAtCell(x: number, line: number, layer: 'bottom' | 'top' | undefined, callback: (decoration: IInternalDecoration) => void): void;
|
||||
}
|
||||
export interface IInternalDecoration extends IDecoration {
|
||||
readonly options: IDecorationOptions;
|
||||
|
||||
Reference in New Issue
Block a user