mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge branch 'master' into parser_colon_notation
This commit is contained in:
@@ -19,3 +19,7 @@ package-lock.json
|
||||
# Keep bundled code out of Git
|
||||
dist/
|
||||
demo/dist/
|
||||
|
||||
# dont commit benchmark folders
|
||||
.benchmark/
|
||||
timeline/
|
||||
|
||||
@@ -7,7 +7,7 @@ Xterm.js is a front-end component written in TypeScript that lets applications b
|
||||
## Features
|
||||
|
||||
- **Terminal apps just work**: Xterm.js works with most terminal apps such as `bash`, `vim` and `tmux`, this includes support for curses-based apps and mouse event support.
|
||||
- **Perfomant**: Xterm.js is *really* fast, it even includes a GPU-accelerated renderer.
|
||||
- **Performant**: Xterm.js is *really* fast, it even includes a GPU-accelerated renderer.
|
||||
- **Rich unicode support**: Supports CJK, emojis and IMEs.
|
||||
- **Self-contained**: Requires zero dependencies to work.
|
||||
- **Accessible**: Screen reader support can be turned on using the `screenReaderMode` option.
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
lib
|
||||
node_modules
|
||||
@@ -0,0 +1,5 @@
|
||||
**/*.api.js
|
||||
**/*.api.ts
|
||||
tsconfig.json
|
||||
.yarnrc
|
||||
webpack.config.js
|
||||
@@ -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.
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "xterm-addon-webgl",
|
||||
"version": "0.1.0",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
},
|
||||
"main": "lib/xterm-addon-webgl.js",
|
||||
"types": "typings/xterm-addon-webgl.d.ts",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p src",
|
||||
"prepackage": "npm run build",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"xterm": "^3.14.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { FLAGS } from './Constants';
|
||||
import { IBufferLine } from 'common/Types';
|
||||
|
||||
export function getCompatAttr(bufferLine: IBufferLine, index: number): number {
|
||||
// TODO: Need to move WebGL over to the new system and remove this block
|
||||
const cell = new CellData();
|
||||
bufferLine.loadCell(index, cell);
|
||||
const oldBg = cell.getBgColor() === -1 ? 256 : cell.getBgColor();
|
||||
const oldFg = cell.getFgColor() === -1 ? 256 : cell.getFgColor();
|
||||
const oldAttr =
|
||||
(cell.isBold() ? FLAGS.BOLD : 0) |
|
||||
(cell.isUnderline() ? FLAGS.UNDERLINE : 0) |
|
||||
(cell.isBlink() ? FLAGS.BLINK : 0) |
|
||||
(cell.isInverse() ? FLAGS.INVERSE : 0) |
|
||||
(cell.isDim() ? FLAGS.DIM : 0) |
|
||||
(cell.isItalic() ? FLAGS.ITALIC : 0);
|
||||
const attrCompat =
|
||||
oldBg |
|
||||
(oldFg << 9) |
|
||||
(oldAttr << 18);
|
||||
return attrCompat;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* @license MIT
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
*/
|
||||
|
||||
import { IColor } from 'browser/Types';
|
||||
|
||||
export function getLuminance(color: IColor): number {
|
||||
// Coefficients taken from: https://www.w3.org/TR/AERT/#color-contrast
|
||||
const r = color.rgba >> 24 & 0xff;
|
||||
const g = color.rgba >> 16 & 0xff;
|
||||
const b = color.rgba >> 8 & 0xff;
|
||||
return (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
// TODO: Should be removed after chardata workaround is fixed
|
||||
export const enum FLAGS {
|
||||
BOLD = 1,
|
||||
UNDERLINE = 2,
|
||||
BLINK = 4,
|
||||
INVERSE = 8,
|
||||
INVISIBLE = 16,
|
||||
DIM = 32,
|
||||
ITALIC = 64
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { createProgram, PROJECTION_MATRIX } from './WebglUtils';
|
||||
import { WebglCharAtlas } from './atlas/WebglCharAtlas';
|
||||
import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel, IRasterizedGlyph } from './Types';
|
||||
import { INDICIES_PER_CELL } from './WebglRenderer';
|
||||
import { COMBINED_CHAR_BIT_MASK } from './RenderModel';
|
||||
import { fill } from 'common/TypedArrayUtils';
|
||||
import { slice } from './TypedArray';
|
||||
import { NULL_CELL_CODE, WHITESPACE_CELL_CODE } from 'common/buffer/Constants';
|
||||
import { getLuminance } from './ColorUtils';
|
||||
import { Terminal, IBufferLine } from 'xterm';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
|
||||
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[];
|
||||
selectionAttributes: Float32Array;
|
||||
count: number;
|
||||
}
|
||||
|
||||
const enum VertexAttribLocations {
|
||||
UNIT_QUAD = 0,
|
||||
CELL_POSITION = 1,
|
||||
OFFSET = 2,
|
||||
SIZE = 3,
|
||||
TEXCOORD = 4,
|
||||
TEXSIZE = 5
|
||||
}
|
||||
|
||||
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.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;
|
||||
|
||||
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_texcoord = a_texcoord + a_unitquad * a_texsize;
|
||||
}`;
|
||||
|
||||
const fragmentShaderSource = `#version 300 es
|
||||
precision lowp float;
|
||||
|
||||
in vec2 v_texcoord;
|
||||
|
||||
uniform sampler2D u_texture;
|
||||
|
||||
out vec4 outColor;
|
||||
|
||||
void main() {
|
||||
outColor = texture(u_texture, v_texcoord);
|
||||
}`;
|
||||
|
||||
const INDICES_PER_CELL = 10;
|
||||
const BYTES_PER_CELL = INDICES_PER_CELL * Float32Array.BYTES_PER_ELEMENT;
|
||||
const CELL_POSITION_INDICES = 2;
|
||||
|
||||
export class GlyphRenderer {
|
||||
private _atlas: WebglCharAtlas;
|
||||
|
||||
private _program: WebGLProgram;
|
||||
private _vertexArrayObject: IWebGLVertexArrayObject;
|
||||
private _projectionLocation: WebGLUniformLocation;
|
||||
private _resolutionLocation: WebGLUniformLocation;
|
||||
private _textureLocation: WebGLUniformLocation;
|
||||
private _atlasTexture: WebGLTexture;
|
||||
private _attributesBuffer: WebGLBuffer;
|
||||
private _activeBuffer: number = 0;
|
||||
|
||||
private _vertices: IVertices = {
|
||||
count: 0,
|
||||
attributes: new Float32Array(0),
|
||||
attributesBuffers: [
|
||||
new Float32Array(0),
|
||||
new Float32Array(0)
|
||||
],
|
||||
selectionAttributes: new Float32Array(0)
|
||||
};
|
||||
|
||||
constructor(
|
||||
private _terminal: Terminal,
|
||||
private _colors: IColorSet,
|
||||
private _gl: IWebGL2RenderingContext,
|
||||
private _dimensions: IRenderDimensions
|
||||
) {
|
||||
const gl = this._gl;
|
||||
|
||||
this._program = createProgram(gl, vertexShaderSource, fragmentShaderSource);
|
||||
|
||||
// Uniform locations
|
||||
this._projectionLocation = gl.getUniformLocation(this._program, 'u_projection');
|
||||
this._resolutionLocation = gl.getUniformLocation(this._program, 'u_resolution');
|
||||
this._textureLocation = 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();
|
||||
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
|
||||
// unitQuadVertuces to allow is to draw 2 triangles from the vertices
|
||||
const unitQuadElementIndices = new Uint8Array([0, 1, 3, 0, 2, 3]);
|
||||
const elementIndicesBuffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elementIndicesBuffer);
|
||||
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, unitQuadElementIndices, gl.STATIC_DRAW);
|
||||
|
||||
// Setup attributes
|
||||
this._attributesBuffer = gl.createBuffer();
|
||||
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.TEXCOORD);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.TEXCOORD, 2, gl.FLOAT, false, BYTES_PER_CELL, 4 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribDivisor(VertexAttribLocations.TEXCOORD, 1);
|
||||
gl.enableVertexAttribArray(VertexAttribLocations.TEXSIZE);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.TEXSIZE, 2, gl.FLOAT, false, BYTES_PER_CELL, 6 * 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, 8 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribDivisor(VertexAttribLocations.CELL_POSITION, 1);
|
||||
|
||||
// Setup empty texture atlas
|
||||
this._atlasTexture = gl.createTexture();
|
||||
gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 255, 255]));
|
||||
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);
|
||||
|
||||
// Allow drawing of transparent texture
|
||||
gl.enable(gl.BLEND);
|
||||
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
// Set viewport
|
||||
this.onResize();
|
||||
}
|
||||
|
||||
public beginFrame(): boolean {
|
||||
return this._atlas.beginFrame();
|
||||
}
|
||||
|
||||
public updateCell(x: number, y: number, code: number, attr: number, bg: number, fg: number, chars: string): void {
|
||||
this._updateCell(this._vertices.attributes, x, y, code, attr, bg, fg, chars);
|
||||
}
|
||||
|
||||
private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, attr: number, bg: number, fg: number, chars?: string): void {
|
||||
const terminal = this._terminal;
|
||||
|
||||
const i = (y * terminal.cols + x) * INDICES_PER_CELL;
|
||||
|
||||
// Exit early if this is a null/space character
|
||||
if (code === NULL_CELL_CODE || code === WHITESPACE_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);
|
||||
return;
|
||||
}
|
||||
|
||||
let rasterizedGlyph: IRasterizedGlyph;
|
||||
if (chars && chars.length > 1) {
|
||||
rasterizedGlyph = this._atlas.getRasterizedGlyphCombinedChar(chars, attr, bg, fg);
|
||||
} else {
|
||||
rasterizedGlyph = this._atlas.getRasterizedGlyph(code, attr, bg, fg);
|
||||
}
|
||||
|
||||
// Fill empty if no glyph was found
|
||||
if (!rasterizedGlyph) {
|
||||
fill(array, 0, i, i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES);
|
||||
return;
|
||||
}
|
||||
|
||||
// a_origin
|
||||
array[i ] = -rasterizedGlyph.offset.x + this._dimensions.scaledCharLeft;
|
||||
array[i + 1] = -rasterizedGlyph.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;
|
||||
// a_texcoord
|
||||
array[i + 4] = rasterizedGlyph.texturePositionClipSpace.x;
|
||||
array[i + 5] = rasterizedGlyph.texturePositionClipSpace.y;
|
||||
// a_texsize
|
||||
array[i + 6] = rasterizedGlyph.sizeClipSpace.x;
|
||||
array[i + 7] = rasterizedGlyph.sizeClipSpace.y;
|
||||
// a_cellpos only changes on resize
|
||||
}
|
||||
|
||||
public updateSelection(model: IRenderModel, columnSelectMode: boolean): void {
|
||||
const terminal = this._terminal;
|
||||
|
||||
this._vertices.selectionAttributes = slice(this._vertices.attributes, 0);
|
||||
|
||||
// TODO: Make fg and bg configurable, currently since the buffer doesn't
|
||||
// support truecolor the char atlas cannot store it.
|
||||
const lumi = getLuminance(this._colors.background);
|
||||
const fg = lumi > 0.5 ? 7 : 0;
|
||||
const bg = lumi > 0.5 ? 0 : 7;
|
||||
|
||||
if (columnSelectMode) {
|
||||
const startCol = model.selection.startCol;
|
||||
const width = model.selection.endCol - startCol;
|
||||
const height = model.selection.viewportCappedEndRow - model.selection.viewportCappedStartRow + 1;
|
||||
for (let y = model.selection.viewportCappedStartRow; y < model.selection.viewportCappedStartRow + height; y++) {
|
||||
this._updateSelectionRange(startCol, startCol + width, y, model, bg, fg);
|
||||
}
|
||||
} else {
|
||||
// Draw first row
|
||||
const startCol = model.selection.viewportStartRow === model.selection.viewportCappedStartRow ? model.selection.startCol : 0;
|
||||
const startRowEndCol = model.selection.viewportCappedStartRow === model.selection.viewportCappedEndRow ? model.selection.endCol : terminal.cols;
|
||||
this._updateSelectionRange(startCol, startRowEndCol, model.selection.viewportCappedStartRow, model, bg, fg);
|
||||
|
||||
// Draw middle rows
|
||||
const middleRowsCount = Math.max(model.selection.viewportCappedEndRow - model.selection.viewportCappedStartRow - 1, 0);
|
||||
for (let y = model.selection.viewportCappedStartRow + 1; y <= model.selection.viewportCappedStartRow + middleRowsCount; y++) {
|
||||
this._updateSelectionRange(0, startRowEndCol, y, model, bg, fg);
|
||||
}
|
||||
|
||||
// Draw final row
|
||||
if (model.selection.viewportCappedStartRow !== model.selection.viewportCappedEndRow) {
|
||||
// Only draw viewportEndRow if it's not the same as viewportStartRow
|
||||
const endCol = model.selection.viewportEndRow === model.selection.viewportCappedEndRow ? model.selection.endCol : terminal.cols;
|
||||
this._updateSelectionRange(0, endCol, model.selection.viewportCappedEndRow, model, bg, fg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _updateSelectionRange(startCol: number, endCol: number, y: number, model: IRenderModel, bg: number, fg: number): void {
|
||||
const terminal = this._terminal;
|
||||
const row = y + terminal.buffer.viewportY;
|
||||
let line: IBufferLine | undefined;
|
||||
for (let x = startCol; x < endCol; x++) {
|
||||
const offset = (y * this._terminal.cols + x) * INDICIES_PER_CELL;
|
||||
// Because the cache uses attr as a lookup key it needs to contain the selection colors as well
|
||||
let attr = model.cells[offset + 1];
|
||||
attr = attr & ~0x3ffff | bg << 9 | fg;
|
||||
const code = model.cells[offset];
|
||||
if (code & COMBINED_CHAR_BIT_MASK) {
|
||||
if (!line) {
|
||||
line = terminal.buffer.getLine(row);
|
||||
}
|
||||
const chars = line.getCell(x).char;
|
||||
this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], attr, bg, fg, chars);
|
||||
} else {
|
||||
this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], attr, bg, fg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public onResize(): void {
|
||||
const terminal = this._terminal;
|
||||
const gl = this._gl;
|
||||
|
||||
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
||||
|
||||
// Update vertices
|
||||
const newCount = terminal.cols * terminal.rows * INDICES_PER_CELL;
|
||||
if (this._vertices.count !== newCount) {
|
||||
this._vertices.count = newCount;
|
||||
this._vertices.attributes = new Float32Array(newCount);
|
||||
for (let i = 0; i < this._vertices.attributesBuffers.length; i++) {
|
||||
this._vertices.attributesBuffers[i] = new Float32Array(newCount);
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
for (let y = 0; y < terminal.rows; y++) {
|
||||
for (let x = 0; x < terminal.cols; x++) {
|
||||
this._vertices.attributes[i + 8] = x / terminal.cols;
|
||||
this._vertices.attributes[i + 9] = y / terminal.rows;
|
||||
i += INDICES_PER_CELL;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public setColors(): void {
|
||||
}
|
||||
|
||||
public render(renderModel: IRenderModel, isSelectionVisible: boolean): 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 = (isSelectionVisible ? this._vertices.selectionAttributes : 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 texture atlas if it's changed
|
||||
if (this._atlas.hasCanvasChanged) {
|
||||
this._atlas.hasCanvasChanged = false;
|
||||
gl.uniform1i(this._textureLocation, 0);
|
||||
gl.activeTexture(gl.TEXTURE0 + 0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._atlas.cacheCanvas);
|
||||
gl.generateMipmap(gl.TEXTURE_2D);
|
||||
}
|
||||
|
||||
// Set uniforms
|
||||
gl.uniformMatrix4fv(this._projectionLocation, false, PROJECTION_MATRIX);
|
||||
gl.uniform2f(this._resolutionLocation, gl.canvas.width, gl.canvas.height);
|
||||
|
||||
// Draw the viewport
|
||||
gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, bufferLength / INDICES_PER_CELL);
|
||||
}
|
||||
|
||||
public setAtlas(atlas: WebglCharAtlas): void {
|
||||
const gl = this._gl;
|
||||
this._atlas = atlas;
|
||||
|
||||
gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, atlas.cacheCanvas);
|
||||
gl.generateMipmap(gl.TEXTURE_2D);
|
||||
}
|
||||
|
||||
public setDimensions(dimensions: IRenderDimensions): void {
|
||||
this._dimensions = dimensions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { createProgram, expandFloat32Array, PROJECTION_MATRIX } from './WebglUtils';
|
||||
import { IRenderModel, IWebGLVertexArrayObject, IWebGL2RenderingContext, ISelectionRenderModel } from './Types';
|
||||
import { fill } from 'common/TypedArrayUtils';
|
||||
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
|
||||
import { is256Color } from './atlas/CharAtlasUtils';
|
||||
import { DEFAULT_COLOR } from 'common/buffer/Constants';
|
||||
import { Terminal } from 'xterm';
|
||||
import { IColorSet, IColor } from 'browser/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
|
||||
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 vec3 a_color;
|
||||
layout (location = ${VertexAttribLocations.UNIT_QUAD}) in vec2 a_unitquad;
|
||||
|
||||
uniform mat4 u_projection;
|
||||
uniform vec2 u_resolution;
|
||||
|
||||
out vec3 v_color;
|
||||
|
||||
void main() {
|
||||
vec2 zeroToOne = (a_position + (a_unitquad * a_size)) / u_resolution;
|
||||
gl_Position = u_projection * vec4(zeroToOne, 0.0, 1.0);
|
||||
v_color = a_color;
|
||||
}`;
|
||||
|
||||
const fragmentShaderSource = `#version 300 es
|
||||
precision lowp float;
|
||||
|
||||
in vec3 v_color;
|
||||
|
||||
out vec4 outColor;
|
||||
|
||||
void main() {
|
||||
outColor = vec4(v_color, 1);
|
||||
}`;
|
||||
|
||||
interface IVertices {
|
||||
attributes: Float32Array;
|
||||
selection: Float32Array;
|
||||
count: number;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
export class RectangleRenderer {
|
||||
|
||||
private _program: WebGLProgram;
|
||||
private _vertexArrayObject: IWebGLVertexArrayObject;
|
||||
private _resolutionLocation: WebGLUniformLocation;
|
||||
private _attributesBuffer: WebGLBuffer;
|
||||
private _projectionLocation: WebGLUniformLocation;
|
||||
private _bgFloat: Float32Array;
|
||||
private _selectionFloat: Float32Array;
|
||||
|
||||
private _vertices: IVertices = {
|
||||
count: 0,
|
||||
attributes: new Float32Array(INITIAL_BUFFER_RECTANGLE_CAPACITY),
|
||||
selection: new Float32Array(3 * INDICES_PER_RECTANGLE)
|
||||
};
|
||||
|
||||
constructor(
|
||||
private _terminal: Terminal,
|
||||
private _colors: IColorSet,
|
||||
private _gl: IWebGL2RenderingContext,
|
||||
private _dimensions: IRenderDimensions
|
||||
) {
|
||||
const gl = this._gl;
|
||||
|
||||
this._program = createProgram(gl, vertexShaderSource, fragmentShaderSource);
|
||||
|
||||
// Uniform locations
|
||||
this._resolutionLocation = gl.getUniformLocation(this._program, 'u_resolution');
|
||||
this._projectionLocation = 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();
|
||||
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
|
||||
// unitQuadVertuces to allow is to draw 2 triangles from the vertices
|
||||
const unitQuadElementIndices = new Uint8Array([0, 1, 3, 0, 2, 3]);
|
||||
const elementIndicesBuffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elementIndicesBuffer);
|
||||
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, unitQuadElementIndices, gl.STATIC_DRAW);
|
||||
|
||||
// Setup attributes
|
||||
this._attributesBuffer = gl.createBuffer();
|
||||
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();
|
||||
}
|
||||
|
||||
public render(): void {
|
||||
const gl = this._gl;
|
||||
|
||||
gl.useProgram(this._program);
|
||||
|
||||
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);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, this._vertices.attributes, gl.DYNAMIC_DRAW);
|
||||
gl.drawElementsInstanced(this._gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, this._vertices.count);
|
||||
|
||||
// Bind selection buffer and draw
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, this._vertices.selection, gl.DYNAMIC_DRAW);
|
||||
gl.drawElementsInstanced(this._gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, 3);
|
||||
}
|
||||
|
||||
public onResize(): void {
|
||||
this._updateViewportRectangle();
|
||||
}
|
||||
|
||||
public setColors(): void {
|
||||
this._updateCachedColors();
|
||||
this._updateViewportRectangle();
|
||||
}
|
||||
|
||||
private _updateCachedColors(): void {
|
||||
this._bgFloat = this._colorToFloat32Array(this._colors.background);
|
||||
this._selectionFloat = this._colorToFloat32Array(this._colors.selection);
|
||||
}
|
||||
|
||||
private _updateViewportRectangle(): void {
|
||||
// Set first rectangle that clears the screen
|
||||
this._addRectangleFloat(
|
||||
this._vertices.attributes,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
this._terminal.cols * this._dimensions.scaledCellWidth,
|
||||
this._terminal.rows * this._dimensions.scaledCellHeight,
|
||||
this._bgFloat
|
||||
);
|
||||
}
|
||||
|
||||
public updateSelection(model: ISelectionRenderModel, columnSelectMode: boolean): void {
|
||||
const terminal = this._terminal;
|
||||
|
||||
if (!model.hasSelection) {
|
||||
fill(this._vertices.selection, 0, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (columnSelectMode) {
|
||||
const startCol = model.startCol;
|
||||
const width = model.endCol - startCol;
|
||||
const height = model.viewportCappedEndRow - model.viewportCappedStartRow + 1;
|
||||
this._addRectangleFloat(
|
||||
this._vertices.selection,
|
||||
0,
|
||||
startCol * this._dimensions.scaledCellWidth,
|
||||
model.viewportCappedStartRow * this._dimensions.scaledCellHeight,
|
||||
width * this._dimensions.scaledCellWidth,
|
||||
height * this._dimensions.scaledCellHeight,
|
||||
this._selectionFloat
|
||||
);
|
||||
fill(this._vertices.selection, 0, INDICES_PER_RECTANGLE);
|
||||
} else {
|
||||
// Draw first row
|
||||
const startCol = model.viewportStartRow === model.viewportCappedStartRow ? model.startCol : 0;
|
||||
const startRowEndCol = model.viewportCappedStartRow === model.viewportCappedEndRow ? model.endCol : terminal.cols;
|
||||
this._addRectangleFloat(
|
||||
this._vertices.selection,
|
||||
0,
|
||||
startCol * this._dimensions.scaledCellWidth,
|
||||
model.viewportCappedStartRow * this._dimensions.scaledCellHeight,
|
||||
(startRowEndCol - startCol) * this._dimensions.scaledCellWidth,
|
||||
this._dimensions.scaledCellHeight,
|
||||
this._selectionFloat
|
||||
);
|
||||
|
||||
// Draw middle rows
|
||||
const middleRowsCount = Math.max(model.viewportCappedEndRow - model.viewportCappedStartRow - 1, 0);
|
||||
this._addRectangleFloat(
|
||||
this._vertices.selection,
|
||||
INDICES_PER_RECTANGLE,
|
||||
0,
|
||||
(model.viewportCappedStartRow + 1) * this._dimensions.scaledCellHeight,
|
||||
terminal.cols * this._dimensions.scaledCellWidth,
|
||||
middleRowsCount * this._dimensions.scaledCellHeight,
|
||||
this._selectionFloat
|
||||
);
|
||||
|
||||
// Draw final row
|
||||
if (model.viewportCappedStartRow !== model.viewportCappedEndRow) {
|
||||
// Only draw viewportEndRow if it's not the same as viewportStartRow
|
||||
const endCol = model.viewportEndRow === model.viewportCappedEndRow ? model.endCol : terminal.cols;
|
||||
this._addRectangleFloat(
|
||||
this._vertices.selection,
|
||||
INDICES_PER_RECTANGLE * 2,
|
||||
0,
|
||||
model.viewportCappedEndRow * this._dimensions.scaledCellHeight,
|
||||
endCol * this._dimensions.scaledCellWidth,
|
||||
this._dimensions.scaledCellHeight,
|
||||
this._selectionFloat
|
||||
);
|
||||
} else {
|
||||
fill(this._vertices.selection, 0, INDICES_PER_RECTANGLE * 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public updateBackgrounds(model: IRenderModel): void {
|
||||
const terminal = this._terminal;
|
||||
const vertices = this._vertices;
|
||||
|
||||
let rectangleCount = 1;
|
||||
|
||||
for (let y = 0; y < terminal.rows; y++) {
|
||||
let currentStartX = -1;
|
||||
let currentBg = DEFAULT_COLOR;
|
||||
for (let x = 0; x < terminal.cols; x++) {
|
||||
const modelIndex = ((y * terminal.cols) + x) * 4;
|
||||
const bg = model.cells[modelIndex + 2];
|
||||
if (bg !== currentBg) {
|
||||
// A rectangle needs to be drawn if going from non-default to another color
|
||||
if (currentBg !== DEFAULT_COLOR) {
|
||||
const offset = rectangleCount++ * INDICES_PER_RECTANGLE;
|
||||
this._updateRectangle(vertices, offset, currentBg, currentStartX, x, y);
|
||||
}
|
||||
currentStartX = x;
|
||||
currentBg = bg;
|
||||
}
|
||||
}
|
||||
// Finish rectangle if it's still going
|
||||
if (currentBg !== DEFAULT_COLOR) {
|
||||
const offset = rectangleCount++ * INDICES_PER_RECTANGLE;
|
||||
this._updateRectangle(vertices, offset, currentBg, currentStartX, terminal.cols, y);
|
||||
}
|
||||
}
|
||||
vertices.count = rectangleCount;
|
||||
}
|
||||
|
||||
private _updateRectangle(vertices: IVertices, offset: number, bg: number, startX: number, endX: number, y: number): void {
|
||||
let color: IColor | null = null;
|
||||
if (bg === INVERTED_DEFAULT_COLOR) {
|
||||
color = this._colors.foreground;
|
||||
} else if (is256Color(bg)) {
|
||||
color = this._colors.ansi[bg];
|
||||
} else {
|
||||
// TODO: Add support for true color
|
||||
color = this._colors.foreground;
|
||||
}
|
||||
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 = ((color.rgba >> 24) & 0xFF) / 255;
|
||||
const g = ((color.rgba >> 16) & 0xFF) / 255;
|
||||
const b = ((color.rgba >> 8 ) & 0xFF) / 255;
|
||||
|
||||
this._addRectangle(vertices.attributes, offset, x1, y1, (endX - startX) * this._dimensions.scaledCellWidth, this._dimensions.scaledCellHeight, r, g, b, 1);
|
||||
}
|
||||
|
||||
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 + 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;
|
||||
array[offset + 1] = y1;
|
||||
array[offset + 2] = width;
|
||||
array[offset + 3] = 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
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IRenderModel, ISelectionRenderModel } from './Types';
|
||||
import { fill } from 'common/TypedArrayUtils';
|
||||
|
||||
export const RENDER_MODEL_INDICIES_PER_CELL = 4;
|
||||
|
||||
export const COMBINED_CHAR_BIT_MASK = 0x80000000;
|
||||
|
||||
export class RenderModel implements IRenderModel {
|
||||
public cells: Uint32Array;
|
||||
public lineLengths: Uint32Array;
|
||||
public selection: ISelectionRenderModel;
|
||||
|
||||
constructor() {
|
||||
this.cells = new Uint32Array(0);
|
||||
this.lineLengths = new Uint32Array(0);
|
||||
this.selection = {
|
||||
hasSelection: false,
|
||||
viewportStartRow: 0,
|
||||
viewportEndRow: 0,
|
||||
viewportCappedStartRow: 0,
|
||||
viewportCappedEndRow: 0,
|
||||
startCol: 0,
|
||||
endCol: 0
|
||||
};
|
||||
}
|
||||
|
||||
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 {
|
||||
fill(this.cells, 0, 0);
|
||||
fill(this.lineLengths, 0, 0);
|
||||
this.clearSelection();
|
||||
}
|
||||
|
||||
public clearSelection(): void {
|
||||
this.selection.hasSelection = false;
|
||||
this.selection.viewportStartRow = 0;
|
||||
this.selection.viewportEndRow = 0;
|
||||
this.selection.viewportCappedStartRow = 0;
|
||||
this.selection.viewportCappedEndRow = 0;
|
||||
this.selection.startCol = 0;
|
||||
this.selection.endCol = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* 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));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
export interface IRasterizedGlyphSet {
|
||||
[flags: number]: IRasterizedGlyph;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a rasterized glyph within a texture atlas. Some numbers are
|
||||
* tracked in CSS pixels as well in order to reduce calculations during the
|
||||
* render loop.
|
||||
*/
|
||||
export interface IRasterizedGlyph {
|
||||
/**
|
||||
* The x and y offset between the glyph's top/left and the top/left of a cell
|
||||
* in pixels.
|
||||
*/
|
||||
offset: IVector;
|
||||
/**
|
||||
* the x and y position of the glyph in the texture in pixels.
|
||||
*/
|
||||
texturePosition: IVector;
|
||||
/**
|
||||
* the x and y position of the glyph in the texture in clip space coordinates.
|
||||
*/
|
||||
texturePositionClipSpace: IVector;
|
||||
/**
|
||||
* The width and height of the glyph in the texture in pixels.
|
||||
*/
|
||||
size: IVector;
|
||||
/**
|
||||
* The width and height of the glyph in the texture in clip space coordinates.
|
||||
*/
|
||||
sizeClipSpace: IVector;
|
||||
}
|
||||
|
||||
export interface IVector {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface IBoundingBox {
|
||||
top: number;
|
||||
left: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
}
|
||||
|
||||
export interface IRenderModel {
|
||||
cells: Uint32Array;
|
||||
lineLengths: Uint32Array;
|
||||
selection: ISelectionRenderModel;
|
||||
}
|
||||
|
||||
export interface ISelectionRenderModel {
|
||||
hasSelection: boolean;
|
||||
viewportStartRow: number;
|
||||
viewportEndRow: number;
|
||||
viewportCappedStartRow: number;
|
||||
viewportCappedEndRow: number;
|
||||
startCol: number;
|
||||
endCol: number;
|
||||
}
|
||||
|
||||
export interface IWebGL2RenderingContext extends WebGLRenderingContext {
|
||||
vertexAttribDivisor(index: number, divisor: number): void;
|
||||
createVertexArray(): IWebGLVertexArrayObject;
|
||||
bindVertexArray(vao: IWebGLVertexArrayObject): void;
|
||||
drawElementsInstanced(mode: number, count: number, type: number, offset: number, instanceCount: number): void;
|
||||
}
|
||||
|
||||
export interface IWebGLVertexArrayObject {
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal, ITerminalAddon } from 'xterm';
|
||||
import { WebglRenderer } from './WebglRenderer';
|
||||
import { IRenderService } from 'browser/services/Services';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
|
||||
export class WebglAddon implements ITerminalAddon {
|
||||
constructor(
|
||||
private _preserveDrawingBuffer?: boolean
|
||||
) {}
|
||||
|
||||
public activate(terminal: Terminal): void {
|
||||
if (!terminal.element) {
|
||||
throw new Error('Cannot activate WebglRendererAddon before Terminal.open');
|
||||
}
|
||||
const renderService: IRenderService = (<any>terminal)._core._renderService;
|
||||
const colors: IColorSet = (<any>terminal)._core._colorManager.colors;
|
||||
renderService.setRenderer(new WebglRenderer(terminal, colors, this._preserveDrawingBuffer));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
throw new Error('WebglRendererAddon.dispose Not yet implemented');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import * as puppeteer from 'puppeteer';
|
||||
import { assert } from 'chai';
|
||||
import { ITerminalOptions } from '../../../src/Types';
|
||||
import { ITheme } from 'xterm';
|
||||
|
||||
const APP = 'http://127.0.0.1:3000/test';
|
||||
|
||||
let browser: puppeteer.Browser;
|
||||
let page: puppeteer.Page;
|
||||
const width = 800;
|
||||
const height = 600;
|
||||
|
||||
describe('WebGL Renderer Integration Tests', function(): void {
|
||||
this.timeout(20000);
|
||||
|
||||
before(async function(): Promise<any> {
|
||||
browser = await puppeteer.launch({
|
||||
headless: process.argv.indexOf('--headless') !== -1,
|
||||
slowMo: 80,
|
||||
args: [`--window-size=${width},${height}`]
|
||||
});
|
||||
page = (await browser.pages())[0];
|
||||
await page.setViewport({ width, height });
|
||||
await page.goto(APP);
|
||||
await openTerminal();
|
||||
await page.evaluate(`window.term.loadAddon(new WebglAddon(true));`);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
browser.close();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await page.evaluate(`window.term.reset()`);
|
||||
});
|
||||
|
||||
describe('WebGL Renderer', () => {
|
||||
it('foreground colors normal', async function(): Promise<any> {
|
||||
const theme: ITheme = {
|
||||
black: '#010203',
|
||||
red: '#040506',
|
||||
green: '#070809',
|
||||
yellow: '#0a0b0c',
|
||||
blue: '#0d0e0f',
|
||||
magenta: '#101112',
|
||||
cyan: '#131415',
|
||||
white: '#161718'
|
||||
};
|
||||
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||
await writeSync(`\\x1b[30m█\\x1b[31m█\\x1b[32m█\\x1b[33m█\\x1b[34m█\\x1b[35m█\\x1b[36m█\\x1b[37m█`);
|
||||
assert.deepEqual(await getCellColor(1, 1), [1, 2, 3, 255]);
|
||||
assert.deepEqual(await getCellColor(2, 1), [4, 5, 6, 255]);
|
||||
assert.deepEqual(await getCellColor(3, 1), [7, 8, 9, 255]);
|
||||
assert.deepEqual(await getCellColor(4, 1), [10, 11, 12, 255]);
|
||||
assert.deepEqual(await getCellColor(5, 1), [13, 14, 15, 255]);
|
||||
assert.deepEqual(await getCellColor(6, 1), [16, 17, 18, 255]);
|
||||
assert.deepEqual(await getCellColor(7, 1), [19, 20, 21, 255]);
|
||||
assert.deepEqual(await getCellColor(8, 1), [22, 23, 24, 255]);
|
||||
});
|
||||
|
||||
it('foreground colors bright', async function(): Promise<any> {
|
||||
const theme: ITheme = {
|
||||
brightBlack: '#010203',
|
||||
brightRed: '#040506',
|
||||
brightGreen: '#070809',
|
||||
brightYellow: '#0a0b0c',
|
||||
brightBlue: '#0d0e0f',
|
||||
brightMagenta: '#101112',
|
||||
brightCyan: '#131415',
|
||||
brightWhite: '#161718'
|
||||
};
|
||||
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||
await writeSync(`\\x1b[90m█\\x1b[91m█\\x1b[92m█\\x1b[93m█\\x1b[94m█\\x1b[95m█\\x1b[96m█\\x1b[97m█`);
|
||||
assert.deepEqual(await getCellColor(1, 1), [1, 2, 3, 255]);
|
||||
assert.deepEqual(await getCellColor(2, 1), [4, 5, 6, 255]);
|
||||
assert.deepEqual(await getCellColor(3, 1), [7, 8, 9, 255]);
|
||||
assert.deepEqual(await getCellColor(4, 1), [10, 11, 12, 255]);
|
||||
assert.deepEqual(await getCellColor(5, 1), [13, 14, 15, 255]);
|
||||
assert.deepEqual(await getCellColor(6, 1), [16, 17, 18, 255]);
|
||||
assert.deepEqual(await getCellColor(7, 1), [19, 20, 21, 255]);
|
||||
assert.deepEqual(await getCellColor(8, 1), [22, 23, 24, 255]);
|
||||
});
|
||||
|
||||
it('background colors normal', async function(): Promise<any> {
|
||||
const theme: ITheme = {
|
||||
black: '#010203',
|
||||
red: '#040506',
|
||||
green: '#070809',
|
||||
yellow: '#0a0b0c',
|
||||
blue: '#0d0e0f',
|
||||
magenta: '#101112',
|
||||
cyan: '#131415',
|
||||
white: '#161718'
|
||||
};
|
||||
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||
await writeSync(`\\x1b[40m \\x1b[41m \\x1b[42m \\x1b[43m \\x1b[44m \\x1b[45m \\x1b[46m \\x1b[47m `);
|
||||
assert.deepEqual(await getCellColor(1, 1), [1, 2, 3, 255]);
|
||||
assert.deepEqual(await getCellColor(2, 1), [4, 5, 6, 255]);
|
||||
assert.deepEqual(await getCellColor(3, 1), [7, 8, 9, 255]);
|
||||
assert.deepEqual(await getCellColor(4, 1), [10, 11, 12, 255]);
|
||||
assert.deepEqual(await getCellColor(5, 1), [13, 14, 15, 255]);
|
||||
assert.deepEqual(await getCellColor(6, 1), [16, 17, 18, 255]);
|
||||
assert.deepEqual(await getCellColor(7, 1), [19, 20, 21, 255]);
|
||||
assert.deepEqual(await getCellColor(8, 1), [22, 23, 24, 255]);
|
||||
});
|
||||
|
||||
it('background colors bright', async function(): Promise<any> {
|
||||
const theme: ITheme = {
|
||||
brightBlack: '#010203',
|
||||
brightRed: '#040506',
|
||||
brightGreen: '#070809',
|
||||
brightYellow: '#0a0b0c',
|
||||
brightBlue: '#0d0e0f',
|
||||
brightMagenta: '#101112',
|
||||
brightCyan: '#131415',
|
||||
brightWhite: '#161718'
|
||||
};
|
||||
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||
await writeSync(`\\x1b[100m \\x1b[101m \\x1b[102m \\x1b[103m \\x1b[104m \\x1b[105m \\x1b[106m \\x1b[107m `);
|
||||
assert.deepEqual(await getCellColor(1, 1), [1, 2, 3, 255]);
|
||||
assert.deepEqual(await getCellColor(2, 1), [4, 5, 6, 255]);
|
||||
assert.deepEqual(await getCellColor(3, 1), [7, 8, 9, 255]);
|
||||
assert.deepEqual(await getCellColor(4, 1), [10, 11, 12, 255]);
|
||||
assert.deepEqual(await getCellColor(5, 1), [13, 14, 15, 255]);
|
||||
assert.deepEqual(await getCellColor(6, 1), [16, 17, 18, 255]);
|
||||
assert.deepEqual(await getCellColor(7, 1), [19, 20, 21, 255]);
|
||||
assert.deepEqual(await getCellColor(8, 1), [22, 23, 24, 255]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function openTerminal(options: ITerminalOptions = {}): Promise<void> {
|
||||
await page.evaluate(`window.term = new Terminal(${JSON.stringify(options)})`);
|
||||
await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`);
|
||||
if (options.rendererType === 'dom') {
|
||||
await page.waitForSelector('.xterm-rows');
|
||||
} else {
|
||||
await page.waitForSelector('.xterm-text-layer');
|
||||
}
|
||||
}
|
||||
|
||||
async function writeSync(data: string): Promise<void> {
|
||||
await page.evaluate(`window.term.write('${data}');`);
|
||||
while (true) {
|
||||
if (await page.evaluate(`window.term._core.writeBuffer.length === 0`)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getCellColor(col: number, row: number): Promise<number[]> {
|
||||
await page.evaluate(`
|
||||
window.gl = window.term._core._renderService._renderer._gl;
|
||||
window.result = new Uint8Array(4);
|
||||
window.d = window.term._core._renderService.dimensions;
|
||||
window.gl.readPixels(
|
||||
Math.floor((${col - 0.5}) * window.d.scaledCellWidth),
|
||||
Math.floor(window.gl.drawingBufferHeight - 1 - (${row - 0.5}) * window.d.scaledCellHeight),
|
||||
1, 1, window.gl.RGBA, window.gl.UNSIGNED_BYTE, window.result
|
||||
);
|
||||
`);
|
||||
return await page.evaluate(`Array.from(window.result)`);
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal } from '../../../src/Types';
|
||||
import { GlyphRenderer } from './GlyphRenderer';
|
||||
import { LinkRenderLayer } from './renderLayer/LinkRenderLayer';
|
||||
import { CursorRenderLayer } from './renderLayer/CursorRenderLayer';
|
||||
import { acquireCharAtlas } from './atlas/CharAtlasCache';
|
||||
import { WebglCharAtlas } from './atlas/WebglCharAtlas';
|
||||
import { RectangleRenderer } from './RectangleRenderer';
|
||||
import { IWebGL2RenderingContext } from './Types';
|
||||
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
|
||||
import { RenderModel, COMBINED_CHAR_BIT_MASK } from './RenderModel';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { DEFAULT_COLOR, CHAR_DATA_CHAR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_ATTR_INDEX, NULL_CELL_CODE } from 'common/buffer/Constants';
|
||||
import { Terminal } from 'xterm';
|
||||
import { getLuminance } from './ColorUtils';
|
||||
import { IRenderLayer } from './renderLayer/Types';
|
||||
import { IRenderDimensions, IRenderer } from 'browser/renderer/Types';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { FLAGS } from './Constants';
|
||||
import { getCompatAttr } from './CharDataCompat';
|
||||
|
||||
export const INDICIES_PER_CELL = 4;
|
||||
|
||||
export class WebglRenderer extends Disposable implements IRenderer {
|
||||
private _renderLayers: IRenderLayer[];
|
||||
private _charAtlas: WebglCharAtlas;
|
||||
private _devicePixelRatio: number;
|
||||
|
||||
private _model: RenderModel = new RenderModel();
|
||||
|
||||
private _canvas: HTMLCanvasElement;
|
||||
private _gl: IWebGL2RenderingContext;
|
||||
private _rectangleRenderer: RectangleRenderer;
|
||||
private _glyphRenderer: GlyphRenderer;
|
||||
|
||||
public dimensions: IRenderDimensions;
|
||||
|
||||
private _core: ITerminal;
|
||||
|
||||
constructor(
|
||||
private _terminal: Terminal,
|
||||
private _colors: IColorSet,
|
||||
preserveDrawingBuffer?: boolean
|
||||
) {
|
||||
super();
|
||||
|
||||
this._core = (<any>this._terminal)._core;
|
||||
|
||||
this._applyBgLuminanceBasedSelection();
|
||||
|
||||
this._renderLayers = [
|
||||
new LinkRenderLayer(this._core.screenElement, 2, this._colors, this._core),
|
||||
new CursorRenderLayer(this._core.screenElement, 3, this._colors)
|
||||
];
|
||||
this.dimensions = {
|
||||
scaledCharWidth: null,
|
||||
scaledCharHeight: null,
|
||||
scaledCellWidth: null,
|
||||
scaledCellHeight: null,
|
||||
scaledCharLeft: null,
|
||||
scaledCharTop: null,
|
||||
scaledCanvasWidth: null,
|
||||
scaledCanvasHeight: null,
|
||||
canvasWidth: null,
|
||||
canvasHeight: null,
|
||||
actualCellWidth: null,
|
||||
actualCellHeight: null
|
||||
};
|
||||
this._devicePixelRatio = window.devicePixelRatio;
|
||||
this._updateDimensions();
|
||||
|
||||
this._canvas = document.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._core.screenElement.appendChild(this._canvas);
|
||||
|
||||
this._rectangleRenderer = new RectangleRenderer(this._terminal, this._colors, this._gl, this.dimensions);
|
||||
this._glyphRenderer = new GlyphRenderer(this._terminal, this._colors, this._gl, this.dimensions);
|
||||
|
||||
// Update dimensions and acquire char atlas
|
||||
this.onCharSizeChanged();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._renderLayers.forEach(l => l.dispose());
|
||||
this._core.screenElement.removeChild(this._canvas);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private _applyBgLuminanceBasedSelection(): void {
|
||||
// HACK: This is needed until webgl renderer adds support for selection colors
|
||||
if (getLuminance(this._colors.background) > 0.5) {
|
||||
this._colors.selection = { css: '#000', rgba: 255 };
|
||||
} else {
|
||||
this._colors.selection = { css: '#fff', rgba: 4294967295 };
|
||||
}
|
||||
}
|
||||
|
||||
public setColors(colors: IColorSet): void {
|
||||
this._colors = colors;
|
||||
|
||||
this._applyBgLuminanceBasedSelection();
|
||||
|
||||
// Clear layers and force a full render
|
||||
this._renderLayers.forEach(l => {
|
||||
l.setColors(this._terminal, this._colors);
|
||||
l.reset(this._terminal);
|
||||
});
|
||||
|
||||
this._rectangleRenderer.setColors();
|
||||
this._glyphRenderer.setColors();
|
||||
|
||||
this._refreshCharAtlas();
|
||||
|
||||
// Force a full refresh
|
||||
this._model.clear();
|
||||
}
|
||||
|
||||
public onDevicePixelRatioChange(): void {
|
||||
// If the device pixel ratio changed, the char atlas needs to be regenerated
|
||||
// and the terminal needs to refreshed
|
||||
if (this._devicePixelRatio !== window.devicePixelRatio) {
|
||||
this._devicePixelRatio = window.devicePixelRatio;
|
||||
this.onResize(this._terminal.cols, this._terminal.rows);
|
||||
}
|
||||
}
|
||||
|
||||
public onResize(cols: number, rows: number): void {
|
||||
// Update character and canvas dimensions
|
||||
this._updateDimensions();
|
||||
|
||||
this._model.resize(this._terminal.cols, this._terminal.rows);
|
||||
this._rectangleRenderer.onResize();
|
||||
|
||||
// Resize all render layers
|
||||
this._renderLayers.forEach(l => l.resize(this._terminal, this.dimensions));
|
||||
|
||||
// Resize the canvas
|
||||
this._canvas.width = this.dimensions.scaledCanvasWidth;
|
||||
this._canvas.height = this.dimensions.scaledCanvasHeight;
|
||||
this._canvas.style.width = `${this.dimensions.canvasWidth}px`;
|
||||
this._canvas.style.height = `${this.dimensions.canvasHeight}px`;
|
||||
|
||||
// Resize the screen
|
||||
this._core.screenElement.style.width = `${this.dimensions.canvasWidth}px`;
|
||||
this._core.screenElement.style.height = `${this.dimensions.canvasHeight}px`;
|
||||
this._glyphRenderer.setDimensions(this.dimensions);
|
||||
this._glyphRenderer.onResize();
|
||||
|
||||
this._refreshCharAtlas();
|
||||
|
||||
// Force a full refresh
|
||||
this._model.clear();
|
||||
}
|
||||
|
||||
public onCharSizeChanged(): void {
|
||||
this.onResize(this._terminal.cols, this._terminal.rows);
|
||||
}
|
||||
|
||||
public onBlur(): void {
|
||||
this._renderLayers.forEach(l => l.onBlur(this._terminal));
|
||||
}
|
||||
|
||||
public onFocus(): void {
|
||||
this._renderLayers.forEach(l => l.onFocus(this._terminal));
|
||||
}
|
||||
|
||||
public onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean): void {
|
||||
this._renderLayers.forEach(l => l.onSelectionChanged(this._terminal, start, end, columnSelectMode));
|
||||
|
||||
this._updateSelectionModel(start, end);
|
||||
|
||||
this._rectangleRenderer.updateSelection(this._model.selection, columnSelectMode);
|
||||
this._glyphRenderer.updateSelection(this._model, columnSelectMode);
|
||||
|
||||
// TODO: #2102 Should this move to RenderCoordinator?
|
||||
this._core.refresh(0, this._terminal.rows - 1);
|
||||
}
|
||||
|
||||
public onCursorMove(): void {
|
||||
this._renderLayers.forEach(l => l.onCursorMove(this._terminal));
|
||||
}
|
||||
|
||||
public onOptionsChanged(): void {
|
||||
this._renderLayers.forEach(l => l.onOptionsChanged(this._terminal));
|
||||
this._updateDimensions();
|
||||
this._refreshCharAtlas();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(): void {
|
||||
if (this.dimensions.scaledCharWidth <= 0 && this.dimensions.scaledCharHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const atlas = acquireCharAtlas(this._terminal, this._colors, this.dimensions.scaledCharWidth, this.dimensions.scaledCharHeight);
|
||||
if (!('getRasterizedGlyph' in atlas)) {
|
||||
throw new Error('The webgl renderer only works with the webgl char atlas');
|
||||
}
|
||||
this._charAtlas = atlas as WebglCharAtlas;
|
||||
this._charAtlas.warmUp();
|
||||
this._glyphRenderer.setAtlas(this._charAtlas);
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._renderLayers.forEach(l => l.reset(this._terminal));
|
||||
}
|
||||
|
||||
public registerCharacterJoiner(handler: (text: string) => [number, number][]): number {
|
||||
return -1;
|
||||
}
|
||||
|
||||
public deregisterCharacterJoiner(joinerId: number): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public renderRows(start: number, end: number): void {
|
||||
// Update render layers
|
||||
this._renderLayers.forEach(l => l.onGridChanged(this._terminal, start, end));
|
||||
|
||||
// Tell renderer the frame is beginning
|
||||
if (this._glyphRenderer.beginFrame()) {
|
||||
this._model.clear();
|
||||
}
|
||||
|
||||
// Update model to reflect what's drawn
|
||||
this._updateModel(start, end);
|
||||
|
||||
// Render
|
||||
this._rectangleRenderer.render();
|
||||
this._glyphRenderer.render(this._model, this._model.selection.hasSelection);
|
||||
}
|
||||
|
||||
private _updateModel(start: number, end: number): void {
|
||||
const terminal = this._core;
|
||||
|
||||
for (let y = start; y <= end; y++) {
|
||||
const row = y + terminal.buffer.ydisp;
|
||||
const line = terminal.buffer.lines.get(row);
|
||||
this._model.lineLengths[y] = 0;
|
||||
for (let x = 0; x < terminal.cols; x++) {
|
||||
const charData = line.get(x);
|
||||
const chars = charData[CHAR_DATA_CHAR_INDEX];
|
||||
let code = charData[CHAR_DATA_CODE_INDEX];
|
||||
const attr = getCompatAttr(line, x); // charData[CHAR_DATA_ATTR_INDEX];
|
||||
const i = ((y * terminal.cols) + x) * INDICIES_PER_CELL;
|
||||
|
||||
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 + 1] === attr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Resolve bg and fg and cache in the model
|
||||
const flags = attr >> 18;
|
||||
let bg = attr & 0x1ff;
|
||||
let fg = (attr >> 9) & 0x1ff;
|
||||
|
||||
// If inverse flag is on, the foreground should become the background.
|
||||
if (flags & FLAGS.INVERSE) {
|
||||
const temp = bg;
|
||||
bg = fg;
|
||||
fg = temp;
|
||||
if (fg === DEFAULT_COLOR) {
|
||||
fg = INVERTED_DEFAULT_COLOR;
|
||||
}
|
||||
if (bg === DEFAULT_COLOR) {
|
||||
bg = INVERTED_DEFAULT_COLOR;
|
||||
}
|
||||
}
|
||||
const drawInBrightColor = terminal.options.drawBoldTextInBrightColors && !!(flags & FLAGS.BOLD) && fg < 8 && fg !== INVERTED_DEFAULT_COLOR;
|
||||
fg += drawInBrightColor ? 8 : 0;
|
||||
|
||||
// Flag combined chars with a bit mask so they're easily identifiable
|
||||
if (chars.length > 1) {
|
||||
code = code | COMBINED_CHAR_BIT_MASK;
|
||||
}
|
||||
|
||||
this._model.cells[i ] = code;
|
||||
this._model.cells[i + 1] = attr;
|
||||
this._model.cells[i + 2] = bg;
|
||||
this._model.cells[i + 3] = fg;
|
||||
|
||||
this._glyphRenderer.updateCell(x, y, code, attr, bg, fg, chars);
|
||||
}
|
||||
}
|
||||
this._rectangleRenderer.updateBackgrounds(this._model);
|
||||
}
|
||||
|
||||
private _updateSelectionModel(start: [number, number], end: [number, number]): void {
|
||||
const terminal = this._terminal;
|
||||
|
||||
// Selection does not exist
|
||||
if (!start || !end || (start[0] === end[0] && start[1] === end[1])) {
|
||||
this._model.clearSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
// Translate from buffer position to viewport position
|
||||
const viewportStartRow = start[1] - terminal.buffer.viewportY;
|
||||
const viewportEndRow = end[1] - terminal.buffer.viewportY;
|
||||
const viewportCappedStartRow = Math.max(viewportStartRow, 0);
|
||||
const viewportCappedEndRow = Math.min(viewportEndRow, terminal.rows - 1);
|
||||
|
||||
// No need to draw the selection
|
||||
if (viewportCappedStartRow >= terminal.rows || viewportCappedEndRow < 0) {
|
||||
this._model.clearSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
this._model.selection.hasSelection = true;
|
||||
this._model.selection.viewportStartRow = viewportStartRow;
|
||||
this._model.selection.viewportEndRow = viewportEndRow;
|
||||
this._model.selection.viewportCappedStartRow = viewportCappedStartRow;
|
||||
this._model.selection.viewportCappedEndRow = viewportCappedEndRow;
|
||||
this._model.selection.startCol = start[0];
|
||||
this._model.selection.endCol = end[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculates the character and canvas dimensions.
|
||||
*/
|
||||
private _updateDimensions(): void {
|
||||
// TODO: Acquire CharSizeService properly
|
||||
|
||||
// Perform a new measure if the CharMeasure dimensions are not yet available
|
||||
if (!(<any>this._core)._charSizeService.width || !(<any>this._core)._charSizeService.height) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate the scaled character width. Width is floored as it must be
|
||||
// drawn to an integer grid in order for the CharAtlas "stamps" to not be
|
||||
// blurry. When text is drawn to the grid not using the CharAtlas, it is
|
||||
// clipped to ensure there is no overlap with the next cell.
|
||||
|
||||
// NOTE: ceil fixes sometime, floor does others :s
|
||||
|
||||
this.dimensions.scaledCharWidth = Math.floor((<any>this._core)._charSizeService.width * this._devicePixelRatio);
|
||||
|
||||
// Calculate the scaled 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.scaledCharHeight = Math.ceil((<any>this._core)._charSizeService.height * this._devicePixelRatio);
|
||||
|
||||
// Calculate the scaled cell height, if lineHeight is not 1 then the value
|
||||
// will be floored because since lineHeight can never be lower then 1, there
|
||||
// is a guarentee that the scaled line height will always be larger than
|
||||
// scaled char height.
|
||||
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.getOption('lineHeight'));
|
||||
|
||||
// Calculate the y coordinate within a cell that text should draw from in
|
||||
// order to draw in the center of a cell.
|
||||
this.dimensions.scaledCharTop = this._terminal.getOption('lineHeight') === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2);
|
||||
|
||||
// Calculate the scaled cell width, taking the letterSpacing into account.
|
||||
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.getOption('letterSpacing'));
|
||||
|
||||
// Calculate the x coordinate with a cell that text should draw from in
|
||||
// order to draw in the center of a cell.
|
||||
this.dimensions.scaledCharLeft = Math.floor(this._terminal.getOption('letterSpacing') / 2);
|
||||
|
||||
// Recalculate the canvas dimensions; scaled* define the actual number of
|
||||
// pixel in the canvas
|
||||
this.dimensions.scaledCanvasHeight = this._terminal.rows * this.dimensions.scaledCellHeight;
|
||||
this.dimensions.scaledCanvasWidth = this._terminal.cols * this.dimensions.scaledCellWidth;
|
||||
|
||||
// The the size of the canvas on the page. It's very important that this
|
||||
// rounds to nearest integer and not ceils as browsers often set
|
||||
// window.devicePixelRatio as something like 1.100000023841858, when it's
|
||||
// actually 1.1. Ceiling causes blurriness as the backing canvas image is 1
|
||||
// pixel too large for the canvas element size.
|
||||
this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / this._devicePixelRatio);
|
||||
this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / this._devicePixelRatio);
|
||||
|
||||
// this.dimensions.scaledCanvasHeight = this.dimensions.canvasHeight * devicePixelRatio;
|
||||
// this.dimensions.scaledCanvasWidth = this.dimensions.canvasWidth * devicePixelRatio;
|
||||
|
||||
// Get the _actual_ dimensions of an individual cell. This needs to be
|
||||
// derived from the canvasWidth/Height calculated above which takes into
|
||||
// account window.devicePixelRatio. 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.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows;
|
||||
// this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols;
|
||||
|
||||
// This fixes 110% and 125%, not 150% or 175% though
|
||||
this.dimensions.actualCellHeight = this.dimensions.scaledCellHeight / this._devicePixelRatio;
|
||||
this.dimensions.actualCellWidth = this.dimensions.scaledCellWidth / this._devicePixelRatio;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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 = gl.createProgram();
|
||||
gl.attachShader(program, createShader(gl, gl.VERTEX_SHADER, vertexSource));
|
||||
gl.attachShader(program, createShader(gl, gl.FRAGMENT_SHADER, fragmentSource));
|
||||
gl.linkProgram(program);
|
||||
const success = gl.getProgramParameter(program, gl.LINK_STATUS);
|
||||
if (success) {
|
||||
return program;
|
||||
}
|
||||
|
||||
console.log(gl.getProgramInfoLog(program));
|
||||
gl.deleteProgram(program);
|
||||
}
|
||||
|
||||
export function createShader(gl: WebGLRenderingContext, type: number, source: string): WebGLShader | undefined {
|
||||
const shader = gl.createShader(type);
|
||||
gl.shaderSource(shader, source);
|
||||
gl.compileShader(shader);
|
||||
const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
|
||||
if (success) {
|
||||
return shader;
|
||||
}
|
||||
|
||||
console.log(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;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IGlyphIdentifier } from './Types';
|
||||
import { IDisposable } from 'xterm';
|
||||
|
||||
export abstract class BaseCharAtlas implements IDisposable {
|
||||
private _didWarmUp: boolean = false;
|
||||
|
||||
public dispose(): void { }
|
||||
|
||||
/**
|
||||
* Perform any work needed to warm the cache before it can be used. May be called multiple times.
|
||||
* Implement _doWarmUp instead if you only want to get called once.
|
||||
*/
|
||||
public warmUp(): void {
|
||||
if (!this._didWarmUp) {
|
||||
this._doWarmUp();
|
||||
this._didWarmUp = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform any work needed to warm the cache before it can be used. Used by the default
|
||||
* implementation of warmUp(), and will only be called once.
|
||||
*/
|
||||
protected _doWarmUp(): void { }
|
||||
|
||||
/**
|
||||
* Called when we start drawing a new frame.
|
||||
*
|
||||
* TODO: We rely on this getting called by TextRenderLayer. This should really be called by
|
||||
* Renderer instead, but we need to make Renderer the source-of-truth for the char atlas, instead
|
||||
* of BaseRenderLayer.
|
||||
*/
|
||||
public beginFrame(): void { }
|
||||
|
||||
/**
|
||||
* May be called before warmUp finishes, however it is okay for the implementation to
|
||||
* do nothing and return false in that case.
|
||||
*
|
||||
* @param ctx Where to draw the character onto.
|
||||
* @param glyph Information about what to draw
|
||||
* @param x The position on the context to start drawing at
|
||||
* @param y The position on the context to start drawing at
|
||||
* @returns The success state. True if we drew the character.
|
||||
*/
|
||||
public abstract draw(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
glyph: IGlyphIdentifier,
|
||||
x: number,
|
||||
y: number
|
||||
): boolean;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user