/** * Copyright (c) 2017 The xterm.js authors. All rights reserved. * @license MIT */ import { RendererConstants } from 'browser/renderer/shared/Constants'; import { ICoreBrowserService } from 'browser/services/Services'; const enum Constants { /** * The time between cursor blinks. */ BLINK_INTERVAL = 600, } export class CursorBlinkStateManager { public isCursorVisible: boolean; private _animationFrame: number | undefined; private _blinkStartTimeout: number | undefined; private _blinkInterval: number | undefined; private _idleTimeout: number | undefined; private _isIdlePaused: boolean = false; /** * The time at which the animation frame was restarted, this is used on the * next render to restart the timers so they don't need to restart the timers * multiple times over a short period. */ private _animationTimeRestarted: number | undefined; constructor( private _renderCallback: () => void, private _coreBrowserService: ICoreBrowserService ) { this.isCursorVisible = true; if (this._coreBrowserService.isFocused) { this._restartInterval(); this._resetIdleTimer(); } } public get isPaused(): boolean { return !(this._blinkStartTimeout || this._blinkInterval); } public dispose(): void { if (this._blinkInterval) { this._coreBrowserService.window.clearInterval(this._blinkInterval); this._blinkInterval = undefined; } if (this._blinkStartTimeout) { this._coreBrowserService.window.clearTimeout(this._blinkStartTimeout); this._blinkStartTimeout = undefined; } if (this._animationFrame) { this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame); this._animationFrame = undefined; } if (this._idleTimeout) { this._coreBrowserService.window.clearTimeout(this._idleTimeout); this._idleTimeout = undefined; } } public restartBlinkAnimation(): void { if (this._isIdlePaused) { this._resetIdleTimer(); } if (this.isPaused) { return; } // Save a timestamp so that the restart can be done on the next interval this._animationTimeRestarted = Date.now(); // Force a cursor render to ensure it's visible and in the correct position this.isCursorVisible = true; if (!this._animationFrame) { this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => { this._renderCallback(); this._animationFrame = undefined; }); } } private _restartInterval(timeToStart: number = Constants.BLINK_INTERVAL): void { // Clear any existing interval if (this._blinkInterval) { this._coreBrowserService.window.clearInterval(this._blinkInterval); this._blinkInterval = undefined; } // Setup the initial timeout which will hide the cursor, this is done before // the regular interval is setup in order to support restarting the blink // animation in a lightweight way (without thrashing clearInterval and // setInterval). this._blinkStartTimeout = this._coreBrowserService.window.setTimeout(() => { // Check if another animation restart was requested while this was being // started if (this._animationTimeRestarted) { const time = Constants.BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted); this._animationTimeRestarted = undefined; if (time > 0) { this._restartInterval(time); return; } } // Hide the cursor this.isCursorVisible = false; this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => { this._renderCallback(); this._animationFrame = undefined; }); // Setup the blink interval this._blinkInterval = this._coreBrowserService.window.setInterval(() => { // Adjust the animation time if it was restarted if (this._animationTimeRestarted) { // calc time diff // Make restart interval do a setTimeout initially? const time = Constants.BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted); this._animationTimeRestarted = undefined; this._restartInterval(time); return; } // Invert visibility and render this.isCursorVisible = !this.isCursorVisible; this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => { this._renderCallback(); this._animationFrame = undefined; }); }, Constants.BLINK_INTERVAL); }, timeToStart); } public pause(): void { this.isCursorVisible = true; this._isIdlePaused = false; if (this._blinkInterval) { this._coreBrowserService.window.clearInterval(this._blinkInterval); this._blinkInterval = undefined; } if (this._blinkStartTimeout) { this._coreBrowserService.window.clearTimeout(this._blinkStartTimeout); this._blinkStartTimeout = undefined; } if (this._animationFrame) { this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame); this._animationFrame = undefined; } if (this._idleTimeout) { this._coreBrowserService.window.clearTimeout(this._idleTimeout); this._idleTimeout = undefined; } } public resume(): void { // Clear out any existing timers just in case this.pause(); this._animationTimeRestarted = undefined; this._restartInterval(); this._resetIdleTimer(); this.restartBlinkAnimation(); } /** * Resets the idle timer. If the terminal is idle for the idle timeout period, * the cursor blinking will stop. */ private _resetIdleTimer(): void { this._isIdlePaused = false; if (this._idleTimeout) { this._coreBrowserService.window.clearTimeout(this._idleTimeout); } this._idleTimeout = this._coreBrowserService.window.setTimeout(() => { this._stopBlinkingDueToIdle(); }, RendererConstants.CURSOR_BLINK_IDLE_TIMEOUT); } /** * Stops cursor blinking due to idle timeout. */ private _stopBlinkingDueToIdle(): void { // Make cursor visible and stop blinking this.isCursorVisible = true; this._isIdlePaused = true; if (this._blinkInterval) { this._coreBrowserService.window.clearInterval(this._blinkInterval); this._blinkInterval = undefined; } if (this._blinkStartTimeout) { this._coreBrowserService.window.clearTimeout(this._blinkStartTimeout); this._blinkStartTimeout = undefined; } if (this._animationFrame) { this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame); this._animationFrame = undefined; } // Clear the idle timeout as we've already acted on it this._coreBrowserService.window.clearTimeout(this._idleTimeout); this._idleTimeout = undefined; // Trigger a render to show the cursor in its final visible state this._renderCallback(); } }