xterm
[VSoRC/.git] / node_modules / xterm / src / browser / renderer / TextRenderLayer.ts
diff --git a/node_modules/xterm/src/browser/renderer/TextRenderLayer.ts b/node_modules/xterm/src/browser/renderer/TextRenderLayer.ts
new file mode 100644 (file)
index 0000000..330400c
--- /dev/null
@@ -0,0 +1,324 @@
+/**
+ * Copyright (c) 2017 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { ICharacterJoinerRegistry, IRenderDimensions } from 'browser/renderer/Types';
+import { CharData, ICellData } from 'common/Types';
+import { GridCache } from 'browser/renderer/GridCache';
+import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer';
+import { AttributeData } from 'common/buffer/AttributeData';
+import { NULL_CELL_CODE, Content } from 'common/buffer/Constants';
+import { JoinedCellData } from 'browser/renderer/CharacterJoinerRegistry';
+import { IColorSet } from 'browser/Types';
+import { CellData } from 'common/buffer/CellData';
+import { IOptionsService, IBufferService } from 'common/services/Services';
+
+/**
+ * This CharData looks like a null character, which will forc a clear and render
+ * when the character changes (a regular space ' ' character may not as it's
+ * drawn state is a cleared cell).
+ */
+// const OVERLAP_OWNED_CHAR_DATA: CharData = [null, '', 0, -1];
+
+export class TextRenderLayer extends BaseRenderLayer {
+  private _state: GridCache<CharData>;
+  private _characterWidth: number = 0;
+  private _characterFont: string = '';
+  private _characterOverlapCache: { [key: string]: boolean } = {};
+  private _characterJoinerRegistry: ICharacterJoinerRegistry;
+  private _workCell = new CellData();
+
+  constructor(
+    container: HTMLElement,
+    zIndex: number,
+    colors: IColorSet,
+    characterJoinerRegistry: ICharacterJoinerRegistry,
+    alpha: boolean,
+    rendererId: number,
+    readonly bufferService: IBufferService,
+    readonly optionsService: IOptionsService
+  ) {
+    super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService);
+    this._state = new GridCache<CharData>();
+    this._characterJoinerRegistry = characterJoinerRegistry;
+  }
+
+  public resize(dim: IRenderDimensions): void {
+    super.resize(dim);
+
+    // Clear the character width cache if the font or width has changed
+    const terminalFont = this._getFont(false, false);
+    if (this._characterWidth !== dim.scaledCharWidth || this._characterFont !== terminalFont) {
+      this._characterWidth = dim.scaledCharWidth;
+      this._characterFont = terminalFont;
+      this._characterOverlapCache = {};
+    }
+    // Resizing the canvas discards the contents of the canvas so clear state
+    this._state.clear();
+    this._state.resize(this._bufferService.cols, this._bufferService.rows);
+  }
+
+  public reset(): void {
+    this._state.clear();
+    this._clearAll();
+  }
+
+  private _forEachCell(
+    firstRow: number,
+    lastRow: number,
+    joinerRegistry: ICharacterJoinerRegistry | null,
+    callback: (
+      cell: ICellData,
+      x: number,
+      y: number
+    ) => void
+  ): void {
+    for (let y = firstRow; y <= lastRow; y++) {
+      const row = y + this._bufferService.buffer.ydisp;
+      const line = this._bufferService.buffer.lines.get(row);
+      const joinedRanges = joinerRegistry ? joinerRegistry.getJoinedCharacters(row) : [];
+      for (let x = 0; x < this._bufferService.cols; x++) {
+        line!.loadCell(x, this._workCell);
+        let cell = this._workCell;
+
+        // If true, indicates that the current character(s) to draw were joined.
+        let isJoined = false;
+        let lastCharX = x;
+
+        // The character to the left is a wide character, drawing is owned by
+        // the char at x-1
+        if (cell.getWidth() === 0) {
+          continue;
+        }
+
+        // Process any joined character ranges as needed. Because of how the
+        // ranges are produced, we know that they are valid for the characters
+        // and attributes of our input.
+        if (joinedRanges.length > 0 && x === joinedRanges[0][0]) {
+          isJoined = true;
+          const range = joinedRanges.shift()!;
+
+          // We already know the exact start and end column of the joined range,
+          // so we get the string and width representing it directly
+
+          cell = new JoinedCellData(
+            this._workCell,
+            line!.translateToString(true, range[0], range[1]),
+            range[1] - range[0]
+          );
+
+          // Skip over the cells occupied by this range in the loop
+          lastCharX = range[1] - 1;
+        }
+
+        // If the character is an overlapping char and the character to the
+        // right is a space, take ownership of the cell to the right. We skip
+        // this check for joined characters because their rendering likely won't
+        // yield the same result as rendering the last character individually.
+        if (!isJoined && this._isOverlapping(cell)) {
+          // If the character is overlapping, we want to force a re-render on every
+          // frame. This is specifically to work around the case where two
+          // overlaping chars `a` and `b` are adjacent, the cursor is moved to b and a
+          // space is added. Without this, the first half of `b` would never
+          // get removed, and `a` would not re-render because it thinks it's
+          // already in the correct state.
+          // this._state.cache[x][y] = OVERLAP_OWNED_CHAR_DATA;
+          if (lastCharX < line!.length - 1 && line!.getCodePoint(lastCharX + 1) === NULL_CELL_CODE) {
+            // patch width to 2
+            cell.content &= ~Content.WIDTH_MASK;
+            cell.content |= 2 << Content.WIDTH_SHIFT;
+            // this._clearChar(x + 1, y);
+            // The overlapping char's char data will force a clear and render when the
+            // overlapping char is no longer to the left of the character and also when
+            // the space changes to another character.
+            // this._state.cache[x + 1][y] = OVERLAP_OWNED_CHAR_DATA;
+          }
+        }
+
+        callback(
+          cell,
+          x,
+          y
+        );
+
+        x = lastCharX;
+      }
+    }
+  }
+
+  /**
+   * Draws the background for a specified range of columns. Tries to batch adjacent cells of the
+   * same color together to reduce draw calls.
+   */
+  private _drawBackground(firstRow: number, lastRow: number): void {
+    const ctx = this._ctx;
+    const cols = this._bufferService.cols;
+    let startX: number = 0;
+    let startY: number = 0;
+    let prevFillStyle: string | null = null;
+
+    ctx.save();
+
+    this._forEachCell(firstRow, lastRow, null, (cell, x, y) => {
+      // libvte and xterm both draw the background (but not foreground) of invisible characters,
+      // so we should too.
+      let nextFillStyle = null; // null represents default background color
+
+      if (cell.isInverse()) {
+        if (cell.isFgDefault()) {
+          nextFillStyle = this._colors.foreground.css;
+        } else if (cell.isFgRGB()) {
+          nextFillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
+        } else {
+          nextFillStyle = this._colors.ansi[cell.getFgColor()].css;
+        }
+      } else if (cell.isBgRGB()) {
+        nextFillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
+      } else if (cell.isBgPalette()) {
+        nextFillStyle = this._colors.ansi[cell.getBgColor()].css;
+      }
+
+      if (prevFillStyle === null) {
+        // This is either the first iteration, or the default background was set. Either way, we
+        // don't need to draw anything.
+        startX = x;
+        startY = y;
+      }
+
+      if (y !== startY) {
+        // our row changed, draw the previous row
+        ctx.fillStyle = prevFillStyle ? prevFillStyle : '';
+        this._fillCells(startX, startY, cols - startX, 1);
+        startX = x;
+        startY = y;
+      } else if (prevFillStyle !== nextFillStyle) {
+        // our color changed, draw the previous characters in this row
+        ctx.fillStyle = prevFillStyle ? prevFillStyle : '';
+        this._fillCells(startX, startY, x - startX, 1);
+        startX = x;
+        startY = y;
+      }
+
+      prevFillStyle = nextFillStyle;
+    });
+
+    // flush the last color we encountered
+    if (prevFillStyle !== null) {
+      ctx.fillStyle = prevFillStyle;
+      this._fillCells(startX, startY, cols - startX, 1);
+    }
+
+    ctx.restore();
+  }
+
+  private _drawForeground(firstRow: number, lastRow: number): void {
+    this._forEachCell(firstRow, lastRow, this._characterJoinerRegistry, (cell, x, y) => {
+      if (cell.isInvisible()) {
+        return;
+      }
+      this._drawChars(cell, x, y);
+      if (cell.isUnderline()) {
+        this._ctx.save();
+
+        if (cell.isInverse()) {
+          if (cell.isBgDefault()) {
+            this._ctx.fillStyle = this._colors.background.css;
+          } else if (cell.isBgRGB()) {
+            this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
+          } else {
+            this._ctx.fillStyle = this._colors.ansi[cell.getBgColor()].css;
+          }
+        } else {
+          if (cell.isFgDefault()) {
+            this._ctx.fillStyle = this._colors.foreground.css;
+          } else if (cell.isFgRGB()) {
+            this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
+          } else {
+            let fg = cell.getFgColor();
+            if (this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
+              fg += 8;
+            }
+            this._ctx.fillStyle = this._colors.ansi[fg].css;
+          }
+        }
+
+        this._fillBottomLineAtCells(x, y, cell.getWidth());
+        this._ctx.restore();
+      }
+    });
+  }
+
+  public onGridChanged(firstRow: number, lastRow: number): void {
+    // Resize has not been called yet
+    if (this._state.cache.length === 0) {
+      return;
+    }
+
+    if (this._charAtlas) {
+      this._charAtlas.beginFrame();
+    }
+
+    this._clearCells(0, firstRow, this._bufferService.cols, lastRow - firstRow + 1);
+    this._drawBackground(firstRow, lastRow);
+    this._drawForeground(firstRow, lastRow);
+  }
+
+  public onOptionsChanged(): void {
+    this._setTransparency(this._optionsService.options.allowTransparency);
+  }
+
+  /**
+   * Whether a character is overlapping to the next cell.
+   */
+  private _isOverlapping(cell: ICellData): boolean {
+    // Only single cell characters can be overlapping, rendering issues can
+    // occur without this check
+    if (cell.getWidth() !== 1) {
+      return false;
+    }
+
+    // We assume that any ascii character will not overlap
+    if (cell.getCode() < 256) {
+      return false;
+    }
+
+    const chars = cell.getChars();
+
+    // Deliver from cache if available
+    if (this._characterOverlapCache.hasOwnProperty(chars)) {
+      return this._characterOverlapCache[chars];
+    }
+
+    // Setup the font
+    this._ctx.save();
+    this._ctx.font = this._characterFont;
+
+    // Measure the width of the character, but Math.floor it
+    // because that is what the renderer does when it calculates
+    // the character dimensions we are comparing against
+    const overlaps = Math.floor(this._ctx.measureText(chars).width) > this._characterWidth;
+
+    // Restore the original context
+    this._ctx.restore();
+
+    // Cache and return
+    this._characterOverlapCache[chars] = overlaps;
+    return overlaps;
+  }
+
+  /**
+   * Clear the charcater at the cell specified.
+   * @param x The column of the char.
+   * @param y The row of the char.
+   */
+  // private _clearChar(x: number, y: number): void {
+  //   let colsToClear = 1;
+  //   // Clear the adjacent character if it was wide
+  //   const state = this._state.cache[x][y];
+  //   if (state && state[CHAR_DATA_WIDTH_INDEX] === 2) {
+  //     colsToClear = 2;
+  //   }
+  //   this.clearCells(x, y, colsToClear, 1);
+  // }
+}