Initial DOM renderer implementation

This commit is contained in:
Daniel Imms
2018-05-02 13:32:22 -07:00
parent e48b15e859
commit 16393b228d
5 changed files with 244 additions and 2 deletions
+8 -2
View File
@@ -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);
+1
View File
@@ -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';
}
}
+1
View File
@@ -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;
+224
View File
@@ -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 = <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 <style>?
charElement.style.width = `${terminal.charMeasure.width * width}px`;
const flags = attr >> 18;
let bg = attr & 0x1ff;
let fg = (attr >> 9) & 0x1ff;
// If inverse flag is on, the foreground should become the background.
if (flags & FLAGS.INVERSE) {
const temp = bg;
bg = fg;
fg = temp;
if (fg === 256) {
// TODO: INVERTED_DEFAULT_COLOR should not be in atlas
fg = INVERTED_DEFAULT_COLOR;
}
if (bg === 257) {
bg = INVERTED_DEFAULT_COLOR;
}
}
if (flags & FLAGS.BOLD) {
// Convert the FG color to the bold variant
if (fg < 8) {
fg += 8;
}
}
charElement.textContent = char;
// TODO: Make 257 a constant
if (fg !== 257) {
charElement.classList.add(`xterm-fg-${fg}`);
}
// TODO: Make 256 a constant
if (bg !== 256) {
charElement.classList.add(`xterm-bg-${bg}`);
}
rowElement.appendChild(charElement);
}
}
}
}
+10
View File
@@ -13,6 +13,11 @@ declare module 'xterm' {
*/
export type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900';
/**
* A string representing a renderer type.
*/
export type RendererType = 'dom' | 'canvas';
/**
* An object containing start up options for the terminal.
*/
@@ -96,6 +101,11 @@ declare module 'xterm' {
*/
macOptionIsMeta?: boolean;
/**
* The type of renderer to use.
*/
rendererType?: RendererType;
/**
* Whether to select the word under the cursor on right click, this is
* standard behavior in a lot of macOS applications.