Merge branch 'master' into fixdemo

This commit is contained in:
Simon Lamon
2022-08-01 17:56:30 +02:00
committed by GitHub
20 changed files with 242 additions and 119 deletions
+5
View File
@@ -2,6 +2,7 @@
An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables a canvas-based renderer using a 2d context to draw. This addon requires xterm.js v5+.
The purpose of this addon is to be used as a fallback for the [webgl addon](https://www.npmjs.com/package/xterm-addon-webgl) when better performance is desired over the default DOM renderer, but WebGL2 isn't supported or performant for some reason.
### Install
@@ -21,3 +22,7 @@ terminal.loadAddon(new CanvasAddon());
```
See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/xterm-addon-canvas/typings/xterm-addon-canvas.d.ts) for more advanced usage.
### See also
- [xterm-addon-webgl](https://www.npmjs.com/package/xterm-addon-webgl) A renderer for xterm.js that uses WebGL
+4
View File
@@ -36,3 +36,7 @@ terminal.loadAddon(addon);
```
Read more about handling WebGL context losses on the [Khronos wiki](https://www.khronos.org/webgl/wiki/HandlingContextLost).
### See also
- [xterm-addon-canvas](https://www.npmjs.com/package/xterm-addon-canvas) A renderer for xterm.js that uses a 2d canvas that can be used as a fallback when WebGL is not available
@@ -203,10 +203,11 @@ export class GlyphRenderer extends Disposable {
return;
}
if (bg !== lastBg && rasterizedGlyph.offset.x > 0) {
const clippedPixels = rasterizedGlyph.offset.x;
const leftCellPadding = Math.floor((this._dimensions.scaledCellWidth - this._dimensions.scaledCharWidth) / 2);
if (bg !== lastBg && rasterizedGlyph.offset.x > leftCellPadding) {
const clippedPixels = rasterizedGlyph.offset.x - leftCellPadding;
// a_origin
array[i ] = this._dimensions.scaledCharLeft;
array[i ] = -(rasterizedGlyph.offset.x - clippedPixels) + this._dimensions.scaledCharLeft;
array[i + 1] = -rasterizedGlyph.offset.y + this._dimensions.scaledCharTop;
// a_size
array[i + 2] = (rasterizedGlyph.size.x - clippedPixels) / this._dimensions.scaledCanvasWidth;
@@ -12,6 +12,7 @@ import { IColorSet } from 'browser/Types';
import { IRenderDimensions } from 'browser/renderer/Types';
import { RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { DIM_OPACITY } from 'browser/renderer/Constants';
const enum VertexAttribLocations {
POSITION = 0,
@@ -208,6 +209,7 @@ export class RectangleRenderer extends Disposable {
private _updateRectangle(vertices: IVertices, offset: number, fg: number, bg: number, startX: number, endX: number, y: number): void {
let rgba: number | undefined;
let isDefault = false;
if (fg & FgFlags.INVERSE) {
switch (fg & Attributes.CM_MASK) {
case Attributes.CM_P16:
@@ -233,6 +235,7 @@ export class RectangleRenderer extends Disposable {
case Attributes.CM_DEFAULT:
default:
rgba = this._colors.background.rgba;
isDefault = true;
}
}
@@ -244,7 +247,7 @@ export class RectangleRenderer extends Disposable {
const r = ((rgba >> 24) & 0xFF) / 255;
const g = ((rgba >> 16) & 0xFF) / 255;
const b = ((rgba >> 8 ) & 0xFF) / 255;
const a = bg & BgFlags.DIM ? 0.5 : 1;
const 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);
}
+6 -2
View File
@@ -7,13 +7,16 @@ import { Terminal, ITerminalAddon, IEvent } from 'xterm';
import { WebglRenderer } from './WebglRenderer';
import { ICharacterJoinerService, ICoreBrowserService, IRenderService } from 'browser/services/Services';
import { IColorSet } from 'browser/Types';
import { EventEmitter } from 'common/EventEmitter';
import { EventEmitter, forwardEvent } from 'common/EventEmitter';
import { isSafari } from 'common/Platform';
import { ICoreService, IDecorationService } from 'common/services/Services';
export class WebglAddon implements ITerminalAddon {
private _terminal?: Terminal;
private _renderer?: WebglRenderer;
private _onChangeTextureAtlas = new EventEmitter<HTMLElement>();
public get onChangeTextureAtlas(): IEvent<HTMLElement> { return this._onChangeTextureAtlas.event; }
private _onContextLoss = new EventEmitter<void>();
public get onContextLoss(): IEvent<void> { return this._onContextLoss.event; }
@@ -36,7 +39,8 @@ export class WebglAddon implements ITerminalAddon {
const decorationService: IDecorationService = (terminal as any)._core._decorationService;
const colors: IColorSet = (terminal as any)._core._colorManager.colors;
this._renderer = new WebglRenderer(terminal, colors, characterJoinerService, coreBrowserService, coreService, decorationService, this._preserveDrawingBuffer);
this._renderer.onContextLoss(() => this._onContextLoss.fire());
forwardEvent(this._renderer.onContextLoss, this._onContextLoss);
forwardEvent(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas);
renderService.setRenderer(this._renderer);
}
+16 -5
View File
@@ -25,7 +25,6 @@ import { ICharacterJoinerService, ICoreBrowserService } from 'browser/services/S
import { CharData, ICellData } from 'common/Types';
import { AttributeData } from 'common/buffer/AttributeData';
import { ICoreService, IDecorationService } from 'common/services/Services';
import { color, rgba as rgbaNs } from 'common/Color';
export class WebglRenderer extends Disposable implements IRenderer {
private _renderLayers: IRenderLayer[];
@@ -46,6 +45,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
private _core: ITerminal;
private _isAttached: boolean;
private _onChangeTextureAtlas = new EventEmitter<HTMLCanvasElement>();
public get onChangeTextureAtlas(): IEvent<HTMLCanvasElement> { return this._onChangeTextureAtlas.event; }
private _onRequestRedraw = new EventEmitter<IRequestRedrawEvent>();
public get onRequestRedraw(): IEvent<IRequestRedrawEvent> { return this._onRequestRedraw.event; }
@@ -240,7 +241,10 @@ export class WebglRenderer extends Disposable implements IRenderer {
if (!('getRasterizedGlyph' in atlas)) {
throw new Error('The webgl renderer only works with the webgl char atlas');
}
this._charAtlas = atlas as WebglCharAtlas;
if (this._charAtlas !== atlas) {
this._onChangeTextureAtlas.fire(atlas.cacheCanvas);
}
this._charAtlas = atlas;
this._charAtlas.warmUp();
this._glyphRenderer.setAtlas(this._charAtlas);
}
@@ -402,6 +406,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
// override logic throughout the different sub-renderers
let bgOverride: number | undefined;
let fgOverride: number | undefined;
let isSelected: boolean = false;
// Apply decorations on the bottom layer
for (const d of this._decorationService.getDecorationsAtCell(x, y, 'bottom')) {
@@ -414,7 +419,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
}
// Apply the selection color if needed
if (this._isCellSelected(x, y)) {
isSelected = this._isCellSelected(x, y);
if (isSelected) {
bgOverride = (this._coreBrowserService.isFocused ? this._colors.selectionBackgroundOpaque : this._colors.selectionInactiveBackgroundOpaque).rgba >> 8 & 0xFFFFFF;
if (this._colors.selectionForeground) {
fgOverride = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
@@ -434,8 +440,13 @@ export class WebglRenderer extends Disposable implements IRenderer {
// Convert any overrides from rgba to the fg/bg packed format. This resolves the inverse flag
// ahead of time in order to use the correct cache key
if (bgOverride !== undefined) {
// Non-RGB attributes from model + override + force RGB color mode
bgOverride = (this._workCell.bg & ~Attributes.RGB_MASK) | bgOverride | Attributes.CM_RGB;
if (isSelected) {
// Non-RGB attributes from model + force non-dim + override + force RGB color mode
bgOverride = (this._workCell.bg & ~Attributes.RGB_MASK & ~BgFlags.DIM) | bgOverride | Attributes.CM_RGB;
} else {
// Non-RGB attributes from model + override + force RGB color mode
bgOverride = (this._workCell.bg & ~Attributes.RGB_MASK) | bgOverride | Attributes.CM_RGB;
}
}
if (fgOverride !== undefined) {
// Non-RGB attributes from model + force disable inverse + override + force RGB color mode
@@ -45,6 +45,12 @@ const NULL_RASTERIZED_GLYPH: IRasterizedGlyph = {
const TMP_CANVAS_GLYPH_PADDING = 2;
interface ICharAtlasActiveRow {
x: number;
y: number;
height: number;
}
export class WebglCharAtlas implements IDisposable {
private _didWarmUp: boolean = false;
@@ -59,13 +65,22 @@ export class WebglCharAtlas implements IDisposable {
// A temporary context that glyphs are drawn to before being transfered to the atlas.
private _tmpCtx: CanvasRenderingContext2D;
// Since glyphs are expected to be around the same height, the packing
// strategy used it to fill a row with glyphs while keeping track of the
// tallest glyph in the row. Once the row is full a new row is started at
// (0,lastRow+lastRowTallestGlyph).
private _currentRowY: number = 0;
private _currentRowX: number = 0;
private _currentRowHeight: number = 0;
// Texture atlas current positioning data. The texture packing strategy used is to fill from
// left-to-right and top-to-bottom. When the glyph being written is less than half of the current
// row's height, the following happens:
//
// - The current row becomes the fixed height row A
// - A new fixed height row B the exact size of the glyph is created below the current row
// - A new dynamic height current row is created below B
//
// This strategy does a good job preventing space being wasted for very short glyphs such as
// underscores, hyphens etc. or those with underlines rendered.
private _currentRow: ICharAtlasActiveRow = {
x: 0,
y: 0,
height: 0
};
private readonly _fixedRows: ICharAtlasActiveRow[] = [];
public hasCanvasChanged = false;
@@ -118,7 +133,7 @@ export class WebglCharAtlas implements IDisposable {
}
public beginFrame(): boolean {
if (this._currentRowY > TEXTURE_CAPACITY) {
if (this._currentRow.y > TEXTURE_CAPACITY) {
this.clearTexture();
this.warmUp();
return true;
@@ -127,15 +142,16 @@ export class WebglCharAtlas implements IDisposable {
}
public clearTexture(): void {
if (this._currentRowX === 0 && this._currentRowY === 0) {
if (this._currentRow.x === 0 && this._currentRow.y === 0) {
return;
}
this._cacheCtx.clearRect(0, 0, TEXTURE_WIDTH, TEXTURE_HEIGHT);
this._cacheMap = {};
this._cacheMapCombined = {};
this._currentRowHeight = 0;
this._currentRowX = 0;
this._currentRowY = 0;
this._currentRow.x = 0;
this._currentRow.y = 0;
this._currentRow.height = 0;
this._fixedRows.length = 0;
this._didWarmUp = false;
}
@@ -225,7 +241,7 @@ export class WebglCharAtlas implements IDisposable {
if (dim) {
// Blend here instead of using opacity because transparent colors mess with clipping the
// glyph's bounding box
result = color.blend(this._config.colors.background, color.multiplyOpacity(result, 0.5));
result = color.blend(this._config.colors.background, color.multiplyOpacity(result, DIM_OPACITY));
}
return result;
@@ -267,7 +283,7 @@ export class WebglCharAtlas implements IDisposable {
// Apply dim to the color, opacity is fine to use for the foreground color
if (dim) {
result = color.multiplyOpacity(result, 0.5);
result = color.multiplyOpacity(result, DIM_OPACITY);
}
return result;
@@ -401,29 +417,31 @@ export class WebglCharAtlas implements IDisposable {
`${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`;
this._tmpCtx.textBaseline = TEXT_BASELINE;
const powerLineGlyph = chars.length === 1 && isPowerlineGlyph(chars.charCodeAt(0));
const powerlineGlyph = chars.length === 1 && isPowerlineGlyph(chars.charCodeAt(0));
const foregroundColor = this._getForegroundColor(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, dim, bold, excludeFromContrastRatioDemands(chars.charCodeAt(0)));
this._tmpCtx.fillStyle = foregroundColor.css;
// For powerline glyphs left/top padding is excluded (https://github.com/microsoft/vscode/issues/120129)
const padding = powerLineGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING * 2;
const padding = powerlineGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING * 2;
// Draw custom characters if applicable
let drawSuccess = false;
let customGlyph = false;
if (this._config.customGlyphs !== false) {
drawSuccess = tryDrawCustomChar(this._tmpCtx, chars, padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight);
customGlyph = tryDrawCustomChar(this._tmpCtx, chars, padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight);
}
// Whether to clear pixels based on a threshold difference between the glyph color and the
// background color. This should be disabled when the glyph contains multiple colors such as
// underline colors to prevent important colors could get cleared.
let enableClearThresholdCheck = true;
let enableClearThresholdCheck = !powerlineGlyph;
// Draw underline
if (underline) {
this._tmpCtx.save();
const lineWidth = Math.max(1, Math.floor(this._config.fontSize * window.devicePixelRatio / 10));
const yOffset = this._tmpCtx.lineWidth % 2 === 1 ? 0.5 : 0; // When the width is odd, draw at 0.5 position
const lineWidth = Math.max(1, Math.floor(this._config.fontSize * window.devicePixelRatio / 15));
// When the width is odd, draw at 0.5 position. Offset by an additional 1 dpr to bring the
// underline closer to the character
const yOffset = (lineWidth % 2 === 1 ? 0.5 : 0) + window.devicePixelRatio;
this._tmpCtx.lineWidth = lineWidth;
// Underline color
@@ -492,18 +510,18 @@ export class WebglCharAtlas implements IDisposable {
break;
case UnderlineStyle.DOTTED:
this._tmpCtx.setLineDash([window.devicePixelRatio * 2, window.devicePixelRatio]);
this._tmpCtx.moveTo(xLeft, yMid);
this._tmpCtx.lineTo(xRight, yMid);
this._tmpCtx.moveTo(xLeft, yTop);
this._tmpCtx.lineTo(xRight, yTop);
break;
case UnderlineStyle.DASHED:
this._tmpCtx.setLineDash([window.devicePixelRatio * 4, window.devicePixelRatio * 3]);
this._tmpCtx.moveTo(xLeft, yMid);
this._tmpCtx.lineTo(xRight, yMid);
this._tmpCtx.moveTo(xLeft, yTop);
this._tmpCtx.lineTo(xRight, yTop);
break;
case UnderlineStyle.SINGLE:
default:
this._tmpCtx.moveTo(xLeft, yMid);
this._tmpCtx.lineTo(xRight, yMid);
this._tmpCtx.moveTo(xLeft, yTop);
this._tmpCtx.lineTo(xRight, yTop);
break;
}
this._tmpCtx.stroke();
@@ -511,20 +529,28 @@ export class WebglCharAtlas implements IDisposable {
// Draw stroke in the background color for non custom characters in order to give an outline
// between the text and the underline
if (!drawSuccess) {
if (!customGlyph) {
// This only works when transparency is disabled because it's not clear how to clear stroked
// text
if (!this._config.allowTransparency && chars !== ' ') {
// This translates to 1/2 the line width in either direction
this._tmpCtx.save();
// Clip the region to only draw in valid pixels near the underline to avoid a slight
// outline around the whole glyph, as well as additional pixels in the glyph at the top
// which would increase GPU memory demands
const clipRegion = new Path2D();
clipRegion.rect(xLeft, yTop - Math.ceil(lineWidth / 2), this._config.scaledCellWidth, yBot - yTop + Math.ceil(lineWidth / 2));
this._tmpCtx.clip(clipRegion);
this._tmpCtx.lineWidth = window.devicePixelRatio * 3;
this._tmpCtx.strokeStyle = backgroundColor.css;
this._tmpCtx.strokeText(chars, padding, padding + this._config.scaledCharHeight);
this._tmpCtx.restore();
}
}
}
// Draw the character
if (!drawSuccess) {
if (!customGlyph) {
this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight);
}
@@ -577,25 +603,69 @@ export class WebglCharAtlas implements IDisposable {
return NULL_RASTERIZED_GLYPH;
}
const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, allowedWidth, powerLineGlyph, drawSuccess);
const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, allowedWidth, powerlineGlyph, customGlyph, padding);
const clippedImageData = this._clipImageData(imageData, this._workBoundingBox);
// Check if there is enough room in the current row and go to next if needed
if (this._currentRowX + rasterizedGlyph.size.x > TEXTURE_WIDTH) {
this._currentRowX = 0;
this._currentRowY += this._currentRowHeight;
this._currentRowHeight = 0;
// Find the best atlas row to use
let activeRow: ICharAtlasActiveRow;
while (true) {
// Select the ideal existing row, preferring fixed rows over the current row
activeRow = this._currentRow;
for (const row of this._fixedRows) {
if ((activeRow === this._currentRow || row.height < activeRow.height) && rasterizedGlyph.size.y <= row.height) {
activeRow = row;
}
}
// Create a new one if vertical space would be wasted, fixing the previously active row in the
// process as it now has a fixed height
if (activeRow.height > rasterizedGlyph.size.y * 2) {
// Fix the current row as the new row is being added below
if (this._currentRow.height > 0) {
this._fixedRows.push(this._currentRow);
}
// Create the new fixed height row
activeRow = {
x: 0,
y: this._currentRow.y + this._currentRow.height,
height: rasterizedGlyph.size.y
};
this._fixedRows.push(activeRow);
// Create the new current row below the new fixed height row
this._currentRow = {
x: 0,
y: activeRow.y + activeRow.height,
height: 0
};
}
// Exit the loop if there is enough room in the row
if (activeRow.x + rasterizedGlyph.size.x <= TEXTURE_WIDTH) {
break;
}
// If there is enough room in the current row, finish it and try again
if (activeRow === this._currentRow) {
activeRow.x = 0;
activeRow.y += activeRow.height;
activeRow.height = 0;
} else {
this._fixedRows.splice(this._fixedRows.indexOf(activeRow), 1);
}
}
// Record texture position
rasterizedGlyph.texturePosition.x = this._currentRowX;
rasterizedGlyph.texturePosition.y = this._currentRowY;
rasterizedGlyph.texturePositionClipSpace.x = this._currentRowX / TEXTURE_WIDTH;
rasterizedGlyph.texturePositionClipSpace.y = this._currentRowY / TEXTURE_HEIGHT;
rasterizedGlyph.texturePosition.x = activeRow.x;
rasterizedGlyph.texturePosition.y = activeRow.y;
rasterizedGlyph.texturePositionClipSpace.x = activeRow.x / TEXTURE_WIDTH;
rasterizedGlyph.texturePositionClipSpace.y = activeRow.y / TEXTURE_HEIGHT;
// Update atlas current row
this._currentRowHeight = Math.max(this._currentRowHeight, rasterizedGlyph.size.y);
this._currentRowX += rasterizedGlyph.size.x;
// Update atlas current row, for fixed rows the glyph height will never be larger than the row
// height
activeRow.height = Math.max(activeRow.height, rasterizedGlyph.size.y);
activeRow.x += rasterizedGlyph.size.x;
// putImageData doesn't do any blending, so it will overwrite any existing cache entry for us
this._cacheCtx.putImageData(clippedImageData, rasterizedGlyph.texturePosition.x, rasterizedGlyph.texturePosition.y);
@@ -610,10 +680,10 @@ export class WebglCharAtlas implements IDisposable {
* @param imageData The image data to read.
* @param boundingBox An IBoundingBox to put the clipped bounding box values.
*/
private _findGlyphBoundingBox(imageData: ImageData, boundingBox: IBoundingBox, allowedWidth: number, restrictedGlyph: boolean, customGlyph: boolean): IRasterizedGlyph {
private _findGlyphBoundingBox(imageData: ImageData, boundingBox: IBoundingBox, allowedWidth: number, restrictedGlyph: boolean, customGlyph: boolean, padding: number): IRasterizedGlyph {
boundingBox.top = 0;
const height = restrictedGlyph ? this._config.scaledCellHeight : this._tmpCanvas.height;
const width = restrictedGlyph ? this._config.scaledCharWidth : allowedWidth;
const width = restrictedGlyph ? this._config.scaledCellWidth : allowedWidth;
let found = false;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
@@ -685,8 +755,8 @@ export class WebglCharAtlas implements IDisposable {
y: (boundingBox.bottom - boundingBox.top + 1) / TEXTURE_HEIGHT
},
offset: {
x: -boundingBox.left + (restrictedGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING) + (customGlyph ? Math.floor(this._config.letterSpacing / 2) : 0),
y: -boundingBox.top + (restrictedGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING) + (customGlyph ? this._config.lineHeight === 1 ? 0 : Math.round((this._config.scaledCellHeight - this._config.scaledCharHeight) / 2) : 0)
x: -boundingBox.left + padding + ((restrictedGlyph || customGlyph) ? Math.floor((this._config.scaledCellWidth - this._config.scaledCharWidth) / 2) : 0),
y: -boundingBox.top + padding + ((restrictedGlyph || customGlyph) ? this._config.lineHeight === 1 ? 0 : Math.round((this._config.scaledCellHeight - this._config.scaledCharHeight) / 2) : 0)
}
};
}
@@ -763,8 +833,3 @@ function checkCompletelyTransparent(imageData: ImageData): boolean {
}
return true;
}
function toPaddedHex(c: number): string {
const s = c.toString(16);
return s.length < 2 ? '0' + s : s;
}
+10 -5
View File
@@ -12,6 +12,16 @@ declare module 'xterm-addon-webgl' {
export class WebglAddon implements ITerminalAddon {
public textureAtlas?: HTMLCanvasElement;
/**
* An event that is fired when the renderer loses its canvas context.
*/
public get onContextLoss(): IEvent<void>;
/**
* An event that is fired when the texture atlas of the renderer changes.
*/
public get onChangeTextureAtlas(): IEvent<HTMLCanvasElement>;
constructor(preserveDrawingBuffer?: boolean);
/**
@@ -29,10 +39,5 @@ declare module 'xterm-addon-webgl' {
* Clears the terminal's texture atlas and triggers a redraw.
*/
public clearTextureAtlas(): void;
/**
* Fired when the WebglRenderer loses context
*/
public get onContextLoss(): IEvent<void>;
}
}
+13 -11
View File
@@ -240,7 +240,8 @@ function createTerminal(): void {
try {
typedTerm.loadAddon(addons.webgl.instance);
setTimeout(() => {
document.body.appendChild(addons.webgl.instance.textureAtlas);
addTextureAtlas(addons.webgl.instance.textureAtlas);
addons.webgl.instance.onChangeTextureAtlas(e => addTextureAtlas(e));
}, 0);
}
catch {
@@ -404,20 +405,18 @@ function initOptions(term: TerminalType): void {
const input = <HTMLInputElement>document.getElementById(`opt-${o}`);
addDomListener(input, 'change', () => {
console.log('change', o, input.value);
if (o === 'cols' || o === 'rows') {
updateTerminalSize();
} else if (o === 'lineHeight') {
if (o === 'lineHeight') {
term.options.lineHeight = parseFloat(input.value);
updateTerminalSize();
} else if (o === 'scrollSensitivity') {
term.options.scrollSensitivity = parseFloat(input.value);
updateTerminalSize();
} else if (o === 'scrollback') {
term.options.scrollback = parseInt(input.value);
setTimeout(() => updateTerminalSize(), 5);
} else {
term.options[o] = parseInt(input.value);
}
// Always update terminal size in case the option changes the dimensions
updateTerminalSize();
});
});
Object.keys(stringOptions).forEach(o => {
@@ -526,7 +525,7 @@ function initAddons(term: TerminalType): void {
}
} else {
if (name === 'webgl') {
document.body.removeChild((addon.instance as WebglAddon).textureAtlas);
(addon.instance as WebglAddon).textureAtlas.remove();
} else if (name === 'unicode11') {
term.unicode.activeVersion = '6';
}
@@ -602,6 +601,9 @@ function htmlSerializeButtonHandler(): void {
document.getElementById("htmlserialize-output-result").innerText = "Copied to clipboard";
}
function addTextureAtlas(e: HTMLCanvasElement) {
document.querySelector('#texture-atlas').appendChild(e);
}
function writeCustomGlyphHandler() {
term.write('\n\r');
@@ -700,7 +702,7 @@ function powerlineSymbolTest() {
` 3 \ue0b1 \x1b[33;44m\ue0b0\x1b[39m` +
` 4 \ue0b1 \x1b[34;45m\ue0b0\x1b[39m` +
` 5 \ue0b1 \x1b[35;46m\ue0b0\x1b[39m` +
` 6 \ue0b1 \x1b[36;47m\ue0b0\x1b[39m` +
` 6 \ue0b1 \x1b[36;47m\ue0b0\x1b[30m` +
` 7 \ue0b1 \x1b[37;49m\ue0b0\x1b[0m`
);
term.writeln('');
@@ -713,7 +715,7 @@ function powerlineSymbolTest() {
` 3 \ue0b3 \x1b[7;33;44m\ue0b2\x1b[27;39m` +
` 4 \ue0b3 \x1b[7;34;45m\ue0b2\x1b[27;39m` +
` 5 \ue0b3 \x1b[7;35;46m\ue0b2\x1b[27;39m` +
` 6 \ue0b3 \x1b[7;36;47m\ue0b2\x1b[27;39m` +
` 6 \ue0b3 \x1b[7;36;47m\ue0b2\x1b[27;30m` +
` 7 \ue0b3 \x1b[7;37;49m\ue0b2\x1b[0m`
);
term.writeln('');
@@ -726,7 +728,7 @@ function powerlineSymbolTest() {
` 3 \ue0b5 \x1b[33;44m\ue0b4\x1b[39m` +
` 4 \ue0b5 \x1b[34;45m\ue0b4\x1b[39m` +
` 5 \ue0b5 \x1b[35;46m\ue0b4\x1b[39m` +
` 6 \ue0b5 \x1b[36;47m\ue0b4\x1b[39m` +
` 6 \ue0b5 \x1b[36;47m\ue0b4\x1b[30m` +
` 7 \ue0b5 \x1b[37;49m\ue0b4\x1b[0m`
);
term.writeln('');
@@ -739,7 +741,7 @@ function powerlineSymbolTest() {
` 3 \ue0b7 \x1b[7;33;44m\ue0b6\x1b[27;39m` +
` 4 \ue0b7 \x1b[7;34;45m\ue0b6\x1b[27;39m` +
` 5 \ue0b7 \x1b[7;35;46m\ue0b6\x1b[27;39m` +
` 6 \ue0b7 \x1b[7;36;47m\ue0b6\x1b[27;39m` +
` 6 \ue0b7 \x1b[7;36;47m\ue0b6\x1b[27;30m` +
` 7 \ue0b7 \x1b[7;37;49m\ue0b6\x1b[0m`
);
term.writeln('');
+1
View File
@@ -87,6 +87,7 @@
</div>
</div>
</div>
<div id="texture-atlas"></div>
<script src="dist/client-bundle.js" defer ></script>
<script>
var tab = localStorage.getItem("tab");
+14
View File
@@ -89,3 +89,17 @@ pre {
max-height: 100vh;
overflow-y: auto;
}
#texture-atlas {
width: 100%;
height: 600px;
overflow: scroll;
}
#texture-atlas canvas {
image-rendering: pixelated;
}
#texture-atlas canvas:hover {
/* zoom to 4x on hover */
width: 4096px;
height: 4096px;
}
-5
View File
@@ -957,11 +957,6 @@ export class Terminal extends CoreTerminal implements ITerminal {
}
public addMarker(cursorYOffset: number): IMarker | undefined {
// Disallow markers on the alt buffer
if (this.buffer !== this.buffers.normal) {
return;
}
return this.buffer.addMarker(this.buffer.ybase + this.buffer.y + cursorYOffset);
}
+10 -10
View File
@@ -334,8 +334,8 @@ export const boxDrawingDefinitions: { [character: string]: { [fontWeight: number
interface IVectorShape {
d: string;
type: VectorType;
/** Padding to apply to the vector's x axis in CSS pixels. */
horizontalPadding?: number;
leftPadding?: number;
rightPadding?: number;
}
const enum VectorType {
@@ -355,19 +355,19 @@ export const powerlineDefinitions: { [index: string]: IVectorShape } = {
// Right triangle solid
'\u{E0B0}': { d: 'M0,0 L1,.5 L0,1', type: VectorType.FILL },
// Right triangle line
'\u{E0B1}': { d: 'M0,0 L1,.5 L0,1', type: VectorType.STROKE, horizontalPadding: 0.5 },
'\u{E0B1}': { d: 'M0,0 L1,.5 L0,1', type: VectorType.STROKE, leftPadding: window.devicePixelRatio / 2, rightPadding: window.devicePixelRatio / 2 },
// Left triangle solid
'\u{E0B2}': { d: 'M1,0 L0,.5 L1,1', type: VectorType.FILL },
// Left triangle line
'\u{E0B3}': { d: 'M1,0 L0,.5 L1,1', type: VectorType.STROKE, horizontalPadding: 0.5 },
'\u{E0B3}': { d: 'M1,0 L0,.5 L1,1', type: VectorType.STROKE, leftPadding: window.devicePixelRatio / 2, rightPadding: window.devicePixelRatio / 2 },
// Right semi-circle solid,
'\u{E0B4}': { d: 'M0,0 L0,1 C0.552,1,1,0.776,1,.5 C1,0.224,0.552,0,0,0', type: VectorType.FILL },
// Right semi-circle line,
'\u{E0B5}': { d: 'M0,1 C0.552,1,1,0.776,1,.5 C1,0.224,0.552,0,0,0', type: VectorType.STROKE },
'\u{E0B5}': { d: 'M0,1 C0.552,1,1,0.776,1,.5 C1,0.224,0.552,0,0,0', type: VectorType.STROKE, rightPadding: window.devicePixelRatio / 2 },
// Left semi-circle solid,
'\u{E0B6}': { d: 'M1,0 L1,1 C0.448,1,0,0.776,0,.5 C0,0.224,0.448,0,1,0', type: VectorType.FILL },
// Left semi-circle line,
'\u{E0B7}': { d: 'M1,1 C0.448,1,0,0.776,0,.5 C0,0.224,0.448,0,1,0', type: VectorType.STROKE }
'\u{E0B7}': { d: 'M1,1 C0.448,1,0,0.776,0,.5 C0,0.224,0.448,0,1,0', type: VectorType.STROKE, leftPadding: window.devicePixelRatio / 2 }
};
/**
@@ -590,7 +590,7 @@ function drawPowerlineChar(
if (!args[0] || !args[1]) {
continue;
}
f(ctx, translateArgs(args, scaledCellWidth, scaledCellHeight, xOffset, yOffset, charDefinition.horizontalPadding));
f(ctx, translateArgs(args, scaledCellWidth, scaledCellHeight, xOffset, yOffset, charDefinition.leftPadding, charDefinition.rightPadding));
}
if (charDefinition.type === VectorType.STROKE) {
ctx.strokeStyle = ctx.fillStyle;
@@ -611,7 +611,7 @@ const svgToCanvasInstructionMap: { [index: string]: any } = {
'M': (ctx: CanvasRenderingContext2D, args: number[]) => ctx.moveTo(args[0], args[1])
};
function translateArgs(args: string[], cellWidth: number, cellHeight: number, xOffset: number, yOffset: number, horizontalPadding: number = 0): number[] {
function translateArgs(args: string[], cellWidth: number, cellHeight: number, xOffset: number, yOffset: number, leftPadding: number = 0, rightPadding: number = 0): number[] {
const result = args.map(e => parseFloat(e) || parseInt(e));
if (result.length < 2) {
@@ -620,14 +620,14 @@ function translateArgs(args: string[], cellWidth: number, cellHeight: number, xO
for (let x = 0; x < result.length; x += 2) {
// Translate from 0-1 to 0-cellWidth
result[x] *= cellWidth - (horizontalPadding * 2 * window.devicePixelRatio);
result[x] *= cellWidth - (leftPadding * window.devicePixelRatio) - (rightPadding * window.devicePixelRatio);
// Ensure coordinate doesn't escape cell bounds and round to the nearest 0.5 to ensure a crisp
// line at 100% devicePixelRatio
if (result[x] !== 0) {
result[x] = clamp(Math.round(result[x] + 0.5) - 0.5, cellWidth, 0);
}
// Apply the cell's offset (ie. x*cellWidth)
result[x] += xOffset + (horizontalPadding * window.devicePixelRatio);
result[x] += xOffset + (leftPadding * window.devicePixelRatio);
}
for (let y = 1; y < result.length; y += 2) {
@@ -139,7 +139,7 @@ describe('DomRendererRowFactory', () => {
lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-underline-1" style="text-decoration-color: rgb(255,255,255);">a</span>'
'<span class="xterm-underline-1">a</span>'
);
});
it('should add class for double underline style', () => {
@@ -150,7 +150,7 @@ describe('DomRendererRowFactory', () => {
lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-underline-2" style="text-decoration-color: rgb(255,255,255);">a</span>'
'<span class="xterm-underline-2">a</span>'
);
});
it('should add class for curly underline style', () => {
@@ -161,7 +161,7 @@ describe('DomRendererRowFactory', () => {
lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-underline-3" style="text-decoration-color: rgb(255,255,255);">a</span>'
'<span class="xterm-underline-3">a</span>'
);
});
it('should add class for double dotted style', () => {
@@ -172,7 +172,7 @@ describe('DomRendererRowFactory', () => {
lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-underline-4" style="text-decoration-color: rgb(255,255,255);">a</span>'
'<span class="xterm-underline-4">a</span>'
);
});
it('should add class for dashed underline style', () => {
@@ -183,7 +183,7 @@ describe('DomRendererRowFactory', () => {
lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-underline-5" style="text-decoration-color: rgb(255,255,255);">a</span>'
'<span class="xterm-underline-5">a</span>'
);
});
});
+1 -1
View File
@@ -149,7 +149,7 @@ export class ExtendedAttrs implements IExtendedAttrs {
constructor(
underlineStyle: UnderlineStyle = UnderlineStyle.NONE,
underlineColor: number = -1
underlineColor: number = Attributes.CM_DEFAULT
) {
this.underlineStyle = underlineStyle;
this.underlineColor = underlineColor;
+10
View File
@@ -71,4 +71,14 @@ describe('BufferSet', () => {
assert.equal(bufferSet.active.y, 10);
});
});
describe('markers', () => {
it('should clear the markers when the buffer is switched', () => {
bufferSet.activateAltBuffer();
bufferSet.alt.addMarker(1);
assert.equal(bufferSet.alt.markers.length, 1);
bufferSet.activateNormalBuffer();
assert.equal(bufferSet.alt.markers.length, 0);
});
});
});
+1
View File
@@ -83,6 +83,7 @@ export class BufferSet extends Disposable implements IBufferSet {
// The alt buffer should always be cleared when we switch to the normal
// buffer. This frees up memory since the alt buffer should always be new
// when activated.
this._alt.clearAllMarkers();
this._alt.clear();
this._activeBuffer = this._normal;
this._onBufferActivate.fire({
+2 -5
View File
@@ -125,13 +125,10 @@ describe('Headless API Tests', function (): void {
});
it('get options', () => {
const options: ITerminalOptions = term.options;
strictEqual(options.cols, 80);
strictEqual(options.rows, 24);
strictEqual(options.lineHeight, 1);
strictEqual(options.cursorWidth, 1);
});
it('set options', async () => {
const options: ITerminalOptions = term.options;
throws(() => options.cols = 40);
throws(() => options.rows = 20);
term.options.scrollback = 1;
strictEqual(term.options.scrollback, 1);
term.options= {
+2 -3
View File
@@ -7,11 +7,10 @@ import { IEvent } from 'common/EventEmitter';
import { BufferNamespaceApi } from 'common/public/BufferNamespaceApi';
import { ParserApi } from 'common/public/ParserApi';
import { UnicodeApi } from 'common/public/UnicodeApi';
import { IBufferNamespace as IBufferNamespaceApi, IMarker, IModes, IParser, ITerminalAddon, IUnicodeHandling, Terminal as ITerminalApi } from 'xterm-headless';
import { IBufferNamespace as IBufferNamespaceApi, IMarker, IModes, IParser, ITerminalAddon, ITerminalInitOnlyOptions, IUnicodeHandling, Terminal as ITerminalApi } from 'xterm-headless';
import { Terminal as TerminalCore } from 'headless/Terminal';
import { AddonManager } from 'common/public/AddonManager';
import { ITerminalOptions } from 'common/Types';
/**
* The set of options that only have an effect when set in the Terminal constructor.
*/
@@ -24,7 +23,7 @@ export class Terminal implements ITerminalApi {
private _buffer: BufferNamespaceApi | undefined;
private _publicOptions: ITerminalOptions;
constructor(options?: ITerminalOptions) {
constructor(options?: ITerminalOptions & ITerminalInitOnlyOptions) {
this._core = new TerminalCore(options);
this._addonManager = new AddonManager();
+18 -12
View File
@@ -14,7 +14,7 @@ declare module 'xterm-headless' {
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'off';
/**
* An object containing start up options for the terminal.
* An object containing options for the terminal.
*/
export interface ITerminalOptions {
/**
@@ -48,11 +48,6 @@ declare module 'xterm-headless' {
*/
convertEol?: boolean;
/**
* The number of columns in the terminal.
*/
cols?: number;
/**
* Whether the cursor blinks.
*/
@@ -145,11 +140,6 @@ declare module 'xterm-headless' {
*/
rightClickSelectsWord?: boolean;
/**
* The number of rows in the terminal.
*/
rows?: number;
/**
* Whether screen reader support is enabled. When on this will expose
* supporting elements in the DOM to support NVDA on Windows and VoiceOver
@@ -210,6 +200,22 @@ declare module 'xterm-headless' {
windowOptions?: IWindowOptions;
}
/**
* An object containing additional options for the terminal that can only be
* set on start up.
*/
export interface ITerminalInitOnlyOptions {
/**
* The number of columns in the terminal.
*/
cols?: number;
/**
* The number of rows in the terminal.
*/
rows?: number;
}
/**
* Contains colors to theme the terminal with.
*/
@@ -558,7 +564,7 @@ declare module 'xterm-headless' {
*
* @param options An object containing a set of options.
*/
constructor(options?: ITerminalOptions);
constructor(options?: ITerminalOptions & ITerminalInitOnlyOptions);
/**
* Adds an event listener for when the bell is triggered.