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 {