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 d15f1ee7..6b264e4b 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -16,8 +16,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; @@ -70,10 +72,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`; @@ -100,10 +104,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); } /** @@ -114,9 +118,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); } @@ -128,10 +132,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); } /** @@ -143,10 +147,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); } /** @@ -171,17 +175,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); } } @@ -204,9 +208,17 @@ export abstract class BaseRenderLayer implements IRenderLayer { // 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); + // TODO: Make clip rect use cell size? + this._ctx.rect( + x * this._scaledCellWidth + this._scaledCharLeft, + y * this._scaledCellHeight + this._scaledCharTop, + 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._ctx.fillText( + charData[CHAR_DATA_CHAR_INDEX], + x * this._scaledCellWidth + this._scaledCharLeft, + y * this._scaledCellHeight + this._scaledCharTop); } /** @@ -242,8 +254,14 @@ export abstract class BaseRenderLayer implements IRenderLayer { const charAtlasCellWidth = this._scaledCharWidth + CHAR_ATLAS_CELL_SPACING; const charAtlasCellHeight = this._scaledCharHeight + CHAR_ATLAS_CELL_SPACING; this._ctx.drawImage(this._charAtlas, - code * charAtlasCellWidth, colorIndex * charAtlasCellHeight, charAtlasCellWidth, this._scaledCharHeight, - x * this._scaledCharWidth, y * this._scaledLineHeight + this._scaledLineDrawY, this._scaledCharWidth, this._scaledCharHeight); + code * charAtlasCellWidth, + colorIndex * charAtlasCellHeight, + charAtlasCellWidth, + this._scaledCharHeight, + x * this._scaledCellWidth + this._scaledCharLeft, + y * this._scaledCellHeight + this._scaledCharTop, + this._scaledCharWidth, + this._scaledCharHeight); } else { this._drawUncachedChar(terminal, char, width, fg, x, y, bold); } @@ -285,11 +303,19 @@ export abstract class BaseRenderLayer implements IRenderLayer { // 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); + // TODO: Why is this be clipped at char top? + this._ctx.rect( + 0, + y * this._scaledCellHeight + this._scaledCharTop, + terminal.cols * this._scaledCharWidth, + this._scaledCharHeight); this._ctx.clip(); // 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(); } } 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..52cad50e 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -40,8 +40,8 @@ export class Renderer extends EventEmitter implements IRenderer { this.dimensions = { scaledCharWidth: null, scaledCharHeight: null, - scaledLineHeight: null, - scaledLineDrawY: null, + scaledCellHeight: null, + scaledCharTop: null, scaledCanvasWidth: null, scaledCanvasHeight: null, canvasWidth: null, @@ -91,20 +91,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