From 1207acc49517e6ecde9128a1b33bef68b1116e56 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 23 Jul 2022 07:05:52 -0700 Subject: [PATCH 01/23] Add underline test button to demo --- demo/client.ts | 18 ++++++++++++++++++ demo/index.html | 1 + 2 files changed, 19 insertions(+) diff --git a/demo/client.ts b/demo/client.ts index 03a9044c..078d97f9 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -161,6 +161,7 @@ if (document.location.pathname === '/test') { document.getElementById('custom-glyph').addEventListener('click', writeCustomGlyphHandler); document.getElementById('load-test').addEventListener('click', loadTest); document.getElementById('powerline-symbol-test').addEventListener('click', powerlineSymbolTest); + document.getElementById('underline-test').addEventListener('click', underlineTest); document.getElementById('add-decoration').addEventListener('click', addDecoration); document.getElementById('add-overview-ruler').addEventListener('click', addOverviewRuler); } @@ -650,6 +651,23 @@ function powerlineSymbolTest() { term.writeln('nf-mdi-github_face (\\uFbd9) \ufbd9'); } +function underlineTest() { + function u(style: number): string { + return `\x1b[4:${style}m`; + } + function c(): string { + return '\x1b[0m'; + } + term.write('\n\n\r'); + term.writeln('Underline styles:'); + term.writeln(`${u(0)}4:0m - No underline`); + term.writeln(`${u(1)}4:1m - Straight`); + term.writeln(`${u(2)}4:2m - Double`); + term.writeln(`${u(3)}4:3m - Curly`); + term.writeln(`${u(4)}4:4m - Dotted`); + term.writeln(`${u(5)}4:5m - Dashed\x1b[0m`); +} + function addDecoration() { term.options['overviewRulerWidth'] = 15; const marker = term.addMarker(1); diff --git a/demo/index.html b/demo/index.html index 23f78aa6..bff5b4b2 100644 --- a/demo/index.html +++ b/demo/index.html @@ -77,6 +77,7 @@
Styles
+
Decorations
From cc8748313e6f294bc936a6aae64ba4810239e880 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 23 Jul 2022 07:13:29 -0700 Subject: [PATCH 02/23] Expose underline attrs as getter/setter In preparation for storing them in a packed format for glyph caching --- src/common/buffer/AttributeData.ts | 27 +++++++++++++++++++++------ src/common/buffer/BufferLine.ts | 2 +- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/common/buffer/AttributeData.ts b/src/common/buffer/AttributeData.ts index 43d378ea..1ee608a7 100644 --- a/src/common/buffer/AttributeData.ts +++ b/src/common/buffer/AttributeData.ts @@ -30,7 +30,7 @@ export class AttributeData implements IAttributeData { // data public fg = 0; public bg = 0; - public extended = new ExtendedAttrs(); + public extended: IExtendedAttrs = new ExtendedAttrs(); // flags public isInverse(): number { return this.fg & FgFlags.INVERSE; } @@ -127,12 +127,27 @@ export class AttributeData implements IAttributeData { * Holds information about different underline styles and color. */ export class ExtendedAttrs implements IExtendedAttrs { + // underline style, NONE is empty + private _underlineStyle: UnderlineStyle = UnderlineStyle.NONE; + public get underlineStyle(): UnderlineStyle { return this._underlineStyle; } + public set underlineStyle(value: UnderlineStyle) { + this._underlineStyle = value; + } + + // underline color, -1 is empty (same as FG) + private _underlineColor: number = -1; + public get underlineColor(): number { return this._underlineColor; } + public set underlineColor(value: number) { + this._underlineColor = value; + } + constructor( - // underline style, NONE is empty - public underlineStyle: UnderlineStyle = UnderlineStyle.NONE, - // underline color, -1 is empty (same as FG) - public underlineColor: number = -1 - ) {} + underlineStyle: UnderlineStyle = UnderlineStyle.NONE, + underlineColor: number = -1 + ) { + this._underlineStyle = underlineStyle; + this._underlineColor = underlineColor; + } public clone(): IExtendedAttrs { return new ExtendedAttrs(this.underlineStyle, this.underlineColor); diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index f0bf4fcb..6d2a442f 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -55,7 +55,7 @@ export const DEFAULT_ATTR_DATA = Object.freeze(new AttributeData()); export class BufferLine implements IBufferLine { protected _data: Uint32Array; protected _combined: {[index: number]: string} = {}; - protected _extendedAttrs: {[index: number]: ExtendedAttrs} = {}; + protected _extendedAttrs: {[index: number]: IExtendedAttrs} = {}; public length: number; constructor(cols: number, fillCellData?: ICellData, public isWrapped: boolean = false) { From 34cd5966b9345398f08ab8ac0500639a1426d41d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 23 Jul 2022 07:31:51 -0700 Subject: [PATCH 03/23] Webgl 3 key cache map --- addons/xterm-addon-webgl/src/GlyphRenderer.ts | 6 +- addons/xterm-addon-webgl/src/Types.d.ts | 2 +- .../src/atlas/WebglCharAtlas.ts | 72 ++++++++++--------- src/common/buffer/Constants.ts | 1 + 4 files changed, 43 insertions(+), 38 deletions(-) diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index 9e60a7fc..28b37c29 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -192,9 +192,11 @@ export class GlyphRenderer extends Disposable { // Get the glyph if (chars && chars.length > 1) { - rasterizedGlyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg); + // TODO: Use actual ext + rasterizedGlyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, 0); } else { - rasterizedGlyph = this._atlas.getRasterizedGlyph(code, bg, fg); + // TODO: Use actual ext + rasterizedGlyph = this._atlas.getRasterizedGlyph(code, bg, fg, 0); } // Fill empty if no glyph was found diff --git a/addons/xterm-addon-webgl/src/Types.d.ts b/addons/xterm-addon-webgl/src/Types.d.ts index d8a27aa7..c803d3e4 100644 --- a/addons/xterm-addon-webgl/src/Types.d.ts +++ b/addons/xterm-addon-webgl/src/Types.d.ts @@ -4,7 +4,7 @@ */ export interface IRasterizedGlyphSet { - [bg: number]: { [fg: number]: IRasterizedGlyph } | undefined; + [bg: number]: { [fg: number]: { [ext: number]: IRasterizedGlyph } } | undefined; } /** diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 05751c42..642c0e83 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -6,12 +6,12 @@ import { ICharAtlasConfig } from './Types'; import { DIM_OPACITY, TEXT_BASELINE } from 'browser/renderer/atlas/Constants'; import { IRasterizedGlyph, IBoundingBox, IRasterizedGlyphSet } from '../Types'; -import { DEFAULT_COLOR, Attributes } from 'common/buffer/Constants'; +import { DEFAULT_COLOR, Attributes, DEFAULT_EXT } from 'common/buffer/Constants'; import { throwIfFalsy } from '../WebglUtils'; import { IColor } from 'common/Types'; import { IDisposable } from 'xterm'; import { AttributeData } from 'common/buffer/AttributeData'; -import { channels, color, rgba } from 'common/Color'; +import { color, rgba } from 'common/Color'; import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs'; import { excludeFromContrastRatioDemands, isPowerlineGlyph } from 'browser/renderer/RendererUtils'; @@ -109,7 +109,9 @@ export class WebglCharAtlas implements IDisposable { const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR); this._cacheMap[i] = { [DEFAULT_COLOR]: { - [DEFAULT_COLOR]: rasterizedGlyph + [DEFAULT_COLOR]: { + [DEFAULT_EXT]: rasterizedGlyph + } } }; } @@ -137,48 +139,50 @@ export class WebglCharAtlas implements IDisposable { this._didWarmUp = false; } - public getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number): IRasterizedGlyph { - let rasterizedGlyphSet = this._cacheMapCombined[chars]; - if (!rasterizedGlyphSet) { - rasterizedGlyphSet = {}; - this._cacheMapCombined[chars] = rasterizedGlyphSet; - } - let rasterizedGlyph: IRasterizedGlyph | undefined; - const rasterizedGlyphSetBg = rasterizedGlyphSet[bg]; - if (rasterizedGlyphSetBg) { - rasterizedGlyph = rasterizedGlyphSetBg[fg]; - } - if (!rasterizedGlyph) { - rasterizedGlyph = this._drawToCache(chars, bg, fg); - if (!rasterizedGlyphSet[bg]) { - rasterizedGlyphSet[bg] = {}; - } - rasterizedGlyphSet[bg]![fg] = rasterizedGlyph; - } - return rasterizedGlyph; + public getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number): IRasterizedGlyph { + return this._getFromCacheMap(this._cacheMapCombined, chars, bg, fg, ext); + } + + public getRasterizedGlyph(code: number, bg: number, fg: number, ext: number): IRasterizedGlyph { + return this._getFromCacheMap(this._cacheMap, code, bg, fg, ext); } /** * Gets the glyphs texture coords, drawing the texture if it's not already */ - public getRasterizedGlyph(code: number, bg: number, fg: number): IRasterizedGlyph { - let rasterizedGlyphSet = this._cacheMap[code]; + private _getFromCacheMap( + cacheMap: { [key: string | number]: IRasterizedGlyphSet }, + key: string | number, + bg: number, + fg: number, + ext: number + ): IRasterizedGlyph { + let rasterizedGlyphSet = cacheMap[key]; if (!rasterizedGlyphSet) { rasterizedGlyphSet = {}; - this._cacheMap[code] = rasterizedGlyphSet; + this._cacheMapCombined[key] = rasterizedGlyphSet; } + + let rasterizedGlyphSetBg = rasterizedGlyphSet[bg]; + if (!rasterizedGlyphSetBg) { + rasterizedGlyphSetBg = {}; + rasterizedGlyphSet[bg] = rasterizedGlyphSetBg; + } + let rasterizedGlyph: IRasterizedGlyph | undefined; - const rasterizedGlyphSetBg = rasterizedGlyphSet[bg]; - if (rasterizedGlyphSetBg) { - rasterizedGlyph = rasterizedGlyphSetBg[fg]; + let rasterizedGlyphSetFg = rasterizedGlyphSetBg[fg]; + if (!rasterizedGlyphSetFg) { + rasterizedGlyphSetFg = {}; + rasterizedGlyphSetBg[fg] = rasterizedGlyphSetFg; + } else { + rasterizedGlyph = rasterizedGlyphSetFg[ext]; } + if (!rasterizedGlyph) { - rasterizedGlyph = this._drawToCache(code, bg, fg); - if (!rasterizedGlyphSet[bg]) { - rasterizedGlyphSet[bg] = {}; - } - rasterizedGlyphSet[bg]![fg] = rasterizedGlyph; + rasterizedGlyph = this._drawToCache(key, bg, fg); + rasterizedGlyphSetFg[ext] = rasterizedGlyph; } + return rasterizedGlyph; } @@ -308,8 +312,6 @@ export class WebglCharAtlas implements IDisposable { return color; } - private _drawToCache(code: number, bg: number, fg: number): IRasterizedGlyph; - private _drawToCache(chars: string, bg: number, fg: number): IRasterizedGlyph; private _drawToCache(codeOrChars: number | string, bg: number, fg: number): IRasterizedGlyph { const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars; diff --git a/src/common/buffer/Constants.ts b/src/common/buffer/Constants.ts index a2c1b884..13dec2c1 100644 --- a/src/common/buffer/Constants.ts +++ b/src/common/buffer/Constants.ts @@ -5,6 +5,7 @@ export const DEFAULT_COLOR = 256; export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0); +export const DEFAULT_EXT = 0; export const CHAR_DATA_ATTR_INDEX = 0; export const CHAR_DATA_CHAR_INDEX = 1; From 8b4e8199d7e67e1b048c102dc29781cd602ea6b2 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 23 Jul 2022 07:38:34 -0700 Subject: [PATCH 04/23] Get underline style affecting webgl rendering --- addons/xterm-addon-webgl/src/GlyphRenderer.ts | 10 +++---- addons/xterm-addon-webgl/src/WebglRenderer.ts | 8 ++++-- .../src/atlas/WebglCharAtlas.ts | 28 +++++++++++++++---- 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index 28b37c29..e679232c 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -169,11 +169,11 @@ export class GlyphRenderer extends Disposable { return this._atlas ? this._atlas.beginFrame() : true; } - public updateCell(x: number, y: number, code: number, bg: number, fg: number, chars: string, lastBg: number): void { - this._updateCell(this._vertices.attributes, x, y, code, bg, fg, chars, lastBg); + public updateCell(x: number, y: number, code: number, bg: number, fg: number, ext: number, chars: string, lastBg: number): void { + this._updateCell(this._vertices.attributes, x, y, code, bg, fg, ext, chars, lastBg); } - private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, bg: number, fg: number, chars: string, lastBg: number): void { + private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, bg: number, fg: number, ext: number, chars: string, lastBg: number): void { const terminal = this._terminal; const i = (y * terminal.cols + x) * INDICES_PER_CELL; @@ -193,10 +193,10 @@ export class GlyphRenderer extends Disposable { // Get the glyph if (chars && chars.length > 1) { // TODO: Use actual ext - rasterizedGlyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, 0); + rasterizedGlyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext); } else { // TODO: Use actual ext - rasterizedGlyph = this._atlas.getRasterizedGlyph(code, bg, fg, 0); + rasterizedGlyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext); } // Fill empty if no glyph was found diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 1e385b33..eb8b8d8d 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -32,7 +32,7 @@ export class WebglRenderer extends Disposable implements IRenderer { private _model: RenderModel = new RenderModel(); private _workCell: CellData = new CellData(); - private _workColors: { fg: number, bg: number } = { fg: 0, bg: 0 }; + private _workColors: { fg: number, bg: number, ext: number } = { fg: 0, bg: 0, ext: 0 }; private _canvas: HTMLCanvasElement; private _gl: IWebGL2RenderingContext; @@ -357,7 +357,7 @@ export class WebglRenderer extends Disposable implements IRenderer { this._model.cells[i + RENDER_MODEL_BG_OFFSET] = this._workColors.bg; this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._workColors.fg; - this._glyphRenderer.updateCell(x, y, code, this._workColors.bg, this._workColors.fg, chars, lastBg); + this._glyphRenderer.updateCell(x, y, code, this._workColors.bg, this._workColors.fg, this._workColors.ext, chars, lastBg); if (isJoined) { // Restore work cell @@ -366,7 +366,7 @@ export class WebglRenderer extends Disposable implements IRenderer { // Null out non-first cells for (x++; x < lastCharX; x++) { const j = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL; - this._glyphRenderer.updateCell(x, y, NULL_CELL_CODE, 0, 0, NULL_CELL_CHAR, 0); + this._glyphRenderer.updateCell(x, y, NULL_CELL_CODE, 0, 0, 0, NULL_CELL_CHAR, 0); this._model.cells[j] = NULL_CELL_CODE; this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._workColors.bg; this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._workColors.fg; @@ -384,6 +384,8 @@ export class WebglRenderer extends Disposable implements IRenderer { private _loadColorsForCell(x: number, y: number): void { this._workColors.bg = this._workCell.bg; this._workColors.fg = this._workCell.fg; + // TODO: Use extended packed format as key + this._workColors.ext = this._workCell.extended.underlineStyle; // Get any foreground/background overrides, this happens on the model to avoid spreading // override logic throughout the different sub-renderers diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 642c0e83..7971fd87 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -6,7 +6,7 @@ import { ICharAtlasConfig } from './Types'; import { DIM_OPACITY, TEXT_BASELINE } from 'browser/renderer/atlas/Constants'; import { IRasterizedGlyph, IBoundingBox, IRasterizedGlyphSet } from '../Types'; -import { DEFAULT_COLOR, Attributes, DEFAULT_EXT } from 'common/buffer/Constants'; +import { DEFAULT_COLOR, Attributes, DEFAULT_EXT, UnderlineStyle } from 'common/buffer/Constants'; import { throwIfFalsy } from '../WebglUtils'; import { IColor } from 'common/Types'; import { IDisposable } from 'xterm'; @@ -106,7 +106,7 @@ export class WebglCharAtlas implements IDisposable { private _doWarmUp(): void { // Pre-fill with ASCII 33-126 for (let i = 33; i < 126; i++) { - const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR); + const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT); this._cacheMap[i] = { [DEFAULT_COLOR]: { [DEFAULT_COLOR]: { @@ -179,7 +179,7 @@ export class WebglCharAtlas implements IDisposable { } if (!rasterizedGlyph) { - rasterizedGlyph = this._drawToCache(key, bg, fg); + rasterizedGlyph = this._drawToCache(key, bg, fg, ext); rasterizedGlyphSetFg[ext] = rasterizedGlyph; } @@ -312,7 +312,7 @@ export class WebglCharAtlas implements IDisposable { return color; } - private _drawToCache(codeOrChars: number | string, bg: number, fg: number): IRasterizedGlyph { + private _drawToCache(codeOrChars: number | string, bg: number, fg: number, ext: number): IRasterizedGlyph { const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars; this.hasCanvasChanged = true; @@ -333,6 +333,8 @@ export class WebglCharAtlas implements IDisposable { this._workAttributeData.fg = fg; this._workAttributeData.bg = bg; + // TODO: Use packed ext format + this._workAttributeData.extended.underlineStyle = ext; const invisible = !!this._workAttributeData.isInvisible(); if (invisible) { @@ -421,8 +423,22 @@ export class WebglCharAtlas implements IDisposable { this._tmpCtx.strokeStyle = this._tmpCtx.fillStyle; this._tmpCtx.beginPath(); if (underline) { - this._tmpCtx.moveTo(padding, padding + this._config.scaledCharHeight - yOffset); - this._tmpCtx.lineTo(padding + this._config.scaledCharWidth, padding + this._config.scaledCharHeight - yOffset); + console.log('underline', this._workAttributeData.extended.underlineStyle); + switch (this._workAttributeData.extended.underlineStyle) { + case UnderlineStyle.DOUBLE: + break; + case UnderlineStyle.CURLY: + break; + case UnderlineStyle.DOTTED: + break; + case UnderlineStyle.DASHED: + break; + case UnderlineStyle.SINGLE: + default: + this._tmpCtx.moveTo(padding, padding + this._config.scaledCharHeight - yOffset); + this._tmpCtx.lineTo(padding + this._config.scaledCharWidth, padding + this._config.scaledCharHeight - yOffset); + break; + } } if (strikethrough) { this._tmpCtx.moveTo(padding, padding + Math.floor(this._config.scaledCharHeight / 2) - yOffset); From c116fa645c67516bfb009070ff8f6c875595bc4e Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 23 Jul 2022 08:23:02 -0700 Subject: [PATCH 05/23] Render underline style on webgl --- .../src/atlas/WebglCharAtlas.ts | 41 +++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 7971fd87..501aec86 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -325,7 +325,7 @@ export class WebglCharAtlas implements IDisposable { this._tmpCanvas.width = allowedWidth; } // Include line height when drawing glyphs - const allowedHeight = this._config.scaledCellHeight + TMP_CANVAS_GLYPH_PADDING * 2; + const allowedHeight = this._config.scaledCellHeight + TMP_CANVAS_GLYPH_PADDING * 4; if (this._tmpCanvas.height < allowedHeight) { this._tmpCanvas.height = allowedHeight; } @@ -386,7 +386,7 @@ export class WebglCharAtlas implements IDisposable { } // For powerline glyphs left/top padding is excluded (https://github.com/microsoft/vscode/issues/120129) - const padding = powerLineGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING; + const padding = powerLineGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING * 2; // Draw custom characters if applicable let drawSuccess = false; @@ -417,26 +417,54 @@ export class WebglCharAtlas implements IDisposable { // Draw underline and strikethrough if (underline || strikethrough) { - const lineWidth = Math.max(1, Math.floor(this._config.fontSize / 10)); + 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 this._tmpCtx.lineWidth = lineWidth; this._tmpCtx.strokeStyle = this._tmpCtx.fillStyle; this._tmpCtx.beginPath(); if (underline) { - console.log('underline', this._workAttributeData.extended.underlineStyle); + const xLeft = padding; + const xRight = padding + this._config.scaledCharWidth; + const yMid = padding + this._config.scaledCharHeight - yOffset; switch (this._workAttributeData.extended.underlineStyle) { case UnderlineStyle.DOUBLE: + const yBot = Math.ceil(padding + this._config.scaledCharHeight + lineWidth) - yOffset; + const yTop = Math.ceil(padding + this._config.scaledCharHeight - lineWidth) - yOffset; + this._tmpCtx.moveTo(xLeft, yTop); + this._tmpCtx.lineTo(xRight, yTop); + this._tmpCtx.moveTo(xLeft, yBot); + this._tmpCtx.lineTo(xRight, yBot); break; case UnderlineStyle.CURLY: + const xMid = padding + this._config.scaledCharWidth / 2; + const yMidBot = Math.ceil(padding + this._config.scaledCharHeight - lineWidth / 2) - yOffset; + const yMidTop = Math.ceil(padding + this._config.scaledCharHeight + lineWidth / 2) - yOffset; + this._tmpCtx.moveTo(xLeft, yMid); + this._tmpCtx.bezierCurveTo( + xLeft, yMidBot, + xMid, yMidBot, + xMid, yMid + ); + this._tmpCtx.bezierCurveTo( + xMid, yMidTop, + xRight, yMidTop, + xRight, yMid + ); break; case UnderlineStyle.DOTTED: + this._tmpCtx.setLineDash([window.devicePixelRatio * 2, window.devicePixelRatio]); + this._tmpCtx.moveTo(xLeft, yMid); + this._tmpCtx.lineTo(xRight, yMid); break; case UnderlineStyle.DASHED: + this._tmpCtx.setLineDash([window.devicePixelRatio * 4, window.devicePixelRatio * 3]); + this._tmpCtx.moveTo(xLeft, yMid); + this._tmpCtx.lineTo(xRight, yMid); break; case UnderlineStyle.SINGLE: default: - this._tmpCtx.moveTo(padding, padding + this._config.scaledCharHeight - yOffset); - this._tmpCtx.lineTo(padding + this._config.scaledCharWidth, padding + this._config.scaledCharHeight - yOffset); + this._tmpCtx.moveTo(xLeft, yMid); + this._tmpCtx.lineTo(xRight, yMid); break; } } @@ -445,7 +473,6 @@ export class WebglCharAtlas implements IDisposable { this._tmpCtx.lineTo(padding + this._config.scaledCharWidth, padding + Math.floor(this._config.scaledCharHeight / 2) - yOffset); } this._tmpCtx.stroke(); - this._tmpCtx.closePath(); } this._tmpCtx.restore(); From baa8a1e6eca336131dba21df584b60cc7b2cb22a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 23 Jul 2022 08:27:15 -0700 Subject: [PATCH 06/23] Stub out canvas renderer underline style --- src/browser/renderer/TextRenderLayer.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/browser/renderer/TextRenderLayer.ts b/src/browser/renderer/TextRenderLayer.ts index ef5a9b62..a6fba850 100644 --- a/src/browser/renderer/TextRenderLayer.ts +++ b/src/browser/renderer/TextRenderLayer.ts @@ -8,7 +8,7 @@ import { CharData, ICellData } from 'common/Types'; import { GridCache } from 'browser/renderer/GridCache'; import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer'; import { AttributeData } from 'common/buffer/AttributeData'; -import { NULL_CELL_CODE, Content } from 'common/buffer/Constants'; +import { NULL_CELL_CODE, Content, UnderlineStyle } from 'common/buffer/Constants'; import { IColorSet } from 'browser/Types'; import { CellData } from 'common/buffer/CellData'; import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services'; @@ -262,7 +262,20 @@ export class TextRenderLayer extends BaseRenderLayer { this._fillMiddleLineAtCells(x, y, cell.getWidth()); } if (cell.isUnderline()) { - this._fillBottomLineAtCells(x, y, cell.getWidth()); + switch (cell.extended.underlineStyle) { + case UnderlineStyle.DOUBLE: + break; + case UnderlineStyle.CURLY: + break; + case UnderlineStyle.DOTTED: + break; + case UnderlineStyle.DASHED: + break; + case UnderlineStyle.SINGLE: + default: + this._fillBottomLineAtCells(x, y, cell.getWidth()); + break; + } } this._ctx.restore(); } From 1d9b4d4778f679d086e3f6bd96ae20a078d9f5a4 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 23 Jul 2022 08:58:10 -0700 Subject: [PATCH 07/23] Canvas renderer underline style --- src/browser/renderer/BaseRenderLayer.ts | 67 ++++++++++++++++++++++++- src/browser/renderer/TextRenderLayer.ts | 5 ++ 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/src/browser/renderer/BaseRenderLayer.ts b/src/browser/renderer/BaseRenderLayer.ts index 0a9b8057..759c3129 100644 --- a/src/browser/renderer/BaseRenderLayer.ts +++ b/src/browser/renderer/BaseRenderLayer.ts @@ -188,14 +188,77 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param x The column to fill. * @param y The row to fill. */ - protected _fillBottomLineAtCells(x: number, y: number, width: number = 1): void { + protected _fillBottomLineAtCells(x: number, y: number, width: number = 1, pixelOffset: number = 0): void { this._ctx.fillRect( x * this._scaledCellWidth, - (y + 1) * this._scaledCellHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */, + (y + 1) * this._scaledCellHeight + pixelOffset - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */, width * this._scaledCellWidth, window.devicePixelRatio); } + protected _curlyUnderlineAtCell(x: number, y: number, width: number = 1): void { + this._ctx.save(); + this._ctx.beginPath(); + this._ctx.strokeStyle = this._ctx.fillStyle; + this._ctx.lineWidth = window.devicePixelRatio; + 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 - window.devicePixelRatio - 1; + const yMidBot = yMid - window.devicePixelRatio; + const yMidTop = yMid + window.devicePixelRatio; + this._ctx.moveTo(xLeft, yMid); + this._ctx.bezierCurveTo( + xLeft, yMidBot, + xMid, yMidBot, + xMid, yMid + ); + this._ctx.bezierCurveTo( + xMid, yMidTop, + xRight, yMidTop, + xRight, yMid + ); + } + this._ctx.stroke(); + this._ctx.restore(); + } + + protected _dottedUnderlineAtCell(x: number, y: number, width: number = 1): void { + this._ctx.save(); + this._ctx.beginPath(); + this._ctx.strokeStyle = this._ctx.fillStyle; + this._ctx.lineWidth = window.devicePixelRatio; + this._ctx.setLineDash([window.devicePixelRatio * 2, window.devicePixelRatio]); + const xLeft = x * this._scaledCellWidth; + const yMid = (y + 1) * this._scaledCellHeight - window.devicePixelRatio - 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; + this._ctx.lineTo(xRight, yMid); + } + this._ctx.stroke(); + this._ctx.closePath(); + this._ctx.restore(); + } + + protected _dashedUnderlineAtCell(x: number, y: number, width: number = 1): void { + this._ctx.save(); + this._ctx.beginPath(); + this._ctx.strokeStyle = this._ctx.fillStyle; + this._ctx.lineWidth = window.devicePixelRatio; + this._ctx.setLineDash([window.devicePixelRatio * 4, window.devicePixelRatio * 3]); + const xLeft = x * this._scaledCellWidth; + const xRight = (x + width) * this._scaledCellWidth; + const yMid = (y + 1) * this._scaledCellHeight - window.devicePixelRatio - 1; + this._ctx.moveTo(xLeft, yMid); + this._ctx.lineTo(xRight, yMid); + this._ctx.stroke(); + this._ctx.closePath(); + this._ctx.restore(); + } + /** * Fills a 1px line (2px on HDPI) at the left of the cell. This uses the * existing fillStyle on the context. diff --git a/src/browser/renderer/TextRenderLayer.ts b/src/browser/renderer/TextRenderLayer.ts index a6fba850..d4173e1f 100644 --- a/src/browser/renderer/TextRenderLayer.ts +++ b/src/browser/renderer/TextRenderLayer.ts @@ -264,12 +264,17 @@ export class TextRenderLayer extends BaseRenderLayer { if (cell.isUnderline()) { switch (cell.extended.underlineStyle) { case UnderlineStyle.DOUBLE: + this._fillBottomLineAtCells(x, y, cell.getWidth(), -window.devicePixelRatio); + this._fillBottomLineAtCells(x, y, cell.getWidth(), window.devicePixelRatio); break; case UnderlineStyle.CURLY: + this._curlyUnderlineAtCell(x, y, cell.getWidth()); break; case UnderlineStyle.DOTTED: + this._dottedUnderlineAtCell(x, y, cell.getWidth()); break; case UnderlineStyle.DASHED: + this._dashedUnderlineAtCell(x, y, cell.getWidth()); break; case UnderlineStyle.SINGLE: default: From 72d18f7d5ea506fd486884eddb97ea87cfabdfcd Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 23 Jul 2022 09:20:53 -0700 Subject: [PATCH 08/23] DOM renderer underline style --- css/xterm.css | 8 +++++--- src/browser/renderer/dom/DomRendererRowFactory.ts | 13 ++++++++----- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/css/xterm.css b/css/xterm.css index 95fc61ed..e9fd8153 100644 --- a/css/xterm.css +++ b/css/xterm.css @@ -163,9 +163,11 @@ opacity: 0.5; } -.xterm-underline { - text-decoration: underline; -} +.xterm-underline-1 { text-decoration: underline; } +.xterm-underline-2 { text-decoration: double underline; } +.xterm-underline-3 { text-decoration: wavy underline; } +.xterm-underline-4 { text-decoration: dotted underline; } +.xterm-underline-5 { text-decoration: dashed underline; } .xterm-strikethrough { text-decoration: line-through; diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index fadf5032..47f4b31c 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -5,7 +5,7 @@ import { IBufferLine, ICellData, IColor } from 'common/Types'; import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; -import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants'; +import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Attributes, UnderlineStyle } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; import { color, rgba } from 'common/Color'; @@ -155,16 +155,19 @@ export class DomRendererRowFactory { charElement.classList.add(DIM_CLASS); } - if (cell.isUnderline()) { - charElement.classList.add(UNDERLINE_CLASS); - } - if (cell.isInvisible()) { charElement.textContent = WHITESPACE_CELL_CHAR; } else { charElement.textContent = cell.getChars() || WHITESPACE_CELL_CHAR; } + if (cell.isUnderline()) { + charElement.classList.add(`${UNDERLINE_CLASS}-${cell.extended.underlineStyle}`); + if (charElement.textContent === ' ') { + charElement.innerHTML = ' '; + } + } + if (cell.isStrikethrough()) { charElement.classList.add(STRIKETHROUGH_CLASS); } From 8184e0824b86127286fd83fe05b6726e22bdc6c9 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 23 Jul 2022 10:13:26 -0700 Subject: [PATCH 09/23] Add tests for dom underline styles --- .../dom/DomRendererRowFactory.test.ts | 66 ++++++++++++++++--- 1 file changed, 57 insertions(+), 9 deletions(-) diff --git a/src/browser/renderer/dom/DomRendererRowFactory.test.ts b/src/browser/renderer/dom/DomRendererRowFactory.test.ts index db5d258e..026d8881 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.test.ts @@ -6,7 +6,7 @@ import jsdom = require('jsdom'); import { assert } from 'chai'; import { DomRendererRowFactory } from 'browser/renderer/dom/DomRendererRowFactory'; -import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, DEFAULT_ATTR, FgFlags, BgFlags, Attributes } from 'common/buffer/Constants'; +import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, DEFAULT_ATTR, FgFlags, BgFlags, Attributes, UnderlineStyle } from 'common/buffer/Constants'; import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { IBufferLine } from 'common/Types'; import { CellData } from 'common/buffer/CellData'; @@ -129,14 +129,62 @@ describe('DomRendererRowFactory', () => { ); }); - it('should add class for underline', () => { - const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); - cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.UNDERLINE; - lineData.setCell(0, cell); - const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20); - assert.equal(getFragmentHtml(fragment), - 'a' - ); + describe('underline', () => { + it('should add class for straight underline style', () => { + const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); + cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.UNDERLINE; + cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED; + cell.extended.underlineStyle = UnderlineStyle.SINGLE; + lineData.setCell(0, cell); + const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20); + assert.equal(getFragmentHtml(fragment), + 'a' + ); + }); + it('should add class for double underline style', () => { + const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); + cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.UNDERLINE; + cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED; + cell.extended.underlineStyle = UnderlineStyle.DOUBLE; + lineData.setCell(0, cell); + const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20); + assert.equal(getFragmentHtml(fragment), + 'a' + ); + }); + it('should add class for curly underline style', () => { + const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); + cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.UNDERLINE; + cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED; + cell.extended.underlineStyle = UnderlineStyle.CURLY; + lineData.setCell(0, cell); + const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20); + assert.equal(getFragmentHtml(fragment), + 'a' + ); + }); + it('should add class for double dotted style', () => { + const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); + cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.UNDERLINE; + cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED; + cell.extended.underlineStyle = UnderlineStyle.DOTTED; + lineData.setCell(0, cell); + const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20); + assert.equal(getFragmentHtml(fragment), + 'a' + ); + }); + it('should add class for dashed underline style', () => { + const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); + cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.UNDERLINE; + cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED; + cell.extended.underlineStyle = UnderlineStyle.DASHED; + lineData.setCell(0, cell); + const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20); + assert.equal(getFragmentHtml(fragment), + 'a' + ); + }); }); it('should add class for strikethrough', () => { From 8287e783a2582f3b6debe728395462581d03f283 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 23 Jul 2022 10:24:51 -0700 Subject: [PATCH 10/23] Underline color canvas --- src/browser/renderer/TextRenderLayer.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/browser/renderer/TextRenderLayer.ts b/src/browser/renderer/TextRenderLayer.ts index d4173e1f..7adb8f85 100644 --- a/src/browser/renderer/TextRenderLayer.ts +++ b/src/browser/renderer/TextRenderLayer.ts @@ -262,6 +262,18 @@ export class TextRenderLayer extends BaseRenderLayer { this._fillMiddleLineAtCells(x, y, cell.getWidth()); } if (cell.isUnderline()) { + const color = cell.extended.underlineColor; + if (!cell.isUnderlineColorDefault()) { + if (cell.isUnderlineColorRGB()) { + this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getUnderlineColor()).join(',')})`; + } else { + let fg = cell.getUnderlineColor(); + if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8) { + fg += 8; + } + this._ctx.fillStyle = this._colors.ansi[fg].css; + } + } switch (cell.extended.underlineStyle) { case UnderlineStyle.DOUBLE: this._fillBottomLineAtCells(x, y, cell.getWidth(), -window.devicePixelRatio); From 1f89223c2a254b5b06008b62120c05ba477c79f9 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 23 Jul 2022 10:26:20 -0700 Subject: [PATCH 11/23] DOM renderer underline color --- src/browser/renderer/TextRenderLayer.ts | 1 - src/browser/renderer/dom/DomRendererRowFactory.ts | 12 ++++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/browser/renderer/TextRenderLayer.ts b/src/browser/renderer/TextRenderLayer.ts index 7adb8f85..4f0dc6df 100644 --- a/src/browser/renderer/TextRenderLayer.ts +++ b/src/browser/renderer/TextRenderLayer.ts @@ -262,7 +262,6 @@ export class TextRenderLayer extends BaseRenderLayer { this._fillMiddleLineAtCells(x, y, cell.getWidth()); } if (cell.isUnderline()) { - const color = cell.extended.underlineColor; if (!cell.isUnderlineColorDefault()) { if (cell.isUnderlineColorRGB()) { this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getUnderlineColor()).join(',')})`; diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index 47f4b31c..d9003332 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -13,6 +13,7 @@ import { IColorSet } from 'browser/Types'; import { ICharacterJoinerService, ISelectionService } from 'browser/services/Services'; import { JoinedCellData } from 'browser/services/CharacterJoinerService'; import { excludeFromContrastRatioDemands } from 'browser/renderer/RendererUtils'; +import { AttributeData } from 'common/buffer/AttributeData'; export const BOLD_CLASS = 'xterm-bold'; export const DIM_CLASS = 'xterm-dim'; @@ -166,6 +167,17 @@ export class DomRendererRowFactory { if (charElement.textContent === ' ') { charElement.innerHTML = ' '; } + if (!cell.isUnderlineColorDefault()) { + if (cell.isUnderlineColorRGB()) { + charElement.style.textDecorationColor = `rgb(${AttributeData.toColorRGB(cell.getUnderlineColor()).join(',')})`; + } else { + let fg = cell.getUnderlineColor(); + if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8) { + fg += 8; + } + charElement.style.textDecorationColor = this._colors.ansi[fg].css; + } + } } if (cell.isStrikethrough()) { From 8cd01dc421489398c90a493f5a0d2a6135f5019f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 23 Jul 2022 10:38:58 -0700 Subject: [PATCH 12/23] Add underline color test to demo --- demo/client.ts | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index 078d97f9..1cfc43e5 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -655,8 +655,8 @@ function underlineTest() { function u(style: number): string { return `\x1b[4:${style}m`; } - function c(): string { - return '\x1b[0m'; + function c(color: string): string { + return `\x1b[58:${color}m`; } term.write('\n\n\r'); term.writeln('Underline styles:'); @@ -666,6 +666,33 @@ function underlineTest() { term.writeln(`${u(3)}4:3m - Curly`); term.writeln(`${u(4)}4:4m - Dotted`); term.writeln(`${u(5)}4:5m - Dashed\x1b[0m`); + term.writeln(''); + term.writeln(`Underline colors (256 color mode):`); + for (let i = 0; i < 256; i++) { + term.write((i !== 0 ? '\x1b[0m, ' : '') + u(1 + i % 5) + c('5:' + i) + i); + } + term.writeln(`\n\n\rUnderline colors (true color mode):`); + term.write('\n\r'); + for (let i = 0; i < 80; i++) { + const v = Math.round(i / 79 * 255); + term.write(u(1) + c(`2:0:${v}:${v}:${v}`) + (i < 4 ? 'grey'[i] : ' ')); + } + term.write('\n\r'); + for (let i = 0; i < 80; i++) { + const v = Math.round(i / 79 * 255); + term.write(u(1) + c(`2:0:${v}:${0}:${0}`) + (i < 3 ? 'red'[i] : ' ')); + } + term.write('\n\r'); + for (let i = 0; i < 80; i++) { + const v = Math.round(i / 79 * 255); + term.write(u(1) + c(`2:0:${0}:${v}:${0}`) + (i < 5 ? 'green'[i] : ' ')); + } + term.write('\n\r'); + for (let i = 0; i < 80; i++) { + const v = Math.round(i / 79 * 255); + term.write(u(1) + c(`2:0:${0}:${0}:${v}`) + (i < 4 ? 'blue'[i] : ' ')); + } + term.write('\x1b[0m\n\r'); } function addDecoration() { From 03f15a7fb77d45741475d516a49e6691009174a4 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 23 Jul 2022 11:12:17 -0700 Subject: [PATCH 13/23] Store extended attributes in single number --- addons/xterm-addon-webgl/src/WebglRenderer.ts | 3 +- .../src/atlas/WebglCharAtlas.ts | 4 +-- src/common/Types.d.ts | 1 + src/common/buffer/AttributeData.ts | 28 +++++++++++-------- src/common/buffer/Constants.ts | 7 +++++ 5 files changed, 28 insertions(+), 15 deletions(-) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index eb8b8d8d..7790b8c2 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -384,8 +384,7 @@ export class WebglRenderer extends Disposable implements IRenderer { private _loadColorsForCell(x: number, y: number): void { this._workColors.bg = this._workCell.bg; this._workColors.fg = this._workCell.fg; - // TODO: Use extended packed format as key - this._workColors.ext = this._workCell.extended.underlineStyle; + this._workColors.ext = this._workCell.extended.ext; // Get any foreground/background overrides, this happens on the model to avoid spreading // override logic throughout the different sub-renderers diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 501aec86..19e7d992 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -333,8 +333,7 @@ export class WebglCharAtlas implements IDisposable { this._workAttributeData.fg = fg; this._workAttributeData.bg = bg; - // TODO: Use packed ext format - this._workAttributeData.extended.underlineStyle = ext; + this._workAttributeData.extended.ext = ext; const invisible = !!this._workAttributeData.isInvisible(); if (invisible) { @@ -423,6 +422,7 @@ export class WebglCharAtlas implements IDisposable { this._tmpCtx.strokeStyle = this._tmpCtx.fillStyle; this._tmpCtx.beginPath(); if (underline) { + console.log('ext', ext, this._workAttributeData.extended.underlineColor, this._workAttributeData.extended.underlineStyle); const xLeft = padding; const xRight = padding + this._config.scaledCharWidth; const yMid = padding + this._config.scaledCharHeight - yOffset; diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index ed9a7124..56815da0 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -113,6 +113,7 @@ export interface IColor { export type IColorRGB = [number, number, number]; export interface IExtendedAttrs { + ext: number; underlineStyle: number; underlineColor: number; clone(): IExtendedAttrs; diff --git a/src/common/buffer/AttributeData.ts b/src/common/buffer/AttributeData.ts index 1ee608a7..6878069a 100644 --- a/src/common/buffer/AttributeData.ts +++ b/src/common/buffer/AttributeData.ts @@ -4,7 +4,7 @@ */ import { IAttributeData, IColorRGB, IExtendedAttrs } from 'common/Types'; -import { Attributes, FgFlags, BgFlags, UnderlineStyle } from 'common/buffer/Constants'; +import { Attributes, FgFlags, BgFlags, UnderlineStyle, ExtFlags } from 'common/buffer/Constants'; export class AttributeData implements IAttributeData { public static toColorRGB(value: number): IColorRGB { @@ -127,26 +127,32 @@ export class AttributeData implements IAttributeData { * Holds information about different underline styles and color. */ export class ExtendedAttrs implements IExtendedAttrs { - // underline style, NONE is empty - private _underlineStyle: UnderlineStyle = UnderlineStyle.NONE; - public get underlineStyle(): UnderlineStyle { return this._underlineStyle; } + private _ext: number = 0; + public get ext(): number { return this._ext; } + public set ext(value: number) { this._ext = value; } + + public get underlineStyle(): UnderlineStyle { + return (this._ext & ExtFlags.UNDERLINE_STYLE) >> 26; + } public set underlineStyle(value: UnderlineStyle) { - this._underlineStyle = value; + this._ext &= ~ExtFlags.UNDERLINE_STYLE; + this._ext |= (value << 26) & ExtFlags.UNDERLINE_STYLE; } - // underline color, -1 is empty (same as FG) - private _underlineColor: number = -1; - public get underlineColor(): number { return this._underlineColor; } + public get underlineColor(): number { + return this._ext & (Attributes.CM_MASK | Attributes.RGB_MASK); + } public set underlineColor(value: number) { - this._underlineColor = value; + this._ext &= ~(Attributes.CM_MASK | Attributes.RGB_MASK); + this._ext |= value & (Attributes.CM_MASK | Attributes.RGB_MASK); } constructor( underlineStyle: UnderlineStyle = UnderlineStyle.NONE, underlineColor: number = -1 ) { - this._underlineStyle = underlineStyle; - this._underlineColor = underlineColor; + this.underlineStyle = underlineStyle; + this.underlineColor = underlineColor; } public clone(): IExtendedAttrs { diff --git a/src/common/buffer/Constants.ts b/src/common/buffer/Constants.ts index 13dec2c1..0dfa86fd 100644 --- a/src/common/buffer/Constants.ts +++ b/src/common/buffer/Constants.ts @@ -130,6 +130,13 @@ export const enum BgFlags { HAS_EXTENDED = 0x10000000 } +export const enum ExtFlags { + /** + * bit 27..32 (upper 3 unused) + */ + UNDERLINE_STYLE = 0x1C000000 +} + export const enum UnderlineStyle { NONE = 0, SINGLE = 1, From d9703aa7fe74078622d69506802cfdca0f494a6f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 23 Jul 2022 11:19:35 -0700 Subject: [PATCH 14/23] Webgl underline color rendering --- .../src/atlas/WebglCharAtlas.ts | 117 ++++++++++-------- 1 file changed, 67 insertions(+), 50 deletions(-) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 19e7d992..f8be6a5e 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -414,63 +414,80 @@ export class WebglCharAtlas implements IDisposable { } } - // Draw underline and strikethrough - if (underline || strikethrough) { + // Draw underline + if (strikethrough) { 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 this._tmpCtx.lineWidth = lineWidth; this._tmpCtx.strokeStyle = this._tmpCtx.fillStyle; this._tmpCtx.beginPath(); - if (underline) { - console.log('ext', ext, this._workAttributeData.extended.underlineColor, this._workAttributeData.extended.underlineStyle); - const xLeft = padding; - const xRight = padding + this._config.scaledCharWidth; - const yMid = padding + this._config.scaledCharHeight - yOffset; - switch (this._workAttributeData.extended.underlineStyle) { - case UnderlineStyle.DOUBLE: - const yBot = Math.ceil(padding + this._config.scaledCharHeight + lineWidth) - yOffset; - const yTop = Math.ceil(padding + this._config.scaledCharHeight - lineWidth) - yOffset; - this._tmpCtx.moveTo(xLeft, yTop); - this._tmpCtx.lineTo(xRight, yTop); - this._tmpCtx.moveTo(xLeft, yBot); - this._tmpCtx.lineTo(xRight, yBot); - break; - case UnderlineStyle.CURLY: - const xMid = padding + this._config.scaledCharWidth / 2; - const yMidBot = Math.ceil(padding + this._config.scaledCharHeight - lineWidth / 2) - yOffset; - const yMidTop = Math.ceil(padding + this._config.scaledCharHeight + lineWidth / 2) - yOffset; - this._tmpCtx.moveTo(xLeft, yMid); - this._tmpCtx.bezierCurveTo( - xLeft, yMidBot, - xMid, yMidBot, - xMid, yMid - ); - this._tmpCtx.bezierCurveTo( - xMid, yMidTop, - xRight, yMidTop, - xRight, yMid - ); - break; - case UnderlineStyle.DOTTED: - this._tmpCtx.setLineDash([window.devicePixelRatio * 2, window.devicePixelRatio]); - this._tmpCtx.moveTo(xLeft, yMid); - this._tmpCtx.lineTo(xRight, yMid); - break; - case UnderlineStyle.DASHED: - this._tmpCtx.setLineDash([window.devicePixelRatio * 4, window.devicePixelRatio * 3]); - this._tmpCtx.moveTo(xLeft, yMid); - this._tmpCtx.lineTo(xRight, yMid); - break; - case UnderlineStyle.SINGLE: - default: - this._tmpCtx.moveTo(xLeft, yMid); - this._tmpCtx.lineTo(xRight, yMid); - break; + this._tmpCtx.moveTo(padding, padding + Math.floor(this._config.scaledCharHeight / 2) - yOffset); + this._tmpCtx.lineTo(padding + this._config.scaledCharWidth, padding + Math.floor(this._config.scaledCharHeight / 2) - yOffset); + this._tmpCtx.stroke(); + } + + // Draw underline + if (underline) { + 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 + this._tmpCtx.lineWidth = lineWidth; + // Underline color + if (this._workAttributeData.isUnderlineColorDefault()) { + this._tmpCtx.strokeStyle = this._tmpCtx.fillStyle; + } else if (this._workAttributeData.isUnderlineColorRGB()) { + this._tmpCtx.strokeStyle = `rgb(${AttributeData.toColorRGB(this._workAttributeData.getUnderlineColor()).join(',')})`; + } else { + let fg = this._workAttributeData.getUnderlineColor(); + if (this._config.drawBoldTextInBrightColors && this._workAttributeData.isBold() && fg < 8) { + fg += 8; } + this._tmpCtx.strokeStyle = this._getColorFromAnsiIndex(fg).css; } - if (strikethrough) { - this._tmpCtx.moveTo(padding, padding + Math.floor(this._config.scaledCharHeight / 2) - yOffset); - this._tmpCtx.lineTo(padding + this._config.scaledCharWidth, padding + Math.floor(this._config.scaledCharHeight / 2) - yOffset); + // Underline style/stroke + this._tmpCtx.beginPath(); + const xLeft = padding; + const xRight = padding + this._config.scaledCharWidth; + const yMid = padding + this._config.scaledCharHeight - yOffset; + switch (this._workAttributeData.extended.underlineStyle) { + case UnderlineStyle.DOUBLE: + const yBot = Math.ceil(padding + this._config.scaledCharHeight + lineWidth) - yOffset; + const yTop = Math.ceil(padding + this._config.scaledCharHeight - lineWidth) - yOffset; + this._tmpCtx.moveTo(xLeft, yTop); + this._tmpCtx.lineTo(xRight, yTop); + this._tmpCtx.moveTo(xLeft, yBot); + this._tmpCtx.lineTo(xRight, yBot); + break; + case UnderlineStyle.CURLY: + const xMid = padding + this._config.scaledCharWidth / 2; + const yMidBot = Math.ceil(padding + this._config.scaledCharHeight - lineWidth / 2) - yOffset; + const yMidTop = Math.ceil(padding + this._config.scaledCharHeight + lineWidth / 2) - yOffset; + this._tmpCtx.moveTo(xLeft, yMid); + this._tmpCtx.bezierCurveTo( + xLeft, yMidBot, + xMid, yMidBot, + xMid, yMid + ); + this._tmpCtx.bezierCurveTo( + xMid, yMidTop, + xRight, yMidTop, + xRight, yMid + ); + break; + case UnderlineStyle.DOTTED: + this._tmpCtx.setLineDash([window.devicePixelRatio * 2, window.devicePixelRatio]); + this._tmpCtx.moveTo(xLeft, yMid); + this._tmpCtx.lineTo(xRight, yMid); + break; + case UnderlineStyle.DASHED: + this._tmpCtx.setLineDash([window.devicePixelRatio * 4, window.devicePixelRatio * 3]); + this._tmpCtx.moveTo(xLeft, yMid); + this._tmpCtx.lineTo(xRight, yMid); + break; + case UnderlineStyle.SINGLE: + default: + this._tmpCtx.moveTo(xLeft, yMid); + this._tmpCtx.lineTo(xRight, yMid); + break; } this._tmpCtx.stroke(); } From 70b5e0249c8f2cca87a44235bf1d9fecbe43a33c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 23 Jul 2022 11:22:40 -0700 Subject: [PATCH 15/23] Underline demo formatting --- demo/client.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index 1cfc43e5..1e17a922 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -660,6 +660,7 @@ function underlineTest() { } term.write('\n\n\r'); term.writeln('Underline styles:'); + term.writeln(''); term.writeln(`${u(0)}4:0m - No underline`); term.writeln(`${u(1)}4:1m - Straight`); term.writeln(`${u(2)}4:2m - Double`); @@ -668,11 +669,12 @@ function underlineTest() { term.writeln(`${u(5)}4:5m - Dashed\x1b[0m`); term.writeln(''); term.writeln(`Underline colors (256 color mode):`); + term.writeln(''); for (let i = 0; i < 256; i++) { term.write((i !== 0 ? '\x1b[0m, ' : '') + u(1 + i % 5) + c('5:' + i) + i); } - term.writeln(`\n\n\rUnderline colors (true color mode):`); - term.write('\n\r'); + term.writeln(`\x1b[0m\n\n\rUnderline colors (true color mode):`); + term.writeln(''); for (let i = 0; i < 80; i++) { const v = Math.round(i / 79 * 255); term.write(u(1) + c(`2:0:${v}:${v}:${v}`) + (i < 4 ? 'grey'[i] : ' ')); From 19fb806214a8a9349054f1e9be26bf906ea35a81 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 23 Jul 2022 13:44:12 -0700 Subject: [PATCH 16/23] Fix tests --- src/browser/renderer/dom/DomRendererRowFactory.test.ts | 10 +++++----- src/common/buffer/BufferLine.test.ts | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/browser/renderer/dom/DomRendererRowFactory.test.ts b/src/browser/renderer/dom/DomRendererRowFactory.test.ts index 026d8881..fdbcea33 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.test.ts @@ -138,7 +138,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + 'a' ); }); it('should add class for double underline style', () => { @@ -149,7 +149,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + 'a' ); }); it('should add class for curly underline style', () => { @@ -160,7 +160,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + 'a' ); }); it('should add class for double dotted style', () => { @@ -171,7 +171,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + 'a' ); }); it('should add class for dashed underline style', () => { @@ -182,7 +182,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + 'a' ); }); }); diff --git a/src/common/buffer/BufferLine.test.ts b/src/common/buffer/BufferLine.test.ts index fa15a854..111aae03 100644 --- a/src/common/buffer/BufferLine.test.ts +++ b/src/common/buffer/BufferLine.test.ts @@ -45,7 +45,7 @@ describe('AttributeData', () => { assert.equal(attrs.getUnderlineColor(), 45); // should use FG color if underlineColor holds no value - attrs.extended.underlineColor = -1; + attrs.extended.underlineColor = 0; attrs.fg |= Attributes.CM_P256 | 123; assert.equal(attrs.getUnderlineColor(), 123); }); @@ -62,7 +62,7 @@ describe('AttributeData', () => { assert.equal(attrs.getUnderlineColor(), (1 << 16) | (2 << 8) | 3); // should use FG color if underlineColor holds no value - attrs.extended.underlineColor = -1; + attrs.extended.underlineColor = 0; attrs.fg |= Attributes.CM_P256 | 123; assert.equal(attrs.getUnderlineColor(), 123); }); From fd8fc183020e78f6377a182fc790a9729bf1c046 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 23 Jul 2022 14:28:15 -0700 Subject: [PATCH 17/23] Clear stroke around text in underline --- .../src/atlas/WebglCharAtlas.ts | 78 +++++++++++-------- demo/client.ts | 1 - 2 files changed, 46 insertions(+), 33 deletions(-) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index f8be6a5e..98852b5e 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -393,38 +393,6 @@ export class WebglCharAtlas implements IDisposable { drawSuccess = tryDrawCustomChar(this._tmpCtx, chars, padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight); } - // Draw the character - if (!drawSuccess) { - this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight); - } - - // If this charcater is underscore and beyond the cell bounds, shift it up until it is visible, - // try for a maximum of 5 pixels. - if (chars === '_' && !this._config.allowTransparency) { - let isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor, foregroundColor, this._config.allowTransparency); - if (isBeyondCellBounds) { - for (let offset = 1; offset <= 5; offset++) { - this._tmpCtx.clearRect(0, 0, this._tmpCanvas.width, this._tmpCanvas.height); - this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight - offset); - isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor, foregroundColor, this._config.allowTransparency); - if (!isBeyondCellBounds) { - break; - } - } - } - } - - // Draw underline - if (strikethrough) { - 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 - this._tmpCtx.lineWidth = lineWidth; - this._tmpCtx.strokeStyle = this._tmpCtx.fillStyle; - this._tmpCtx.beginPath(); - this._tmpCtx.moveTo(padding, padding + Math.floor(this._config.scaledCharHeight / 2) - yOffset); - this._tmpCtx.lineTo(padding + this._config.scaledCharWidth, padding + Math.floor(this._config.scaledCharHeight / 2) - yOffset); - this._tmpCtx.stroke(); - } // Draw underline if (underline) { @@ -490,6 +458,52 @@ export class WebglCharAtlas implements IDisposable { break; } this._tmpCtx.stroke(); + + // Draw stroke in the background color for non custom characters in order to give an outline + // between the text and the underline + if (!drawSuccess) { + // This only works when transparency is disabled because it's not clear how to clear stroked + // text + if (!this._config.allowTransparency) { + // This translates to 1/2 the line width in either direction + this._tmpCtx.lineWidth = window.devicePixelRatio * 3; + this._tmpCtx.strokeStyle = backgroundColor.css; + this._tmpCtx.strokeText(chars, padding, padding + this._config.scaledCharHeight); + } + } + } + + // Draw the character + if (!drawSuccess) { + this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight); + } + + // If this charcater is underscore and beyond the cell bounds, shift it up until it is visible, + // try for a maximum of 5 pixels. + if (chars === '_' && !this._config.allowTransparency) { + let isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor, foregroundColor, this._config.allowTransparency); + if (isBeyondCellBounds) { + for (let offset = 1; offset <= 5; offset++) { + this._tmpCtx.clearRect(0, 0, this._tmpCanvas.width, this._tmpCanvas.height); + this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight - offset); + isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor, foregroundColor, this._config.allowTransparency); + if (!isBeyondCellBounds) { + break; + } + } + } + } + + // Draw strokethrough + if (strikethrough) { + 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 + this._tmpCtx.lineWidth = lineWidth; + this._tmpCtx.strokeStyle = this._tmpCtx.fillStyle; + this._tmpCtx.beginPath(); + this._tmpCtx.moveTo(padding, padding + Math.floor(this._config.scaledCharHeight / 2) - yOffset); + this._tmpCtx.lineTo(padding + this._config.scaledCharWidth, padding + Math.floor(this._config.scaledCharHeight / 2) - yOffset); + this._tmpCtx.stroke(); } this._tmpCtx.restore(); diff --git a/demo/client.ts b/demo/client.ts index 1e17a922..ae7f1968 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -174,7 +174,6 @@ function createTerminal(): void { const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0; term = new Terminal({ - allowTransparency: true, windowsMode: isWindows, fontFamily: 'Fira Code, courier-new, courier, monospace' } as ITerminalOptions); From 43bbea8eb546a6f64026414351eaacd641de32c4 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 23 Jul 2022 14:43:14 -0700 Subject: [PATCH 18/23] Set webgl as default demo renderer --- demo/client.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/demo/client.ts b/demo/client.ts index ae7f1968..4098c775 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -184,6 +184,7 @@ function createTerminal(): void { addons.serialize.instance = new SerializeAddon(); addons.fit.instance = new FitAddon(); addons.unicode11.instance = new Unicode11Addon(); + addons.webgl.instance = new WebglAddon(); // TODO: Remove arguments when link provider API is the default addons['web-links'].instance = new WebLinksAddon(undefined, undefined, true); typedTerm.loadAddon(addons.fit.instance); @@ -208,6 +209,7 @@ function createTerminal(): void { term.open(terminalContainer); addons.fit.instance!.fit(); + typedTerm.loadAddon(addons.webgl.instance); term.focus(); addDomListener(paddingElement, 'change', setPadding); From 759a5928a205336618a27bbf399231fd24d87392 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 23 Jul 2022 14:48:26 -0700 Subject: [PATCH 19/23] Continuous curly underline --- .../src/atlas/WebglCharAtlas.ts | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 98852b5e..1e7d7eda 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -396,6 +396,7 @@ export class WebglCharAtlas implements IDisposable { // 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 this._tmpCtx.lineWidth = lineWidth; @@ -413,13 +414,15 @@ export class WebglCharAtlas implements IDisposable { } // Underline style/stroke this._tmpCtx.beginPath(); + + // TODO: Support letter spacing const xLeft = padding; const xRight = padding + this._config.scaledCharWidth; + const yTop = Math.ceil(padding + this._config.scaledCharHeight - lineWidth) - yOffset; const yMid = padding + this._config.scaledCharHeight - yOffset; + const yBot = Math.ceil(padding + this._config.scaledCharHeight + lineWidth) - yOffset; switch (this._workAttributeData.extended.underlineStyle) { case UnderlineStyle.DOUBLE: - const yBot = Math.ceil(padding + this._config.scaledCharHeight + lineWidth) - yOffset; - const yTop = Math.ceil(padding + this._config.scaledCharHeight - lineWidth) - yOffset; this._tmpCtx.moveTo(xLeft, yTop); this._tmpCtx.lineTo(xRight, yTop); this._tmpCtx.moveTo(xLeft, yBot); @@ -429,7 +432,19 @@ export class WebglCharAtlas implements IDisposable { const xMid = padding + this._config.scaledCharWidth / 2; const yMidBot = Math.ceil(padding + this._config.scaledCharHeight - lineWidth / 2) - yOffset; const yMidTop = Math.ceil(padding + this._config.scaledCharHeight + lineWidth / 2) - yOffset; - this._tmpCtx.moveTo(xLeft, yMid); + // Clip the left and right edges of the underline such that it can be drawn just outside + // the edge of the cell to ensure a continuous stroke when there are multiple underlined + // glyphs adjacent to one another. + const clipRegion = new Path2D(); + clipRegion.rect(xLeft, yTop, this._config.scaledCellWidth, yBot - yTop); + this._tmpCtx.clip(clipRegion); + // Start 1/2 cell before and end 1/2 cells after to ensure a smooth curve with other cells + this._tmpCtx.moveTo(xLeft - this._config.scaledCharWidth / 2, yMid); + this._tmpCtx.bezierCurveTo( + xLeft - this._config.scaledCharWidth / 2, yMidTop, + xLeft, yMidTop, + xLeft, yMid + ); this._tmpCtx.bezierCurveTo( xLeft, yMidBot, xMid, yMidBot, @@ -440,6 +455,11 @@ export class WebglCharAtlas implements IDisposable { xRight, yMidTop, xRight, yMid ); + this._tmpCtx.bezierCurveTo( + xRight, yMidBot, + xRight + this._config.scaledCellWidth / 2, yMidBot, + xRight + this._config.scaledCellWidth / 2, yMid + ); break; case UnderlineStyle.DOTTED: this._tmpCtx.setLineDash([window.devicePixelRatio * 2, window.devicePixelRatio]); @@ -458,6 +478,7 @@ export class WebglCharAtlas implements IDisposable { break; } this._tmpCtx.stroke(); + this._tmpCtx.restore(); // Draw stroke in the background color for non custom characters in order to give an outline // between the text and the underline From 4087284822affa02ab26e20a5723eed040098c0f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 23 Jul 2022 15:19:17 -0700 Subject: [PATCH 20/23] Fix ext caching issues --- addons/xterm-addon-webgl/src/GlyphRenderer.ts | 4 +--- addons/xterm-addon-webgl/src/RenderModel.ts | 3 ++- addons/xterm-addon-webgl/src/WebglRenderer.ts | 12 +++++++----- addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts | 2 +- demo/client.ts | 3 +++ 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index e679232c..6eff4ced 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -185,17 +185,15 @@ export class GlyphRenderer extends Disposable { return; } - let rasterizedGlyph: IRasterizedGlyph; if (!this._atlas) { return; } // Get the glyph + let rasterizedGlyph: IRasterizedGlyph; if (chars && chars.length > 1) { - // TODO: Use actual ext rasterizedGlyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext); } else { - // TODO: Use actual ext rasterizedGlyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext); } diff --git a/addons/xterm-addon-webgl/src/RenderModel.ts b/addons/xterm-addon-webgl/src/RenderModel.ts index b93e1e85..2969a6d1 100644 --- a/addons/xterm-addon-webgl/src/RenderModel.ts +++ b/addons/xterm-addon-webgl/src/RenderModel.ts @@ -6,9 +6,10 @@ import { IRenderModel, ISelectionRenderModel } from './Types'; import { fill } from 'common/TypedArrayUtils'; -export const RENDER_MODEL_INDICIES_PER_CELL = 3; +export const RENDER_MODEL_INDICIES_PER_CELL = 4; export const RENDER_MODEL_BG_OFFSET = 1; export const RENDER_MODEL_FG_OFFSET = 2; +export const RENDER_MODEL_EXT_OFFSET = 3; export const COMBINED_CHAR_BIT_MASK = 0x80000000; diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 7790b8c2..904e8d5a 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -10,9 +10,9 @@ import { acquireCharAtlas } from './atlas/CharAtlasCache'; import { WebglCharAtlas } from './atlas/WebglCharAtlas'; import { RectangleRenderer } from './RectangleRenderer'; import { IWebGL2RenderingContext } from './Types'; -import { RenderModel, COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel'; +import { RenderModel, COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_EXT_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel'; import { Disposable } from 'common/Lifecycle'; -import { Attributes, Content, FgFlags, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants'; +import { Attributes, BgFlags, Content, FgFlags, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants'; import { Terminal, IEvent } from 'xterm'; import { IRenderLayer } from './renderLayer/Types'; import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types'; @@ -343,7 +343,8 @@ export class WebglRenderer extends Disposable implements IRenderer { // Nothing has changed, no updates needed if (this._model.cells[i] === code && this._model.cells[i + RENDER_MODEL_BG_OFFSET] === this._workColors.bg && - this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._workColors.fg) { + this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._workColors.fg && + this._model.cells[i + RENDER_MODEL_EXT_OFFSET] === this._workColors.ext) { continue; } @@ -356,6 +357,7 @@ export class WebglRenderer extends Disposable implements IRenderer { this._model.cells[i] = code; this._model.cells[i + RENDER_MODEL_BG_OFFSET] = this._workColors.bg; this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._workColors.fg; + this._model.cells[i + RENDER_MODEL_EXT_OFFSET] = this._workColors.ext; this._glyphRenderer.updateCell(x, y, code, this._workColors.bg, this._workColors.fg, this._workColors.ext, chars, lastBg); @@ -370,6 +372,7 @@ export class WebglRenderer extends Disposable implements IRenderer { this._model.cells[j] = NULL_CELL_CODE; this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._workColors.bg; this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._workColors.fg; + this._model.cells[j + RENDER_MODEL_EXT_OFFSET] = this._workColors.ext; } } } @@ -384,8 +387,7 @@ export class WebglRenderer extends Disposable implements IRenderer { private _loadColorsForCell(x: number, y: number): void { this._workColors.bg = this._workCell.bg; this._workColors.fg = this._workCell.fg; - this._workColors.ext = this._workCell.extended.ext; - + this._workColors.ext = this._workCell.bg & BgFlags.HAS_EXTENDED ? this._workCell.extended.ext : 0; // Get any foreground/background overrides, this happens on the model to avoid spreading // override logic throughout the different sub-renderers let bgOverride: number | undefined; diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 1e7d7eda..8638eec1 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -160,7 +160,7 @@ export class WebglCharAtlas implements IDisposable { let rasterizedGlyphSet = cacheMap[key]; if (!rasterizedGlyphSet) { rasterizedGlyphSet = {}; - this._cacheMapCombined[key] = rasterizedGlyphSet; + cacheMap[key] = rasterizedGlyphSet; } let rasterizedGlyphSetBg = rasterizedGlyphSet[bg]; diff --git a/demo/client.ts b/demo/client.ts index 4098c775..f8fa7eb5 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -210,6 +210,9 @@ function createTerminal(): void { term.open(terminalContainer); addons.fit.instance!.fit(); typedTerm.loadAddon(addons.webgl.instance); + setTimeout(() => { + document.body.appendChild(addons.webgl.instance.textureAtlas); + }, 0); term.focus(); addDomListener(paddingElement, 'change', setPadding); From 941cc5100d7eb8afcfe80583412ed5d9cc12fc9a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 30 Jul 2022 06:53:34 -0700 Subject: [PATCH 21/23] Increase curly height when dpr <= 1 --- .../src/atlas/WebglCharAtlas.ts | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 93a98c98..48418c48 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -451,8 +451,10 @@ export class WebglCharAtlas implements IDisposable { break; case UnderlineStyle.CURLY: const xMid = padding + this._config.scaledCharWidth / 2; - const yMidBot = Math.ceil(padding + this._config.scaledCharHeight - lineWidth / 2) - yOffset; - const yMidTop = Math.ceil(padding + this._config.scaledCharHeight + lineWidth / 2) - yOffset; + // Choose the bezier top and bottom based on the device pixel ratio, the curly line is + // made taller when the line width is as otherwise it's not very clear otherwise. + const yCurlyBot = lineWidth <= 1 ? yBot : Math.ceil(padding + this._config.scaledCharHeight - lineWidth / 2) - yOffset; + const yCurlyTop = lineWidth <= 1 ? yTop : Math.ceil(padding + this._config.scaledCharHeight + lineWidth / 2) - yOffset; // Clip the left and right edges of the underline such that it can be drawn just outside // the edge of the cell to ensure a continuous stroke when there are multiple underlined // glyphs adjacent to one another. @@ -462,23 +464,23 @@ export class WebglCharAtlas implements IDisposable { // Start 1/2 cell before and end 1/2 cells after to ensure a smooth curve with other cells this._tmpCtx.moveTo(xLeft - this._config.scaledCharWidth / 2, yMid); this._tmpCtx.bezierCurveTo( - xLeft - this._config.scaledCharWidth / 2, yMidTop, - xLeft, yMidTop, + xLeft - this._config.scaledCharWidth / 2, yCurlyTop, + xLeft, yCurlyTop, xLeft, yMid ); this._tmpCtx.bezierCurveTo( - xLeft, yMidBot, - xMid, yMidBot, + xLeft, yCurlyBot, + xMid, yCurlyBot, xMid, yMid ); this._tmpCtx.bezierCurveTo( - xMid, yMidTop, - xRight, yMidTop, + xMid, yCurlyTop, + xRight, yCurlyTop, xRight, yMid ); this._tmpCtx.bezierCurveTo( - xRight, yMidBot, - xRight + this._config.scaledCellWidth / 2, yMidBot, + xRight, yCurlyBot, + xRight + this._config.scaledCellWidth / 2, yCurlyBot, xRight + this._config.scaledCellWidth / 2, yMid ); break; From d0ec9f2591158df9c059d77ef7432c5efc7c3bbc Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 30 Jul 2022 07:05:05 -0700 Subject: [PATCH 22/23] Support letter spacing in webgl --- addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 48418c48..68857853 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -421,6 +421,7 @@ export class WebglCharAtlas implements IDisposable { 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 this._tmpCtx.lineWidth = lineWidth; + // Underline color if (this._workAttributeData.isUnderlineColorDefault()) { this._tmpCtx.strokeStyle = this._tmpCtx.fillStyle; @@ -433,12 +434,11 @@ export class WebglCharAtlas implements IDisposable { } this._tmpCtx.strokeStyle = this._getColorFromAnsiIndex(fg).css; } + // Underline style/stroke this._tmpCtx.beginPath(); - - // TODO: Support letter spacing const xLeft = padding; - const xRight = padding + this._config.scaledCharWidth; + const xRight = padding + this._config.scaledCellWidth; const yTop = Math.ceil(padding + this._config.scaledCharHeight - lineWidth) - yOffset; const yMid = padding + this._config.scaledCharHeight - yOffset; const yBot = Math.ceil(padding + this._config.scaledCharHeight + lineWidth) - yOffset; @@ -450,7 +450,7 @@ export class WebglCharAtlas implements IDisposable { this._tmpCtx.lineTo(xRight, yBot); break; case UnderlineStyle.CURLY: - const xMid = padding + this._config.scaledCharWidth / 2; + const xMid = padding + this._config.scaledCellWidth / 2; // Choose the bezier top and bottom based on the device pixel ratio, the curly line is // made taller when the line width is as otherwise it's not very clear otherwise. const yCurlyBot = lineWidth <= 1 ? yBot : Math.ceil(padding + this._config.scaledCharHeight - lineWidth / 2) - yOffset; @@ -462,9 +462,9 @@ export class WebglCharAtlas implements IDisposable { clipRegion.rect(xLeft, yTop, this._config.scaledCellWidth, yBot - yTop); this._tmpCtx.clip(clipRegion); // Start 1/2 cell before and end 1/2 cells after to ensure a smooth curve with other cells - this._tmpCtx.moveTo(xLeft - this._config.scaledCharWidth / 2, yMid); + this._tmpCtx.moveTo(xLeft - this._config.scaledCellWidth / 2, yMid); this._tmpCtx.bezierCurveTo( - xLeft - this._config.scaledCharWidth / 2, yCurlyTop, + xLeft - this._config.scaledCellWidth / 2, yCurlyTop, xLeft, yCurlyTop, xLeft, yMid ); From f0ddaf49ea3aa6b8995ca7268de507b06e1deeb2 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 30 Jul 2022 07:34:07 -0700 Subject: [PATCH 23/23] Disable threshold check for glyphs with colored underlines This causes some underlines to disappear which looks very bad for colored underlines --- .../src/atlas/WebglCharAtlas.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 68857853..e5f0fe6c 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -414,6 +414,10 @@ export class WebglCharAtlas implements IDisposable { drawSuccess = 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; // Draw underline if (underline) { @@ -426,8 +430,10 @@ export class WebglCharAtlas implements IDisposable { if (this._workAttributeData.isUnderlineColorDefault()) { this._tmpCtx.strokeStyle = this._tmpCtx.fillStyle; } else if (this._workAttributeData.isUnderlineColorRGB()) { + enableClearThresholdCheck = false; this._tmpCtx.strokeStyle = `rgb(${AttributeData.toColorRGB(this._workAttributeData.getUnderlineColor()).join(',')})`; } else { + enableClearThresholdCheck = false; let fg = this._workAttributeData.getUnderlineColor(); if (this._config.drawBoldTextInBrightColors && this._workAttributeData.isBold() && fg < 8) { fg += 8; @@ -508,7 +514,7 @@ export class WebglCharAtlas implements IDisposable { if (!drawSuccess) { // This only works when transparency is disabled because it's not clear how to clear stroked // text - if (!this._config.allowTransparency) { + if (!this._config.allowTransparency && chars !== ' ') { // This translates to 1/2 the line width in either direction this._tmpCtx.lineWidth = window.devicePixelRatio * 3; this._tmpCtx.strokeStyle = backgroundColor.css; @@ -525,12 +531,12 @@ export class WebglCharAtlas implements IDisposable { // If this charcater is underscore and beyond the cell bounds, shift it up until it is visible // even on the bottom row, try for a maximum of 5 pixels. if (chars === '_' && !this._config.allowTransparency) { - let isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor, foregroundColor); + let isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor, foregroundColor, enableClearThresholdCheck); if (isBeyondCellBounds) { for (let offset = 1; offset <= 5; offset++) { this._tmpCtx.clearRect(0, 0, this._tmpCanvas.width, this._tmpCanvas.height); this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight - offset); - isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor, foregroundColor); + isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor, foregroundColor, enableClearThresholdCheck); if (!isBeyondCellBounds) { break; } @@ -561,7 +567,7 @@ export class WebglCharAtlas implements IDisposable { // Clear out the background color and determine if the glyph is empty. let isEmpty: boolean; if (!this._config.allowTransparency) { - isEmpty = clearColor(imageData, backgroundColor, foregroundColor); + isEmpty = clearColor(imageData, backgroundColor, foregroundColor, enableClearThresholdCheck); } else { isEmpty = checkCompletelyTransparent(imageData); } @@ -708,7 +714,7 @@ export class WebglCharAtlas implements IDisposable { * transparent. * @returns True if the result is "empty", meaning all pixels are fully transparent. */ -function clearColor(imageData: ImageData, bg: IColor, fg: IColor): boolean { +function clearColor(imageData: ImageData, bg: IColor, fg: IColor, enableThresholdCheck: boolean): boolean { // Get color channels const r = bg.rgba >>> 24; const g = bg.rgba >>> 16 & 0xFF; @@ -735,7 +741,8 @@ function clearColor(imageData: ImageData, bg: IColor, fg: IColor): boolean { imageData.data[offset + 3] = 0; } else { // Check the threshold based difference - if ((Math.abs(imageData.data[offset] - r) + + if (enableThresholdCheck && + (Math.abs(imageData.data[offset] - r) + Math.abs(imageData.data[offset + 1] - g) + Math.abs(imageData.data[offset + 2] - b)) < threshold) { imageData.data[offset + 3] = 0;