Files
xterm.js/addons/addon-webgl/src/CursorBlinkStateManager.ts
T
2025-12-28 13:42:22 -08:00

206 lines
6.8 KiB
TypeScript

/**
* 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();
}
}