Merge branch 'master' into buffer_optimizations

This commit is contained in:
Jörg Breitbart
2022-12-03 11:24:51 +01:00
85 changed files with 1502 additions and 1015 deletions
+5 -1
View File
@@ -39,7 +39,8 @@
"**/*.js"
],
"plugins": [
"@typescript-eslint"
"@typescript-eslint",
"jsdoc"
],
"rules": {
"no-extra-semi": "error",
@@ -141,6 +142,9 @@
"warn",
"always"
],
"jsdoc/check-alignment": 1,
"jsdoc/check-param-names": 1,
"jsdoc/no-multi-asterisks": 1,
"keyword-spacing": "warn",
"new-parens": "warn",
"no-duplicate-imports": "warn",
+2 -1
View File
@@ -186,7 +186,7 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**DockerStacks**](https://docker-stacks.com/): Local LAMP/LEMP development studio
- [**Codecademy**](https://codecademy.com/): Uses xterm.js in its courses on Bash.
- [**Laravel Ssh Web Client**](https://github.com/roke22/Laravel-ssh-client): Laravel server inventory with ssh web client to connect at server using xterm.js
- [**Repl.it**](https://repl.it): Collaborative browser based IDE with support for 50+ different languages.
- [**Replit**](https://replit.com): Collaborative browser based IDE with support for 50+ different languages.
- [**TeleType**](https://github.com/akshaykmr/TeleType): cli tool that allows you to share your terminal online conveniently. Show off mad cli-fu, help a colleague, teach, or troubleshoot.
- [**Intervue**](https://www.intervue.io): Pair programming for interviews. Multiple programming languages are supported, with results displayed by xterm.js.
- [**TRASA**](https://trasa.io): Zero trust access to Web, SSH, RDP, and Database services.
@@ -213,6 +213,7 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**Go SSH Web Client**](https://github.com/wuchihsu/go-ssh-web-client): A simple SSH web client using Go, WebSocket and Xterm.js.
- [**web3os**](https://web3os.sh): A decentralized operating system for the next web
- [**Cratecode**](https://cratecode.com): Learn to program for free through interactive online lessons. Cratecode uses xterm.js to give users access to their own Linux environment.
- [**Super Terminal**](https://github.com/bugwheels94/super-terminal): It is a http based terminal for developers who dont like repetition and save time.
- [And much more...](https://github.com/xtermjs/xterm.js/network/dependents?package_id=UGFja2FnZS0xNjYzMjc4OQ%3D%3D)
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it on our list. Note: Please add any new contributions to the end of the list only.
+18 -4
View File
@@ -47,16 +47,14 @@ export class AttachAddon implements ITerminalAddon {
}
private _sendData(data: string): void {
// TODO: do something better than just swallowing
// the data if the socket is not in a working condition
if (this._socket.readyState !== 1) {
if (!this._checkOpenSocket()) {
return;
}
this._socket.send(data);
}
private _sendBinary(data: string): void {
if (this._socket.readyState !== 1) {
if (!this._checkOpenSocket()) {
return;
}
const buffer = new Uint8Array(data.length);
@@ -65,6 +63,22 @@ export class AttachAddon implements ITerminalAddon {
}
this._socket.send(buffer);
}
private _checkOpenSocket(): boolean {
switch (this._socket.readyState) {
case WebSocket.OPEN:
return true;
case WebSocket.CONNECTING:
throw new Error('Attach addon was loaded before socket was open');
case WebSocket.CLOSING:
console.warn('Attach addon socket is closing');
return false;
case WebSocket.CLOSED:
throw new Error('Attach addon socket is closed');
default:
throw new Error('Unexpected socket state');
}
}
}
function addSocketListener<K extends keyof WebSocketEventMap>(socket: WebSocket, type: K, handler: (this: WebSocket, ev: WebSocketEventMap[K]) => any): IDisposable {
@@ -11,34 +11,40 @@ import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
import { IRasterizedGlyph, IRenderDimensions, ISelectionRenderModel, ITextureAtlas } from 'browser/renderer/shared/Types';
import { createSelectionRenderModel } from 'browser/renderer/shared/SelectionRenderModel';
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
import { ReadonlyColorSet } from 'browser/Types';
import { CellData } from 'common/buffer/CellData';
import { WHITESPACE_CELL_CODE } from 'common/buffer/Constants';
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
import { ICellData } from 'common/Types';
import { ICellData, IDisposable } from 'common/Types';
import { Terminal } from 'xterm';
import { IRenderLayer } from './Types';
import { CellColorResolver } from 'browser/renderer/shared/CellColorResolver';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { isSafari } from 'common/Platform';
import { EventEmitter, forwardEvent } from 'common/EventEmitter';
export abstract class BaseRenderLayer extends Disposable implements IRenderLayer {
private _canvas: HTMLCanvasElement;
protected _ctx!: CanvasRenderingContext2D;
private _scaledCharWidth: number = 0;
private _scaledCharHeight: number = 0;
private _scaledCellWidth: number = 0;
private _scaledCellHeight: number = 0;
private _scaledCharLeft: number = 0;
private _scaledCharTop: number = 0;
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 _selectionModel: ISelectionRenderModel = createSelectionRenderModel();
private _cellColorResolver: CellColorResolver;
private _bitmapGenerator?: BitmapGenerator;
private _bitmapGenerator: (BitmapGenerator | undefined)[] = [];
protected _charAtlas!: ITextureAtlas;
private _charAtlasDisposable?: IDisposable;
public get canvas(): HTMLCanvasElement { return this._canvas; }
public get cacheCanvas(): HTMLCanvasElement { return this._charAtlas?.cacheCanvas!; }
public get cacheCanvas(): HTMLCanvasElement { return this._charAtlas?.pages[0].canvas!; }
private readonly _onAddTextureAtlasCanvas = this.register(new EventEmitter<HTMLCanvasElement>());
public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event;
constructor(
private readonly _terminal: Terminal,
@@ -112,25 +118,29 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
* @param colorSet The color set to use for the char atlas.
*/
private _refreshCharAtlas(colorSet: ReadonlyColorSet): void {
if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) {
if (this._deviceCharWidth <= 0 && this._deviceCharHeight <= 0) {
return;
}
this._charAtlas = acquireTextureAtlas(this._terminal, colorSet, this._scaledCellWidth, this._scaledCellHeight, this._scaledCharWidth, this._scaledCharHeight, this._coreBrowserService.dpr);
this._charAtlasDisposable?.dispose();
this._charAtlas = acquireTextureAtlas(this._terminal, colorSet, this._deviceCellWidth, this._deviceCellHeight, this._deviceCharWidth, this._deviceCharHeight, this._coreBrowserService.dpr);
this._charAtlasDisposable = forwardEvent(this._charAtlas.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas);
this._charAtlas.warmUp();
this._bitmapGenerator = new BitmapGenerator(this._charAtlas.cacheCanvas);
for (let i = 0; i < this._charAtlas.pages.length; i++) {
this._bitmapGenerator[i] = new BitmapGenerator(this._charAtlas.pages[i].canvas);
}
}
public resize(dim: IRenderDimensions): void {
this._scaledCellWidth = dim.scaledCellWidth;
this._scaledCellHeight = dim.scaledCellHeight;
this._scaledCharWidth = dim.scaledCharWidth;
this._scaledCharHeight = dim.scaledCharHeight;
this._scaledCharLeft = dim.scaledCharLeft;
this._scaledCharTop = dim.scaledCharTop;
this._canvas.width = dim.scaledCanvasWidth;
this._canvas.height = dim.scaledCanvasHeight;
this._canvas.style.width = `${dim.canvasWidth}px`;
this._canvas.style.height = `${dim.canvasHeight}px`;
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) {
@@ -155,24 +165,24 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
*/
protected _fillCells(x: number, y: number, width: number, height: number): void {
this._ctx.fillRect(
x * this._scaledCellWidth,
y * this._scaledCellHeight,
width * this._scaledCellWidth,
height * this._scaledCellHeight);
x * this._deviceCellWidth,
y * this._deviceCellHeight,
width * this._deviceCellWidth,
height * this._deviceCellHeight);
}
/**
* Fills a 1px line (2px on HDPI) at the middle of the cell. This uses the
* existing fillStyle on the context.
* @param x The column to fill.
* @param y The row to fill.
*/
* Fills a 1px line (2px on HDPI) at the middle of the cell. This uses the
* existing fillStyle on the context.
* @param x The column to fill.
* @param y The row to fill.
*/
protected _fillMiddleLineAtCells(x: number, y: number, width: number = 1): void {
const cellOffset = Math.ceil(this._scaledCellHeight * 0.5);
const cellOffset = Math.ceil(this._deviceCellHeight * 0.5);
this._ctx.fillRect(
x * this._scaledCellWidth,
(y + 1) * this._scaledCellHeight - cellOffset - this._coreBrowserService.dpr,
width * this._scaledCellWidth,
x * this._deviceCellWidth,
(y + 1) * this._deviceCellHeight - cellOffset - this._coreBrowserService.dpr,
width * this._deviceCellWidth,
this._coreBrowserService.dpr);
}
@@ -184,9 +194,9 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
*/
protected _fillBottomLineAtCells(x: number, y: number, width: number = 1, pixelOffset: number = 0): void {
this._ctx.fillRect(
x * this._scaledCellWidth,
(y + 1) * this._scaledCellHeight + pixelOffset - this._coreBrowserService.dpr - 1 /* Ensure it's drawn within the cell */,
width * this._scaledCellWidth,
x * this._deviceCellWidth,
(y + 1) * this._deviceCellHeight + pixelOffset - this._coreBrowserService.dpr - 1 /* Ensure it's drawn within the cell */,
width * this._deviceCellWidth,
this._coreBrowserService.dpr);
}
@@ -197,10 +207,10 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
const lineWidth = this._coreBrowserService.dpr;
this._ctx.lineWidth = lineWidth;
for (let xOffset = 0; xOffset < width; xOffset++) {
const xLeft = (x + xOffset) * this._scaledCellWidth;
const xMid = (x + xOffset + 0.5) * this._scaledCellWidth;
const xRight = (x + xOffset + 1) * this._scaledCellWidth;
const yMid = (y + 1) * this._scaledCellHeight - lineWidth - 1;
const xLeft = (x + xOffset) * this._deviceCellWidth;
const xMid = (x + xOffset + 0.5) * this._deviceCellWidth;
const xRight = (x + xOffset + 1) * this._deviceCellWidth;
const yMid = (y + 1) * this._deviceCellHeight - lineWidth - 1;
const yMidBot = yMid - lineWidth;
const yMidTop = yMid + lineWidth;
this._ctx.moveTo(xLeft, yMid);
@@ -226,12 +236,12 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
const lineWidth = this._coreBrowserService.dpr;
this._ctx.lineWidth = lineWidth;
this._ctx.setLineDash([lineWidth * 2, lineWidth]);
const xLeft = x * this._scaledCellWidth;
const yMid = (y + 1) * this._scaledCellHeight - lineWidth - 1;
const xLeft = x * this._deviceCellWidth;
const yMid = (y + 1) * this._deviceCellHeight - lineWidth - 1;
this._ctx.moveTo(xLeft, yMid);
for (let xOffset = 0; xOffset < width; xOffset++) {
// const xLeft = x * this._scaledCellWidth;
const xRight = (x + width + xOffset) * this._scaledCellWidth;
// const xLeft = x * this._deviceCellWidth;
const xRight = (x + width + xOffset) * this._deviceCellWidth;
this._ctx.lineTo(xRight, yMid);
}
this._ctx.stroke();
@@ -246,9 +256,9 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
const lineWidth = this._coreBrowserService.dpr;
this._ctx.lineWidth = lineWidth;
this._ctx.setLineDash([lineWidth * 4, lineWidth * 3]);
const xLeft = x * this._scaledCellWidth;
const xRight = (x + width) * this._scaledCellWidth;
const yMid = (y + 1) * this._scaledCellHeight - lineWidth - 1;
const xLeft = x * this._deviceCellWidth;
const xRight = (x + width) * this._deviceCellWidth;
const yMid = (y + 1) * this._deviceCellHeight - lineWidth - 1;
this._ctx.moveTo(xLeft, yMid);
this._ctx.lineTo(xRight, yMid);
this._ctx.stroke();
@@ -264,10 +274,10 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
*/
protected _fillLeftLineAtCell(x: number, y: number, width: number): void {
this._ctx.fillRect(
x * this._scaledCellWidth,
y * this._scaledCellHeight,
x * this._deviceCellWidth,
y * this._deviceCellHeight,
this._coreBrowserService.dpr * width,
this._scaledCellHeight);
this._deviceCellHeight);
}
/**
@@ -280,10 +290,10 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
const lineWidth = this._coreBrowserService.dpr;
this._ctx.lineWidth = lineWidth;
this._ctx.strokeRect(
x * this._scaledCellWidth + lineWidth / 2,
y * this._scaledCellHeight + (lineWidth / 2),
width * this._scaledCellWidth - lineWidth,
(height * this._scaledCellHeight) - lineWidth);
x * this._deviceCellWidth + lineWidth / 2,
y * this._deviceCellHeight + (lineWidth / 2),
width * this._deviceCellWidth - lineWidth,
(height * this._deviceCellHeight) - lineWidth);
}
/**
@@ -308,17 +318,17 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
protected _clearCells(x: number, y: number, width: number, height: number): void {
if (this._alpha) {
this._ctx.clearRect(
x * this._scaledCellWidth,
y * this._scaledCellHeight,
width * this._scaledCellWidth,
height * this._scaledCellHeight);
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._scaledCellWidth,
y * this._scaledCellHeight,
width * this._scaledCellWidth,
height * this._scaledCellHeight);
x * this._deviceCellWidth,
y * this._deviceCellHeight,
width * this._deviceCellWidth,
height * this._deviceCellHeight);
}
}
@@ -329,7 +339,6 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
* @param cell The cell data for the character to draw.
* @param x The column to draw at.
* @param y The row to draw at.
* @param color The color of the character.
*/
protected _fillCharTrueColor(cell: CellData, x: number, y: number): void {
this._ctx.font = this._getFont(false, false);
@@ -339,15 +348,15 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
// Draw custom characters if applicable
let drawSuccess = false;
if (this._optionsService.rawOptions.customGlyphs !== false) {
drawSuccess = tryDrawCustomChar(this._ctx, cell.getChars(), x * this._scaledCellWidth, y * this._scaledCellHeight, this._scaledCellWidth, this._scaledCellHeight, this._optionsService.rawOptions.fontSize, this._coreBrowserService.dpr);
drawSuccess = tryDrawCustomChar(this._ctx, cell.getChars(), x * this._deviceCellWidth, y * this._deviceCellHeight, this._deviceCellWidth, this._deviceCellHeight, this._optionsService.rawOptions.fontSize, this._coreBrowserService.dpr);
}
// Draw the character
if (!drawSuccess) {
this._ctx.fillText(
cell.getChars(),
x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight);
x * this._deviceCellWidth + this._deviceCharLeft,
y * this._deviceCellHeight + this._deviceCharTop + this._deviceCharHeight);
}
}
@@ -357,7 +366,7 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
*/
protected _drawChars(cell: ICellData, x: number, y: number): void {
const chars = cell.getChars();
this._cellColorResolver.resolve(cell, x, y);
this._cellColorResolver.resolve(cell, x, this._bufferService.buffer.ydisp + y);
let glyph: IRasterizedGlyph;
if (chars && chars.length > 1) {
glyph = this._charAtlas.getRasterizedGlyphCombinedChar(chars, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext);
@@ -367,18 +376,21 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
this._ctx.save();
this._clipRow(y);
// Draw the image, use the bitmap if it's available
if (this._charAtlas.hasCanvasChanged) {
this._bitmapGenerator?.refresh();
this._charAtlas.hasCanvasChanged = false;
if (this._charAtlas.pages[glyph.texturePage].version !== this._bitmapGenerator[glyph.texturePage]?.version) {
if (!this._bitmapGenerator[glyph.texturePage]) {
this._bitmapGenerator[glyph.texturePage] = new BitmapGenerator(this._charAtlas.pages[glyph.texturePage].canvas);
}
this._bitmapGenerator[glyph.texturePage]!.refresh();
this._bitmapGenerator[glyph.texturePage]!.version = this._charAtlas.pages[glyph.texturePage].version;
}
this._ctx.drawImage(
this._bitmapGenerator?.bitmap || this._charAtlas!.cacheCanvas,
this._bitmapGenerator[glyph.texturePage]?.bitmap || this._charAtlas!.pages[glyph.texturePage].canvas,
glyph.texturePosition.x,
glyph.texturePosition.y,
glyph.size.x,
glyph.size.y,
x * this._scaledCellWidth - glyph.offset.x,
y * this._scaledCellHeight - glyph.offset.y,
x * this._deviceCellWidth - glyph.offset.x,
y * this._deviceCellHeight - glyph.offset.y,
glyph.size.x,
glyph.size.y
);
@@ -393,9 +405,9 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
this._ctx.beginPath();
this._ctx.rect(
0,
y * this._scaledCellHeight,
this._bufferService.cols * this._scaledCellWidth,
this._scaledCellHeight);
y * this._deviceCellHeight,
this._bufferService.cols * this._deviceCellWidth,
this._deviceCellHeight);
this._ctx.clip();
}
@@ -428,6 +440,7 @@ class BitmapGenerator {
private _commitTimeout: number | undefined = undefined;
private _bitmap: ImageBitmap | undefined = undefined;
public get bitmap(): ImageBitmap | undefined { return this._bitmap; }
public version: number = -1;
constructor(private readonly _canvas: HTMLCanvasElement) {
}
@@ -435,6 +448,10 @@ class BitmapGenerator {
public refresh(): void {
// Clear the bitmap immediately as it's stale
this._bitmap = undefined;
// Disable ImageBitmaps on Safari because of https://bugs.webkit.org/show_bug.cgi?id=149990
if (isSafari) {
return;
}
if (this._commitTimeout === undefined) {
this._commitTimeout = window.setTimeout(() => this._generate(), GLYPH_BITMAP_COMMIT_DELAY);
}
@@ -17,6 +17,8 @@ export class CanvasAddon extends Disposable implements ITerminalAddon {
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;
public get textureAtlas(): HTMLCanvasElement | undefined {
return this._renderer?.textureAtlas;
@@ -46,6 +48,7 @@ export class CanvasAddon extends Disposable implements ITerminalAddon {
this._renderer = new CanvasRenderer(terminal, screenElement, linkifier, bufferService, charSizeService, optionsService, characterJoinerService, coreService, coreBrowserService, decorationService, themeService);
this.register(forwardEvent(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas));
this.register(forwardEvent(this._renderer.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas));
renderService.setRenderer(this._renderer);
renderService.handleResize(bufferService.cols, bufferService.rows);
+25 -32
View File
@@ -5,10 +5,11 @@
import { removeTerminalFromCache } from 'browser/renderer/shared/CharAtlasCache';
import { observeDevicePixelDimensions } from 'browser/renderer/shared/DevicePixelObserver';
import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { IColorSet, ILinkifier2, ReadonlyColorSet } from 'browser/Types';
import { EventEmitter } from 'common/EventEmitter';
import { ILinkifier2 } from 'browser/Types';
import { EventEmitter, forwardEvent } from 'common/EventEmitter';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { Terminal } from 'xterm';
@@ -28,6 +29,8 @@ export class CanvasRenderer extends Disposable implements IRenderer {
public readonly onRequestRedraw = this._onRequestRedraw.event;
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;
constructor(
private readonly _terminal: Terminal,
@@ -50,20 +53,10 @@ export class CanvasRenderer extends Disposable implements IRenderer {
new LinkRenderLayer(this._terminal, this._screenElement, 2, linkifier2, this._bufferService, this._optionsService, decorationService, this._coreBrowserService, _themeService),
new CursorRenderLayer(this._terminal, this._screenElement, 3, this._onRequestRedraw, this._bufferService, this._optionsService, coreService, this._coreBrowserService, decorationService, _themeService)
];
this.dimensions = {
scaledCharWidth: 0,
scaledCharHeight: 0,
scaledCellWidth: 0,
scaledCellHeight: 0,
scaledCharLeft: 0,
scaledCharTop: 0,
scaledCanvasWidth: 0,
scaledCanvasHeight: 0,
canvasWidth: 0,
canvasHeight: 0,
actualCellWidth: 0,
actualCellHeight: 0
};
for (const layer of this._renderLayers) {
forwardEvent(layer.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas);
}
this.dimensions = createRenderDimensions();
this._devicePixelRatio = this._coreBrowserService.dpr;
this._updateDimensions();
@@ -99,8 +92,8 @@ export class CanvasRenderer extends Disposable implements IRenderer {
}
// Resize the screen
this._screenElement.style.width = `${this.dimensions.canvasWidth}px`;
this._screenElement.style.height = `${this.dimensions.canvasHeight}px`;
this._screenElement.style.width = `${this.dimensions.css.canvas.width}px`;
this._screenElement.style.height = `${this.dimensions.css.canvas.height}px`;
}
public handleCharSizeChanged(): void {
@@ -163,23 +156,23 @@ export class CanvasRenderer extends Disposable implements IRenderer {
// See the WebGL renderer for an explanation of this section.
const dpr = this._coreBrowserService.dpr;
this.dimensions.scaledCharWidth = Math.floor(this._charSizeService.width * dpr);
this.dimensions.scaledCharHeight = Math.ceil(this._charSizeService.height * dpr);
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._optionsService.rawOptions.lineHeight);
this.dimensions.scaledCharTop = this._optionsService.rawOptions.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2);
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._optionsService.rawOptions.letterSpacing);
this.dimensions.scaledCharLeft = Math.floor(this._optionsService.rawOptions.letterSpacing / 2);
this.dimensions.scaledCanvasHeight = this._bufferService.rows * this.dimensions.scaledCellHeight;
this.dimensions.scaledCanvasWidth = this._bufferService.cols * this.dimensions.scaledCellWidth;
this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / dpr);
this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / dpr);
this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._bufferService.rows;
this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._bufferService.cols;
this.dimensions.device.char.width = Math.floor(this._charSizeService.width * dpr);
this.dimensions.device.char.height = Math.ceil(this._charSizeService.height * dpr);
this.dimensions.device.cell.height = Math.floor(this.dimensions.device.char.height * this._optionsService.rawOptions.lineHeight);
this.dimensions.device.char.top = this._optionsService.rawOptions.lineHeight === 1 ? 0 : Math.round((this.dimensions.device.cell.height - this.dimensions.device.char.height) / 2);
this.dimensions.device.cell.width = this.dimensions.device.char.width + Math.round(this._optionsService.rawOptions.letterSpacing);
this.dimensions.device.char.left = Math.floor(this._optionsService.rawOptions.letterSpacing / 2);
this.dimensions.device.canvas.height = this._bufferService.rows * this.dimensions.device.cell.height;
this.dimensions.device.canvas.width = this._bufferService.cols * this.dimensions.device.cell.width;
this.dimensions.css.canvas.height = Math.round(this.dimensions.device.canvas.height / dpr);
this.dimensions.css.canvas.width = Math.round(this.dimensions.device.canvas.width / dpr);
this.dimensions.css.cell.height = this.dimensions.css.canvas.height / this._bufferService.rows;
this.dimensions.css.cell.width = this.dimensions.css.canvas.width / this._bufferService.cols;
}
private _setCanvasDevicePixelDimensions(width: number, height: number): void {
this.dimensions.scaledCanvasHeight = height;
this.dimensions.scaledCanvasWidth = width;
this.dimensions.device.canvas.height = height;
this.dimensions.device.canvas.width = width;
// Resize all render layers
for (const l of this._renderLayers) {
l.resize(this.dimensions);
@@ -53,8 +53,8 @@ export class TextRenderLayer extends BaseRenderLayer {
// Clear the character width cache if the font or width has changed
const terminalFont = this._getFont(false, false);
if (this._characterWidth !== dim.scaledCharWidth || this._characterFont !== terminalFont) {
this._characterWidth = dim.scaledCharWidth;
if (this._characterWidth !== dim.device.char.width || this._characterFont !== terminalFont) {
this._characterWidth = dim.device.char.width;
this._characterFont = terminalFont;
this._characterOverlapCache = {};
}
+3 -18
View File
@@ -4,24 +4,8 @@
*/
import { IDisposable } from 'common/Types';
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
import { IEvent } from 'common/EventEmitter';
// TODO: Use core interfaces
export interface IRenderDimensions {
scaledCharWidth: number;
scaledCharHeight: number;
scaledCellWidth: number;
scaledCellHeight: number;
scaledCharLeft: number;
scaledCharTop: number;
scaledCanvasWidth: number;
scaledCanvasHeight: number;
canvasWidth: number;
canvasHeight: number;
actualCellWidth: number;
actualCellHeight: number;
}
import { IRenderDimensions } from 'browser/renderer/shared/Types';
export interface IRequestRedrawEvent {
start: number;
@@ -58,13 +42,14 @@ export interface IRenderLayer extends IDisposable {
readonly canvas: HTMLCanvasElement;
readonly cacheCanvas: HTMLCanvasElement;
readonly onAddTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
/**
* Called when the terminal loses focus.
*/
handleBlur(): void;
/**
* * Called when the terminal gets focus.
* Called when the terminal gets focus.
*/
handleFocus(): void;
@@ -17,6 +17,11 @@ declare module 'xterm-addon-canvas' {
*/
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();
/**
+5 -3
View File
@@ -4,6 +4,7 @@
*/
import { Terminal, ITerminalAddon } from 'xterm';
import { IRenderDimensions } from 'browser/renderer/shared/Types';
interface ITerminalDimensions {
/**
@@ -58,8 +59,9 @@ export class FitAddon implements ITerminalAddon {
// TODO: Remove reliance on private API
const core = (this._terminal as any)._core;
const dims: IRenderDimensions = core._renderService.dimensions;
if (core._renderService.dimensions.actualCellWidth === 0 || core._renderService.dimensions.actualCellHeight === 0) {
if (dims.css.cell.width === 0 || dims.css.cell.height === 0) {
return undefined;
}
@@ -81,8 +83,8 @@ export class FitAddon implements ITerminalAddon {
const availableHeight = parentElementHeight - elementPaddingVer;
const availableWidth = parentElementWidth - elementPaddingHor - scrollbarWidth;
const geometry = {
cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / core._renderService.dimensions.actualCellWidth)),
rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / core._renderService.dimensions.actualCellHeight))
cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / dims.css.cell.width)),
rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / dims.css.cell.height))
};
return geometry;
}
+11 -1
View File
@@ -13,10 +13,20 @@
"strict": true,
"types": [
"../../../node_modules/@types/mocha"
]
],
"paths": {
"browser/*": [
"../../../src/browser/*"
]
}
},
"include": [
"./**/*",
"../../../typings/xterm.d.ts"
],
"references": [
{
"path": "../../../src/browser"
}
]
}
+10 -10
View File
@@ -126,7 +126,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
* doesn't exist, do nothing.
* @param term The search term.
* @param searchOptions Search options.
* @return Whether a result was found.
* @returns Whether a result was found.
*/
public findNext(term: string, searchOptions?: ISearchOptions): boolean {
if (!this._terminal) {
@@ -307,7 +307,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
* doesn't exist, do nothing.
* @param term The search term.
* @param searchOptions Search options.
* @return Whether a result was found.
* @returns Whether a result was found.
*/
public findPrevious(term: string, searchOptions?: ISearchOptions): boolean {
if (!this._terminal) {
@@ -477,10 +477,10 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
* started on an earlier line then it is skipped since it will be properly searched when the terminal line that the
* text starts on is searched.
* @param term The search term.
* @param position The position to start the search.
* @param searchPosition The position to start the search.
* @param searchOptions Search options.
* @param isReverseSearch Whether the search should start from the right side of the terminal and search to the left.
* @return The search result if it was found.
* @returns The search result if it was found.
*/
protected _findInLine(term: string, searchPosition: ISearchPosition, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult | undefined {
const terminal = this._terminal!;
@@ -627,7 +627,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
* Wide characters will count as two columns in the resulting string. This
* function is useful for getting the actual text underneath the raw selection
* position.
* @param line The line being translated.
* @param lineIndex The index of the line being translated.
* @param trimRight Whether to trim whitespace to the right.
*/
private _translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): LineCacheEntry {
@@ -662,7 +662,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
/**
* Selects and scrolls to a result.
* @param result The result to select.
* @return Whether a result was selected.
* @returns Whether a result was selected.
*/
private _selectResult(result: ISearchResult | undefined, options?: ISearchDecorationOptions, noScroll?: boolean): boolean {
const terminal = this._terminal!;
@@ -702,10 +702,10 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
}
/**
* Applies styles to the decoration when it is rendered
* @param element the decoration's element
* @param backgroundColor the background color to apply
* @param borderColor the border color to apply
* Applies styles to the decoration when it is rendered.
* @param element The decoration's element.
* @param borderColor The border color to apply.
* @param isActiveResult Whether the element is part of the active search result.
* @returns
*/
private _applyStyles(element: HTMLElement, borderColor: string | undefined, isActiveResult: boolean): void {
@@ -74,7 +74,7 @@ describe('xterm-addon-serialize', () => {
terminal = new Terminal({ cols: 10, rows: 2, allowProposedApi: true });
terminal.loadAddon(serializeAddon);
(terminal as any)._core._themeService = new ThemeService(new OptionsService({}));
(terminal as any)._core._themeService = new ThemeService((terminal as any)._core.optionsService);
(terminal as any)._core._selectionService = new TestSelectionService((terminal as any)._core._bufferService);
});
@@ -205,5 +205,25 @@ describe('xterm-addon-serialize', () => {
});
assert.equal((output.match(/color: #ffffff; background-color: #000000; font-family: courier-new, courier, monospace; font-size: 15px;/g) || []).length, 1, output);
});
it('cells with custom color styling', async () => {
terminal.options.theme.black = '#ffa500';
terminal.options.theme = { ... terminal.options.theme };
await writeP(terminal, ' ' + sgr('38;5;0') + 'terminal' + sgr('39') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='color: #ffa500;'>terminal<\/span>/g) || []).length, 1, output);
});
it('cells with color styling - xterm headless', async () => {
// a headless terminal doesn't have a themeservice
(terminal as any)._core._themeService = undefined;
await writeP(terminal, ' ' + sgr('38;5;46') + 'terminal' + sgr('39') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='color: #00ff00;'>terminal<\/span>/g) || []).length, 1, output);
});
});
});
@@ -7,7 +7,8 @@
import { Terminal, ITerminalAddon, IBuffer, IBufferCell, IBufferRange } from 'xterm';
import { IColorSet } from 'browser/Types';
import { IAttributeData } from 'common/Types';
import { IAttributeData, IColor } from 'common/Types';
import { DEFAULT_ANSI_COLORS } from 'browser/services/ThemeService';
function constrain(value: number, low: number, high: number): number {
return Math.max(low, Math.min(value, high));
@@ -534,7 +535,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
private _htmlContent = '';
private _colors: IColorSet;
private _ansiColors: Readonly<IColor[]>;
constructor(
buffer: IBuffer,
@@ -543,8 +544,13 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
) {
super(buffer);
// https://github.com/xtermjs/xterm.js/issues/3601
this._colors = (_terminal as any)._core._themeService.colors;
// For xterm headless: fallback to ansi colors
if ((_terminal as any)._core._themeService) {
this._ansiColors = (_terminal as any)._core._themeService.colors.ansi;
}
else {
this._ansiColors = DEFAULT_ANSI_COLORS;
}
}
private _padStart(target: string, targetLength: number, padString: string): string {
@@ -600,7 +606,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
return rgb.map(x => this._padStart(x.toString(16), 2, '0')).join('');
}
if (isFg ? cell.isFgPalette() : cell.isBgPalette()) {
return this._colors.ansi[color].css;
return this._ansiColors[color].css;
}
return undefined;
}
@@ -4,7 +4,6 @@
*/
import { IUnicodeVersionProvider } from 'xterm';
import { fill } from 'common/TypedArrayUtils';
type CharWidth = 0 | 1 | 2;
@@ -198,15 +197,15 @@ export class UnicodeV11 implements IUnicodeVersionProvider {
constructor() {
if (!table) {
table = new Uint8Array(65536);
fill(table, 1);
table.fill(1);
table[0] = 0;
fill(table, 0, 1, 32);
fill(table, 0, 0x7f, 0xa0);
table.fill(0, 1, 32);
table.fill(0, 0x7f, 0xa0);
for (let r = 0; r < BMP_COMBINING.length; ++r) {
fill(table, 0, BMP_COMBINING[r][0], BMP_COMBINING[r][1] + 1);
table.fill(0, BMP_COMBINING[r][0], BMP_COMBINING[r][1] + 1);
}
for (let r = 0; r < BMP_WIDE.length; ++r) {
fill(table, 2, BMP_WIDE[r][0], BMP_WIDE[r][1] + 1);
table.fill(2, BMP_WIDE[r][0], BMP_WIDE[r][1] + 1);
}
}
}
@@ -47,6 +47,12 @@ export class LinkComputer {
const [line, startLineIndex] = LinkComputer._translateBufferLineToStringWithWrap(y - 1, false, terminal);
// Don't try if the wrapped line if excessively large as the regex matching will block the main
// thread.
if (line.length > 1024) {
return [];
}
let match;
let stringIndex = -1;
const result: ILink[] = [];
@@ -105,9 +111,8 @@ export class LinkComputer {
/**
* Gets the entire line for the buffer line
* @param line The line being translated.
* @param lineIndex The index of the line being translated.
* @param trimRight Whether to trim whitespace to the right.
* @param terminal The terminal
*/
private static _translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean, terminal: Terminal): [string, number] {
let lineString = '';
+118 -73
View File
@@ -3,15 +3,14 @@
* @license MIT
*/
import { createProgram, PROJECTION_MATRIX } from './WebglUtils';
import { createProgram, GLTexture, PROJECTION_MATRIX } from './WebglUtils';
import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel } from './Types';
import { fill } from 'common/TypedArrayUtils';
import { NULL_CELL_CODE } from 'common/buffer/Constants';
import { Terminal } from 'xterm';
import { IColorSet } from 'browser/Types';
import { IRasterizedGlyph, IRenderDimensions, ITextureAtlas } from 'browser/renderer/shared/Types';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
import { TextureAtlas } from 'browser/renderer/shared/TextureAtlas';
interface IVertices {
attributes: Float32Array;
@@ -30,8 +29,9 @@ const enum VertexAttribLocations {
CELL_POSITION = 1,
OFFSET = 2,
SIZE = 3,
TEXCOORD = 4,
TEXSIZE = 5
TEXPAGE = 4,
TEXCOORD = 5,
TEXSIZE = 6
}
const vertexShaderSource = `#version 300 es
@@ -39,6 +39,7 @@ 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;
@@ -46,27 +47,38 @@ 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;
}`;
const fragmentShaderSource = `#version 300 es
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;
uniform sampler2D u_texture[${maxFragmentShaderTextureUnits}];
out vec4 outColor;
void main() {
outColor = texture(u_texture, v_texcoord);
}`;
if (v_texpage == 0) {
outColor = texture(u_texture[0], v_texcoord);
} ${textureConditionals}
}`);
}
const INDICES_PER_CELL = 10;
const INDICES_PER_CELL = 11;
const BYTES_PER_CELL = INDICES_PER_CELL * Float32Array.BYTES_PER_ELEMENT;
const CELL_POSITION_INDICES = 2;
@@ -77,18 +89,17 @@ 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 _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 = {
private readonly _vertices: IVertices = {
count: 0,
attributes: new Float32Array(0),
attributesBuffers: [
@@ -98,14 +109,22 @@ export class GlyphRenderer extends Disposable {
};
constructor(
private _terminal: Terminal,
private _gl: IWebGL2RenderingContext,
private readonly _terminal: Terminal,
private readonly _gl: IWebGL2RenderingContext,
private _dimensions: IRenderDimensions
) {
super();
const gl = this._gl;
this._program = throwIfFalsy(createProgram(gl, vertexShaderSource, fragmentShaderSource));
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
@@ -127,8 +146,9 @@ export class GlyphRenderer extends Disposable {
gl.vertexAttribPointer(VertexAttribLocations.UNIT_QUAD, 2, this._gl.FLOAT, false, 0, 0);
// Setup the unit quad element array buffer, this points to indices in
// unitQuadVertices to allow is to draw 2 triangles from the vertices
const unitQuadElementIndices = new Uint8Array([0, 1, 3, 0, 2, 3]);
// 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);
@@ -144,23 +164,41 @@ export class GlyphRenderer extends Disposable {
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, 4 * Float32Array.BYTES_PER_ELEMENT);
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, 6 * Float32Array.BYTES_PER_ELEMENT);
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, 8 * Float32Array.BYTES_PER_ELEMENT);
gl.vertexAttribPointer(VertexAttribLocations.CELL_POSITION, 2, gl.FLOAT, false, BYTES_PER_CELL, 9 * Float32Array.BYTES_PER_ELEMENT);
gl.vertexAttribDivisor(VertexAttribLocations.CELL_POSITION, 1);
// Setup empty texture atlas
this._atlasTexture = throwIfFalsy(gl.createTexture());
this.register(toDisposable(() => gl.deleteTexture(this._atlasTexture)));
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);
// 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);
@@ -188,7 +226,7 @@ export class GlyphRenderer extends Disposable {
// Exit early if this is a null character, allow space character to continue as it may have
// underline/strikethrough styles
if (code === NULL_CELL_CODE || code === undefined/* This is used for the right side of wide chars */) {
fill(array, 0, $i, $i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES);
array.fill(0, $i, $i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES);
return;
}
@@ -203,34 +241,38 @@ export class GlyphRenderer extends Disposable {
$glyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext);
}
$leftCellPadding = Math.floor((this._dimensions.scaledCellWidth - this._dimensions.scaledCharWidth) / 2);
$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.scaledCharLeft;
array[$i + 1] = -$glyph.offset.y + this._dimensions.scaledCharTop;
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.scaledCanvasWidth;
array[$i + 3] = $glyph.size.y / this._dimensions.scaledCanvasHeight;
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 + 4] = $glyph.texturePositionClipSpace.x + $clippedPixels / this._atlas.cacheCanvas.width;
array[$i + 5] = $glyph.texturePositionClipSpace.y;
array[$i + 5] = $glyph.texturePositionClipSpace.x + $clippedPixels / this._atlas.pages[$glyph.texturePage].canvas.width;
array[$i + 6] = $glyph.texturePositionClipSpace.y;
// a_texsize
array[$i + 6] = $glyph.sizeClipSpace.x - $clippedPixels / this._atlas.cacheCanvas.width;
array[$i + 7] = $glyph.sizeClipSpace.y;
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.scaledCharLeft;
array[$i + 1] = -$glyph.offset.y + this._dimensions.scaledCharTop;
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.scaledCanvasWidth;
array[$i + 3] = $glyph.size.y / this._dimensions.scaledCanvasHeight;
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 + 4] = $glyph.texturePositionClipSpace.x;
array[$i + 5] = $glyph.texturePositionClipSpace.y;
array[$i + 5] = $glyph.texturePositionClipSpace.x;
array[$i + 6] = $glyph.texturePositionClipSpace.y;
// a_texsize
array[$i + 6] = $glyph.sizeClipSpace.x;
array[$i + 7] = $glyph.sizeClipSpace.y;
array[$i + 7] = $glyph.sizeClipSpace.x;
array[$i + 8] = $glyph.sizeClipSpace.y;
}
// a_cellpos only changes on resize
}
@@ -245,7 +287,8 @@ export class GlyphRenderer extends Disposable {
} else {
this._vertices.attributes.fill(0);
}
for (let i = 0; i < this._vertices.attributesBuffers.length; i++) {
let i = 0;
for (; i < this._vertices.attributesBuffers.length; i++) {
if (this._vertices.count !== newCount) {
this._vertices.attributesBuffers[i] = new Float32Array(newCount);
} else {
@@ -253,11 +296,11 @@ export class GlyphRenderer extends Disposable {
}
}
this._vertices.count = newCount;
let i = 0;
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;
this._vertices.attributes[i + 9] = x / terminal.cols;
this._vertices.attributes[i + 10] = y / terminal.rows;
i += INDICES_PER_CELL;
}
}
@@ -266,6 +309,7 @@ export class GlyphRenderer extends Disposable {
public handleResize(): void {
const gl = this._gl;
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.uniform2f(this._resolutionLocation, gl.canvas.width, gl.canvas.height);
this.clear();
}
@@ -302,31 +346,32 @@ export class GlyphRenderer extends Disposable {
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);
// 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);
}
}
// 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);
gl.drawElementsInstanced(gl.TRIANGLE_STRIP, 4, gl.UNSIGNED_BYTE, 0, bufferLength / INDICES_PER_CELL);
}
public setAtlas(atlas: ITextureAtlas): void {
const gl = this._gl;
this._atlas = atlas;
for (const glTexture of this._atlasTextures) {
glTexture.version = -1;
}
}
gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, atlas.cacheCanvas);
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 {
@@ -113,8 +113,9 @@ export class RectangleRenderer extends Disposable {
gl.vertexAttribPointer(VertexAttribLocations.UNIT_QUAD, 2, this._gl.FLOAT, false, 0, 0);
// Setup the unit quad element array buffer, this points to indices in
// unitQuadVertices to allow is to draw 2 triangles from the vertices
const unitQuadElementIndices = new Uint8Array([0, 1, 3, 0, 2, 3]);
// 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);
@@ -153,7 +154,7 @@ export class RectangleRenderer extends Disposable {
// 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);
gl.drawElementsInstanced(this._gl.TRIANGLE_STRIP, 4, gl.UNSIGNED_BYTE, 0, this._vertices.count);
}
public handleResize(): void {
@@ -175,8 +176,8 @@ export class RectangleRenderer extends Disposable {
0,
0,
0,
this._terminal.cols * this._dimensions.scaledCellWidth,
this._terminal.rows * this._dimensions.scaledCellHeight,
this._terminal.cols * this._dimensions.device.cell.width,
this._terminal.rows * this._dimensions.device.cell.height,
this._bgFloat
);
}
@@ -264,21 +265,21 @@ export class RectangleRenderer extends Disposable {
if (vertices.attributes.length < offset + 4) {
vertices.attributes = expandFloat32Array(vertices.attributes, this._terminal.rows * this._terminal.cols * INDICES_PER_RECTANGLE);
}
$x1 = startX * this._dimensions.scaledCellWidth;
$y1 = y * this._dimensions.scaledCellHeight;
$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 = (!$isDefault && bg & BgFlags.DIM) ? DIM_OPACITY : 1;
this._addRectangle(vertices.attributes, offset, $x1, $y1, (endX - startX) * this._dimensions.scaledCellWidth, this._dimensions.scaledCellHeight, $r, $g, $b, $a);
this._addRectangle(vertices.attributes, offset, $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.scaledCanvasWidth;
array[offset + 1] = y1 / this._dimensions.scaledCanvasHeight;
array[offset + 2] = width / this._dimensions.scaledCanvasWidth;
array[offset + 3] = height / this._dimensions.scaledCanvasHeight;
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;
@@ -286,10 +287,10 @@ export class RectangleRenderer extends Disposable {
}
private _addRectangleFloat(array: Float32Array, offset: number, x1: number, y1: number, width: number, height: number, color: Float32Array): void {
array[offset ] = x1 / this._dimensions.scaledCanvasWidth;
array[offset + 1] = y1 / this._dimensions.scaledCanvasHeight;
array[offset + 2] = width / this._dimensions.scaledCanvasWidth;
array[offset + 3] = height / this._dimensions.scaledCanvasHeight;
array[offset ] = 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];
+2 -3
View File
@@ -4,7 +4,6 @@
*/
import { IRenderModel } from './Types';
import { fill } from 'common/TypedArrayUtils';
import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
import { createSelectionRenderModel } from 'browser/renderer/shared/SelectionRenderModel';
@@ -35,7 +34,7 @@ export class RenderModel implements IRenderModel {
}
public clear(): void {
fill(this.cells, 0, 0);
fill(this.lineLengths, 0, 0);
this.cells.fill(0, 0);
this.lineLengths.fill(0, 0);
}
}
+23 -6
View File
@@ -3,11 +3,11 @@
* @license MIT
*/
import { ICharacterJoinerService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
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 { isSafari } from 'common/Platform';
import { getSafariVersion, isSafari } from 'common/Platform';
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { ICoreTerminal } from 'common/Types';
import { ITerminalAddon, Terminal } from 'xterm';
@@ -17,8 +17,12 @@ export class WebglAddon extends Disposable implements ITerminalAddon {
private _terminal?: Terminal;
private _renderer?: WebglRenderer;
private readonly _onChangeTextureAtlas = this.register(new EventEmitter<HTMLElement>());
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;
@@ -29,8 +33,8 @@ export class WebglAddon extends Disposable implements ITerminalAddon {
}
public activate(terminal: Terminal): void {
if (isSafari) {
throw new Error('Webgl is not currently supported on Safari');
if (isSafari && getSafariVersion() < 16) {
throw new Error('Webgl2 is only supported on Safari 16 and above');
}
const core = (terminal as any)._core as ITerminal;
@@ -46,13 +50,26 @@ export class WebglAddon extends Disposable implements ITerminalAddon {
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 themeService: IThemeService = unsafeCore._themeService;
this._renderer = this.register(new WebglRenderer(terminal, themeService, characterJoinerService, coreBrowserService, optionsService, coreService, decorationService, this._preserveDrawingBuffer));
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(() => {

Some files were not shown because too many files have changed in this diff Show More