diff --git a/src/browser/renderer/BaseRenderLayer.ts b/src/browser/renderer/BaseRenderLayer.ts index 44b23840..e7116ed6 100644 --- a/src/browser/renderer/BaseRenderLayer.ts +++ b/src/browser/renderer/BaseRenderLayer.ts @@ -17,7 +17,7 @@ import { IBufferService, IOptionsService } from 'common/services/Services'; import { throwIfFalsy } from 'browser/renderer/RendererUtils'; import { channels, color, rgba } from 'browser/Color'; import { removeElementFromParent } from 'browser/Dom'; -import { boxCharacters, boxDrawingBoxes, drawBoxChar } from 'browser/renderer/BoxAndBlockCharacters'; +import { tryDrawCustomChar } from 'browser/renderer/BoxAndBlockCharacters'; export abstract class BaseRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; @@ -260,8 +260,15 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.font = this._getFont(false, false); this._ctx.textBaseline = 'ideographic'; this._clipRow(y); - // TODO: fix - if (!this._drawBoxChar(cell, x, y)) { + + // Draw custom characters if applicable + let drawSuccess = false; + if (this._optionsService.options.customBlockAndBoxCharacters !== false) { + drawSuccess = tryDrawCustomChar(this._ctx, cell.getChars(), x, y, this._scaledCellWidth, this._scaledCellHeight, this._scaledCharLeft, this._scaledCharTop); + } + + // Draw the character + if (!drawSuccess) { this._ctx.fillText( cell.getChars(), x * this._scaledCellWidth + this._scaledCharLeft, @@ -377,46 +384,24 @@ export abstract class BaseRenderLayer implements IRenderLayer { if (cell.isDim()) { this._ctx.globalAlpha = DIM_OPACITY; } - if (!this._drawBoxChar(cell, x, y)) { - // Draw the character + + // Draw custom characters if applicable + let drawSuccess = false; + if (this._optionsService.options.customBlockAndBoxCharacters !== false) { + drawSuccess = tryDrawCustomChar(this._ctx, cell.getChars(), x, y, this._scaledCellWidth, this._scaledCellHeight, this._scaledCharLeft, this._scaledCharTop); + } + + // Draw the character + if (!drawSuccess) { this._ctx.fillText( cell.getChars(), x * this._scaledCellWidth + this._scaledCharLeft, y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight); } + this._ctx.restore(); } - private _drawBoxChar(cell: ICellData, x: number, y: number): boolean { - const char = cell.getChars(); - - const boxes = boxDrawingBoxes[char]; - if (boxes) { - this._ctx.strokeStyle = this._ctx.fillStyle; - const xOffset = x * this._scaledCellWidth + this._scaledCharLeft; - const yOffset = y * this._scaledCellHeight + this._scaledCharTop; - for (let i = 0; i < boxes.length; i++) { - const box = boxes[i]; - const xEighth = this._scaledCellWidth / 8; - const yEighth = this._scaledCellHeight / 8; - this._ctx.fillRect( - xOffset, - yOffset, - box.w * xEighth, - box.h * yEighth); - } - return true; - } - - const lineSegments = boxCharacters[char]; - if (!lineSegments) { - return false; - } - this._ctx.strokeStyle = this._ctx.fillStyle; - drawBoxChar(this._ctx, char, x * this._scaledCellWidth, y * this._scaledCellHeight, this._scaledCellWidth, this._scaledCellHeight); - return true; - } - /** * Clips a row to ensure no pixels will be drawn outside the cells in the row. diff --git a/src/browser/renderer/BoxAndBlockCharacters.ts b/src/browser/renderer/BoxAndBlockCharacters.ts index ac37c761..81cfaba8 100644 --- a/src/browser/renderer/BoxAndBlockCharacters.ts +++ b/src/browser/renderer/BoxAndBlockCharacters.ts @@ -1,5 +1,16 @@ +/** + * Copyright (c) 2021 The xterm.js authors. All rights reserved. + * @license MIT + */ -export const boxDrawingBoxes: { [index: string]: any } = { +interface IBlockVector { + x: number; + y: number; + w: number; + h: number; +} + +export const blockElementChars: { [index: string]: IBlockVector[] | undefined } = { '▀': [{ x: 0, y: 0, w: 8, h: 4 }], '█': [{ x: 0, y: 0, w: 8, h: 8 }], '▇': [{ x: 0, y: 1, w: 8, h: 7 }], @@ -125,7 +136,7 @@ const enum Style { } // This contains the definitions of all box drawing characters as SVG paths (ie. the svg d attribute) -export const boxCharacters: { [character: string]: { [fontWeight: number]: string | ((xp: number, yp: number) => string) } } = { +export const boxDrawingChars: { [character: string]: { [fontWeight: number]: string | ((xp: number, yp: number) => string) } | undefined } = { // Uniform normal and bold '─': { [Style.NORMAL]: Shapes.LEFT_TO_RIGHT }, '━': { [Style.BOLD]: Shapes.LEFT_TO_RIGHT }, @@ -247,7 +258,6 @@ export const boxCharacters: { [character: string]: { [fontWeight: number]: strin '╊': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT, [Style.BOLD]: `${Shapes.TOP_TO_BOTTOM} ${Shapes.MIDDLE_TO_RIGHT}` }, // Dashed - // TODO: Spacing dashes evenly, use 1/2 padding on each edge so the line is continuous '╌': { [Style.NORMAL]: Shapes.TWO_DASHES_HORIZONTAL }, '╍': { [Style.BOLD]: Shapes.TWO_DASHES_HORIZONTAL }, '┄': { [Style.NORMAL]: Shapes.THREE_DASHES_HORIZONTAL }, @@ -268,18 +278,78 @@ export const boxCharacters: { [character: string]: { [fontWeight: number]: strin '╰': { [Style.NORMAL]: 'C.5,0,.5,.5,1,.5' } }; -export function drawBoxChar(ctx: CanvasRenderingContext2D, c: string, xOffset: number, yOffset: number, cellWidth: number, cellHeight: number): void { - const match: { [fontWeight: number]: string | ((xp: number, yp: number) => string) } = boxCharacters[c]; - if (!match) { - return; +/** + * Try drawing a custom block element or box drawing character, returning whether it was + * successfully drawn. + */ +export function tryDrawCustomChar( + ctx: CanvasRenderingContext2D, + c: string, + x: number, + y: number, + scaledCellWidth: number, + scaledCellHeight: number, + scaledCharLeft: number, + scaledCharTop: number +): boolean { + const blockElementInstruction = blockElementChars[c]; + if (blockElementInstruction) { + drawBlockElementChar(ctx, blockElementInstruction, x, y, scaledCellWidth, scaledCellHeight, scaledCharLeft, scaledCharTop); + return true; } - for (const [fontWeight, instructions] of Object.entries(match)) { + + const boxDrawingInstruction = boxDrawingChars[c]; + if (boxDrawingInstruction) { + drawBoxDrawingChar(ctx, boxDrawingInstruction, x, y, scaledCellWidth, scaledCellHeight); + return true; + } + + return false; +} + +function drawBlockElementChar( + ctx: CanvasRenderingContext2D, + instruction: IBlockVector[], + x: number, + y: number, + scaledCellWidth: number, + scaledCellHeight: number, + scaledCharLeft: number, + scaledCharTop: number +): void { + const xOffset = x * scaledCellWidth + scaledCharLeft; + const yOffset = y * scaledCellHeight + scaledCharTop; + for (let i = 0; i < instruction.length; i++) { + const box = instruction[i]; + const xEighth = scaledCellWidth / 8; + const yEighth = scaledCellHeight / 8; + ctx.fillRect( + xOffset, + yOffset, + box.w * xEighth, + box.h * yEighth + ); + } +} + +function drawBoxDrawingChar( + ctx: CanvasRenderingContext2D, + charDefinition: { [fontWeight: number]: string | ((xp: number, yp: number) => string) }, + x: number, + y: number, + scaledCellWidth: number, + scaledCellHeight: number +): void { + const xOffset = x * scaledCellWidth; + const yOffset = y * scaledCellHeight; + ctx.strokeStyle = ctx.fillStyle; + for (const [fontWeight, instructions] of Object.entries(charDefinition)) { ctx.beginPath(); ctx.lineWidth = window.devicePixelRatio * Number.parseInt(fontWeight); let actualInstructions: string; if (typeof instructions === 'function') { const xp = .15; - const yp = .15 / cellHeight * cellWidth; + const yp = .15 / scaledCellHeight * scaledCellWidth; actualInstructions = instructions(xp, yp); } else { actualInstructions = instructions; @@ -295,7 +365,7 @@ export function drawBoxChar(ctx: CanvasRenderingContext2D, c: string, xOffset: n if (!args[0] || !args[1]) { continue; } - f(ctx, translateArgs(args, cellWidth, cellHeight, xOffset, yOffset)); + f(ctx, translateArgs(args, scaledCellWidth, scaledCellHeight, xOffset, yOffset)); } ctx.stroke(); ctx.closePath(); diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index b7a1c58e..062166bc 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -21,6 +21,7 @@ export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({ cursorBlink: false, cursorStyle: 'block', cursorWidth: 1, + customBlockAndBoxCharacters: true, bellSound: DEFAULT_BELL_SOUND, bellStyle: 'none', drawBoldTextInBrightColors: true, diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index ce297322..07615a41 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -244,6 +244,7 @@ export interface ITerminalOptions { cursorBlink: boolean; cursorStyle: 'block' | 'underline' | 'bar'; cursorWidth: number; + customBlockAndBoxCharacters: boolean; disableStdin: boolean; drawBoldTextInBrightColors: boolean; fastScrollModifier: 'alt' | 'ctrl' | 'shift' | undefined; diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts index b6e1505b..dc886aa1 100644 --- a/typings/xterm-headless.d.ts +++ b/typings/xterm-headless.d.ts @@ -78,6 +78,14 @@ declare module 'xterm-headless' { */ cursorWidth?: number; + /** + * Whether to draw custom block element and box drawing characters instead of using the font. + * This should typically result in better rendering with continuous lines. Note that this + * doesn't work with the DOM renderer which renders all characters using the font. The default + * is true. + */ + customBlockAndBoxCharacters?: boolean; + /** * Whether input should be disabled. */ diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 3828b415..f6fbe709 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -90,6 +90,14 @@ declare module 'xterm' { */ cursorWidth?: number; + /** + * Whether to draw custom block element and box drawing characters instead of using the font. + * This should typically result in better rendering with continuous lines. Note that this + * doesn't work with the DOM renderer which renders all characters using the font. The default + * is true. + */ + customBlockAndBoxCharacters?: boolean; + /** * Whether input should be disabled. */