diff --git a/src/AccessibilityManager.ts b/src/AccessibilityManager.ts index 6f80b08c..ed190400 100644 --- a/src/AccessibilityManager.ts +++ b/src/AccessibilityManager.ts @@ -5,6 +5,7 @@ import { ITerminal, IBuffer, IDisposable } from './Interfaces'; import { isMac } from './utils/Browser'; +import { RenderDebouncer } from './utils/RenderDebouncer'; const MAX_ROWS_TO_READ = 20; @@ -15,9 +16,7 @@ export class AccessibilityManager implements IDisposable { private _liveRegion: HTMLElement; private _liveRegionLineCount: number = 0; - private _refreshRowStart: number; - private _refreshRowEnd: number; - private _refreshAnimationFrame: number = null; + private _renderRowsDebouncer: RenderDebouncer; private _disposables: IDisposable[] = []; @@ -44,6 +43,8 @@ export class AccessibilityManager implements IDisposable { this._refreshRowsDimensions(); this._accessibilityTreeRoot.appendChild(this._rowContainer); + this._renderRowsDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this)); + this._liveRegion = document.createElement('div'); this._liveRegion.classList.add('live-region'); this._liveRegion.setAttribute('aria-live', 'assertive'); @@ -80,10 +81,7 @@ export class AccessibilityManager implements IDisposable { } public dispose(): void { - if (this._refreshAnimationFrame) { - window.cancelAnimationFrame(this._refreshAnimationFrame); - this._refreshAnimationFrame = null; - } + this._renderRowsDebouncer.dispose(); this._terminal.element.removeChild(this._accessibilityTreeRoot); this._accessibilityTreeRoot = null; this._rowContainer = null; @@ -169,27 +167,15 @@ export class AccessibilityManager implements IDisposable { } private _refreshRows(start?: number, end?: number): void { - start = start || 0; - end = end || this._terminal.rows - 1; - this._refreshRowStart = this._refreshRowStart ? Math.min(this._refreshRowStart, start) : start; - this._refreshRowEnd = this._refreshRowEnd ? Math.max(this._refreshRowEnd, end) : end; - - if (this._refreshAnimationFrame) { - return; - } - - this._refreshAnimationFrame = window.requestAnimationFrame(() => this._innerRefreshRows()); + this._renderRowsDebouncer.refresh(start, end); } - private _innerRefreshRows(): void { + private _renderRows(start: number, end: number): void { const buffer: IBuffer = (this._terminal.buffer); - for (let i = this._refreshRowStart; i <= this._refreshRowEnd; i++) { + for (let i = start; i <= end; i++) { const lineData = buffer.translateBufferLineToString(buffer.ybase + i, true); this._rowElements[i].textContent = lineData; } - this._refreshRowStart = null; - this._refreshRowEnd = null; - this._refreshAnimationFrame = null; } private _refreshRowsDimensions(): void { diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 4cdf9c52..3d2a4d95 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -13,11 +13,10 @@ import { BaseRenderLayer } from './BaseRenderLayer'; import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions } from './Interfaces'; import { LinkRenderLayer } from './LinkRenderLayer'; import { EventEmitter } from '../EventEmitter'; +import { RenderDebouncer } from '../utils/RenderDebouncer'; export class Renderer extends EventEmitter implements IRenderer { - /** A queue of the rows to be refreshed */ - private _refreshRowsQueue: {start: number, end: number}[] = []; - private _refreshAnimationFrame: number = null; + private _renderDebouncer: RenderDebouncer; private _renderLayers: IRenderLayer[]; private _devicePixelRatio: number; @@ -53,6 +52,7 @@ export class Renderer extends EventEmitter implements IRenderer { }; this._devicePixelRatio = window.devicePixelRatio; this._updateDimensions(); + this._renderDebouncer = new RenderDebouncer(this._terminal, this._refreshLoop.bind(this)); } public onWindowResize(devicePixelRatio: number): void { @@ -129,42 +129,14 @@ export class Renderer extends EventEmitter implements IRenderer { * @param {number} end The end row. */ public queueRefresh(start: number, end: number): void { - this._refreshRowsQueue.push({ start: start, end: end }); - if (!this._refreshAnimationFrame) { - this._refreshAnimationFrame = window.requestAnimationFrame(this._refreshLoop.bind(this)); - } + this._renderDebouncer.refresh(start, end); } /** * Performs the refresh loop callback, calling refresh only if a refresh is * necessary before queueing up the next one. */ - private _refreshLoop(): void { - let start; - let end; - if (this._refreshRowsQueue.length > 4) { - // Just do a full refresh when 5+ refreshes are queued - start = 0; - end = this._terminal.rows - 1; - } else { - // Get start and end rows that need refreshing - start = this._refreshRowsQueue[0].start; - end = this._refreshRowsQueue[0].end; - for (let i = 1; i < this._refreshRowsQueue.length; i++) { - if (this._refreshRowsQueue[i].start < start) { - start = this._refreshRowsQueue[i].start; - } - if (this._refreshRowsQueue[i].end > end) { - end = this._refreshRowsQueue[i].end; - } - } - } - this._refreshRowsQueue = []; - this._refreshAnimationFrame = null; - - // Render - start = Math.max(start, 0); - end = Math.min(end, this._terminal.rows - 1); + private _refreshLoop(start: number, end: number): void { this._renderLayers.forEach(l => l.onGridChanged(this._terminal, start, end)); this._terminal.emit('refresh', {start, end}); } diff --git a/src/utils/RenderDebouncer.ts b/src/utils/RenderDebouncer.ts new file mode 100644 index 00000000..c494fb98 --- /dev/null +++ b/src/utils/RenderDebouncer.ts @@ -0,0 +1,50 @@ +import { ITerminal, IDisposable } from '../Interfaces'; + +/** + * Debounces calls to render terminal rows using animation frames. + */ +export class RenderDebouncer implements IDisposable { + private _rowStart: number; + private _rowEnd: number; + private _animationFrame: number = null; + + constructor( + private _terminal: ITerminal, + private _callback: (start: number, end: number) => void + ) { + } + + public dispose(): void { + if (this._animationFrame) { + window.cancelAnimationFrame(this._animationFrame); + this._animationFrame = null; + } + } + + public refresh(rowStart?: number, rowEnd?: number): void { + rowStart = rowStart || 0; + rowEnd = rowEnd || this._terminal.rows - 1; + this._rowStart = this._rowStart ? Math.min(this._rowStart, rowStart) : rowStart; + this._rowEnd = this._rowEnd ? Math.max(this._rowEnd, rowEnd) : rowEnd; + + if (this._animationFrame) { + return; + } + + this._animationFrame = window.requestAnimationFrame(() => this._innerRefresh()); + } + + private _innerRefresh(): void { + // Clamp values + this._rowStart = Math.max(this._rowStart, 0); + this._rowEnd = Math.min(this._rowEnd, this._terminal.rows - 1); + + // Run render callback + this._callback(this._rowStart, this._rowEnd); + + // Reset debouncer + this._rowStart = null; + this._rowEnd = null; + this._animationFrame = null; + } +}