UNPKG

@xterm/addon-webgl

Version:

An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables a WebGL2-based renderer. This addon requires xterm.js v4+.

221 lines (198 loc) 8.21 kB
/** * Copyright (c) 2017 The xterm.js authors. All rights reserved. * @license MIT */ import { ReadonlyColorSet } from 'browser/Types'; import { acquireTextureAtlas } from 'browser/renderer/shared/CharAtlasCache'; import { TEXT_BASELINE } from 'browser/renderer/shared/Constants'; import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; import { IRenderDimensions, ITextureAtlas } from 'browser/renderer/shared/Types'; import { ICoreBrowserService, IThemeService } from 'browser/services/Services'; import { Disposable, toDisposable } from 'common/Lifecycle'; import { CellData } from 'common/buffer/CellData'; import { IOptionsService } from 'common/services/Services'; import { Terminal } from '@xterm/xterm'; import { IRenderLayer } from './Types'; export abstract class BaseRenderLayer extends Disposable implements IRenderLayer { private _canvas: HTMLCanvasElement; protected _ctx!: CanvasRenderingContext2D; private _deviceCharWidth: number = 0; private _deviceCharHeight: number = 0; private _deviceCellWidth: number = 0; private _deviceCellHeight: number = 0; private _deviceCharLeft: number = 0; private _deviceCharTop: number = 0; protected _charAtlas: ITextureAtlas | undefined; constructor( terminal: Terminal, private _container: HTMLElement, id: string, zIndex: number, private _alpha: boolean, protected readonly _coreBrowserService: ICoreBrowserService, protected readonly _optionsService: IOptionsService, protected readonly _themeService: IThemeService ) { super(); this._canvas = this._coreBrowserService.mainDocument.createElement('canvas'); this._canvas.classList.add(`xterm-${id}-layer`); this._canvas.style.zIndex = zIndex.toString(); this._initCanvas(); this._container.appendChild(this._canvas); this.register(this._themeService.onChangeColors(e => { this._refreshCharAtlas(terminal, e); this.reset(terminal); })); this.register(toDisposable(() => { this._canvas.remove(); })); } private _initCanvas(): void { this._ctx = throwIfFalsy(this._canvas.getContext('2d', { alpha: this._alpha })); // Draw the background if this is an opaque layer if (!this._alpha) { this._clearAll(); } } public handleBlur(terminal: Terminal): void {} public handleFocus(terminal: Terminal): void {} public handleCursorMove(terminal: Terminal): void {} public handleGridChanged(terminal: Terminal, startRow: number, endRow: number): void {} public handleSelectionChanged(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {} protected _setTransparency(terminal: Terminal, alpha: boolean): void { // Do nothing when alpha doesn't change if (alpha === this._alpha) { return; } // Create new canvas and replace old one const oldCanvas = this._canvas; this._alpha = alpha; // Cloning preserves properties this._canvas = this._canvas.cloneNode() as HTMLCanvasElement; this._initCanvas(); this._container.replaceChild(this._canvas, oldCanvas); // Regenerate char atlas and force a full redraw this._refreshCharAtlas(terminal, this._themeService.colors); this.handleGridChanged(terminal, 0, terminal.rows - 1); } /** * Refreshes the char atlas, aquiring a new one if necessary. * @param terminal The terminal. * @param colorSet The color set to use for the char atlas. */ private _refreshCharAtlas(terminal: Terminal, colorSet: ReadonlyColorSet): void { if (this._deviceCharWidth <= 0 && this._deviceCharHeight <= 0) { return; } this._charAtlas = acquireTextureAtlas(terminal, this._optionsService.rawOptions, colorSet, this._deviceCellWidth, this._deviceCellHeight, this._deviceCharWidth, this._deviceCharHeight, this._coreBrowserService.dpr); this._charAtlas.warmUp(); } public resize(terminal: Terminal, dim: IRenderDimensions): void { this._deviceCellWidth = dim.device.cell.width; this._deviceCellHeight = dim.device.cell.height; this._deviceCharWidth = dim.device.char.width; this._deviceCharHeight = dim.device.char.height; this._deviceCharLeft = dim.device.char.left; this._deviceCharTop = dim.device.char.top; this._canvas.width = dim.device.canvas.width; this._canvas.height = dim.device.canvas.height; this._canvas.style.width = `${dim.css.canvas.width}px`; this._canvas.style.height = `${dim.css.canvas.height}px`; // Draw the background if this is an opaque layer if (!this._alpha) { this._clearAll(); } this._refreshCharAtlas(terminal, this._themeService.colors); } public abstract reset(terminal: Terminal): void; /** * Fills a 1px line (2px on HDPI) at the bottom of the cell. This uses the * existing fillStyle on the context. * @param x The column to fill. * @param y The row to fill. */ protected _fillBottomLineAtCells(x: number, y: number, width: number = 1): void { this._ctx.fillRect( x * this._deviceCellWidth, (y + 1) * this._deviceCellHeight - this._coreBrowserService.dpr - 1 /* Ensure it's drawn within the cell */, width * this._deviceCellWidth, this._coreBrowserService.dpr); } /** * Clears the entire canvas. */ protected _clearAll(): void { if (this._alpha) { this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); } else { this._ctx.fillStyle = this._themeService.colors.background.css; this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height); } } /** * Clears 1+ cells completely. * @param x The column to start at. * @param y The row to start at. * @param width The number of columns to clear. * @param height The number of rows to clear. */ protected _clearCells(x: number, y: number, width: number, height: number): void { if (this._alpha) { this._ctx.clearRect( x * this._deviceCellWidth, y * this._deviceCellHeight, width * this._deviceCellWidth, height * this._deviceCellHeight); } else { this._ctx.fillStyle = this._themeService.colors.background.css; this._ctx.fillRect( x * this._deviceCellWidth, y * this._deviceCellHeight, width * this._deviceCellWidth, height * this._deviceCellHeight); } } /** * Draws a truecolor character at the cell. The character will be clipped to * ensure that it fits with the cell, including the cell to the right if it's * a wide character. This uses the existing fillStyle on the context. * @param terminal The terminal. * @param cell The cell data for the character to draw. * @param x The column to draw at. * @param y The row to draw at. */ protected _fillCharTrueColor(terminal: Terminal, cell: CellData, x: number, y: number): void { this._ctx.font = this._getFont(terminal, false, false); this._ctx.textBaseline = TEXT_BASELINE; this._clipCell(x, y, cell.getWidth()); this._ctx.fillText( cell.getChars(), x * this._deviceCellWidth + this._deviceCharLeft, y * this._deviceCellHeight + this._deviceCharTop + this._deviceCharHeight); } /** * Clips a cell to ensure no pixels will be drawn outside of it. * @param x The column to clip. * @param y The row to clip. * @param width The number of columns to clip. */ private _clipCell(x: number, y: number, width: number): void { this._ctx.beginPath(); this._ctx.rect( x * this._deviceCellWidth, y * this._deviceCellHeight, width * this._deviceCellWidth, this._deviceCellHeight); this._ctx.clip(); } /** * Gets the current font. * @param terminal The terminal. * @param isBold If we should use the bold fontWeight. */ protected _getFont(terminal: Terminal, isBold: boolean, isItalic: boolean): string { const fontWeight = isBold ? terminal.options.fontWeightBold : terminal.options.fontWeight; const fontStyle = isItalic ? 'italic' : ''; return `${fontStyle} ${fontWeight} ${terminal.options.fontSize! * this._coreBrowserService.dpr}px ${terminal.options.fontFamily}`; } }