From 882d7e4a301c50eea93a41a40604bb49e8352301 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 1 Sep 2017 11:34:52 -0700 Subject: [PATCH] Add basic support for cursor blinking --- src/Terminal.ts | 1 + src/renderer/BaseRenderLayer.ts | 22 +++++++- src/renderer/CursorRenderLayer.ts | 88 ++++++++++++++++++++++++++++--- src/renderer/Interfaces.ts | 10 +++- src/renderer/Renderer.ts | 9 ++++ 5 files changed, 121 insertions(+), 9 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 92fc5440..0c6eb65d 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -497,6 +497,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT case 'bellSound': case 'bellStyle': this.syncBellSound(); break; } + this.renderer.onOptionsChanged(); } private restartCursorBlinking(): void { diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 331bc678..047269c3 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -1,5 +1,5 @@ import { IRenderLayer } from './Interfaces'; -import { ITerminal } from '../Interfaces'; +import { ITerminal, ITerminalOptions } from '../Interfaces'; import { COLORS } from './Color'; export abstract class BaseRenderLayer implements IRenderLayer { @@ -27,6 +27,10 @@ export abstract class BaseRenderLayer implements IRenderLayer { } } + public onOptionsChanged(options: ITerminal): void { + // TODO: Should this do anything? + } + public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void { this.scaledCharWidth = terminal.charMeasure.width * window.devicePixelRatio; this.scaledCharHeight = terminal.charMeasure.height * window.devicePixelRatio; @@ -56,6 +60,22 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.fillRect(startCol * this.scaledCharWidth, startRow * this.scaledCharHeight, colWidth * this.scaledCharWidth, colHeight * this.scaledCharHeight); } + protected fillBottomLineAtCell(x: number, y: number): void { + this._ctx.fillRect( + x * this.scaledCharWidth, + (y + 1) * this.scaledCharHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */, + this.scaledCharWidth, + window.devicePixelRatio); + } + + protected fillLeftLineAtCell(x: number, y: number): void { + this._ctx.fillRect( + x * this.scaledCharWidth, + y * this.scaledCharHeight, + window.devicePixelRatio, + this.scaledCharHeight); + } + protected clearAll(): void { this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); } diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index 7b5be052..89707590 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -1,28 +1,57 @@ import { IDataRenderLayer } from './Interfaces'; -import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; +import { IBuffer, ICharMeasure, ITerminal, ITerminalOptions } from '../Interfaces'; import { CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; import { COLORS, COLOR_CODES } from './Color'; import { GridCache } from './GridCache'; import { FLAGS } from './Types'; import { BaseRenderLayer } from './BaseRenderLayer'; +import { CharData } from '../Types'; + +/** + * The time between cursor blinks. + */ +const BLINK_INTERVAL = 600; export class CursorRenderLayer extends BaseRenderLayer implements IDataRenderLayer { private _state: [number, number]; + private _cursorRenderers: {[key: string]: (terminal: ITerminal, x: number, y: number, charData: CharData) => void}; + private _animationFrame: number; + private _blinkInterval: number; + private _isVisible: boolean; constructor(container: HTMLElement, zIndex: number) { super(container, 'cursor', zIndex); this._state = null; + this._isVisible = true; + this._cursorRenderers = { + 'bar': this._renderBarCursor.bind(this), + 'block': this._renderBlockCursor.bind(this), + 'underline': this._renderUnderlineCursor.bind(this) + }; } public reset(terminal: ITerminal): void { this._clearCursor(); + this._isVisible = true; + } + + public onOptionsChanged(terminal: ITerminal): void { + super.onOptionsChanged(terminal); + this._refreshBlinkState(terminal); } public render(terminal: ITerminal, startRow: number, endRow: number): void { + // Only render if the animation frame is not active + if (!this._blinkInterval) { + this._render(terminal, false); + } + } + + private _render(terminal: ITerminal, triggeredByAnimationFrame: boolean): void { // TODO: Track blur/focus somehow, support unfocused cursor // Don't draw the cursor if it's hidden - if (!terminal.cursorState || terminal.cursorHidden) { + if (!terminal.cursorState || terminal.cursorHidden || !this._isVisible) { this._clearCursor(); return; } @@ -44,14 +73,11 @@ export class CursorRenderLayer extends BaseRenderLayer implements IDataRenderLay this._clearCursor(); } + const charData = terminal.buffer.lines.get(cursorY)[terminal.buffer.x]; this._ctx.save(); this._ctx.fillStyle = COLORS[COLOR_CODES.WHITE]; - this.fillCells(terminal.buffer.x, viewportRelativeCursorY, 1, 1); + this._cursorRenderers[terminal.options.cursorStyle || 'block'](terminal, terminal.buffer.x, viewportRelativeCursorY, charData); this._ctx.restore(); - - const charData = terminal.buffer.lines.get(cursorY)[terminal.buffer.x]; - this.drawChar(terminal, charData[CHAR_DATA_CHAR_INDEX], charData[CHAR_DATA_CODE_INDEX], COLOR_CODES.BLACK, terminal.buffer.x, viewportRelativeCursorY); - this._state = [terminal.buffer.x, viewportRelativeCursorY]; } @@ -61,4 +87,52 @@ export class CursorRenderLayer extends BaseRenderLayer implements IDataRenderLay this._state = null; } } + + private _renderBarCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void { + this.fillLeftLineAtCell(x, y); + } + + private _renderBlockCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void { + this.fillCells(x, y, 1, 1); + this.drawChar(terminal, charData[CHAR_DATA_CHAR_INDEX], charData[CHAR_DATA_CODE_INDEX], COLOR_CODES.BLACK, x, y); + } + + private _renderUnderlineCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void { + this.fillBottomLineAtCell(x, y); + } + + private _refreshBlinkState(terminal: ITerminal): void { + if (terminal.options.cursorBlink) { + if (!this._blinkInterval) { + this._blinkInterval = setInterval(() => { + this._isVisible = !this._isVisible; + this._animationFrame = window.requestAnimationFrame(() => { + this._render(terminal, true); + this._animationFrame = null; + }); + }, BLINK_INTERVAL); + } + } else { + if (this._animationFrame) { + window.clearInterval(this._blinkInterval); + this._blinkInterval = null; + window.cancelAnimationFrame(this._animationFrame); + this._animationFrame = null; + this._isVisible = true; + } + } + } + + private _restartBlinkAnimation(terminal: ITerminal): void { + // TODO: Restart the blink animation when input is received + // How can this be done efficiently, without thrashing with restarting the timers? + } + + private _pauseBlinkAnimation(): void { + // TODO: Pause the blink animation on blur + } + + private _resumeBlinkAnimation(): void { + // TODO: Resume the blink animation on focus + } } diff --git a/src/renderer/Interfaces.ts b/src/renderer/Interfaces.ts index 82b2e77a..55cffbc5 100644 --- a/src/renderer/Interfaces.ts +++ b/src/renderer/Interfaces.ts @@ -1,6 +1,8 @@ -import { ITerminal } from '../Interfaces'; +import { ITerminal, ITerminalOptions } from '../Interfaces'; export interface IRenderLayer { + onOptionsChanged(options: ITerminal): void; + /** * Resize the render layer. */ @@ -12,10 +14,16 @@ export interface IRenderLayer { reset(terminal: ITerminal): void; } +/** + * A render layer that renders when there is a data change. + */ export interface IDataRenderLayer extends IRenderLayer { render(terminal: ITerminal, startRow: number, endRow: number): void; } +/** + * A render layer that renders when there is a selection change. + */ export interface ISelectionRenderLayer extends IRenderLayer { render(terminal: ITerminal, start: [number, number], end: [number, number]): void; } diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 2cce350b..2903460a 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -56,6 +56,15 @@ export class Renderer { } } + public onOptionsChanged(): void { + for (let i = 0; i < this._dataRenderLayers.length; i++) { + this._dataRenderLayers[i].onOptionsChanged(this._terminal); + } + for (let i = 0; i < this._selectionRenderLayers.length; i++) { + this._selectionRenderLayers[i].onOptionsChanged(this._terminal); + } + } + public clear(): void { for (let i = 0; i < this._dataRenderLayers.length; i++) { this._dataRenderLayers[i].reset(this._terminal);