diff --git a/addons/addon-canvas/src/BaseRenderLayer.ts b/addons/addon-canvas/src/BaseRenderLayer.ts index e7e234003e..d3f0ae6b18 100644 --- a/addons/addon-canvas/src/BaseRenderLayer.ts +++ b/addons/addon-canvas/src/BaseRenderLayer.ts @@ -373,9 +373,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, this._cellColorResolver.result.underlineVariantOffset, 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, this._cellColorResolver.result.underlineVariantOffset, true); } if (!glyph.size.x || !glyph.size.y) { return; diff --git a/addons/addon-webgl/src/GlyphRenderer.ts b/addons/addon-webgl/src/GlyphRenderer.ts index 1fb0e18c54..563272feba 100644 --- a/addons/addon-webgl/src/GlyphRenderer.ts +++ b/addons/addon-webgl/src/GlyphRenderer.ts @@ -212,15 +212,15 @@ export class GlyphRenderer extends Disposable { return this._atlas ? this._atlas.beginFrame() : true; } - 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, underlineVariantOffset: 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, underlineVariantOffset); } - 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, underlineVariantOffset: 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 @@ -236,9 +236,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, underlineVariantOffset, false); } else { - $glyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext, false); + $glyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext, underlineVariantOffset, false); } $leftCellPadding = Math.floor((this._dimensions.device.cell.width - this._dimensions.device.char.width) / 2); diff --git a/addons/addon-webgl/src/WebglRenderer.ts b/addons/addon-webgl/src/WebglRenderer.ts index 2bccc9b7c9..33fa30c861 100644 --- a/addons/addon-webgl/src/WebglRenderer.ts +++ b/addons/addon-webgl/src/WebglRenderer.ts @@ -496,7 +496,7 @@ 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.value!.updateCell(x, y, code, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, chars, lastBg); + this._glyphRenderer.value!.updateCell(x, y, code, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, chars, lastBg, this._cellColorResolver.result.underlineVariantOffset); if (isJoined) { // Restore work cell @@ -505,7 +505,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.value!.updateCell(x, y, NULL_CELL_CODE, 0, 0, 0, NULL_CELL_CHAR, 0); + this._glyphRenderer.value!.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/CellColorResolver.ts b/src/browser/renderer/shared/CellColorResolver.ts index 5837a675b2..f3099acd98 100644 --- a/src/browser/renderer/shared/CellColorResolver.ts +++ b/src/browser/renderer/shared/CellColorResolver.ts @@ -1,10 +1,11 @@ import { ISelectionRenderModel } from 'browser/renderer/shared/Types'; import { ICoreBrowserService, IThemeService } from 'browser/services/Services'; import { ReadonlyColorSet } from 'browser/Types'; -import { Attributes, BgFlags, ExtFlags, FgFlags, NULL_CELL_CODE, UnderlineStyle } from 'common/buffer/Constants'; +import { Attributes, BgFlags, FgFlags, NULL_CELL_CODE, UnderlineStyle } from 'common/buffer/Constants'; import { IDecorationService, IOptionsService } from 'common/services/Services'; import { ICellData } from 'common/Types'; import { Terminal } from '@xterm/xterm'; +import { getCurlyVariantOffset } from 'browser/renderer/shared/RendererUtils'; // Work variables to avoid garbage collection let $fg = 0; @@ -13,17 +14,19 @@ let $hasFg = false; let $hasBg = false; let $isSelected = false; let $colors: ReadonlyColorSet | undefined; -let $variantOffset = 0; +// let $variantOffset = 0; +let $underlineVariantOffset = 0; export class CellColorResolver { /** * The shared result of the {@link resolve} call. This is only safe to use immediately after as * any other calls will share object. */ - public readonly result: { fg: number, bg: number, ext: number } = { + public readonly result: { fg: number, bg: number, ext: number, underlineVariantOffset: number } = { fg: 0, bg: 0, - ext: 0 + ext: 0, + underlineVariantOffset: 0 }; constructor( @@ -54,12 +57,19 @@ export class CellColorResolver { $hasFg = false; $isSelected = false; $colors = this._themeService.colors; - $variantOffset = 0; + // $variantOffset = 0; + $underlineVariantOffset = 0; const code = cell.getCode(); - if (code !== NULL_CELL_CODE && cell.extended.underlineStyle === UnderlineStyle.DOTTED) { + + // Underline handle + if (code !== NULL_CELL_CODE && cell.extended.underlineStyle !== UnderlineStyle.NONE) { const lineWidth = Math.max(1, Math.floor(this._optionService.rawOptions.fontSize * this._coreBrowserService.dpr / 15)); - $variantOffset = x * deviceCellWidth % (Math.round(lineWidth) * 2); + if (cell.extended.underlineStyle === UnderlineStyle.DOTTED) { + $underlineVariantOffset = x * deviceCellWidth % (Math.round(lineWidth) * 2); + } else if (cell.extended.underlineStyle === UnderlineStyle.CURLY) { + $underlineVariantOffset = getCurlyVariantOffset(x, deviceCellWidth, lineWidth); + } } // Apply decorations on the bottom layer @@ -144,7 +154,8 @@ export class CellColorResolver { this.result.fg = $hasFg ? $fg : this.result.fg; // Reset overrides variantOffset - this.result.ext &= ~ExtFlags.VARIANT_OFFSET; - this.result.ext |= ($variantOffset << 29) & ExtFlags.VARIANT_OFFSET; + // this.result.ext &= ~ExtFlags.VARIANT_OFFSET; + // this.result.ext |= ($variantOffset << 29) & ExtFlags.VARIANT_OFFSET; + this.result.underlineVariantOffset = $underlineVariantOffset; } } diff --git a/src/browser/renderer/shared/RendererUtils.test.ts b/src/browser/renderer/shared/RendererUtils.test.ts index a050e8a94f..41e5846ba8 100644 --- a/src/browser/renderer/shared/RendererUtils.test.ts +++ b/src/browser/renderer/shared/RendererUtils.test.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { computeNextVariantOffset } from 'browser/renderer/shared/RendererUtils'; +import { computeNextVariantOffset, createDrawCurlyPlan } from 'browser/renderer/shared/RendererUtils'; import { assert } from 'chai'; describe('RendererUtils', () => { @@ -28,7 +28,7 @@ describe('RendererUtils', () => { line = 2; variantOffset = 0; cells = [cellWidth, cellWidth, doubleCellWidth, doubleCellWidth]; - result = [3, 2, 0 ,2]; + result = [3, 2, 0, 2]; for (let index = 0; index < cells.length; index++) { const cell = cells[index]; variantOffset = computeNextVariantOffset(cell, line, variantOffset); diff --git a/src/browser/renderer/shared/RendererUtils.ts b/src/browser/renderer/shared/RendererUtils.ts index 59b87b0e30..105031fc8f 100644 --- a/src/browser/renderer/shared/RendererUtils.ts +++ b/src/browser/renderer/shared/RendererUtils.ts @@ -3,7 +3,8 @@ * @license MIT */ -import { IDimensions, IRenderDimensions } from 'browser/renderer/shared/Types'; +import { IDimensions, IRenderDimensions, UnderlineCurlyJoinOrLine, UnderlineCurlyLineType, UnderlineDrawCurlyOp } from 'browser/renderer/shared/Types'; +import { TwoKeyMap } from 'common/MultiKeyMap'; export function throwIfFalsy(value: T | undefined | null): T { if (!value) { @@ -60,3 +61,104 @@ function createDimension(): IDimensions { export function computeNextVariantOffset(cellWidth: number, lineWidth: number, currentOffset: number = 0): number { return (cellWidth - (Math.round(lineWidth) * 2 - currentOffset)) % (Math.round(lineWidth) * 2); } + +// TwoKeyMap +const curlyVariantCache = new TwoKeyMap(); + +export function getCurlyVariant(cellWidth: number, lineWidth: number, offset: number): string { + if (curlyVariantCache.get(cellWidth, lineWidth)) { + const curlyVariants = curlyVariantCache.get(cellWidth, lineWidth); + if (curlyVariants && curlyVariants.length > 0) { + if (!curlyVariants[offset]) { + return curlyVariants[0]; + } + return curlyVariants[offset]; + } + } + return ''; +} + +export function getCurlyVariantOffset(x: number, cellWidth: number, lineWidth: number): number { + if (curlyVariantCache.get(cellWidth, lineWidth)) { + const curlyVariants = curlyVariantCache.get(cellWidth, lineWidth) as any[]; + return x % curlyVariants.length; + } + if (!curlyVariantCache.get(cellWidth, lineWidth)) { + const curlyVariants = createDrawCurlyPlan(cellWidth, lineWidth); + curlyVariantCache.set(cellWidth, lineWidth, curlyVariants); + return x % curlyVariants.length; + } + return 0; +} + +const defaultCurlyLinePixels = 3; + +export function createDrawCurlyPlan(cellWidth: number, lineWidth: number): string[] { + return createVariantSequences(cellWidth, lineWidth, defaultCurlyLinePixels + (lineWidth > 1 ? 1 : 0)); +} + +function createVariantSequences(cellWidth: number, joinPixels: number, linePixels: number): string[] { + const result: string[] = []; + let totalPixels = cellWidth * ((joinPixels + linePixels) * 2); + let joinOrLine: UnderlineCurlyJoinOrLine = 'join'; + let upOrDown: UnderlineCurlyLineType = 'up'; + let lastUpOrDown: UnderlineCurlyLineType = 'up'; + // Split between cells to be processed + let waitHandlePixels = 0; + while (totalPixels > 0) { + const cellResult: any[] = []; + let cellCurrentWidth = cellWidth; + while (cellCurrentWidth > 0) { + if (joinOrLine === 'join') { + let token: UnderlineDrawCurlyOp = upOrDown === 'up' ? 'Y' : 'B'; + if (waitHandlePixels > 0) { + // right + token = lastUpOrDown === 'up' ? 'M' : 'P'; + cellResult.push(`${token}${waitHandlePixels}`); + cellCurrentWidth -= waitHandlePixels; + waitHandlePixels = 0; + joinOrLine = 'line'; + } else { + // left + const usingWidth = joinPixels; + if (usingWidth > cellCurrentWidth) { + token = lastUpOrDown === 'up' ? 'Z' : 'Q'; + cellResult.push(`${token}${cellCurrentWidth}`); + waitHandlePixels = usingWidth - cellCurrentWidth; + cellCurrentWidth = 0; + } else { + cellResult.push(`${token}${joinPixels}`); + cellCurrentWidth -= joinPixels; + joinOrLine = 'line'; + } + } + } else if (joinOrLine === 'line') { + const token: UnderlineDrawCurlyOp = upOrDown === 'up' ? 'U' : 'D'; + if (waitHandlePixels > 0) { + cellResult.push(`${token}${waitHandlePixels}`); + cellCurrentWidth -= waitHandlePixels; + waitHandlePixels = 0; + joinOrLine = 'join'; + lastUpOrDown = upOrDown; + upOrDown = upOrDown === 'up' ? 'down' : 'up'; + } else { + const usingWidth = linePixels; + if (usingWidth > cellCurrentWidth) { + cellResult.push(`${token}${cellCurrentWidth}`); + waitHandlePixels = usingWidth - cellCurrentWidth; + cellCurrentWidth = 0; + } else { + cellResult.push(`${token}${linePixels}`); + cellCurrentWidth -= linePixels; + joinOrLine = 'join'; + lastUpOrDown = upOrDown; + upOrDown = upOrDown === 'up' ? 'down' : 'up'; + } + } + } + } + totalPixels -= cellWidth; + result.push(cellResult.join(' ')); + } + return result; +} diff --git a/src/browser/renderer/shared/TextureAtlas.ts b/src/browser/renderer/shared/TextureAtlas.ts index f3f67b8b21..f1544b094b 100644 --- a/src/browser/renderer/shared/TextureAtlas.ts +++ b/src/browser/renderer/shared/TextureAtlas.ts @@ -6,11 +6,11 @@ import { IColorContrastCache } from 'browser/Types'; import { DIM_OPACITY, TEXT_BASELINE } from 'browser/renderer/shared/Constants'; import { tryDrawCustomChar } from 'browser/renderer/shared/CustomGlyphs'; -import { computeNextVariantOffset, excludeFromContrastRatioDemands, isPowerlineGlyph, isRestrictedPowerlineGlyph, throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; -import { IBoundingBox, ICharAtlasConfig, IRasterizedGlyph, ITextureAtlas } from 'browser/renderer/shared/Types'; +import { computeNextVariantOffset, excludeFromContrastRatioDemands, getCurlyVariant, isPowerlineGlyph, isRestrictedPowerlineGlyph, throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; +import { IBoundingBox, ICharAtlasConfig, IRasterizedGlyph, ITextureAtlas, UnderlineDrawCurlyOp } from 'browser/renderer/shared/Types'; import { NULL_COLOR, color, rgba } from 'common/Color'; import { EventEmitter } from 'common/EventEmitter'; -import { FourKeyMap } from 'common/MultiKeyMap'; +import { FiveKeyMap } from 'common/MultiKeyMap'; import { IdleTaskQueue } from 'common/TaskQueue'; import { IColor } from 'common/Types'; import { AttributeData } from 'common/buffer/AttributeData'; @@ -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)) { + if (!this._cacheMap.get(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT, 0)) { const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT); - this._cacheMap.set(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT, rasterizedGlyph); + this._cacheMap.set(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT, 0, rasterizedGlyph); } }); } @@ -242,29 +242,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, underlineVariantOffset: number, restrictToCellHeight: boolean): IRasterizedGlyph { + return this._getFromCacheMap(this._cacheMapCombined, chars, bg, fg, ext, underlineVariantOffset, 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, underlineVariantOffset: number, restrictToCellHeight: boolean): IRasterizedGlyph { + return this._getFromCacheMap(this._cacheMap, code, bg, fg, ext, underlineVariantOffset, 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, + underlineVariantOffset: number, restrictToCellHeight: boolean = false ): IRasterizedGlyph { - $glyph = cacheMap.get(key, bg, fg, ext); + $glyph = cacheMap.get(key, bg, fg, ext, underlineVariantOffset); if (!$glyph) { - $glyph = this._drawToCache(key, bg, fg, ext, restrictToCellHeight); - cacheMap.set(key, bg, fg, ext, $glyph); + $glyph = this._drawToCache(key, bg, fg, ext, underlineVariantOffset, restrictToCellHeight); + cacheMap.set(key, bg, fg, ext, underlineVariantOffset, $glyph); } return $glyph; } @@ -425,7 +426,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, underlineVariantoffset: number = 0, restrictToCellHeight: boolean = false): IRasterizedGlyph { const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars; // Uncomment for debugging @@ -545,13 +546,12 @@ 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 = this._workAttributeData.getUnderlineVariantOffset(); + let nextOffset = underlineVariantoffset; 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; - const xChMid = xChLeft + this._config.deviceCellWidth / 2; switch (this._workAttributeData.extended.underlineStyle) { case UnderlineStyle.DOUBLE: this._tmpCtx.moveTo(xChLeft, yTop); @@ -560,39 +560,97 @@ export class TextureAtlas implements ITextureAtlas { this._tmpCtx.lineTo(xChRight, yBot); break; case UnderlineStyle.CURLY: - // 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.deviceCharHeight - lineWidth / 2) - yOffset; - const yCurlyTop = lineWidth <= 1 ? yTop : Math.ceil(padding + this._config.deviceCharHeight + 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. - const clipRegion = new Path2D(); - clipRegion.rect(xChLeft, yTop, this._config.deviceCellWidth, 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(xChLeft - this._config.deviceCellWidth / 2, yMid); - this._tmpCtx.bezierCurveTo( - xChLeft - this._config.deviceCellWidth / 2, yCurlyTop, - xChLeft, yCurlyTop, - xChLeft, yMid - ); - this._tmpCtx.bezierCurveTo( - xChLeft, yCurlyBot, - xChMid, yCurlyBot, - xChMid, yMid - ); - this._tmpCtx.bezierCurveTo( - xChMid, yCurlyTop, - xChRight, yCurlyTop, - xChRight, yMid - ); - this._tmpCtx.bezierCurveTo( - xChRight, yCurlyBot, - xChRight + this._config.deviceCellWidth / 2, yCurlyBot, - xChRight + this._config.deviceCellWidth / 2, yMid - ); + // Draw Curly through segments. + // Segments are distinguished from up to down + // up: + // *** + // * + // down: + // * + // *** + + // [TODO] Up or down offset, To be verified. + const yMidRectOffset = Math.floor(yMid) - lineWidth; + + const plan = getCurlyVariant(this._config.deviceCellWidth, lineWidth, nextOffset) as string; + const steps = plan.split(' '); + let xOffset = 0; + + steps.forEach(step => { + const op: UnderlineDrawCurlyOp = step.substring(0, 1) as UnderlineDrawCurlyOp; + const pixels = Number.parseInt(step.substring(1)); + + // draw join + if (op === 'Y' || op === 'B' || op === 'M' || op === 'Q' || op === 'P' || op === 'Z') { + // TODO recode + if (pixels < lineWidth) { + if (op === 'Q') { + for (let index = 0; index < pixels; index++) { + this._tmpCtx.fillRect(xChLeft + xOffset + index, yMidRectOffset - index, 1, lineWidth); + } + xOffset += pixels; + return; + } + + if (op === 'P') { + for (let index = 0; index < pixels; index++) { + this._tmpCtx.fillRect(xChLeft + xOffset + index, yMidRectOffset - (lineWidth - pixels) - index, 1, lineWidth); + } + xOffset += pixels; + return; + } + + if (op === 'Z') { + for (let index = 0; index < pixels; index++) { + this._tmpCtx.fillRect(xChLeft + xOffset + index, yMidRectOffset - (lineWidth - 1) + index, 1, lineWidth); + } + xOffset += pixels; + return; + } + + if (op === 'M') { + for (let index = 0; index < pixels; index++) { + this._tmpCtx.fillRect(xChLeft + xOffset + index, yMidRectOffset - (lineWidth - 1) + (lineWidth - pixels) + index, 1, lineWidth); + } + xOffset += pixels; + return; + } + } else { + if (op === 'Y') { + for (let index = 0; index < pixels; index++) { + this._tmpCtx.fillRect(xChLeft + xOffset + index, yMidRectOffset - index, 1, lineWidth); + } + xOffset += pixels; + return; + } + + if (op === 'B') { + for (let index = 0; index < pixels; index++) { + this._tmpCtx.fillRect(xChLeft + xOffset + index, yMidRectOffset - (lineWidth - 1) + index, 1, lineWidth); + } + xOffset += pixels; + return; + } + } + return; + } + + // draw line + if (op === 'U') { + this._tmpCtx.fillRect(xChLeft + xOffset, yMidRectOffset - lineWidth, pixels, lineWidth); + xOffset += pixels; + return; + } + + if (op === 'D') { + this._tmpCtx.fillRect(xChLeft + xOffset, yMidRectOffset + 1, pixels, lineWidth); + xOffset += pixels; + return; + } + }); + + // handle next + nextOffset++; break; case UnderlineStyle.DOTTED: const offsetWidth = nextOffset === 0 ? 0 : diff --git a/src/browser/renderer/shared/Types.d.ts b/src/browser/renderer/shared/Types.d.ts index c0d5e9d656..a31754a4b1 100644 --- a/src/browser/renderer/shared/Types.d.ts +++ b/src/browser/renderer/shared/Types.d.ts @@ -107,8 +107,8 @@ export interface ITextureAtlas extends IDisposable { * 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, underlineVariantOffset: number, restrictToCellHeight: boolean): IRasterizedGlyph; + getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number, underlineVariantOffset: number, restrictToCellHeight: boolean): IRasterizedGlyph; } /** @@ -171,3 +171,18 @@ export interface ISelectionRenderModel { update(terminal: ITerminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode?: boolean): void; isCellSelected(terminal: Terminal, x: number, y: number): boolean; } + +export type UnderlineCurlyLineType = 'up' | 'down'; +export type UnderlineCurlyJoinOrLine = 'join' | 'line'; + +/** + * Y Complete upward join + * B Complete downward join + * M Split Right downward join + * P Split right upward join + * Q Split left upward join + * Z Split left downward join + * U Up line + * D Down line + */ +export type UnderlineDrawCurlyOp = 'Y' | 'B' | 'M' | 'Q' | 'P' | 'Z' | 'U' | 'D'; diff --git a/src/common/MultiKeyMap.ts b/src/common/MultiKeyMap.ts index 6287a8f243..edd5abb5f9 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 = {}; + } +}