Merge remote-tracking branch 'upstream/master' into pr/tisilent/4837

This commit is contained in:
Daniel Imms
2023-11-02 09:30:17 -07:00
278 changed files with 848 additions and 565 deletions
+2
View File
@@ -0,0 +1,2 @@
lib
node_modules
+29
View File
@@ -0,0 +1,29 @@
# Blacklist - exclude everything except npm defaults such as LICENSE, etc
*
!*/
# Whitelist - lib/
!lib/**/*.d.ts
!lib/**/*.js
!lib/**/*.js.map
!lib/**/*.css
# Whitelist - src/
!src/**/*.ts
!src/**/*.d.ts
!src/**/*.js
!src/**/*.js.map
!src/**/*.css
# Blacklist - src/ test files
src/**/*.test.ts
src/**/*.test.d.ts
src/**/*.test.js
src/**/*.test.js.map
# Whitelist - typings/
!typings/*.d.ts
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2018, The xterm.js authors (https://github.com/xtermjs/xterm.js)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+41
View File
@@ -0,0 +1,41 @@
## @xterm/addon-webgl
An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables a WebGL2-based renderer. This addon requires xterm.js v4+.
### Install
```bash
npm install --save @xterm/addon-webgl
```
### Usage
```ts
import { Terminal } from '@xterm/xterm';
import { WebglAddon } from '@xterm/addon-webgl';
const terminal = new Terminal();
terminal.open(element);
terminal.loadAddon(new WebglAddon());
```
See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/addon-webgl/typings/addon-webgl.d.ts) for more advanced usage.
### Handling Context Loss
The browser may drop WebGL contexts for various reasons like OOM or after the system has been suspended. There is an API exposed that fires the `webglcontextlost` event fired on the canvas so embedders can handle it however they wish. An easy, but suboptimal way, to handle this is by disposing of WebglAddon when the event fires:
```ts
const terminal = new Terminal();
const addon = new WebglAddon();
addon.onContextLoss(e => {
addon.dispose();
});
terminal.loadAddon(addon);
```
Read more about handling WebGL context losses on the [Khronos wiki](https://www.khronos.org/webgl/wiki/HandlingContextLost).
### See also
- [@xterm/addon-canvas](https://www.npmjs.com/package/@xterm/addon-canvas) A renderer for xterm.js that uses a 2d canvas that can be used as a fallback when WebGL is not available
+28
View File
@@ -0,0 +1,28 @@
{
"name": "@xterm/addon-webgl",
"version": "0.16.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
},
"main": "lib/addon-webgl.js",
"types": "typings/addon-webgl.d.ts",
"repository": "https://github.com/xtermjs/xterm.js/tree/master/addons/addon-webgl",
"license": "MIT",
"keywords": [
"terminal",
"webgl",
"xterm",
"xterm.js"
],
"scripts": {
"build": "../../node_modules/.bin/tsc -p .",
"prepackage": "npm run build",
"package": "../../node_modules/.bin/webpack",
"prepublishOnly": "npm run package",
"start-server-only": "node ../../demo/start-server-only"
},
"peerDependencies": {
"@xterm/xterm": "^5.0.0"
}
}
+381
View File
@@ -0,0 +1,381 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
import { TextureAtlas } from 'browser/renderer/shared/TextureAtlas';
import { IRasterizedGlyph, IRenderDimensions, ITextureAtlas } from 'browser/renderer/shared/Types';
import { NULL_CELL_CODE } from 'common/buffer/Constants';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { Terminal } from '@xterm/xterm';
import { IRenderModel, IWebGL2RenderingContext, IWebGLVertexArrayObject } from './Types';
import { createProgram, GLTexture, PROJECTION_MATRIX } from './WebglUtils';
interface IVertices {
attributes: Float32Array;
/**
* These buffers are the ones used to bind to WebGL, the reason there are
* multiple is to allow double buffering to work as you cannot modify the
* buffer while it's being used by the GPU. Having multiple lets us start
* working on the next frame.
*/
attributesBuffers: Float32Array[];
count: number;
}
const enum VertexAttribLocations {
UNIT_QUAD = 0,
CELL_POSITION = 1,
OFFSET = 2,
SIZE = 3,
TEXPAGE = 4,
TEXCOORD = 5,
TEXSIZE = 6
}
const vertexShaderSource = `#version 300 es
layout (location = ${VertexAttribLocations.UNIT_QUAD}) in vec2 a_unitquad;
layout (location = ${VertexAttribLocations.CELL_POSITION}) in vec2 a_cellpos;
layout (location = ${VertexAttribLocations.OFFSET}) in vec2 a_offset;
layout (location = ${VertexAttribLocations.SIZE}) in vec2 a_size;
layout (location = ${VertexAttribLocations.TEXPAGE}) in float a_texpage;
layout (location = ${VertexAttribLocations.TEXCOORD}) in vec2 a_texcoord;
layout (location = ${VertexAttribLocations.TEXSIZE}) in vec2 a_texsize;
uniform mat4 u_projection;
uniform vec2 u_resolution;
out vec2 v_texcoord;
flat out int v_texpage;
void main() {
vec2 zeroToOne = (a_offset / u_resolution) + a_cellpos + (a_unitquad * a_size);
gl_Position = u_projection * vec4(zeroToOne, 0.0, 1.0);
v_texpage = int(a_texpage);
v_texcoord = a_texcoord + a_unitquad * a_texsize;
}`;
function createFragmentShaderSource(maxFragmentShaderTextureUnits: number): string {
let textureConditionals = '';
for (let i = 1; i < maxFragmentShaderTextureUnits; i++) {
textureConditionals += ` else if (v_texpage == ${i}) { outColor = texture(u_texture[${i}], v_texcoord); }`;
}
return (`#version 300 es
precision lowp float;
in vec2 v_texcoord;
flat in int v_texpage;
uniform sampler2D u_texture[${maxFragmentShaderTextureUnits}];
out vec4 outColor;
void main() {
if (v_texpage == 0) {
outColor = texture(u_texture[0], v_texcoord);
} ${textureConditionals}
}`);
}
const INDICES_PER_CELL = 11;
const BYTES_PER_CELL = INDICES_PER_CELL * Float32Array.BYTES_PER_ELEMENT;
const CELL_POSITION_INDICES = 2;
// Work variables to avoid garbage collection
let $i = 0;
let $glyph: IRasterizedGlyph | undefined = undefined;
let $leftCellPadding = 0;
let $clippedPixels = 0;
export class GlyphRenderer extends Disposable {
private readonly _program: WebGLProgram;
private readonly _vertexArrayObject: IWebGLVertexArrayObject;
private readonly _projectionLocation: WebGLUniformLocation;
private readonly _resolutionLocation: WebGLUniformLocation;
private readonly _textureLocation: WebGLUniformLocation;
private readonly _atlasTextures: GLTexture[];
private readonly _attributesBuffer: WebGLBuffer;
private _atlas: ITextureAtlas | undefined;
private _activeBuffer: number = 0;
private readonly _vertices: IVertices = {
count: 0,
attributes: new Float32Array(0),
attributesBuffers: [
new Float32Array(0),
new Float32Array(0)
]
};
constructor(
private readonly _terminal: Terminal,
private readonly _gl: IWebGL2RenderingContext,
private _dimensions: IRenderDimensions
) {
super();
const gl = this._gl;
if (TextureAtlas.maxAtlasPages === undefined) {
// Typically 8 or 16
TextureAtlas.maxAtlasPages = Math.min(32, throwIfFalsy(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS) as number | null));
// Almost all clients will support >= 4096
TextureAtlas.maxTextureSize = throwIfFalsy(gl.getParameter(gl.MAX_TEXTURE_SIZE) as number | null);
}
this._program = throwIfFalsy(createProgram(gl, vertexShaderSource, createFragmentShaderSource(TextureAtlas.maxAtlasPages)));
this.register(toDisposable(() => gl.deleteProgram(this._program)));
// Uniform locations
this._projectionLocation = throwIfFalsy(gl.getUniformLocation(this._program, 'u_projection'));
this._resolutionLocation = throwIfFalsy(gl.getUniformLocation(this._program, 'u_resolution'));
this._textureLocation = throwIfFalsy(gl.getUniformLocation(this._program, 'u_texture'));
// Create and set the vertex array object
this._vertexArrayObject = gl.createVertexArray();
gl.bindVertexArray(this._vertexArrayObject);
// Setup a_unitquad, this defines the 4 vertices of a rectangle
const unitQuadVertices = new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]);
const unitQuadVerticesBuffer = gl.createBuffer();
this.register(toDisposable(() => gl.deleteBuffer(unitQuadVerticesBuffer)));
gl.bindBuffer(gl.ARRAY_BUFFER, unitQuadVerticesBuffer);
gl.bufferData(gl.ARRAY_BUFFER, unitQuadVertices, gl.STATIC_DRAW);
gl.enableVertexAttribArray(VertexAttribLocations.UNIT_QUAD);
gl.vertexAttribPointer(VertexAttribLocations.UNIT_QUAD, 2, this._gl.FLOAT, false, 0, 0);
// Setup the unit quad element array buffer, this points to indices in
// unitQuadVertices to allow is to draw 2 triangles from the vertices via a
// triangle strip
const unitQuadElementIndices = new Uint8Array([0, 1, 2, 3]);
const elementIndicesBuffer = gl.createBuffer();
this.register(toDisposable(() => gl.deleteBuffer(elementIndicesBuffer)));
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elementIndicesBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, unitQuadElementIndices, gl.STATIC_DRAW);
// Setup attributes
this._attributesBuffer = throwIfFalsy(gl.createBuffer());
this.register(toDisposable(() => gl.deleteBuffer(this._attributesBuffer)));
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
gl.enableVertexAttribArray(VertexAttribLocations.OFFSET);
gl.vertexAttribPointer(VertexAttribLocations.OFFSET, 2, gl.FLOAT, false, BYTES_PER_CELL, 0);
gl.vertexAttribDivisor(VertexAttribLocations.OFFSET, 1);
gl.enableVertexAttribArray(VertexAttribLocations.SIZE);
gl.vertexAttribPointer(VertexAttribLocations.SIZE, 2, gl.FLOAT, false, BYTES_PER_CELL, 2 * Float32Array.BYTES_PER_ELEMENT);
gl.vertexAttribDivisor(VertexAttribLocations.SIZE, 1);
gl.enableVertexAttribArray(VertexAttribLocations.TEXPAGE);
gl.vertexAttribPointer(VertexAttribLocations.TEXPAGE, 1, gl.FLOAT, false, BYTES_PER_CELL, 4 * Float32Array.BYTES_PER_ELEMENT);
gl.vertexAttribDivisor(VertexAttribLocations.TEXPAGE, 1);
gl.enableVertexAttribArray(VertexAttribLocations.TEXCOORD);
gl.vertexAttribPointer(VertexAttribLocations.TEXCOORD, 2, gl.FLOAT, false, BYTES_PER_CELL, 5 * Float32Array.BYTES_PER_ELEMENT);
gl.vertexAttribDivisor(VertexAttribLocations.TEXCOORD, 1);
gl.enableVertexAttribArray(VertexAttribLocations.TEXSIZE);
gl.vertexAttribPointer(VertexAttribLocations.TEXSIZE, 2, gl.FLOAT, false, BYTES_PER_CELL, 7 * Float32Array.BYTES_PER_ELEMENT);
gl.vertexAttribDivisor(VertexAttribLocations.TEXSIZE, 1);
gl.enableVertexAttribArray(VertexAttribLocations.CELL_POSITION);
gl.vertexAttribPointer(VertexAttribLocations.CELL_POSITION, 2, gl.FLOAT, false, BYTES_PER_CELL, 9 * Float32Array.BYTES_PER_ELEMENT);
gl.vertexAttribDivisor(VertexAttribLocations.CELL_POSITION, 1);
// Setup static uniforms
gl.useProgram(this._program);
const textureUnits = new Int32Array(TextureAtlas.maxAtlasPages);
for (let i = 0; i < TextureAtlas.maxAtlasPages; i++) {
textureUnits[i] = i;
}
gl.uniform1iv(this._textureLocation, textureUnits);
gl.uniformMatrix4fv(this._projectionLocation, false, PROJECTION_MATRIX);
// Setup 1x1 red pixel textures for all potential atlas pages, if one of these invalid textures
// is ever drawn it will show characters as red rectangles.
this._atlasTextures = [];
for (let i = 0; i < TextureAtlas.maxAtlasPages; i++) {
const glTexture = new GLTexture(throwIfFalsy(gl.createTexture()));
this.register(toDisposable(() => gl.deleteTexture(glTexture.texture)));
gl.activeTexture(gl.TEXTURE0 + i);
gl.bindTexture(gl.TEXTURE_2D, glTexture.texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([255, 0, 0, 255]));
this._atlasTextures[i] = glTexture;
}
// Allow drawing of transparent texture
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
// Set viewport
this.handleResize();
}
public beginFrame(): boolean {
return this._atlas ? this._atlas.beginFrame() : true;
}
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 {
$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 */) {
array.fill(0, $i, $i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES);
return;
}
if (!this._atlas) {
return;
}
// Get the glyph
if (chars && chars.length > 1) {
$glyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext, false);
} else {
$glyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext, false);
}
$leftCellPadding = Math.floor((this._dimensions.device.cell.width - this._dimensions.device.char.width) / 2);
if (bg !== lastBg && $glyph.offset.x > $leftCellPadding) {
$clippedPixels = $glyph.offset.x - $leftCellPadding;
// a_origin
array[$i ] = -($glyph.offset.x - $clippedPixels) + this._dimensions.device.char.left;
array[$i + 1] = -$glyph.offset.y + this._dimensions.device.char.top;
// a_size
array[$i + 2] = ($glyph.size.x - $clippedPixels) / this._dimensions.device.canvas.width;
array[$i + 3] = $glyph.size.y / this._dimensions.device.canvas.height;
// a_texpage
array[$i + 4] = $glyph.texturePage;
// a_texcoord
array[$i + 5] = $glyph.texturePositionClipSpace.x + $clippedPixels / this._atlas.pages[$glyph.texturePage].canvas.width;
array[$i + 6] = $glyph.texturePositionClipSpace.y;
// a_texsize
array[$i + 7] = $glyph.sizeClipSpace.x - $clippedPixels / this._atlas.pages[$glyph.texturePage].canvas.width;
array[$i + 8] = $glyph.sizeClipSpace.y;
} else {
// a_origin
array[$i ] = -$glyph.offset.x + this._dimensions.device.char.left;
array[$i + 1] = -$glyph.offset.y + this._dimensions.device.char.top;
// a_size
array[$i + 2] = $glyph.size.x / this._dimensions.device.canvas.width;
array[$i + 3] = $glyph.size.y / this._dimensions.device.canvas.height;
// a_texpage
array[$i + 4] = $glyph.texturePage;
// a_texcoord
array[$i + 5] = $glyph.texturePositionClipSpace.x;
array[$i + 6] = $glyph.texturePositionClipSpace.y;
// a_texsize
array[$i + 7] = $glyph.sizeClipSpace.x;
array[$i + 8] = $glyph.sizeClipSpace.y;
}
// a_cellpos only changes on resize
}
public clear(): void {
const terminal = this._terminal;
const newCount = terminal.cols * terminal.rows * INDICES_PER_CELL;
// Clear vertices
if (this._vertices.count !== newCount) {
this._vertices.attributes = new Float32Array(newCount);
} else {
this._vertices.attributes.fill(0);
}
let i = 0;
for (; i < this._vertices.attributesBuffers.length; i++) {
if (this._vertices.count !== newCount) {
this._vertices.attributesBuffers[i] = new Float32Array(newCount);
} else {
this._vertices.attributesBuffers[i].fill(0);
}
}
this._vertices.count = newCount;
i = 0;
for (let y = 0; y < terminal.rows; y++) {
for (let x = 0; x < terminal.cols; x++) {
this._vertices.attributes[i + 9] = x / terminal.cols;
this._vertices.attributes[i + 10] = y / terminal.rows;
i += INDICES_PER_CELL;
}
}
}
public handleResize(): void {
const gl = this._gl;
gl.useProgram(this._program);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.uniform2f(this._resolutionLocation, gl.canvas.width, gl.canvas.height);
this.clear();
}
public render(renderModel: IRenderModel): void {
if (!this._atlas) {
return;
}
const gl = this._gl;
gl.useProgram(this._program);
gl.bindVertexArray(this._vertexArrayObject);
// Alternate buffers each frame as the active buffer gets locked while it's in use by the GPU
this._activeBuffer = (this._activeBuffer + 1) % 2;
const activeBuffer = this._vertices.attributesBuffers[this._activeBuffer];
// Copy data for each cell of each line up to its line length (the last non-whitespace cell)
// from the attributes buffer into activeBuffer, which is the one that gets bound to the GPU.
// The reasons for this are as follows:
// - So the active buffer can be alternated so we don't get blocked on rendering finishing
// - To copy either the normal attributes buffer or the selection attributes buffer when there
// is a selection
// - So we don't send vertices for all the line-ending whitespace to the GPU
let bufferLength = 0;
for (let y = 0; y < renderModel.lineLengths.length; y++) {
const si = y * this._terminal.cols * INDICES_PER_CELL;
const sub = this._vertices.attributes.subarray(si, si + renderModel.lineLengths[y] * INDICES_PER_CELL);
activeBuffer.set(sub, bufferLength);
bufferLength += sub.length;
}
// Bind the attributes buffer
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
gl.bufferData(gl.ARRAY_BUFFER, activeBuffer.subarray(0, bufferLength), gl.STREAM_DRAW);
// Bind the atlas page texture if they have changed
for (let i = 0; i < this._atlas.pages.length; i++) {
if (this._atlas.pages[i].version !== this._atlasTextures[i].version) {
this._bindAtlasPageTexture(gl, this._atlas, i);
}
}
// Draw the viewport
gl.drawElementsInstanced(gl.TRIANGLE_STRIP, 4, gl.UNSIGNED_BYTE, 0, bufferLength / INDICES_PER_CELL);
}
public setAtlas(atlas: ITextureAtlas): void {
this._atlas = atlas;
for (const glTexture of this._atlasTextures) {
glTexture.version = -1;
}
}
private _bindAtlasPageTexture(gl: IWebGL2RenderingContext, atlas: ITextureAtlas, i: number): void {
gl.activeTexture(gl.TEXTURE0 + i);
gl.bindTexture(gl.TEXTURE_2D, this._atlasTextures[i].texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, atlas.pages[i].canvas);
gl.generateMipmap(gl.TEXTURE_2D);
this._atlasTextures[i].version = atlas.pages[i].version;
}
public setDimensions(dimensions: IRenderDimensions): void {
this._dimensions = dimensions;
}
}
+382
View File
@@ -0,0 +1,382 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
import { IRenderDimensions } from 'browser/renderer/shared/Types';
import { IThemeService } from 'browser/services/Services';
import { ReadonlyColorSet } from 'browser/Types';
import { Attributes, FgFlags } from 'common/buffer/Constants';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { IColor } from 'common/Types';
import { Terminal } from '@xterm/xterm';
import { RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
import { IRenderModel, IWebGL2RenderingContext, IWebGLVertexArrayObject } from './Types';
import { createProgram, expandFloat32Array, PROJECTION_MATRIX } from './WebglUtils';
const enum VertexAttribLocations {
POSITION = 0,
SIZE = 1,
COLOR = 2,
UNIT_QUAD = 3
}
const vertexShaderSource = `#version 300 es
layout (location = ${VertexAttribLocations.POSITION}) in vec2 a_position;
layout (location = ${VertexAttribLocations.SIZE}) in vec2 a_size;
layout (location = ${VertexAttribLocations.COLOR}) in vec4 a_color;
layout (location = ${VertexAttribLocations.UNIT_QUAD}) in vec2 a_unitquad;
uniform mat4 u_projection;
out vec4 v_color;
void main() {
vec2 zeroToOne = a_position + (a_unitquad * a_size);
gl_Position = u_projection * vec4(zeroToOne, 0.0, 1.0);
v_color = a_color;
}`;
const fragmentShaderSource = `#version 300 es
precision lowp float;
in vec4 v_color;
out vec4 outColor;
void main() {
outColor = v_color;
}`;
const INDICES_PER_RECTANGLE = 8;
const BYTES_PER_RECTANGLE = INDICES_PER_RECTANGLE * Float32Array.BYTES_PER_ELEMENT;
const INITIAL_BUFFER_RECTANGLE_CAPACITY = 20 * INDICES_PER_RECTANGLE;
class Vertices {
public attributes: Float32Array;
public count: number;
constructor() {
this.attributes = new Float32Array(INITIAL_BUFFER_RECTANGLE_CAPACITY);
this.count = 0;
}
}
// Work variables to avoid garbage collection
let $rgba = 0;
let $x1 = 0;
let $y1 = 0;
let $r = 0;
let $g = 0;
let $b = 0;
let $a = 0;
export class RectangleRenderer extends Disposable {
private _program: WebGLProgram;
private _vertexArrayObject: IWebGLVertexArrayObject;
private _attributesBuffer: WebGLBuffer;
private _projectionLocation: WebGLUniformLocation;
private _bgFloat!: Float32Array;
private _cursorFloat!: Float32Array;
private _vertices: Vertices = new Vertices();
private _verticesCursor: Vertices = new Vertices();
constructor(
private _terminal: Terminal,
private _gl: IWebGL2RenderingContext,
private _dimensions: IRenderDimensions,
private readonly _themeService: IThemeService
) {
super();
const gl = this._gl;
this._program = throwIfFalsy(createProgram(gl, vertexShaderSource, fragmentShaderSource));
this.register(toDisposable(() => gl.deleteProgram(this._program)));
// Uniform locations
this._projectionLocation = throwIfFalsy(gl.getUniformLocation(this._program, 'u_projection'));
// Create and set the vertex array object
this._vertexArrayObject = gl.createVertexArray();
gl.bindVertexArray(this._vertexArrayObject);
// Setup a_unitquad, this defines the 4 vertices of a rectangle
const unitQuadVertices = new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]);
const unitQuadVerticesBuffer = gl.createBuffer();
this.register(toDisposable(() => gl.deleteBuffer(unitQuadVerticesBuffer)));
gl.bindBuffer(gl.ARRAY_BUFFER, unitQuadVerticesBuffer);
gl.bufferData(gl.ARRAY_BUFFER, unitQuadVertices, gl.STATIC_DRAW);
gl.enableVertexAttribArray(VertexAttribLocations.UNIT_QUAD);
gl.vertexAttribPointer(VertexAttribLocations.UNIT_QUAD, 2, this._gl.FLOAT, false, 0, 0);
// Setup the unit quad element array buffer, this points to indices in
// unitQuadVertices to allow is to draw 2 triangles from the vertices via a
// triangle strip
const unitQuadElementIndices = new Uint8Array([0, 1, 2, 3]);
const elementIndicesBuffer = gl.createBuffer();
this.register(toDisposable(() => gl.deleteBuffer(elementIndicesBuffer)));
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elementIndicesBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, unitQuadElementIndices, gl.STATIC_DRAW);
// Setup attributes
this._attributesBuffer = throwIfFalsy(gl.createBuffer());
this.register(toDisposable(() => gl.deleteBuffer(this._attributesBuffer)));
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
gl.enableVertexAttribArray(VertexAttribLocations.POSITION);
gl.vertexAttribPointer(VertexAttribLocations.POSITION, 2, gl.FLOAT, false, BYTES_PER_RECTANGLE, 0);
gl.vertexAttribDivisor(VertexAttribLocations.POSITION, 1);
gl.enableVertexAttribArray(VertexAttribLocations.SIZE);
gl.vertexAttribPointer(VertexAttribLocations.SIZE, 2, gl.FLOAT, false, BYTES_PER_RECTANGLE, 2 * Float32Array.BYTES_PER_ELEMENT);
gl.vertexAttribDivisor(VertexAttribLocations.SIZE, 1);
gl.enableVertexAttribArray(VertexAttribLocations.COLOR);
gl.vertexAttribPointer(VertexAttribLocations.COLOR, 4, gl.FLOAT, false, BYTES_PER_RECTANGLE, 4 * Float32Array.BYTES_PER_ELEMENT);
gl.vertexAttribDivisor(VertexAttribLocations.COLOR, 1);
this._updateCachedColors(_themeService.colors);
this.register(this._themeService.onChangeColors(e => {
this._updateCachedColors(e);
this._updateViewportRectangle();
}));
}
public renderBackgrounds(): void {
this._renderVertices(this._vertices);
}
public renderCursor(): void {
this._renderVertices(this._verticesCursor);
}
private _renderVertices(vertices: Vertices): void {
const gl = this._gl;
gl.useProgram(this._program);
gl.bindVertexArray(this._vertexArrayObject);
gl.uniformMatrix4fv(this._projectionLocation, false, PROJECTION_MATRIX);
// Bind attributes buffer and draw
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices.attributes, gl.DYNAMIC_DRAW);
gl.drawElementsInstanced(this._gl.TRIANGLE_STRIP, 4, gl.UNSIGNED_BYTE, 0, vertices.count);
}
public handleResize(): void {
this._updateViewportRectangle();
}
public setDimensions(dimensions: IRenderDimensions): void {
this._dimensions = dimensions;
}
private _updateCachedColors(colors: ReadonlyColorSet): void {
this._bgFloat = this._colorToFloat32Array(colors.background);
this._cursorFloat = this._colorToFloat32Array(colors.cursor);
}
private _updateViewportRectangle(): void {
// Set first rectangle that clears the screen
this._addRectangleFloat(
this._vertices.attributes,
0,
0,
0,
this._terminal.cols * this._dimensions.device.cell.width,
this._terminal.rows * this._dimensions.device.cell.height,
this._bgFloat
);
}
public updateBackgrounds(model: IRenderModel): void {
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 (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)) {
offset = rectangleCount++ * INDICES_PER_RECTANGLE;
this._updateRectangle(vertices, offset, currentFg, currentBg, currentStartX, x, y);
}
currentStartX = x;
currentBg = bg;
currentFg = fg;
currentInverse = inverse;
}
}
// Finish rectangle if it's still going
if (currentBg !== 0 || (currentInverse && currentFg !== 0)) {
offset = rectangleCount++ * INDICES_PER_RECTANGLE;
this._updateRectangle(vertices, offset, currentFg, currentBg, currentStartX, terminal.cols, y);
}
}
vertices.count = rectangleCount;
}
public updateCursor(model: IRenderModel): void {
const vertices = this._verticesCursor;
const cursor = model.cursor;
if (!cursor || cursor.style === 'block') {
vertices.count = 0;
return;
}
let offset: number;
let rectangleCount = 0;
if (cursor.style === 'bar' || cursor.style === 'outline') {
// Left edge
offset = rectangleCount++ * INDICES_PER_RECTANGLE;
this._addRectangleFloat(
vertices.attributes,
offset,
cursor.x * this._dimensions.device.cell.width,
cursor.y * this._dimensions.device.cell.height,
cursor.style === 'bar' ? cursor.dpr * cursor.cursorWidth : cursor.dpr,
this._dimensions.device.cell.height,
this._cursorFloat
);
}
if (cursor.style === 'underline' || cursor.style === 'outline') {
// Bottom edge
offset = rectangleCount++ * INDICES_PER_RECTANGLE;
this._addRectangleFloat(
vertices.attributes,
offset,
cursor.x * this._dimensions.device.cell.width,
(cursor.y + 1) * this._dimensions.device.cell.height - cursor.dpr,
cursor.width * this._dimensions.device.cell.width,
cursor.dpr,
this._cursorFloat
);
}
if (cursor.style === 'outline') {
// Top edge
offset = rectangleCount++ * INDICES_PER_RECTANGLE;
this._addRectangleFloat(
vertices.attributes,
offset,
cursor.x * this._dimensions.device.cell.width,
cursor.y * this._dimensions.device.cell.height,
cursor.width * this._dimensions.device.cell.width,
cursor.dpr,
this._cursorFloat
);
// Right edge
offset = rectangleCount++ * INDICES_PER_RECTANGLE;
this._addRectangleFloat(
vertices.attributes,
offset,
(cursor.x + cursor.width) * this._dimensions.device.cell.width - cursor.dpr,
cursor.y * this._dimensions.device.cell.height,
cursor.dpr,
this._dimensions.device.cell.height,
this._cursorFloat
);
}
vertices.count = rectangleCount;
}
private _updateRectangle(vertices: Vertices, offset: number, fg: number, bg: number, startX: number, endX: number, y: number): void {
if (fg & FgFlags.INVERSE) {
switch (fg & Attributes.CM_MASK) {
case Attributes.CM_P16:
case Attributes.CM_P256:
$rgba = this._themeService.colors.ansi[fg & Attributes.PCOLOR_MASK].rgba;
break;
case Attributes.CM_RGB:
$rgba = (fg & Attributes.RGB_MASK) << 8;
break;
case Attributes.CM_DEFAULT:
default:
$rgba = this._themeService.colors.foreground.rgba;
}
} else {
switch (bg & Attributes.CM_MASK) {
case Attributes.CM_P16:
case Attributes.CM_P256:
$rgba = this._themeService.colors.ansi[bg & Attributes.PCOLOR_MASK].rgba;
break;
case Attributes.CM_RGB:
$rgba = (bg & Attributes.RGB_MASK) << 8;
break;
case Attributes.CM_DEFAULT:
default:
$rgba = this._themeService.colors.background.rgba;
}
}
if (vertices.attributes.length < offset + 4) {
vertices.attributes = expandFloat32Array(vertices.attributes, this._terminal.rows * this._terminal.cols * INDICES_PER_RECTANGLE);
}
$x1 = startX * this._dimensions.device.cell.width;
$y1 = y * this._dimensions.device.cell.height;
$r = (($rgba >> 24) & 0xFF) / 255;
$g = (($rgba >> 16) & 0xFF) / 255;
$b = (($rgba >> 8 ) & 0xFF) / 255;
$a = 1;
this._addRectangle(vertices.attributes, offset, $x1, $y1, (endX - startX) * this._dimensions.device.cell.width, this._dimensions.device.cell.height, $r, $g, $b, $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 / this._dimensions.device.canvas.width;
array[offset + 1] = y1 / this._dimensions.device.canvas.height;
array[offset + 2] = width / this._dimensions.device.canvas.width;
array[offset + 3] = height / this._dimensions.device.canvas.height;
array[offset + 4] = r;
array[offset + 5] = g;
array[offset + 6] = b;
array[offset + 7] = a;
}
private _addRectangleFloat(array: Float32Array, offset: number, x1: number, y1: number, width: number, height: number, color: Float32Array): void {
array[offset ] = x1 / this._dimensions.device.canvas.width;
array[offset + 1] = y1 / this._dimensions.device.canvas.height;
array[offset + 2] = width / this._dimensions.device.canvas.width;
array[offset + 3] = height / this._dimensions.device.canvas.height;
array[offset + 4] = color[0];
array[offset + 5] = color[1];
array[offset + 6] = color[2];
array[offset + 7] = color[3];
}
private _colorToFloat32Array(color: IColor): Float32Array {
return new Float32Array([
((color.rgba >> 24) & 0xFF) / 255,
((color.rgba >> 16) & 0xFF) / 255,
((color.rgba >> 8 ) & 0xFF) / 255,
((color.rgba ) & 0xFF) / 255
]);
}
}
+41
View File
@@ -0,0 +1,41 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ICursorRenderModel, IRenderModel } from './Types';
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;
export const RENDER_MODEL_FG_OFFSET = 2;
export const RENDER_MODEL_EXT_OFFSET = 3;
export const COMBINED_CHAR_BIT_MASK = 0x80000000;
export class RenderModel implements IRenderModel {
public cells: Uint32Array;
public lineLengths: Uint32Array;
public selection: ISelectionRenderModel;
public cursor?: ICursorRenderModel;
constructor() {
this.cells = new Uint32Array(0);
this.lineLengths = new Uint32Array(0);
this.selection = createSelectionRenderModel();
}
public resize(cols: number, rows: number): void {
const indexCount = cols * rows * RENDER_MODEL_INDICIES_PER_CELL;
if (indexCount !== this.cells.length) {
this.cells = new Uint32Array(indexCount);
this.lineLengths = new Uint32Array(rows);
}
}
public clear(): void {
this.cells.fill(0, 0);
this.lineLengths.fill(0, 0);
}
}
+120
View File
@@ -0,0 +1,120 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { sliceFallback } from './TypedArray';
type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray | Int8Array | Int16Array | Int32Array | Float32Array | Float64Array;
function deepEquals(a: TypedArray, b: TypedArray): void {
assert.equal(a.length, b.length);
for (let i = 0; i < a.length; ++i) {
assert.equal(a[i], b[i]);
}
}
describe('polyfill conformance tests', function(): void {
describe('TypedArray.slice', () => {
describe('should work with all typed array types', () => {
it('Uint8Array', () => {
const a = new Uint8Array(5);
deepEquals(sliceFallback(a, 2), a.slice(2));
deepEquals(sliceFallback(a, 65535), a.slice(65535));
deepEquals(sliceFallback(a, -1), a.slice(-1));
});
it('Uint16Array', () => {
const u161 = new Uint16Array(5);
const u162 = new Uint16Array(5);
deepEquals(sliceFallback(u161, 2), u162.slice(2));
deepEquals(sliceFallback(u161, 65535), u162.slice(65535));
deepEquals(sliceFallback(u161, -1), u162.slice(-1));
});
it('Uint32Array', () => {
const u321 = new Uint32Array(5);
const u322 = new Uint32Array(5);
deepEquals(sliceFallback(u321, 2), u322.slice(2));
deepEquals(sliceFallback(u321, 65537), u322.slice(65537));
deepEquals(sliceFallback(u321, -1), u322.slice(-1));
});
it('Int8Array', () => {
const i81 = new Int8Array(5);
const i82 = new Int8Array(5);
deepEquals(sliceFallback(i81, 2), i82.slice(2));
deepEquals(sliceFallback(i81, 65537), i82.slice(65537));
deepEquals(sliceFallback(i81, -1), i82.slice(-1));
});
it('Int16Array', () => {
const i161 = new Int16Array(5);
const i162 = new Int16Array(5);
deepEquals(sliceFallback(i161, 2), i162.slice(2));
deepEquals(sliceFallback(i161, 65535), i162.slice(65535));
deepEquals(sliceFallback(i161, -1), i162.slice(-1));
});
it('Int32Array', () => {
const i321 = new Int32Array(5);
const i322 = new Int32Array(5);
deepEquals(sliceFallback(i321, 2), i322.slice(2));
deepEquals(sliceFallback(i321, 65537), i322.slice(65537));
deepEquals(sliceFallback(i321, -1), i322.slice(-1));
});
it('Float32Array', () => {
const f321 = new Float32Array(5);
const f322 = new Float32Array(5);
deepEquals(sliceFallback(f321, 2), f322.slice(2));
deepEquals(sliceFallback(f321, 65537), f322.slice(65537));
deepEquals(sliceFallback(f321, -1), f322.slice(-1));
});
it('Float64Array', () => {
const f641 = new Float64Array(5);
const f642 = new Float64Array(5);
deepEquals(sliceFallback(f641, 2), f642.slice(2));
deepEquals(sliceFallback(f641, 65537), f642.slice(65537));
deepEquals(sliceFallback(f641, -1), f642.slice(-1));
});
it('Uint8ClampedArray', () => {
const u8Clamped1 = new Uint8ClampedArray(5);
const u8Clamped2 = new Uint8ClampedArray(5);
deepEquals(sliceFallback(u8Clamped1, 2), u8Clamped2.slice(2));
deepEquals(sliceFallback(u8Clamped1, 65537), u8Clamped2.slice(65537));
deepEquals(sliceFallback(u8Clamped1, -1), u8Clamped2.slice(-1));
});
});
it('start', () => {
const arr = new Uint32Array([1, 2, 3, 4, 5]);
deepEquals(sliceFallback(arr, -1), arr.slice(-1));
deepEquals(sliceFallback(arr, 0), arr.slice(0));
deepEquals(sliceFallback(arr, 1), arr.slice(1));
deepEquals(sliceFallback(arr, 2), arr.slice(2));
deepEquals(sliceFallback(arr, 3), arr.slice(3));
deepEquals(sliceFallback(arr, 4), arr.slice(4));
deepEquals(sliceFallback(arr, 5), arr.slice(5));
});
it('end', () => {
const arr = new Uint32Array([1, 2, 3, 4, 5]);
deepEquals(sliceFallback(arr, -1, -2), arr.slice(-1, -2));
deepEquals(sliceFallback(arr, 0, -2), arr.slice(0, -2));
deepEquals(sliceFallback(arr, 1, -2), arr.slice(1, -2));
deepEquals(sliceFallback(arr, 2, -2), arr.slice(2, -2));
deepEquals(sliceFallback(arr, 3, -2), arr.slice(3, -2));
deepEquals(sliceFallback(arr, 4, -2), arr.slice(4, -2));
deepEquals(sliceFallback(arr, 5, -2), arr.slice(5, -2));
deepEquals(sliceFallback(arr, -1, 3), arr.slice(-1, 3));
deepEquals(sliceFallback(arr, 0, 3), arr.slice(0, 3));
deepEquals(sliceFallback(arr, 1, 3), arr.slice(1, 3));
deepEquals(sliceFallback(arr, 2, 3), arr.slice(2, 3));
deepEquals(sliceFallback(arr, 3, 3), arr.slice(3, 3));
deepEquals(sliceFallback(arr, 4, 3), arr.slice(4, 3));
deepEquals(sliceFallback(arr, 5, 3), arr.slice(5, 3));
deepEquals(sliceFallback(arr, -1, 8), arr.slice(-1, 8));
deepEquals(sliceFallback(arr, 0, 8), arr.slice(0, 8));
deepEquals(sliceFallback(arr, 1, 8), arr.slice(1, 8));
deepEquals(sliceFallback(arr, 2, 8), arr.slice(2, 8));
deepEquals(sliceFallback(arr, 3, 8), arr.slice(3, 8));
deepEquals(sliceFallback(arr, 4, 8), arr.slice(4, 8));
deepEquals(sliceFallback(arr, 5, 8), arr.slice(5, 8));
});
});
});
+32
View File
@@ -0,0 +1,32 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
export type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray | Int8Array | Int16Array | Int32Array | Float32Array | Float64Array;
export function slice<T extends TypedArray>(array: T, start?: number, end?: number): T {
// all modern engines that support .slice
if (array.slice) {
return array.slice(start, end) as T;
}
return sliceFallback(array, start, end);
}
export function sliceFallback<T extends TypedArray>(array: T, start: number = 0, end: number = array.length): T {
if (start < 0) {
start = (array.length + start) % array.length;
}
if (end >= array.length) {
end = array.length;
} else {
end = (array.length + end) % array.length;
}
start = Math.min(start, end);
const result: T = new (array.constructor as any)(end - start);
for (let i = 0; i < end - start; ++i) {
result[i] = array[i + start];
}
return result;
}
+33
View File
@@ -0,0 +1,33 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
import { CursorInactiveStyle, CursorStyle } from 'common/Types';
export interface IRenderModel {
cells: Uint32Array;
lineLengths: Uint32Array;
selection: ISelectionRenderModel;
cursor?: ICursorRenderModel;
}
export interface ICursorRenderModel {
x: number;
y: number;
width: number;
style: CursorStyle | CursorInactiveStyle;
cursorWidth: number;
dpr: number;
}
export interface IWebGL2RenderingContext extends WebGLRenderingContext {
vertexAttribDivisor(index: number, divisor: number): void;
createVertexArray(): IWebGLVertexArrayObject;
bindVertexArray(vao: IWebGLVertexArrayObject): void;
drawElementsInstanced(mode: number, count: number, type: number, offset: number, instanceCount: number): void;
}
export interface IWebGLVertexArrayObject {
}
+94
View File
@@ -0,0 +1,94 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import type { ITerminalAddon, Terminal } from '@xterm/xterm';
import type { WebglAddon as IWebglApi } from '@xterm/addon-webgl';
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
import { ITerminal } from 'browser/Types';
import { EventEmitter, forwardEvent } from 'common/EventEmitter';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { getSafariVersion, isSafari } from 'common/Platform';
import { ICoreService, IDecorationService, ILogService, IOptionsService } from 'common/services/Services';
import { WebglRenderer } from './WebglRenderer';
import { setTraceLogger } from 'common/services/LogService';
export class WebglAddon extends Disposable implements ITerminalAddon , IWebglApi {
private _terminal?: Terminal;
private _renderer?: WebglRenderer;
private readonly _onChangeTextureAtlas = this.register(new EventEmitter<HTMLCanvasElement>());
public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event;
private readonly _onAddTextureAtlasCanvas = this.register(new EventEmitter<HTMLCanvasElement>());
public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event;
private readonly _onRemoveTextureAtlasCanvas = this.register(new EventEmitter<HTMLCanvasElement>());
public readonly onRemoveTextureAtlasCanvas = this._onRemoveTextureAtlasCanvas.event;
private readonly _onContextLoss = this.register(new EventEmitter<void>());
public readonly onContextLoss = this._onContextLoss.event;
constructor(
private _preserveDrawingBuffer?: boolean
) {
if (isSafari && getSafariVersion() < 16) {
throw new Error('Webgl2 is only supported on Safari 16 and above');
}
super();
}
public activate(terminal: Terminal): void {
const core = (terminal as any)._core as ITerminal;
if (!terminal.element) {
this.register(core.onWillOpen(() => this.activate(terminal)));
return;
}
this._terminal = terminal;
const coreService: ICoreService = core.coreService;
const optionsService: IOptionsService = core.optionsService;
const unsafeCore = core as any;
const renderService: IRenderService = unsafeCore._renderService;
const characterJoinerService: ICharacterJoinerService = unsafeCore._characterJoinerService;
const charSizeService: ICharSizeService = unsafeCore._charSizeService;
const coreBrowserService: ICoreBrowserService = unsafeCore._coreBrowserService;
const decorationService: IDecorationService = unsafeCore._decorationService;
const logService: ILogService = unsafeCore._logService;
const themeService: IThemeService = unsafeCore._themeService;
// Set trace logger just in case it hasn't been yet which could happen when the addon is
// bundled separately to the core module
setTraceLogger(logService);
this._renderer = this.register(new WebglRenderer(
terminal,
characterJoinerService,
charSizeService,
coreBrowserService,
coreService,
decorationService,
optionsService,
themeService,
this._preserveDrawingBuffer
));
this.register(forwardEvent(this._renderer.onContextLoss, this._onContextLoss));
this.register(forwardEvent(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas));
this.register(forwardEvent(this._renderer.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas));
this.register(forwardEvent(this._renderer.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas));
renderService.setRenderer(this._renderer);
this.register(toDisposable(() => {
const renderService: IRenderService = (this._terminal as any)._core._renderService;
renderService.setRenderer((this._terminal as any)._core._createRenderer());
renderService.handleResize(terminal.cols, terminal.rows);
}));
}
public get textureAtlas(): HTMLCanvasElement | undefined {
return this._renderer?.textureAtlas;
}
public clearTextureAtlas(): void {
this._renderer?.clearTextureAtlas();
}
}
+646
View File
@@ -0,0 +1,646 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { addDisposableDomListener } from 'browser/Lifecycle';
import { ITerminal } from 'browser/Types';
import { CellColorResolver } from 'browser/renderer/shared/CellColorResolver';
import { acquireTextureAtlas, removeTerminalFromCache } from 'browser/renderer/shared/CharAtlasCache';
import { CursorBlinkStateManager } from 'browser/renderer/shared/CursorBlinkStateManager';
import { observeDevicePixelDimensions } from 'browser/renderer/shared/DevicePixelObserver';
import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent, ITextureAtlas } from 'browser/renderer/shared/Types';
import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { EventEmitter, forwardEvent } from 'common/EventEmitter';
import { Disposable, MutableDisposable, getDisposeArrayDisposable, toDisposable } from 'common/Lifecycle';
import { CharData, IBufferLine, ICellData } from 'common/Types';
import { AttributeData } from 'common/buffer/AttributeData';
import { CellData } from 'common/buffer/CellData';
import { Attributes, Content, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { Terminal } from '@xterm/xterm';
import { GlyphRenderer } from './GlyphRenderer';
import { RectangleRenderer } from './RectangleRenderer';
import { COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_EXT_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL, RenderModel } from './RenderModel';
import { IWebGL2RenderingContext } from './Types';
import { LinkRenderLayer } from './renderLayer/LinkRenderLayer';
import { IRenderLayer } from './renderLayer/Types';
export class WebglRenderer extends Disposable implements IRenderer {
private _renderLayers: IRenderLayer[];
private _cursorBlinkStateManager: MutableDisposable<CursorBlinkStateManager> = new MutableDisposable();
private _charAtlasDisposable = this.register(new MutableDisposable());
private _charAtlas: ITextureAtlas | undefined;
private _devicePixelRatio: number;
private _model: RenderModel = new RenderModel();
private _workCell: CellData = new CellData();
private _cellColorResolver: CellColorResolver;
private _canvas: HTMLCanvasElement;
private _gl: IWebGL2RenderingContext;
private _rectangleRenderer: MutableDisposable<RectangleRenderer> = this.register(new MutableDisposable());
private _glyphRenderer: MutableDisposable<GlyphRenderer> = this.register(new MutableDisposable());
public readonly dimensions: IRenderDimensions;
private _core: ITerminal;
private _isAttached: boolean;
private _contextRestorationTimeout: number | undefined;
private readonly _onChangeTextureAtlas = this.register(new EventEmitter<HTMLCanvasElement>());
public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event;
private readonly _onAddTextureAtlasCanvas = this.register(new EventEmitter<HTMLCanvasElement>());
public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event;
private readonly _onRemoveTextureAtlasCanvas = this.register(new EventEmitter<HTMLCanvasElement>());
public readonly onRemoveTextureAtlasCanvas = this._onRemoveTextureAtlasCanvas.event;
private readonly _onRequestRedraw = this.register(new EventEmitter<IRequestRedrawEvent>());
public readonly onRequestRedraw = this._onRequestRedraw.event;
private readonly _onContextLoss = this.register(new EventEmitter<void>());
public readonly onContextLoss = this._onContextLoss.event;
constructor(
private _terminal: Terminal,
private readonly _characterJoinerService: ICharacterJoinerService,
private readonly _charSizeService: ICharSizeService,
private readonly _coreBrowserService: ICoreBrowserService,
private readonly _coreService: ICoreService,
private readonly _decorationService: IDecorationService,
private readonly _optionsService: IOptionsService,
private readonly _themeService: IThemeService,
preserveDrawingBuffer?: boolean
) {
super();
this.register(this._themeService.onChangeColors(() => this._handleColorChange()));
this._cellColorResolver = new CellColorResolver(this._terminal, this._model.selection, this._decorationService, this._coreBrowserService, this._themeService);
this._core = (this._terminal as any)._core;
this._renderLayers = [
new LinkRenderLayer(this._core.screenElement!, 2, this._terminal, this._core.linkifier2, this._coreBrowserService, _optionsService, this._themeService)
];
this.dimensions = createRenderDimensions();
this._devicePixelRatio = this._coreBrowserService.dpr;
this._updateDimensions();
this._updateCursorBlink();
this.register(_optionsService.onOptionChange(() => this._handleOptionsChanged()));
this._canvas = this._coreBrowserService.mainDocument.createElement('canvas');
const contextAttributes = {
antialias: false,
depth: false,
preserveDrawingBuffer
};
this._gl = this._canvas.getContext('webgl2', contextAttributes) as IWebGL2RenderingContext;
if (!this._gl) {
throw new Error('WebGL2 not supported ' + this._gl);
}
this.register(addDisposableDomListener(this._canvas, 'webglcontextlost', (e) => {
console.log('webglcontextlost event received');
// Prevent the default behavior in order to enable WebGL context restoration.
e.preventDefault();
// Wait a few seconds to see if the 'webglcontextrestored' event is fired.
// If not, dispatch the onContextLoss notification to observers.
this._contextRestorationTimeout = setTimeout(() => {
this._contextRestorationTimeout = undefined;
console.warn('webgl context not restored; firing onContextLoss');
this._onContextLoss.fire(e);
}, 3000 /* ms */);
}));
this.register(addDisposableDomListener(this._canvas, 'webglcontextrestored', (e) => {
console.warn('webglcontextrestored event received');
clearTimeout(this._contextRestorationTimeout);
this._contextRestorationTimeout = undefined;
// The texture atlas and glyph renderer must be fully reinitialized
// because their contents have been lost.
removeTerminalFromCache(this._terminal);
this._initializeWebGLState();
this._requestRedrawViewport();
}));
this.register(observeDevicePixelDimensions(this._canvas, this._coreBrowserService.window, (w, h) => this._setCanvasDevicePixelDimensions(w, h)));
this._core.screenElement!.appendChild(this._canvas);
[this._rectangleRenderer.value, this._glyphRenderer.value] = this._initializeWebGLState();
this._isAttached = this._coreBrowserService.window.document.body.contains(this._core.screenElement!);
this.register(toDisposable(() => {
for (const l of this._renderLayers) {
l.dispose();
}
this._canvas.parentElement?.removeChild(this._canvas);
removeTerminalFromCache(this._terminal);
}));
}
public get textureAtlas(): HTMLCanvasElement | undefined {
return this._charAtlas?.pages[0].canvas;
}
private _handleColorChange(): void {
this._refreshCharAtlas();
// Force a full refresh
this._clearModel(true);
}
public handleDevicePixelRatioChange(): void {
// If the device pixel ratio changed, the char atlas needs to be regenerated
// and the terminal needs to refreshed
if (this._devicePixelRatio !== this._coreBrowserService.dpr) {
this._devicePixelRatio = this._coreBrowserService.dpr;
this.handleResize(this._terminal.cols, this._terminal.rows);
}
}
public handleResize(cols: number, rows: number): void {
// Update character and canvas dimensions
this._updateDimensions();
this._model.resize(this._terminal.cols, this._terminal.rows);
// Resize all render layers
for (const l of this._renderLayers) {
l.resize(this._terminal, this.dimensions);
}
// Resize the canvas
this._canvas.width = this.dimensions.device.canvas.width;
this._canvas.height = this.dimensions.device.canvas.height;
this._canvas.style.width = `${this.dimensions.css.canvas.width}px`;
this._canvas.style.height = `${this.dimensions.css.canvas.height}px`;
// Resize the screen
this._core.screenElement!.style.width = `${this.dimensions.css.canvas.width}px`;
this._core.screenElement!.style.height = `${this.dimensions.css.canvas.height}px`;
this._rectangleRenderer.value?.setDimensions(this.dimensions);
this._rectangleRenderer.value?.handleResize();
this._glyphRenderer.value?.setDimensions(this.dimensions);
this._glyphRenderer.value?.handleResize();
this._refreshCharAtlas();
// Force a full refresh. Resizing `_glyphRenderer` should clear it already,
// so there is no need to clear it again here.
this._clearModel(false);
}
public handleCharSizeChanged(): void {
this.handleResize(this._terminal.cols, this._terminal.rows);
}
public handleBlur(): void {
for (const l of this._renderLayers) {
l.handleBlur(this._terminal);
}
this._cursorBlinkStateManager.value?.pause();
// Request a redraw for active/inactive selection background
this._requestRedrawViewport();
}
public handleFocus(): void {
for (const l of this._renderLayers) {
l.handleFocus(this._terminal);
}
this._cursorBlinkStateManager.value?.resume();
// Request a redraw for active/inactive selection background
this._requestRedrawViewport();
}
public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
for (const l of this._renderLayers) {
l.handleSelectionChanged(this._terminal, start, end, columnSelectMode);
}
this._model.selection.update(this._core, start, end, columnSelectMode);
this._requestRedrawViewport();
}
public handleCursorMove(): void {
for (const l of this._renderLayers) {
l.handleCursorMove(this._terminal);
}
this._cursorBlinkStateManager.value?.restartBlinkAnimation();
}
private _handleOptionsChanged(): void {
this._updateDimensions();
this._refreshCharAtlas();
this._updateCursorBlink();
}
/**
* Initializes members dependent on WebGL context state.
*/
private _initializeWebGLState(): [RectangleRenderer, GlyphRenderer] {
this._rectangleRenderer.value = new RectangleRenderer(this._terminal, this._gl, this.dimensions, this._themeService);
this._glyphRenderer.value = new GlyphRenderer(this._terminal, this._gl, this.dimensions);
// Update dimensions and acquire char atlas
this.handleCharSizeChanged();
return [this._rectangleRenderer.value, this._glyphRenderer.value];
}
/**
* Refreshes the char atlas, aquiring a new one if necessary.
*/
private _refreshCharAtlas(): void {
if (this.dimensions.device.char.width <= 0 && this.dimensions.device.char.height <= 0) {
// Mark as not attached so char atlas gets refreshed on next render
this._isAttached = false;
return;
}
const atlas = acquireTextureAtlas(
this._terminal,
this._optionsService.rawOptions,
this._themeService.colors,
this.dimensions.device.cell.width,
this.dimensions.device.cell.height,
this.dimensions.device.char.width,
this.dimensions.device.char.height,
this._coreBrowserService.dpr
);
if (this._charAtlas !== atlas) {
this._onChangeTextureAtlas.fire(atlas.pages[0].canvas);
this._charAtlasDisposable.value = getDisposeArrayDisposable([
forwardEvent(atlas.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas),
forwardEvent(atlas.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas)
]);
}
this._charAtlas = atlas;
this._charAtlas.warmUp();
this._glyphRenderer.value?.setAtlas(this._charAtlas);
}
/**
* Clear the model.
* @param clearGlyphRenderer Whether to also clear the glyph renderer. This
* should be true generally to make sure it is in the same state as the model.
*/
private _clearModel(clearGlyphRenderer: boolean): void {
this._model.clear();
if (clearGlyphRenderer) {
this._glyphRenderer.value?.clear();
}
}
public clearTextureAtlas(): void {
this._charAtlas?.clearTexture();
this._clearModel(true);
this._requestRedrawViewport();
}
public clear(): void {
this._clearModel(true);
for (const l of this._renderLayers) {
l.reset(this._terminal);
}
this._cursorBlinkStateManager.value?.restartBlinkAnimation();
this._updateCursorBlink();
}
public registerCharacterJoiner(handler: (text: string) => [number, number][]): number {
return -1;
}
public deregisterCharacterJoiner(joinerId: number): boolean {
return false;
}
public renderRows(start: number, end: number): void {
if (!this._isAttached) {
if (this._coreBrowserService.window.document.body.contains(this._core.screenElement!) && this._charSizeService.width && this._charSizeService.height) {
this._updateDimensions();
this._refreshCharAtlas();
this._isAttached = true;
} else {
return;
}
}
// Update render layers
for (const l of this._renderLayers) {
l.handleGridChanged(this._terminal, start, end);
}
if (!this._glyphRenderer.value || !this._rectangleRenderer.value) {
return;
}
// Tell renderer the frame is beginning
// upon a model clear also refresh the full viewport model
// (also triggered by an atlas page merge, part of #4480)
if (this._glyphRenderer.value.beginFrame()) {
this._clearModel(true);
this._updateModel(0, this._terminal.rows - 1);
} else {
// just update changed lines to draw
this._updateModel(start, end);
}
// Render
this._rectangleRenderer.value.renderBackgrounds();
this._glyphRenderer.value.render(this._model);
if (!this._cursorBlinkStateManager.value || this._cursorBlinkStateManager.value.isCursorVisible) {
this._rectangleRenderer.value.renderCursor();
}
}
private _updateCursorBlink(): void {
if (this._terminal.options.cursorBlink) {
this._cursorBlinkStateManager.value = new CursorBlinkStateManager(() => {
this._requestRedrawCursor();
}, this._coreBrowserService);
} else {
this._cursorBlinkStateManager.clear();
}
// Request a refresh from the terminal as management of rendering is being
// moved back to the terminal
this._requestRedrawCursor();
}
private _updateModel(start: number, end: number): void {
const terminal = this._core;
let cell: ICellData = this._workCell;
// 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;
start = clamp(start, terminal.rows - 1, 0);
end = clamp(end, terminal.rows - 1, 0);
const cursorY = this._terminal.buffer.active.baseY + this._terminal.buffer.active.cursorY;
// in case cursor.x == cols adjust visual cursor to cols - 1
const cursorX = Math.min(this._terminal.buffer.active.cursorX, terminal.cols - 1);
let lastCursorX = -1;
const isCursorVisible =
this._coreService.isCursorInitialized &&
!this._coreService.isCursorHidden &&
(!this._cursorBlinkStateManager.value || this._cursorBlinkStateManager.value.isCursorVisible);
this._model.cursor = undefined;
let modelUpdated = false;
for (y = start; y <= end; y++) {
row = y + terminal.buffer.ydisp;
line = terminal.buffer.lines.get(row)!;
this._model.lineLengths[y] = 0;
joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
for (x = 0; x < terminal.cols; x++) {
lastBg = this._cellColorResolver.result.bg;
line.loadCell(x, cell);
if (x === 0) {
lastBg = this._cellColorResolver.result.bg;
}
// If true, indicates that the current character(s) to draw were joined.
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;
range = joinedRanges.shift()!;
// We already know the exact start and end column of the joined range,
// so we get the string and width representing it directly.
cell = new JoinedCellData(
cell,
line!.translateToString(true, range[0], range[1]),
range[1] - range[0]
);
// Skip over the cells occupied by this range in the loop
lastCharX = range[1] - 1;
}
chars = cell.getChars();
code = cell.getCode();
i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
// Load colors/resolve overrides into work colors
this._cellColorResolver.resolve(cell, x, row);
// Override colors for cursor cell
if (isCursorVisible && row === cursorY) {
if (x === cursorX) {
this._model.cursor = {
x: cursorX,
y: this._terminal.buffer.active.cursorY,
width: cell.getWidth(),
style: this._coreBrowserService.isFocused ?
(terminal.options.cursorStyle || 'block') : terminal.options.cursorInactiveStyle,
cursorWidth: terminal.options.cursorWidth,
dpr: this._devicePixelRatio
};
lastCursorX = cursorX + cell.getWidth() - 1;
}
if (x >= cursorX && x <= lastCursorX &&
((this._coreBrowserService.isFocused &&
(terminal.options.cursorStyle || 'block') === 'block') ||
(this._coreBrowserService.isFocused === false &&
terminal.options.cursorInactiveStyle === 'block'))) {
this._cellColorResolver.result.fg =
Attributes.CM_RGB | (this._themeService.colors.cursorAccent.rgba >> 8 & Attributes.RGB_MASK);
this._cellColorResolver.result.bg =
Attributes.CM_RGB | (this._themeService.colors.cursor.rgba >> 8 & Attributes.RGB_MASK);
}
}
if (code !== NULL_CELL_CODE) {
this._model.lineLengths[y] = x + 1;
}
// Nothing has changed, no updates needed
if (this._model.cells[i] === code &&
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;
}
modelUpdated = true;
// Flag combined chars with a bit mask so they're easily identifiable
if (chars.length > 1) {
code |= COMBINED_CHAR_BIT_MASK;
}
// Cache the results in the model
this._model.cells[i] = code;
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.value!.updateCell(x, y, code, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, chars, lastBg);
if (isJoined) {
// Restore work cell
cell = this._workCell;
// Null out non-first cells
for (x++; x < lastCharX; x++) {
j = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
this._glyphRenderer.value!.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._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;
}
}
}
}
if (modelUpdated) {
this._rectangleRenderer.value!.updateBackgrounds(this._model);
}
this._rectangleRenderer.value!.updateCursor(this._model);
}
/**
* Recalculates the character and canvas dimensions.
*/
private _updateDimensions(): void {
// Perform a new measure if the CharMeasure dimensions are not yet available
if (!this._charSizeService.width || !this._charSizeService.height) {
return;
}
// Calculate the device character width. Width is floored as it must be drawn to an integer grid
// in order for the char atlas glyphs to not be blurry.
this.dimensions.device.char.width = Math.floor(this._charSizeService.width * this._devicePixelRatio);
// Calculate the device character height. Height is ceiled in case devicePixelRatio is a
// floating point number in order to ensure there is enough space to draw the character to the
// cell.
this.dimensions.device.char.height = Math.ceil(this._charSizeService.height * this._devicePixelRatio);
// Calculate the device cell height, if lineHeight is _not_ 1, the resulting value will be
// floored since lineHeight can never be lower then 1, this guarentees the device cell height
// will always be larger than device char height.
this.dimensions.device.cell.height = Math.floor(this.dimensions.device.char.height * this._optionsService.rawOptions.lineHeight);
// Calculate the y offset within a cell that glyph should draw at in order for it to be centered
// correctly within the cell.
this.dimensions.device.char.top = this._optionsService.rawOptions.lineHeight === 1 ? 0 : Math.round((this.dimensions.device.cell.height - this.dimensions.device.char.height) / 2);
// Calculate the device cell width, taking the letterSpacing into account.
this.dimensions.device.cell.width = this.dimensions.device.char.width + Math.round(this._optionsService.rawOptions.letterSpacing);
// Calculate the x offset with a cell that text should draw from in order for it to be centered
// correctly within the cell.
this.dimensions.device.char.left = Math.floor(this._optionsService.rawOptions.letterSpacing / 2);
// Recalculate the canvas dimensions, the device dimensions define the actual number of pixel in
// the canvas
this.dimensions.device.canvas.height = this._terminal.rows * this.dimensions.device.cell.height;
this.dimensions.device.canvas.width = this._terminal.cols * this.dimensions.device.cell.width;
// The the size of the canvas on the page. It's important that this rounds to nearest integer
// and not ceils as browsers often have floating point precision issues where
// `window.devicePixelRatio` ends up being something like `1.100000023841858` for example, when
// it's actually 1.1. Ceiling may causes blurriness as the backing canvas image is 1 pixel too
// large for the canvas element size.
this.dimensions.css.canvas.height = Math.round(this.dimensions.device.canvas.height / this._devicePixelRatio);
this.dimensions.css.canvas.width = Math.round(this.dimensions.device.canvas.width / this._devicePixelRatio);
// Get the CSS dimensions of an individual cell. This needs to be derived from the calculated
// device pixel canvas value above. CharMeasure.width/height by itself is insufficient when the
// page is not at 100% zoom level as CharMeasure is measured in CSS pixels, but the actual char
// size on the canvas can differ.
this.dimensions.css.cell.height = this.dimensions.device.cell.height / this._devicePixelRatio;
this.dimensions.css.cell.width = this.dimensions.device.cell.width / this._devicePixelRatio;
}
private _setCanvasDevicePixelDimensions(width: number, height: number): void {
if (this._canvas.width === width && this._canvas.height === height) {
return;
}
// While the actual canvas size has changed, keep device canvas dimensions 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();
}
private _requestRedrawViewport(): void {
this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 });
}
private _requestRedrawCursor(): void {
const cursorY = this._terminal.buffer.active.cursorY;
this._onRequestRedraw.fire({ start: cursorY, end: cursorY });
}
}
// TODO: Share impl with core
export class JoinedCellData extends AttributeData implements ICellData {
private _width: number;
// .content carries no meaning for joined CellData, simply nullify it
// thus we have to overload all other .content accessors
public content: number = 0;
public fg: number;
public bg: number;
public combinedData: string = '';
constructor(firstCell: ICellData, chars: string, width: number) {
super();
this.fg = firstCell.fg;
this.bg = firstCell.bg;
this.combinedData = chars;
this._width = width;
}
public isCombined(): number {
// always mark joined cell data as combined
return Content.IS_COMBINED_MASK;
}
public getWidth(): number {
return this._width;
}
public getChars(): string {
return this.combinedData;
}
public getCode(): number {
// code always gets the highest possible fake codepoint (read as -1)
// this is needed as code is used by caches as identifier
return 0x1FFFFF;
}
public setFromCharData(value: CharData): void {
throw new Error('not implemented');
}
public getAsCharData(): CharData {
return [this.fg, this.getChars(), this.getWidth(), this.getCode()];
}
}
function clamp(value: number, max: number, min: number = 0): number {
return Math.max(Math.min(value, max), min);
}
+63
View File
@@ -0,0 +1,63 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @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.
*/
export const PROJECTION_MATRIX = new Float32Array([
2, 0, 0, 0,
0, -2, 0, 0,
0, 0, 1, 0,
-1, 1, 0, 1
]);
export function createProgram(gl: WebGLRenderingContext, vertexSource: string, fragmentSource: string): WebGLProgram | undefined {
const program = throwIfFalsy(gl.createProgram());
gl.attachShader(program, throwIfFalsy(createShader(gl, gl.VERTEX_SHADER, vertexSource)));
gl.attachShader(program, throwIfFalsy(createShader(gl, gl.FRAGMENT_SHADER, fragmentSource)));
gl.linkProgram(program);
const success = gl.getProgramParameter(program, gl.LINK_STATUS);
if (success) {
return program;
}
console.error(gl.getProgramInfoLog(program));
gl.deleteProgram(program);
}
export function createShader(gl: WebGLRenderingContext, type: number, source: string): WebGLShader | undefined {
const shader = throwIfFalsy(gl.createShader(type));
gl.shaderSource(shader, source);
gl.compileShader(shader);
const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (success) {
return shader;
}
console.error(gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
}
export function expandFloat32Array(source: Float32Array, max: number): Float32Array {
const newLength = Math.min(source.length * 2, max);
const newArray = new Float32Array(newLength);
for (let i = 0; i < source.length; i++) {
newArray[i] = source[i];
}
return newArray;
}
export class GLTexture {
public texture: WebGLTexture;
public version: number;
constructor(texture: WebGLTexture) {
this.texture = texture;
this.version = -1;
}
}
@@ -0,0 +1,220 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ReadonlyColorSet } from 'browser/Types';
import { acquireTextureAtlas } from 'browser/renderer/shared/CharAtlasCache';
import { TEXT_BASELINE } from 'browser/renderer/shared/Constants';
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
import { IRenderDimensions, ITextureAtlas } from 'browser/renderer/shared/Types';
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { CellData } from 'common/buffer/CellData';
import { IOptionsService } from 'common/services/Services';
import { Terminal } from '@xterm/xterm';
import { IRenderLayer } from './Types';
export abstract class BaseRenderLayer extends Disposable implements IRenderLayer {
private _canvas: HTMLCanvasElement;
protected _ctx!: CanvasRenderingContext2D;
private _deviceCharWidth: number = 0;
private _deviceCharHeight: number = 0;
private _deviceCellWidth: number = 0;
private _deviceCellHeight: number = 0;
private _deviceCharLeft: number = 0;
private _deviceCharTop: number = 0;
protected _charAtlas: ITextureAtlas | undefined;
constructor(
terminal: Terminal,
private _container: HTMLElement,
id: string,
zIndex: number,
private _alpha: boolean,
protected readonly _coreBrowserService: ICoreBrowserService,
protected readonly _optionsService: IOptionsService,
protected readonly _themeService: IThemeService
) {
super();
this._canvas = this._coreBrowserService.mainDocument.createElement('canvas');
this._canvas.classList.add(`xterm-${id}-layer`);
this._canvas.style.zIndex = zIndex.toString();
this._initCanvas();
this._container.appendChild(this._canvas);
this.register(this._themeService.onChangeColors(e => {
this._refreshCharAtlas(terminal, e);
this.reset(terminal);
}));
this.register(toDisposable(() => {
this._canvas.remove();
}));
}
private _initCanvas(): void {
this._ctx = throwIfFalsy(this._canvas.getContext('2d', { alpha: this._alpha }));
// Draw the background if this is an opaque layer
if (!this._alpha) {
this._clearAll();
}
}
public handleBlur(terminal: Terminal): void {}
public handleFocus(terminal: Terminal): void {}
public handleCursorMove(terminal: Terminal): void {}
public handleGridChanged(terminal: Terminal, startRow: number, endRow: number): void {}
public handleSelectionChanged(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {}
protected _setTransparency(terminal: Terminal, alpha: boolean): void {
// Do nothing when alpha doesn't change
if (alpha === this._alpha) {
return;
}
// Create new canvas and replace old one
const oldCanvas = this._canvas;
this._alpha = alpha;
// Cloning preserves properties
this._canvas = this._canvas.cloneNode() as HTMLCanvasElement;
this._initCanvas();
this._container.replaceChild(this._canvas, oldCanvas);
// Regenerate char atlas and force a full redraw
this._refreshCharAtlas(terminal, this._themeService.colors);
this.handleGridChanged(terminal, 0, terminal.rows - 1);
}
/**
* Refreshes the char atlas, aquiring a new one if necessary.
* @param terminal The terminal.
* @param colorSet The color set to use for the char atlas.
*/
private _refreshCharAtlas(terminal: Terminal, colorSet: ReadonlyColorSet): void {
if (this._deviceCharWidth <= 0 && this._deviceCharHeight <= 0) {
return;
}
this._charAtlas = acquireTextureAtlas(terminal, this._optionsService.rawOptions, colorSet, this._deviceCellWidth, this._deviceCellHeight, this._deviceCharWidth, this._deviceCharHeight, this._coreBrowserService.dpr);
this._charAtlas.warmUp();
}
public resize(terminal: Terminal, dim: IRenderDimensions): void {
this._deviceCellWidth = dim.device.cell.width;
this._deviceCellHeight = dim.device.cell.height;
this._deviceCharWidth = dim.device.char.width;
this._deviceCharHeight = dim.device.char.height;
this._deviceCharLeft = dim.device.char.left;
this._deviceCharTop = dim.device.char.top;
this._canvas.width = dim.device.canvas.width;
this._canvas.height = dim.device.canvas.height;
this._canvas.style.width = `${dim.css.canvas.width}px`;
this._canvas.style.height = `${dim.css.canvas.height}px`;
// Draw the background if this is an opaque layer
if (!this._alpha) {
this._clearAll();
}
this._refreshCharAtlas(terminal, this._themeService.colors);
}
public abstract reset(terminal: Terminal): void;
/**
* Fills a 1px line (2px on HDPI) at the bottom of the cell. This uses the
* existing fillStyle on the context.
* @param x The column to fill.
* @param y The row to fill.
*/
protected _fillBottomLineAtCells(x: number, y: number, width: number = 1): void {
this._ctx.fillRect(
x * this._deviceCellWidth,
(y + 1) * this._deviceCellHeight - this._coreBrowserService.dpr - 1 /* Ensure it's drawn within the cell */,
width * this._deviceCellWidth,
this._coreBrowserService.dpr);
}
/**
* Clears the entire canvas.
*/
protected _clearAll(): void {
if (this._alpha) {
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
} else {
this._ctx.fillStyle = this._themeService.colors.background.css;
this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height);
}
}
/**
* Clears 1+ cells completely.
* @param x The column to start at.
* @param y The row to start at.
* @param width The number of columns to clear.
* @param height The number of rows to clear.
*/
protected _clearCells(x: number, y: number, width: number, height: number): void {
if (this._alpha) {
this._ctx.clearRect(
x * this._deviceCellWidth,
y * this._deviceCellHeight,
width * this._deviceCellWidth,
height * this._deviceCellHeight);
} else {
this._ctx.fillStyle = this._themeService.colors.background.css;
this._ctx.fillRect(
x * this._deviceCellWidth,
y * this._deviceCellHeight,
width * this._deviceCellWidth,
height * this._deviceCellHeight);
}
}
/**
* Draws a truecolor character at the cell. The character will be clipped to
* ensure that it fits with the cell, including the cell to the right if it's
* a wide character. This uses the existing fillStyle on the context.
* @param terminal The terminal.
* @param cell The cell data for the character to draw.
* @param x The column to draw at.
* @param y The row to draw at.
*/
protected _fillCharTrueColor(terminal: Terminal, cell: CellData, x: number, y: number): void {
this._ctx.font = this._getFont(terminal, false, false);
this._ctx.textBaseline = TEXT_BASELINE;
this._clipCell(x, y, cell.getWidth());
this._ctx.fillText(
cell.getChars(),
x * this._deviceCellWidth + this._deviceCharLeft,
y * this._deviceCellHeight + this._deviceCharTop + this._deviceCharHeight);
}
/**
* Clips a cell to ensure no pixels will be drawn outside of it.
* @param x The column to clip.
* @param y The row to clip.
* @param width The number of columns to clip.
*/
private _clipCell(x: number, y: number, width: number): void {
this._ctx.beginPath();
this._ctx.rect(
x * this._deviceCellWidth,
y * this._deviceCellHeight,
width * this._deviceCellWidth,
this._deviceCellHeight);
this._ctx.clip();
}
/**
* Gets the current font.
* @param terminal The terminal.
* @param isBold If we should use the bold fontWeight.
*/
protected _getFont(terminal: Terminal, isBold: boolean, isItalic: boolean): string {
const fontWeight = isBold ? terminal.options.fontWeightBold : terminal.options.fontWeight;
const fontStyle = isItalic ? 'italic' : '';
return `${fontStyle} ${fontWeight} ${terminal.options.fontSize! * this._coreBrowserService.dpr}px ${terminal.options.fontFamily}`;
}
}
@@ -0,0 +1,82 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { is256Color } from 'browser/renderer/shared/CharAtlasUtils';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
import { IRenderDimensions } from 'browser/renderer/shared/Types';
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { ILinkifier2, ILinkifierEvent } from 'browser/Types';
import { IOptionsService } from 'common/services/Services';
import { Terminal } from '@xterm/xterm';
import { BaseRenderLayer } from './BaseRenderLayer';
export class LinkRenderLayer extends BaseRenderLayer {
private _state: ILinkifierEvent | undefined;
constructor(
container: HTMLElement,
zIndex: number,
terminal: Terminal,
linkifier2: ILinkifier2,
coreBrowserService: ICoreBrowserService,
optionsService: IOptionsService,
themeService: IThemeService
) {
super(terminal, container, 'link', zIndex, true, coreBrowserService, optionsService, themeService);
this.register(linkifier2.onShowLinkUnderline(e => this._handleShowLinkUnderline(e)));
this.register(linkifier2.onHideLinkUnderline(e => this._handleHideLinkUnderline(e)));
}
public resize(terminal: Terminal, dim: IRenderDimensions): void {
super.resize(terminal, dim);
// Resizing the canvas discards the contents of the canvas so clear state
this._state = undefined;
}
public reset(terminal: Terminal): void {
this._clearCurrentLink();
}
private _clearCurrentLink(): void {
if (this._state) {
this._clearCells(this._state.x1, this._state.y1, this._state.cols - this._state.x1, 1);
const middleRowCount = this._state.y2 - this._state.y1 - 1;
if (middleRowCount > 0) {
this._clearCells(0, this._state.y1 + 1, this._state.cols, middleRowCount);
}
this._clearCells(0, this._state.y2, this._state.x2, 1);
this._state = undefined;
}
}
private _handleShowLinkUnderline(e: ILinkifierEvent): void {
if (e.fg === INVERTED_DEFAULT_COLOR) {
this._ctx.fillStyle = this._themeService.colors.background.css;
} else if (e.fg !== undefined && is256Color(e.fg)) {
// 256 color support
this._ctx.fillStyle = this._themeService.colors.ansi[e.fg!].css;
} else {
this._ctx.fillStyle = this._themeService.colors.foreground.css;
}
if (e.y1 === e.y2) {
// Single line link
this._fillBottomLineAtCells(e.x1, e.y1, e.x2 - e.x1);
} else {
// Multi-line link
this._fillBottomLineAtCells(e.x1, e.y1, e.cols - e.x1);
for (let y = e.y1 + 1; y < e.y2; y++) {
this._fillBottomLineAtCells(0, y, e.cols);
}
this._fillBottomLineAtCells(0, e.y2, e.x2);
}
this._state = e;
}
private _handleHideLinkUnderline(e: ILinkifierEvent): void {
this._clearCurrentLink();
}
}
@@ -0,0 +1,55 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IDisposable, Terminal } from '@xterm/xterm';
import { IRenderDimensions } from 'browser/renderer/shared/Types';
export interface IRenderLayer extends IDisposable {
/**
* Called when the terminal loses focus.
*/
handleBlur(terminal: Terminal): void;
/**
* Called when the terminal gets focus.
*/
handleFocus(terminal: Terminal): void;
/**
* Called when the cursor is moved.
*/
handleCursorMove(terminal: Terminal): void;
/**
* Called when the data in the grid has changed (or needs to be rendered
* again).
*/
handleGridChanged(terminal: Terminal, startRow: number, endRow: number): void;
/**
* Calls when the selection changes.
*/
handleSelectionChanged(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
/**
* Registers a handler to join characters to render as a group
*/
registerCharacterJoiner?(handler: (text: string) => [number, number][]): void;
/**
* Deregisters the specified character joiner handler
*/
deregisterCharacterJoiner?(joinerId: number): void;
/**
* Resize the render layer.
*/
resize(terminal: Terminal, dim: IRenderDimensions): void;
/**
* Clear the state of the render layer.
*/
reset(terminal: Terminal): void;
}
+44
View File
@@ -0,0 +1,44 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2021",
"lib": [
"dom",
"es2021"
],
"rootDir": ".",
"outDir": "../out",
"sourceMap": true,
"removeComments": true,
"baseUrl": ".",
"paths": {
"common/*": [
"../../../src/common/*"
],
"browser/*": [
"../../../src/browser/*"
],
"@xterm/addon-webgl": [
"../typings/addon-webgl.d.ts"
]
},
"strict": true,
"downlevelIteration": true,
"experimentalDecorators": true,
"types": [
"../../../node_modules/@types/mocha"
]
},
"include": [
"./**/*",
"../../../typings/xterm.d.ts"
],
"references": [
{
"path": "../../../src/common"
},
{
"path": "../../../src/browser"
}
]
}
@@ -0,0 +1,33 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import test from '@playwright/test';
import { ISharedRendererTestContext, injectSharedRendererTests, injectSharedRendererTestsStandalone } from '../../../out-test/playwright/SharedRendererTests';
import { ITestContext, createTestContext, openTerminal } from '../../../out-test/playwright/TestUtils';
import { platform } from 'os';
let ctx: ITestContext;
const ctxWrapper: ISharedRendererTestContext = { value: undefined } as any;
test.beforeAll(async ({ browser }) => {
ctx = await createTestContext(browser);
await openTerminal(ctx);
ctxWrapper.value = ctx;
await ctx.page.evaluate(`
window.addon = new window.WebglAddon(true);
window.term.loadAddon(window.addon);
`);
});
test.afterAll(async () => await ctx.page.close());
test.describe('WebGL Renderer Integration Tests', async () => {
// HACK: webgl2 is often not supported in headless firefox on Linux
// https://github.com/microsoft/playwright/issues/11566
if (platform() === 'linux') {
test.skip(({ browserName }) => browserName === 'firefox');
}
injectSharedRendererTests(ctxWrapper);
injectSharedRendererTestsStandalone(ctxWrapper);
});
@@ -0,0 +1,35 @@
import { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
testDir: '.',
timeout: 10000,
projects: [
{
name: 'Chrome Stable',
use: {
browserName: 'chromium',
channel: 'chrome'
}
},
{
name: 'Firefox Stable',
use: {
browserName: 'firefox'
}
},
{
name: 'WebKit',
use: {
browserName: 'webkit'
}
}
],
reporter: 'list',
webServer: {
command: 'npm run start-server-only',
port: 3000,
timeout: 120000,
reuseExistingServer: !process.env.CI
}
};
export default config;
+41
View File
@@ -0,0 +1,41 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2021",
"lib": [
"es2021",
],
"rootDir": ".",
"outDir": "../out-test",
"sourceMap": true,
"removeComments": true,
"baseUrl": ".",
"paths": {
"common/*": [
"../../../src/common/*"
],
"browser/*": [
"../../../src/browser/*"
]
},
"strict": true,
"types": [
"../../../node_modules/@types/node",
"../../../node_modules/@lunapaint/png-codec",
"../../../out-test/playwright/TestUtils",
"../../../out-test/playwright/SharedRendererTests"
]
},
"include": [
"./**/*",
"../../../typings/xterm.d.ts"
],
"references": [
{
"path": "../../../src/common"
},
{
"path": "../../../src/browser"
}
]
}
+8
View File
@@ -0,0 +1,8 @@
{
"files": [],
"include": [],
"references": [
{ "path": "./src" },
{ "path": "./test" }
]
}
+48
View File
@@ -0,0 +1,48 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { Terminal, ITerminalAddon, IEvent } from '@xterm/xterm';
declare module '@xterm/addon-webgl' {
/**
* An xterm.js addon that provides search functionality.
*/
export class WebglAddon implements ITerminalAddon {
public textureAtlas?: HTMLCanvasElement;
/**
* An event that is fired when the renderer loses its canvas context.
*/
public readonly onContextLoss: IEvent<void>;
/**
* An event that is fired when the texture atlas of the renderer changes.
*/
public readonly onChangeTextureAtlas: IEvent<HTMLCanvasElement>;
/**
* An event that is fired when the a new page is added to the texture atlas.
*/
public readonly onAddTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
constructor(preserveDrawingBuffer?: boolean);
/**
* Activates the addon.
* @param terminal The terminal the addon is being loaded in.
*/
public activate(terminal: Terminal): void;
/**
* Disposes the addon.
*/
public dispose(): void;
/**
* Clears the terminal's texture atlas and triggers a redraw.
*/
public clearTextureAtlas(): void;
}
}
+39
View File
@@ -0,0 +1,39 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
const path = require('path');
const addonName = 'WebglAddon';
const mainFile = 'addon-webgl.js';
module.exports = {
entry: `./out/${addonName}.js`,
devtool: 'source-map',
module: {
rules: [
{
test: /\.js$/,
use: ["source-map-loader"],
enforce: "pre",
exclude: /node_modules/
}
]
},
resolve: {
modules: ['./node_modules'],
extensions: [ '.js' ],
alias: {
common: path.resolve('../../out/common'),
browser: path.resolve('../../out/browser')
}
},
output: {
filename: mainFile,
path: path.resolve('./lib'),
library: addonName,
libraryTarget: 'umd'
},
mode: 'production'
};