xterm
[VSoRC/.git] / node_modules / xterm / src / browser / renderer / TextRenderLayer.ts
1 /**
2  * Copyright (c) 2017 The xterm.js authors. All rights reserved.
3  * @license MIT
4  */
5
6 import { ICharacterJoinerRegistry, IRenderDimensions } from 'browser/renderer/Types';
7 import { CharData, ICellData } from 'common/Types';
8 import { GridCache } from 'browser/renderer/GridCache';
9 import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer';
10 import { AttributeData } from 'common/buffer/AttributeData';
11 import { NULL_CELL_CODE, Content } from 'common/buffer/Constants';
12 import { JoinedCellData } from 'browser/renderer/CharacterJoinerRegistry';
13 import { IColorSet } from 'browser/Types';
14 import { CellData } from 'common/buffer/CellData';
15 import { IOptionsService, IBufferService } from 'common/services/Services';
16
17 /**
18  * This CharData looks like a null character, which will forc a clear and render
19  * when the character changes (a regular space ' ' character may not as it's
20  * drawn state is a cleared cell).
21  */
22 // const OVERLAP_OWNED_CHAR_DATA: CharData = [null, '', 0, -1];
23
24 export class TextRenderLayer extends BaseRenderLayer {
25   private _state: GridCache<CharData>;
26   private _characterWidth: number = 0;
27   private _characterFont: string = '';
28   private _characterOverlapCache: { [key: string]: boolean } = {};
29   private _characterJoinerRegistry: ICharacterJoinerRegistry;
30   private _workCell = new CellData();
31
32   constructor(
33     container: HTMLElement,
34     zIndex: number,
35     colors: IColorSet,
36     characterJoinerRegistry: ICharacterJoinerRegistry,
37     alpha: boolean,
38     rendererId: number,
39     readonly bufferService: IBufferService,
40     readonly optionsService: IOptionsService
41   ) {
42     super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService);
43     this._state = new GridCache<CharData>();
44     this._characterJoinerRegistry = characterJoinerRegistry;
45   }
46
47   public resize(dim: IRenderDimensions): void {
48     super.resize(dim);
49
50     // Clear the character width cache if the font or width has changed
51     const terminalFont = this._getFont(false, false);
52     if (this._characterWidth !== dim.scaledCharWidth || this._characterFont !== terminalFont) {
53       this._characterWidth = dim.scaledCharWidth;
54       this._characterFont = terminalFont;
55       this._characterOverlapCache = {};
56     }
57     // Resizing the canvas discards the contents of the canvas so clear state
58     this._state.clear();
59     this._state.resize(this._bufferService.cols, this._bufferService.rows);
60   }
61
62   public reset(): void {
63     this._state.clear();
64     this._clearAll();
65   }
66
67   private _forEachCell(
68     firstRow: number,
69     lastRow: number,
70     joinerRegistry: ICharacterJoinerRegistry | null,
71     callback: (
72       cell: ICellData,
73       x: number,
74       y: number
75     ) => void
76   ): void {
77     for (let y = firstRow; y <= lastRow; y++) {
78       const row = y + this._bufferService.buffer.ydisp;
79       const line = this._bufferService.buffer.lines.get(row);
80       const joinedRanges = joinerRegistry ? joinerRegistry.getJoinedCharacters(row) : [];
81       for (let x = 0; x < this._bufferService.cols; x++) {
82         line!.loadCell(x, this._workCell);
83         let cell = this._workCell;
84
85         // If true, indicates that the current character(s) to draw were joined.
86         let isJoined = false;
87         let lastCharX = x;
88
89         // The character to the left is a wide character, drawing is owned by
90         // the char at x-1
91         if (cell.getWidth() === 0) {
92           continue;
93         }
94
95         // Process any joined character ranges as needed. Because of how the
96         // ranges are produced, we know that they are valid for the characters
97         // and attributes of our input.
98         if (joinedRanges.length > 0 && x === joinedRanges[0][0]) {
99           isJoined = true;
100           const range = joinedRanges.shift()!;
101
102           // We already know the exact start and end column of the joined range,
103           // so we get the string and width representing it directly
104
105           cell = new JoinedCellData(
106             this._workCell,
107             line!.translateToString(true, range[0], range[1]),
108             range[1] - range[0]
109           );
110
111           // Skip over the cells occupied by this range in the loop
112           lastCharX = range[1] - 1;
113         }
114
115         // If the character is an overlapping char and the character to the
116         // right is a space, take ownership of the cell to the right. We skip
117         // this check for joined characters because their rendering likely won't
118         // yield the same result as rendering the last character individually.
119         if (!isJoined && this._isOverlapping(cell)) {
120           // If the character is overlapping, we want to force a re-render on every
121           // frame. This is specifically to work around the case where two
122           // overlaping chars `a` and `b` are adjacent, the cursor is moved to b and a
123           // space is added. Without this, the first half of `b` would never
124           // get removed, and `a` would not re-render because it thinks it's
125           // already in the correct state.
126           // this._state.cache[x][y] = OVERLAP_OWNED_CHAR_DATA;
127           if (lastCharX < line!.length - 1 && line!.getCodePoint(lastCharX + 1) === NULL_CELL_CODE) {
128             // patch width to 2
129             cell.content &= ~Content.WIDTH_MASK;
130             cell.content |= 2 << Content.WIDTH_SHIFT;
131             // this._clearChar(x + 1, y);
132             // The overlapping char's char data will force a clear and render when the
133             // overlapping char is no longer to the left of the character and also when
134             // the space changes to another character.
135             // this._state.cache[x + 1][y] = OVERLAP_OWNED_CHAR_DATA;
136           }
137         }
138
139         callback(
140           cell,
141           x,
142           y
143         );
144
145         x = lastCharX;
146       }
147     }
148   }
149
150   /**
151    * Draws the background for a specified range of columns. Tries to batch adjacent cells of the
152    * same color together to reduce draw calls.
153    */
154   private _drawBackground(firstRow: number, lastRow: number): void {
155     const ctx = this._ctx;
156     const cols = this._bufferService.cols;
157     let startX: number = 0;
158     let startY: number = 0;
159     let prevFillStyle: string | null = null;
160
161     ctx.save();
162
163     this._forEachCell(firstRow, lastRow, null, (cell, x, y) => {
164       // libvte and xterm both draw the background (but not foreground) of invisible characters,
165       // so we should too.
166       let nextFillStyle = null; // null represents default background color
167
168       if (cell.isInverse()) {
169         if (cell.isFgDefault()) {
170           nextFillStyle = this._colors.foreground.css;
171         } else if (cell.isFgRGB()) {
172           nextFillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
173         } else {
174           nextFillStyle = this._colors.ansi[cell.getFgColor()].css;
175         }
176       } else if (cell.isBgRGB()) {
177         nextFillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
178       } else if (cell.isBgPalette()) {
179         nextFillStyle = this._colors.ansi[cell.getBgColor()].css;
180       }
181
182       if (prevFillStyle === null) {
183         // This is either the first iteration, or the default background was set. Either way, we
184         // don't need to draw anything.
185         startX = x;
186         startY = y;
187       }
188
189       if (y !== startY) {
190         // our row changed, draw the previous row
191         ctx.fillStyle = prevFillStyle ? prevFillStyle : '';
192         this._fillCells(startX, startY, cols - startX, 1);
193         startX = x;
194         startY = y;
195       } else if (prevFillStyle !== nextFillStyle) {
196         // our color changed, draw the previous characters in this row
197         ctx.fillStyle = prevFillStyle ? prevFillStyle : '';
198         this._fillCells(startX, startY, x - startX, 1);
199         startX = x;
200         startY = y;
201       }
202
203       prevFillStyle = nextFillStyle;
204     });
205
206     // flush the last color we encountered
207     if (prevFillStyle !== null) {
208       ctx.fillStyle = prevFillStyle;
209       this._fillCells(startX, startY, cols - startX, 1);
210     }
211
212     ctx.restore();
213   }
214
215   private _drawForeground(firstRow: number, lastRow: number): void {
216     this._forEachCell(firstRow, lastRow, this._characterJoinerRegistry, (cell, x, y) => {
217       if (cell.isInvisible()) {
218         return;
219       }
220       this._drawChars(cell, x, y);
221       if (cell.isUnderline()) {
222         this._ctx.save();
223
224         if (cell.isInverse()) {
225           if (cell.isBgDefault()) {
226             this._ctx.fillStyle = this._colors.background.css;
227           } else if (cell.isBgRGB()) {
228             this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
229           } else {
230             this._ctx.fillStyle = this._colors.ansi[cell.getBgColor()].css;
231           }
232         } else {
233           if (cell.isFgDefault()) {
234             this._ctx.fillStyle = this._colors.foreground.css;
235           } else if (cell.isFgRGB()) {
236             this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
237           } else {
238             let fg = cell.getFgColor();
239             if (this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
240               fg += 8;
241             }
242             this._ctx.fillStyle = this._colors.ansi[fg].css;
243           }
244         }
245
246         this._fillBottomLineAtCells(x, y, cell.getWidth());
247         this._ctx.restore();
248       }
249     });
250   }
251
252   public onGridChanged(firstRow: number, lastRow: number): void {
253     // Resize has not been called yet
254     if (this._state.cache.length === 0) {
255       return;
256     }
257
258     if (this._charAtlas) {
259       this._charAtlas.beginFrame();
260     }
261
262     this._clearCells(0, firstRow, this._bufferService.cols, lastRow - firstRow + 1);
263     this._drawBackground(firstRow, lastRow);
264     this._drawForeground(firstRow, lastRow);
265   }
266
267   public onOptionsChanged(): void {
268     this._setTransparency(this._optionsService.options.allowTransparency);
269   }
270
271   /**
272    * Whether a character is overlapping to the next cell.
273    */
274   private _isOverlapping(cell: ICellData): boolean {
275     // Only single cell characters can be overlapping, rendering issues can
276     // occur without this check
277     if (cell.getWidth() !== 1) {
278       return false;
279     }
280
281     // We assume that any ascii character will not overlap
282     if (cell.getCode() < 256) {
283       return false;
284     }
285
286     const chars = cell.getChars();
287
288     // Deliver from cache if available
289     if (this._characterOverlapCache.hasOwnProperty(chars)) {
290       return this._characterOverlapCache[chars];
291     }
292
293     // Setup the font
294     this._ctx.save();
295     this._ctx.font = this._characterFont;
296
297     // Measure the width of the character, but Math.floor it
298     // because that is what the renderer does when it calculates
299     // the character dimensions we are comparing against
300     const overlaps = Math.floor(this._ctx.measureText(chars).width) > this._characterWidth;
301
302     // Restore the original context
303     this._ctx.restore();
304
305     // Cache and return
306     this._characterOverlapCache[chars] = overlaps;
307     return overlaps;
308   }
309
310   /**
311    * Clear the charcater at the cell specified.
312    * @param x The column of the char.
313    * @param y The row of the char.
314    */
315   // private _clearChar(x: number, y: number): void {
316   //   let colsToClear = 1;
317   //   // Clear the adjacent character if it was wide
318   //   const state = this._state.cache[x][y];
319   //   if (state && state[CHAR_DATA_WIDTH_INDEX] === 2) {
320   //     colsToClear = 2;
321   //   }
322   //   this.clearCells(x, y, colsToClear, 1);
323   // }
324 }