diff --git a/src/browser/AccessibilityManager.ts b/src/browser/AccessibilityManager.ts index c1ffc39a6..160aa3fcc 100644 --- a/src/browser/AccessibilityManager.ts +++ b/src/browser/AccessibilityManager.ts @@ -7,7 +7,7 @@ import * as Strings from 'browser/LocalizableStrings'; import { ITerminal } from 'browser/Types'; import { IBuffer } from 'common/buffer/Types'; import { isMac } from 'common/Platform'; -import { RenderDebouncer } from 'browser/RenderDebouncer'; +import { TimeBasedDebouncer } from 'browser/TimeBasedDebouncer'; import { addDisposableDomListener } from 'browser/Lifecycle'; import { Disposable } from 'common/Lifecycle'; import { ScreenDprMonitor } from 'browser/ScreenDprMonitor'; @@ -28,7 +28,7 @@ export class AccessibilityManager extends Disposable { private _liveRegion: HTMLElement; private _liveRegionLineCount: number = 0; - private _renderRowsDebouncer: RenderDebouncer; + private _renderRowsDebouncer: TimeBasedDebouncer; private _screenDprMonitor: ScreenDprMonitor; private _topBoundaryFocusListener: (e: FocusEvent) => void; @@ -72,7 +72,7 @@ export class AccessibilityManager extends Disposable { this._refreshRowsDimensions(); this._accessibilityTreeRoot.appendChild(this._rowContainer); - this._renderRowsDebouncer = new RenderDebouncer(this._renderRows.bind(this)); + this._renderRowsDebouncer = new TimeBasedDebouncer(this._renderRows.bind(this)); this._refreshRows(); this._liveRegion = document.createElement('div'); diff --git a/src/browser/RenderDebouncer.ts b/src/browser/RenderDebouncer.ts index 334d9a1ce..2a06fdd66 100644 --- a/src/browser/RenderDebouncer.ts +++ b/src/browser/RenderDebouncer.ts @@ -5,8 +5,6 @@ import { IDisposable } from 'common/Types'; -const RENDER_DEBOUNCE_THRESHOLD_MS = 1000; // 1 Second - /** * Debounces calls to render terminal rows using animation frames. */ @@ -16,17 +14,17 @@ export class RenderDebouncer implements IDisposable { private _rowCount: number | undefined; private _animationFrame: number | undefined; - // The last moment that the Terminal was refreshed at - private _lastRefreshMs = 0; - // Whether a trailing refresh should be triggered due to a refresh request that was throttled - private _additionalRefreshRequested = false; - constructor( private _renderCallback: (start: number, end: number) => void ) { } - public dispose(): void {} + public dispose(): void { + if (this._animationFrame) { + window.cancelAnimationFrame(this._animationFrame); + this._animationFrame = undefined; + } + } public refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void { this._rowCount = rowCount; @@ -37,25 +35,11 @@ export class RenderDebouncer implements IDisposable { this._rowStart = this._rowStart !== undefined ? Math.min(this._rowStart, rowStart) : rowStart; this._rowEnd = this._rowEnd !== undefined ? Math.max(this._rowEnd, rowEnd) : rowEnd; - // Only refresh if the time since last refresh is above a threshold, otherwise wait for - // enough time to pass before refreshing again. - const refreshRequestTime: number = Date.now(); - if (refreshRequestTime - this._lastRefreshMs >= RENDER_DEBOUNCE_THRESHOLD_MS) { - // Enough time has lapsed since the last refresh; refresh immediately - this._lastRefreshMs = refreshRequestTime; - this._innerRefresh(); - } else if (!this._additionalRefreshRequested) { - // This is the first additional request throttled; set up trailing refresh - const elapsed = refreshRequestTime - this._lastRefreshMs; - const waitPeriodBeforeTrailingRefresh = RENDER_DEBOUNCE_THRESHOLD_MS - elapsed; - this._additionalRefreshRequested = true; - - setTimeout(() => { - this._lastRefreshMs = Date.now(); - this._innerRefresh(); - this._additionalRefreshRequested = false; - }, waitPeriodBeforeTrailingRefresh); + if (this._animationFrame) { + return; } + + this._animationFrame = window.requestAnimationFrame(() => this._innerRefresh()); } private _innerRefresh(): void { @@ -71,9 +55,9 @@ export class RenderDebouncer implements IDisposable { // Reset debouncer (this happens before render callback as the render could trigger it again) this._rowStart = undefined; this._rowEnd = undefined; + this._animationFrame = undefined; // Run render callback this._renderCallback(start, end); } } - diff --git a/src/browser/TimeBasedDebouncer.ts b/src/browser/TimeBasedDebouncer.ts new file mode 100644 index 000000000..8f444c4fe --- /dev/null +++ b/src/browser/TimeBasedDebouncer.ts @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +const RENDER_DEBOUNCE_THRESHOLD_MS = 1000; // 1 Second + +/** + * Debounces calls to update screen readers to update at most once per second. + */ +export class TimeBasedDebouncer { + private _rowStart: number | undefined; + private _rowEnd: number | undefined; + private _rowCount: number | undefined; + + // The last moment that the Terminal was refreshed at + private _lastRefreshMs = 0; + // Whether a trailing refresh should be triggered due to a refresh request that was throttled + private _additionalRefreshRequested = false; + + constructor( + private _renderCallback: (start: number, end: number) => void + ) { + } + + public dispose(): void {} + + public refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void { + this._rowCount = rowCount; + // Get the min/max row start/end for the arg values + rowStart = rowStart !== undefined ? rowStart : 0; + rowEnd = rowEnd !== undefined ? rowEnd : this._rowCount - 1; + // Set the properties to the updated values + this._rowStart = this._rowStart !== undefined ? Math.min(this._rowStart, rowStart) : rowStart; + this._rowEnd = this._rowEnd !== undefined ? Math.max(this._rowEnd, rowEnd) : rowEnd; + + // Only refresh if the time since last refresh is above a threshold, otherwise wait for + // enough time to pass before refreshing again. + const refreshRequestTime: number = Date.now(); + if (refreshRequestTime - this._lastRefreshMs >= RENDER_DEBOUNCE_THRESHOLD_MS) { + // Enough time has lapsed since the last refresh; refresh immediately + this._lastRefreshMs = refreshRequestTime; + this._innerRefresh(); + } else if (!this._additionalRefreshRequested) { + // This is the first additional request throttled; set up trailing refresh + const elapsed = refreshRequestTime - this._lastRefreshMs; + const waitPeriodBeforeTrailingRefresh = RENDER_DEBOUNCE_THRESHOLD_MS - elapsed; + this._additionalRefreshRequested = true; + + setTimeout(() => { + this._lastRefreshMs = Date.now(); + this._innerRefresh(); + this._additionalRefreshRequested = false; + }, waitPeriodBeforeTrailingRefresh); + } + } + + private _innerRefresh(): void { + // Make sure values are set + if (this._rowStart === undefined || this._rowEnd === undefined || this._rowCount === undefined) { + return; + } + + // Clamp values + const start = Math.max(this._rowStart, 0); + const end = Math.min(this._rowEnd, this._rowCount - 1); + + // Reset debouncer (this happens before render callback as the render could trigger it again) + this._rowStart = undefined; + this._rowEnd = undefined; + + // Run render callback + this._renderCallback(start, end); + } +} +