From 5aaf9ef11b3afb046d7b77758c0dfeb18571d629 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 29 Jul 2018 13:49:13 -0700 Subject: [PATCH] Allow renderer to be swapped in and out at runtime Fixes #1589 --- demo/main.js | 7 +++---- src/Terminal.ts | 35 ++++++++++++++++++++++++++------- src/common/Lifecycle.ts | 12 +++++++++++ src/renderer/BaseRenderLayer.ts | 4 ++++ src/renderer/Renderer.ts | 5 +++++ src/renderer/Types.ts | 2 +- src/renderer/dom/DomRenderer.ts | 9 +++++++++ 7 files changed, 62 insertions(+), 12 deletions(-) diff --git a/demo/main.js b/demo/main.js index a9b11655..70544e38 100644 --- a/demo/main.js +++ b/demo/main.js @@ -174,9 +174,7 @@ function initOptions(term) { 'termName', 'useFlowControl', // Complex option - 'theme', - // Only in constructor - 'rendererType' + 'theme' ]; var stringOptions = { bellSound: null, @@ -185,7 +183,8 @@ function initOptions(term) { experimentalCharAtlas: ['none', 'static', 'dynamic'], fontFamily: null, fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], - fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'] + fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], + rendererType: ['dom', 'canvas'] }; var options = Object.keys(term._core.options); var booleanOptions = []; diff --git a/src/Terminal.ts b/src/Terminal.ts index e8366290..43ccaa08 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -72,7 +72,7 @@ const WRITE_BATCH_SIZE = 300; /** * The set of options that only have an effect when set in the Terminal constructor. */ -const CONSTRUCTOR_ONLY_OPTIONS = ['cols', 'rows', 'rendererType']; +const CONSTRUCTOR_ONLY_OPTIONS = ['cols', 'rows']; const DEFAULT_OPTIONS: ITerminalOptions = { cols: 80, @@ -370,6 +370,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II if (CONSTRUCTOR_ONLY_OPTIONS.indexOf(key) !== -1) { console.error(`Option "${key}" can only be set in the constructor`); } + if (this.options[key] === value) { + return; + } switch (key) { case 'bellStyle': if (!value) { @@ -396,6 +399,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II console.warn(`${key} cannot be less than 1, value: ${value}`); return; } + case 'rendererType': + if (!value) { + value = 'canvas'; + } + break; case 'tabStopWidth': if (value < 1) { console.warn(`${key} cannot be less than 1, value: ${value}`); @@ -455,6 +463,15 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.renderer.onResize(this.cols, this.rows); this.refresh(0, this.rows - 1); } + case 'rendererType': + if (this.renderer) { + this.unregister(this.renderer); + this.renderer.dispose(); + this.renderer = null; + } + this._setupRenderer(); + this.renderer.onCharSizeChanged(); + break; case 'scrollback': this.buffers.resize(this.cols, this.rows); if (this.viewport) { @@ -674,12 +691,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // Performance: Add viewport and helper elements from the fragment this.element.appendChild(fragment); - switch (this.options.rendererType) { - case 'canvas': this.renderer = new Renderer(this, this.options.theme); break; - case 'dom': this.renderer = new DomRenderer(this, this.options.theme); break; - default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`); - } - this.register(this.renderer); + this._setupRenderer(); this.options.theme = null; this.viewport = new Viewport(this, this._viewportElement, this._viewportScrollArea, this.charMeasure); this.viewport.onThemeChanged(this.renderer.colorManager.colors); @@ -736,6 +748,15 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } + private _setupRenderer(): void { + switch (this.options.rendererType) { + case 'canvas': this.renderer = new Renderer(this, this.options.theme); break; + case 'dom': this.renderer = new DomRenderer(this, this.options.theme); break; + default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`); + } + this.register(this.renderer); + } + /** * Sets the theme on the renderer. The renderer must have been initialized. * @param theme The theme to ste. diff --git a/src/common/Lifecycle.ts b/src/common/Lifecycle.ts index b9cf1418..46828521 100644 --- a/src/common/Lifecycle.ts +++ b/src/common/Lifecycle.ts @@ -30,4 +30,16 @@ export abstract class Disposable implements IDisposable { public register(d: T): void { this._disposables.push(d); } + + /** + * Unregisters a disposable object if it has been registered, if not do + * nothing. + * @param d The disposable to unregister. + */ + public unregister(d: T): void { + const index = this._disposables.indexOf(d); + if (index !== -1) { + this._disposables.splice(index, 1); + } + } } diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 111e63ff..19c23dc0 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -36,6 +36,10 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._container.appendChild(this._canvas); } + public dispose(): void { + this._container.removeChild(this._canvas); + } + private _initCanvas(): void { this._ctx = this._canvas.getContext('2d', {alpha: this._alpha}); // Draw the background if this is an opaque layer diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 406717b5..b7a3be1d 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -76,6 +76,11 @@ export class Renderer extends EventEmitter implements IRenderer { } } + public dispose(): void { + super.dispose(); + this._renderLayers.forEach(l => l.dispose()); + } + public onIntersectionChange(entry: IntersectionObserverEntry): void { this._isPaused = entry.intersectionRatio === 0; if (!this._isPaused && this._needsFullRefresh) { diff --git a/src/renderer/Types.ts b/src/renderer/Types.ts index 4006f28c..824304ed 100644 --- a/src/renderer/Types.ts +++ b/src/renderer/Types.ts @@ -66,7 +66,7 @@ export interface IRenderDimensions { actualCellHeight: number; } -export interface IRenderLayer { +export interface IRenderLayer extends IDisposable { /** * Called when the terminal loses focus. */ diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts index e336c79e..32dea6ed 100644 --- a/src/renderer/dom/DomRenderer.ts +++ b/src/renderer/dom/DomRenderer.ts @@ -81,6 +81,15 @@ export class DomRenderer extends EventEmitter implements IRenderer { this._terminal.screenElement.appendChild(this._selectionContainer); } + public dispose(): void { + this._terminal.element.classList.remove(TERMINAL_CLASS_PREFIX + this._terminalClass); + this._terminal.screenElement.removeChild(this._rowContainer); + this._terminal.screenElement.removeChild(this._selectionContainer); + this._terminal.screenElement.removeChild(this._themeStyleElement); + this._terminal.screenElement.removeChild(this._dimensionsStyleElement); + super.dispose(); + } + private _updateDimensions(): void { this.dimensions.scaledCharWidth = this._terminal.charMeasure.width * window.devicePixelRatio; this.dimensions.scaledCharHeight = this._terminal.charMeasure.height * window.devicePixelRatio;