xterm
[VSoRC/.git] / node_modules / xterm / src / renderer / dom / DomRenderer.ts
1 /**
2  * Copyright (c) 2018 The xterm.js authors. All rights reserved.
3  * @license MIT
4  */
5
6 import { IRenderer, IRenderDimensions, CharacterJoinerHandler } from 'browser/renderer/Types';
7 import { ITerminal } from '../../Types';
8 import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_BLINK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from 'browser/renderer/dom/DomRendererRowFactory';
9 import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
10 import { Disposable } from 'common/Lifecycle';
11 import { IColorSet, ILinkifierEvent } from 'browser/Types';
12 import { ICharSizeService } from 'browser/services/Services';
13 import { IOptionsService } from 'common/services/Services';
14
15 const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-';
16 const ROW_CONTAINER_CLASS = 'xterm-rows';
17 const FG_CLASS_PREFIX = 'xterm-fg-';
18 const BG_CLASS_PREFIX = 'xterm-bg-';
19 const FOCUS_CLASS = 'xterm-focus';
20 const SELECTION_CLASS = 'xterm-selection';
21
22 let nextTerminalId = 1;
23
24 /**
25  * A fallback renderer for when canvas is slow. This is not meant to be
26  * particularly fast or feature complete, more just stable and usable for when
27  * canvas is not an option.
28  */
29 export class DomRenderer extends Disposable implements IRenderer {
30   private _rowFactory: DomRendererRowFactory;
31   private _terminalClass: number = nextTerminalId++;
32
33   private _themeStyleElement: HTMLStyleElement;
34   private _dimensionsStyleElement: HTMLStyleElement;
35   private _rowContainer: HTMLElement;
36   private _rowElements: HTMLElement[] = [];
37   private _selectionContainer: HTMLElement;
38
39   public dimensions: IRenderDimensions;
40
41   constructor(
42     private _terminal: ITerminal,
43     private _colors: IColorSet,
44     private _charSizeService: ICharSizeService,
45     private _optionsService: IOptionsService
46   ) {
47     super();
48
49     this._rowContainer = document.createElement('div');
50     this._rowContainer.classList.add(ROW_CONTAINER_CLASS);
51     this._rowContainer.style.lineHeight = 'normal';
52     this._rowContainer.setAttribute('aria-hidden', 'true');
53     this._refreshRowElements(this._terminal.cols, this._terminal.rows);
54     this._selectionContainer = document.createElement('div');
55     this._selectionContainer.classList.add(SELECTION_CLASS);
56     this._selectionContainer.setAttribute('aria-hidden', 'true');
57
58     this.dimensions = {
59       scaledCharWidth: null,
60       scaledCharHeight: null,
61       scaledCellWidth: null,
62       scaledCellHeight: null,
63       scaledCharLeft: null,
64       scaledCharTop: null,
65       scaledCanvasWidth: null,
66       scaledCanvasHeight: null,
67       canvasWidth: null,
68       canvasHeight: null,
69       actualCellWidth: null,
70       actualCellHeight: null
71     };
72     this._updateDimensions();
73     this._injectCss();
74
75     this._rowFactory = new DomRendererRowFactory(document, this._optionsService);
76
77     this._terminal.element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass);
78     this._terminal.screenElement.appendChild(this._rowContainer);
79     this._terminal.screenElement.appendChild(this._selectionContainer);
80
81     this._terminal.linkifier.onLinkHover(e => this._onLinkHover(e));
82     this._terminal.linkifier.onLinkLeave(e => this._onLinkLeave(e));
83   }
84
85   public dispose(): void {
86     this._terminal.element.classList.remove(TERMINAL_CLASS_PREFIX + this._terminalClass);
87     this._terminal.screenElement.removeChild(this._rowContainer);
88     this._terminal.screenElement.removeChild(this._selectionContainer);
89     this._terminal.screenElement.removeChild(this._themeStyleElement);
90     this._terminal.screenElement.removeChild(this._dimensionsStyleElement);
91     super.dispose();
92   }
93
94   private _updateDimensions(): void {
95     this.dimensions.scaledCharWidth = this._charSizeService.width * window.devicePixelRatio;
96     this.dimensions.scaledCharHeight = Math.ceil(this._charSizeService.height * window.devicePixelRatio);
97     this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing);
98     this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight);
99     this.dimensions.scaledCharLeft = 0;
100     this.dimensions.scaledCharTop = 0;
101     this.dimensions.scaledCanvasWidth = this.dimensions.scaledCellWidth * this._terminal.cols;
102     this.dimensions.scaledCanvasHeight = this.dimensions.scaledCellHeight * this._terminal.rows;
103     this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / window.devicePixelRatio);
104     this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / window.devicePixelRatio);
105     this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols;
106     this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows;
107
108     this._rowElements.forEach(element => {
109       element.style.width = `${this.dimensions.canvasWidth}px`;
110       element.style.height = `${this.dimensions.actualCellHeight}px`;
111       element.style.lineHeight = `${this.dimensions.actualCellHeight}px`;
112       // Make sure rows don't overflow onto following row
113       element.style.overflow = 'hidden';
114     });
115
116     if (!this._dimensionsStyleElement) {
117       this._dimensionsStyleElement = document.createElement('style');
118       this._terminal.screenElement.appendChild(this._dimensionsStyleElement);
119     }
120
121     const styles =
122         `${this._terminalSelector} .${ROW_CONTAINER_CLASS} span {` +
123         ` display: inline-block;` +
124         ` height: 100%;` +
125         ` vertical-align: top;` +
126         ` width: ${this.dimensions.actualCellWidth}px` +
127         `}`;
128
129     this._dimensionsStyleElement.innerHTML = styles;
130
131     this._selectionContainer.style.height = (<any>this._terminal)._viewportElement.style.height;
132     this._terminal.screenElement.style.width = `${this.dimensions.canvasWidth}px`;
133     this._terminal.screenElement.style.height = `${this.dimensions.canvasHeight}px`;
134   }
135
136   public setColors(colors: IColorSet): void {
137     this._colors = colors;
138     this._injectCss();
139   }
140
141   private _injectCss(): void {
142     if (!this._themeStyleElement) {
143       this._themeStyleElement = document.createElement('style');
144       this._terminal.screenElement.appendChild(this._themeStyleElement);
145     }
146
147     // Base CSS
148     let styles =
149         `${this._terminalSelector} .${ROW_CONTAINER_CLASS} {` +
150         ` color: ${this._colors.foreground.css};` +
151         ` background-color: ${this._colors.background.css};` +
152         ` font-family: ${this._terminal.options.fontFamily};` +
153         ` font-size: ${this._terminal.options.fontSize}px;` +
154         `}`;
155     // Text styles
156     styles +=
157         `${this._terminalSelector} span:not(.${BOLD_CLASS}) {` +
158         ` font-weight: ${this._terminal.options.fontWeight};` +
159         `}` +
160         `${this._terminalSelector} span.${BOLD_CLASS} {` +
161         ` font-weight: ${this._terminal.options.fontWeightBold};` +
162         `}` +
163         `${this._terminalSelector} span.${ITALIC_CLASS} {` +
164         ` font-style: italic;` +
165         `}`;
166     // Blink animation
167     styles +=
168         `@keyframes blink_box_shadow {` +
169         ` 50% {` +
170         `  box-shadow: none;` +
171         ` }` +
172         `}`;
173     styles +=
174         `@keyframes blink_block {` +
175         ` 0% {` +
176         `  background-color: ${this._colors.cursor.css};` +
177         `  color: ${this._colors.cursorAccent.css};` +
178         ` }` +
179         ` 50% {` +
180         `  background-color: ${this._colors.cursorAccent.css};` +
181         `  color: ${this._colors.cursor.css};` +
182         ` }` +
183         `}`;
184     // Cursor
185     styles +=
186         `${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` +
187         ` outline: 1px solid ${this._colors.cursor.css};` +
188         ` outline-offset: -1px;` +
189         `}` +
190         `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_BLINK_CLASS}:not(.${CURSOR_STYLE_BLOCK_CLASS}) {` +
191         ` animation: blink_box_shadow 1s step-end infinite;` +
192         `}` +
193         `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_BLINK_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` +
194         ` animation: blink_block 1s step-end infinite;` +
195         `}` +
196         `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` +
197         ` background-color: ${this._colors.cursor.css};` +
198         ` color: ${this._colors.cursorAccent.css};` +
199         `}` +
200         `${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BAR_CLASS} {` +
201         ` box-shadow: 1px 0 0 ${this._colors.cursor.css} inset;` +
202         `}` +
203         `${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_UNDERLINE_CLASS} {` +
204         ` box-shadow: 0 -1px 0 ${this._colors.cursor.css} inset;` +
205         `}`;
206     // Selection
207     styles +=
208         `${this._terminalSelector} .${SELECTION_CLASS} {` +
209         ` position: absolute;` +
210         ` top: 0;` +
211         ` left: 0;` +
212         ` z-index: 1;` +
213         ` pointer-events: none;` +
214         `}` +
215         `${this._terminalSelector} .${SELECTION_CLASS} div {` +
216         ` position: absolute;` +
217         ` background-color: ${this._colors.selection.css};` +
218         `}`;
219     // Colors
220     this._colors.ansi.forEach((c, i) => {
221       styles +=
222           `${this._terminalSelector} .${FG_CLASS_PREFIX}${i} { color: ${c.css}; }` +
223           `${this._terminalSelector} .${BG_CLASS_PREFIX}${i} { background-color: ${c.css}; }`;
224     });
225     styles +=
226         `${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { color: ${this._colors.background.css}; }` +
227         `${this._terminalSelector} .${BG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { background-color: ${this._colors.foreground.css}; }`;
228
229     this._themeStyleElement.innerHTML = styles;
230   }
231
232   public onDevicePixelRatioChange(): void {
233     this._updateDimensions();
234   }
235
236   private _refreshRowElements(cols: number, rows: number): void {
237     // Add missing elements
238     for (let i = this._rowElements.length; i <= rows; i++) {
239       const row = document.createElement('div');
240       this._rowContainer.appendChild(row);
241       this._rowElements.push(row);
242     }
243     // Remove excess elements
244     while (this._rowElements.length > rows) {
245       this._rowContainer.removeChild(this._rowElements.pop());
246     }
247   }
248
249   public onResize(cols: number, rows: number): void {
250     this._refreshRowElements(cols, rows);
251     this._updateDimensions();
252   }
253
254   public onCharSizeChanged(): void {
255     this._updateDimensions();
256   }
257
258   public onBlur(): void {
259     this._rowContainer.classList.remove(FOCUS_CLASS);
260   }
261
262   public onFocus(): void {
263     this._rowContainer.classList.add(FOCUS_CLASS);
264   }
265
266   public onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean): void {
267     // Remove all selections
268     while (this._selectionContainer.children.length) {
269       this._selectionContainer.removeChild(this._selectionContainer.children[0]);
270     }
271
272     // Selection does not exist
273     if (!start || !end) {
274       return;
275     }
276
277     // Translate from buffer position to viewport position
278     const viewportStartRow = start[1] - this._terminal.buffer.ydisp;
279     const viewportEndRow = end[1] - this._terminal.buffer.ydisp;
280     const viewportCappedStartRow = Math.max(viewportStartRow, 0);
281     const viewportCappedEndRow = Math.min(viewportEndRow, this._terminal.rows - 1);
282
283     // No need to draw the selection
284     if (viewportCappedStartRow >= this._terminal.rows || viewportCappedEndRow < 0) {
285       return;
286     }
287
288     // Create the selections
289     const documentFragment = document.createDocumentFragment();
290
291     if (columnSelectMode) {
292       documentFragment.appendChild(
293         this._createSelectionElement(viewportCappedStartRow, start[0], end[0], viewportCappedEndRow - viewportCappedStartRow + 1)
294       );
295     } else {
296       // Draw first row
297       const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
298       const endCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : this._terminal.cols;
299       documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow, startCol, endCol));
300       // Draw middle rows
301       const middleRowsCount = viewportCappedEndRow - viewportCappedStartRow - 1;
302       documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow + 1, 0, this._terminal.cols, middleRowsCount));
303       // Draw final row
304       if (viewportCappedStartRow !== viewportCappedEndRow) {
305         // Only draw viewportEndRow if it's not the same as viewporttartRow
306         const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._terminal.cols;
307         documentFragment.appendChild(this._createSelectionElement(viewportCappedEndRow, 0, endCol));
308       }
309     }
310     this._selectionContainer.appendChild(documentFragment);
311   }
312
313   /**
314    * Creates a selection element at the specified position.
315    * @param row The row of the selection.
316    * @param colStart The start column.
317    * @param colEnd The end columns.
318    */
319   private _createSelectionElement(row: number, colStart: number, colEnd: number, rowCount: number = 1): HTMLElement {
320     const element = document.createElement('div');
321     element.style.height = `${rowCount * this.dimensions.actualCellHeight}px`;
322     element.style.top = `${row * this.dimensions.actualCellHeight}px`;
323     element.style.left = `${colStart * this.dimensions.actualCellWidth}px`;
324     element.style.width = `${this.dimensions.actualCellWidth * (colEnd - colStart)}px`;
325     return element;
326   }
327
328   public onCursorMove(): void {
329     // No-op, the cursor is drawn when rows are drawn
330   }
331
332   public onOptionsChanged(): void {
333     // Force a refresh
334     this._updateDimensions();
335     this._injectCss();
336     this._terminal.refresh(0, this._terminal.rows - 1);
337   }
338
339   public clear(): void {
340     this._rowElements.forEach(e => e.innerHTML = '');
341   }
342
343   public renderRows(start: number, end: number): void {
344     const terminal = this._terminal;
345
346     const cursorAbsoluteY = terminal.buffer.ybase + terminal.buffer.y;
347     const cursorX = this._terminal.buffer.x;
348     const cursorBlink = this._terminal.options.cursorBlink;
349
350     for (let y = start; y <= end; y++) {
351       const rowElement = this._rowElements[y];
352       rowElement.innerHTML = '';
353
354       const row = y + terminal.buffer.ydisp;
355       const lineData = terminal.buffer.lines.get(row);
356       const cursorStyle = terminal.options.cursorStyle;
357       rowElement.appendChild(this._rowFactory.createRow(lineData, row === cursorAbsoluteY, cursorStyle, cursorX, cursorBlink, this.dimensions.actualCellWidth, terminal.cols));
358     }
359   }
360
361   private get _terminalSelector(): string {
362     return `.${TERMINAL_CLASS_PREFIX}${this._terminalClass}`;
363   }
364
365   public registerCharacterJoiner(handler: CharacterJoinerHandler): number { return -1; }
366   public deregisterCharacterJoiner(joinerId: number): boolean { return false; }
367
368   private _onLinkHover(e: ILinkifierEvent): void {
369     this._setCellUnderline(e.x1, e.x2, e.y1, e.y2, e.cols, true);
370   }
371
372   private _onLinkLeave(e: ILinkifierEvent): void {
373     this._setCellUnderline(e.x1, e.x2, e.y1, e.y2, e.cols, false);
374   }
375
376   private _setCellUnderline(x: number, x2: number, y: number, y2: number, cols: number, enabled: boolean): void {
377     while (x !== x2 || y !== y2) {
378       const row = this._rowElements[y];
379       if (!row) {
380         return;
381       }
382       const span = <HTMLElement>row.children[x];
383       if (span) {
384         span.style.textDecoration = enabled ? 'underline' : 'none';
385       }
386       if (++x >= cols) {
387         x = 0;
388         y++;
389       }
390     }
391   }
392 }