2 * Copyright (c) 2017 The xterm.js authors. All rights reserved.
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';
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).
22 // const OVERLAP_OWNED_CHAR_DATA: CharData = [null, '', 0, -1];
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();
33 container: HTMLElement,
36 characterJoinerRegistry: ICharacterJoinerRegistry,
39 readonly bufferService: IBufferService,
40 readonly optionsService: IOptionsService
42 super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService);
43 this._state = new GridCache<CharData>();
44 this._characterJoinerRegistry = characterJoinerRegistry;
47 public resize(dim: IRenderDimensions): void {
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 = {};
57 // Resizing the canvas discards the contents of the canvas so clear state
59 this._state.resize(this._bufferService.cols, this._bufferService.rows);
62 public reset(): void {
70 joinerRegistry: ICharacterJoinerRegistry | null,
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;
85 // If true, indicates that the current character(s) to draw were joined.
89 // The character to the left is a wide character, drawing is owned by
91 if (cell.getWidth() === 0) {
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]) {
100 const range = joinedRanges.shift()!;
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
105 cell = new JoinedCellData(
107 line!.translateToString(true, range[0], range[1]),
111 // Skip over the cells occupied by this range in the loop
112 lastCharX = range[1] - 1;
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) {
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;
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.
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;
163 this._forEachCell(firstRow, lastRow, null, (cell, x, y) => {
164 // libvte and xterm both draw the background (but not foreground) of invisible characters,
166 let nextFillStyle = null; // null represents default background color
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(',')})`;
174 nextFillStyle = this._colors.ansi[cell.getFgColor()].css;
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;
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.
190 // our row changed, draw the previous row
191 ctx.fillStyle = prevFillStyle ? prevFillStyle : '';
192 this._fillCells(startX, startY, cols - startX, 1);
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);
203 prevFillStyle = nextFillStyle;
206 // flush the last color we encountered
207 if (prevFillStyle !== null) {
208 ctx.fillStyle = prevFillStyle;
209 this._fillCells(startX, startY, cols - startX, 1);
215 private _drawForeground(firstRow: number, lastRow: number): void {
216 this._forEachCell(firstRow, lastRow, this._characterJoinerRegistry, (cell, x, y) => {
217 if (cell.isInvisible()) {
220 this._drawChars(cell, x, y);
221 if (cell.isUnderline()) {
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(',')})`;
230 this._ctx.fillStyle = this._colors.ansi[cell.getBgColor()].css;
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(',')})`;
238 let fg = cell.getFgColor();
239 if (this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
242 this._ctx.fillStyle = this._colors.ansi[fg].css;
246 this._fillBottomLineAtCells(x, y, cell.getWidth());
252 public onGridChanged(firstRow: number, lastRow: number): void {
253 // Resize has not been called yet
254 if (this._state.cache.length === 0) {
258 if (this._charAtlas) {
259 this._charAtlas.beginFrame();
262 this._clearCells(0, firstRow, this._bufferService.cols, lastRow - firstRow + 1);
263 this._drawBackground(firstRow, lastRow);
264 this._drawForeground(firstRow, lastRow);
267 public onOptionsChanged(): void {
268 this._setTransparency(this._optionsService.options.allowTransparency);
272 * Whether a character is overlapping to the next cell.
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) {
281 // We assume that any ascii character will not overlap
282 if (cell.getCode() < 256) {
286 const chars = cell.getChars();
288 // Deliver from cache if available
289 if (this._characterOverlapCache.hasOwnProperty(chars)) {
290 return this._characterOverlapCache[chars];
295 this._ctx.font = this._characterFont;
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;
302 // Restore the original context
306 this._characterOverlapCache[chars] = overlaps;
311 * Clear the charcater at the cell specified.
312 * @param x The column of the char.
313 * @param y The row of the char.
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) {
322 // this.clearCells(x, y, colsToClear, 1);