/** * Copyright (c) 2017 The xterm.js authors. All rights reserved. * @license MIT */ import { TextRenderLayer } from './TextRenderLayer'; import { SelectionRenderLayer } from './SelectionRenderLayer'; import { CursorRenderLayer } from './CursorRenderLayer'; import { IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types'; import { IRenderLayer } from './Types'; import { LinkRenderLayer } from './LinkRenderLayer'; import { Disposable } from 'common/Lifecycle'; import { IColorSet, ILinkifier2 } from 'browser/Types'; import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService } from 'browser/services/Services'; import { IBufferService, IOptionsService, IDecorationService, ICoreService } from 'common/services/Services'; import { removeTerminalFromCache } from './atlas/CharAtlasCache'; import { EventEmitter, IEvent } from 'common/EventEmitter'; import { observeDevicePixelDimensions } from 'browser/renderer/DevicePixelObserver'; let nextRendererId = 1; export class CanvasRenderer extends Disposable implements IRenderer { private _id = nextRendererId++; private _renderLayers: IRenderLayer[]; private _devicePixelRatio: number; public dimensions: IRenderDimensions; private _onRequestRedraw = new EventEmitter(); public get onRequestRedraw(): IEvent { return this._onRequestRedraw.event; } constructor( private _colors: IColorSet, private readonly _screenElement: HTMLElement, linkifier2: ILinkifier2, private readonly _bufferService: IBufferService, private readonly _charSizeService: ICharSizeService, private readonly _optionsService: IOptionsService, characterJoinerService: ICharacterJoinerService, coreService: ICoreService, coreBrowserService: ICoreBrowserService, decorationService: IDecorationService ) { super(); const allowTransparency = this._optionsService.rawOptions.allowTransparency; this._renderLayers = [ new TextRenderLayer(this._screenElement, 0, this._colors, allowTransparency, this._id, this._bufferService, this._optionsService, characterJoinerService, decorationService), new SelectionRenderLayer(this._screenElement, 1, this._colors, this._id, this._bufferService, coreBrowserService, decorationService, this._optionsService), new LinkRenderLayer(this._screenElement, 2, this._colors, this._id, linkifier2, this._bufferService, this._optionsService, decorationService), new CursorRenderLayer(this._screenElement, 3, this._colors, this._id, this._onRequestRedraw, this._bufferService, this._optionsService, coreService, coreBrowserService, decorationService) ]; this.dimensions = { scaledCharWidth: 0, scaledCharHeight: 0, scaledCellWidth: 0, scaledCellHeight: 0, scaledCharLeft: 0, scaledCharTop: 0, scaledCanvasWidth: 0, scaledCanvasHeight: 0, canvasWidth: 0, canvasHeight: 0, actualCellWidth: 0, actualCellHeight: 0 }; this._devicePixelRatio = window.devicePixelRatio; this._updateDimensions(); this.register(observeDevicePixelDimensions(this._renderLayers[0].canvas, (w, h) => this._setCanvasDevicePixelDimensions(w, h))); this.onOptionsChanged(); } public dispose(): void { for (const l of this._renderLayers) { l.dispose(); } super.dispose(); removeTerminalFromCache(this._id); } public onDevicePixelRatioChange(): void { // If the device pixel ratio changed, the char atlas needs to be regenerated // and the terminal needs to refreshed if (this._devicePixelRatio !== window.devicePixelRatio) { this._devicePixelRatio = window.devicePixelRatio; this.onResize(this._bufferService.cols, this._bufferService.rows); } } public setColors(colors: IColorSet): void { this._colors = colors; // Clear layers and force a full render for (const l of this._renderLayers) { l.setColors(this._colors); l.reset(); } } public onResize(cols: number, rows: number): void { // Update character and canvas dimensions this._updateDimensions(); // Resize all render layers for (const l of this._renderLayers) { l.resize(this.dimensions); } // Resize the screen this._screenElement.style.width = `${this.dimensions.canvasWidth}px`; this._screenElement.style.height = `${this.dimensions.canvasHeight}px`; } public onCharSizeChanged(): void { this.onResize(this._bufferService.cols, this._bufferService.rows); } public onBlur(): void { this._runOperation(l => l.onBlur()); } public onFocus(): void { this._runOperation(l => l.onFocus()); } public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void { this._runOperation(l => l.onSelectionChanged(start, end, columnSelectMode)); // Selection foreground requires a full re-render if (this._colors.selectionForeground) { this._onRequestRedraw.fire({ start: 0, end: this._bufferService.rows - 1 }); } } public onCursorMove(): void { this._runOperation(l => l.onCursorMove()); } public onOptionsChanged(): void { this._runOperation(l => l.onOptionsChanged()); } public clear(): void { this._runOperation(l => l.reset()); } private _runOperation(operation: (layer: IRenderLayer) => void): void { for (const l of this._renderLayers) { operation(l); } } /** * Performs the refresh loop callback, calling refresh only if a refresh is * necessary before queueing up the next one. */ public renderRows(start: number, end: number): void { for (const l of this._renderLayers) { l.onGridChanged(start, end); } } public clearTextureAtlas(): void { for (const layer of this._renderLayers) { layer.clearTextureAtlas(); } } /** * Recalculates the character and canvas dimensions. */ private _updateDimensions(): void { if (!this._charSizeService.hasValidSize) { return; } // See the WebGL renderer for an explanation of this section. this.dimensions.scaledCharWidth = Math.floor(this._charSizeService.width * window.devicePixelRatio); this.dimensions.scaledCharHeight = Math.ceil(this._charSizeService.height * window.devicePixelRatio); this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._optionsService.rawOptions.lineHeight); this.dimensions.scaledCharTop = this._optionsService.rawOptions.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2); this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._optionsService.rawOptions.letterSpacing); this.dimensions.scaledCharLeft = Math.floor(this._optionsService.rawOptions.letterSpacing / 2); this.dimensions.scaledCanvasHeight = this._bufferService.rows * this.dimensions.scaledCellHeight; this.dimensions.scaledCanvasWidth = this._bufferService.cols * this.dimensions.scaledCellWidth; this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / window.devicePixelRatio); this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / window.devicePixelRatio); this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._bufferService.rows; this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._bufferService.cols; } private _setCanvasDevicePixelDimensions(width: number, height: number): void { this.dimensions.scaledCanvasHeight = height; this.dimensions.scaledCanvasWidth = width; // Resize all render layers for (const l of this._renderLayers) { l.resize(this.dimensions); } this._requestRedrawViewport(); } private _requestRedrawViewport(): void { this._onRequestRedraw.fire({ start: 0, end: this._bufferService.rows - 1 }); } }