From c92a2f964a403d35e28229f0bacc2019f9cb2510 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 2 Sep 2017 12:27:09 -0700 Subject: [PATCH] Support lineHeight --- src/Interfaces.ts | 1 + src/SelectionManager.ts | 4 ++-- src/Terminal.ts | 28 ++++++++++++++++++++-------- src/Viewport.ts | 15 ++++++++------- src/addons/fit/fit.js | 4 +++- src/renderer/BaseRenderLayer.ts | 26 +++++++++++++++----------- src/renderer/Renderer.ts | 2 +- src/utils/Mouse.ts | 8 ++++---- typings/xterm.d.ts | 4 ++-- 9 files changed, 56 insertions(+), 36 deletions(-) diff --git a/src/Interfaces.ts b/src/Interfaces.ts index 0b50161c..8af07934 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -127,6 +127,7 @@ export interface ITerminalOptions { fontFamily?: string; geometry?: [number, number]; handler?: (data: string) => void; + lineHeight?: number; rows?: number; screenKeys?: boolean; scrollback?: number; diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index dc58858e..1425aedc 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -273,7 +273,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager * @param event The mouse event. */ private _getMouseBufferCoords(event: MouseEvent): [number, number] { - const coords = Mouse.getCoords(event, this._rowContainer, this._charMeasure, this._terminal.cols, this._terminal.rows, true); + const coords = Mouse.getCoords(event, this._rowContainer, this._charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows, true); if (!coords) { return null; } @@ -293,7 +293,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager */ private _getMouseEventScrollAmount(event: MouseEvent): number { let offset = Mouse.getCoordsRelativeToElement(event, this._rowContainer)[1]; - const terminalHeight = this._terminal.rows * this._charMeasure.height; + const terminalHeight = this._terminal.rows * Math.ceil(this._charMeasure.height * this._terminal.options.lineHeight); if (offset >= 0 && offset <= terminalHeight) { return 0; } diff --git a/src/Terminal.ts b/src/Terminal.ts index 59d39e5a..9dba6701 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -72,6 +72,7 @@ const DEFAULT_OPTIONS: ITerminalOptions = { bellStyle: 'none', fontFamily: 'courier-new, courier, monospace', fontSize: 15, + lineHeight: 1.0, scrollback: 1000, screenKeys: false, debug: false, @@ -356,19 +357,24 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT } break; case 'cursorStyle': - if (!value) { - value = 'block'; - } - break; + if (!value) { + value = 'block'; + } + break; + case 'lineHeight': + if (value < 1) { + console.warn(`${key} cannot be less than 1, value: ${value}`); + return; + } case 'tabStopWidth': if (value < 1) { - console.warn(`tabStopWidth cannot be less than 1, value: ${value}`); + console.warn(`${key} cannot be less than 1, value: ${value}`); return; } break; case 'scrollback': if (value < 0) { - console.warn(`scrollback cannot be less than 0, value: ${value}`); + console.warn(`${key} cannot be less than 0, value: ${value}`); return; } if (this.options[key] !== value) { @@ -395,6 +401,12 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.renderer.clear(); this.charMeasure.measure(this.options); break; + case 'lineHeight': + // When the font changes the size of the cells may change which requires a renderer clear + this.renderer.clear(); + this.renderer.onResize(this.cols, this.rows); + this.refresh(0, this.rows - 1); + // this.charMeasure.measure(this.options); case 'scrollback': this.buffers.resize(this.cols, this.rows); this.viewport.syncScrollArea(); @@ -709,7 +721,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT button = getButton(ev); // get mouse coordinates - pos = getRawByteCoords(ev, self.rowContainer, self.charMeasure, self.cols, self.rows); + pos = getRawByteCoords(ev, self.rowContainer, self.charMeasure, self.options.lineHeight, self.cols, self.rows); if (!pos) return; sendEvent(button, pos); @@ -735,7 +747,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7< function sendMove(ev: MouseEvent): void { let button = pressed; - let pos = getRawByteCoords(ev, self.rowContainer, self.charMeasure, self.cols, self.rows); + let pos = getRawByteCoords(ev, self.rowContainer, self.charMeasure, self.options.lineHeight, self.cols, self.rows); if (!pos) return; // buttons marked as motions diff --git a/src/Viewport.ts b/src/Viewport.ts index 4ad4ddec..b061eeb6 100644 --- a/src/Viewport.ts +++ b/src/Viewport.ts @@ -46,19 +46,20 @@ export class Viewport implements IViewport { */ private refresh(): void { if (this.charMeasure.height > 0) { - const rowHeightChanged = this.charMeasure.height !== this.currentRowHeight; + const lineHeight = Math.ceil(this.charMeasure.height * this.terminal.options.lineHeight); + const rowHeightChanged = lineHeight !== this.currentRowHeight; if (rowHeightChanged) { - this.currentRowHeight = this.charMeasure.height; - this.viewportElement.style.lineHeight = this.charMeasure.height + 'px'; - this.terminal.rowContainer.style.lineHeight = this.charMeasure.height + 'px'; + this.currentRowHeight = lineHeight; + this.viewportElement.style.lineHeight = lineHeight + 'px'; + this.terminal.rowContainer.style.lineHeight = lineHeight + 'px'; } const viewportHeightChanged = this.lastRecordedViewportHeight !== this.terminal.rows; if (rowHeightChanged || viewportHeightChanged) { this.lastRecordedViewportHeight = this.terminal.rows; - this.viewportElement.style.height = this.charMeasure.height * this.terminal.rows + 'px'; + this.viewportElement.style.height = lineHeight * this.terminal.rows + 'px'; this.terminal.selectionContainer.style.height = this.viewportElement.style.height; } - this.scrollArea.style.height = (this.charMeasure.height * this.lastRecordedBufferLength) + 'px'; + this.scrollArea.style.height = (lineHeight * this.lastRecordedBufferLength) + 'px'; } } @@ -75,7 +76,7 @@ export class Viewport implements IViewport { this.refresh(); } else { // If size has changed, refresh viewport - if (this.charMeasure.height !== this.currentRowHeight) { + if (Math.ceil(this.charMeasure.height * this.terminal.options.lineHeight) !== this.currentRowHeight) { this.refresh(); } } diff --git a/src/addons/fit/fit.js b/src/addons/fit/fit.js index 1e46932c..b774cebf 100644 --- a/src/addons/fit/fit.js +++ b/src/addons/fit/fit.js @@ -49,7 +49,7 @@ var geometry = { cols: parseInt(availableWidth / term.charMeasure.width, 10), - rows: parseInt(availableHeight / term.charMeasure.height, 10) + rows: parseInt(availableHeight / (term.charMeasure.height * term.getOption('lineHeight')), 10) }; return geometry; @@ -62,6 +62,8 @@ var geometry = exports.proposeGeometry(term); if (geometry) { + // Force a full render + term.renderer.clear(); term.resize(geometry.cols, geometry.rows); } }, 0); diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 57427122..912d351f 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -11,6 +11,8 @@ export abstract class BaseRenderLayer implements IRenderLayer { protected _ctx: CanvasRenderingContext2D; private scaledCharWidth: number; private scaledCharHeight: number; + private scaledLineHeight: number; + private scaledLineDrawY: number; // TODO: This should be shared between terminals, but not for static as some // terminals may have different styles @@ -55,6 +57,8 @@ export abstract class BaseRenderLayer implements IRenderLayer { public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void { this.scaledCharWidth = terminal.charMeasure.width * window.devicePixelRatio; this.scaledCharHeight = terminal.charMeasure.height * window.devicePixelRatio; + this.scaledLineHeight = Math.ceil(this.scaledCharHeight * terminal.options.lineHeight); + this.scaledLineDrawY = terminal.options.lineHeight === 1 ? 0 : Math.round((this.scaledLineHeight - this.scaledCharHeight) / 2); this._canvas.width = canvasWidth * window.devicePixelRatio; this._canvas.height = canvasHeight * window.devicePixelRatio; this._canvas.style.width = `${canvasWidth}px`; @@ -68,13 +72,13 @@ export abstract class BaseRenderLayer implements IRenderLayer { public abstract reset(terminal: ITerminal): void; protected fillCells(startCol: number, startRow: number, colWidth: number, colHeight: number): void { - this._ctx.fillRect(startCol * this.scaledCharWidth, startRow * this.scaledCharHeight, colWidth * this.scaledCharWidth, colHeight * this.scaledCharHeight); + this._ctx.fillRect(startCol * this.scaledCharWidth, startRow * this.scaledLineHeight, colWidth * this.scaledCharWidth, colHeight * this.scaledLineHeight); } protected drawBottomLineAtCell(x: number, y: number): void { this._ctx.fillRect( x * this.scaledCharWidth, - (y + 1) * this.scaledCharHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */, + (y + 1) * this.scaledLineHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */, this.scaledCharWidth, window.devicePixelRatio); } @@ -82,9 +86,9 @@ export abstract class BaseRenderLayer implements IRenderLayer { protected drawLeftLineAtCell(x: number, y: number): void { this._ctx.fillRect( x * this.scaledCharWidth, - y * this.scaledCharHeight, + y * this.scaledLineHeight, window.devicePixelRatio, - this.scaledCharHeight); + this.scaledLineHeight); } protected drawRectAtCell(x: number, y: number, width: number, height: number, color: string): void { @@ -92,9 +96,9 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.lineWidth = window.devicePixelRatio; this._ctx.strokeRect( x * this.scaledCharWidth + window.devicePixelRatio / 2, - y * this.scaledCharHeight + (window.devicePixelRatio / 2), + y * this.scaledLineHeight + (window.devicePixelRatio / 2), (width * this.scaledCharWidth) - window.devicePixelRatio, - (height * this.scaledCharHeight) - window.devicePixelRatio); + (height * this.scaledLineHeight) - window.devicePixelRatio); } protected clearAll(): void { @@ -102,7 +106,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { } protected clearCells(startCol: number, startRow: number, colWidth: number, colHeight: number): void { - this._ctx.clearRect(startCol * this.scaledCharWidth, startRow * this.scaledCharHeight, colWidth * this.scaledCharWidth, colHeight * this.scaledCharHeight); + this._ctx.clearRect(startCol * this.scaledCharWidth, startRow * this.scaledLineHeight, colWidth * this.scaledCharWidth, colHeight * this.scaledLineHeight); } protected drawCharTrueColor(terminal: ITerminal, charData: CharData, x: number, y: number, color: string): void { @@ -116,7 +120,7 @@ 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.scaledCharHeight, charData[CHAR_DATA_WIDTH_INDEX] * this.scaledCharWidth, this.scaledCharHeight); + 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); @@ -137,7 +141,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { const charAtlasCellHeight = this.scaledCharHeight + CHAR_ATLAS_CELL_SPACING; this._ctx.drawImage(this._charAtlas, code * charAtlasCellWidth, colorIndex * charAtlasCellHeight, this.scaledCharWidth, this.scaledCharHeight, - x * this.scaledCharWidth, y * this.scaledCharHeight, this.scaledCharWidth, this.scaledCharHeight); + x * this.scaledCharWidth, y * this.scaledLineHeight + this.scaledLineDrawY, this.scaledCharWidth, this.scaledCharHeight); } else { this._drawUncachedChar(terminal, char, width, fg, x, y); } @@ -164,11 +168,11 @@ 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.scaledCharHeight, width * this.scaledCharWidth, this.scaledCharHeight); + this._ctx.rect(x * this.scaledCharWidth, y * this.scaledLineHeight + this.scaledLineDrawY, width * this.scaledCharWidth, this.scaledCharHeight); this._ctx.clip(); // Draw the character - this._ctx.fillText(char, x * this.scaledCharWidth, y * this.scaledCharHeight); + this._ctx.fillText(char, x * this.scaledCharWidth, y * this.scaledLineHeight + this.scaledLineDrawY); this._ctx.restore(); } } diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index cc85c7e7..b3df2b7a 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -46,7 +46,7 @@ export class Renderer { public onResize(cols: number, rows: number): void { const width = this._terminal.charMeasure.width * this._terminal.cols; - const height = this._terminal.charMeasure.height * this._terminal.rows; + const height = Math.ceil(this._terminal.charMeasure.height * this._terminal.options.lineHeight) * this._terminal.rows; this._renderLayers.forEach(l => l.resize(this._terminal, width, height, false)); } diff --git a/src/utils/Mouse.ts b/src/utils/Mouse.ts index c61624e9..f25a6b2e 100644 --- a/src/utils/Mouse.ts +++ b/src/utils/Mouse.ts @@ -36,7 +36,7 @@ export function getCoordsRelativeToElement(event: MouseEvent, element: HTMLEleme * apply an offset to the x value such that the left half of the cell will * select that cell and the right half will select the next cell. */ -export function getCoords(event: MouseEvent, rowContainer: HTMLElement, charMeasure: CharMeasure, colCount: number, rowCount: number, isSelection?: boolean): [number, number] { +export function getCoords(event: MouseEvent, rowContainer: HTMLElement, charMeasure: CharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number] { // Coordinates cannot be measured if charMeasure has not been initialized if (!charMeasure.width || !charMeasure.height) { return null; @@ -49,7 +49,7 @@ export function getCoords(event: MouseEvent, rowContainer: HTMLElement, charMeas // Convert to cols/rows. coords[0] = Math.ceil((coords[0] + (isSelection ? charMeasure.width / 2 : 0)) / charMeasure.width); - coords[1] = Math.ceil(coords[1] / charMeasure.height); + coords[1] = Math.ceil(coords[1] / Math.ceil(charMeasure.height * lineHeight)); // Ensure coordinates are within the terminal viewport. coords[0] = Math.min(Math.max(coords[0], 1), colCount + 1); @@ -68,8 +68,8 @@ export function getCoords(event: MouseEvent, rowContainer: HTMLElement, charMeas * @param colCount The number of columns in the terminal. * @param rowCount The number of rows in the terminal. */ -export function getRawByteCoords(event: MouseEvent, rowContainer: HTMLElement, charMeasure: CharMeasure, colCount: number, rowCount: number): { x: number, y: number } { - const coords = getCoords(event, rowContainer, charMeasure, colCount, rowCount); +export function getRawByteCoords(event: MouseEvent, rowContainer: HTMLElement, charMeasure: CharMeasure, lineHeight: number, colCount: number, rowCount: number): { x: number, y: number } { + const coords = getCoords(event, rowContainer, charMeasure, lineHeight, colCount, rowCount); let x = coords[0]; let y = coords[1]; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index dce15bca..2956c937 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -328,7 +328,7 @@ declare module 'xterm' { * Retrieves an option's value from the terminal. * @param key The option key. */ - getOption(key: 'cols' | 'fontSize' | 'rows' | 'tabStopWidth' | 'scrollback'): number; + getOption(key: 'cols' | 'fontSize' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback'): number; /** * Retrieves an option's value from the terminal. * @param key The option key. @@ -380,7 +380,7 @@ declare module 'xterm' { * @param key The option key. * @param value The option value. */ - setOption(key: 'cols' | 'fontSize' | 'rows' | 'tabStopWidth' | 'scrollback', value: number): void; + setOption(key: 'cols' | 'fontSize' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback', value: number): void; /** * Sets an option on the terminal. * @param key The option key.