From 16393b228dc66f655b16f6b34469396c2865659f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 2 May 2018 13:32:22 -0700 Subject: [PATCH 01/23] Initial DOM renderer implementation --- src/Terminal.ts | 10 +- src/Viewport.ts | 1 + src/renderer/Renderer.ts | 1 + src/renderer/dom/DomRenderer.ts | 224 ++++++++++++++++++++++++++++++++ typings/xterm.d.ts | 10 ++ 5 files changed, 244 insertions(+), 2 deletions(-) create mode 100644 src/renderer/dom/DomRenderer.ts diff --git a/src/Terminal.ts b/src/Terminal.ts index ba1fff3df1..ce0b32a819 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -49,6 +49,7 @@ import { AccessibilityManager } from './AccessibilityManager'; import { ScreenDprMonitor } from './utils/ScreenDprMonitor'; import { ITheme, ILocalizableStrings, IMarker, IDisposable } from 'xterm'; import { removeTerminalFromCache } from './renderer/atlas/CharAtlas'; +import { DomRenderer } from './renderer/dom/DomRenderer'; // reg + shift key mappings for digits and special chars const KEYCODE_KEY_MAPPINGS = { @@ -121,7 +122,8 @@ const DEFAULT_OPTIONS: ITerminalOptions = { allowTransparency: false, tabStopWidth: 8, theme: null, - rightClickSelectsWord: Browser.isMac + rightClickSelectsWord: Browser.isMac, + rendererType: 'canvas' }; export class Terminal extends EventEmitter implements ITerminal, IDisposable, IInputHandlingTerminal { @@ -703,7 +705,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // Performance: Add viewport and helper elements from the fragment this.element.appendChild(fragment); - this.renderer = new Renderer(this, this.options.theme); + if (this.options.rendererType === 'canvas') { + this.renderer = new Renderer(this, this.options.theme); + } else { + this.renderer = new DomRenderer(this, this.options.theme); + } this.options.theme = null; this.viewport = new Viewport(this, this._viewportElement, this._viewportScrollArea, this.charMeasure); this.viewport.onThemeChanged(this.renderer.colorManager.colors); diff --git a/src/Viewport.ts b/src/Viewport.ts index c951d6b53e..433e267e36 100644 --- a/src/Viewport.ts +++ b/src/Viewport.ts @@ -64,6 +64,7 @@ export class Viewport implements IViewport { const newBufferHeight = Math.round(this._currentRowHeight * this._lastRecordedBufferLength) + (this._lastRecordedViewportHeight - this._terminal.renderer.dimensions.canvasHeight); if (this._lastRecordedBufferHeight !== newBufferHeight) { this._lastRecordedBufferHeight = newBufferHeight; + console.log('set scroll area height to ', this._currentRowHeight, this._lastRecordedBufferLength, this._lastRecordedBufferHeight); this._scrollArea.style.height = this._lastRecordedBufferHeight + 'px'; } } diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 9404a461a8..9db1bdc505 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -15,6 +15,7 @@ import { RenderDebouncer } from '../utils/RenderDebouncer'; import { ScreenDprMonitor } from '../utils/ScreenDprMonitor'; import { ITheme } from 'xterm'; +// TODO: Rename to CanvasRenderer and move this, render layers and atlas into ./src/renderer/canvas export class Renderer extends EventEmitter implements IRenderer { private _renderDebouncer: RenderDebouncer; diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts new file mode 100644 index 0000000000..3fd3a00bf0 --- /dev/null +++ b/src/renderer/dom/DomRenderer.ts @@ -0,0 +1,224 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IRenderer, IRenderDimensions, IColorSet, FLAGS } from '../Types'; +import { ITerminal } from '../../Types'; +import { ITheme } from 'xterm'; +import { EventEmitter } from '../../EventEmitter'; +import { ColorManager } from '../ColorManager'; +import { INVERTED_DEFAULT_COLOR } from '../atlas/Types'; +import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX } from '../../Buffer'; + +const ROW_CONTAINER_CLASS = 'xterm-rows'; + +export class DomRenderer extends EventEmitter implements IRenderer { + public dimensions: IRenderDimensions; + public colorManager: ColorManager; + + private _styleElement: HTMLStyleElement; + private _rowContainer: HTMLElement; + private _rowElements: HTMLElement[] = []; + + // TODO: Theme/ColorManager might be better owned by Terminal not IRenderer to reduce duplication? + constructor(private _terminal: ITerminal, theme: ITheme | undefined) { + super(); + const allowTransparency = this._terminal.options.allowTransparency; + this.colorManager = new ColorManager(document, allowTransparency); + this.setTheme(theme); + + this._rowContainer = document.createElement('div'); + this._rowContainer.classList.add(ROW_CONTAINER_CLASS); + this._refreshRowElements(this._terminal.rows, this._terminal.cols); + + // TODO: Should IRendererDimensions lose canvas-related concepts? + this.dimensions = { + scaledCharWidth: null, + scaledCharHeight: null, + scaledCellWidth: null, + scaledCellHeight: null, + scaledCharLeft: null, + scaledCharTop: null, + scaledCanvasWidth: null, + scaledCanvasHeight: null, + canvasWidth: null, + canvasHeight: null, + actualCellWidth: null, + actualCellHeight: null + }; + this._updateDimensions(); + + this._terminal.screenElement.appendChild(this._rowContainer); + } + + private _updateDimensions(): void { + this.dimensions.scaledCharWidth = this._terminal.charMeasure.width * window.devicePixelRatio; + this.dimensions.scaledCharHeight = this._terminal.charMeasure.height * window.devicePixelRatio; + this.dimensions.scaledCellWidth = this._terminal.charMeasure.width * window.devicePixelRatio; + this.dimensions.scaledCellHeight = this._terminal.charMeasure.height * window.devicePixelRatio; + // TODO: Support line height and letter spacing + this.dimensions.scaledCharLeft = 0; + this.dimensions.scaledCharTop = 0; + this.dimensions.scaledCanvasWidth = this.dimensions.scaledCellWidth * this._terminal.cols; + this.dimensions.scaledCanvasHeight = this.dimensions.scaledCellHeight * this._terminal.rows; + this.dimensions.canvasWidth = this._terminal.charMeasure.width * this._terminal.cols; + this.dimensions.canvasHeight = this._terminal.charMeasure.height * this._terminal.rows; + this.dimensions.actualCellWidth = this._terminal.charMeasure.width; + this.dimensions.actualCellHeight = this._terminal.charMeasure.height; + + this._rowElements.forEach(element => { + element.style.width = `${this.dimensions.canvasWidth}px`; + element.style.height = `${this._terminal.charMeasure.height}px`; + }); + + console.log('_updateDimensions', this.dimensions); + } + + public setTheme(theme: ITheme | undefined): IColorSet { + console.log('setTheme'); + if (theme) { + this.colorManager.setTheme(theme); + } + + // TODO: CSS selectors would need to use some ID otherwise it will affect other terminals + this._styleElement = document.createElement('style'); + let styles = + `.xterm .${ROW_CONTAINER_CLASS} {` + + ` color: ${this.colorManager.colors.foreground.css};` + + ` background-color: ${this.colorManager.colors.background.css};` + + `}` + + `.xterm .${ROW_CONTAINER_CLASS} span {` + + ` display: inline-block;` + + `}`; + this.colorManager.colors.ansi.forEach((c, i) => { + styles += + `.xterm .xterm-fg-${i} { color: ${c.css}; }` + + `.xterm .xterm-bg-${i} { background-color: ${c.css}; }`; + }); + this._styleElement.innerHTML = styles; + this._terminal.screenElement.appendChild(this._styleElement); + return this.colorManager.colors; + } + + public onWindowResize(devicePixelRatio: number): void { + console.log('onWindowResize', arguments); + } + + private _refreshRowElements(cols: number, rows: number): void { + console.log('resize', cols, rows); + // Add missing elements + for (let i = this._rowElements.length; i <= rows; i++) { + const row = document.createElement('div'); + this._rowContainer.appendChild(row); + this._rowElements.push(row); + } + // Remove excess elements + while (this._rowElements.length > rows) { + this._rowContainer.removeChild(this._rowElements.pop()); + } + // console.log('refresh row elements', rows, this._rowElements.length); + } + + public onResize(cols: number, rows: number): void { + console.log('onResize', arguments); + this._refreshRowElements(cols, rows); + this._updateDimensions(); + } + + // TODO: onCharSizeChanged is no longer called :'( + public onCharSizeChanged(): void { + this._updateDimensions(); + } + + public onBlur(): void { + console.log('onBlur', arguments); + } + + public onFocus(): void { + console.log('onFocus', arguments); + } + + public onSelectionChanged(start: [number, number], end: [number, number]): void { + console.log('onSelectionChanged', arguments); + } + + public onCursorMove(): void { + console.log('onCursorMove', arguments); + } + + public onOptionsChanged(): void { + console.log('onOptionsChanged', arguments); + } + + public clear(): void { + console.log('clear', arguments); + this._rowElements.forEach(e => e.innerHTML = ''); + } + + public refreshRows(start: number, end: number): void { + console.log('refreshRows', arguments); + const terminal = this._terminal; + + for (let y = start; y <= end; y++) { + const rowElement = this._rowElements[y]; + rowElement.innerHTML = ''; + + const row = y + terminal.buffer.ydisp; + const line = terminal.buffer.lines.get(row); + for (let x = 0; x < terminal.cols; x++) { + const charData = line[x]; + const code: number = charData[CHAR_DATA_CODE_INDEX]; + const char: string = charData[CHAR_DATA_CHAR_INDEX]; + const attr: number = charData[CHAR_DATA_ATTR_INDEX]; + let width: number = charData[CHAR_DATA_WIDTH_INDEX]; + + // The character to the left is a wide character, drawing is owned by + // the char at x-1 + if (width === 0) { + continue; + } + + const charElement = document.createElement('span'); + // TODO: Move standard width to