2 * Copyright (c) 2017 The xterm.js authors. All rights reserved.
6 import { IRenderDimensions } from 'browser/renderer/Types';
7 import { BaseRenderLayer } from '../browser/renderer/BaseRenderLayer';
8 import { ITerminal } from '../Types';
9 import { ICellData } from 'common/Types';
10 import { CellData } from 'common/buffer/CellData';
11 import { IColorSet } from 'browser/Types';
12 import { IBufferService, IOptionsService } from 'common/services/Services';
14 interface ICursorState {
23 * The time between cursor blinks.
25 const BLINK_INTERVAL = 600;
27 export class CursorRenderLayer extends BaseRenderLayer {
28 private _state: ICursorState;
29 private _cursorRenderers: {[key: string]: (x: number, y: number, cell: ICellData) => void};
30 private _cursorBlinkStateManager: CursorBlinkStateManager;
31 private _cell: ICellData = new CellData();
34 container: HTMLElement,
37 private _terminal: ITerminal,
39 readonly bufferService: IBufferService,
40 readonly optionsService: IOptionsService
42 super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService);
50 this._cursorRenderers = {
51 'bar': this._renderBarCursor.bind(this),
52 'block': this._renderBlockCursor.bind(this),
53 'underline': this._renderUnderlineCursor.bind(this)
55 // TODO: Consider initial options? Maybe onOptionsChanged should be called at the end of open?
58 public resize(dim: IRenderDimensions): void {
60 // Resizing the canvas discards the contents of the canvas so clear state
70 public reset(): void {
72 if (this._cursorBlinkStateManager) {
73 this._cursorBlinkStateManager.dispose();
74 this._cursorBlinkStateManager = null;
75 this.onOptionsChanged();
79 public onBlur(): void {
80 if (this._cursorBlinkStateManager) {
81 this._cursorBlinkStateManager.pause();
83 this._terminal.refresh(this._bufferService.buffer.y, this._bufferService.buffer.y);
86 public onFocus(): void {
87 if (this._cursorBlinkStateManager) {
88 this._cursorBlinkStateManager.resume();
90 this._terminal.refresh(this._bufferService.buffer.y, this._bufferService.buffer.y);
94 public onOptionsChanged(): void {
95 if (this._optionsService.options.cursorBlink) {
96 if (!this._cursorBlinkStateManager) {
97 this._cursorBlinkStateManager = new CursorBlinkStateManager(this._terminal.isFocused, () => {
102 if (this._cursorBlinkStateManager) {
103 this._cursorBlinkStateManager.dispose();
104 this._cursorBlinkStateManager = null;
107 // Request a refresh from the terminal as management of rendering is being
108 // moved back to the terminal
109 this._terminal.refresh(this._bufferService.buffer.y, this._bufferService.buffer.y);
112 public onCursorMove(): void {
113 if (this._cursorBlinkStateManager) {
114 this._cursorBlinkStateManager.restartBlinkAnimation();
118 public onGridChanged(startRow: number, endRow: number): void {
119 if (!this._cursorBlinkStateManager || this._cursorBlinkStateManager.isPaused) {
122 this._cursorBlinkStateManager.restartBlinkAnimation();
126 private _render(triggeredByAnimationFrame: boolean): void {
127 // Don't draw the cursor if it's hidden
128 if (!this._terminal.cursorState || this._terminal.cursorHidden) {
133 const cursorY = this._bufferService.buffer.ybase + this._bufferService.buffer.y;
134 const viewportRelativeCursorY = cursorY - this._bufferService.buffer.ydisp;
136 // Don't draw the cursor if it's off-screen
137 if (viewportRelativeCursorY < 0 || viewportRelativeCursorY >= this._bufferService.rows) {
142 this._bufferService.buffer.lines.get(cursorY).loadCell(this._bufferService.buffer.x, this._cell);
143 if (this._cell.content === undefined) {
147 if (!this._terminal.isFocused) {
150 this._ctx.fillStyle = this._colors.cursor.css;
151 const cursorStyle = this._optionsService.options.cursorStyle;
152 if (cursorStyle && cursorStyle !== 'block') {
153 this._cursorRenderers[cursorStyle](this._bufferService.buffer.x, viewportRelativeCursorY, this._cell);
155 this._renderBlurCursor(this._bufferService.buffer.x, viewportRelativeCursorY, this._cell);
158 this._state.x = this._bufferService.buffer.x;
159 this._state.y = viewportRelativeCursorY;
160 this._state.isFocused = false;
161 this._state.style = cursorStyle;
162 this._state.width = this._cell.getWidth();
166 // Don't draw the cursor if it's blinking
167 if (this._cursorBlinkStateManager && !this._cursorBlinkStateManager.isCursorVisible) {
173 // The cursor is already in the correct spot, don't redraw
174 if (this._state.x === this._bufferService.buffer.x &&
175 this._state.y === viewportRelativeCursorY &&
176 this._state.isFocused === this._terminal.isFocused &&
177 this._state.style === this._optionsService.options.cursorStyle &&
178 this._state.width === this._cell.getWidth()) {
185 this._cursorRenderers[this._optionsService.options.cursorStyle || 'block'](this._bufferService.buffer.x, viewportRelativeCursorY, this._cell);
188 this._state.x = this._bufferService.buffer.x;
189 this._state.y = viewportRelativeCursorY;
190 this._state.isFocused = false;
191 this._state.style = this._optionsService.options.cursorStyle;
192 this._state.width = this._cell.getWidth();
195 private _clearCursor(): void {
197 this._clearCells(this._state.x, this._state.y, this._state.width, 1);
208 private _renderBarCursor(x: number, y: number, cell: ICellData): void {
210 this._ctx.fillStyle = this._colors.cursor.css;
211 this._fillLeftLineAtCell(x, y);
215 private _renderBlockCursor(x: number, y: number, cell: ICellData): void {
217 this._ctx.fillStyle = this._colors.cursor.css;
218 this._fillCells(x, y, cell.getWidth(), 1);
219 this._ctx.fillStyle = this._colors.cursorAccent.css;
220 this._fillCharTrueColor(cell, x, y);
224 private _renderUnderlineCursor(x: number, y: number, cell: ICellData): void {
226 this._ctx.fillStyle = this._colors.cursor.css;
227 this._fillBottomLineAtCells(x, y);
231 private _renderBlurCursor(x: number, y: number, cell: ICellData): void {
233 this._ctx.strokeStyle = this._colors.cursor.css;
234 this._strokeRectAtCell(x, y, cell.getWidth(), 1);
239 class CursorBlinkStateManager {
240 public isCursorVisible: boolean;
242 private _animationFrame: number;
243 private _blinkStartTimeout: number;
244 private _blinkInterval: number;
247 * The time at which the animation frame was restarted, this is used on the
248 * next render to restart the timers so they don't need to restart the timers
249 * multiple times over a short period.
251 private _animationTimeRestarted: number;
255 private _renderCallback: () => void
257 this.isCursorVisible = true;
259 this._restartInterval();
263 public get isPaused(): boolean { return !(this._blinkStartTimeout || this._blinkInterval); }
265 public dispose(): void {
266 if (this._blinkInterval) {
267 window.clearInterval(this._blinkInterval);
268 this._blinkInterval = null;
270 if (this._blinkStartTimeout) {
271 window.clearTimeout(this._blinkStartTimeout);
272 this._blinkStartTimeout = null;
274 if (this._animationFrame) {
275 window.cancelAnimationFrame(this._animationFrame);
276 this._animationFrame = null;
280 public restartBlinkAnimation(): void {
284 // Save a timestamp so that the restart can be done on the next interval
285 this._animationTimeRestarted = Date.now();
286 // Force a cursor render to ensure it's visible and in the correct position
287 this.isCursorVisible = true;
288 if (!this._animationFrame) {
289 this._animationFrame = window.requestAnimationFrame(() => {
290 this._renderCallback();
291 this._animationFrame = null;
296 private _restartInterval(timeToStart: number = BLINK_INTERVAL): void {
297 // Clear any existing interval
298 if (this._blinkInterval) {
299 window.clearInterval(this._blinkInterval);
302 // Setup the initial timeout which will hide the cursor, this is done before
303 // the regular interval is setup in order to support restarting the blink
304 // animation in a lightweight way (without thrashing clearInterval and
306 this._blinkStartTimeout = <number><any>setTimeout(() => {
307 // Check if another animation restart was requested while this was being
309 if (this._animationTimeRestarted) {
310 const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted);
311 this._animationTimeRestarted = null;
313 this._restartInterval(time);
319 this.isCursorVisible = false;
320 this._animationFrame = window.requestAnimationFrame(() => {
321 this._renderCallback();
322 this._animationFrame = null;
325 // Setup the blink interval
326 this._blinkInterval = <number><any>setInterval(() => {
327 // Adjust the animation time if it was restarted
328 if (this._animationTimeRestarted) {
330 // Make restart interval do a setTimeout initially?
331 const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted);
332 this._animationTimeRestarted = null;
333 this._restartInterval(time);
337 // Invert visibility and render
338 this.isCursorVisible = !this.isCursorVisible;
339 this._animationFrame = window.requestAnimationFrame(() => {
340 this._renderCallback();
341 this._animationFrame = null;
347 public pause(): void {
348 this.isCursorVisible = true;
349 if (this._blinkInterval) {
350 window.clearInterval(this._blinkInterval);
351 this._blinkInterval = null;
353 if (this._blinkStartTimeout) {
354 window.clearTimeout(this._blinkStartTimeout);
355 this._blinkStartTimeout = null;
357 if (this._animationFrame) {
358 window.cancelAnimationFrame(this._animationFrame);
359 this._animationFrame = null;
363 public resume(): void {
364 this._animationTimeRestarted = null;
365 this._restartInterval();
366 this.restartBlinkAnimation();