diff --git a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts index ff29bbf1..30f6263c 100644 --- a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts @@ -375,9 +375,9 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer let glyph: IRasterizedGlyph; if (chars && chars.length > 1) { - glyph = this._charAtlas.getRasterizedGlyphCombinedChar(chars, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, true); + glyph = this._charAtlas.getRasterizedGlyphCombinedChar(chars, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, 0, true); } else { - glyph = this._charAtlas.getRasterizedGlyph(cell.getCode() || WHITESPACE_CELL_CODE, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, true); + glyph = this._charAtlas.getRasterizedGlyph(cell.getCode() || WHITESPACE_CELL_CODE, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, 0, true); } if (!glyph.size.x || !glyph.size.y) { return; diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index 269d9374..09523a9e 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -215,15 +215,15 @@ export class GlyphRenderer extends Disposable { } @traceCall - public updateCell(x: number, y: number, code: number, bg: number, fg: number, ext: number, chars: string, lastBg: number): void { + public updateCell(x: number, y: number, code: number, bg: number, fg: number, ext: number, chars: string, lastBg: number, variantOffset: number): void { // Since this function is called for every cell (`rows*cols`), it must be very optimized. It // should not instantiate any variables unless a new glyph is drawn to the cache where the // slight slowdown is acceptable for the developer ergonomics provided as it's a once of for // each glyph. - this._updateCell(this._vertices.attributes, x, y, code, bg, fg, ext, chars, lastBg); + this._updateCell(this._vertices.attributes, x, y, code, bg, fg, ext, chars, lastBg, variantOffset); } - private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, bg: number, fg: number, ext: 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, variantOffset: number): void { $i = (y * this._terminal.cols + x) * INDICES_PER_CELL; // Exit early if this is a null character, allow space character to continue as it may have @@ -239,9 +239,9 @@ export class GlyphRenderer extends Disposable { // Get the glyph if (chars && chars.length > 1) { - $glyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext, false); + $glyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext, variantOffset, false); } else { - $glyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext, false); + $glyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext, variantOffset, false); } $leftCellPadding = Math.floor((this._dimensions.device.cell.width - this._dimensions.device.char.width) / 2); diff --git a/addons/xterm-addon-webgl/src/WebglAddon.ts b/addons/xterm-addon-webgl/src/WebglAddon.ts index 3e091f0b..9e961cc6 100644 --- a/addons/xterm-addon-webgl/src/WebglAddon.ts +++ b/addons/xterm-addon-webgl/src/WebglAddon.ts @@ -8,7 +8,7 @@ import { ITerminal } from 'browser/Types'; import { EventEmitter, forwardEvent } from 'common/EventEmitter'; import { Disposable, toDisposable } from 'common/Lifecycle'; import { getSafariVersion, isSafari } from 'common/Platform'; -import { ICoreService, IDecorationService, ILogService, IOptionsService } from 'common/services/Services'; +import { ICoreService, IDecorationService, ILogService, IOptionsService, IUnicodeService } from 'common/services/Services'; import { ITerminalAddon, Terminal } from 'xterm'; import { WebglRenderer } from './WebglRenderer'; import { setTraceLogger } from 'common/services/LogService'; @@ -54,6 +54,7 @@ export class WebglAddon extends Disposable implements ITerminalAddon { const decorationService: IDecorationService = unsafeCore._decorationService; const logService: ILogService = unsafeCore._logService; const themeService: IThemeService = unsafeCore._themeService; + const unicodeService: IUnicodeService = unsafeCore.unicodeService; // Set trace logger just in case it hasn't been yet which could happen when the addon is // bundled separately to the core module @@ -68,6 +69,7 @@ export class WebglAddon extends Disposable implements ITerminalAddon { decorationService, optionsService, themeService, + unicodeService, this._preserveDrawingBuffer )); this.register(forwardEvent(this._renderer.onContextLoss, this._onContextLoss)); diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index ae770410..2091347c 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -14,10 +14,10 @@ import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IThemeS import { ITerminal } from 'browser/Types'; import { AttributeData } from 'common/buffer/AttributeData'; import { CellData } from 'common/buffer/CellData'; -import { Attributes, Content, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants'; +import { Attributes, Content, ExtFlags, NULL_CELL_CHAR, NULL_CELL_CODE, UnderlineStyle } from 'common/buffer/Constants'; import { EventEmitter, forwardEvent } from 'common/EventEmitter'; import { Disposable, MutableDisposable, getDisposeArrayDisposable, toDisposable } from 'common/Lifecycle'; -import { ICoreService, IDecorationService, ILogService, IOptionsService } from 'common/services/Services'; +import { ICoreService, IDecorationService, ILogService, IOptionsService, IUnicodeService } from 'common/services/Services'; import { CharData, IBufferLine, ICellData } from 'common/Types'; import { IDisposable, Terminal } from 'xterm'; import { GlyphRenderer } from './GlyphRenderer'; @@ -71,6 +71,7 @@ export class WebglRenderer extends Disposable implements IRenderer { private readonly _decorationService: IDecorationService, private readonly _optionsService: IOptionsService, private readonly _themeService: IThemeService, + private readonly _unicodeService: IUnicodeService, preserveDrawingBuffer?: boolean ) { super(); @@ -395,6 +396,7 @@ export class WebglRenderer extends Disposable implements IRenderer { let i: number; let x: number; let j: number; + let variantOffset: number = -1; start = clamp(start, terminal.rows - 1, 0); end = clamp(end, terminal.rows - 1, 0); @@ -409,11 +411,18 @@ export class WebglRenderer extends Disposable implements IRenderer { this._model.cursor = undefined; let modelUpdated = false; + const fontSize = this._optionsService.rawOptions.fontSize; + const drp = this._coreBrowserService.dpr; + const lineWidth = Math.max(1, Math.floor(fontSize * drp / 15)); + const deviceCellWidth = this._charAtlas?.getDeviceCellWidth(); + for (y = start; y <= end; y++) { row = y + terminal.buffer.ydisp; line = terminal.buffer.lines.get(row)!; this._model.lineLengths[y] = 0; joinedRanges = this._characterJoinerService.getJoinedCharacters(row); + // row start variant init + variantOffset = 0; for (x = 0; x < terminal.cols; x++) { lastBg = this._cellColorResolver.result.bg; line.loadCell(x, cell); @@ -447,6 +456,14 @@ export class WebglRenderer extends Disposable implements IRenderer { chars = cell.getChars(); code = cell.getCode(); + + let chWidth: number; + if (typeof code === 'number') { + chWidth = this._unicodeService.wcwidth(code); + } else { + chWidth = this._unicodeService.getStringCellWidth(code); + } + i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL; // Load colors/resolve overrides into work colors @@ -480,13 +497,27 @@ export class WebglRenderer extends Disposable implements IRenderer { if (code !== NULL_CELL_CODE) { this._model.lineLengths[y] = x + 1; + } else { + if (cell.extended.underlineStyle !== UnderlineStyle.DOTTED) { + variantOffset = 0; + } } + this._cellColorResolver.result.ext &= ~ExtFlags.VARIANT_OFFSET; + this._cellColorResolver.result.ext |= (variantOffset << 29) & ExtFlags.VARIANT_OFFSET; + // Nothing has changed, no updates needed if (this._model.cells[i] === code && this._model.cells[i + RENDER_MODEL_BG_OFFSET] === this._cellColorResolver.result.bg && this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._cellColorResolver.result.fg && this._model.cells[i + RENDER_MODEL_EXT_OFFSET] === this._cellColorResolver.result.ext) { + if (cell.extended.underlineStyle === UnderlineStyle.DOTTED) { + if (code !== NULL_CELL_CODE) { + variantOffset = ((deviceCellWidth! * chWidth) - ((lineWidth * 2) - variantOffset)) % (lineWidth * 2); + } + } else { + variantOffset = 0; + } continue; } @@ -503,7 +534,14 @@ export class WebglRenderer extends Disposable implements IRenderer { this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg; this._model.cells[i + RENDER_MODEL_EXT_OFFSET] = this._cellColorResolver.result.ext; - this._glyphRenderer!.updateCell(x, y, code, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, chars, lastBg); + this._glyphRenderer!.updateCell(x, y, code, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, chars, lastBg, variantOffset); + if (cell.extended.underlineStyle === UnderlineStyle.DOTTED) { + if (code !== NULL_CELL_CODE) { + variantOffset = ((deviceCellWidth! * chWidth) - ((lineWidth * 2) - variantOffset)) % (lineWidth * 2); + } + } else { + variantOffset = 0; + } if (isJoined) { // Restore work cell @@ -512,7 +550,7 @@ export class WebglRenderer extends Disposable implements IRenderer { // Null out non-first cells for (x++; x < lastCharX; x++) { j = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL; - this._glyphRenderer!.updateCell(x, y, NULL_CELL_CODE, 0, 0, 0, NULL_CELL_CHAR, 0); + this._glyphRenderer!.updateCell(x, y, NULL_CELL_CODE, 0, 0, 0, NULL_CELL_CHAR, 0, 0); this._model.cells[j] = NULL_CELL_CODE; this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._cellColorResolver.result.bg; this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg; diff --git a/src/browser/renderer/shared/TextureAtlas.ts b/src/browser/renderer/shared/TextureAtlas.ts index 3ee21519..bc8fd2dc 100644 --- a/src/browser/renderer/shared/TextureAtlas.ts +++ b/src/browser/renderer/shared/TextureAtlas.ts @@ -11,7 +11,7 @@ import { color, NULL_COLOR, rgba } from 'common/Color'; import { tryDrawCustomChar } from 'browser/renderer/shared/CustomGlyphs'; import { excludeFromContrastRatioDemands, isPowerlineGlyph, isRestrictedPowerlineGlyph, throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; import { IUnicodeService } from 'common/services/Services'; -import { FourKeyMap } from 'common/MultiKeyMap'; +import { FiveKeyMap } from 'common/MultiKeyMap'; import { IdleTaskQueue } from 'common/TaskQueue'; import { IBoundingBox, ICharAtlasConfig, IRasterizedGlyph, IRequestRedrawEvent, ITextureAtlas } from 'browser/renderer/shared/Types'; import { EventEmitter } from 'common/EventEmitter'; @@ -58,8 +58,8 @@ let $glyph = undefined; export class TextureAtlas implements ITextureAtlas { private _didWarmUp: boolean = false; - private _cacheMap: FourKeyMap = new FourKeyMap(); - private _cacheMapCombined: FourKeyMap = new FourKeyMap(); + private _cacheMap: FiveKeyMap = new FiveKeyMap(); + private _cacheMapCombined: FiveKeyMap = new FiveKeyMap(); // The texture that the atlas is drawn to private _pages: AtlasPage[] = []; @@ -121,9 +121,9 @@ export class TextureAtlas implements ITextureAtlas { const queue = new IdleTaskQueue(); for (let i = 33; i < 126; i++) { queue.enqueue(() => { - if (!this._cacheMap.get(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT)) { - const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT); - this._cacheMap.set(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT, rasterizedGlyph); + if (!this._cacheMap.get(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT, 1)) { + const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT, 0); + this._cacheMap.set(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT, 1, rasterizedGlyph); } }); } @@ -244,29 +244,30 @@ export class TextureAtlas implements ITextureAtlas { } } - public getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number, restrictToCellHeight: boolean): IRasterizedGlyph { - return this._getFromCacheMap(this._cacheMapCombined, chars, bg, fg, ext, restrictToCellHeight); + public getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number, variantOffset: number, restrictToCellHeight: boolean): IRasterizedGlyph { + return this._getFromCacheMap(this._cacheMapCombined, chars, bg, fg, ext, variantOffset, restrictToCellHeight); } - public getRasterizedGlyph(code: number, bg: number, fg: number, ext: number, restrictToCellHeight: boolean): IRasterizedGlyph { - return this._getFromCacheMap(this._cacheMap, code, bg, fg, ext, restrictToCellHeight); + public getRasterizedGlyph(code: number, bg: number, fg: number, ext: number, variantOffset: number, restrictToCellHeight: boolean): IRasterizedGlyph { + return this._getFromCacheMap(this._cacheMap, code, bg, fg, ext, variantOffset, restrictToCellHeight); } /** * Gets the glyphs texture coords, drawing the texture if it's not already */ private _getFromCacheMap( - cacheMap: FourKeyMap, + cacheMap: FiveKeyMap, key: string | number, bg: number, fg: number, ext: number, + variantOffset: number, restrictToCellHeight: boolean = false ): IRasterizedGlyph { - $glyph = cacheMap.get(key, bg, fg, ext); + $glyph = cacheMap.get(key, bg, fg, ext, variantOffset); if (!$glyph) { - $glyph = this._drawToCache(key, bg, fg, ext, restrictToCellHeight); - cacheMap.set(key, bg, fg, ext, $glyph); + $glyph = this._drawToCache(key, bg, fg, ext, variantOffset, restrictToCellHeight); + cacheMap.set(key, bg, fg, ext, variantOffset, $glyph); } return $glyph; } @@ -427,7 +428,7 @@ export class TextureAtlas implements ITextureAtlas { } @traceCall - private _drawToCache(codeOrChars: number | string, bg: number, fg: number, ext: number, restrictToCellHeight: boolean = false): IRasterizedGlyph { + private _drawToCache(codeOrChars: number | string, bg: number, fg: number, ext: number,variantOffset: number = 0, restrictToCellHeight: boolean = false): IRasterizedGlyph { const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars; // Uncomment for debugging @@ -547,6 +548,7 @@ export class TextureAtlas implements ITextureAtlas { const yTop = Math.ceil(padding + this._config.deviceCharHeight) - yOffset - (restrictToCellHeight ? lineWidth * 2 : 0); const yMid = yTop + lineWidth; const yBot = yTop + lineWidth * 2; + let nextOffset = variantOffset; for (let i = 0; i < chWidth; i++) { this._tmpCtx.save(); @@ -596,9 +598,26 @@ export class TextureAtlas implements ITextureAtlas { ); break; case UnderlineStyle.DOTTED: - this._tmpCtx.setLineDash([Math.round(lineWidth), Math.round(lineWidth)]); - this._tmpCtx.moveTo(xChLeft, yTop); - this._tmpCtx.lineTo(xChRight, yTop); + const offsetWidth = nextOffset >= lineWidth ? lineWidth * 2 - nextOffset : lineWidth - nextOffset; + if (offsetWidth === 0) { + this._tmpCtx.setLineDash([Math.round(lineWidth), Math.round(lineWidth)]); + this._tmpCtx.moveTo(xChLeft, yTop); + this._tmpCtx.lineTo(xChRight, yTop); + } else { + const isFull = nextOffset >= lineWidth ? false : true; + if (isFull === false) { + this._tmpCtx.setLineDash([Math.round(lineWidth), Math.round(lineWidth)]); + this._tmpCtx.moveTo(xChLeft + offsetWidth, yTop); + this._tmpCtx.lineTo(xChRight, yTop); + } else { + this._tmpCtx.setLineDash([Math.round(lineWidth), Math.round(lineWidth)]); + this._tmpCtx.moveTo(xChLeft, yTop); + this._tmpCtx.lineTo(xChLeft + offsetWidth, yTop); + this._tmpCtx.moveTo(xChLeft + offsetWidth + lineWidth, yTop); + this._tmpCtx.lineTo(xChRight, yTop); + } + } + nextOffset = (xChRight - xChLeft - ((lineWidth * 2) - nextOffset)) % (Math.round(lineWidth) * 2); break; case UnderlineStyle.DASHED: this._tmpCtx.setLineDash([this._config.devicePixelRatio * 4, this._config.devicePixelRatio * 3]); @@ -940,6 +959,10 @@ export class TextureAtlas implements ITextureAtlas { } }; } + + public getDeviceCellWidth(): number { + return this._config.deviceCellWidth; + } } class AtlasPage { diff --git a/src/browser/renderer/shared/Types.d.ts b/src/browser/renderer/shared/Types.d.ts index a7e55e72..be910e37 100644 --- a/src/browser/renderer/shared/Types.d.ts +++ b/src/browser/renderer/shared/Types.d.ts @@ -103,12 +103,14 @@ export interface ITextureAtlas extends IDisposable { */ beginFrame(): boolean; + getDeviceCellWidth(): number; + /** * Clear all glyphs from the texture atlas. */ clearTexture(): void; - getRasterizedGlyph(code: number, bg: number, fg: number, ext: number, restrictToCellHeight: boolean): IRasterizedGlyph; - getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number, restrictToCellHeight: boolean): IRasterizedGlyph; + getRasterizedGlyph(code: number, bg: number, fg: number, ext: number, variantOffset: number, restrictToCellHeight: boolean): IRasterizedGlyph; + getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number, variantOffset: number, restrictToCellHeight: boolean): IRasterizedGlyph; } /** diff --git a/src/common/MultiKeyMap.ts b/src/common/MultiKeyMap.ts index 6287a8f2..edd5abb5 100644 --- a/src/common/MultiKeyMap.ts +++ b/src/common/MultiKeyMap.ts @@ -40,3 +40,22 @@ export class FourKeyMap { + private _data: { [bg: string | number]: FourKeyMap | undefined } = {}; + + public set(first: TFirst, second: TSecond, third: TThird, fourth: TFourth, five: TFive, value: TValue): void { + if (!this._data[first]) { + this._data[first] = new FourKeyMap(); + } + this._data[first as string | number]!.set(second, third, fourth, five, value); + } + + public get(first: TFirst, second: TSecond, third: TThird, fourth: TFourth, five: TFive): TValue | undefined { + return this._data[first as string | number] ? this._data[first as string | number]?.get(second, third, fourth, five) : undefined; + } + + public clear(): void { + this._data = {}; + } +} diff --git a/src/common/buffer/Constants.ts b/src/common/buffer/Constants.ts index f6a31be7..3bfd850a 100644 --- a/src/common/buffer/Constants.ts +++ b/src/common/buffer/Constants.ts @@ -134,9 +134,14 @@ export const enum BgFlags { export const enum ExtFlags { /** - * bit 27..32 (upper 3 unused) + * bit 27..29 */ - UNDERLINE_STYLE = 0x1C000000 + UNDERLINE_STYLE = 0x1C000000, + + /** + * bit 30..32 + */ + VARIANT_OFFSET = 0xE0000000 } export const enum UnderlineStyle {