X-Git-Url: https://git.proxmox.com/?a=blobdiff_plain;f=src%2FRenderer.ts;h=165594fd25f6d165c116d89e209f0f529db98362;hb=HEAD;hp=a839e7d91f81abedc271c598fd2e00ec3abd2f13;hpb=2ec756fd6bfadf2689d1c243960efaa01a7f93a4;p=mirror_xterm.js.git diff --git a/src/Renderer.ts b/src/Renderer.ts index a839e7d..165594f 100644 --- a/src/Renderer.ts +++ b/src/Renderer.ts @@ -3,6 +3,7 @@ */ import { ITerminal } from './Interfaces'; +import { DomElementObjectPool } from './utils/DomElementObjectPool'; /** * The maximum number of refresh frames to skip when the write buffer is non- @@ -30,12 +31,15 @@ export class Renderer { private _refreshFramesSkipped = 0; private _refreshAnimationFrame = null; + private _spanElementObjectPool = new DomElementObjectPool('span'); + constructor(private _terminal: ITerminal) { // Figure out whether boldness affects // the character width of monospace fonts. if (brokenBold === null) { brokenBold = checkBoldBroken((this._terminal).element); } + this._spanElementObjectPool = new DomElementObjectPool('span'); // TODO: Pull more DOM interactions into Renderer.constructor, element for // example should be owned by Renderer (and also exposed by Terminal due to @@ -117,9 +121,8 @@ export class Renderer { * @param {number} end The row to end at (between fromRow and terminal's height terminal - 1) */ private _refresh(start: number, end: number): void { - let x, y, i, line, out, ch, ch_width, width, data, attr, bg, fg, flags, row, parent, focused = document.activeElement; - // If this is a big refresh, remove the terminal rows from the DOM for faster calculations + let parent; if (end - start >= this._terminal.rows / 2) { parent = this._terminal.element.parentNode; if (parent) { @@ -127,8 +130,8 @@ export class Renderer { } } - width = this._terminal.cols; - y = start; + let width = this._terminal.cols; + let y = start; if (end >= this._terminal.rows) { this._terminal.log('`end` is too large. Most likely a bad CSR.'); @@ -136,148 +139,180 @@ export class Renderer { } for (; y <= end; y++) { - row = y + this._terminal.ydisp; + let row = y + this._terminal.buffer.ydisp; - line = this._terminal.lines.get(row); - if (!line || !this._terminal.children[y]) { - // Continue if the line is not available, this means a resize is currently in progress - continue; - } - out = ''; + let line = this._terminal.buffer.lines.get(row); - if (this._terminal.y === y - (this._terminal.ybase - this._terminal.ydisp) - && this._terminal.cursorState - && !this._terminal.cursorHidden) { - x = this._terminal.x; + let x; + if (this._terminal.buffer.y === y - (this._terminal.buffer.ybase - this._terminal.buffer.ydisp) && + this._terminal.cursorState && + !this._terminal.cursorHidden) { + x = this._terminal.buffer.x; } else { x = -1; } - attr = this._terminal.defAttr; - i = 0; + let attr = this._terminal.defAttr; + + const documentFragment = document.createDocumentFragment(); + let innerHTML = ''; + let currentElement; - for (; i < width; i++) { - if (!line[i]) { - // Continue if the character is not available, this means a resize is currently in progress + // Return the row's spans to the pool + while (this._terminal.children[y].children.length) { + const child = this._terminal.children[y].children[0]; + this._terminal.children[y].removeChild(child); + this._spanElementObjectPool.release(child); + } + + for (let i = 0; i < width; i++) { + // TODO: Could data be a more specific type? + let data: any = line[i][0]; + const ch = line[i][1]; + const ch_width: any = line[i][2]; + const isCursor: boolean = i === x; + if (!ch_width) { continue; } - data = line[i][0]; - ch = line[i][1]; - ch_width = line[i][2]; - if (!ch_width) - continue; - if (i === x) data = -1; - - if (data !== attr) { - if (attr !== this._terminal.defAttr) { - out += ''; + if (data !== attr || isCursor) { + if (attr !== this._terminal.defAttr && !isCursor) { + if (innerHTML) { + currentElement.innerHTML = innerHTML; + innerHTML = ''; + } + documentFragment.appendChild(currentElement); + currentElement = null; } - if (data !== this._terminal.defAttr) { - if (data === -1) { - out += ''; - } else { - let classNames = []; - - bg = data & 0x1ff; - fg = (data >> 9) & 0x1ff; - flags = data >> 18; - - if (flags & FLAGS.BOLD) { - if (!brokenBold) { - classNames.push('xterm-bold'); - } - // See: XTerm*boldColors - if (fg < 8) fg += 8; + if (data !== this._terminal.defAttr || isCursor) { + if (innerHTML && !currentElement) { + currentElement = this._spanElementObjectPool.acquire(); + } + if (currentElement) { + if (innerHTML) { + currentElement.innerHTML = innerHTML; + innerHTML = ''; } + documentFragment.appendChild(currentElement); + } + currentElement = this._spanElementObjectPool.acquire(); - if (flags & FLAGS.UNDERLINE) { - classNames.push('xterm-underline'); - } + let bg = data & 0x1ff; + let fg = (data >> 9) & 0x1ff; + let flags = data >> 18; - if (flags & FLAGS.BLINK) { - classNames.push('xterm-blink'); - } + if (isCursor) { + currentElement.classList.add('reverse-video'); + currentElement.classList.add('terminal-cursor'); + } - // If inverse flag is on, then swap the foreground and background variables. - if (flags & FLAGS.INVERSE) { - /* One-line variable swap in JavaScript: http://stackoverflow.com/a/16201730 */ - bg = [fg, fg = bg][0]; - // Should inverse just be before the - // above boldColors effect instead? - if ((flags & 1) && fg < 8) fg += 8; + if (flags & FLAGS.BOLD) { + if (!brokenBold) { + currentElement.classList.add('xterm-bold'); } - - if (flags & FLAGS.INVISIBLE) { - classNames.push('xterm-hidden'); + // See: XTerm*boldColors + if (fg < 8) { + fg += 8; } + } - /** - * Weird situation: Invert flag used black foreground and white background results - * in invalid background color, positioned at the 256 index of the 256 terminal - * color map. Pin the colors manually in such a case. - * - * Source: https://github.com/sourcelair/xterm.js/issues/57 - */ - if (flags & FLAGS.INVERSE) { - if (bg === 257) { - bg = 15; - } - if (fg === 256) { - fg = 0; - } - } + if (flags & FLAGS.UNDERLINE) { + currentElement.classList.add('xterm-underline'); + } - if (bg < 256) { - classNames.push('xterm-bg-color-' + bg); - } + if (flags & FLAGS.BLINK) { + currentElement.classList.add('xterm-blink'); + } - if (fg < 256) { - classNames.push('xterm-color-' + fg); + // If inverse flag is on, then swap the foreground and background variables. + if (flags & FLAGS.INVERSE) { + let temp = bg; + bg = fg; + fg = temp; + // Should inverse just be before the above boldColors effect instead? + if ((flags & 1) && fg < 8) { + fg += 8; } + } - out += '': - out += '>'; - break; - default: - if (ch <= ' ') { - out += ' '; - } else { - out += ch; + if (bg < 256) { + currentElement.classList.add(`xterm-bg-color-${bg}`); } - break; + + if (fg < 256) { + currentElement.classList.add(`xterm-color-${fg}`); + } + + } } + if (ch_width === 2) { - out += ''; + // Wrap wide characters so they're sized correctly. It's more difficult to release these + // from the object pool so just create new ones via innerHTML. + innerHTML += `${ch}`; + } else if (ch.charCodeAt(0) > 255) { + // Wrap any non-wide unicode character as some fonts size them badly + innerHTML += `${ch}`; + } else { + switch (ch) { + case '&': + innerHTML += '&'; + break; + case '<': + innerHTML += '<'; + break; + case '>': + innerHTML += '>'; + break; + default: + if (ch <= ' ') { + innerHTML += ' '; + } else { + innerHTML += ch; + } + break; + } } - attr = data; + // The cursor needs its own element, therefore we set attr to -1 + // which will cause the next character to be rendered in a new element + attr = isCursor ? -1 : data; + } - if (attr !== this._terminal.defAttr) { - out += ''; + if (innerHTML && !currentElement) { + currentElement = this._spanElementObjectPool.acquire(); + } + if (currentElement) { + if (innerHTML) { + currentElement.innerHTML = innerHTML; + innerHTML = ''; + } + documentFragment.appendChild(currentElement); + currentElement = null; } - this._terminal.children[y].innerHTML = out; + this._terminal.children[y].appendChild(documentFragment); } if (parent) { @@ -286,19 +321,80 @@ export class Renderer { this._terminal.emit('refresh', {element: this._terminal.element, start: start, end: end}); }; + + /** + * Refreshes the selection in the DOM. + * @param start The selection start. + * @param end The selection end. + */ + public refreshSelection(start: [number, number], end: [number, number]) { + // Remove all selections + while (this._terminal.selectionContainer.children.length) { + this._terminal.selectionContainer.removeChild(this._terminal.selectionContainer.children[0]); + } + + // Selection does not exist + if (!start || !end) { + return; + } + + // Translate from buffer position to viewport position + const viewportStartRow = start[1] - this._terminal.buffer.ydisp; + const viewportEndRow = end[1] - this._terminal.buffer.ydisp; + const viewportCappedStartRow = Math.max(viewportStartRow, 0); + const viewportCappedEndRow = Math.min(viewportEndRow, this._terminal.rows - 1); + + // No need to draw the selection + if (viewportCappedStartRow >= this._terminal.rows || viewportCappedEndRow < 0) { + return; + } + + // Create the selections + const documentFragment = document.createDocumentFragment(); + // Draw first row + const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0; + const endCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : this._terminal.cols; + documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow, startCol, endCol)); + // Draw middle rows + const middleRowsCount = viewportCappedEndRow - viewportCappedStartRow - 1; + documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow + 1, 0, this._terminal.cols, middleRowsCount)); + // Draw final row + if (viewportCappedStartRow !== viewportCappedEndRow) { + // Only draw viewportEndRow if it's not the same as viewporttartRow + const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._terminal.cols; + documentFragment.appendChild(this._createSelectionElement(viewportCappedEndRow, 0, endCol)); + } + this._terminal.selectionContainer.appendChild(documentFragment); + } + + /** + * Creates a selection element at the specified position. + * @param row The row of the selection. + * @param colStart The start column. + * @param colEnd The end columns. + */ + private _createSelectionElement(row: number, colStart: number, colEnd: number, rowCount: number = 1): HTMLElement { + const element = document.createElement('div'); + element.style.height = `${rowCount * this._terminal.charMeasure.height}px`; + element.style.top = `${row * this._terminal.charMeasure.height}px`; + element.style.left = `${colStart * this._terminal.charMeasure.width}px`; + element.style.width = `${this._terminal.charMeasure.width * (colEnd - colStart)}px`; + return element; + } } -// if bold is broken, we can't -// use it in the terminal. +// If bold is broken, we can't use it in the terminal. function checkBoldBroken(terminal) { const document = terminal.ownerDocument; const el = document.createElement('span'); el.innerHTML = 'hello world'; terminal.appendChild(el); const w1 = el.offsetWidth; + const h1 = el.offsetHeight; el.style.fontWeight = 'bold'; const w2 = el.offsetWidth; + const h2 = el.offsetHeight; terminal.removeChild(el); - return w1 !== w2; + return w1 !== w2 || h1 !== h2; }