From c2b5a243f2ddb689046f0a25ef3a210977ae0978 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 26 May 2018 08:15:40 -0700 Subject: [PATCH] Refactor and add some tests to dom renderer --- src/Buffer.ts | 3 +- src/InputHandler.ts | 22 ++--- src/Terminal.ts | 16 ++-- src/Types.ts | 2 - src/renderer/dom/DomRenderer.ts | 73 ++------------ .../dom/DomRendererRowFactory.test.ts | 95 +++++++++++++++++++ src/renderer/dom/DomRendererRowFactory.ts | 84 ++++++++++++++++ src/utils/TestUtils.test.ts | 2 - 8 files changed, 206 insertions(+), 91 deletions(-) create mode 100644 src/renderer/dom/DomRendererRowFactory.test.ts create mode 100644 src/renderer/dom/DomRendererRowFactory.ts diff --git a/src/Buffer.ts b/src/Buffer.ts index 1ea303b2..d39a5549 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -8,6 +8,7 @@ import { LineData, CharData, ITerminal, IBuffer } from './Types'; import { EventEmitter } from './EventEmitter'; import { IDisposable, IMarker } from 'xterm'; +export const DEFAULT_ATTR = (0 << 18) | (257 << 9) | (256 << 0); export const CHAR_DATA_ATTR_INDEX = 0; export const CHAR_DATA_CHAR_INDEX = 1; export const CHAR_DATA_WIDTH_INDEX = 2; @@ -116,7 +117,7 @@ export class Buffer implements IBuffer { if (this.lines.length > 0) { // Deal with columns increasing (we don't do anything when columns reduce) if (this._terminal.cols < newCols) { - const ch: CharData = [this._terminal.defAttr, ' ', 1, 32]; // does xterm use the default attr? + const ch: CharData = [DEFAULT_ATTR, ' ', 1, 32]; // does xterm use the default attr? for (let i = 0; i < this.lines.length; i++) { while (this.lines.get(i).length < newCols) { this.lines.get(i).push(ch); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 7b12b6de..e2e20c73 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -7,7 +7,7 @@ import { CharData, IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, ICharset } from './Types'; import { C0, C1 } from './EscapeSequences'; import { CHARSETS, DEFAULT_CHARSET } from './Charsets'; -import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX } from './Buffer'; +import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, DEFAULT_ATTR } from './Buffer'; import { FLAGS } from './renderer/Types'; import { wcwidth } from './CharWidth'; import { EscapeSequenceParser } from './EscapeSequenceParser'; @@ -970,7 +970,7 @@ export class InputHandler implements IInputHandler { const buffer = this._terminal.buffer; const line = buffer.lines.get(buffer.ybase + buffer.y); - const ch = line[buffer.x - 1] || [this._terminal.defAttr, ' ', 1, 32]; + const ch = line[buffer.x - 1] || [DEFAULT_ATTR, ' ', 1, 32]; while (param--) { line[buffer.x++] = ch; @@ -1553,7 +1553,7 @@ export class InputHandler implements IInputHandler { public charAttributes(params: number[]): void { // Optimize a single SGR0. if (params.length === 1 && params[0] === 0) { - this._terminal.curAttr = this._terminal.defAttr; + this._terminal.curAttr = DEFAULT_ATTR; return; } @@ -1581,9 +1581,9 @@ export class InputHandler implements IInputHandler { bg = p - 100; } else if (p === 0) { // default - flags = this._terminal.defAttr >> 18; - fg = (this._terminal.defAttr >> 9) & 0x1ff; - bg = this._terminal.defAttr & 0x1ff; + flags = DEFAULT_ATTR >> 18; + fg = (DEFAULT_ATTR >> 9) & 0x1ff; + bg = DEFAULT_ATTR & 0x1ff; // flags = 0; // fg = 0x1ff; // bg = 0x1ff; @@ -1627,10 +1627,10 @@ export class InputHandler implements IInputHandler { flags &= ~FLAGS.INVISIBLE; } else if (p === 39) { // reset fg - fg = (this._terminal.defAttr >> 9) & 0x1ff; + fg = (DEFAULT_ATTR >> 9) & 0x1ff; } else if (p === 49) { // reset bg - bg = this._terminal.defAttr & 0x1ff; + bg = DEFAULT_ATTR & 0x1ff; } else if (p === 38) { // fg color 256 if (params[i + 1] === 2) { @@ -1663,8 +1663,8 @@ export class InputHandler implements IInputHandler { } } else if (p === 100) { // reset fg/bg - fg = (this._terminal.defAttr >> 9) & 0x1ff; - bg = this._terminal.defAttr & 0x1ff; + fg = (DEFAULT_ATTR >> 9) & 0x1ff; + bg = DEFAULT_ATTR & 0x1ff; } else { this._terminal.error('Unknown SGR attribute: %d.', p); } @@ -1759,7 +1759,7 @@ export class InputHandler implements IInputHandler { this._terminal.applicationCursor = false; this._terminal.buffer.scrollTop = 0; this._terminal.buffer.scrollBottom = this._terminal.rows - 1; - this._terminal.curAttr = this._terminal.defAttr; + this._terminal.curAttr = DEFAULT_ATTR; this._terminal.buffer.x = this._terminal.buffer.y = 0; // ? this._terminal.charset = null; this._terminal.glevel = 0; // ?? diff --git a/src/Terminal.ts b/src/Terminal.ts index 0e4b4a41..18ff2f74 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -25,7 +25,7 @@ import { ICharset, IInputHandlingTerminal, IViewport, ICompositionHelper, ITermi import { IMouseZoneManager } from './input/Types'; import { IRenderer } from './renderer/Types'; import { BufferSet } from './BufferSet'; -import { Buffer, MAX_BUFFER_SIZE } from './Buffer'; +import { Buffer, MAX_BUFFER_SIZE, DEFAULT_ATTR } from './Buffer'; import { CompositionHelper } from './CompositionHelper'; import { EventEmitter } from './EventEmitter'; import { Viewport } from './Viewport'; @@ -192,7 +192,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II private _refreshEnd: number; public savedCols: number; - public defAttr: number; public curAttr: number; public params: (string | number)[]; @@ -315,8 +314,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // TODO: Can this be just []? this.charsets = [null]; - this.defAttr = (0 << 18) | (257 << 9) | (256 << 0); - this.curAttr = (0 << 18) | (257 << 9) | (256 << 0); + this.curAttr = DEFAULT_ATTR; this.params = []; this.currentParam = 0; @@ -360,8 +358,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II * back_color_erase feature for xterm. */ public eraseAttr(): number { - // if (this.is('screen')) return this.defAttr; - return (this.defAttr & ~0x1ff) | (this.curAttr & 0x1ff); + // if (this.is('screen')) return DEFAULT_ATTR; + return (DEFAULT_ATTR & ~0x1ff) | (this.curAttr & 0x1ff); } /** @@ -2063,7 +2061,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II * set, the terminal's current column count would be used. */ public blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData { - const attr = cur ? this.eraseAttr() : this.defAttr; + const attr = cur ? this.eraseAttr() : DEFAULT_ATTR; const ch: CharData = [attr, ' ', 1, 32 /* ' '.charCodeAt(0) */]; // width defaults to 1 halfwidth character const line: LineData = []; @@ -2083,14 +2081,14 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } /** - * If cur return the back color xterm feature attribute. Else return defAttr. + * If cur return the back color xterm feature attribute. Else return default attribute. * @param cur */ public ch(cur?: boolean): CharData { if (cur) { return [this.eraseAttr(), ' ', 1, 32 /* ' '.charCodeAt(0) */]; } - return [this.defAttr, ' ', 1, 32 /* ' '.charCodeAt(0) */]; + return [DEFAULT_ATTR, ' ', 1, 32 /* ' '.charCodeAt(0) */]; } /** diff --git a/src/Types.ts b/src/Types.ts index 5075daa9..7642ea16 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -43,7 +43,6 @@ export interface IInputHandlingTerminal extends IEventEmitter { insertMode: boolean; wraparoundMode: boolean; bracketedPasteMode: boolean; - defAttr: number; curAttr: number; savedCols: number; x10Mouse: boolean; @@ -213,7 +212,6 @@ export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAcce writeBuffer: string[]; cursorHidden: boolean; cursorState: number; - defAttr: number; options: ITerminalOptions; buffer: IBuffer; buffers: IBufferSet; diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts index 06ff7728..c5263282 100644 --- a/src/renderer/dom/DomRenderer.ts +++ b/src/renderer/dom/DomRenderer.ts @@ -3,20 +3,16 @@ * @license MIT */ -import { IRenderer, IRenderDimensions, IColorSet, FLAGS } from '../Types'; +import { IRenderer, IRenderDimensions, IColorSet } 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 } from '../../Buffer'; import { RenderDebouncer } from '../../utils/RenderDebouncer'; +import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, DomRendererRowFactory } from './DomRendererRowFactory'; const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-'; const ROW_CONTAINER_CLASS = 'xterm-rows'; -const BOLD_CLASS = 'xterm-bold'; -const ITALIC_CLASS = 'xterm-italic'; -const CURSOR_CLASS = 'xterm-cursor'; const FG_CLASS_PREFIX = 'xterm-fg-'; const BG_CLASS_PREFIX = 'xterm-bg-'; const FOCUS_CLASS = 'xterm-focus'; @@ -34,6 +30,7 @@ let nextTerminalId = 1; */ export class DomRenderer extends EventEmitter implements IRenderer { private _renderDebouncer: RenderDebouncer; + private _rowFactory: DomRendererRowFactory; private _terminalClass: number = nextTerminalId++; private _themeStyleElement: HTMLStyleElement; @@ -77,6 +74,7 @@ export class DomRenderer extends EventEmitter implements IRenderer { this._updateDimensions(); this._renderDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this)); + this._rowFactory = new DomRendererRowFactory(document); this._terminal.element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass); this._terminal.screenElement.appendChild(this._rowContainer); @@ -297,72 +295,15 @@ export class DomRenderer extends EventEmitter implements IRenderer { const terminal = this._terminal; const cursorAbsoluteY = terminal.buffer.ybase + terminal.buffer.y; + const cursorX = this._terminal.buffer.x; 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 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'); - if (width > 1) { - charElement.style.width = `${terminal.charMeasure.width * width}px`; - } - - const flags = attr >> 18; - let bg = attr & 0x1ff; - let fg = (attr >> 9) & 0x1ff; - - if (row === cursorAbsoluteY && x === this._terminal.buffer.x) { - charElement.classList.add(CURSOR_CLASS); - } - - // 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.classList.add(BOLD_CLASS); - } - - if (flags & FLAGS.ITALIC) { - charElement.classList.add(ITALIC_CLASS); - } - - charElement.textContent = char; - if (fg !== 257) { - charElement.classList.add(`xterm-fg-${fg}`); - } - if (bg !== 256) { - charElement.classList.add(`xterm-bg-${bg}`); - } - rowElement.appendChild(charElement); - } + const lineData = terminal.buffer.lines.get(row); + rowElement.appendChild(this._rowFactory.createRow(lineData, row === cursorAbsoluteY, cursorX, terminal.charMeasure.width)); } this._terminal.emit('refresh', {start, end}); diff --git a/src/renderer/dom/DomRendererRowFactory.test.ts b/src/renderer/dom/DomRendererRowFactory.test.ts new file mode 100644 index 00000000..93a60d95 --- /dev/null +++ b/src/renderer/dom/DomRendererRowFactory.test.ts @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import jsdom = require('jsdom'); +import { assert } from 'chai'; +import { DomRendererRowFactory } from './DomRendererRowFactory'; +import { LineData } from '../../Types'; +import { DEFAULT_ATTR } from '../../Buffer'; +import { FLAGS } from '../Types'; + +describe('DomRendererRowFactory', () => { + let dom: jsdom.JSDOM; + let rowFactory: DomRendererRowFactory; + let lineData: LineData; + + beforeEach(() => { + dom = new jsdom.JSDOM(''); + rowFactory = new DomRendererRowFactory(dom.window.document); + lineData = createEmptyLineData(4); + }); + + describe('createRow', () => { + it('should create an element for every character in the row', () => { + const fragment = rowFactory.createRow(lineData, false, 0, 5); + assert.equal(getFragmentHtml(fragment), + ' ' + + ' ' + + ' ' + + ' ' + ); + }); + + it('should set correct attributes for double width characters', () => { + lineData[1] = [DEFAULT_ATTR, '語', 2, '語'.charCodeAt(0)]; + // There should be no element for the following "empty" cell + lineData[2] = [DEFAULT_ATTR, '', 0, undefined]; + const fragment = rowFactory.createRow(lineData, false, 0, 5); + assert.equal(getFragmentHtml(fragment), + ' ' + + '' + + ' ' + ); + }); + + it('should add class for cursor', () => { + const fragment = rowFactory.createRow(lineData, true, 1, 5); + assert.equal(getFragmentHtml(fragment), + ' ' + + ' ' + + ' ' + + ' ' + ); + }); + + describe('attributes', () => { + it('should add class for bold', () => { + lineData[1] = [DEFAULT_ATTR | (FLAGS.BOLD << 18), 'a', 1, 'a'.charCodeAt(0)]; + const fragment = rowFactory.createRow(lineData, false, 0, 5); + assert.equal(getFragmentHtml(fragment), + ' ' + + 'a' + + ' ' + + ' ' + ); + }); + + it('should add class for italic', () => { + lineData[1] = [DEFAULT_ATTR | (FLAGS.ITALIC << 18), 'a', 1, 'a'.charCodeAt(0)]; + const fragment = rowFactory.createRow(lineData, false, 0, 5); + assert.equal(getFragmentHtml(fragment), + ' ' + + 'a' + + ' ' + + ' ' + ); + }); + }); + }); + + function getFragmentHtml(fragment: DocumentFragment): string { + const element = dom.window.document.createElement('div'); + element.appendChild(fragment); + return element.innerHTML; + } + + function createEmptyLineData(cols: number): LineData { + const lineData: LineData = []; + for (let i = 0; i < cols; i++) { + lineData.push([DEFAULT_ATTR, ' ', 1, 32 /* ' '.charCodeAt(0) */]); + } + return lineData; + } +}); diff --git a/src/renderer/dom/DomRendererRowFactory.ts b/src/renderer/dom/DomRendererRowFactory.ts new file mode 100644 index 00000000..bfecace5 --- /dev/null +++ b/src/renderer/dom/DomRendererRowFactory.ts @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { LineData } from '../../Types'; +import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../../Buffer'; +import { FLAGS } from '../Types'; +import { INVERTED_DEFAULT_COLOR } from '../atlas/Types'; + +export const BOLD_CLASS = 'xterm-bold'; +export const ITALIC_CLASS = 'xterm-italic'; +export const CURSOR_CLASS = 'xterm-cursor'; + +export class DomRendererRowFactory { + constructor( + private _document: Document + ) { + } + + public createRow(lineData: LineData, isCursorRow: boolean, cursorX: number, cellWidth: number): DocumentFragment { + const fragment = this._document.createDocumentFragment(); + for (let x = 0; x < lineData.length; x++) { + const charData = lineData[x]; + 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 = this._document.createElement('span'); + if (width > 1) { + charElement.style.width = `${cellWidth * width}px`; + } + + const flags = attr >> 18; + let bg = attr & 0x1ff; + let fg = (attr >> 9) & 0x1ff; + + if (isCursorRow && x === cursorX) { + charElement.classList.add(CURSOR_CLASS); + } + + // 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.classList.add(BOLD_CLASS); + } + + if (flags & FLAGS.ITALIC) { + charElement.classList.add(ITALIC_CLASS); + } + + charElement.textContent = char; + if (fg !== 257) { + charElement.classList.add(`xterm-fg-${fg}`); + } + if (bg !== 256) { + charElement.classList.add(`xterm-bg-${bg}`); + } + fragment.appendChild(charElement); + } + return fragment; + } +} diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index e3ae89d3..ba154ebf 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -107,7 +107,6 @@ export class MockTerminal implements ITerminal { children: HTMLElement[]; cursorHidden: boolean; cursorState: number; - defAttr: number; scrollback: number; buffers: IBufferSet; buffer: IBuffer; @@ -182,7 +181,6 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { insertMode: boolean; wraparoundMode: boolean; bracketedPasteMode: boolean; - defAttr: number; curAttr: number; savedCols: number; x10Mouse: boolean;