From 309487a020d6ac1d0cbb30d8bd2ef2648092ed2c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 30 Jul 2022 11:07:33 -0700 Subject: [PATCH 1/7] Fix character offset to respect texture padding in webgl Fixes #3974 --- addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts | 12 ++++++------ demo/client.ts | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index e5f0fe6cf1..a7237c484b 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -401,12 +401,12 @@ 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; @@ -577,7 +577,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, padding); const clippedImageData = this._clipImageData(imageData, this._workBoundingBox); // Check if there is enough room in the current row and go to next if needed @@ -610,7 +610,7 @@ 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, padding: number): IRasterizedGlyph { boundingBox.top = 0; const height = restrictedGlyph ? this._config.scaledCellHeight : this._tmpCanvas.height; const width = restrictedGlyph ? this._config.scaledCharWidth : allowedWidth; @@ -685,8 +685,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 ? Math.floor(this._config.letterSpacing / 2) : 0), + y: -boundingBox.top + padding + (restrictedGlyph ? this._config.lineHeight === 1 ? 0 : Math.round((this._config.scaledCellHeight - this._config.scaledCharHeight) / 2) : 0) } }; } diff --git a/demo/client.ts b/demo/client.ts index 1c01a38a2d..dd3b43253b 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -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(''); From 266d79517945f3460ca4623423d43a42f6cfd9ba Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 31 Jul 2022 10:58:04 -0700 Subject: [PATCH 2/7] Fix line height and letter spacing for custom glyphs --- .../src/atlas/WebglCharAtlas.ts | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index a7237c484b..f9ba9d2f25 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -409,15 +409,15 @@ export class WebglCharAtlas implements IDisposable { 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) { @@ -511,7 +511,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 !== ' ') { @@ -524,7 +524,7 @@ export class WebglCharAtlas implements IDisposable { } // Draw the character - if (!drawSuccess) { + if (!customGlyph) { this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight); } @@ -577,7 +577,10 @@ export class WebglCharAtlas implements IDisposable { return NULL_RASTERIZED_GLYPH; } - const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, allowedWidth, powerlineGlyph, padding); + const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, allowedWidth, powerlineGlyph, customGlyph, padding); + if (powerlineGlyph) { + console.log(`powerline glyph ${chars}`, rasterizedGlyph, this._workBoundingBox); + } const clippedImageData = this._clipImageData(imageData, this._workBoundingBox); // Check if there is enough room in the current row and go to next if needed @@ -610,10 +613,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, padding: number): 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 +688,8 @@ export class WebglCharAtlas implements IDisposable { y: (boundingBox.bottom - boundingBox.top + 1) / TEXTURE_HEIGHT }, offset: { - x: -boundingBox.left + padding + (restrictedGlyph ? Math.floor(this._config.letterSpacing / 2) : 0), - y: -boundingBox.top + padding + (restrictedGlyph ? this._config.lineHeight === 1 ? 0 : Math.round((this._config.scaledCellHeight - this._config.scaledCharHeight) / 2) : 0) + x: -boundingBox.left + padding + ((restrictedGlyph || customGlyph) ? Math.round((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 +766,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; -} From 75ca5006a27f98409c3a6ae595039cbf3efee64d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 31 Jul 2022 11:16:12 -0700 Subject: [PATCH 3/7] Expose texture atlas change event and use in demo --- addons/xterm-addon-webgl/src/WebglAddon.ts | 8 ++++++-- addons/xterm-addon-webgl/src/WebglRenderer.ts | 7 ++++++- demo/client.ts | 2 ++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/addons/xterm-addon-webgl/src/WebglAddon.ts b/addons/xterm-addon-webgl/src/WebglAddon.ts index 02ab942e19..5b98a048d7 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 d2d4f478d4..c4d40f0dbd 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -46,6 +46,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 +242,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/demo/client.ts b/demo/client.ts index dd3b43253b..1c4147023c 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -240,6 +240,7 @@ function createTerminal(): void { typedTerm.loadAddon(addons.webgl.instance); setTimeout(() => { document.body.appendChild(addons.webgl.instance.textureAtlas); + addons.webgl.instance.onChangeTextureAtlas(e => document.body.appendChild(e)); }, 0); term.focus(); @@ -506,6 +507,7 @@ function initAddons(term: TerminalType): void { if (name === 'webgl') { setTimeout(() => { document.body.appendChild((addon.instance as WebglAddon).textureAtlas); + (addon.instance as WebglAddon).onChangeTextureAtlas(e => document.body.appendChild(e)); }, 0); } else if (name === 'unicode11') { term.unicode.activeVersion = '11'; From d6fe5c73849a1bf530fdea476984ebd6368c3e66 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 31 Jul 2022 11:21:27 -0700 Subject: [PATCH 4/7] Update size when letter spacing changes --- addons/xterm-addon-webgl/src/WebglRenderer.ts | 1 - .../typings/xterm-addon-webgl.d.ts | 15 ++++++++++----- demo/client.ts | 13 ++++--------- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index c4d40f0dbd..beed030466 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[]; 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 ee390d8ac9..74aed0cc7e 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 1c4147023c..98c60f707a 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -400,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); } + if (['cols', 'rows', 'letterSpacing', 'lineHeight'].includes(o)) { + updateTerminalSize(); }); }); Object.keys(stringOptions).forEach(o => { @@ -505,10 +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); - (addon.instance as WebglAddon).onChangeTextureAtlas(e => document.body.appendChild(e)); - }, 0); + (addon.instance as WebglAddon).onChangeTextureAtlas(e => document.body.appendChild(e)); } else if (name === 'unicode11') { term.unicode.activeVersion = '11'; } else if (name === 'search') { From 208364147ab31efdad082de46d74da3b305d0ad3 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 31 Jul 2022 11:35:12 -0700 Subject: [PATCH 5/7] Allow texture atlas to be zoomed on hover --- demo/client.ts | 10 +++++++--- demo/index.html | 1 + demo/style.css | 14 ++++++++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index 98c60f707a..85355daec2 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -239,8 +239,8 @@ function createTerminal(): void { addons.fit.instance!.fit(); typedTerm.loadAddon(addons.webgl.instance); setTimeout(() => { - document.body.appendChild(addons.webgl.instance.textureAtlas); - addons.webgl.instance.onChangeTextureAtlas(e => document.body.appendChild(e)); + addTextureAtlas(addons.webgl.instance.textureAtlas); + addons.webgl.instance.onChangeTextureAtlas(e => addTextureAtlas(e)); }, 0); term.focus(); @@ -412,6 +412,7 @@ function initOptions(term: TerminalType): void { } if (['cols', 'rows', 'letterSpacing', 'lineHeight'].includes(o)) { updateTerminalSize(); + } }); }); Object.keys(stringOptions).forEach(o => { @@ -503,7 +504,7 @@ function initAddons(term: TerminalType): void { addon.instance = new addon.ctor(); term.loadAddon(addon.instance); if (name === 'webgl') { - (addon.instance as WebglAddon).onChangeTextureAtlas(e => document.body.appendChild(e)); + (addon.instance as WebglAddon).onChangeTextureAtlas(e => addTextureAtlas(e)); } else if (name === 'unicode11') { term.unicode.activeVersion = '11'; } else if (name === 'search') { @@ -587,6 +588,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'); diff --git a/demo/index.html b/demo/index.html index bff5b4b2ba..214dd25f83 100644 --- a/demo/index.html +++ b/demo/index.html @@ -87,6 +87,7 @@

Test

+