aboutsummaryrefslogtreecommitdiffstats
path: root/node_modules/xterm/src/browser/renderer/atlas/DynamicCharAtlas.ts
diff options
context:
space:
mode:
authorAnthony Schneider <tonyschneider3@gmail.com>2022-02-11 19:40:35 -0600
committerAnthony Schneider <tonyschneider3@gmail.com>2022-02-11 19:40:35 -0600
commitb52feccdcc58c1f4583c8542632d6c026335dea7 (patch)
tree5e242dd13ed4bbfff85a07109ef826f80874e2a6 /node_modules/xterm/src/browser/renderer/atlas/DynamicCharAtlas.ts
parent94862321e2e4a58e3209c037e8061f0435b3aa82 (diff)
Changed javascript to be in its own file. Began (messy) setup for terminal.
Diffstat (limited to 'node_modules/xterm/src/browser/renderer/atlas/DynamicCharAtlas.ts')
-rw-r--r--node_modules/xterm/src/browser/renderer/atlas/DynamicCharAtlas.ts404
1 files changed, 404 insertions, 0 deletions
diff --git a/node_modules/xterm/src/browser/renderer/atlas/DynamicCharAtlas.ts b/node_modules/xterm/src/browser/renderer/atlas/DynamicCharAtlas.ts
new file mode 100644
index 0000000..118dbcd
--- /dev/null
+++ b/node_modules/xterm/src/browser/renderer/atlas/DynamicCharAtlas.ts
@@ -0,0 +1,404 @@
+/**
+ * Copyright (c) 2017 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, TEXT_BASELINE } from 'browser/renderer/atlas/Constants';
+import { IGlyphIdentifier, ICharAtlasConfig } from 'browser/renderer/atlas/Types';
+import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas';
+import { DEFAULT_ANSI_COLORS } from 'browser/ColorManager';
+import { LRUMap } from 'browser/renderer/atlas/LRUMap';
+import { isFirefox, isSafari } from 'common/Platform';
+import { IColor } from 'browser/Types';
+import { throwIfFalsy } from 'browser/renderer/RendererUtils';
+import { color } from 'browser/Color';
+
+// In practice we're probably never going to exhaust a texture this large. For debugging purposes,
+// however, it can be useful to set this to a really tiny value, to verify that LRU eviction works.
+const TEXTURE_WIDTH = 1024;
+const TEXTURE_HEIGHT = 1024;
+
+const TRANSPARENT_COLOR = {
+ css: 'rgba(0, 0, 0, 0)',
+ rgba: 0
+};
+
+// Drawing to the cache is expensive: If we have to draw more than this number of glyphs to the
+// cache in a single frame, give up on trying to cache anything else, and try to finish the current
+// frame ASAP.
+//
+// This helps to limit the amount of damage a program can do when it would otherwise thrash the
+// cache.
+const FRAME_CACHE_DRAW_LIMIT = 100;
+
+/**
+ * The number of milliseconds to wait before generating the ImageBitmap, this is to debounce/batch
+ * the operation as window.createImageBitmap is asynchronous.
+ */
+const GLYPH_BITMAP_COMMIT_DELAY = 100;
+
+interface IGlyphCacheValue {
+ index: number;
+ isEmpty: boolean;
+ inBitmap: boolean;
+}
+
+export function getGlyphCacheKey(glyph: IGlyphIdentifier): number {
+ // Note that this only returns a valid key when code < 256
+ // Layout:
+ // 0b00000000000000000000000000000001: italic (1)
+ // 0b00000000000000000000000000000010: dim (1)
+ // 0b00000000000000000000000000000100: bold (1)
+ // 0b00000000000000000000111111111000: fg (9)
+ // 0b00000000000111111111000000000000: bg (9)
+ // 0b00011111111000000000000000000000: code (8)
+ // 0b11100000000000000000000000000000: unused (3)
+ return glyph.code << 21 | glyph.bg << 12 | glyph.fg << 3 | (glyph.bold ? 0 : 4) + (glyph.dim ? 0 : 2) + (glyph.italic ? 0 : 1);
+}
+
+export class DynamicCharAtlas extends BaseCharAtlas {
+ // An ordered map that we're using to keep track of where each glyph is in the atlas texture.
+ // It's ordered so that we can determine when to remove the old entries.
+ private _cacheMap: LRUMap<IGlyphCacheValue>;
+
+ // The texture that the atlas is drawn to
+ private _cacheCanvas: HTMLCanvasElement;
+ private _cacheCtx: CanvasRenderingContext2D;
+
+ // A temporary context that glyphs are drawn to before being transfered to the atlas.
+ private _tmpCtx: CanvasRenderingContext2D;
+
+ // The number of characters stored in the atlas by width/height
+ private _width: number;
+ private _height: number;
+
+ private _drawToCacheCount: number = 0;
+
+ // An array of glyph keys that are waiting on the bitmap to be generated.
+ private _glyphsWaitingOnBitmap: IGlyphCacheValue[] = [];
+
+ // The timeout that is used to batch bitmap generation so it's not requested for every new glyph.
+ private _bitmapCommitTimeout: number | null = null;
+
+ // The bitmap to draw from, this is much faster on other browsers than others.
+ private _bitmap: ImageBitmap | null = null;
+
+ constructor(document: Document, private _config: ICharAtlasConfig) {
+ super();
+ this._cacheCanvas = document.createElement('canvas');
+ this._cacheCanvas.width = TEXTURE_WIDTH;
+ this._cacheCanvas.height = TEXTURE_HEIGHT;
+ // The canvas needs alpha because we use clearColor to convert the background color to alpha.
+ // It might also contain some characters with transparent backgrounds if allowTransparency is
+ // set.
+ this._cacheCtx = throwIfFalsy(this._cacheCanvas.getContext('2d', { alpha: true }));
+
+ const tmpCanvas = document.createElement('canvas');
+ tmpCanvas.width = this._config.scaledCharWidth;
+ tmpCanvas.height = this._config.scaledCharHeight;
+ this._tmpCtx = throwIfFalsy(tmpCanvas.getContext('2d', { alpha: this._config.allowTransparency }));
+
+ this._width = Math.floor(TEXTURE_WIDTH / this._config.scaledCharWidth);
+ this._height = Math.floor(TEXTURE_HEIGHT / this._config.scaledCharHeight);
+ const capacity = this._width * this._height;
+ this._cacheMap = new LRUMap(capacity);
+ this._cacheMap.prealloc(capacity);
+
+ // This is useful for debugging
+ // document.body.appendChild(this._cacheCanvas);
+ }
+
+ public dispose(): void {
+ if (this._bitmapCommitTimeout !== null) {
+ window.clearTimeout(this._bitmapCommitTimeout);
+ this._bitmapCommitTimeout = null;
+ }
+ }
+
+ public beginFrame(): void {
+ this._drawToCacheCount = 0;
+ }
+
+ public clear(): void {
+ if (this._cacheMap.size > 0) {
+ const capacity = this._width * this._height;
+ this._cacheMap = new LRUMap(capacity);
+ this._cacheMap.prealloc(capacity);
+ }
+ this._cacheCtx.clearRect(0, 0, TEXTURE_WIDTH, TEXTURE_HEIGHT);
+ this._tmpCtx.clearRect(0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight);
+ }
+
+ public draw(
+ ctx: CanvasRenderingContext2D,
+ glyph: IGlyphIdentifier,
+ x: number,
+ y: number
+ ): boolean {
+ // Space is always an empty cell, special case this as it's so common
+ if (glyph.code === 32) {
+ return true;
+ }
+
+ // Exit early for uncachable glyphs
+ if (!this._canCache(glyph)) {
+ return false;
+ }
+
+ const glyphKey = getGlyphCacheKey(glyph);
+ const cacheValue = this._cacheMap.get(glyphKey);
+ if (cacheValue !== null && cacheValue !== undefined) {
+ this._drawFromCache(ctx, cacheValue, x, y);
+ return true;
+ }
+ if (this._drawToCacheCount < FRAME_CACHE_DRAW_LIMIT) {
+ let index;
+ if (this._cacheMap.size < this._cacheMap.capacity) {
+ index = this._cacheMap.size;
+ } else {
+ // we're out of space, so our call to set will delete this item
+ index = this._cacheMap.peek()!.index;
+ }
+ const cacheValue = this._drawToCache(glyph, index);
+ this._cacheMap.set(glyphKey, cacheValue);
+ this._drawFromCache(ctx, cacheValue, x, y);
+ return true;
+ }
+ return false;
+ }
+
+ private _canCache(glyph: IGlyphIdentifier): boolean {
+ // Only cache ascii and extended characters for now, to be safe. In the future, we could do
+ // something more complicated to determine the expected width of a character.
+ //
+ // If we switch the renderer over to webgl at some point, we may be able to use blending modes
+ // to draw overlapping glyphs from the atlas:
+ // https://github.com/servo/webrender/issues/464#issuecomment-255632875
+ // https://webglfundamentals.org/webgl/lessons/webgl-text-texture.html
+ return glyph.code < 256;
+ }
+
+ private _toCoordinateX(index: number): number {
+ return (index % this._width) * this._config.scaledCharWidth;
+ }
+
+ private _toCoordinateY(index: number): number {
+ return Math.floor(index / this._width) * this._config.scaledCharHeight;
+ }
+
+ private _drawFromCache(
+ ctx: CanvasRenderingContext2D,
+ cacheValue: IGlyphCacheValue,
+ x: number,
+ y: number
+ ): void {
+ // We don't actually need to do anything if this is whitespace.
+ if (cacheValue.isEmpty) {
+ return;
+ }
+ const cacheX = this._toCoordinateX(cacheValue.index);
+ const cacheY = this._toCoordinateY(cacheValue.index);
+ ctx.drawImage(
+ cacheValue.inBitmap ? this._bitmap! : this._cacheCanvas,
+ cacheX,
+ cacheY,
+ this._config.scaledCharWidth,
+ this._config.scaledCharHeight,
+ x,
+ y,
+ this._config.scaledCharWidth,
+ this._config.scaledCharHeight
+ );
+ }
+
+ private _getColorFromAnsiIndex(idx: number): IColor {
+ if (idx < this._config.colors.ansi.length) {
+ return this._config.colors.ansi[idx];
+ }
+ return DEFAULT_ANSI_COLORS[idx];
+ }
+
+ private _getBackgroundColor(glyph: IGlyphIdentifier): IColor {
+ if (this._config.allowTransparency) {
+ // The background color might have some transparency, so we need to render it as fully
+ // transparent in the atlas. Otherwise we'd end up drawing the transparent background twice
+ // around the anti-aliased edges of the glyph, and it would look too dark.
+ return TRANSPARENT_COLOR;
+ }
+ if (glyph.bg === INVERTED_DEFAULT_COLOR) {
+ return this._config.colors.foreground;
+ }
+ if (glyph.bg < 256) {
+ return this._getColorFromAnsiIndex(glyph.bg);
+ }
+ return this._config.colors.background;
+ }
+
+ private _getForegroundColor(glyph: IGlyphIdentifier): IColor {
+ if (glyph.fg === INVERTED_DEFAULT_COLOR) {
+ return color.opaque(this._config.colors.background);
+ }
+ if (glyph.fg < 256) {
+ // 256 color support
+ return this._getColorFromAnsiIndex(glyph.fg);
+ }
+ return this._config.colors.foreground;
+ }
+
+ // TODO: We do this (or something similar) in multiple places. We should split this off
+ // into a shared function.
+ private _drawToCache(glyph: IGlyphIdentifier, index: number): IGlyphCacheValue {
+ this._drawToCacheCount++;
+
+ this._tmpCtx.save();
+
+ // draw the background
+ const backgroundColor = this._getBackgroundColor(glyph);
+ // Use a 'copy' composite operation to clear any existing glyph out of _tmpCtxWithAlpha, regardless of
+ // transparency in backgroundColor
+ this._tmpCtx.globalCompositeOperation = 'copy';
+ this._tmpCtx.fillStyle = backgroundColor.css;
+ this._tmpCtx.fillRect(0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight);
+ this._tmpCtx.globalCompositeOperation = 'source-over';
+
+ // draw the foreground/glyph
+ const fontWeight = glyph.bold ? this._config.fontWeightBold : this._config.fontWeight;
+ const fontStyle = glyph.italic ? 'italic' : '';
+ this._tmpCtx.font =
+ `${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`;
+ this._tmpCtx.textBaseline = TEXT_BASELINE;
+
+ this._tmpCtx.fillStyle = this._getForegroundColor(glyph).css;
+
+ // Apply alpha to dim the character
+ if (glyph.dim) {
+ this._tmpCtx.globalAlpha = DIM_OPACITY;
+ }
+ // Draw the character
+ this._tmpCtx.fillText(glyph.chars, 0, this._config.scaledCharHeight);
+
+ // clear the background from the character to avoid issues with drawing over the previous
+ // character if it extends past it's bounds
+ let imageData = this._tmpCtx.getImageData(
+ 0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight
+ );
+ let isEmpty = false;
+ if (!this._config.allowTransparency) {
+ isEmpty = clearColor(imageData, backgroundColor);
+ }
+
+ // If this charcater is underscore and empty, shift it up until it is visible, try for a maximum
+ // of 5 pixels.
+ if (isEmpty && glyph.chars === '_' && !this._config.allowTransparency) {
+ for (let offset = 1; offset <= 5; offset++) {
+ // Draw the character
+ this._tmpCtx.fillText(glyph.chars, 0, this._config.scaledCharHeight - offset);
+
+ // clear the background from the character to avoid issues with drawing over the previous
+ // character if it extends past it's bounds
+ imageData = this._tmpCtx.getImageData(
+ 0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight
+ );
+ isEmpty = clearColor(imageData, backgroundColor);
+ if (!isEmpty) {
+ break;
+ }
+ }
+ }
+
+ this._tmpCtx.restore();
+
+ // copy the data from imageData to _cacheCanvas
+ const x = this._toCoordinateX(index);
+ const y = this._toCoordinateY(index);
+ // putImageData doesn't do any blending, so it will overwrite any existing cache entry for us
+ this._cacheCtx.putImageData(imageData, x, y);
+
+ // Add the glyph and queue it to the bitmap (if the browser supports it)
+ const cacheValue = {
+ index,
+ isEmpty,
+ inBitmap: false
+ };
+ this._addGlyphToBitmap(cacheValue);
+
+ return cacheValue;
+ }
+
+ private _addGlyphToBitmap(cacheValue: IGlyphCacheValue): void {
+ // Support is patchy for createImageBitmap at the moment, pass a canvas back
+ // if support is lacking as drawImage works there too. Firefox is also
+ // included here as ImageBitmap appears both buggy and has horrible
+ // performance (tested on v55).
+ if (!('createImageBitmap' in window) || isFirefox || isSafari) {
+ return;
+ }
+
+ // Add the glyph to the queue
+ this._glyphsWaitingOnBitmap.push(cacheValue);
+
+ // Check if bitmap generation timeout already exists
+ if (this._bitmapCommitTimeout !== null) {
+ return;
+ }
+
+ this._bitmapCommitTimeout = window.setTimeout(() => this._generateBitmap(), GLYPH_BITMAP_COMMIT_DELAY);
+ }
+
+ private _generateBitmap(): void {
+ const glyphsMovingToBitmap = this._glyphsWaitingOnBitmap;
+ this._glyphsWaitingOnBitmap = [];
+ window.createImageBitmap(this._cacheCanvas).then(bitmap => {
+ // Set bitmap
+ this._bitmap = bitmap;
+
+ // Mark all new glyphs as in bitmap, excluding glyphs that came in after
+ // the bitmap was requested
+ for (let i = 0; i < glyphsMovingToBitmap.length; i++) {
+ const value = glyphsMovingToBitmap[i];
+ // It doesn't matter if the value was already evicted, it will be
+ // released from memory after this block if so.
+ value.inBitmap = true;
+ }
+ });
+ this._bitmapCommitTimeout = null;
+ }
+}
+
+// This is used for debugging the renderer, just swap out `new DynamicCharAtlas` with
+// `new NoneCharAtlas`.
+export class NoneCharAtlas extends BaseCharAtlas {
+ constructor(document: Document, config: ICharAtlasConfig) {
+ super();
+ }
+
+ public draw(
+ ctx: CanvasRenderingContext2D,
+ glyph: IGlyphIdentifier,
+ x: number,
+ y: number
+ ): boolean {
+ return false;
+ }
+}
+
+/**
+ * Makes a partiicular rgb color in an ImageData completely transparent.
+ * @returns True if the result is "empty", meaning all pixels are fully transparent.
+ */
+function clearColor(imageData: ImageData, color: IColor): boolean {
+ let isEmpty = true;
+ const r = color.rgba >>> 24;
+ const g = color.rgba >>> 16 & 0xFF;
+ const b = color.rgba >>> 8 & 0xFF;
+ for (let offset = 0; offset < imageData.data.length; offset += 4) {
+ if (imageData.data[offset] === r &&
+ imageData.data[offset + 1] === g &&
+ imageData.data[offset + 2] === b) {
+ imageData.data[offset + 3] = 0;
+ } else {
+ isEmpty = false;
+ }
+ }
+ return isEmpty;
+}