/** * Copyright (c) 2019 The xterm.js authors. All rights reserved. * @license MIT */ import { IRenderer, IRenderDimensions } from 'browser/renderer/Types'; import { RenderDebouncer } from 'browser/RenderDebouncer'; import { EventEmitter, IEvent } from 'common/EventEmitter'; import { Disposable } from 'common/Lifecycle'; import { ScreenDprMonitor } from 'browser/ScreenDprMonitor'; import { addDisposableDomListener } from 'browser/Lifecycle'; import { IColorSet, IRenderDebouncer } from 'browser/Types'; import { IOptionsService, IBufferService } from 'common/services/Services'; import { ICharSizeService, IRenderService } from 'browser/services/Services'; interface ISelectionState { start: [number, number] | undefined; end: [number, number] | undefined; columnSelectMode: boolean; } export class RenderService extends Disposable implements IRenderService { public serviceBrand: undefined; private _renderDebouncer: IRenderDebouncer; private _screenDprMonitor: ScreenDprMonitor; private _isPaused: boolean = false; private _needsFullRefresh: boolean = false; private _isNextRenderRedrawOnly: boolean = true; private _needsSelectionRefresh: boolean = false; private _canvasWidth: number = 0; private _canvasHeight: number = 0; private _selectionState: ISelectionState = { start: undefined, end: undefined, columnSelectMode: false }; private _onDimensionsChange = new EventEmitter(); public get onDimensionsChange(): IEvent { return this._onDimensionsChange.event; } private _onRender = new EventEmitter<{ start: number, end: number }>(); public get onRenderedBufferChange(): IEvent<{ start: number, end: number }> { return this._onRender.event; } 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; } constructor( private _renderer: IRenderer, private _rowCount: number, screenElement: HTMLElement, @IOptionsService optionsService: IOptionsService, @ICharSizeService private readonly _charSizeService: ICharSizeService, @IBufferService bufferService: IBufferService ) { super(); this.register({ dispose: () => this._renderer.dispose() }); this._renderDebouncer = new RenderDebouncer((start, end) => this._renderRows(start, end)); this.register(this._renderDebouncer); this._screenDprMonitor = new ScreenDprMonitor(); this._screenDprMonitor.setListener(() => this.onDevicePixelRatioChange()); this.register(this._screenDprMonitor); this.register(bufferService.onResize(e => this._fullRefresh())); this.register(optionsService.onOptionChange(() => this._renderer.onOptionsChanged())); this.register(this._charSizeService.onCharSizeChange(() => this.onCharSizeChanged())); // No need to register this as renderer is explicitly disposed in RenderService.dispose 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. this.register(addDisposableDomListener(window, 'resize', () => this.onDevicePixelRatioChange())); // Detect whether IntersectionObserver is detected and enable renderer pause // and resume based on terminal visibility if so if ('IntersectionObserver' in window) { const observer = new IntersectionObserver(e => this._onIntersectionChange(e[e.length - 1]), { threshold: 0 }); observer.observe(screenElement); this.register({ dispose: () => observer.disconnect() }); } } private _onIntersectionChange(entry: IntersectionObserverEntry): void { this._isPaused = entry.isIntersecting === undefined ? (entry.intersectionRatio === 0) : !entry.isIntersecting; // Terminal was hidden on open if (!this._isPaused && !this._charSizeService.hasValidSize) { this._charSizeService.measure(); } if (!this._isPaused && this._needsFullRefresh) { this.refreshRows(0, this._rowCount - 1); this._needsFullRefresh = false; } } public refreshRows(start: number, end: number, isRedrawOnly: boolean = false): void { if (this._isPaused) { this._needsFullRefresh = true; return; } if (!isRedrawOnly) { this._isNextRenderRedrawOnly = false; } this._renderDebouncer.refresh(start, end, this._rowCount); } private _renderRows(start: number, end: number): void { this._renderer.renderRows(start, end); // Update selection if needed if (this._needsSelectionRefresh) { this._renderer.onSelectionChanged(this._selectionState.start, this._selectionState.end, this._selectionState.columnSelectMode); this._needsSelectionRefresh = false; } // Fire render event only if it was not a redraw if (!this._isNextRenderRedrawOnly) { this._onRender.fire({ start, end }); } this._isNextRenderRedrawOnly = true; } public resize(cols: number, rows: number): void { this._rowCount = rows; this._fireOnCanvasResize(); } public changeOptions(): void { this._renderer.onOptionsChanged(); this.refreshRows(0, this._rowCount - 1); this._fireOnCanvasResize(); } private _fireOnCanvasResize(): void { // 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; } this._onDimensionsChange.fire(this._renderer.dimensions); } public dispose(): void { super.dispose(); } public setRenderer(renderer: IRenderer): void { // TODO: RenderService should be the only one to dispose the renderer this._renderer.dispose(); this._renderer = renderer; this._renderer.onRequestRedraw(e => this.refreshRows(e.start, e.end, true)); // Force a refresh this._needsSelectionRefresh = true; this._fullRefresh(); } private _fullRefresh(): void { if (this._isPaused) { this._needsFullRefresh = true; } else { this.refreshRows(0, this._rowCount - 1); } } public clearTextureAtlas(): void { this._renderer?.clearTextureAtlas?.(); this._fullRefresh(); } public setColors(colors: IColorSet): void { this._renderer.setColors(colors); this._fullRefresh(); } public onDevicePixelRatioChange(): void { // Force char size measurement as DomMeasureStrategy(getBoundingClientRect) is not stable // when devicePixelRatio changes this._charSizeService.measure(); this._renderer.onDevicePixelRatioChange(); this.refreshRows(0, this._rowCount - 1); } public onResize(cols: number, rows: number): void { this._renderer.onResize(cols, rows); this._fullRefresh(); } // TODO: Is this useful when we have onResize? public onCharSizeChanged(): void { this._renderer.onCharSizeChanged(); } public onBlur(): void { this._renderer.onBlur(); } public onFocus(): void { 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); } public onCursorMove(): void { this._renderer.onCursorMove(); } public clear(): void { this._renderer.clear(); } }