diff --git a/fixtures/typings-test/typings-test.ts b/fixtures/typings-test/typings-test.ts index 928ca238..bc8f1bd6 100644 --- a/fixtures/typings-test/typings-test.ts +++ b/fixtures/typings-test/typings-test.ts @@ -140,6 +140,7 @@ namespace methods_core { const r18: (data: string) => void = t.getOption('handler'); const r19: string = t.getOption('bellSound'); const r20: string = t.getOption('bellStyle'); + const r22: number = t.getOption('letterSpacing'); } { const t: Terminal = new Terminal(); @@ -157,6 +158,7 @@ namespace methods_core { t.setOption('useFlowControl', true); t.setOption('visualBell', true); t.setOption('colors', ['a', 'b']); + t.setOption('letterSpacing', 1); t.setOption('cols', 1); t.setOption('rows', 1); t.setOption('tabStopWidth', 1); diff --git a/src/Interfaces.ts b/src/Interfaces.ts index 2c77e8d3..0ab0af5a 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -139,6 +139,7 @@ export interface ITerminalOptions { fontFamily?: string; geometry?: [number, number]; handler?: (data: string) => void; + letterSpacing?: number; lineHeight?: number; rows?: number; screenKeys?: boolean; diff --git a/src/Terminal.ts b/src/Terminal.ts index 8486f265..5ea01092 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -80,6 +80,7 @@ const DEFAULT_OPTIONS: ITerminalOptions = { fontFamily: 'courier-new, courier, monospace', fontSize: 15, lineHeight: 1.0, + letterSpacing: 0, scrollback: 1000, screenKeys: false, debug: false, @@ -410,6 +411,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.renderer.clear(); this.charMeasure.measure(this.options); break; + case 'letterSpacing': case 'lineHeight': // When the font changes the size of the cells may change which requires a renderer clear this.renderer.clear(); diff --git a/src/Viewport.ts b/src/Viewport.ts index 064076a2..d20749c3 100644 --- a/src/Viewport.ts +++ b/src/Viewport.ts @@ -47,7 +47,7 @@ export class Viewport implements IViewport { */ private refresh(): void { if (this.charMeasure.height > 0) { - this.currentRowHeight = this.terminal.renderer.dimensions.scaledLineHeight / window.devicePixelRatio; + this.currentRowHeight = this.terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio; if (this.lastRecordedViewportHeight !== this.terminal.renderer.dimensions.canvasHeight) { this.lastRecordedViewportHeight = this.terminal.renderer.dimensions.canvasHeight; @@ -75,7 +75,7 @@ export class Viewport implements IViewport { this.refresh(); } else { // If size has changed, refresh viewport - if (this.terminal.renderer.dimensions.scaledLineHeight / window.devicePixelRatio !== this.currentRowHeight) { + if (this.terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio !== this.currentRowHeight) { this.refresh(); } } diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index fa1846b6..b2597eb7 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -17,8 +17,10 @@ export abstract class BaseRenderLayer implements IRenderLayer { protected _ctx: CanvasRenderingContext2D; private _scaledCharWidth: number; private _scaledCharHeight: number; - private _scaledLineHeight: number; - private _scaledLineDrawY: number; + private _scaledCellWidth: number; + private _scaledCellHeight: number; + private _scaledCharLeft: number; + private _scaledCharTop: number; private _charAtlas: HTMLCanvasElement | ImageBitmap; @@ -71,10 +73,12 @@ export abstract class BaseRenderLayer implements IRenderLayer { } public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void { + this._scaledCellWidth = dim.scaledCellWidth; + this._scaledCellHeight = dim.scaledCellHeight; this._scaledCharWidth = dim.scaledCharWidth; this._scaledCharHeight = dim.scaledCharHeight; - this._scaledLineHeight = dim.scaledLineHeight; - this._scaledLineDrawY = dim.scaledLineDrawY; + this._scaledCharLeft = dim.scaledCharLeft; + this._scaledCharTop = dim.scaledCharTop; this._canvas.width = dim.scaledCanvasWidth; this._canvas.height = dim.scaledCanvasHeight; this._canvas.style.width = `${dim.canvasWidth}px`; @@ -101,10 +105,10 @@ export abstract class BaseRenderLayer implements IRenderLayer { */ protected fillCells(x: number, y: number, width: number, height: number): void { this._ctx.fillRect( - x * this._scaledCharWidth, - y * this._scaledLineHeight, - width * this._scaledCharWidth, - height * this._scaledLineHeight); + x * this._scaledCellWidth, + y * this._scaledCellHeight, + width * this._scaledCellWidth, + height * this._scaledCellHeight); } /** @@ -115,9 +119,9 @@ export abstract class BaseRenderLayer implements IRenderLayer { */ protected fillBottomLineAtCells(x: number, y: number, width: number = 1): void { this._ctx.fillRect( - x * this._scaledCharWidth, - (y + 1) * this._scaledLineHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */, - width * this._scaledCharWidth, + x * this._scaledCellWidth, + (y + 1) * this._scaledCellHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */, + width * this._scaledCellWidth, window.devicePixelRatio); } @@ -129,10 +133,10 @@ export abstract class BaseRenderLayer implements IRenderLayer { */ protected fillLeftLineAtCell(x: number, y: number): void { this._ctx.fillRect( - x * this._scaledCharWidth, - y * this._scaledLineHeight, + x * this._scaledCellWidth, + y * this._scaledCellHeight, window.devicePixelRatio, - this._scaledLineHeight); + this._scaledCellHeight); } /** @@ -144,10 +148,10 @@ export abstract class BaseRenderLayer implements IRenderLayer { protected strokeRectAtCell(x: number, y: number, width: number, height: number): void { this._ctx.lineWidth = window.devicePixelRatio; this._ctx.strokeRect( - x * this._scaledCharWidth + window.devicePixelRatio / 2, - y * this._scaledLineHeight + (window.devicePixelRatio / 2), - width * this._scaledCharWidth - window.devicePixelRatio, - (height * this._scaledLineHeight) - window.devicePixelRatio); + x * this._scaledCellWidth + window.devicePixelRatio / 2, + y * this._scaledCellHeight + (window.devicePixelRatio / 2), + width * this._scaledCellWidth - window.devicePixelRatio, + (height * this._scaledCellHeight) - window.devicePixelRatio); } /** @@ -172,17 +176,17 @@ export abstract class BaseRenderLayer implements IRenderLayer { protected clearCells(x: number, y: number, width: number, height: number): void { if (this._alpha) { this._ctx.clearRect( - x * this._scaledCharWidth, - y * this._scaledLineHeight, - width * this._scaledCharWidth, - height * this._scaledLineHeight); + x * this._scaledCellWidth, + y * this._scaledCellHeight, + width * this._scaledCellWidth, + height * this._scaledCellHeight); } else { this._ctx.fillStyle = this._colors.background; this._ctx.fillRect( - x * this._scaledCharWidth, - y * this._scaledLineHeight, - width * this._scaledCharWidth, - height * this._scaledLineHeight); + x * this._scaledCellWidth, + y * this._scaledCellHeight, + width * this._scaledCellWidth, + height * this._scaledCellHeight); } } @@ -199,15 +203,11 @@ export abstract class BaseRenderLayer implements IRenderLayer { protected fillCharTrueColor(terminal: ITerminal, charData: CharData, x: number, y: number): void { this._ctx.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`; this._ctx.textBaseline = 'top'; - - // Since uncached characters are not coming off the char atlas with source - // coordinates, it means that text drawn to the canvas (particularly '_') - // can bleed into other cells. This code will clip the following fillText, - // ensuring that its contents don't go beyond the cell bounds. - this._ctx.beginPath(); - this._ctx.rect(x * this._scaledCharWidth, y * this._scaledLineHeight + this._scaledLineDrawY, charData[CHAR_DATA_WIDTH_INDEX] * this._scaledCharWidth, this._scaledCharHeight); - this._ctx.clip(); - this._ctx.fillText(charData[CHAR_DATA_CHAR_INDEX], x * this._scaledCharWidth, y * this._scaledCharHeight); + this._clipRow(terminal, y); + this._ctx.fillText( + charData[CHAR_DATA_CHAR_INDEX], + x * this._scaledCellWidth + this._scaledCharLeft, + y * this._scaledCellHeight + this._scaledCharTop); } /** @@ -250,8 +250,14 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.globalAlpha = DIM_OPACITY; } this._ctx.drawImage(this._charAtlas, - code * charAtlasCellWidth, colorIndex * charAtlasCellHeight, charAtlasCellWidth, this._scaledCharHeight, - x * this._scaledCharWidth, y * this._scaledLineHeight + this._scaledLineDrawY, charAtlasCellWidth, this._scaledCharHeight); + code * charAtlasCellWidth, + colorIndex * charAtlasCellHeight, + charAtlasCellWidth, + this._scaledCharHeight, + x * this._scaledCellWidth + this._scaledCharLeft, + y * this._scaledCellHeight + this._scaledCharTop, + charAtlasCellWidth, + this._scaledCharHeight); } else { this._drawUncachedChar(terminal, char, width, fg, x, y, bold, dim); } @@ -288,21 +294,33 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.fillStyle = this._colors.foreground; } - // Since uncached characters are not coming off the char atlas with source - // coordinates, it means that text drawn to the canvas (particularly '_') - // can bleed into other cells. This code will clip the following fillText, - // ensuring that its contents don't go beyond the cell bounds. - this._ctx.beginPath(); - this._ctx.rect(0, y * this._scaledLineHeight + this._scaledLineDrawY, terminal.cols * this._scaledCharWidth, this._scaledCharHeight); - this._ctx.clip(); + this._clipRow(terminal, y); // Apply alpha to dim the character if (dim) { this._ctx.globalAlpha = DIM_OPACITY; } // Draw the character - this._ctx.fillText(char, x * this._scaledCharWidth, y * this._scaledLineHeight + this._scaledLineDrawY); + this._ctx.fillText( + char, + x * this._scaledCellWidth + this._scaledCharLeft, + y * this._scaledCellHeight + this._scaledCharTop); this._ctx.restore(); } + + /** + * Clips a row to ensure no pixels will be drawn outside the cells in the row. + * @param terminal The terminal. + * @param y The row to clip. + */ + private _clipRow(terminal: ITerminal, y: number): void { + this._ctx.beginPath(); + this._ctx.rect( + 0, + y * this._scaledCellHeight, + terminal.cols * this._scaledCellWidth, + this._scaledCellHeight); + this._ctx.clip(); + } } diff --git a/src/renderer/Interfaces.ts b/src/renderer/Interfaces.ts index 74400b8c..be1b39dd 100644 --- a/src/renderer/Interfaces.ts +++ b/src/renderer/Interfaces.ts @@ -86,8 +86,10 @@ export interface IColorSet { export interface IRenderDimensions { scaledCharWidth: number; scaledCharHeight: number; - scaledLineHeight: number; - scaledLineDrawY: number; + scaledCellWidth: number; + scaledCellHeight: number; + scaledCharLeft: number; + scaledCharTop: number; scaledCanvasWidth: number; scaledCanvasHeight: number; canvasWidth: number; diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 168d6dc8..ea373e5e 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -40,8 +40,10 @@ export class Renderer extends EventEmitter implements IRenderer { this.dimensions = { scaledCharWidth: null, scaledCharHeight: null, - scaledLineHeight: null, - scaledLineDrawY: null, + scaledCellWidth: null, + scaledCellHeight: null, + scaledCharLeft: null, + scaledCharTop: null, scaledCanvasWidth: null, scaledCanvasHeight: null, canvasWidth: null, @@ -91,20 +93,27 @@ export class Renderer extends EventEmitter implements IRenderer { // enough space to draw the character to the cell. this.dimensions.scaledCharHeight = Math.ceil(this._terminal.charMeasure.height * window.devicePixelRatio); - // Calculate the scaled line height, if lineHeight is not 1 then the value + // Calculate the scaled cell height, if lineHeight is not 1 then the value // will be floored because since lineHeight can never be lower then 1, there // is a guarentee that the scaled line height will always be larger than // scaled char height. - this.dimensions.scaledLineHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight); + this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight); // Calculate the y coordinate within a cell that text should draw from in // order to draw in the center of a cell. - this.dimensions.scaledLineDrawY = this._terminal.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledLineHeight - this.dimensions.scaledCharHeight) / 2); + this.dimensions.scaledCharTop = this._terminal.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2); + + // Calculate the scaled cell width, taking the letterSpacing into account. + this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing); + + // Calculate the x coordinate with a cell that text should draw from in + // order to draw in the center of a cell. + this.dimensions.scaledCharLeft = Math.floor(this._terminal.options.letterSpacing / 2); // Recalculate the canvas dimensions; scaled* define the actual number of // pixel in the canvas - this.dimensions.scaledCanvasHeight = this._terminal.rows * this.dimensions.scaledLineHeight; - this.dimensions.scaledCanvasWidth = this._terminal.cols * this.dimensions.scaledCharWidth; + this.dimensions.scaledCanvasHeight = this._terminal.rows * this.dimensions.scaledCellHeight; + this.dimensions.scaledCanvasWidth = this._terminal.cols * this.dimensions.scaledCellWidth; // The the size of the canvas on the page. It's very important that this // rounds to nearest integer and not ceils as browsers often set diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index de2a261b..36176f8b 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -51,6 +51,11 @@ interface ITerminalOptions { */ fontFamily?: string; + /** + * The spacing in whole pixels between characters.. + */ + letterSpacing?: number; + /** * The line height used to render text. */ @@ -407,7 +412,7 @@ declare module 'xterm' { * Retrieves an option's value from the terminal. * @param key The option key. */ - getOption(key: 'cols' | 'fontSize' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback'): number; + getOption(key: 'cols' | 'fontSize' | 'letterSpacing' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback'): number; /** * Retrieves an option's value from the terminal. * @param key The option key. @@ -459,7 +464,7 @@ declare module 'xterm' { * @param key The option key. * @param value The option value. */ - setOption(key: 'cols' | 'fontSize' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback', value: number): void; + setOption(key: 'cols' | 'fontSize' | 'letterSpacing' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback', value: number): void; /** * Sets an option on the terminal. * @param key The option key.