From 539fed4daa086a5bc48e863a8a2d3d69c973c2f8 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 20 May 2023 08:13:50 -0700 Subject: [PATCH 01/12] Don't apply dim to background color Fixes #4161 --- addons/xterm-addon-canvas/src/CanvasAddon.ts | 2 +- addons/xterm-addon-canvas/src/TextRenderLayer.ts | 6 ------ addons/xterm-addon-webgl/src/RectangleRenderer.ts | 2 +- css/xterm.css | 4 +++- src/browser/renderer/dom/DomRenderer.ts | 8 +++++++- src/browser/renderer/dom/DomRendererRowFactory.ts | 1 - src/browser/renderer/shared/TextureAtlas.ts | 6 ------ 7 files changed, 12 insertions(+), 17 deletions(-) diff --git a/addons/xterm-addon-canvas/src/CanvasAddon.ts b/addons/xterm-addon-canvas/src/CanvasAddon.ts index b96ce21d..d6136174 100644 --- a/addons/xterm-addon-canvas/src/CanvasAddon.ts +++ b/addons/xterm-addon-canvas/src/CanvasAddon.ts @@ -4,7 +4,7 @@ */ import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services'; -import { IColorSet, ITerminal } from 'browser/Types'; +import { ITerminal } from 'browser/Types'; import { CanvasRenderer } from './CanvasRenderer'; import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; import { ITerminalAddon, Terminal } from 'xterm'; diff --git a/addons/xterm-addon-canvas/src/TextRenderLayer.ts b/addons/xterm-addon-canvas/src/TextRenderLayer.ts index 4ee7c548..0066cc7d 100644 --- a/addons/xterm-addon-canvas/src/TextRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/TextRenderLayer.ts @@ -188,12 +188,6 @@ export class TextRenderLayer extends BaseRenderLayer { nextFillStyle = this._themeService.colors.ansi[cell.getBgColor()].css; } - // Apply dim to the background, this is relatively slow as the CSS is re-parsed but dim is - // rarely used - if (nextFillStyle && cell.isDim()) { - nextFillStyle = color.multiplyOpacity(css.toColor(nextFillStyle), 0.5).css; - } - // Get any decoration foreground/background overrides, this must be fetched before the early // exist but applied after inverse let isTop = false; diff --git a/addons/xterm-addon-webgl/src/RectangleRenderer.ts b/addons/xterm-addon-webgl/src/RectangleRenderer.ts index f45ae3df..fb5a0762 100644 --- a/addons/xterm-addon-webgl/src/RectangleRenderer.ts +++ b/addons/xterm-addon-webgl/src/RectangleRenderer.ts @@ -270,7 +270,7 @@ export class RectangleRenderer extends Disposable { $r = (($rgba >> 24) & 0xFF) / 255; $g = (($rgba >> 16) & 0xFF) / 255; $b = (($rgba >> 8 ) & 0xFF) / 255; - $a = (!$isDefault && bg & BgFlags.DIM) ? DIM_OPACITY : 1; + $a = 1; this._addRectangle(vertices.attributes, offset, $x1, $y1, (endX - startX) * this._dimensions.device.cell.width, this._dimensions.device.cell.height, $r, $g, $b, $a); } diff --git a/css/xterm.css b/css/xterm.css index 2746016b..9527521c 100644 --- a/css/xterm.css +++ b/css/xterm.css @@ -161,7 +161,9 @@ } .xterm-dim { - opacity: 0.5; + /* Dim should not apply to background, so the opacity of the foreground color is applied + * explicitly in the generated class and reset to 1 here */ + opacity: 1 !important; } .xterm-underline-1 { text-decoration: underline; } diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index fe6cba2d..4c59a032 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { BOLD_CLASS, CURSOR_BLINK_CLASS, CURSOR_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory, ITALIC_CLASS } from 'browser/renderer/dom/DomRendererRowFactory'; +import { BOLD_CLASS, CURSOR_BLINK_CLASS, CURSOR_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DIM_CLASS, DomRendererRowFactory, ITALIC_CLASS } from 'browser/renderer/dom/DomRendererRowFactory'; import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants'; import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils'; import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types'; @@ -149,6 +149,10 @@ export class DomRenderer extends Disposable implements IRenderer { ` font-family: ${this._optionsService.rawOptions.fontFamily};` + ` font-size: ${this._optionsService.rawOptions.fontSize}px;` + `}`; + styles += + `${this._terminalSelector} .${ROW_CONTAINER_CLASS} .xterm-dim {` + + ` color: ${color.multiplyOpacity(colors.foreground, 0.5).css};` + + `}`; // Text styles styles += `${this._terminalSelector} span:not(.${BOLD_CLASS}) {` + @@ -224,10 +228,12 @@ export class DomRenderer extends Disposable implements IRenderer { for (const [i, c] of colors.ansi.entries()) { styles += `${this._terminalSelector} .${FG_CLASS_PREFIX}${i} { color: ${c.css}; }` + + `${this._terminalSelector} .${FG_CLASS_PREFIX}${i}.${DIM_CLASS} { color: ${color.multiplyOpacity(c, 0.5).css}; }` + `${this._terminalSelector} .${BG_CLASS_PREFIX}${i} { background-color: ${c.css}; }`; } styles += `${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { color: ${color.opaque(colors.background).css}; }` + + `${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR}.${DIM_CLASS} { color: ${color.multiplyOpacity(color.opaque(colors.background), 0.5).css}; }` + `${this._terminalSelector} .${BG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { background-color: ${colors.foreground.css}; }`; this._themeStyleElement.textContent = styles; diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index cc64a438..ae5e37cb 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -9,7 +9,6 @@ import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/ import { CellData } from 'common/buffer/CellData'; import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; import { color, rgba } from 'common/Color'; -import { IColorSet, ReadonlyColorSet } from 'browser/Types'; import { ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services'; import { JoinedCellData } from 'browser/services/CharacterJoinerService'; import { excludeFromContrastRatioDemands } from 'browser/renderer/shared/RendererUtils'; diff --git a/src/browser/renderer/shared/TextureAtlas.ts b/src/browser/renderer/shared/TextureAtlas.ts index ce168465..e95cdc16 100644 --- a/src/browser/renderer/shared/TextureAtlas.ts +++ b/src/browser/renderer/shared/TextureAtlas.ts @@ -304,12 +304,6 @@ export class TextureAtlas implements ITextureAtlas { break; } - if (dim) { - // Blend here instead of using opacity because transparent colors mess with clipping the - // glyph's bounding box - result = color.blend(this._config.colors.background, color.multiplyOpacity(result, DIM_OPACITY)); - } - return result; } From 5a145fcffea3532303cb512930d65723b7dae6dd Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 20 May 2023 08:41:11 -0700 Subject: [PATCH 02/12] Add webgl api tests for dim --- .../test/WebglRenderer.api.ts | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts index 5e34aede..8b097d23 100644 --- a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts @@ -364,6 +364,55 @@ describe('WebGL Renderer Integration Tests', async () => { } }); + it('foreground 16-255 dim', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\\x1b[2;38;5;${16 + y * 16 + x}m█\x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(page, data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + const cssColor = COLORS_16_TO_255[y * 16 + x]; + const r = parseInt(cssColor.slice(1, 3), 16); + const g = parseInt(cssColor.slice(3, 5), 16); + const b = parseInt(cssColor.slice(5, 7), 16); + // It's difficult to assert the exact color due to rounding, just ensure the color differs + // to the regular color + await pollFor(page, async () => { + const c = await getCellColor(x + 1, y + 1); + return ( + (c[0] === 0 || c[0] !== r) && + (c[1] === 0 || c[1] !== g) && + (c[2] === 0 || c[2] !== b) + ); + }, true); + } + } + }); + + itWebgl('background 16-255 dim', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\\x1b[2;48;5;${16 + y * 16 + x}m \\x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(page, data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + const cssColor = COLORS_16_TO_255[y * 16 + x]; + const r = parseInt(cssColor.slice(1, 3), 16); + const g = parseInt(cssColor.slice(3, 5), 16); + const b = parseInt(cssColor.slice(5, 7), 16); + await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); + } + } + }); + itWebgl('foreground true color red', async () => { let data = ''; for (let y = 0; y < 16; y++) { From d5c47ec531742a128d0beba24e5c53872dce2005 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 20 May 2023 09:35:37 -0700 Subject: [PATCH 03/12] it -> itWebgl --- addons/xterm-addon-webgl/test/WebglRenderer.api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts index 8b097d23..3edfdc33 100644 --- a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts @@ -364,7 +364,7 @@ describe('WebGL Renderer Integration Tests', async () => { } }); - it('foreground 16-255 dim', async () => { + itWebgl('foreground 16-255 dim', async () => { let data = ''; for (let y = 0; y < 240 / 16; y++) { for (let x = 0; x < 16; x++) { From 2bfd95fe541cae2eb90c66d1123c64445b34a19b Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 20 May 2023 09:56:56 -0700 Subject: [PATCH 04/12] Add support for SGR 53, 55 overline Specced in ECMA-48 --- css/xterm.css | 10 +++ demo/client.ts | 7 +- .../dom/DomRendererRowFactory.test.ts | 10 +++ .../renderer/dom/DomRendererRowFactory.ts | 8 ++ src/browser/renderer/shared/TextureAtlas.ts | 74 ++++++++++++++++++- src/common/InputHandler.ts | 6 ++ src/common/Types.d.ts | 1 + src/common/buffer/AttributeData.ts | 1 + src/common/buffer/Constants.ts | 3 +- 9 files changed, 116 insertions(+), 4 deletions(-) diff --git a/css/xterm.css b/css/xterm.css index 2746016b..cd8b449f 100644 --- a/css/xterm.css +++ b/css/xterm.css @@ -170,6 +170,16 @@ .xterm-underline-4 { text-decoration: dotted underline; } .xterm-underline-5 { text-decoration: dashed underline; } +.xterm-overline { + text-decoration: overline; +} + +.xterm-overline.xterm-underline-1 { text-decoration: overline underline; } +.xterm-overline.xterm-underline-2 { text-decoration: overline double underline; } +.xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; } +.xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; } +.xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; } + .xterm-strikethrough { text-decoration: line-through; } diff --git a/demo/client.ts b/demo/client.ts index 68cdec65..df50cf68 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -965,7 +965,9 @@ function sgrTest(): void { { ps: 45, name: 'Background Magenta' }, { ps: 46, name: 'Background Cyan' }, { ps: 47, name: 'Background White' }, - { ps: 49, name: 'Background default' } + { ps: 49, name: 'Background default' }, + { ps: 53, name: 'Overlined' }, + { ps: 55, name: 'Not overlined' } ]; const maxNameLength = entries.reduce((p, c) => Math.max(c.name.length, p), 0); for (const e of entries) { @@ -977,7 +979,8 @@ function sgrTest(): void { } const comboEntries: { ps: number[] }[] = [ { ps: [1, 2, 3, 4, 5, 6, 7, 9] }, - { ps: [2, 41] } + { ps: [2, 41] }, + { ps: [4, 53] } ]; term.write('\n\n\r'); term.writeln(`Combinations`); diff --git a/src/browser/renderer/dom/DomRendererRowFactory.test.ts b/src/browser/renderer/dom/DomRendererRowFactory.test.ts index 5969abff..0c2fb79d 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.test.ts @@ -167,6 +167,16 @@ describe('DomRendererRowFactory', () => { }); }); + it('should add class for overline', () => { + const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); + cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.OVERLINE; + lineData.setCell(0, cell); + const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING); + assert.equal(getFragmentHtml(fragment), + 'a' + ); + }); + it('should add class for strikethrough', () => { const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.STRIKETHROUGH; diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index cc64a438..ff1dd680 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -19,6 +19,7 @@ export const BOLD_CLASS = 'xterm-bold'; export const DIM_CLASS = 'xterm-dim'; export const ITALIC_CLASS = 'xterm-italic'; export const UNDERLINE_CLASS = 'xterm-underline'; +export const OVERLINE_CLASS = 'xterm-overline'; export const STRIKETHROUGH_CLASS = 'xterm-strikethrough'; export const CURSOR_CLASS = 'xterm-cursor'; export const CURSOR_BLINK_CLASS = 'xterm-cursor-blink'; @@ -186,6 +187,13 @@ export class DomRendererRowFactory { } } + if (cell.isOverline()) { + charElement.classList.add(OVERLINE_CLASS); + if (charElement.textContent === ' ') { + charElement.textContent = '\xa0'; // =   + } + } + if (cell.isStrikethrough()) { charElement.classList.add(STRIKETHROUGH_CLASS); } diff --git a/src/browser/renderer/shared/TextureAtlas.ts b/src/browser/renderer/shared/TextureAtlas.ts index ce168465..28236ad4 100644 --- a/src/browser/renderer/shared/TextureAtlas.ts +++ b/src/browser/renderer/shared/TextureAtlas.ts @@ -455,6 +455,7 @@ export class TextureAtlas implements ITextureAtlas { const italic = !!this._workAttributeData.isItalic(); const underline = !!this._workAttributeData.isUnderline(); const strikethrough = !!this._workAttributeData.isStrikethrough(); + const overline = !!this._workAttributeData.isOverline(); let fgColor = this._workAttributeData.getFgColor(); let fgColorMode = this._workAttributeData.getFgColorMode(); let bgColor = this._workAttributeData.getBgColor(); @@ -638,12 +639,83 @@ export class TextureAtlas implements ITextureAtlas { } } + // Overline + if (overline) { + this._tmpCtx.save(); + const lineWidth = Math.max(1, Math.floor(this._config.fontSize * this._config.devicePixelRatio / 15)); + // When the line width is odd, draw at a 0.5 position + const yOffset = lineWidth % 2 === 1 ? 0.5 : 0; + this._tmpCtx.lineWidth = lineWidth; + + // Overline color + 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; + } + this._tmpCtx.strokeStyle = this._getColorFromAnsiIndex(fg).css; + } + + // Overline style/stroke + this._tmpCtx.beginPath(); + const xLeft = padding; + const yTop = padding + yOffset; + const yBot = Math.ceil(padding + lineWidth * 2) + yOffset; + + for (let i = 0; i < chWidth; i++) { + this._tmpCtx.save(); + const xChLeft = xLeft + i * this._config.deviceCellWidth; + const xChRight = xLeft + (i + 1) * this._config.deviceCellWidth; + this._tmpCtx.moveTo(xChLeft, yTop); + this._tmpCtx.lineTo(xChRight, yTop); + this._tmpCtx.stroke(); + this._tmpCtx.restore(); + } + 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. Only do this when font size is >= 12 as the underline + // looks odd when the font size is too small + if (!customGlyph && this._config.fontSize >= 12) { + // This only works when transparency is disabled because it's not clear how to clear stroked + // text + if (!this._config.allowTransparency && chars !== ' ') { + // Measure the text, only draw the stroke if there is a descent beyond an alphabetic text + // baseline + this._tmpCtx.save(); + this._tmpCtx.textBaseline = 'alphabetic'; + const metrics = this._tmpCtx.measureText(chars); + this._tmpCtx.restore(); + if ('actualBoundingBoxDescent' in metrics && metrics.actualBoundingBoxDescent > 0) { + // This translates to 1/2 the line width in either direction + this._tmpCtx.save(); + // Clip the region to only draw in valid pixels near the underline to avoid a slight + // outline around the whole glyph, as well as additional pixels in the glyph at the top + // which would increase GPU memory demands + const clipRegion = new Path2D(); + clipRegion.rect(xLeft, yTop - Math.ceil(lineWidth / 2), this._config.deviceCellWidth * chWidth, yBot - yTop + Math.ceil(lineWidth / 2)); + this._tmpCtx.clip(clipRegion); + this._tmpCtx.lineWidth = this._config.devicePixelRatio * 3; + this._tmpCtx.strokeStyle = backgroundColor.css; + this._tmpCtx.strokeText(chars, padding, padding + this._config.deviceCharHeight); + this._tmpCtx.restore(); + } + } + } + } + // Draw the character if (!customGlyph) { this._tmpCtx.fillText(chars, padding, padding + this._config.deviceCharHeight); } - // If this charcater is underscore and beyond the cell bounds, shift it up until it is visible + // If this character 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.deviceCellWidth, this._config.deviceCellHeight), backgroundColor, foregroundColor, enableClearThresholdCheck); diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index fa65b93b..65942e4d 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -2551,6 +2551,12 @@ export class InputHandler extends Disposable implements IInputHandler { } else if (p === 38 || p === 48 || p === 58) { // fg color 256 and RGB i += this._extractColor(params, i, attr); + } else if (p === 53) { + // overline + attr.bg |= BgFlags.OVERLINE; + } else if (p === 55) { + // not overline + attr.bg &= ~BgFlags.OVERLINE; } else if (p === 59) { attr.extended = attr.extended.clone(); attr.extended.underlineColor = -1; diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index 73471512..70143525 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -165,6 +165,7 @@ export interface IAttributeData { isDim(): number; isStrikethrough(): number; isProtected(): number; + isOverline(): number; /** * The color mode of the foreground color which determines how to decode {@link getFgColor}, diff --git a/src/common/buffer/AttributeData.ts b/src/common/buffer/AttributeData.ts index c9f4cd61..f4d12c2b 100644 --- a/src/common/buffer/AttributeData.ts +++ b/src/common/buffer/AttributeData.ts @@ -47,6 +47,7 @@ export class AttributeData implements IAttributeData { public isDim(): number { return this.bg & BgFlags.DIM; } public isStrikethrough(): number { return this.fg & FgFlags.STRIKETHROUGH; } public isProtected(): number { return this.bg & BgFlags.PROTECTED; } + public isOverline(): number { return this.bg & BgFlags.OVERLINE; } // color modes public getFgColorMode(): number { return this.fg & Attributes.CM_MASK; } diff --git a/src/common/buffer/Constants.ts b/src/common/buffer/Constants.ts index da455794..f6a31be7 100644 --- a/src/common/buffer/Constants.ts +++ b/src/common/buffer/Constants.ts @@ -128,7 +128,8 @@ export const enum BgFlags { ITALIC = 0x4000000, DIM = 0x8000000, HAS_EXTENDED = 0x10000000, - PROTECTED = 0x20000000 + PROTECTED = 0x20000000, + OVERLINE = 0x40000000 } export const enum ExtFlags { From eb56356133c91df39fbdab2e416d30a79d678acd Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 20 May 2023 10:04:14 -0700 Subject: [PATCH 05/12] Don't dodge overline, simplify drawing --- src/browser/renderer/shared/TextureAtlas.ts | 67 ++------------------- 1 file changed, 4 insertions(+), 63 deletions(-) diff --git a/src/browser/renderer/shared/TextureAtlas.ts b/src/browser/renderer/shared/TextureAtlas.ts index 28236ad4..9b1a9f79 100644 --- a/src/browser/renderer/shared/TextureAtlas.ts +++ b/src/browser/renderer/shared/TextureAtlas.ts @@ -641,73 +641,14 @@ export class TextureAtlas implements ITextureAtlas { // Overline if (overline) { - this._tmpCtx.save(); const lineWidth = Math.max(1, Math.floor(this._config.fontSize * this._config.devicePixelRatio / 15)); - // When the line width is odd, draw at a 0.5 position const yOffset = lineWidth % 2 === 1 ? 0.5 : 0; this._tmpCtx.lineWidth = lineWidth; - - // Overline color - 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; - } - this._tmpCtx.strokeStyle = this._getColorFromAnsiIndex(fg).css; - } - - // Overline style/stroke + this._tmpCtx.strokeStyle = this._tmpCtx.fillStyle; this._tmpCtx.beginPath(); - const xLeft = padding; - const yTop = padding + yOffset; - const yBot = Math.ceil(padding + lineWidth * 2) + yOffset; - - for (let i = 0; i < chWidth; i++) { - this._tmpCtx.save(); - const xChLeft = xLeft + i * this._config.deviceCellWidth; - const xChRight = xLeft + (i + 1) * this._config.deviceCellWidth; - this._tmpCtx.moveTo(xChLeft, yTop); - this._tmpCtx.lineTo(xChRight, yTop); - this._tmpCtx.stroke(); - this._tmpCtx.restore(); - } - 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. Only do this when font size is >= 12 as the underline - // looks odd when the font size is too small - if (!customGlyph && this._config.fontSize >= 12) { - // This only works when transparency is disabled because it's not clear how to clear stroked - // text - if (!this._config.allowTransparency && chars !== ' ') { - // Measure the text, only draw the stroke if there is a descent beyond an alphabetic text - // baseline - this._tmpCtx.save(); - this._tmpCtx.textBaseline = 'alphabetic'; - const metrics = this._tmpCtx.measureText(chars); - this._tmpCtx.restore(); - if ('actualBoundingBoxDescent' in metrics && metrics.actualBoundingBoxDescent > 0) { - // This translates to 1/2 the line width in either direction - this._tmpCtx.save(); - // Clip the region to only draw in valid pixels near the underline to avoid a slight - // outline around the whole glyph, as well as additional pixels in the glyph at the top - // which would increase GPU memory demands - const clipRegion = new Path2D(); - clipRegion.rect(xLeft, yTop - Math.ceil(lineWidth / 2), this._config.deviceCellWidth * chWidth, yBot - yTop + Math.ceil(lineWidth / 2)); - this._tmpCtx.clip(clipRegion); - this._tmpCtx.lineWidth = this._config.devicePixelRatio * 3; - this._tmpCtx.strokeStyle = backgroundColor.css; - this._tmpCtx.strokeText(chars, padding, padding + this._config.deviceCharHeight); - this._tmpCtx.restore(); - } - } - } + this._tmpCtx.moveTo(padding, padding + yOffset); + this._tmpCtx.lineTo(padding + this._config.deviceCharWidth * chWidth, padding + yOffset); + this._tmpCtx.stroke(); } // Draw the character From 048010c219c70578578929c3863ccf4733305347 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 21 May 2023 12:28:15 +0200 Subject: [PATCH 06/12] fix buffer corruption for utf8 transport --- demo/server.js | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/demo/server.js b/demo/server.js index 92b82e98..f477ae79 100644 --- a/demo/server.js +++ b/demo/server.js @@ -111,35 +111,32 @@ function startServer() { } // binary message buffering function bufferUtf8(socket, timeout, maxSize) { - const dataBuffer = new Uint8Array(maxSize); - let sender = null; + const chunks = []; let length = 0; + let sender = null; return (data) => { - function flush() { - socket.send(Buffer.from(dataBuffer.buffer, 0, length)); + chunks.push(data); + length += data.length; + if (length > maxSize || userInput) { + userInput = false; + socket.send(Buffer.concat(chunks)); + chunks.length = 0; length = 0; if (sender) { clearTimeout(sender); sender = null; } - } - if (length + data.length > maxSize) { - flush(); - } - dataBuffer.set(data, length); - length += data.length; - if (length > maxSize || userInput) { - userInput = false; - flush(); } else if (!sender) { sender = setTimeout(() => { + socket.send(Buffer.concat(chunks)); + chunks.length = 0; + length = 0; sender = null; - flush(); }, timeout); } }; } - const send = (USE_BINARY ? bufferUtf8 : buffer)(ws, 5, 262144); + const send = (USE_BINARY ? bufferUtf8 : buffer)(ws, 3, 262144); // WARNING: This is a naive implementation that will not throttle the flow of data. This means // it could flood the communication channel and make the terminal unresponsive. Learn more about From f270127ce594d6795b939f9c32a91a337748a04f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 21 May 2023 16:32:57 +0200 Subject: [PATCH 07/12] fix webgl part of #4480 --- addons/xterm-addon-webgl/src/WebglRenderer.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index fcb0dd80..4fc4427f 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -336,13 +336,16 @@ export class WebglRenderer extends Disposable implements IRenderer { } // Tell renderer the frame is beginning + // upon a model clear also refresh the full viewport model + // (also triggered by an atlas page merge, part of #4480) if (this._glyphRenderer.beginFrame()) { this._clearModel(true); + this._updateModel(0, this._terminal.rows - 1); + } else { + // just update changed lines to draw + this._updateModel(start, end); } - // Update model to reflect what's drawn - this._updateModel(start, end); - // Render this._rectangleRenderer?.render(); this._glyphRenderer?.render(this._model); From b662824b81fe50564c78559843a3e39261abece1 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 21 May 2023 08:16:38 -0700 Subject: [PATCH 08/12] Add isOverline to public API Part of #4529 --- typings/xterm-headless.d.ts | 2 ++ typings/xterm.d.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts index 2ceb6a94..741bce85 100644 --- a/typings/xterm-headless.d.ts +++ b/typings/xterm-headless.d.ts @@ -999,6 +999,8 @@ declare module 'xterm-headless' { isInvisible(): number; /** Whether the cell has the strikethrough attribute (CSI 9 m). */ isStrikethrough(): number; + /** Whether the cell has the overline attribute (CSI 53 m). */ + isOverline(): number; /** Whether the cell is using the RGB foreground color mode. */ isFgRGB(): boolean; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 68b37153..6a9be2f1 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -1516,6 +1516,8 @@ declare module 'xterm' { isInvisible(): number; /** Whether the cell has the strikethrough attribute (CSI 9 m). */ isStrikethrough(): number; + /** Whether the cell has the overline attribute (CSI 53 m). */ + isOverline(): number; /** Whether the cell is using the RGB foreground color mode. */ isFgRGB(): boolean; From 325d133a75b17a1079f6cd2aa741e3e77cb542c4 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 21 May 2023 08:16:57 -0700 Subject: [PATCH 09/12] Add overline to vt comments Part of #4529 --- src/common/InputHandler.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 65942e4d..faa5bfde 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -2425,6 +2425,8 @@ export class InputHandler extends Disposable implements IInputHandler { * | 47 | Background color: White. | #Y | * | 48 | Background color: Extended color. | #P[Support for RGB and indexed colors, see below.] | * | 49 | Background color: Default (original). | #Y | + * | 53 | Overlined. | #Y | + * | 55 | Not Overlined. | #Y | * | 58 | Underline color: Extended color. | #P[Support for RGB and indexed colors, see below.] | * | 90 - 97 | Bright foreground color (analogous to 30 - 37). | #Y | * | 100 - 107 | Bright background color (analogous to 40 - 47). | #Y | From 6188c07bf72a9f084a9ce620b0dfd8529ea20b6b Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 21 May 2023 08:17:06 -0700 Subject: [PATCH 10/12] Serialize overline Fixes #4529 --- addons/xterm-addon-serialize/src/SerializeAddon.ts | 6 +++++- .../test/SerializeAddon.api.ts | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index b83ff160..0279fee8 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -77,6 +77,7 @@ function equalFlags(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): bo return cell1.isInverse() === cell2.isInverse() && cell1.isBold() === cell2.isBold() && cell1.isUnderline() === cell2.isUnderline() + && cell1.isOverline() === cell2.isOverline() && cell1.isBlink() === cell2.isBlink() && cell1.isInvisible() === cell2.isInvisible() && cell1.isItalic() === cell2.isItalic() @@ -264,6 +265,7 @@ class StringSerializeHandler extends BaseSerializeHandler { if (cell.isInverse() !== oldCell.isInverse()) { sgrSeq.push(cell.isInverse() ? 7 : 27); } if (cell.isBold() !== oldCell.isBold()) { sgrSeq.push(cell.isBold() ? 1 : 22); } if (cell.isUnderline() !== oldCell.isUnderline()) { sgrSeq.push(cell.isUnderline() ? 4 : 24); } + if (cell.isOverline() !== oldCell.isOverline()) { sgrSeq.push(cell.isOverline() ? 53 : 55); } if (cell.isBlink() !== oldCell.isBlink()) { sgrSeq.push(cell.isBlink() ? 5 : 25); } if (cell.isInvisible() !== oldCell.isInvisible()) { sgrSeq.push(cell.isInvisible() ? 8 : 28); } if (cell.isItalic() !== oldCell.isItalic()) { sgrSeq.push(cell.isItalic() ? 3 : 23); } @@ -625,7 +627,9 @@ export class HTMLSerializeHandler extends BaseSerializeHandler { if (cell.isInverse()) { content.push('color: #000000; background-color: #BFBFBF;'); } if (cell.isBold()) { content.push('font-weight: bold;'); } - if (cell.isUnderline()) { content.push('text-decoration: underline;'); } + if (cell.isUnderline() && cell.isOverline()) { content.push('text-decoration: overline underline;'); } + else if (cell.isUnderline()) { content.push('text-decoration: underline;'); } + else if (cell.isOverline()) { content.push('text-decoration: overline;'); } if (cell.isBlink()) { content.push('text-decoration: blink;'); } if (cell.isInvisible()) { content.push('visibility: hidden;'); } if (cell.isItalic()) { content.push('font-style: italic;'); } diff --git a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts index 157b7072..5128910a 100644 --- a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts +++ b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts @@ -220,6 +220,18 @@ describe('SerializeAddon', () => { assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); }); + it('serialize all rows of content with overline', async () => { + const cols = 10; + const line = '+'.repeat(cols); + const lines: string[] = [ + sgr(OVERLINED) + line, // Overlined + sgr(UNDERLINED) + line, // Overlined, Underlined + sgr(NORMAL) + line // Normal + ]; + await writeSync(page, lines.join('\\r\\n')); + assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + }); + it('serialize all rows of content with color16 and style separately', async function(): Promise { const cols = 10; const line = '+'.repeat(cols); @@ -601,6 +613,7 @@ const BLINK = '5'; const INVERSE = '7'; const INVISIBLE = '8'; const STRIKETHROUGH = '9'; +const OVERLINED = '53'; const NO_BOLD = '22'; const NO_DIM = '22'; @@ -610,3 +623,4 @@ const NO_BLINK = '25'; const NO_INVERSE = '27'; const NO_INVISIBLE = '28'; const NO_STRIKETHROUGH = '29'; +const NO_OVERLINED = '55'; From 02ce3eff87b38eae15d8881c6257edf20a0db7c8 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 21 May 2023 08:51:31 -0700 Subject: [PATCH 11/12] Workaround drawing off invalid bitmap in canvas Fixes #4480 --- addons/xterm-addon-canvas/src/BaseRenderLayer.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts index 88d929f1..3cfdc969 100644 --- a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts @@ -379,7 +379,16 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer } this._ctx.save(); this._clipRow(y); + // Draw the image, use the bitmap if it's available + + // HACK: If the canvas doesn't match, delete the generator. It's not clear how this happens but + // something is wrong with either the lifecycle of _bitmapGenerator or the page canvases are + // swapped out unexpectedly + if (this._bitmapGenerator[glyph.texturePage] && this._charAtlas.pages[glyph.texturePage].canvas !== this._bitmapGenerator[glyph.texturePage]!.canvas) { + delete this._bitmapGenerator[glyph.texturePage]; + } + if (this._charAtlas.pages[glyph.texturePage].version !== this._bitmapGenerator[glyph.texturePage]?.version) { if (!this._bitmapGenerator[glyph.texturePage]) { this._bitmapGenerator[glyph.texturePage] = new BitmapGenerator(this._charAtlas.pages[glyph.texturePage].canvas); @@ -446,11 +455,12 @@ class BitmapGenerator { public get bitmap(): ImageBitmap | undefined { return this._bitmap; } public version: number = -1; - constructor(private readonly _canvas: HTMLCanvasElement) { + constructor(public readonly canvas: HTMLCanvasElement) { } public refresh(): void { // Clear the bitmap immediately as it's stale + this._bitmap?.close(); this._bitmap = undefined; // Disable ImageBitmaps on Safari because of https://bugs.webkit.org/show_bug.cgi?id=149990 if (isSafari) { @@ -466,9 +476,10 @@ class BitmapGenerator { private _generate(): void { if (this._state === BitmapGeneratorState.IDLE) { + this._bitmap?.close(); this._bitmap = undefined; this._state = BitmapGeneratorState.GENERATING; - window.createImageBitmap(this._canvas).then(bitmap => { + window.createImageBitmap(this.canvas).then(bitmap => { if (this._state === BitmapGeneratorState.GENERATING_INVALID) { this.refresh(); } else { From 8b422442b7079525dca8b2a1c6b167856d1d7e7e Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 21 May 2023 09:05:21 -0700 Subject: [PATCH 12/12] Close generator before deleting it --- addons/xterm-addon-canvas/src/BaseRenderLayer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts index 3cfdc969..1d1936e7 100644 --- a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts @@ -386,6 +386,7 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer // something is wrong with either the lifecycle of _bitmapGenerator or the page canvases are // swapped out unexpectedly if (this._bitmapGenerator[glyph.texturePage] && this._charAtlas.pages[glyph.texturePage].canvas !== this._bitmapGenerator[glyph.texturePage]!.canvas) { + this._bitmapGenerator[glyph.texturePage]?.bitmap?.close(); delete this._bitmapGenerator[glyph.texturePage]; }