diff --git a/src/browser/AccessibilityManager.ts b/src/browser/AccessibilityManager.ts index c1ffc39a..1be3342d 100644 --- a/src/browser/AccessibilityManager.ts +++ b/src/browser/AccessibilityManager.ts @@ -4,10 +4,10 @@ */ import * as Strings from 'browser/LocalizableStrings'; -import { ITerminal } from 'browser/Types'; +import { ITerminal, IRenderDebouncer } 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: IRenderDebouncer; 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 2a06fdd6..02521070 100644 --- a/src/browser/RenderDebouncer.ts +++ b/src/browser/RenderDebouncer.ts @@ -3,12 +3,12 @@ * @license MIT */ -import { IDisposable } from 'common/Types'; +import { IRenderDebouncer } from 'browser/Types'; /** * Debounces calls to render terminal rows using animation frames. */ -export class RenderDebouncer implements IDisposable { +export class RenderDebouncer implements IRenderDebouncer { private _rowStart: number | undefined; private _rowEnd: number | undefined; private _rowCount: number | undefined; diff --git a/src/browser/TimeBasedDebouncer.ts b/src/browser/TimeBasedDebouncer.ts new file mode 100644 index 00000000..707e25cb --- /dev/null +++ b/src/browser/TimeBasedDebouncer.ts @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +const RENDER_DEBOUNCE_THRESHOLD_MS = 1000; // 1 Second + +import { IRenderDebouncer } from 'browser/Types'; + +/** + * Debounces calls to update screen readers to update at most once configurable interval of time. + */ +export class TimeBasedDebouncer implements IRenderDebouncer { + 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; + + private _refreshTimeoutID: number | undefined; + + constructor( + private _renderCallback: (start: number, end: number) => void, + private readonly _debounceThresholdMS = RENDER_DEBOUNCE_THRESHOLD_MS + ) { + } + + public dispose(): void { + if (this._refreshTimeoutID) { + clearTimeout(this._refreshTimeoutID); + } + } + + 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 >= this._debounceThresholdMS) { + // 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 = this._debounceThresholdMS - elapsed; + this._additionalRefreshRequested = true; + + this._refreshTimeoutID = window.setTimeout(() => { + this._lastRefreshMs = Date.now(); + this._innerRefresh(); + this._additionalRefreshRequested = false; + this._refreshTimeoutID = undefined; // No longer need to clear the timeout + }, 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); + } +} + diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index c268c7bf..0d74b39f 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -309,3 +309,7 @@ export interface ICharacterJoiner { id: number; handler: CharacterJoinerHandler; } + +export interface IRenderDebouncer extends IDisposable { + refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void; +} diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index fc2eb435..332e71da 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -9,7 +9,7 @@ import { EventEmitter, IEvent } from 'common/EventEmitter'; import { Disposable } from 'common/Lifecycle'; import { ScreenDprMonitor } from 'browser/ScreenDprMonitor'; import { addDisposableDomListener } from 'browser/Lifecycle'; -import { IColorSet } from 'browser/Types'; +import { IColorSet, IRenderDebouncer } from 'browser/Types'; import { IOptionsService, IBufferService } from 'common/services/Services'; import { ICharSizeService, IRenderService } from 'browser/services/Services'; @@ -22,7 +22,7 @@ interface ISelectionState { export class RenderService extends Disposable implements IRenderService { public serviceBrand: undefined; - private _renderDebouncer: RenderDebouncer; + private _renderDebouncer: IRenderDebouncer; private _screenDprMonitor: ScreenDprMonitor; private _isPaused: boolean = false;