diff --git a/addons/addon-webgl/src/CursorBlinkStateManager.ts b/addons/addon-webgl/src/CursorBlinkStateManager.ts index c5bb0870..36f2a909 100644 --- a/addons/addon-webgl/src/CursorBlinkStateManager.ts +++ b/addons/addon-webgl/src/CursorBlinkStateManager.ts @@ -10,12 +10,19 @@ import { ICoreBrowserService } from 'browser/services/Services'; */ const BLINK_INTERVAL = 600; +/** + * The idle time after which cursor blinking stops (2 minutes). + */ +const IDLE_TIMEOUT = 5000; //2 * 60 * 1000; + 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 @@ -31,6 +38,7 @@ export class CursorBlinkStateManager { this.isCursorVisible = true; if (this._coreBrowserService.isFocused) { this._restartInterval(); + this._resetIdleTimer(); } } @@ -49,9 +57,16 @@ export class CursorBlinkStateManager { 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; } @@ -121,6 +136,7 @@ export class CursorBlinkStateManager { public pause(): void { this.isCursorVisible = true; + this._isIdlePaused = false; if (this._blinkInterval) { this._coreBrowserService.window.clearInterval(this._blinkInterval); this._blinkInterval = undefined; @@ -133,6 +149,10 @@ export class CursorBlinkStateManager { this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame); this._animationFrame = undefined; } + if (this._idleTimeout) { + this._coreBrowserService.window.clearTimeout(this._idleTimeout); + this._idleTimeout = undefined; + } } public resume(): void { @@ -141,6 +161,47 @@ export class CursorBlinkStateManager { this._animationTimeRestarted = undefined; this._restartInterval(); + this._resetIdleTimer(); this.restartBlinkAnimation(); } + + /** + * Resets the idle timer. If the terminal is idle for IDLE_TIMEOUT, 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(); + }, 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(); + } } diff --git a/addons/addon-webgl/src/WebglRenderer.ts b/addons/addon-webgl/src/WebglRenderer.ts index fb14b3c0..987d587c 100644 --- a/addons/addon-webgl/src/WebglRenderer.ts +++ b/addons/addon-webgl/src/WebglRenderer.ts @@ -136,6 +136,8 @@ export class WebglRenderer extends Disposable implements IRenderer { this._observerDisposable.value = observeDevicePixelDimensions(this._canvas, w, (w, h) => this._setCanvasDevicePixelDimensions(w, h)); })); + this._register(addDisposableListener(this._coreBrowserService.mainDocument, 'mousedown', () => this._cursorBlinkStateManager.value?.restartBlinkAnimation())); + this._core.screenElement!.appendChild(this._canvas); [this._rectangleRenderer.value, this._glyphRenderer.value] = this._initializeWebGLState(); diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index 1c271797..454adb1f 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -15,6 +15,7 @@ import { color } from 'common/Color'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IBufferService, ICoreService, IInstantiationService, IOptionsService } from 'common/services/Services'; import { Emitter } from 'vs/base/common/event'; +import { addDisposableListener } from 'vs/base/browser/dom'; const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-'; @@ -23,6 +24,12 @@ const FG_CLASS_PREFIX = 'xterm-fg-'; const BG_CLASS_PREFIX = 'xterm-bg-'; const FOCUS_CLASS = 'xterm-focus'; const SELECTION_CLASS = 'xterm-selection'; +const CURSOR_BLINK_IDLE_CLASS = 'xterm-cursor-blink-idle'; + +/** + * The idle time after which cursor blinking stops (2 minutes). + */ +const IDLE_TIMEOUT = 5000; // 2 * 60 * 1000; let nextTerminalId = 1; @@ -42,6 +49,7 @@ export class DomRenderer extends Disposable implements IRenderer { private _selectionContainer: HTMLElement; private _widthCache: WidthCache; private _selectionRenderModel: ISelectionRenderModel = createSelectionRenderModel(); + private _idleTimeout: number | undefined; public dimensions: IRenderDimensions; @@ -89,6 +97,8 @@ export class DomRenderer extends Disposable implements IRenderer { this._register(this._linkifier2.onShowLinkUnderline(e => this._handleLinkHover(e))); this._register(this._linkifier2.onHideLinkUnderline(e => this._handleLinkLeave(e))); + this._register(addDisposableListener(this._document, 'mousedown', () => this._resetIdleTimer())); + this._register(toDisposable(() => { this._element.classList.remove(TERMINAL_CLASS_PREFIX + this._terminalClass); @@ -99,6 +109,7 @@ export class DomRenderer extends Disposable implements IRenderer { this._widthCache.dispose(); this._themeStyleElement.remove(); this._dimensionsStyleElement.remove(); + this._clearIdleTimer(); })); this._widthCache = new WidthCache(this._document, this._helperContainer); @@ -225,6 +236,10 @@ export class DomRenderer extends Disposable implements IRenderer { `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS} {` + ` animation: ${blinkAnimationBlockId} 1s step-end infinite;` + `}` + + // Disable cursor blinking when idle + `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${CURSOR_BLINK_IDLE_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS} {` + + ` animation: none !important;` + + `}` + // !important helps fix an issue where the cursor will not render on top of the selection, // however it's very hard to fix this issue and retain the blink animation without the use of // !important. So this edge case fails when cursor blink is on. @@ -328,11 +343,14 @@ export class DomRenderer extends Disposable implements IRenderer { public handleBlur(): void { this._rowContainer.classList.remove(FOCUS_CLASS); + this._clearIdleTimer(); this.renderRows(0, this._bufferService.rows - 1); } public handleFocus(): void { this._rowContainer.classList.add(FOCUS_CLASS); + this._rowContainer.classList.remove(CURSOR_BLINK_IDLE_CLASS); + this._resetIdleTimer(); this.renderRows(this._bufferService.buffer.y, this._bufferService.buffer.y); } @@ -406,7 +424,8 @@ export class DomRenderer extends Disposable implements IRenderer { } public handleCursorMove(): void { - // No-op, the cursor is drawn when rows are drawn + // Reset idle timer on cursor movement (which happens on input) + this._resetIdleTimer(); } private _handleOptionsChanged(): void { @@ -539,4 +558,37 @@ export class DomRenderer extends Disposable implements IRenderer { ); } } + + /** + * Resets the idle timer. If the terminal is idle for IDLE_TIMEOUT, cursor + * blinking will stop. + */ + private _resetIdleTimer(): void { + if (!this._coreBrowserService.isFocused) { + return; + } + this._clearIdleTimer(); + this._rowContainer.classList.remove(CURSOR_BLINK_IDLE_CLASS); + this._idleTimeout = this._coreBrowserService.window.setTimeout(() => { + this._stopBlinkingDueToIdle(); + }, IDLE_TIMEOUT); + } + + /** + * Clears the idle timer. + */ + private _clearIdleTimer(): void { + if (this._idleTimeout) { + this._coreBrowserService.window.clearTimeout(this._idleTimeout); + this._idleTimeout = undefined; + } + } + + /** + * Stops cursor blinking due to idle timeout. + */ + private _stopBlinkingDueToIdle(): void { + this._rowContainer.classList.add(CURSOR_BLINK_IDLE_CLASS); + this._idleTimeout = undefined; + } }