diff --git a/src/Interfaces.ts b/src/Interfaces.ts index fb66f4d7..1b1625bc 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -122,6 +122,8 @@ export interface ITerminalOptions { cursorStyle?: string; debug?: boolean; disableStdin?: boolean; + fontSize?: number; + fontFamily?: string; geometry?: [number, number]; handler?: (data: string) => void; rows?: number; @@ -186,7 +188,7 @@ export interface ICompositionHelper { export interface ICharMeasure { width: number; height: number; - measure(): void; + measure(options: ITerminalOptions): void; } export interface ILinkifier { diff --git a/src/Terminal.ts b/src/Terminal.ts index f88ebb7f..c9c6466c 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -151,6 +151,8 @@ const DEFAULT_OPTIONS: ITerminalOptions = { cursorStyle: 'block', bellSound: BellSound, bellStyle: 'none', + fontFamily: 'courier-new, courier, monospace', + fontSize: 15, scrollback: 1000, screenKeys: false, debug: false, @@ -481,6 +483,12 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.element.classList.toggle(`xterm-cursor-style-underline`, value === 'underline'); this.element.classList.toggle(`xterm-cursor-style-bar`, value === 'bar'); break; + case 'fontFamily': + case 'fontSize': + // When the font changes the size of the cells may change which requires a renderer clear + this.renderer.clear(); + this.charMeasure.measure(this.options); + break; case 'scrollback': this.buffers.resize(this.cols, this.rows); this.viewport.syncScrollArea(); @@ -742,15 +750,15 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.parent.appendChild(this.element); this.charMeasure = new CharMeasure(document, this.helperContainer); - this.charMeasure.on('charsizechanged', () => { - this.updateCharSizeStyles(); - }); - this.charMeasure.measure(); this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure); this.renderer = new Renderer(this); this.on('resize', () => this.renderer.onResize(this.cols, this.rows)); - this.charMeasure.on('charsizechanged', () => this.renderer.onCharSizeChanged(this.charMeasure.width, this.charMeasure.height)); + this.charMeasure.on('charsizechanged', () => { + this.renderer.onCharSizeChanged(this.charMeasure.width, this.charMeasure.height); + // Force a refresh for the char size change + this.renderer.queueRefresh(0, this.rows - 1); + }); this.selectionManager = new SelectionManager(this, this.buffer, this.rowContainer, this.charMeasure); this.element.addEventListener('mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e)); @@ -766,6 +774,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.on('scroll', () => this.selectionManager.refresh()); this.viewportElement.addEventListener('scroll', () => this.selectionManager.refresh()); + // Measure the character size + this.charMeasure.measure(this.options); + // Setup loop that draws to screen this.refresh(0, this.rows - 1); @@ -796,17 +807,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT } } - /** - * Updates the helper CSS class with any changes necessary after the terminal's - * character width has been changed. - */ - public updateCharSizeStyles(): void { - this.charSizeStyleElement.textContent = - `.xterm-wide-char{width:${this.charMeasure.width * 2}px;}` + - `.xterm-normal-char{width:${this.charMeasure.width}px;}` + - `.xterm-rows > div{height:${this.charMeasure.height}px;}`; - } - /** * XTerm mouse events * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking @@ -1918,7 +1918,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT if (x === this.cols && y === this.rows) { // Check if we still need to measure the char size (fixes #785). if (!this.charMeasure.width || !this.charMeasure.height) { - this.charMeasure.measure(); + this.charMeasure.measure(this.options); } return; } @@ -1942,7 +1942,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.rows = y; this.buffers.setupTabStops(this.cols); - this.charMeasure.measure(); + this.charMeasure.measure(this.options); this.refresh(0, this.rows - 1); diff --git a/src/renderer/BackgroundRenderLayer.ts b/src/renderer/BackgroundRenderLayer.ts index a9722eab..37b9c69e 100644 --- a/src/renderer/BackgroundRenderLayer.ts +++ b/src/renderer/BackgroundRenderLayer.ts @@ -19,6 +19,11 @@ export class BackgroundRenderLayer extends BaseRenderLayer implements IDataRende this._state.resize(terminal.cols, terminal.rows); } + public clear(terminal: ITerminal): void { + this._state.clear(); + this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); + } + public render(terminal: ITerminal, startRow: number, endRow: number): void { const scaledCharWidth = Math.ceil(terminal.charMeasure.width) * window.devicePixelRatio; const scaledCharHeight = Math.ceil(terminal.charMeasure.height) * window.devicePixelRatio; diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 5e53c411..419b814c 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -39,14 +39,16 @@ export abstract class BaseRenderLayer implements IRenderLayer { BaseRenderLayer._charAtlas = null; BaseRenderLayer._charAtlasCharWidth = terminal.charMeasure.width; BaseRenderLayer._charAtlasCharHeight = terminal.charMeasure.height; - BaseRenderLayer._charAtlasGenerator.generate(terminal.charMeasure.width, terminal.charMeasure.height).then(bitmap => { + BaseRenderLayer._charAtlasGenerator.generate(terminal, terminal.charMeasure.width, terminal.charMeasure.height).then(bitmap => { BaseRenderLayer._charAtlas = bitmap; }); } } } - protected drawChar(char: string, code: number, fg: number, x: number, y: number, scaledCharWidth: number, scaledCharHeight: number): void { + public abstract clear(terminal: ITerminal): void; + + protected drawChar(terminal: ITerminal, char: string, code: number, fg: number, x: number, y: number, scaledCharWidth: number, scaledCharHeight: number): void { let colorIndex = 0; if (fg < 256) { colorIndex = fg + 1; @@ -57,15 +59,15 @@ export abstract class BaseRenderLayer implements IRenderLayer { code * scaledCharWidth, colorIndex * scaledCharHeight, scaledCharWidth, scaledCharHeight, x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); } else { - this._drawUncachedChar(char, fg, x, y, scaledCharWidth, scaledCharHeight); + this._drawUncachedChar(terminal, char, fg, x, y, scaledCharWidth, scaledCharHeight); } // This draws the atlas (for debugging purposes) // this._ctx.drawImage(BaseRenderLayer._charAtlas, 0, 0); } - private _drawUncachedChar(char: string, fg: number, x: number, y: number, scaledCharWidth: number, scaledCharHeight: number): void { + private _drawUncachedChar(terminal: ITerminal, char: string, fg: number, x: number, y: number, scaledCharWidth: number, scaledCharHeight: number): void { this._ctx.save(); - this._ctx.font = `${16 * window.devicePixelRatio}px courier`; + this._ctx.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`; this._ctx.textBaseline = 'top'; // 256 color support @@ -91,16 +93,17 @@ class CharAtlasGenerator { this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); } - public generate(charWidth: number, charHeight: number): Promise { + public generate(terminal: ITerminal, charWidth: number, charHeight: number): Promise { const scaledCharWidth = Math.ceil(charWidth) * window.devicePixelRatio; const scaledCharHeight = Math.ceil(charHeight) * window.devicePixelRatio; - +console.log('generate'); this._canvas.width = 255 * scaledCharWidth; this._canvas.height = (/*default*/1 + /*0-15*/16) * scaledCharHeight; this._ctx.save(); this._ctx.fillStyle = '#ffffff'; - this._ctx.font = `${16 * window.devicePixelRatio}px courier`; + this._ctx.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`; + console.log(this._ctx.font, scaledCharWidth, scaledCharHeight); this._ctx.textBaseline = 'top'; // Default color diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index 75dd4d89..76642229 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -14,6 +14,12 @@ export class CursorRenderLayer extends BaseRenderLayer implements IDataRenderLay this._state = null; } + public clear(terminal: ITerminal): void { + const scaledCharWidth = Math.ceil(terminal.charMeasure.width) * window.devicePixelRatio; + const scaledCharHeight = Math.ceil(terminal.charMeasure.height) * window.devicePixelRatio; + this._clearCursor(scaledCharWidth, scaledCharHeight); + } + public render(terminal: ITerminal, startRow: number, endRow: number): void { // TODO: Track blur/focus somehow, support unfocused cursor @@ -50,7 +56,7 @@ export class CursorRenderLayer extends BaseRenderLayer implements IDataRenderLay this._ctx.restore(); const charData = terminal.buffer.lines.get(viewportRelativeCursorY)[terminal.buffer.x]; - this.drawChar(charData[CHAR_DATA_CHAR_INDEX], charData[CHAR_DATA_CODE_INDEX], COLOR_CODES.BLACK, terminal.buffer.x, viewportRelativeCursorY, scaledCharWidth, scaledCharHeight); + this.drawChar(terminal, charData[CHAR_DATA_CHAR_INDEX], charData[CHAR_DATA_CODE_INDEX], COLOR_CODES.BLACK, terminal.buffer.x, viewportRelativeCursorY, scaledCharWidth, scaledCharHeight); this._state = [terminal.buffer.x, viewportRelativeCursorY]; } diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index c3cb833f..3cd7ae1e 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -20,6 +20,11 @@ export class ForegroundRenderLayer extends BaseRenderLayer implements IDataRende this._state.resize(terminal.cols, terminal.rows); } + public clear(terminal: ITerminal): void { + this._state.clear(); + this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); + } + public render(terminal: ITerminal, startRow: number, endRow: number): void { const scaledCharWidth = Math.ceil(terminal.charMeasure.width) * window.devicePixelRatio; const scaledCharHeight = Math.ceil(terminal.charMeasure.height) * window.devicePixelRatio; @@ -31,10 +36,6 @@ export class ForegroundRenderLayer extends BaseRenderLayer implements IDataRende return; } - this._ctx.fillStyle = '#ffffff'; - this._ctx.textBaseline = 'top'; - this._ctx.font = `${16 * window.devicePixelRatio}px courier`; - for (let y = startRow; y <= endRow; y++) { const row = y + terminal.buffer.ydisp; const line = terminal.buffer.lines.get(row); @@ -82,7 +83,7 @@ export class ForegroundRenderLayer extends BaseRenderLayer implements IDataRende } } - this.drawChar(char, code, fg, x, y, scaledCharWidth, scaledCharHeight); + this.drawChar(terminal, char, code, fg, x, y, scaledCharWidth, scaledCharHeight); } } } diff --git a/src/renderer/GridCache.ts b/src/renderer/GridCache.ts index eba3dad6..a4657fcb 100644 --- a/src/renderer/GridCache.ts +++ b/src/renderer/GridCache.ts @@ -17,4 +17,12 @@ export class GridCache { } this.cache.length = width; } + + public clear() { + for (let x = 0; x < this.cache.length; x++) { + for (let y = 0; y < this.cache[x].length; y++) { + this.cache[x][y] = null; + } + } + } } diff --git a/src/renderer/Interfaces.ts b/src/renderer/Interfaces.ts index 4d6d2d3b..8eb3acdd 100644 --- a/src/renderer/Interfaces.ts +++ b/src/renderer/Interfaces.ts @@ -1,7 +1,15 @@ import { ITerminal } from '../Interfaces'; export interface IRenderLayer { + /** + * Resize the render layer. + */ resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void; + + /** + * Clear the state of the render layer. + */ + clear(terminal: ITerminal): void; } export interface IDataRenderLayer extends IRenderLayer { diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 522f7af2..26b472cf 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -40,6 +40,7 @@ export class Renderer { } public onCharSizeChanged(charWidth: number, charHeight: number): void { + console.log('Renderer.onCharSizeChanged', charWidth, charHeight); const width = Math.ceil(charWidth) * this._terminal.cols; const height = Math.ceil(charHeight) * this._terminal.rows; for (let i = 0; i < this._dataRenderLayers.length; i++) { @@ -56,6 +57,15 @@ export class Renderer { } } + public clear(): void { + for (let i = 0; i < this._dataRenderLayers.length; i++) { + this._dataRenderLayers[i].clear(this._terminal); + } + for (let i = 0; i < this._selectionRenderLayers.length; i++) { + this._selectionRenderLayers[i].clear(this._terminal); + } + } + /** * Queues a refresh between two rows (inclusive), to be done on next animation * frame. @@ -102,64 +112,4 @@ export class Renderer { } this._terminal.emit('refresh', {start, end}); } - - /** - * Refreshes the selection in the DOM. - * @param start The selection start. - * @param end The selection end. - */ - public refreshSelection(start: [number, number], end: [number, number]): void { - // Remove all selections - while (this._terminal.selectionContainer.children.length) { - this._terminal.selectionContainer.removeChild(this._terminal.selectionContainer.children[0]); - } - - // Selection does not exist - if (!start || !end) { - return; - } - - // Translate from buffer position to viewport position - const viewportStartRow = start[1] - this._terminal.buffer.ydisp; - const viewportEndRow = end[1] - this._terminal.buffer.ydisp; - const viewportCappedStartRow = Math.max(viewportStartRow, 0); - const viewportCappedEndRow = Math.min(viewportEndRow, this._terminal.rows - 1); - - // No need to draw the selection - if (viewportCappedStartRow >= this._terminal.rows || viewportCappedEndRow < 0) { - return; - } - - // Create the selections - const documentFragment = document.createDocumentFragment(); - // Draw first row - const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0; - const endCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : this._terminal.cols; - documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow, startCol, endCol)); - // Draw middle rows - const middleRowsCount = viewportCappedEndRow - viewportCappedStartRow - 1; - documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow + 1, 0, this._terminal.cols, middleRowsCount)); - // Draw final row - if (viewportCappedStartRow !== viewportCappedEndRow) { - // Only draw viewportEndRow if it's not the same as viewporttartRow - const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._terminal.cols; - documentFragment.appendChild(this._createSelectionElement(viewportCappedEndRow, 0, endCol)); - } - this._terminal.selectionContainer.appendChild(documentFragment); - } - - /** - * Creates a selection element at the specified position. - * @param row The row of the selection. - * @param colStart The start column. - * @param colEnd The end columns. - */ - private _createSelectionElement(row: number, colStart: number, colEnd: number, rowCount: number = 1): HTMLElement { - const element = document.createElement('div'); - element.style.height = `${rowCount * this._terminal.charMeasure.height}px`; - element.style.top = `${row * this._terminal.charMeasure.height}px`; - element.style.left = `${colStart * this._terminal.charMeasure.width}px`; - element.style.width = `${this._terminal.charMeasure.width * (colEnd - colStart)}px`; - return element; - } } diff --git a/src/renderer/SelectionRenderLayer.ts b/src/renderer/SelectionRenderLayer.ts index 048be8e4..a2cfb1d6 100644 --- a/src/renderer/SelectionRenderLayer.ts +++ b/src/renderer/SelectionRenderLayer.ts @@ -16,6 +16,16 @@ export class SelectionRenderLayer extends BaseRenderLayer implements ISelectionR }; } + public clear(terminal: ITerminal): void { + if (this._state.start && this._state.end) { + this._state = { + start: null, + end: null + }; + this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); + } + } + public render(terminal: ITerminal, start: [number, number], end: [number, number]): void { const scaledCharWidth = Math.ceil(terminal.charMeasure.width) * window.devicePixelRatio; const scaledCharHeight = Math.ceil(terminal.charMeasure.height) * window.devicePixelRatio; diff --git a/src/utils/CharMeasure.test.ts b/src/utils/CharMeasure.test.ts index 68e1b6a7..02a8dae9 100644 --- a/src/utils/CharMeasure.test.ts +++ b/src/utils/CharMeasure.test.ts @@ -25,13 +25,13 @@ describe('CharMeasure', () => { describe('measure', () => { it('should set _measureElement on first call', () => { - charMeasure.measure(); + charMeasure.measure({}); assert.isDefined((charMeasure)._measureElement, 'CharMeasure.measure should have created _measureElement'); }); it('should be performed async on first call', done => { assert.equal(charMeasure.width, null); - charMeasure.measure(); + charMeasure.measure({}); // Mock getBoundingClientRect since jsdom doesn't have a layout engine (charMeasure)._measureElement.getBoundingClientRect = () => { return { width: 1, height: 1 }; @@ -44,7 +44,7 @@ describe('CharMeasure', () => { }); it('should be performed sync on successive calls', done => { - charMeasure.measure(); + charMeasure.measure({}); // Mock getBoundingClientRect since jsdom doesn't have a layout engine (charMeasure)._measureElement.getBoundingClientRect = () => { return { width: 1, height: 1 }; @@ -55,19 +55,19 @@ describe('CharMeasure', () => { (charMeasure)._measureElement.getBoundingClientRect = () => { return { width: 2, height: 2 }; }; - charMeasure.measure(); + charMeasure.measure({}); assert.equal(charMeasure.width, firstWidth * 2); done(); }, 0); }); it('should NOT do a measure when the parent is hidden', done => { - charMeasure.measure(); + charMeasure.measure({}); setTimeout(() => { const firstWidth = charMeasure.width; container.style.display = 'none'; container.style.fontSize = '2em'; - charMeasure.measure(); + charMeasure.measure({}); assert.equal(charMeasure.width, firstWidth); done(); }, 0); diff --git a/src/utils/CharMeasure.ts b/src/utils/CharMeasure.ts index 45b06f59..ff777811 100644 --- a/src/utils/CharMeasure.ts +++ b/src/utils/CharMeasure.ts @@ -4,11 +4,14 @@ */ import { EventEmitter } from '../EventEmitter.js'; +import { ICharMeasure, ITerminal, ITerminalOptions } from '../Interfaces'; /** - * Utility class that measures the size of a character. + * Utility class that measures the size of a character. Measurements are done in + * the DOM rather than with a canvas context because support for extracting the + * height of characters is patchy across browsers. */ -export class CharMeasure extends EventEmitter { +export class CharMeasure extends EventEmitter implements ICharMeasure { private _document: Document; private _parentElement: HTMLElement; private _measureElement: HTMLElement; @@ -29,7 +32,7 @@ export class CharMeasure extends EventEmitter { return this._height; } - public measure(): void { + public measure(options: ITerminalOptions): void { if (!this._measureElement) { this._measureElement = this._document.createElement('span'); this._measureElement.style.position = 'absolute'; @@ -40,13 +43,15 @@ export class CharMeasure extends EventEmitter { this._parentElement.appendChild(this._measureElement); // Perform _doMeasure async if the element was just attached as sometimes // getBoundingClientRect does not return accurate values without this. - setTimeout(() => this._doMeasure(), 0); + setTimeout(() => this._doMeasure(options), 0); } else { - this._doMeasure(); + this._doMeasure(options); } } - private _doMeasure(): void { + private _doMeasure(options: ITerminalOptions): void { + this._measureElement.style.fontFamily = options.fontFamily; + this._measureElement.style.fontSize = `${options.fontSize}px`; const geometry = this._measureElement.getBoundingClientRect(); // The element is likely currently display:none, we should retain the // previous value. diff --git a/src/xterm.css b/src/xterm.css index f281e527..6a7504f3 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -164,11 +164,6 @@ top: 0; } -/* .terminal .xterm-bg-layer { z-index: 0; } -.terminal .xterm-selection-layer { z-index: 1; } -.terminal .xterm-fg-layer { z-index: 2; } -.terminal .xterm-cursor-layer { z-index: 3; } */ - .terminal .xterm-rows { position: absolute; left: 0; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index ccc83ed9..dce15bca 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -313,7 +313,7 @@ declare module 'xterm' { * Retrieves an option's value from the terminal. * @param key The option key. */ - getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'termName'): string; + getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'termName'): string; /** * Retrieves an option's value from the terminal. * @param key The option key. @@ -328,7 +328,7 @@ declare module 'xterm' { * Retrieves an option's value from the terminal. * @param key The option key. */ - getOption(key: 'cols' | 'rows' | 'tabStopWidth' | 'scrollback'): number; + getOption(key: 'cols' | 'fontSize' | 'rows' | 'tabStopWidth' | 'scrollback'): number; /** * Retrieves an option's value from the terminal. * @param key The option key. @@ -350,7 +350,7 @@ declare module 'xterm' { * @param key The option key. * @param value The option value. */ - setOption(key: 'termName' | 'bellSound', value: string): void; + setOption(key: 'fontFamily' | 'termName' | 'bellSound', value: string): void; /** * Sets an option on 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' | 'rows' | 'tabStopWidth' | 'scrollback', value: number): void; + setOption(key: 'cols' | 'fontSize' | 'rows' | 'tabStopWidth' | 'scrollback', value: number): void; /** * Sets an option on the terminal. * @param key The option key.