xterm
[VSoRC/.git] / node_modules / xterm / src / browser / renderer / BaseRenderLayer.ts
1 /**
2  * Copyright (c) 2017 The xterm.js authors. All rights reserved.
3  * @license MIT
4  */
5
6 import { IRenderDimensions, IRenderLayer } from 'browser/renderer/Types';
7 import { ICellData } from 'common/Types';
8 import { DEFAULT_COLOR, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE } from 'common/buffer/Constants';
9 import { IGlyphIdentifier } from 'browser/renderer/atlas/Types';
10 import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
11 import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas';
12 import { acquireCharAtlas } from 'browser/renderer/atlas/CharAtlasCache';
13 import { AttributeData } from 'common/buffer/AttributeData';
14 import { IColorSet } from 'browser/Types';
15 import { CellData } from 'common/buffer/CellData';
16 import { IBufferService, IOptionsService } from 'common/services/Services';
17 import { throwIfFalsy } from 'browser/renderer/RendererUtils';
18
19 export abstract class BaseRenderLayer implements IRenderLayer {
20   private _canvas: HTMLCanvasElement;
21   protected _ctx!: CanvasRenderingContext2D;
22   private _scaledCharWidth: number = 0;
23   private _scaledCharHeight: number = 0;
24   private _scaledCellWidth: number = 0;
25   private _scaledCellHeight: number = 0;
26   private _scaledCharLeft: number = 0;
27   private _scaledCharTop: number = 0;
28
29   protected _charAtlas: BaseCharAtlas | undefined;
30
31   /**
32    * An object that's reused when drawing glyphs in order to reduce GC.
33    */
34   private _currentGlyphIdentifier: IGlyphIdentifier = {
35     chars: '',
36     code: 0,
37     bg: 0,
38     fg: 0,
39     bold: false,
40     dim: false,
41     italic: false
42   };
43
44   constructor(
45     private _container: HTMLElement,
46     id: string,
47     zIndex: number,
48     private _alpha: boolean,
49     protected _colors: IColorSet,
50     private _rendererId: number,
51     protected readonly _bufferService: IBufferService,
52     protected readonly _optionsService: IOptionsService
53   ) {
54     this._canvas = document.createElement('canvas');
55     this._canvas.classList.add(`xterm-${id}-layer`);
56     this._canvas.style.zIndex = zIndex.toString();
57     this._initCanvas();
58     this._container.appendChild(this._canvas);
59   }
60
61   public dispose(): void {
62     this._container.removeChild(this._canvas);
63     if (this._charAtlas) {
64       this._charAtlas.dispose();
65     }
66   }
67
68   private _initCanvas(): void {
69     this._ctx = throwIfFalsy(this._canvas.getContext('2d', {alpha: this._alpha}));
70     // Draw the background if this is an opaque layer
71     if (!this._alpha) {
72       this._clearAll();
73     }
74   }
75
76   public onOptionsChanged(): void {}
77   public onBlur(): void {}
78   public onFocus(): void {}
79   public onCursorMove(): void {}
80   public onGridChanged(startRow: number, endRow: number): void {}
81   public onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean = false): void {}
82
83   public setColors(colorSet: IColorSet): void {
84     this._refreshCharAtlas(colorSet);
85   }
86
87   protected _setTransparency(alpha: boolean): void {
88     // Do nothing when alpha doesn't change
89     if (alpha === this._alpha) {
90       return;
91     }
92
93     // Create new canvas and replace old one
94     const oldCanvas = this._canvas;
95     this._alpha = alpha;
96     // Cloning preserves properties
97     this._canvas = <HTMLCanvasElement>this._canvas.cloneNode();
98     this._initCanvas();
99     this._container.replaceChild(this._canvas, oldCanvas);
100
101     // Regenerate char atlas and force a full redraw
102     this._refreshCharAtlas(this._colors);
103     this.onGridChanged(0, this._bufferService.rows - 1);
104   }
105
106   /**
107    * Refreshes the char atlas, aquiring a new one if necessary.
108    * @param colorSet The color set to use for the char atlas.
109    */
110   private _refreshCharAtlas(colorSet: IColorSet): void {
111     if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) {
112       return;
113     }
114     this._charAtlas = acquireCharAtlas(this._optionsService.options, this._rendererId, colorSet, this._scaledCharWidth, this._scaledCharHeight);
115     this._charAtlas.warmUp();
116   }
117
118   public resize(dim: IRenderDimensions): void {
119     this._scaledCellWidth = dim.scaledCellWidth;
120     this._scaledCellHeight = dim.scaledCellHeight;
121     this._scaledCharWidth = dim.scaledCharWidth;
122     this._scaledCharHeight = dim.scaledCharHeight;
123     this._scaledCharLeft = dim.scaledCharLeft;
124     this._scaledCharTop = dim.scaledCharTop;
125     this._canvas.width = dim.scaledCanvasWidth;
126     this._canvas.height = dim.scaledCanvasHeight;
127     this._canvas.style.width = `${dim.canvasWidth}px`;
128     this._canvas.style.height = `${dim.canvasHeight}px`;
129
130     // Draw the background if this is an opaque layer
131     if (!this._alpha) {
132       this._clearAll();
133     }
134
135     this._refreshCharAtlas(this._colors);
136   }
137
138   public abstract reset(): void;
139
140   /**
141    * Fills 1+ cells completely. This uses the existing fillStyle on the context.
142    * @param x The column to start at.
143    * @param y The row to start at
144    * @param width The number of columns to fill.
145    * @param height The number of rows to fill.
146    */
147   protected _fillCells(x: number, y: number, width: number, height: number): void {
148     this._ctx.fillRect(
149         x * this._scaledCellWidth,
150         y * this._scaledCellHeight,
151         width * this._scaledCellWidth,
152         height * this._scaledCellHeight);
153   }
154
155   /**
156    * Fills a 1px line (2px on HDPI) at the bottom of the cell. This uses the
157    * existing fillStyle on the context.
158    * @param x The column to fill.
159    * @param y The row to fill.
160    */
161   protected _fillBottomLineAtCells(x: number, y: number, width: number = 1): void {
162     this._ctx.fillRect(
163         x * this._scaledCellWidth,
164         (y + 1) * this._scaledCellHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */,
165         width * this._scaledCellWidth,
166         window.devicePixelRatio);
167   }
168
169   /**
170    * Fills a 1px line (2px on HDPI) at the left of the cell. This uses the
171    * existing fillStyle on the context.
172    * @param x The column to fill.
173    * @param y The row to fill.
174    */
175   protected _fillLeftLineAtCell(x: number, y: number): void {
176     this._ctx.fillRect(
177         x * this._scaledCellWidth,
178         y * this._scaledCellHeight,
179         window.devicePixelRatio,
180         this._scaledCellHeight);
181   }
182
183   /**
184    * Strokes a 1px rectangle (2px on HDPI) around a cell. This uses the existing
185    * strokeStyle on the context.
186    * @param x The column to fill.
187    * @param y The row to fill.
188    */
189   protected _strokeRectAtCell(x: number, y: number, width: number, height: number): void {
190     this._ctx.lineWidth = window.devicePixelRatio;
191     this._ctx.strokeRect(
192         x * this._scaledCellWidth + window.devicePixelRatio / 2,
193         y * this._scaledCellHeight + (window.devicePixelRatio / 2),
194         width * this._scaledCellWidth - window.devicePixelRatio,
195         (height * this._scaledCellHeight) - window.devicePixelRatio);
196   }
197
198   /**
199    * Clears the entire canvas.
200    */
201   protected _clearAll(): void {
202     if (this._alpha) {
203       this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
204     } else {
205       this._ctx.fillStyle = this._colors.background.css;
206       this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height);
207     }
208   }
209
210   /**
211    * Clears 1+ cells completely.
212    * @param x The column to start at.
213    * @param y The row to start at.
214    * @param width The number of columns to clear.
215    * @param height The number of rows to clear.
216    */
217   protected _clearCells(x: number, y: number, width: number, height: number): void {
218     if (this._alpha) {
219       this._ctx.clearRect(
220           x * this._scaledCellWidth,
221           y * this._scaledCellHeight,
222           width * this._scaledCellWidth,
223           height * this._scaledCellHeight);
224     } else {
225       this._ctx.fillStyle = this._colors.background.css;
226       this._ctx.fillRect(
227           x * this._scaledCellWidth,
228           y * this._scaledCellHeight,
229           width * this._scaledCellWidth,
230           height * this._scaledCellHeight);
231     }
232   }
233
234   /**
235    * Draws a truecolor character at the cell. The character will be clipped to
236    * ensure that it fits with the cell, including the cell to the right if it's
237    * a wide character. This uses the existing fillStyle on the context.
238    * @param cell The cell data for the character to draw.
239    * @param x The column to draw at.
240    * @param y The row to draw at.
241    * @param color The color of the character.
242    */
243   protected _fillCharTrueColor(cell: CellData, x: number, y: number): void {
244     this._ctx.font = this._getFont(false, false);
245     this._ctx.textBaseline = 'middle';
246     this._clipRow(y);
247     this._ctx.fillText(
248         cell.getChars(),
249         x * this._scaledCellWidth + this._scaledCharLeft,
250         y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2);
251   }
252
253   /**
254    * Draws one or more characters at a cell. If possible this will draw using
255    * the character atlas to reduce draw time.
256    * @param chars The character or characters.
257    * @param code The character code.
258    * @param width The width of the characters.
259    * @param x The column to draw at.
260    * @param y The row to draw at.
261    * @param fg The foreground color, in the format stored within the attributes.
262    * @param bg The background color, in the format stored within the attributes.
263    * This is used to validate whether a cached image can be used.
264    * @param bold Whether the text is bold.
265    */
266   protected _drawChars(cell: ICellData, x: number, y: number): void {
267
268     // skip cache right away if we draw in RGB
269     // Note: to avoid bad runtime JoinedCellData will be skipped
270     //       in the cache handler itself (atlasDidDraw == false) and
271     //       fall through to uncached later down below
272     if (cell.isFgRGB() || cell.isBgRGB()) {
273       this._drawUncachedChars(cell, x, y);
274       return;
275     }
276
277     let fg;
278     let bg;
279     if (cell.isInverse()) {
280       fg = (cell.isBgDefault()) ? INVERTED_DEFAULT_COLOR : cell.getBgColor();
281       bg = (cell.isFgDefault()) ? INVERTED_DEFAULT_COLOR : cell.getFgColor();
282     } else {
283       bg = (cell.isBgDefault()) ? DEFAULT_COLOR : cell.getBgColor();
284       fg = (cell.isFgDefault()) ? DEFAULT_COLOR : cell.getFgColor();
285     }
286
287     const drawInBrightColor = this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8 && fg !== INVERTED_DEFAULT_COLOR;
288
289     fg += drawInBrightColor ? 8 : 0;
290     this._currentGlyphIdentifier.chars = cell.getChars() || WHITESPACE_CELL_CHAR;
291     this._currentGlyphIdentifier.code = cell.getCode() || WHITESPACE_CELL_CODE;
292     this._currentGlyphIdentifier.bg = bg;
293     this._currentGlyphIdentifier.fg = fg;
294     this._currentGlyphIdentifier.bold = !!cell.isBold();
295     this._currentGlyphIdentifier.dim = !!cell.isDim();
296     this._currentGlyphIdentifier.italic = !!cell.isItalic();
297     const atlasDidDraw = this._charAtlas && this._charAtlas.draw(
298       this._ctx,
299       this._currentGlyphIdentifier,
300       x * this._scaledCellWidth + this._scaledCharLeft,
301       y * this._scaledCellHeight + this._scaledCharTop
302     );
303
304     if (!atlasDidDraw) {
305       this._drawUncachedChars(cell, x, y);
306     }
307   }
308
309   /**
310    * Draws one or more characters at one or more cells. The character(s) will be
311    * clipped to ensure that they fit with the cell(s), including the cell to the
312    * right if the last character is a wide character.
313    * @param chars The character.
314    * @param width The width of the character.
315    * @param fg The foreground color, in the format stored within the attributes.
316    * @param x The column to draw at.
317    * @param y The row to draw at.
318    */
319   private _drawUncachedChars(cell: ICellData, x: number, y: number): void {
320     this._ctx.save();
321     this._ctx.font = this._getFont(!!cell.isBold(), !!cell.isItalic());
322     this._ctx.textBaseline = 'middle';
323
324     if (cell.isInverse()) {
325       if (cell.isBgDefault()) {
326         this._ctx.fillStyle = this._colors.background.css;
327       } else if (cell.isBgRGB()) {
328         this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
329       } else {
330         this._ctx.fillStyle = this._colors.ansi[cell.getBgColor()].css;
331       }
332     } else {
333       if (cell.isFgDefault()) {
334         this._ctx.fillStyle = this._colors.foreground.css;
335       } else if (cell.isFgRGB()) {
336         this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
337       } else {
338         let fg = cell.getFgColor();
339         if (this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
340           fg += 8;
341         }
342         this._ctx.fillStyle = this._colors.ansi[fg].css;
343       }
344     }
345
346     this._clipRow(y);
347
348     // Apply alpha to dim the character
349     if (cell.isDim()) {
350       this._ctx.globalAlpha = DIM_OPACITY;
351     }
352     // Draw the character
353     this._ctx.fillText(
354         cell.getChars(),
355         x * this._scaledCellWidth + this._scaledCharLeft,
356         y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2);
357     this._ctx.restore();
358   }
359
360   /**
361    * Clips a row to ensure no pixels will be drawn outside the cells in the row.
362    * @param y The row to clip.
363    */
364   private _clipRow(y: number): void {
365     this._ctx.beginPath();
366     this._ctx.rect(
367         0,
368         y * this._scaledCellHeight,
369         this._bufferService.cols * this._scaledCellWidth,
370         this._scaledCellHeight);
371     this._ctx.clip();
372   }
373
374   /**
375    * Gets the current font.
376    * @param isBold If we should use the bold fontWeight.
377    */
378   protected _getFont(isBold: boolean, isItalic: boolean): string {
379     const fontWeight = isBold ? this._optionsService.options.fontWeightBold : this._optionsService.options.fontWeight;
380     const fontStyle = isItalic ? 'italic' : '';
381
382     return `${fontStyle} ${fontWeight} ${this._optionsService.options.fontSize * window.devicePixelRatio}px ${this._optionsService.options.fontFamily}`;
383   }
384 }
385