2 * Copyright (c) 2017 The xterm.js authors. All rights reserved.
6 import { TextRenderLayer } from '../browser/renderer/TextRenderLayer';
7 import { SelectionRenderLayer } from '../browser/renderer/SelectionRenderLayer';
8 import { CursorRenderLayer } from './CursorRenderLayer';
9 import { IRenderLayer, IRenderer, IRenderDimensions, CharacterJoinerHandler, ICharacterJoinerRegistry } from 'browser/renderer/Types';
10 import { ITerminal } from '../Types';
11 import { LinkRenderLayer } from '../browser/renderer/LinkRenderLayer';
12 import { CharacterJoinerRegistry } from 'browser/renderer/CharacterJoinerRegistry';
13 import { Disposable } from 'common/Lifecycle';
14 import { IColorSet } from 'browser/Types';
15 import { ICharSizeService } from 'browser/services/Services';
16 import { IBufferService, IOptionsService } from 'common/services/Services';
17 import { removeTerminalFromCache } from 'browser/renderer/atlas/CharAtlasCache';
19 let nextRendererId = 1;
21 export class Renderer extends Disposable implements IRenderer {
22 private _id = nextRendererId++;
24 private _renderLayers: IRenderLayer[];
25 private _devicePixelRatio: number;
26 private _characterJoinerRegistry: ICharacterJoinerRegistry;
28 public dimensions: IRenderDimensions;
31 private _colors: IColorSet,
32 private readonly _terminal: ITerminal,
33 readonly bufferService: IBufferService,
34 private readonly _charSizeService: ICharSizeService,
35 readonly optionsService: IOptionsService
38 const allowTransparency = this._terminal.options.allowTransparency;
39 this._characterJoinerRegistry = new CharacterJoinerRegistry(bufferService);
41 this._renderLayers = [
42 new TextRenderLayer(this._terminal.screenElement, 0, this._colors, this._characterJoinerRegistry, allowTransparency, this._id, bufferService, optionsService),
43 new SelectionRenderLayer(this._terminal.screenElement, 1, this._colors, this._id, bufferService, optionsService),
44 new LinkRenderLayer(this._terminal.screenElement, 2, this._colors, this._id, this._terminal.linkifier, bufferService, optionsService),
45 new CursorRenderLayer(this._terminal.screenElement, 3, this._colors, this._terminal, this._id, bufferService, optionsService)
48 scaledCharWidth: null,
49 scaledCharHeight: null,
50 scaledCellWidth: null,
51 scaledCellHeight: null,
54 scaledCanvasWidth: null,
55 scaledCanvasHeight: null,
58 actualCellWidth: null,
59 actualCellHeight: null
61 this._devicePixelRatio = window.devicePixelRatio;
62 this._updateDimensions();
63 this.onOptionsChanged();
66 public dispose(): void {
68 this._renderLayers.forEach(l => l.dispose());
69 removeTerminalFromCache(this._id);
72 public onDevicePixelRatioChange(): void {
73 // If the device pixel ratio changed, the char atlas needs to be regenerated
74 // and the terminal needs to refreshed
75 if (this._devicePixelRatio !== window.devicePixelRatio) {
76 this._devicePixelRatio = window.devicePixelRatio;
77 this.onResize(this._terminal.cols, this._terminal.rows);
81 public setColors(colors: IColorSet): void {
82 this._colors = colors;
84 // Clear layers and force a full render
85 this._renderLayers.forEach(l => {
86 l.setColors(this._colors);
91 public onResize(cols: number, rows: number): void {
92 // Update character and canvas dimensions
93 this._updateDimensions();
95 // Resize all render layers
96 this._renderLayers.forEach(l => l.resize(this.dimensions));
99 this._terminal.screenElement.style.width = `${this.dimensions.canvasWidth}px`;
100 this._terminal.screenElement.style.height = `${this.dimensions.canvasHeight}px`;
103 public onCharSizeChanged(): void {
104 this.onResize(this._terminal.cols, this._terminal.rows);
107 public onBlur(): void {
108 this._runOperation(l => l.onBlur());
111 public onFocus(): void {
112 this._runOperation(l => l.onFocus());
115 public onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean = false): void {
116 this._runOperation(l => l.onSelectionChanged(start, end, columnSelectMode));
119 public onCursorMove(): void {
120 this._runOperation(l => l.onCursorMove());
123 public onOptionsChanged(): void {
124 this._runOperation(l => l.onOptionsChanged());
127 public clear(): void {
128 this._runOperation(l => l.reset());
131 private _runOperation(operation: (layer: IRenderLayer) => void): void {
132 this._renderLayers.forEach(l => operation(l));
136 * Performs the refresh loop callback, calling refresh only if a refresh is
137 * necessary before queueing up the next one.
139 public renderRows(start: number, end: number): void {
140 this._renderLayers.forEach(l => l.onGridChanged(start, end));
144 * Recalculates the character and canvas dimensions.
146 private _updateDimensions(): void {
147 if (!this._charSizeService.hasValidSize) {
151 // Calculate the scaled character width. Width is floored as it must be
152 // drawn to an integer grid in order for the CharAtlas "stamps" to not be
153 // blurry. When text is drawn to the grid not using the CharAtlas, it is
154 // clipped to ensure there is no overlap with the next cell.
155 this.dimensions.scaledCharWidth = Math.floor(this._charSizeService.width * window.devicePixelRatio);
157 // Calculate the scaled character height. Height is ceiled in case
158 // devicePixelRatio is a floating point number in order to ensure there is
159 // enough space to draw the character to the cell.
160 this.dimensions.scaledCharHeight = Math.ceil(this._charSizeService.height * window.devicePixelRatio);
162 // Calculate the scaled cell height, if lineHeight is not 1 then the value
163 // will be floored because since lineHeight can never be lower then 1, there
164 // is a guarentee that the scaled line height will always be larger than
165 // scaled char height.
166 this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight);
168 // Calculate the y coordinate within a cell that text should draw from in
169 // order to draw in the center of a cell.
170 this.dimensions.scaledCharTop = this._terminal.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2);
172 // Calculate the scaled cell width, taking the letterSpacing into account.
173 this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing);
175 // Calculate the x coordinate with a cell that text should draw from in
176 // order to draw in the center of a cell.
177 this.dimensions.scaledCharLeft = Math.floor(this._terminal.options.letterSpacing / 2);
179 // Recalculate the canvas dimensions; scaled* define the actual number of
180 // pixel in the canvas
181 this.dimensions.scaledCanvasHeight = this._terminal.rows * this.dimensions.scaledCellHeight;
182 this.dimensions.scaledCanvasWidth = this._terminal.cols * this.dimensions.scaledCellWidth;
184 // The the size of the canvas on the page. It's very important that this
185 // rounds to nearest integer and not ceils as browsers often set
186 // window.devicePixelRatio as something like 1.100000023841858, when it's
187 // actually 1.1. Ceiling causes blurriness as the backing canvas image is 1
188 // pixel too large for the canvas element size.
189 this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / window.devicePixelRatio);
190 this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / window.devicePixelRatio);
192 // Get the _actual_ dimensions of an individual cell. This needs to be
193 // derived from the canvasWidth/Height calculated above which takes into
194 // account window.devicePixelRatio. ICharSizeService.width/height by itself
195 // is insufficient when the page is not at 100% zoom level as it's measured
196 // in CSS pixels, but the actual char size on the canvas can differ.
197 this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows;
198 this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols;
201 public registerCharacterJoiner(handler: CharacterJoinerHandler): number {
202 return this._characterJoinerRegistry.registerCharacterJoiner(handler);
205 public deregisterCharacterJoiner(joinerId: number): boolean {
206 return this._characterJoinerRegistry.deregisterCharacterJoiner(joinerId);