2 * Copyright (c) 2018 The xterm.js authors. All rights reserved.
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';
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';
22 let nextTerminalId = 1;
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.
29 export class DomRenderer extends Disposable implements IRenderer {
30 private _rowFactory: DomRendererRowFactory;
31 private _terminalClass: number = nextTerminalId++;
33 private _themeStyleElement: HTMLStyleElement;
34 private _dimensionsStyleElement: HTMLStyleElement;
35 private _rowContainer: HTMLElement;
36 private _rowElements: HTMLElement[] = [];
37 private _selectionContainer: HTMLElement;
39 public dimensions: IRenderDimensions;
42 private _terminal: ITerminal,
43 private _colors: IColorSet,
44 private _charSizeService: ICharSizeService,
45 private _optionsService: IOptionsService
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');
59 scaledCharWidth: null,
60 scaledCharHeight: null,
61 scaledCellWidth: null,
62 scaledCellHeight: null,
65 scaledCanvasWidth: null,
66 scaledCanvasHeight: null,
69 actualCellWidth: null,
70 actualCellHeight: null
72 this._updateDimensions();
75 this._rowFactory = new DomRendererRowFactory(document, this._optionsService);
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);
81 this._terminal.linkifier.onLinkHover(e => this._onLinkHover(e));
82 this._terminal.linkifier.onLinkLeave(e => this._onLinkLeave(e));
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);
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;
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';
116 if (!this._dimensionsStyleElement) {
117 this._dimensionsStyleElement = document.createElement('style');
118 this._terminal.screenElement.appendChild(this._dimensionsStyleElement);
122 `${this._terminalSelector} .${ROW_CONTAINER_CLASS} span {` +
123 ` display: inline-block;` +
125 ` vertical-align: top;` +
126 ` width: ${this.dimensions.actualCellWidth}px` +
129 this._dimensionsStyleElement.innerHTML = styles;
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`;
136 public setColors(colors: IColorSet): void {
137 this._colors = colors;
141 private _injectCss(): void {
142 if (!this._themeStyleElement) {
143 this._themeStyleElement = document.createElement('style');
144 this._terminal.screenElement.appendChild(this._themeStyleElement);
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;` +
157 `${this._terminalSelector} span:not(.${BOLD_CLASS}) {` +
158 ` font-weight: ${this._terminal.options.fontWeight};` +
160 `${this._terminalSelector} span.${BOLD_CLASS} {` +
161 ` font-weight: ${this._terminal.options.fontWeightBold};` +
163 `${this._terminalSelector} span.${ITALIC_CLASS} {` +
164 ` font-style: italic;` +
168 `@keyframes blink_box_shadow {` +
170 ` box-shadow: none;` +
174 `@keyframes blink_block {` +
176 ` background-color: ${this._colors.cursor.css};` +
177 ` color: ${this._colors.cursorAccent.css};` +
180 ` background-color: ${this._colors.cursorAccent.css};` +
181 ` color: ${this._colors.cursor.css};` +
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;` +
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;` +
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;` +
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};` +
200 `${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BAR_CLASS} {` +
201 ` box-shadow: 1px 0 0 ${this._colors.cursor.css} inset;` +
203 `${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_UNDERLINE_CLASS} {` +
204 ` box-shadow: 0 -1px 0 ${this._colors.cursor.css} inset;` +
208 `${this._terminalSelector} .${SELECTION_CLASS} {` +
209 ` position: absolute;` +
213 ` pointer-events: none;` +
215 `${this._terminalSelector} .${SELECTION_CLASS} div {` +
216 ` position: absolute;` +
217 ` background-color: ${this._colors.selection.css};` +
220 this._colors.ansi.forEach((c, i) => {
222 `${this._terminalSelector} .${FG_CLASS_PREFIX}${i} { color: ${c.css}; }` +
223 `${this._terminalSelector} .${BG_CLASS_PREFIX}${i} { background-color: ${c.css}; }`;
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}; }`;
229 this._themeStyleElement.innerHTML = styles;
232 public onDevicePixelRatioChange(): void {
233 this._updateDimensions();
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);
243 // Remove excess elements
244 while (this._rowElements.length > rows) {
245 this._rowContainer.removeChild(this._rowElements.pop());
249 public onResize(cols: number, rows: number): void {
250 this._refreshRowElements(cols, rows);
251 this._updateDimensions();
254 public onCharSizeChanged(): void {
255 this._updateDimensions();
258 public onBlur(): void {
259 this._rowContainer.classList.remove(FOCUS_CLASS);
262 public onFocus(): void {
263 this._rowContainer.classList.add(FOCUS_CLASS);
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]);
272 // Selection does not exist
273 if (!start || !end) {
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);
283 // No need to draw the selection
284 if (viewportCappedStartRow >= this._terminal.rows || viewportCappedEndRow < 0) {
288 // Create the selections
289 const documentFragment = document.createDocumentFragment();
291 if (columnSelectMode) {
292 documentFragment.appendChild(
293 this._createSelectionElement(viewportCappedStartRow, start[0], end[0], viewportCappedEndRow - viewportCappedStartRow + 1)
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));
301 const middleRowsCount = viewportCappedEndRow - viewportCappedStartRow - 1;
302 documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow + 1, 0, this._terminal.cols, middleRowsCount));
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));
310 this._selectionContainer.appendChild(documentFragment);
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.
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`;
328 public onCursorMove(): void {
329 // No-op, the cursor is drawn when rows are drawn
332 public onOptionsChanged(): void {
334 this._updateDimensions();
336 this._terminal.refresh(0, this._terminal.rows - 1);
339 public clear(): void {
340 this._rowElements.forEach(e => e.innerHTML = '');
343 public renderRows(start: number, end: number): void {
344 const terminal = this._terminal;
346 const cursorAbsoluteY = terminal.buffer.ybase + terminal.buffer.y;
347 const cursorX = this._terminal.buffer.x;
348 const cursorBlink = this._terminal.options.cursorBlink;
350 for (let y = start; y <= end; y++) {
351 const rowElement = this._rowElements[y];
352 rowElement.innerHTML = '';
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));
361 private get _terminalSelector(): string {
362 return `.${TERMINAL_CLASS_PREFIX}${this._terminalClass}`;
365 public registerCharacterJoiner(handler: CharacterJoinerHandler): number { return -1; }
366 public deregisterCharacterJoiner(joinerId: number): boolean { return false; }
368 private _onLinkHover(e: ILinkifierEvent): void {
369 this._setCellUnderline(e.x1, e.x2, e.y1, e.y2, e.cols, true);
372 private _onLinkLeave(e: ILinkifierEvent): void {
373 this._setCellUnderline(e.x1, e.x2, e.y1, e.y2, e.cols, false);
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];
382 const span = <HTMLElement>row.children[x];
384 span.style.textDecoration = enabled ? 'underline' : 'none';