diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index 6eff4ced..6817cb21 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -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; diff --git a/addons/xterm-addon-webgl/src/WebglAddon.ts b/addons/xterm-addon-webgl/src/WebglAddon.ts index 02ab942e..5b98a048 100644 --- a/addons/xterm-addon-webgl/src/WebglAddon.ts +++ b/addons/xterm-addon-webgl/src/WebglAddon.ts @@ -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(); + public get onChangeTextureAtlas(): IEvent { return this._onChangeTextureAtlas.event; } private _onContextLoss = new EventEmitter(); public get onContextLoss(): IEvent { 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); } diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 6616db6e..9e90df02 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -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(); + public get onChangeTextureAtlas(): IEvent { return this._onChangeTextureAtlas.event; } private _onRequestRedraw = new EventEmitter(); public get onRequestRedraw(): IEvent { 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); } diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index cf878dcf..b10fd6ec 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -417,23 +417,23 @@ 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) { @@ -529,7 +529,7 @@ 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 !== ' ') { @@ -550,7 +550,7 @@ export class WebglCharAtlas implements IDisposable { } // Draw the character - if (!drawSuccess) { + if (!customGlyph) { this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight); } @@ -603,7 +603,7 @@ 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); // Find the best atlas row to use @@ -680,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++) { @@ -755,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) } }; } @@ -833,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; -} diff --git a/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts b/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts index ee390d8a..74aed0cc 100644 --- a/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts +++ b/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts @@ -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; + + /** + * An event that is fired when the texture atlas of the renderer changes. + */ + public get onChangeTextureAtlas(): IEvent; + 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; } } diff --git a/demo/client.ts b/demo/client.ts index 1c01a38a..0f35b251 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -239,7 +239,8 @@ function createTerminal(): void { addons.fit.instance!.fit(); 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); term.focus(); @@ -399,20 +400,18 @@ function initOptions(term: TerminalType): void { const input = 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 => { @@ -504,9 +503,7 @@ function initAddons(term: TerminalType): void { addon.instance = new addon.ctor(); term.loadAddon(addon.instance); if (name === 'webgl') { - setTimeout(() => { - document.body.appendChild((addon.instance as WebglAddon).textureAtlas); - }, 0); + (addon.instance as WebglAddon).onChangeTextureAtlas(e => addTextureAtlas(e)); } else if (name === 'unicode11') { term.unicode.activeVersion = '11'; } else if (name === 'search') { @@ -590,6 +587,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'); @@ -688,7 +688,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(''); @@ -701,7 +701,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(''); @@ -714,7 +714,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(''); @@ -727,7 +727,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(''); diff --git a/demo/index.html b/demo/index.html index bff5b4b2..214dd25f 100644 --- a/demo/index.html +++ b/demo/index.html @@ -87,6 +87,7 @@ +