From 693d09cbad07e7d8b0ba88ca7dadda8128be4e2a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 24 Sep 2022 14:41:48 -0700 Subject: [PATCH 1/3] Allow setting renderer addon before open is called --- addons/xterm-addon-webgl/src/WebglAddon.ts | 3 +- demo/client.ts | 18 +++----- src/browser/Terminal.ts | 11 ++++- src/browser/TestUtils.test.ts | 4 ++ src/browser/Types.d.ts | 1 + src/browser/services/RenderService.ts | 51 ++++++++++++++++------ src/browser/services/Services.ts | 1 + 7 files changed, 62 insertions(+), 27 deletions(-) diff --git a/addons/xterm-addon-webgl/src/WebglAddon.ts b/addons/xterm-addon-webgl/src/WebglAddon.ts index 5b98a048d7..a2441a2a1a 100644 --- a/addons/xterm-addon-webgl/src/WebglAddon.ts +++ b/addons/xterm-addon-webgl/src/WebglAddon.ts @@ -26,7 +26,8 @@ export class WebglAddon implements ITerminalAddon { public activate(terminal: Terminal): void { if (!terminal.element) { - throw new Error('Cannot activate WebglAddon before Terminal.open'); + (terminal as any)._core.onWillOpen(() => this.activate(terminal)); + return; } if (isSafari) { throw new Error('Webgl is not currently supported on Safari'); diff --git a/demo/client.ts b/demo/client.ts index 00b10b5711..31a3347cd5 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -267,18 +267,14 @@ function createTerminal(): void { protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://'; socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/'; - term.open(terminalContainer); addons.fit.instance!.fit(); - try { - typedTerm.loadAddon(addons.webgl.instance); - setTimeout(() => { - addTextureAtlas(addons.webgl.instance.textureAtlas); - addons.webgl.instance.onChangeTextureAtlas(e => addTextureAtlas(e)); - }, 0); - } - catch { - addons.webgl.instance = undefined; - } + typedTerm.loadAddon(addons.webgl.instance); + setTimeout(() => { + addTextureAtlas(addons.webgl.instance.textureAtlas); + addons.webgl.instance.onChangeTextureAtlas(e => addTextureAtlas(e)); + }, 0); + + term.open(terminalContainer); term.focus(); addDomListener(paddingElement, 'change', setPadding); diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index afdac748c4..728237b9f2 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -143,6 +143,9 @@ export class Terminal extends CoreTerminal implements ITerminal { public get onA11yChar(): IEvent { return this._onA11yCharEmitter.event; } private _onA11yTabEmitter = new EventEmitter(); public get onA11yTab(): IEvent { return this._onA11yTabEmitter.event; } + private _onWillOpen = new EventEmitter(); + public get onWillOpen(): IEvent { return this._onWillOpen.event; } + /** * Creates a new `Terminal` object. @@ -509,12 +512,16 @@ export class Terminal extends CoreTerminal implements ITerminal { this._characterJoinerService = this._instantiationService.createInstance(CharacterJoinerService); this._instantiationService.setService(ICharacterJoinerService, this._characterJoinerService); - const renderer = this._createRenderer(); - this._renderService = this.register(this._instantiationService.createInstance(RenderService, renderer, this.rows, this.screenElement)); + this._renderService = this.register(this._instantiationService.createInstance(RenderService, this.rows, this.screenElement)); this._instantiationService.setService(IRenderService, this._renderService); this.register(this._renderService.onRenderedViewportChange(e => this._onRender.fire(e))); this.onResize(e => this._renderService!.resize(e.cols, e.rows)); + this._onWillOpen.fire(this.element); + if (!this._renderService.hasRenderer()) { + this._renderService.setRenderer(this._createRenderer()); + } + this._compositionView = document.createElement('div'); this._compositionView.classList.add('composition-view'); this._compositionHelper = this._instantiationService.createInstance(CompositionHelper, this.textarea, this._compositionView); diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index 0b5e00c173..fb0988505e 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -41,6 +41,7 @@ export class MockTerminal implements ITerminal { public onTitleChange!: IEvent; public onBell!: IEvent; public onScroll!: IEvent; + public onWillOpen!: IEvent; public onKey!: IEvent<{ key: string, domEvent: KeyboardEvent }>; public onRender!: IEvent<{ start: number, end: number }>; public onResize!: IEvent<{ cols: number, rows: number }>; @@ -400,6 +401,9 @@ export class MockRenderService implements IRenderService { public resize(cols: number, rows: number): void { throw new Error('Method not implemented.'); } + public hasRenderer(): boolean { + throw new Error('Method not implemented.'); + } public setRenderer(renderer: IRenderer): void { throw new Error('Method not implemented.'); } diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index 48461a5e34..7e9053e14b 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -23,6 +23,7 @@ export interface ITerminal extends IPublicTerminal, ICoreTerminal { onFocus: IEvent; onA11yChar: IEvent; onA11yTab: IEvent; + onWillOpen: IEvent; cancel(ev: Event, force?: boolean): boolean | void; } diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index 3849bdd33a..6c5737c761 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -23,6 +23,7 @@ interface ISelectionState { export class RenderService extends Disposable implements IRenderService { public serviceBrand: undefined; + private _renderer: IRenderer | undefined; private _renderDebouncer: IRenderDebouncerWithCallback; private _screenDprMonitor: ScreenDprMonitor; private _pausedResizeTask = new DebouncedIdleTask(); @@ -48,10 +49,9 @@ export class RenderService extends Disposable implements IRenderService { private _onRefreshRequest = new EventEmitter<{ start: number, end: number }>(); public get onRefreshRequest(): IEvent<{ start: number, end: number }> { return this._onRefreshRequest.event; } - public get dimensions(): IRenderDimensions { return this._renderer.dimensions; } + public get dimensions(): IRenderDimensions { return this._renderer!.dimensions; } constructor( - private _renderer: IRenderer, private _rowCount: number, screenElement: HTMLElement, @IOptionsService optionsService: IOptionsService, @@ -62,7 +62,7 @@ export class RenderService extends Disposable implements IRenderService { ) { super(); - this.register({ dispose: () => this._renderer.dispose() }); + this.register({ dispose: () => this._renderer?.dispose() }); this._renderDebouncer = new RenderDebouncer(coreBrowserService.window, (start, end) => this._renderRows(start, end)); this.register(this._renderDebouncer); @@ -83,7 +83,7 @@ export class RenderService extends Disposable implements IRenderService { this.register(decorationService.onDecorationRemoved(() => this._fullRefresh())); // No need to register this as renderer is explicitly disposed in RenderService.dispose - this._renderer.onRequestRedraw(e => this.refreshRows(e.start, e.end, true)); + // this._renderer.onRequestRedraw(e => this.refreshRows(e.start, e.end, true)); // dprchange should handle this case, we need this as well for browsers that don't support the // matchMedia query. @@ -125,6 +125,9 @@ export class RenderService extends Disposable implements IRenderService { } private _renderRows(start: number, end: number): void { + if (!this._renderer) { + return; + } this._renderer.renderRows(start, end); // Update selection if needed @@ -147,12 +150,18 @@ export class RenderService extends Disposable implements IRenderService { } private _handleOptionsChanged(): void { + if (!this._renderer) { + return; + } this._renderer.onOptionsChanged(); this.refreshRows(0, this._rowCount - 1); this._fireOnCanvasResize(); } private _fireOnCanvasResize(): void { + if (!this._renderer) { + return; + } // Don't fire the event if the dimensions haven't changed if (this._renderer.dimensions.canvasWidth === this._canvasWidth && this._renderer.dimensions.canvasHeight === this._canvasHeight) { return; @@ -164,9 +173,13 @@ export class RenderService extends Disposable implements IRenderService { super.dispose(); } + public hasRenderer(): boolean { + return !!this._renderer; + } + public setRenderer(renderer: IRenderer): void { // TODO: RenderService should be the only one to dispose the renderer - this._renderer.dispose(); + this._renderer?.dispose(); this._renderer = renderer; this._renderer.onRequestRedraw(e => this.refreshRows(e.start, e.end, true)); @@ -188,11 +201,17 @@ export class RenderService extends Disposable implements IRenderService { } public clearTextureAtlas(): void { - this._renderer?.clearTextureAtlas?.(); + if (!this._renderer) { + return; + } + this._renderer.clearTextureAtlas?.(); this._fullRefresh(); } public setColors(colors: IColorSet): void { + if (!this._renderer) { + return; + } this._renderer.setColors(colors); this._fullRefresh(); } @@ -202,13 +221,19 @@ export class RenderService extends Disposable implements IRenderService { // when devicePixelRatio changes this._charSizeService.measure(); + if (!this._renderer) { + return; + } this._renderer.onDevicePixelRatioChange(); this.refreshRows(0, this._rowCount - 1); } public onResize(cols: number, rows: number): void { + if (!this._renderer) { + return; + } if (this._isPaused) { - this._pausedResizeTask.set(() => this._renderer.onResize(cols, rows)); + this._pausedResizeTask.set(() => this._renderer!.onResize(cols, rows)); } else { this._renderer.onResize(cols, rows); } @@ -217,29 +242,29 @@ export class RenderService extends Disposable implements IRenderService { // TODO: Is this useful when we have onResize? public onCharSizeChanged(): void { - this._renderer.onCharSizeChanged(); + this._renderer?.onCharSizeChanged(); } public onBlur(): void { - this._renderer.onBlur(); + this._renderer?.onBlur(); } public onFocus(): void { - this._renderer.onFocus(); + this._renderer?.onFocus(); } public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void { this._selectionState.start = start; this._selectionState.end = end; this._selectionState.columnSelectMode = columnSelectMode; - this._renderer.onSelectionChanged(start, end, columnSelectMode); + this._renderer?.onSelectionChanged(start, end, columnSelectMode); } public onCursorMove(): void { - this._renderer.onCursorMove(); + this._renderer?.onCursorMove(); } public clear(): void { - this._renderer.clear(); + this._renderer?.clear(); } } diff --git a/src/browser/services/Services.ts b/src/browser/services/Services.ts index ab91f8a32f..3b52501b95 100644 --- a/src/browser/services/Services.ts +++ b/src/browser/services/Services.ts @@ -71,6 +71,7 @@ export interface IRenderService extends IDisposable { refreshRows(start: number, end: number): void; clearTextureAtlas(): void; resize(cols: number, rows: number): void; + hasRenderer(): boolean; setRenderer(renderer: IRenderer): void; setColors(colors: IColorSet): void; onDevicePixelRatioChange(): void; From 2febe19222b75fef2cbfb3ed5381aa7ca715107b Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 24 Sep 2022 14:42:57 -0700 Subject: [PATCH 2/3] Set renderer after screen element is attached to dom --- src/browser/Terminal.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index 728237b9f2..aae5cdefd8 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -517,11 +517,6 @@ export class Terminal extends CoreTerminal implements ITerminal { this.register(this._renderService.onRenderedViewportChange(e => this._onRender.fire(e))); this.onResize(e => this._renderService!.resize(e.cols, e.rows)); - this._onWillOpen.fire(this.element); - if (!this._renderService.hasRenderer()) { - this._renderService.setRenderer(this._createRenderer()); - } - this._compositionView = document.createElement('div'); this._compositionView.classList.add('composition-view'); this._compositionHelper = this._instantiationService.createInstance(CompositionHelper, this.textarea, this._compositionView); @@ -530,6 +525,11 @@ export class Terminal extends CoreTerminal implements ITerminal { // Performance: Add viewport and helper elements from the fragment this.element.appendChild(fragment); + this._onWillOpen.fire(this.element); + if (!this._renderService.hasRenderer()) { + this._renderService.setRenderer(this._createRenderer()); + } + this._mouseService = this._instantiationService.createInstance(MouseService); this._instantiationService.setService(IMouseService, this._mouseService); From 868fc7ec2954b4529b0dc5e80688a753c8955a23 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 24 Sep 2022 14:44:13 -0700 Subject: [PATCH 3/3] Allow loading canvas before open --- addons/xterm-addon-canvas/src/CanvasAddon.ts | 26 +++++++++++--------- addons/xterm-addon-webgl/src/WebglAddon.ts | 15 +++++------ 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/addons/xterm-addon-canvas/src/CanvasAddon.ts b/addons/xterm-addon-canvas/src/CanvasAddon.ts index 9fca00a646..32fe242e21 100644 --- a/addons/xterm-addon-canvas/src/CanvasAddon.ts +++ b/addons/xterm-addon-canvas/src/CanvasAddon.ts @@ -14,21 +14,23 @@ export class CanvasAddon implements ITerminalAddon { private _renderer?: CanvasRenderer; public activate(terminal: Terminal): void { + const core = (terminal as any)._core; if (!terminal.element) { - throw new Error('Cannot activate CanvasAddon before Terminal.open'); + core.onWillOpen(() => this.activate(terminal)); + return; } this._terminal = terminal; - const bufferService: IBufferService = (terminal as any)._core._bufferService; - const renderService: IRenderService = (terminal as any)._core._renderService; - const characterJoinerService: ICharacterJoinerService = (terminal as any)._core._characterJoinerService; - const charSizeService: ICharSizeService = (terminal as any)._core._charSizeService; - const coreService: ICoreService = (terminal as any)._core.coreService; - const coreBrowserService: ICoreBrowserService = (terminal as any)._core._coreBrowserService; - const decorationService: IDecorationService = (terminal as any)._core._decorationService; - const optionsService: IOptionsService = (terminal as any)._core.optionsService; - const colors: IColorSet = (terminal as any)._core._colorManager.colors; - const screenElement: HTMLElement = (terminal as any)._core.screenElement; - const linkifier = (terminal as any)._core.linkifier2; + const bufferService: IBufferService = core._bufferService; + const renderService: IRenderService = core._renderService; + const characterJoinerService: ICharacterJoinerService = core._characterJoinerService; + const charSizeService: ICharSizeService = core._charSizeService; + const coreService: ICoreService = core.coreService; + const coreBrowserService: ICoreBrowserService = core._coreBrowserService; + const decorationService: IDecorationService = core._decorationService; + const optionsService: IOptionsService = core.optionsService; + const colors: IColorSet = core._colorManager.colors; + const screenElement: HTMLElement = core.screenElement; + const linkifier = core.linkifier2; this._renderer = new CanvasRenderer(colors, screenElement, linkifier, bufferService, charSizeService, optionsService, characterJoinerService, coreService, coreBrowserService, decorationService); renderService.setRenderer(this._renderer); renderService.onResize(bufferService.cols, bufferService.rows); diff --git a/addons/xterm-addon-webgl/src/WebglAddon.ts b/addons/xterm-addon-webgl/src/WebglAddon.ts index a2441a2a1a..e8fa87998e 100644 --- a/addons/xterm-addon-webgl/src/WebglAddon.ts +++ b/addons/xterm-addon-webgl/src/WebglAddon.ts @@ -25,20 +25,21 @@ export class WebglAddon implements ITerminalAddon { ) {} public activate(terminal: Terminal): void { + const core = (terminal as any)._core; if (!terminal.element) { - (terminal as any)._core.onWillOpen(() => this.activate(terminal)); + core.onWillOpen(() => this.activate(terminal)); return; } if (isSafari) { throw new Error('Webgl is not currently supported on Safari'); } this._terminal = terminal; - const renderService: IRenderService = (terminal as any)._core._renderService; - const characterJoinerService: ICharacterJoinerService = (terminal as any)._core._characterJoinerService; - const coreBrowserService: ICoreBrowserService = (terminal as any)._core._coreBrowserService; - const coreService: ICoreService = (terminal as any)._core.coreService; - const decorationService: IDecorationService = (terminal as any)._core._decorationService; - const colors: IColorSet = (terminal as any)._core._colorManager.colors; + const renderService: IRenderService = core._renderService; + const characterJoinerService: ICharacterJoinerService = core._characterJoinerService; + const coreBrowserService: ICoreBrowserService = core._coreBrowserService; + const coreService: ICoreService = core.coreService; + const decorationService: IDecorationService = core._decorationService; + const colors: IColorSet = core._colorManager.colors; this._renderer = new WebglRenderer(terminal, colors, characterJoinerService, coreBrowserService, coreService, decorationService, this._preserveDrawingBuffer); forwardEvent(this._renderer.onContextLoss, this._onContextLoss); forwardEvent(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas);