xterm
[VSoRC/.git] / node_modules / xterm / src / renderer / CursorRenderLayer.ts
1 /**
2  * Copyright (c) 2017 The xterm.js authors. All rights reserved.
3  * @license MIT
4  */
5
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';
13
14 interface ICursorState {
15   x: number;
16   y: number;
17   isFocused: boolean;
18   style: string;
19   width: number;
20 }
21
22 /**
23  * The time between cursor blinks.
24  */
25 const BLINK_INTERVAL = 600;
26
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();
32
33   constructor(
34     container: HTMLElement,
35     zIndex: number,
36     colors: IColorSet,
37     private _terminal: ITerminal,
38     rendererId: number,
39     readonly bufferService: IBufferService,
40     readonly optionsService: IOptionsService
41   ) {
42     super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService);
43     this._state = {
44       x: null,
45       y: null,
46       isFocused: null,
47       style: null,
48       width: null
49     };
50     this._cursorRenderers = {
51       'bar': this._renderBarCursor.bind(this),
52       'block': this._renderBlockCursor.bind(this),
53       'underline': this._renderUnderlineCursor.bind(this)
54     };
55     // TODO: Consider initial options? Maybe onOptionsChanged should be called at the end of open?
56   }
57
58   public resize(dim: IRenderDimensions): void {
59     super.resize(dim);
60     // Resizing the canvas discards the contents of the canvas so clear state
61     this._state = {
62       x: null,
63       y: null,
64       isFocused: null,
65       style: null,
66       width: null
67     };
68   }
69
70   public reset(): void {
71     this._clearCursor();
72     if (this._cursorBlinkStateManager) {
73       this._cursorBlinkStateManager.dispose();
74       this._cursorBlinkStateManager = null;
75       this.onOptionsChanged();
76     }
77   }
78
79   public onBlur(): void {
80     if (this._cursorBlinkStateManager) {
81       this._cursorBlinkStateManager.pause();
82     }
83     this._terminal.refresh(this._bufferService.buffer.y, this._bufferService.buffer.y);
84   }
85
86   public onFocus(): void {
87     if (this._cursorBlinkStateManager) {
88       this._cursorBlinkStateManager.resume();
89     } else {
90       this._terminal.refresh(this._bufferService.buffer.y, this._bufferService.buffer.y);
91     }
92   }
93
94   public onOptionsChanged(): void {
95     if (this._optionsService.options.cursorBlink) {
96       if (!this._cursorBlinkStateManager) {
97         this._cursorBlinkStateManager = new CursorBlinkStateManager(this._terminal.isFocused, () => {
98           this._render(true);
99         });
100       }
101     } else {
102       if (this._cursorBlinkStateManager) {
103         this._cursorBlinkStateManager.dispose();
104         this._cursorBlinkStateManager = null;
105       }
106     }
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);
110   }
111
112   public onCursorMove(): void {
113     if (this._cursorBlinkStateManager) {
114       this._cursorBlinkStateManager.restartBlinkAnimation();
115     }
116   }
117
118   public onGridChanged(startRow: number, endRow: number): void {
119     if (!this._cursorBlinkStateManager || this._cursorBlinkStateManager.isPaused) {
120       this._render(false);
121     } else {
122       this._cursorBlinkStateManager.restartBlinkAnimation();
123     }
124   }
125
126   private _render(triggeredByAnimationFrame: boolean): void {
127     // Don't draw the cursor if it's hidden
128     if (!this._terminal.cursorState || this._terminal.cursorHidden) {
129       this._clearCursor();
130       return;
131     }
132
133     const cursorY = this._bufferService.buffer.ybase + this._bufferService.buffer.y;
134     const viewportRelativeCursorY = cursorY - this._bufferService.buffer.ydisp;
135
136     // Don't draw the cursor if it's off-screen
137     if (viewportRelativeCursorY < 0 || viewportRelativeCursorY >= this._bufferService.rows) {
138       this._clearCursor();
139       return;
140     }
141
142     this._bufferService.buffer.lines.get(cursorY).loadCell(this._bufferService.buffer.x, this._cell);
143     if (this._cell.content === undefined) {
144       return;
145     }
146
147     if (!this._terminal.isFocused) {
148       this._clearCursor();
149       this._ctx.save();
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);
154       } else {
155         this._renderBlurCursor(this._bufferService.buffer.x, viewportRelativeCursorY, this._cell);
156       }
157       this._ctx.restore();
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();
163       return;
164     }
165
166     // Don't draw the cursor if it's blinking
167     if (this._cursorBlinkStateManager && !this._cursorBlinkStateManager.isCursorVisible) {
168       this._clearCursor();
169       return;
170     }
171
172     if (this._state) {
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()) {
179         return;
180       }
181       this._clearCursor();
182     }
183
184     this._ctx.save();
185     this._cursorRenderers[this._optionsService.options.cursorStyle || 'block'](this._bufferService.buffer.x, viewportRelativeCursorY, this._cell);
186     this._ctx.restore();
187
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();
193   }
194
195   private _clearCursor(): void {
196     if (this._state) {
197       this._clearCells(this._state.x, this._state.y, this._state.width, 1);
198       this._state = {
199         x: null,
200         y: null,
201         isFocused: null,
202         style: null,
203         width: null
204       };
205     }
206   }
207
208   private _renderBarCursor(x: number, y: number, cell: ICellData): void {
209     this._ctx.save();
210     this._ctx.fillStyle = this._colors.cursor.css;
211     this._fillLeftLineAtCell(x, y);
212     this._ctx.restore();
213   }
214
215   private _renderBlockCursor(x: number, y: number, cell: ICellData): void {
216     this._ctx.save();
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);
221     this._ctx.restore();
222   }
223
224   private _renderUnderlineCursor(x: number, y: number, cell: ICellData): void {
225     this._ctx.save();
226     this._ctx.fillStyle = this._colors.cursor.css;
227     this._fillBottomLineAtCells(x, y);
228     this._ctx.restore();
229   }
230
231   private _renderBlurCursor(x: number, y: number, cell: ICellData): void {
232     this._ctx.save();
233     this._ctx.strokeStyle = this._colors.cursor.css;
234     this._strokeRectAtCell(x, y, cell.getWidth(), 1);
235     this._ctx.restore();
236   }
237 }
238
239 class CursorBlinkStateManager {
240   public isCursorVisible: boolean;
241
242   private _animationFrame: number;
243   private _blinkStartTimeout: number;
244   private _blinkInterval: number;
245
246   /**
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.
250    */
251   private _animationTimeRestarted: number;
252
253   constructor(
254     isFocused: boolean,
255     private _renderCallback: () => void
256   ) {
257     this.isCursorVisible = true;
258     if (isFocused) {
259       this._restartInterval();
260     }
261   }
262
263   public get isPaused(): boolean { return !(this._blinkStartTimeout || this._blinkInterval); }
264
265   public dispose(): void {
266     if (this._blinkInterval) {
267       window.clearInterval(this._blinkInterval);
268       this._blinkInterval = null;
269     }
270     if (this._blinkStartTimeout) {
271       window.clearTimeout(this._blinkStartTimeout);
272       this._blinkStartTimeout = null;
273     }
274     if (this._animationFrame) {
275       window.cancelAnimationFrame(this._animationFrame);
276       this._animationFrame = null;
277     }
278   }
279
280   public restartBlinkAnimation(): void {
281     if (this.isPaused) {
282       return;
283     }
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;
292       });
293     }
294   }
295
296   private _restartInterval(timeToStart: number = BLINK_INTERVAL): void {
297     // Clear any existing interval
298     if (this._blinkInterval) {
299       window.clearInterval(this._blinkInterval);
300     }
301
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
305     // setInterval).
306     this._blinkStartTimeout = <number><any>setTimeout(() => {
307       // Check if another animation restart was requested while this was being
308       // started
309       if (this._animationTimeRestarted) {
310         const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted);
311         this._animationTimeRestarted = null;
312         if (time > 0) {
313           this._restartInterval(time);
314           return;
315         }
316       }
317
318       // Hide the cursor
319       this.isCursorVisible = false;
320       this._animationFrame = window.requestAnimationFrame(() => {
321         this._renderCallback();
322         this._animationFrame = null;
323       });
324
325       // Setup the blink interval
326       this._blinkInterval = <number><any>setInterval(() => {
327         // Adjust the animation time if it was restarted
328         if (this._animationTimeRestarted) {
329           // calc time diff
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);
334           return;
335         }
336
337         // Invert visibility and render
338         this.isCursorVisible = !this.isCursorVisible;
339         this._animationFrame = window.requestAnimationFrame(() => {
340           this._renderCallback();
341           this._animationFrame = null;
342         });
343       }, BLINK_INTERVAL);
344     }, timeToStart);
345   }
346
347   public pause(): void {
348     this.isCursorVisible = true;
349     if (this._blinkInterval) {
350       window.clearInterval(this._blinkInterval);
351       this._blinkInterval = null;
352     }
353     if (this._blinkStartTimeout) {
354       window.clearTimeout(this._blinkStartTimeout);
355       this._blinkStartTimeout = null;
356     }
357     if (this._animationFrame) {
358       window.cancelAnimationFrame(this._animationFrame);
359       this._animationFrame = null;
360     }
361   }
362
363   public resume(): void {
364     this._animationTimeRestarted = null;
365     this._restartInterval();
366     this.restartBlinkAnimation();
367   }
368 }