From c6d4c73c8a8c23ce102918fc0342cf33d2986711 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 30 Aug 2017 14:50:07 -0700 Subject: [PATCH 001/108] Initial prototype --- src/Interfaces.ts | 2 ++ src/Renderer.ts | 76 ++++++++++++++++++++++++++++++++++++++++++++++- src/Terminal.ts | 13 ++++++++ 3 files changed, 90 insertions(+), 1 deletion(-) diff --git a/src/Interfaces.ts b/src/Interfaces.ts index fb66f4d7cf..b643de45c7 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -21,6 +21,8 @@ export interface ITerminal extends IEventEmitter { element: HTMLElement; rowContainer: HTMLElement; selectionContainer: HTMLElement; + canvasElement: HTMLCanvasElement; + canvasContext: CanvasRenderingContext2D; selectionManager: ISelectionManager; charMeasure: ICharMeasure; textarea: HTMLTextAreaElement; diff --git a/src/Renderer.ts b/src/Renderer.ts index b51a9ff93c..1586d8fef5 100644 --- a/src/Renderer.ts +++ b/src/Renderer.ts @@ -97,7 +97,8 @@ export class Renderer { } this._refreshRowsQueue = []; this._refreshAnimationFrame = null; - this._refresh(start, end); + // this._refresh(start, end); + this._canvasRender(start, end); } /** @@ -323,6 +324,79 @@ export class Renderer { this._terminal.emit('refresh', {start, end}); }; + private _imageDataCache = {}; + private _colors = [ + // dark: + '#2e3436', + '#cc0000', + '#4e9a06', + '#c4a000', + '#3465a4', + '#75507b', + '#06989a', + '#d3d7cf', + // bright: + '#555753', + '#ef2929', + '#8ae234', + '#fce94f', + '#729fcf', + '#ad7fa8', + '#34e2e2', + '#eeeeec' + ]; + + private _canvasRender(start: number, end: number): void { + const charWidth = Math.ceil(this._terminal.charMeasure.width); + const charHeight = Math.ceil(this._terminal.charMeasure.height); + const ctx = this._terminal.canvasContext; + ctx.font = '16px Hack'; + ctx.fillStyle = '#000000'; + // console.log('fill', start, end); + // console.log('fill', start * charHeight, (end - start + 1) * charHeight); + ctx.fillRect(0, start * charHeight, charWidth * this._terminal.cols, (end - start + 1) * charHeight); + ctx.fillStyle = 'rgb(255, 255, 255)'; + ctx.textBaseline = 'top'; + + for (let y = start; y <= end; y++) { + let row = y + this._terminal.buffer.ydisp; + let line = this._terminal.buffer.lines.get(row); + for (let x = 0; x < this._terminal.cols; x++) { + let data: any = line[x][0]; + const ch = line[x][CHAR_DATA_CHAR_INDEX]; + + // if (ch === ' ') { + // continue; + // } + + let bg = data & 0x1ff; + let fg = (data >> 9) & 0x1ff; + let flags = data >> 18; + + // if (bg < 16) { + // } + + if (fg < 16) { + ctx.fillStyle = this._colors[fg]; + } + + // let imageData; + // let key = ch + fg; + // if (key in this._imageDataCache) { + // imageData = this._imageDataCache[key]; + // } else { + ctx.fillText(ch, x * charWidth, y * charHeight); + // imageData = ctx.getImageData(x * charWidth, y * charHeight, charWidth, charHeight); + // this._imageDataCache[key] = imageData; + // } + + // ctx.fillText(ch, x * charWidth, y * charHeight); + // ctx.putImageData(imageData, x * charWidth, y * charHeight); + } + } + this._imageDataCache = {}; + } + /** * Refreshes the selection in the DOM. * @param start The selection start. diff --git a/src/Terminal.ts b/src/Terminal.ts index 827273ff52..0bb00ecf6f 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -175,6 +175,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT private body: HTMLBodyElement; private viewportScrollArea: HTMLElement; private viewportElement: HTMLElement; + public canvasElement: HTMLCanvasElement; + public canvasContext: CanvasRenderingContext2D; public selectionContainer: HTMLElement; private helperContainer: HTMLElement; private compositionView: HTMLElement; @@ -703,6 +705,12 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.selectionContainer.classList.add('xterm-selection'); this.element.appendChild(this.selectionContainer); + + this.canvasElement = document.createElement('canvas'); + this.canvasContext = this.canvasElement.getContext('2d'); + this.element.appendChild(this.canvasElement); + + // Create the container that will hold the lines of the terminal and then // produce the lines the lines. this.rowContainer = document.createElement('div'); @@ -746,6 +754,11 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT }); this.charMeasure.measure(); + this.charMeasure.on('charsizechanged', () => { + this.canvasElement.setAttribute('width', `${Math.ceil(this.charMeasure.width) * this.cols}px`); + this.canvasElement.setAttribute('height', `${Math.ceil(this.charMeasure.height) * this.rows}px`); + }); + this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure); this.renderer = new Renderer(this); this.selectionManager = new SelectionManager(this, this.buffer, this.rowContainer, this.charMeasure); From 640cd18515e07c93cac7fe08ad229df2073cc415 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 30 Aug 2017 17:07:30 -0700 Subject: [PATCH 002/108] Support bold, underline, fix colors --- src/Renderer.ts | 48 ++++--- src/RendererCanvas.ts | 294 ++++++++++++++++++++++++++++++++++++++++++ src/Terminal.ts | 13 +- 3 files changed, 337 insertions(+), 18 deletions(-) create mode 100644 src/RendererCanvas.ts diff --git a/src/Renderer.ts b/src/Renderer.ts index 1586d8fef5..8c15509654 100644 --- a/src/Renderer.ts +++ b/src/Renderer.ts @@ -97,7 +97,7 @@ export class Renderer { } this._refreshRowsQueue = []; this._refreshAnimationFrame = null; - // this._refresh(start, end); + this._refresh(start, end); this._canvasRender(start, end); } @@ -347,18 +347,26 @@ export class Renderer { ]; private _canvasRender(start: number, end: number): void { - const charWidth = Math.ceil(this._terminal.charMeasure.width); - const charHeight = Math.ceil(this._terminal.charMeasure.height); + + const charWidth = Math.ceil(this._terminal.charMeasure.width) * window.devicePixelRatio; + const charHeight = Math.ceil(this._terminal.charMeasure.height) * window.devicePixelRatio; const ctx = this._terminal.canvasContext; - ctx.font = '16px Hack'; + ctx.fillStyle = '#000000'; // console.log('fill', start, end); // console.log('fill', start * charHeight, (end - start + 1) * charHeight); - ctx.fillRect(0, start * charHeight, charWidth * this._terminal.cols, (end - start + 1) * charHeight); + // ctx.fillRect(0, start * charHeight, charWidth * this._terminal.cols, (end - start + 1) * charHeight); ctx.fillStyle = 'rgb(255, 255, 255)'; ctx.textBaseline = 'top'; + // Indicates whether to reset the font next cell + let resetFont = true; + for (let y = start; y <= end; y++) { + if (resetFont) { + ctx.font = `${16 * window.devicePixelRatio}px courier`; + resetFont = false; + } let row = y + this._terminal.buffer.ydisp; let line = this._terminal.buffer.lines.get(row); for (let x = 0; x < this._terminal.cols; x++) { @@ -376,25 +384,35 @@ export class Renderer { // if (bg < 16) { // } + if (flags & FLAGS.BOLD) { + ctx.font = `bold ${ctx.font}`; + resetFont = true; + // Convert the FG color to the bold variant + if (fg < 8) { + fg += 8; + } + } + if (fg < 16) { ctx.fillStyle = this._colors[fg]; } - // let imageData; - // let key = ch + fg; - // if (key in this._imageDataCache) { - // imageData = this._imageDataCache[key]; - // } else { + // Simulate cache + let imageData; + let key = ch + fg; + if (key in this._imageDataCache) { + imageData = this._imageDataCache[key]; + } else { ctx.fillText(ch, x * charWidth, y * charHeight); - // imageData = ctx.getImageData(x * charWidth, y * charHeight, charWidth, charHeight); - // this._imageDataCache[key] = imageData; - // } + imageData = ctx.getImageData(x * charWidth, y * charHeight, charWidth, charHeight); + this._imageDataCache[key] = imageData; + } + ctx.putImageData(imageData, x * charWidth, y * charHeight); + // Always write text // ctx.fillText(ch, x * charWidth, y * charHeight); - // ctx.putImageData(imageData, x * charWidth, y * charHeight); } } - this._imageDataCache = {}; } /** diff --git a/src/RendererCanvas.ts b/src/RendererCanvas.ts new file mode 100644 index 0000000000..703d14643d --- /dev/null +++ b/src/RendererCanvas.ts @@ -0,0 +1,294 @@ +/** + * @license MIT + */ + +import { ITerminal } from './Interfaces'; +import { DomElementObjectPool } from './utils/DomElementObjectPool'; +import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer'; + +/** + * The maximum number of refresh frames to skip when the write buffer is non- + * empty. Note that these frames may be intermingled with frames that are + * skipped via requestAnimationFrame's mechanism. + */ +const MAX_REFRESH_FRAME_SKIP = 5; + +/** + * Flags used to render terminal text properly. + */ +enum FLAGS { + BOLD = 1, + UNDERLINE = 2, + BLINK = 4, + INVERSE = 8, + INVISIBLE = 16 +}; + +let brokenBold: boolean = null; + +export class Renderer { + /** A queue of the rows to be refreshed */ + private _refreshRowsQueue: {start: number, end: number}[] = []; + private _refreshFramesSkipped = 0; + private _refreshAnimationFrame = null; + + constructor(private _terminal: ITerminal) { + // Figure out whether boldness affects + // the character width of monospace fonts. + if (brokenBold === null) { + brokenBold = checkBoldBroken(this._terminal.element); + } + + // TODO: Pull more DOM interactions into Renderer.constructor, element for + // example should be owned by Renderer (and also exposed by Terminal due to + // to established public API). + } + + /** + * Queues a refresh between two rows (inclusive), to be done on next animation + * frame. + * @param {number} start The start row. + * @param {number} end The end row. + */ + public queueRefresh(start: number, end: number): void { + this._refreshRowsQueue.push({ start: start, end: end }); + if (!this._refreshAnimationFrame) { + this._refreshAnimationFrame = window.requestAnimationFrame(this._refreshLoop.bind(this)); + } + } + + /** + * Performs the refresh loop callback, calling refresh only if a refresh is + * necessary before queueing up the next one. + */ + private _refreshLoop(): void { + // Skip MAX_REFRESH_FRAME_SKIP frames if the writeBuffer is non-empty as it + // will need to be immediately refreshed anyway. This saves a lot of + // rendering time as the viewport DOM does not need to be refreshed, no + // scroll events, no layouts, etc. + const skipFrame = this._terminal.writeBuffer.length > 0 && this._refreshFramesSkipped++ <= MAX_REFRESH_FRAME_SKIP; + if (skipFrame) { + this._refreshAnimationFrame = window.requestAnimationFrame(this._refreshLoop.bind(this)); + return; + } + + this._refreshFramesSkipped = 0; + let start; + let end; + if (this._refreshRowsQueue.length > 4) { + // Just do a full refresh when 5+ refreshes are queued + start = 0; + end = this._terminal.rows - 1; + } else { + // Get start and end rows that need refreshing + start = this._refreshRowsQueue[0].start; + end = this._refreshRowsQueue[0].end; + for (let i = 1; i < this._refreshRowsQueue.length; i++) { + if (this._refreshRowsQueue[i].start < start) { + start = this._refreshRowsQueue[i].start; + } + if (this._refreshRowsQueue[i].end > end) { + end = this._refreshRowsQueue[i].end; + } + } + } + this._refreshRowsQueue = []; + this._refreshAnimationFrame = null; + this._refresh(start, end); + this._terminal.emit('refresh', {start, end}); + } + + private _imageDataCache = {}; + private _colors = [ + // dark: + '#2e3436', + '#cc0000', + '#4e9a06', + '#c4a000', + '#3465a4', + '#75507b', + '#06989a', + '#d3d7cf', + // bright: + '#555753', + '#ef2929', + '#8ae234', + '#fce94f', + '#729fcf', + '#ad7fa8', + '#34e2e2', + '#eeeeec' + ]; + + /** + * Refreshes (re-renders) terminal content within two rows (inclusive) + * + * Rendering Engine: + * + * In the screen buffer, each character is stored as a an array with a character + * and a 32-bit integer: + * - First value: a utf-16 character. + * - Second value: + * - Next 9 bits: background color (0-511). + * - Next 9 bits: foreground color (0-511). + * - Next 14 bits: a mask for misc. flags: + * - 1=bold + * - 2=underline + * - 4=blink + * - 8=inverse + * - 16=invisible + * + * @param {number} start The row to start from (between 0 and terminal's height terminal - 1) + * @param {number} end The row to end at (between fromRow and terminal's height terminal - 1) + */ + private _refresh(start: number, end: number): void { + const charWidth = Math.ceil(this._terminal.charMeasure.width) * window.devicePixelRatio; + const charHeight = Math.ceil(this._terminal.charMeasure.height) * window.devicePixelRatio; + const ctx = this._terminal.canvasContext; + + ctx.fillStyle = '#000000'; + // console.log('fill', start, end); + // console.log('fill', start * charHeight, (end - start + 1) * charHeight); + // ctx.fillRect(0, start * charHeight, charWidth * this._terminal.cols, (end - start + 1) * charHeight); + ctx.fillStyle = 'rgb(255, 255, 255)'; + ctx.textBaseline = 'top'; + + // Indicates whether to reset the font next cell + let resetFont = true; + + for (let y = start; y <= end; y++) { + if (resetFont) { + ctx.font = `${16 * window.devicePixelRatio}px courier`; + resetFont = false; + } + let row = y + this._terminal.buffer.ydisp; + let line = this._terminal.buffer.lines.get(row); + for (let x = 0; x < this._terminal.cols; x++) { + let data: number = line[x][0]; + const ch = line[x][CHAR_DATA_CHAR_INDEX]; + + // if (ch === ' ') { + // continue; + // } + + let bg = data & 0x1ff; + let fg = (data >> 9) & 0x1ff; + let flags = data >> 18; + + // if (bg < 16) { + // } + + if (flags & FLAGS.BOLD) { + ctx.font = `bold ${ctx.font}`; + resetFont = true; + // Convert the FG color to the bold variant + if (fg < 8) { + fg += 8; + } + } + + if (fg > 255) { + ctx.fillStyle = '#ffffff'; + } else if (fg > 15) { + // TODO: Support colors 16-255 + } else { + ctx.fillStyle = this._colors[fg]; + } + + // Simulate cache + let imageData; + let key = ch + data; + if (key in this._imageDataCache) { + imageData = this._imageDataCache[key]; + } else { + ctx.fillText(ch, x * charWidth, y * charHeight); + if (flags & FLAGS.UNDERLINE) { + ctx.fillRect(x * charWidth, (y + 1) * charHeight - window.devicePixelRatio, charWidth, window.devicePixelRatio); + } + imageData = ctx.getImageData(x * charWidth, y * charHeight, charWidth, charHeight); + this._imageDataCache[key] = imageData; + } + ctx.putImageData(imageData, x * charWidth, y * charHeight); + + // Always write text + // ctx.fillText(ch, x * charWidth, y * charHeight); + } + } + } + + /** + * Refreshes the selection in the DOM. + * @param start The selection start. + * @param end The selection end. + */ + public refreshSelection(start: [number, number], end: [number, number]): void { + // 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. +function checkBoldBroken(terminalElement: HTMLElement): boolean { + const document = terminalElement.ownerDocument; + const el = document.createElement('span'); + el.innerHTML = 'hello world'; + terminalElement.appendChild(el); + const w1 = el.offsetWidth; + const h1 = el.offsetHeight; + el.style.fontWeight = 'bold'; + const w2 = el.offsetWidth; + const h2 = el.offsetHeight; + terminalElement.removeChild(el); + return w1 !== w2 || h1 !== h2; +} diff --git a/src/Terminal.ts b/src/Terminal.ts index 0bb00ecf6f..2ddba9a283 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -29,7 +29,8 @@ import { CircularList } from './utils/CircularList'; import { C0 } from './EscapeSequences'; import { InputHandler } from './InputHandler'; import { Parser } from './Parser'; -import { Renderer } from './Renderer'; +// import { Renderer } from './Renderer'; +import { Renderer } from './RendererCanvas'; import { Linkifier } from './Linkifier'; import { SelectionManager } from './SelectionManager'; import { CharMeasure } from './utils/CharMeasure'; @@ -708,6 +709,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.canvasElement = document.createElement('canvas'); this.canvasContext = this.canvasElement.getContext('2d'); + // Scale the context for HDPI screens + this.canvasContext.scale(window.devicePixelRatio, window.devicePixelRatio); this.element.appendChild(this.canvasElement); @@ -755,8 +758,12 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.charMeasure.measure(); this.charMeasure.on('charsizechanged', () => { - this.canvasElement.setAttribute('width', `${Math.ceil(this.charMeasure.width) * this.cols}px`); - this.canvasElement.setAttribute('height', `${Math.ceil(this.charMeasure.height) * this.rows}px`); + const width = Math.ceil(this.charMeasure.width) * this.cols; + const height = Math.ceil(this.charMeasure.height) * this.rows; + this.canvasElement.width = width * window.devicePixelRatio; + this.canvasElement.height = height * window.devicePixelRatio; + this.canvasElement.style.width = `${width}px`; + this.canvasElement.style.height = `${height}px`; }); this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure); From e98794a0a75fe6809bfe4f684fb65a0c53f7785f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 30 Aug 2017 17:27:04 -0700 Subject: [PATCH 003/108] progress --- src/RendererCanvas.ts | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/RendererCanvas.ts b/src/RendererCanvas.ts index 703d14643d..0665b880d8 100644 --- a/src/RendererCanvas.ts +++ b/src/RendererCanvas.ts @@ -98,6 +98,7 @@ export class Renderer { this._terminal.emit('refresh', {start, end}); } + // TODO: This would be better as a large texture atlas rather than a cache of ImageData objects private _imageDataCache = {}; private _colors = [ // dark: @@ -150,20 +151,16 @@ export class Renderer { // console.log('fill', start, end); // console.log('fill', start * charHeight, (end - start + 1) * charHeight); // ctx.fillRect(0, start * charHeight, charWidth * this._terminal.cols, (end - start + 1) * charHeight); - ctx.fillStyle = 'rgb(255, 255, 255)'; + ctx.fillStyle = '#ffffff'; ctx.textBaseline = 'top'; - - // Indicates whether to reset the font next cell - let resetFont = true; + ctx.font = `${16 * window.devicePixelRatio}px courier`; for (let y = start; y <= end; y++) { - if (resetFont) { - ctx.font = `${16 * window.devicePixelRatio}px courier`; - resetFont = false; - } let row = y + this._terminal.buffer.ydisp; let line = this._terminal.buffer.lines.get(row); for (let x = 0; x < this._terminal.cols; x++) { + ctx.save(); + let data: number = line[x][0]; const ch = line[x][CHAR_DATA_CHAR_INDEX]; @@ -180,19 +177,16 @@ export class Renderer { if (flags & FLAGS.BOLD) { ctx.font = `bold ${ctx.font}`; - resetFont = true; // Convert the FG color to the bold variant if (fg < 8) { fg += 8; } } - if (fg > 255) { - ctx.fillStyle = '#ffffff'; - } else if (fg > 15) { - // TODO: Support colors 16-255 - } else { + if (fg < 16) { ctx.fillStyle = this._colors[fg]; + } else if (fg < 256) { + // TODO: Support colors 16-255 } // Simulate cache @@ -212,6 +206,7 @@ export class Renderer { // Always write text // ctx.fillText(ch, x * charWidth, y * charHeight); + ctx.restore(); } } } From 5ce39d3c816404834581b811b789098211b262f8 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 30 Aug 2017 19:12:26 -0700 Subject: [PATCH 004/108] GPU ascii rendering --- src/InputHandler.ts | 15 ++-- src/Interfaces.ts | 2 - src/Renderer.ts | 70 --------------- src/RendererCanvas.ts | 199 ++++++++++++++++++++++++++++++++--------- src/Terminal.ts | 21 +---- src/renderer/Canvas.ts | 14 +++ 6 files changed, 179 insertions(+), 142 deletions(-) create mode 100644 src/renderer/Canvas.ts diff --git a/src/InputHandler.ts b/src/InputHandler.ts index abfdfa0e6b..e9346c1a0c 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -36,13 +36,14 @@ export class InputHandler implements IInputHandler { // dont overflow left if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1]) { if (!this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) { - // found empty cell after fullwidth, need to go 2 cells back - if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2]) + if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2]) { this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char; - + this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2][3] = char.charCodeAt(0); + } } else { this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char; + this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][3] = char.charCodeAt(0); } this._terminal.updateRange(this._terminal.buffer.y); } @@ -81,21 +82,21 @@ export class InputHandler implements IInputHandler { if (removed[CHAR_DATA_WIDTH_INDEX] === 0 && this._terminal.buffer.lines.get(row)[this._terminal.cols - 2] && this._terminal.buffer.lines.get(row)[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) { - this._terminal.buffer.lines.get(row)[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1]; + this._terminal.buffer.lines.get(row)[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]; } // insert empty cell at cursor - this._terminal.buffer.lines.get(row).splice(this._terminal.buffer.x, 0, [this._terminal.curAttr, ' ', 1]); + this._terminal.buffer.lines.get(row).splice(this._terminal.buffer.x, 0, [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]); } } - this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, char, ch_width]; + this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, char, ch_width, char.charCodeAt(0)]; this._terminal.buffer.x++; this._terminal.updateRange(this._terminal.buffer.y); // fullwidth char - set next cell width to zero and advance cursor if (ch_width === 2) { - this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, '', 0]; + this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, '', 0, undefined]; this._terminal.buffer.x++; } } diff --git a/src/Interfaces.ts b/src/Interfaces.ts index b643de45c7..fb66f4d7cf 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -21,8 +21,6 @@ export interface ITerminal extends IEventEmitter { element: HTMLElement; rowContainer: HTMLElement; selectionContainer: HTMLElement; - canvasElement: HTMLCanvasElement; - canvasContext: CanvasRenderingContext2D; selectionManager: ISelectionManager; charMeasure: ICharMeasure; textarea: HTMLTextAreaElement; diff --git a/src/Renderer.ts b/src/Renderer.ts index 8c15509654..f4a275bbe2 100644 --- a/src/Renderer.ts +++ b/src/Renderer.ts @@ -98,7 +98,6 @@ export class Renderer { this._refreshRowsQueue = []; this._refreshAnimationFrame = null; this._refresh(start, end); - this._canvasRender(start, end); } /** @@ -346,75 +345,6 @@ export class Renderer { '#eeeeec' ]; - private _canvasRender(start: number, end: number): void { - - const charWidth = Math.ceil(this._terminal.charMeasure.width) * window.devicePixelRatio; - const charHeight = Math.ceil(this._terminal.charMeasure.height) * window.devicePixelRatio; - const ctx = this._terminal.canvasContext; - - ctx.fillStyle = '#000000'; - // console.log('fill', start, end); - // console.log('fill', start * charHeight, (end - start + 1) * charHeight); - // ctx.fillRect(0, start * charHeight, charWidth * this._terminal.cols, (end - start + 1) * charHeight); - ctx.fillStyle = 'rgb(255, 255, 255)'; - ctx.textBaseline = 'top'; - - // Indicates whether to reset the font next cell - let resetFont = true; - - for (let y = start; y <= end; y++) { - if (resetFont) { - ctx.font = `${16 * window.devicePixelRatio}px courier`; - resetFont = false; - } - let row = y + this._terminal.buffer.ydisp; - let line = this._terminal.buffer.lines.get(row); - for (let x = 0; x < this._terminal.cols; x++) { - let data: any = line[x][0]; - const ch = line[x][CHAR_DATA_CHAR_INDEX]; - - // if (ch === ' ') { - // continue; - // } - - let bg = data & 0x1ff; - let fg = (data >> 9) & 0x1ff; - let flags = data >> 18; - - // if (bg < 16) { - // } - - if (flags & FLAGS.BOLD) { - ctx.font = `bold ${ctx.font}`; - resetFont = true; - // Convert the FG color to the bold variant - if (fg < 8) { - fg += 8; - } - } - - if (fg < 16) { - ctx.fillStyle = this._colors[fg]; - } - - // Simulate cache - let imageData; - let key = ch + fg; - if (key in this._imageDataCache) { - imageData = this._imageDataCache[key]; - } else { - ctx.fillText(ch, x * charWidth, y * charHeight); - imageData = ctx.getImageData(x * charWidth, y * charHeight, charWidth, charHeight); - this._imageDataCache[key] = imageData; - } - ctx.putImageData(imageData, x * charWidth, y * charHeight); - - // Always write text - // ctx.fillText(ch, x * charWidth, y * charHeight); - } - } - } - /** * Refreshes the selection in the DOM. * @param start The selection start. diff --git a/src/RendererCanvas.ts b/src/RendererCanvas.ts index 0665b880d8..9d4156c41f 100644 --- a/src/RendererCanvas.ts +++ b/src/RendererCanvas.ts @@ -5,6 +5,7 @@ import { ITerminal } from './Interfaces'; import { DomElementObjectPool } from './utils/DomElementObjectPool'; import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer'; +import { createBackgroundFillData } from './renderer/Canvas'; /** * The maximum number of refresh frames to skip when the write buffer is non- @@ -32,6 +33,38 @@ export class Renderer { private _refreshFramesSkipped = 0; private _refreshAnimationFrame = null; + private _canvasElement: HTMLCanvasElement; + private _canvasContext: CanvasRenderingContext2D; + private _offscreenCanvas: HTMLCanvasElement; + private _offscreenContext: CanvasRenderingContext2D; + + private _charImageDataAtlas: ImageData; + private _charImageDataAtlasBitmap;//: ImageBitmap; + + // TODO: This would be better as a large texture atlas rather than a cache of ImageData objects + private _imageDataCache = {}; + private _colors = [ + // dark: + '#2e3436', + '#cc0000', + '#4e9a06', + '#c4a000', + '#3465a4', + '#75507b', + '#06989a', + '#d3d7cf', + // bright: + '#555753', + '#ef2929', + '#8ae234', + '#fce94f', + '#729fcf', + '#ad7fa8', + '#34e2e2', + '#eeeeec' + ]; + private _imageData: ImageData; + constructor(private _terminal: ITerminal) { // Figure out whether boldness affects // the character width of monospace fonts. @@ -39,11 +72,65 @@ export class Renderer { brokenBold = checkBoldBroken(this._terminal.element); } + + this._offscreenCanvas = document.createElement('canvas'); + this._offscreenContext = this._offscreenCanvas.getContext('2d'); + + this._canvasElement = document.createElement('canvas'); + this._canvasContext = this._canvasElement.getContext('2d'); + // Scale the context for HDPI screens + this._canvasContext.scale(window.devicePixelRatio, window.devicePixelRatio); + this._terminal.element.appendChild(this._canvasElement); + // TODO: Pull more DOM interactions into Renderer.constructor, element for // example should be owned by Renderer (and also exposed by Terminal due to // to established public API). } + public onResize(cols: number, rows: number): void { + // TODO: Could be triggered immediately after onCharSizeChanged + } + + public onCharSizeChanged(charWidth: number, charHeight: number): void { + const width = Math.ceil(charWidth) * this._terminal.cols; + const height = Math.ceil(charHeight) * this._terminal.rows; + this._canvasElement.width = width * window.devicePixelRatio; + this._canvasElement.height = height * window.devicePixelRatio; + this._canvasElement.style.width = `${width}px`; + this._canvasElement.style.height = `${height}px`; + + this._offscreenCanvas.width = 255 * charWidth * window.devicePixelRatio; + this._offscreenCanvas.height = (/*default*/1 + /*0-15*/16) * charHeight * window.devicePixelRatio; + this._refreshCharImageDataAtlas(); + } + + private _refreshCharImageDataAtlas(): void { + const scaledCharWidth = Math.ceil(this._terminal.charMeasure.width) * window.devicePixelRatio; + const scaledCharHeight = Math.ceil(this._terminal.charMeasure.height) * window.devicePixelRatio; + + this._offscreenContext.save(); + this._offscreenContext.fillStyle = '#ffffff'; + this._offscreenContext.font = `${16 * window.devicePixelRatio}px courier`; + this._offscreenContext.textBaseline = 'top'; + // Default color + for (let i = 0; i < 256; i++) { + this._offscreenContext.fillText(String.fromCharCode(i), i * scaledCharWidth, 0); + } + // Colors 0-15 + for (let colorIndex = 0; colorIndex < 16; colorIndex++) { + for (let i = 0; i < 256; i++) { + this._offscreenContext.fillStyle = this._colors[colorIndex]; + this._offscreenContext.fillText(String.fromCharCode(i), i * scaledCharWidth, (colorIndex + 1) * scaledCharHeight); + } + } + this._offscreenContext.restore(); + + this._charImageDataAtlas = this._offscreenContext.getImageData(0, 0, this._offscreenCanvas.width, this._offscreenCanvas.height); + (window).createImageBitmap(this._charImageDataAtlas).then(bitmap => { + this._charImageDataAtlasBitmap = bitmap; + }); + } + /** * Queues a refresh between two rows (inclusive), to be done on next animation * frame. @@ -98,29 +185,6 @@ export class Renderer { this._terminal.emit('refresh', {start, end}); } - // TODO: This would be better as a large texture atlas rather than a cache of ImageData objects - private _imageDataCache = {}; - private _colors = [ - // dark: - '#2e3436', - '#cc0000', - '#4e9a06', - '#c4a000', - '#3465a4', - '#75507b', - '#06989a', - '#d3d7cf', - // bright: - '#555753', - '#ef2929', - '#8ae234', - '#fce94f', - '#729fcf', - '#ad7fa8', - '#34e2e2', - '#eeeeec' - ]; - /** * Refreshes (re-renders) terminal content within two rows (inclusive) * @@ -143,11 +207,22 @@ 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 { - const charWidth = Math.ceil(this._terminal.charMeasure.width) * window.devicePixelRatio; - const charHeight = Math.ceil(this._terminal.charMeasure.height) * window.devicePixelRatio; - const ctx = this._terminal.canvasContext; + const scaledCharWidth = Math.ceil(this._terminal.charMeasure.width) * window.devicePixelRatio; + const scaledCharHeight = Math.ceil(this._terminal.charMeasure.height) * window.devicePixelRatio; + const ctx = this._canvasContext; + + // TODO: Needs to react to terminal resize + // Initialize image data + if (!this._imageData) { + this._imageData = ctx.createImageData(scaledCharWidth * this._terminal.cols * window.devicePixelRatio, scaledCharHeight * this._terminal.rows * window.devicePixelRatio); + this._imageData.data.set(createBackgroundFillData(this._imageData.width, this._imageData.height, 255, 0, 0, 255)); + } + + // Don't bother rendering until the atlas bitmap is ready + if (!this._charImageDataAtlasBitmap) { + return; + } - ctx.fillStyle = '#000000'; // console.log('fill', start, end); // console.log('fill', start * charHeight, (end - start + 1) * charHeight); // ctx.fillRect(0, start * charHeight, charWidth * this._terminal.cols, (end - start + 1) * charHeight); @@ -155,6 +230,9 @@ export class Renderer { ctx.textBaseline = 'top'; ctx.font = `${16 * window.devicePixelRatio}px courier`; + // Clear out the old data + ctx.clearRect(0, start * scaledCharHeight, scaledCharWidth * this._terminal.cols, (end - start + 1) * scaledCharHeight); + for (let y = start; y <= end; y++) { let row = y + this._terminal.buffer.ydisp; let line = this._terminal.buffer.lines.get(row); @@ -162,7 +240,8 @@ export class Renderer { ctx.save(); let data: number = line[x][0]; - const ch = line[x][CHAR_DATA_CHAR_INDEX]; + // const ch = line[x][CHAR_DATA_CHAR_INDEX]; + const code: number = line[x][3]; // if (ch === ' ') { // continue; @@ -183,32 +262,64 @@ export class Renderer { } } + // if (fg < 16) { + // ctx.fillStyle = this._colors[fg]; + // } else if (fg < 256) { + // // TODO: Support colors 16-255 + // } + + let colorIndex = 0; if (fg < 16) { - ctx.fillStyle = this._colors[fg]; - } else if (fg < 256) { - // TODO: Support colors 16-255 + colorIndex = fg + 1; } // Simulate cache - let imageData; - let key = ch + data; - if (key in this._imageDataCache) { - imageData = this._imageDataCache[key]; - } else { - ctx.fillText(ch, x * charWidth, y * charHeight); - if (flags & FLAGS.UNDERLINE) { - ctx.fillRect(x * charWidth, (y + 1) * charHeight - window.devicePixelRatio, charWidth, window.devicePixelRatio); - } - imageData = ctx.getImageData(x * charWidth, y * charHeight, charWidth, charHeight); - this._imageDataCache[key] = imageData; - } - ctx.putImageData(imageData, x * charWidth, y * charHeight); + // let imageData; + // let key = ch + data; + // if (key in this._imageDataCache) { + // imageData = this._imageDataCache[key]; + // } else { + // ctx.fillText(ch, x * scaledCharWidth, y * scaledCharHeight); + // if (flags & FLAGS.UNDERLINE) { + // ctx.fillRect(x * scaledCharWidth, (y + 1) * scaledCharHeight - window.devicePixelRatio, scaledCharWidth, window.devicePixelRatio); + // } + // imageData = ctx.getImageData(x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); + // this._imageDataCache[key] = imageData; + // } + // ctx.putImageData(imageData, x * scaledCharWidth, y * scaledCharHeight); + + // TODO: Try to get atlas working + // This seems too slow :( + //ctx.putImageData(this._charImageDataAtlas, x * scaledCharWidth - ch.charCodeAt(0) * scaledCharWidth, y * scaledCharHeight, ch.charCodeAt(0) * scaledCharWidth, 0, scaledCharWidth, scaledCharHeight); + + // TODO: Draw background + // Maybe background should be on a separate layer? + // ctx.save(); + // if (bg < 16) { + // ctx.fillStyle = this._colors[fg]; + // } else { + // ctx.fillStyle = '#000000'; + // } + // ctx.fillRect(x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); + // ctx.restore(); + + // ctx.drawImage(this._offscreenCanvas, code * scaledCharWidth, 0, scaledCharWidth, scaledCharHeight, x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); + + // ImageBitmap's draw about twice as fast as from a canvas + ctx.drawImage(this._charImageDataAtlasBitmap, code * scaledCharWidth, colorIndex * scaledCharHeight, scaledCharWidth, scaledCharHeight, x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); // Always write text // ctx.fillText(ch, x * charWidth, y * charHeight); ctx.restore(); } + + + // TODO: Try to get atlas working + // ctx.putImageData(this._charImageDataAtlas, y * scaledCharWidth, y * scaledCharHeight, 65 * scaledCharWidth, 0, scaledCharWidth, scaledCharHeight); } + + // This draws the atlas (for debugging purposes) + //ctx.putImageData(this._charImageDataAtlas, 0, 0); } /** diff --git a/src/Terminal.ts b/src/Terminal.ts index 2ddba9a283..429092989b 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -176,8 +176,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT private body: HTMLBodyElement; private viewportScrollArea: HTMLElement; private viewportElement: HTMLElement; - public canvasElement: HTMLCanvasElement; - public canvasContext: CanvasRenderingContext2D; public selectionContainer: HTMLElement; private helperContainer: HTMLElement; private compositionView: HTMLElement; @@ -706,14 +704,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.selectionContainer.classList.add('xterm-selection'); this.element.appendChild(this.selectionContainer); - - this.canvasElement = document.createElement('canvas'); - this.canvasContext = this.canvasElement.getContext('2d'); - // Scale the context for HDPI screens - this.canvasContext.scale(window.devicePixelRatio, window.devicePixelRatio); - this.element.appendChild(this.canvasElement); - - // Create the container that will hold the lines of the terminal and then // produce the lines the lines. this.rowContainer = document.createElement('div'); @@ -757,17 +747,10 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT }); this.charMeasure.measure(); - this.charMeasure.on('charsizechanged', () => { - const width = Math.ceil(this.charMeasure.width) * this.cols; - const height = Math.ceil(this.charMeasure.height) * this.rows; - this.canvasElement.width = width * window.devicePixelRatio; - this.canvasElement.height = height * window.devicePixelRatio; - this.canvasElement.style.width = `${width}px`; - this.canvasElement.style.height = `${height}px`; - }); - this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure); this.renderer = new Renderer(this); + this.on('resize', () => this.renderer.onResize(this.cols, this.rows)); + this.charMeasure.on('charsizechanged', () => this.renderer.onCharSizeChanged(this.charMeasure.width, this.charMeasure.height)); this.selectionManager = new SelectionManager(this, this.buffer, this.rowContainer, this.charMeasure); this.selectionManager.on('refresh', data => { this.renderer.refreshSelection(data.start, data.end); diff --git a/src/renderer/Canvas.ts b/src/renderer/Canvas.ts new file mode 100644 index 0000000000..97e7f9ea8a --- /dev/null +++ b/src/renderer/Canvas.ts @@ -0,0 +1,14 @@ +export function createBackgroundFillData(width: number, height: number, r: number, g: number, b: number, a: number): Uint8ClampedArray { + const data = new Uint8ClampedArray(width * height * 4); + let offset = 0; + for (let i = 0; i < height; i++) { + for (let j = 0; j < width; j++) { + data[offset] = r; + data[offset + 1] = g; + data[offset + 2] = b; + data[offset + 3] = a; + offset += 4; + } + } + return data; +} From 137847e85c37668e6fa9120f4df233c9bab814bc Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 30 Aug 2017 19:27:37 -0700 Subject: [PATCH 005/108] Linkify all rows at once to prevent multiple timeout restarts --- src/Linkifier.ts | 24 ++++++++++++++++++++++++ src/Terminal.ts | 7 ++++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 2323f22679..5a0fc1e12b 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -47,6 +47,7 @@ export class Linkifier { private _document: Document; private _rows: HTMLElement[]; private _rowTimeoutIds: number[]; + private _rowsTimeoutId: number; private _nextLinkMatcherId = HYPERTEXT_LINK_MATCHER_ID; constructor() { @@ -65,6 +66,29 @@ export class Linkifier { this._rows = rows; } + public linkifyRows(start: number, end: number): void { + // for (let i = start; i <= end; i++) { + // this.linkifyRow(i); + // } + + + // Don't attempt linkify if not yet attached to DOM + if (!this._document) { + return; + } + + if (this._rowsTimeoutId) { + clearTimeout(this._rowsTimeoutId); + } + this._rowsTimeoutId = setTimeout(this._linkifyRows.bind(this, start, end), Linkifier.TIME_BEFORE_LINKIFY); + } + + private _linkifyRows(start: number, end: number): void { + for (let i = start; i <= end; i++) { + this._linkifyRow(i); + } + } + /** * Queues a row for linkification. * @param {number} rowIndex The index of the row to linkify. diff --git a/src/Terminal.ts b/src/Terminal.ts index 429092989b..c076d41066 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1146,9 +1146,10 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT */ private queueLinkification(start: number, end: number): void { if (this.linkifier) { - for (let i = start; i <= end; i++) { - this.linkifier.linkifyRow(i); - } + this.linkifier.linkifyRows(0, this.rows); + // for (let i = start; i <= end; i++) { + // this.linkifier.linkifyRow(i); + // } } } From 0db42e2056ca343a3d4a7ebfaf65b1da6043abb1 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 30 Aug 2017 21:13:57 -0700 Subject: [PATCH 006/108] Pull bg rendering into an IRenderLayer --- src/Buffer.ts | 1 + src/RendererCanvas.ts | 95 ++++++++++++++------------- src/renderer/BackgroundRenderLayer.ts | 70 ++++++++++++++++++++ src/renderer/Interfaces.ts | 6 ++ src/xterm.css | 17 +++++ 5 files changed, 144 insertions(+), 45 deletions(-) create mode 100644 src/renderer/BackgroundRenderLayer.ts create mode 100644 src/renderer/Interfaces.ts diff --git a/src/Buffer.ts b/src/Buffer.ts index 4c73006391..e8ceebf32f 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -6,6 +6,7 @@ import { ITerminal, IBuffer } from './Interfaces'; import { CircularList } from './utils/CircularList'; import { LineData, CharData } from './Types'; +export const CHAR_DATA_ATTR_INDEX = 0; export const CHAR_DATA_CHAR_INDEX = 1; export const CHAR_DATA_WIDTH_INDEX = 2; diff --git a/src/RendererCanvas.ts b/src/RendererCanvas.ts index 9d4156c41f..934662211e 100644 --- a/src/RendererCanvas.ts +++ b/src/RendererCanvas.ts @@ -6,6 +6,8 @@ import { ITerminal } from './Interfaces'; import { DomElementObjectPool } from './utils/DomElementObjectPool'; import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer'; import { createBackgroundFillData } from './renderer/Canvas'; +import { IRenderLayer } from './renderer/Interfaces'; +import { BackgroundRenderLayer } from './renderer/BackgroundRenderLayer'; /** * The maximum number of refresh frames to skip when the write buffer is non- @@ -33,13 +35,14 @@ export class Renderer { private _refreshFramesSkipped = 0; private _refreshAnimationFrame = null; - private _canvasElement: HTMLCanvasElement; - private _canvasContext: CanvasRenderingContext2D; + private _textCanvasElement: HTMLCanvasElement; + private _textCanvasContext: CanvasRenderingContext2D; private _offscreenCanvas: HTMLCanvasElement; private _offscreenContext: CanvasRenderingContext2D; - private _charImageDataAtlas: ImageData; - private _charImageDataAtlasBitmap;//: ImageBitmap; + private _renderLayers: IRenderLayer[]; + + private _charAtlasBitmap;//: ImageBitmap; // TODO: This would be better as a large texture atlas rather than a cache of ImageData objects private _imageDataCache = {}; @@ -72,15 +75,20 @@ export class Renderer { brokenBold = checkBoldBroken(this._terminal.element); } + this._textCanvasElement = document.createElement('canvas'); + this._textCanvasElement.classList.add('xterm-text-canvas'); + this._textCanvasContext = this._textCanvasElement.getContext('2d'); + this._textCanvasContext.scale(window.devicePixelRatio, window.devicePixelRatio); this._offscreenCanvas = document.createElement('canvas'); this._offscreenContext = this._offscreenCanvas.getContext('2d'); + this._offscreenContext.scale(window.devicePixelRatio, window.devicePixelRatio); + + this._renderLayers = [ + new BackgroundRenderLayer(this._terminal.element) + ]; - this._canvasElement = document.createElement('canvas'); - this._canvasContext = this._canvasElement.getContext('2d'); - // Scale the context for HDPI screens - this._canvasContext.scale(window.devicePixelRatio, window.devicePixelRatio); - this._terminal.element.appendChild(this._canvasElement); + this._terminal.element.appendChild(this._textCanvasElement); // TODO: Pull more DOM interactions into Renderer.constructor, element for // example should be owned by Renderer (and also exposed by Terminal due to @@ -94,14 +102,16 @@ export class Renderer { public onCharSizeChanged(charWidth: number, charHeight: number): void { const width = Math.ceil(charWidth) * this._terminal.cols; const height = Math.ceil(charHeight) * this._terminal.rows; - this._canvasElement.width = width * window.devicePixelRatio; - this._canvasElement.height = height * window.devicePixelRatio; - this._canvasElement.style.width = `${width}px`; - this._canvasElement.style.height = `${height}px`; - + this._textCanvasElement.width = width * window.devicePixelRatio; + this._textCanvasElement.height = height * window.devicePixelRatio; + this._textCanvasElement.style.width = `${width}px`; + this._textCanvasElement.style.height = `${height}px`; this._offscreenCanvas.width = 255 * charWidth * window.devicePixelRatio; this._offscreenCanvas.height = (/*default*/1 + /*0-15*/16) * charHeight * window.devicePixelRatio; this._refreshCharImageDataAtlas(); + for (let i = 0; i < this._renderLayers.length; i++) { + this._renderLayers[i].resize(width, height, charWidth, charHeight); + } } private _refreshCharImageDataAtlas(): void { @@ -118,6 +128,10 @@ export class Renderer { } // Colors 0-15 for (let colorIndex = 0; colorIndex < 16; colorIndex++) { + // colors 8-15 are bold + if (colorIndex === 8) { + this._offscreenContext.font = `bold ${this._offscreenContext.font}`; + } for (let i = 0; i < 256; i++) { this._offscreenContext.fillStyle = this._colors[colorIndex]; this._offscreenContext.fillText(String.fromCharCode(i), i * scaledCharWidth, (colorIndex + 1) * scaledCharHeight); @@ -125,10 +139,12 @@ export class Renderer { } this._offscreenContext.restore(); - this._charImageDataAtlas = this._offscreenContext.getImageData(0, 0, this._offscreenCanvas.width, this._offscreenCanvas.height); - (window).createImageBitmap(this._charImageDataAtlas).then(bitmap => { - this._charImageDataAtlasBitmap = bitmap; + const charAtlasImageData = this._offscreenContext.getImageData(0, 0, this._offscreenCanvas.width, this._offscreenCanvas.height); + (window).createImageBitmap(charAtlasImageData).then(bitmap => { + this._charAtlasBitmap = bitmap; }); + + this._offscreenContext.clearRect(0, 0, this._offscreenCanvas.width, this._offscreenCanvas.height); } /** @@ -182,6 +198,9 @@ export class Renderer { this._refreshRowsQueue = []; this._refreshAnimationFrame = null; this._refresh(start, end); + for (let i = 0; i < this._renderLayers.length; i++) { + this._renderLayers[i].render(this._terminal, start, end); + } this._terminal.emit('refresh', {start, end}); } @@ -209,35 +228,35 @@ export class Renderer { private _refresh(start: number, end: number): void { const scaledCharWidth = Math.ceil(this._terminal.charMeasure.width) * window.devicePixelRatio; const scaledCharHeight = Math.ceil(this._terminal.charMeasure.height) * window.devicePixelRatio; - const ctx = this._canvasContext; + const textCtx = this._textCanvasContext; // TODO: Needs to react to terminal resize // Initialize image data if (!this._imageData) { - this._imageData = ctx.createImageData(scaledCharWidth * this._terminal.cols * window.devicePixelRatio, scaledCharHeight * this._terminal.rows * window.devicePixelRatio); + this._imageData = textCtx.createImageData(scaledCharWidth * this._terminal.cols * window.devicePixelRatio, scaledCharHeight * this._terminal.rows * window.devicePixelRatio); this._imageData.data.set(createBackgroundFillData(this._imageData.width, this._imageData.height, 255, 0, 0, 255)); } // Don't bother rendering until the atlas bitmap is ready - if (!this._charImageDataAtlasBitmap) { + if (!this._charAtlasBitmap) { return; } // console.log('fill', start, end); // console.log('fill', start * charHeight, (end - start + 1) * charHeight); // ctx.fillRect(0, start * charHeight, charWidth * this._terminal.cols, (end - start + 1) * charHeight); - ctx.fillStyle = '#ffffff'; - ctx.textBaseline = 'top'; - ctx.font = `${16 * window.devicePixelRatio}px courier`; + textCtx.fillStyle = '#ffffff'; + textCtx.textBaseline = 'top'; + textCtx.font = `${16 * window.devicePixelRatio}px courier`; // Clear out the old data - ctx.clearRect(0, start * scaledCharHeight, scaledCharWidth * this._terminal.cols, (end - start + 1) * scaledCharHeight); + textCtx.clearRect(0, start * scaledCharHeight, scaledCharWidth * this._terminal.cols, (end - start + 1) * scaledCharHeight); for (let y = start; y <= end; y++) { let row = y + this._terminal.buffer.ydisp; let line = this._terminal.buffer.lines.get(row); for (let x = 0; x < this._terminal.cols; x++) { - ctx.save(); + textCtx.save(); let data: number = line[x][0]; // const ch = line[x][CHAR_DATA_CHAR_INDEX]; @@ -247,15 +266,12 @@ export class Renderer { // continue; // } - let bg = data & 0x1ff; + // let bg = data & 0x1ff; let fg = (data >> 9) & 0x1ff; let flags = data >> 18; - // if (bg < 16) { - // } - if (flags & FLAGS.BOLD) { - ctx.font = `bold ${ctx.font}`; + textCtx.font = `bold ${textCtx.font}`; // Convert the FG color to the bold variant if (fg < 8) { fg += 8; @@ -290,27 +306,16 @@ export class Renderer { // TODO: Try to get atlas working // This seems too slow :( - //ctx.putImageData(this._charImageDataAtlas, x * scaledCharWidth - ch.charCodeAt(0) * scaledCharWidth, y * scaledCharHeight, ch.charCodeAt(0) * scaledCharWidth, 0, scaledCharWidth, scaledCharHeight); - - // TODO: Draw background - // Maybe background should be on a separate layer? - // ctx.save(); - // if (bg < 16) { - // ctx.fillStyle = this._colors[fg]; - // } else { - // ctx.fillStyle = '#000000'; - // } - // ctx.fillRect(x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); - // ctx.restore(); + // ctx.putImageData(this._charImageDataAtlas, x * scaledCharWidth - ch.charCodeAt(0) * scaledCharWidth, y * scaledCharHeight, ch.charCodeAt(0) * scaledCharWidth, 0, scaledCharWidth, scaledCharHeight); // ctx.drawImage(this._offscreenCanvas, code * scaledCharWidth, 0, scaledCharWidth, scaledCharHeight, x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); // ImageBitmap's draw about twice as fast as from a canvas - ctx.drawImage(this._charImageDataAtlasBitmap, code * scaledCharWidth, colorIndex * scaledCharHeight, scaledCharWidth, scaledCharHeight, x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); + textCtx.drawImage(this._charAtlasBitmap, code * scaledCharWidth, colorIndex * scaledCharHeight, scaledCharWidth, scaledCharHeight, x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); // Always write text // ctx.fillText(ch, x * charWidth, y * charHeight); - ctx.restore(); + textCtx.restore(); } @@ -319,7 +324,7 @@ export class Renderer { } // This draws the atlas (for debugging purposes) - //ctx.putImageData(this._charImageDataAtlas, 0, 0); + // ctx.putImageData(this._charImageDataAtlas, 0, 0); } /** diff --git a/src/renderer/BackgroundRenderLayer.ts b/src/renderer/BackgroundRenderLayer.ts new file mode 100644 index 0000000000..a624ef2ef8 --- /dev/null +++ b/src/renderer/BackgroundRenderLayer.ts @@ -0,0 +1,70 @@ +import { IRenderLayer } from './Interfaces'; +import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; +import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; + +export class BackgroundRenderLayer implements IRenderLayer { + private _canvas: HTMLCanvasElement; + private _ctx: CanvasRenderingContext2D; + + // TODO: Pull colors into some other class + private _colors = [ + // dark: + '#2e3436', + '#cc0000', + '#4e9a06', + '#c4a000', + '#3465a4', + '#75507b', + '#06989a', + '#d3d7cf', + // bright: + '#555753', + '#ef2929', + '#8ae234', + '#fce94f', + '#729fcf', + '#ad7fa8', + '#34e2e2', + '#eeeeec' + ]; + + constructor(container: HTMLElement) { + this._canvas = document.createElement('canvas'); + this._canvas.classList.add('xterm-bg-canvas'); + this._ctx = this._canvas.getContext('2d'); + this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); + container.appendChild(this._canvas); + } + + public resize(canvasWidth: number, canvasHeight: number, charWidth: number, charHeight: number): void { + this._canvas.width = canvasWidth * window.devicePixelRatio; + this._canvas.height = canvasHeight * window.devicePixelRatio; + this._canvas.style.width = `${canvasWidth}px`; + this._canvas.style.height = `${canvasHeight}px`; + } + + public render(terminal: ITerminal, startRow: number, endRow: number): void { + const scaledCharWidth = Math.ceil(terminal.charMeasure.width) * window.devicePixelRatio; + const scaledCharHeight = Math.ceil(terminal.charMeasure.height) * window.devicePixelRatio; + + for (let y = startRow; y <= endRow; y++) { + let row = y + terminal.buffer.ydisp; + let line = terminal.buffer.lines.get(row); + for (let x = 0; x < terminal.cols; x++) { + const data: number = line[x][CHAR_DATA_ATTR_INDEX]; + const bg = data & 0x1ff; + const flags = data >> 18; + + // TODO: Draw background + if (bg < 16) { + this._ctx.save(); + this._ctx.fillStyle = this._colors[bg]; + this._ctx.fillRect(x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); + this._ctx.restore(); + } else { + this._ctx.clearRect(x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); + } + } + } + } +} diff --git a/src/renderer/Interfaces.ts b/src/renderer/Interfaces.ts new file mode 100644 index 0000000000..6e0c393362 --- /dev/null +++ b/src/renderer/Interfaces.ts @@ -0,0 +1,6 @@ +import { ITerminal } from '../Interfaces'; + +export interface IRenderLayer { + resize(canvasWidth: number, canvasHeight: number, charWidth: number, charHeight: number): void; + render(terminal: ITerminal, startRow: number, endRow: number): void; +} diff --git a/src/xterm.css b/src/xterm.css index 14b0a58d4e..321cf6d648 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -158,10 +158,27 @@ display: inline-block; } +.terminal canvas { + position: absolute; + left: 0; + top: 0; +} + +.terminal .xterm-bg-canvas { + z-index: 0; +} + +.terminal .xterm-text-canvas { + z-index: 1; +} + .terminal .xterm-rows { position: absolute; left: 0; top: 0; + + /* Hide temporarily, this should be removed eventually */ + visibility: hidden; } .terminal .xterm-rows > div { From 5961bdbd9e773ffca83127abe3a98d7465a58a2b Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 30 Aug 2017 21:44:18 -0700 Subject: [PATCH 007/108] typescript@2.3.0 for ImageBitmap interface --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9b550d6765..21ab9b8936 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "nodemon": "1.10.2", "sorcery": "^0.10.0", "tslint": "^4.0.2", - "typescript": "~2.2.0", + "typescript": "~2.3.0", "vinyl-buffer": "^1.0.0", "vinyl-source-stream": "^1.1.0" }, From 2cb6593b6b8070bd68f46e162ebf8f54cb9ef5f5 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 30 Aug 2017 21:44:41 -0700 Subject: [PATCH 008/108] Move fg rendering into IRenderLayer --- src/RendererCanvas.ts | 292 ++++++++------------------ src/renderer/BackgroundRenderLayer.ts | 29 +-- src/renderer/Color.ts | 23 ++ src/renderer/ForegroundRenderLayer.ts | 180 ++++++++++++++++ src/renderer/Interfaces.ts | 2 +- src/renderer/Types.ts | 10 + src/xterm.css | 4 +- 7 files changed, 303 insertions(+), 237 deletions(-) create mode 100644 src/renderer/Color.ts create mode 100644 src/renderer/ForegroundRenderLayer.ts create mode 100644 src/renderer/Types.ts diff --git a/src/RendererCanvas.ts b/src/RendererCanvas.ts index 934662211e..bf952b6c65 100644 --- a/src/RendererCanvas.ts +++ b/src/RendererCanvas.ts @@ -8,6 +8,7 @@ import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer'; import { createBackgroundFillData } from './renderer/Canvas'; import { IRenderLayer } from './renderer/Interfaces'; import { BackgroundRenderLayer } from './renderer/BackgroundRenderLayer'; +import { ForegroundRenderLayer } from './renderer/ForegroundRenderLayer'; /** * The maximum number of refresh frames to skip when the write buffer is non- @@ -16,79 +17,42 @@ import { BackgroundRenderLayer } from './renderer/BackgroundRenderLayer'; */ const MAX_REFRESH_FRAME_SKIP = 5; -/** - * Flags used to render terminal text properly. - */ -enum FLAGS { - BOLD = 1, - UNDERLINE = 2, - BLINK = 4, - INVERSE = 8, - INVISIBLE = 16 -}; - -let brokenBold: boolean = null; - export class Renderer { /** A queue of the rows to be refreshed */ private _refreshRowsQueue: {start: number, end: number}[] = []; private _refreshFramesSkipped = 0; private _refreshAnimationFrame = null; - private _textCanvasElement: HTMLCanvasElement; - private _textCanvasContext: CanvasRenderingContext2D; - private _offscreenCanvas: HTMLCanvasElement; - private _offscreenContext: CanvasRenderingContext2D; + // private _textCanvasElement: HTMLCanvasElement; + // private _textCanvasContext: CanvasRenderingContext2D; + // private _offscreenCanvas: HTMLCanvasElement; + // private _offscreenContext: CanvasRenderingContext2D; private _renderLayers: IRenderLayer[]; - private _charAtlasBitmap;//: ImageBitmap; + // private _charAtlasBitmap;//: ImageBitmap; // TODO: This would be better as a large texture atlas rather than a cache of ImageData objects - private _imageDataCache = {}; - private _colors = [ - // dark: - '#2e3436', - '#cc0000', - '#4e9a06', - '#c4a000', - '#3465a4', - '#75507b', - '#06989a', - '#d3d7cf', - // bright: - '#555753', - '#ef2929', - '#8ae234', - '#fce94f', - '#729fcf', - '#ad7fa8', - '#34e2e2', - '#eeeeec' - ]; - private _imageData: ImageData; + // private _imageDataCache = {}; + // private _imageData: ImageData; constructor(private _terminal: ITerminal) { // Figure out whether boldness affects // the character width of monospace fonts. - if (brokenBold === null) { - brokenBold = checkBoldBroken(this._terminal.element); - } - - this._textCanvasElement = document.createElement('canvas'); - this._textCanvasElement.classList.add('xterm-text-canvas'); - this._textCanvasContext = this._textCanvasElement.getContext('2d'); - this._textCanvasContext.scale(window.devicePixelRatio, window.devicePixelRatio); + // if (brokenBold === null) { + // brokenBold = checkBoldBroken(this._terminal.element); + // } - this._offscreenCanvas = document.createElement('canvas'); - this._offscreenContext = this._offscreenCanvas.getContext('2d'); - this._offscreenContext.scale(window.devicePixelRatio, window.devicePixelRatio); + // this._offscreenCanvas = document.createElement('canvas'); + // this._offscreenContext = this._offscreenCanvas.getContext('2d'); + // this._offscreenContext.scale(window.devicePixelRatio, window.devicePixelRatio); this._renderLayers = [ - new BackgroundRenderLayer(this._terminal.element) + new BackgroundRenderLayer(this._terminal.element), + new ForegroundRenderLayer(this._terminal.element) ]; - this._terminal.element.appendChild(this._textCanvasElement); + // this._terminal.element.appendChild(this._textCanvasElement); // TODO: Pull more DOM interactions into Renderer.constructor, element for // example should be owned by Renderer (and also exposed by Terminal due to @@ -96,56 +60,63 @@ export class Renderer { } public onResize(cols: number, rows: number): void { - // TODO: Could be triggered immediately after onCharSizeChanged + // TODO: This could be triggered immediately after onCharSizeChanged? + const charWidth = this._terminal.charMeasure.width; + const charHeight = this._terminal.charMeasure.height; + const width = Math.ceil(charWidth) * this._terminal.cols; + const height = Math.ceil(charHeight) * this._terminal.rows; + for (let i = 0; i < this._renderLayers.length; i++) { + this._renderLayers[i].resize(width, height, charWidth, charHeight, false); + } } public onCharSizeChanged(charWidth: number, charHeight: number): void { const width = Math.ceil(charWidth) * this._terminal.cols; const height = Math.ceil(charHeight) * this._terminal.rows; - this._textCanvasElement.width = width * window.devicePixelRatio; - this._textCanvasElement.height = height * window.devicePixelRatio; - this._textCanvasElement.style.width = `${width}px`; - this._textCanvasElement.style.height = `${height}px`; - this._offscreenCanvas.width = 255 * charWidth * window.devicePixelRatio; - this._offscreenCanvas.height = (/*default*/1 + /*0-15*/16) * charHeight * window.devicePixelRatio; - this._refreshCharImageDataAtlas(); + // this._textCanvasElement.width = width * window.devicePixelRatio; + // this._textCanvasElement.height = height * window.devicePixelRatio; + // this._textCanvasElement.style.width = `${width}px`; + // this._textCanvasElement.style.height = `${height}px`; + // this._offscreenCanvas.width = 255 * charWidth * window.devicePixelRatio; + // this._offscreenCanvas.height = (/*default*/1 + /*0-15*/16) * charHeight * window.devicePixelRatio; + // this._refreshCharImageDataAtlas(); for (let i = 0; i < this._renderLayers.length; i++) { - this._renderLayers[i].resize(width, height, charWidth, charHeight); + this._renderLayers[i].resize(width, height, charWidth, charHeight, true); } } - private _refreshCharImageDataAtlas(): void { - const scaledCharWidth = Math.ceil(this._terminal.charMeasure.width) * window.devicePixelRatio; - const scaledCharHeight = Math.ceil(this._terminal.charMeasure.height) * window.devicePixelRatio; - - this._offscreenContext.save(); - this._offscreenContext.fillStyle = '#ffffff'; - this._offscreenContext.font = `${16 * window.devicePixelRatio}px courier`; - this._offscreenContext.textBaseline = 'top'; - // Default color - for (let i = 0; i < 256; i++) { - this._offscreenContext.fillText(String.fromCharCode(i), i * scaledCharWidth, 0); - } - // Colors 0-15 - for (let colorIndex = 0; colorIndex < 16; colorIndex++) { - // colors 8-15 are bold - if (colorIndex === 8) { - this._offscreenContext.font = `bold ${this._offscreenContext.font}`; - } - for (let i = 0; i < 256; i++) { - this._offscreenContext.fillStyle = this._colors[colorIndex]; - this._offscreenContext.fillText(String.fromCharCode(i), i * scaledCharWidth, (colorIndex + 1) * scaledCharHeight); - } - } - this._offscreenContext.restore(); - - const charAtlasImageData = this._offscreenContext.getImageData(0, 0, this._offscreenCanvas.width, this._offscreenCanvas.height); - (window).createImageBitmap(charAtlasImageData).then(bitmap => { - this._charAtlasBitmap = bitmap; - }); - - this._offscreenContext.clearRect(0, 0, this._offscreenCanvas.width, this._offscreenCanvas.height); - } + // private _refreshCharImageDataAtlas(): void { + // const scaledCharWidth = Math.ceil(this._terminal.charMeasure.width) * window.devicePixelRatio; + // const scaledCharHeight = Math.ceil(this._terminal.charMeasure.height) * window.devicePixelRatio; + + // this._offscreenContext.save(); + // this._offscreenContext.fillStyle = '#ffffff'; + // this._offscreenContext.font = `${16 * window.devicePixelRatio}px courier`; + // this._offscreenContext.textBaseline = 'top'; + // // Default color + // for (let i = 0; i < 256; i++) { + // this._offscreenContext.fillText(String.fromCharCode(i), i * scaledCharWidth, 0); + // } + // // Colors 0-15 + // for (let colorIndex = 0; colorIndex < 16; colorIndex++) { + // // colors 8-15 are bold + // if (colorIndex === 8) { + // this._offscreenContext.font = `bold ${this._offscreenContext.font}`; + // } + // for (let i = 0; i < 256; i++) { + // this._offscreenContext.fillStyle = this._colors[colorIndex]; + // this._offscreenContext.fillText(String.fromCharCode(i), i * scaledCharWidth, (colorIndex + 1) * scaledCharHeight); + // } + // } + // this._offscreenContext.restore(); + + // const charAtlasImageData = this._offscreenContext.getImageData(0, 0, this._offscreenCanvas.width, this._offscreenCanvas.height); + // (window).createImageBitmap(charAtlasImageData).then(bitmap => { + // this._charAtlasBitmap = bitmap; + // }); + + // this._offscreenContext.clearRect(0, 0, this._offscreenCanvas.width, this._offscreenCanvas.height); + // } /** * Queues a refresh between two rows (inclusive), to be done on next animation @@ -197,7 +168,7 @@ export class Renderer { } this._refreshRowsQueue = []; this._refreshAnimationFrame = null; - this._refresh(start, end); + // this._refresh(start, end); for (let i = 0; i < this._renderLayers.length; i++) { this._renderLayers[i].render(this._terminal, start, end); } @@ -225,107 +196,10 @@ export class Renderer { * @param {number} start The row to start from (between 0 and terminal's height terminal - 1) * @param {number} end The row to end at (between fromRow and terminal's height terminal - 1) */ - private _refresh(start: number, end: number): void { - const scaledCharWidth = Math.ceil(this._terminal.charMeasure.width) * window.devicePixelRatio; - const scaledCharHeight = Math.ceil(this._terminal.charMeasure.height) * window.devicePixelRatio; - const textCtx = this._textCanvasContext; - - // TODO: Needs to react to terminal resize - // Initialize image data - if (!this._imageData) { - this._imageData = textCtx.createImageData(scaledCharWidth * this._terminal.cols * window.devicePixelRatio, scaledCharHeight * this._terminal.rows * window.devicePixelRatio); - this._imageData.data.set(createBackgroundFillData(this._imageData.width, this._imageData.height, 255, 0, 0, 255)); - } - - // Don't bother rendering until the atlas bitmap is ready - if (!this._charAtlasBitmap) { - return; - } - - // console.log('fill', start, end); - // console.log('fill', start * charHeight, (end - start + 1) * charHeight); - // ctx.fillRect(0, start * charHeight, charWidth * this._terminal.cols, (end - start + 1) * charHeight); - textCtx.fillStyle = '#ffffff'; - textCtx.textBaseline = 'top'; - textCtx.font = `${16 * window.devicePixelRatio}px courier`; - - // Clear out the old data - textCtx.clearRect(0, start * scaledCharHeight, scaledCharWidth * this._terminal.cols, (end - start + 1) * scaledCharHeight); - - for (let y = start; y <= end; y++) { - let row = y + this._terminal.buffer.ydisp; - let line = this._terminal.buffer.lines.get(row); - for (let x = 0; x < this._terminal.cols; x++) { - textCtx.save(); - - let data: number = line[x][0]; - // const ch = line[x][CHAR_DATA_CHAR_INDEX]; - const code: number = line[x][3]; - - // if (ch === ' ') { - // continue; - // } - - // let bg = data & 0x1ff; - let fg = (data >> 9) & 0x1ff; - let flags = data >> 18; - - if (flags & FLAGS.BOLD) { - textCtx.font = `bold ${textCtx.font}`; - // Convert the FG color to the bold variant - if (fg < 8) { - fg += 8; - } - } - - // if (fg < 16) { - // ctx.fillStyle = this._colors[fg]; - // } else if (fg < 256) { - // // TODO: Support colors 16-255 - // } - - let colorIndex = 0; - if (fg < 16) { - colorIndex = fg + 1; - } - - // Simulate cache - // let imageData; - // let key = ch + data; - // if (key in this._imageDataCache) { - // imageData = this._imageDataCache[key]; - // } else { - // ctx.fillText(ch, x * scaledCharWidth, y * scaledCharHeight); - // if (flags & FLAGS.UNDERLINE) { - // ctx.fillRect(x * scaledCharWidth, (y + 1) * scaledCharHeight - window.devicePixelRatio, scaledCharWidth, window.devicePixelRatio); - // } - // imageData = ctx.getImageData(x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); - // this._imageDataCache[key] = imageData; - // } - // ctx.putImageData(imageData, x * scaledCharWidth, y * scaledCharHeight); - - // TODO: Try to get atlas working - // This seems too slow :( - // ctx.putImageData(this._charImageDataAtlas, x * scaledCharWidth - ch.charCodeAt(0) * scaledCharWidth, y * scaledCharHeight, ch.charCodeAt(0) * scaledCharWidth, 0, scaledCharWidth, scaledCharHeight); - - // ctx.drawImage(this._offscreenCanvas, code * scaledCharWidth, 0, scaledCharWidth, scaledCharHeight, x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); - - // ImageBitmap's draw about twice as fast as from a canvas - textCtx.drawImage(this._charAtlasBitmap, code * scaledCharWidth, colorIndex * scaledCharHeight, scaledCharWidth, scaledCharHeight, x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); - - // Always write text - // ctx.fillText(ch, x * charWidth, y * charHeight); - textCtx.restore(); - } - - - // TODO: Try to get atlas working - // ctx.putImageData(this._charImageDataAtlas, y * scaledCharWidth, y * scaledCharHeight, 65 * scaledCharWidth, 0, scaledCharWidth, scaledCharHeight); - } - - // This draws the atlas (for debugging purposes) - // ctx.putImageData(this._charImageDataAtlas, 0, 0); - } + // private _refresh(start: number, end: number): void { + // const scaledCharWidth = Math.ceil(this._terminal.charMeasure.width) * window.devicePixelRatio; + // const scaledCharHeight = Math.ceil(this._terminal.charMeasure.height) * window.devicePixelRatio; + // } /** * Refreshes the selection in the DOM. @@ -390,16 +264,16 @@ export class Renderer { // If bold is broken, we can't use it in the terminal. -function checkBoldBroken(terminalElement: HTMLElement): boolean { - const document = terminalElement.ownerDocument; - const el = document.createElement('span'); - el.innerHTML = 'hello world'; - terminalElement.appendChild(el); - const w1 = el.offsetWidth; - const h1 = el.offsetHeight; - el.style.fontWeight = 'bold'; - const w2 = el.offsetWidth; - const h2 = el.offsetHeight; - terminalElement.removeChild(el); - return w1 !== w2 || h1 !== h2; -} +// function checkBoldBroken(terminalElement: HTMLElement): boolean { +// const document = terminalElement.ownerDocument; +// const el = document.createElement('span'); +// el.innerHTML = 'hello world'; +// terminalElement.appendChild(el); +// const w1 = el.offsetWidth; +// const h1 = el.offsetHeight; +// el.style.fontWeight = 'bold'; +// const w2 = el.offsetWidth; +// const h2 = el.offsetHeight; +// terminalElement.removeChild(el); +// return w1 !== w2 || h1 !== h2; +// } diff --git a/src/renderer/BackgroundRenderLayer.ts b/src/renderer/BackgroundRenderLayer.ts index a624ef2ef8..4f8e9ea753 100644 --- a/src/renderer/BackgroundRenderLayer.ts +++ b/src/renderer/BackgroundRenderLayer.ts @@ -1,42 +1,21 @@ import { IRenderLayer } from './Interfaces'; import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; +import { TANGO_COLORS } from './Color'; export class BackgroundRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; private _ctx: CanvasRenderingContext2D; - // TODO: Pull colors into some other class - private _colors = [ - // dark: - '#2e3436', - '#cc0000', - '#4e9a06', - '#c4a000', - '#3465a4', - '#75507b', - '#06989a', - '#d3d7cf', - // bright: - '#555753', - '#ef2929', - '#8ae234', - '#fce94f', - '#729fcf', - '#ad7fa8', - '#34e2e2', - '#eeeeec' - ]; - constructor(container: HTMLElement) { this._canvas = document.createElement('canvas'); - this._canvas.classList.add('xterm-bg-canvas'); + this._canvas.classList.add('xterm-bg-layer'); this._ctx = this._canvas.getContext('2d'); this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); container.appendChild(this._canvas); } - public resize(canvasWidth: number, canvasHeight: number, charWidth: number, charHeight: number): void { + public resize(canvasWidth: number, canvasHeight: number, charWidth: number, charHeight: number, charSizeChanged: boolean): void { this._canvas.width = canvasWidth * window.devicePixelRatio; this._canvas.height = canvasHeight * window.devicePixelRatio; this._canvas.style.width = `${canvasWidth}px`; @@ -58,7 +37,7 @@ export class BackgroundRenderLayer implements IRenderLayer { // TODO: Draw background if (bg < 16) { this._ctx.save(); - this._ctx.fillStyle = this._colors[bg]; + this._ctx.fillStyle = TANGO_COLORS[bg]; this._ctx.fillRect(x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); this._ctx.restore(); } else { diff --git a/src/renderer/Color.ts b/src/renderer/Color.ts new file mode 100644 index 0000000000..250c5c50ae --- /dev/null +++ b/src/renderer/Color.ts @@ -0,0 +1,23 @@ +// TODO: Ideally colors would be exposed through some theme manager since colors +// are moving to JS. + +export const TANGO_COLORS = [ + // dark: + '#2e3436', + '#cc0000', + '#4e9a06', + '#c4a000', + '#3465a4', + '#75507b', + '#06989a', + '#d3d7cf', + // bright: + '#555753', + '#ef2929', + '#8ae234', + '#fce94f', + '#729fcf', + '#ad7fa8', + '#34e2e2', + '#eeeeec' +]; diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts new file mode 100644 index 0000000000..adf6c10a4f --- /dev/null +++ b/src/renderer/ForegroundRenderLayer.ts @@ -0,0 +1,180 @@ +import { IRenderLayer } from './Interfaces'; +import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; +import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; +import { TANGO_COLORS } from './Color'; +import { FLAGS } from './Types'; + +export class ForegroundRenderLayer implements IRenderLayer { + private _canvas: HTMLCanvasElement; + private _ctx: CanvasRenderingContext2D; + private _charAtlas: ImageBitmap; + + private _charAtlasGenerator: CharAtlasGenerator; + + constructor(container: HTMLElement) { + this._canvas = document.createElement('canvas'); + this._canvas.classList.add('xterm-fg-layer'); + this._ctx = this._canvas.getContext('2d'); + this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); + container.appendChild(this._canvas); + this._charAtlasGenerator = new CharAtlasGenerator(); + } + + public resize(canvasWidth: number, canvasHeight: number, charWidth: number, charHeight: number, charSizeChanged: boolean): void { + this._canvas.width = canvasWidth * window.devicePixelRatio; + this._canvas.height = canvasHeight * window.devicePixelRatio; + this._canvas.style.width = `${canvasWidth}px`; + this._canvas.style.height = `${canvasHeight}px`; + if (charSizeChanged) { + this._charAtlas = null; + this._charAtlasGenerator.generate(charWidth, charHeight).then(bitmap => { + this._charAtlas = bitmap; + }); + } + } + + public render(terminal: ITerminal, startRow: number, endRow: number): void { + const scaledCharWidth = Math.ceil(terminal.charMeasure.width) * window.devicePixelRatio; + const scaledCharHeight = Math.ceil(terminal.charMeasure.height) * window.devicePixelRatio; + + // TODO: Needs to react to terminal resize + // Initialize image data + // if (!this._imageData) { + // this._imageData = textCtx.createImageData(scaledCharWidth * this._terminal.cols * window.devicePixelRatio, scaledCharHeight * this._terminal.rows * window.devicePixelRatio); + // this._imageData.data.set(createBackgroundFillData(this._imageData.width, this._imageData.height, 255, 0, 0, 255)); + // } + + // TODO: Ensure that the render is eventually performed + // Don't bother render until the atlas bitmap is ready + if (!this._charAtlas) { + return; + } + + // console.log('fill', start, end); + // console.log('fill', start * charHeight, (end - start + 1) * charHeight); + // ctx.fillRect(0, start * charHeight, charWidth * this._terminal.cols, (end - start + 1) * charHeight); + this._ctx.fillStyle = '#ffffff'; + this._ctx.textBaseline = 'top'; + this._ctx.font = `${16 * window.devicePixelRatio}px courier`; + + // Clear out the old data + // TODO: This should be optimised, we don't want to rewrite every character + this._ctx.clearRect(0, startRow * scaledCharHeight, scaledCharWidth * terminal.cols, (endRow - startRow + 1) * scaledCharHeight); + + for (let y = startRow; y <= endRow; y++) { + let row = y + terminal.buffer.ydisp; + let line = terminal.buffer.lines.get(row); + for (let x = 0; x < terminal.cols; x++) { + console.log('fg', x, y); + this._ctx.save(); + + let data: number = line[x][0]; + // const ch = line[x][CHAR_DATA_CHAR_INDEX]; + const code: number = line[x][3]; + + // if (ch === ' ') { + // continue; + // } + + // let bg = data & 0x1ff; + let fg = (data >> 9) & 0x1ff; + let flags = data >> 18; + + if (flags & FLAGS.BOLD) { + this._ctx.font = `bold ${this._ctx.font}`; + // Convert the FG color to the bold variant + if (fg < 8) { + fg += 8; + } + } + + // if (fg < 16) { + // ctx.fillStyle = this._colors[fg]; + // } else if (fg < 256) { + // // TODO: Support colors 16-255 + // } + + let colorIndex = 0; + if (fg < 16) { + colorIndex = fg + 1; + } + + // Simulate cache + // let imageData; + // let key = ch + data; + // if (key in this._imageDataCache) { + // imageData = this._imageDataCache[key]; + // } else { + // ctx.fillText(ch, x * scaledCharWidth, y * scaledCharHeight); + // if (flags & FLAGS.UNDERLINE) { + // ctx.fillRect(x * scaledCharWidth, (y + 1) * scaledCharHeight - window.devicePixelRatio, scaledCharWidth, window.devicePixelRatio); + // } + // imageData = ctx.getImageData(x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); + // this._imageDataCache[key] = imageData; + // } + // ctx.putImageData(imageData, x * scaledCharWidth, y * scaledCharHeight); + + // TODO: Try to get atlas working + // This seems too slow :( + // ctx.putImageData(this._charImageDataAtlas, x * scaledCharWidth - ch.charCodeAt(0) * scaledCharWidth, y * scaledCharHeight, ch.charCodeAt(0) * scaledCharWidth, 0, scaledCharWidth, scaledCharHeight); + + // ctx.drawImage(this._offscreenCanvas, code * scaledCharWidth, 0, scaledCharWidth, scaledCharHeight, x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); + + // ImageBitmap's draw about twice as fast as from a canvas + this._ctx.drawImage(this._charAtlas, code * scaledCharWidth, colorIndex * scaledCharHeight, scaledCharWidth, scaledCharHeight, x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); + this._ctx.restore(); + } + } + + // This draws the atlas (for debugging purposes) + this._ctx.drawImage(this._charAtlas, 0, 0); + } +} + +class CharAtlasGenerator { + private _canvas: HTMLCanvasElement; + private _ctx: CanvasRenderingContext2D; + + constructor() { + this._canvas = document.createElement('canvas'); + this._ctx = this._canvas.getContext('2d'); + this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); + } + + public generate(charWidth: number, charHeight: number): Promise { + const scaledCharWidth = Math.ceil(charWidth) * window.devicePixelRatio; + const scaledCharHeight = Math.ceil(charHeight) * window.devicePixelRatio; + + this._canvas.width = 255 * scaledCharWidth; + this._canvas.height = (/*default*/1 + /*0-15*/16) * scaledCharHeight; + + this._ctx.save(); + this._ctx.fillStyle = '#ffffff'; + this._ctx.font = `${16 * window.devicePixelRatio}px courier`; + this._ctx.textBaseline = 'top'; + + // Default color + for (let i = 0; i < 256; i++) { + this._ctx.fillText(String.fromCharCode(i), i * scaledCharWidth, 0); + } + + // Colors 0-15 + for (let colorIndex = 0; colorIndex < 16; colorIndex++) { + // colors 8-15 are bold + if (colorIndex === 8) { + this._ctx.font = `bold ${this._ctx.font}`; + } + for (let i = 0; i < 256; i++) { + this._ctx.fillStyle = TANGO_COLORS[colorIndex]; + this._ctx.fillText(String.fromCharCode(i), i * scaledCharWidth, (colorIndex + 1) * scaledCharHeight); + } + } + this._ctx.restore(); + + const charAtlasImageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); + const promise = window.createImageBitmap(charAtlasImageData); + // Clear the rect while the promise is in progress + this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); + return promise; + } +} diff --git a/src/renderer/Interfaces.ts b/src/renderer/Interfaces.ts index 6e0c393362..f957252567 100644 --- a/src/renderer/Interfaces.ts +++ b/src/renderer/Interfaces.ts @@ -1,6 +1,6 @@ import { ITerminal } from '../Interfaces'; export interface IRenderLayer { - resize(canvasWidth: number, canvasHeight: number, charWidth: number, charHeight: number): void; + resize(canvasWidth: number, canvasHeight: number, charWidth: number, charHeight: number, charSizeChanged: boolean): void; render(terminal: ITerminal, startRow: number, endRow: number): void; } diff --git a/src/renderer/Types.ts b/src/renderer/Types.ts new file mode 100644 index 0000000000..09e74439f4 --- /dev/null +++ b/src/renderer/Types.ts @@ -0,0 +1,10 @@ +/** + * Flags used to render terminal text properly. + */ +export enum FLAGS { + BOLD = 1, + UNDERLINE = 2, + BLINK = 4, + INVERSE = 8, + INVISIBLE = 16 +}; diff --git a/src/xterm.css b/src/xterm.css index 321cf6d648..2ee98e54ec 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -164,11 +164,11 @@ top: 0; } -.terminal .xterm-bg-canvas { +.terminal .xterm-bg-layer { z-index: 0; } -.terminal .xterm-text-canvas { +.terminal .xterm-fg-layer { z-index: 1; } From 18e1134e619fcd55c0a668fd5f888868ab411788 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 30 Aug 2017 21:55:24 -0700 Subject: [PATCH 009/108] Tidy up --- src/RendererCanvas.ts | 112 +------------------------- src/renderer/BackgroundRenderLayer.ts | 2 +- src/renderer/ForegroundRenderLayer.ts | 12 +-- 3 files changed, 9 insertions(+), 117 deletions(-) diff --git a/src/RendererCanvas.ts b/src/RendererCanvas.ts index bf952b6c65..27832bed94 100644 --- a/src/RendererCanvas.ts +++ b/src/RendererCanvas.ts @@ -23,40 +23,13 @@ export class Renderer { private _refreshFramesSkipped = 0; private _refreshAnimationFrame = null; - // private _textCanvasElement: HTMLCanvasElement; - // private _textCanvasContext: CanvasRenderingContext2D; - // private _offscreenCanvas: HTMLCanvasElement; - // private _offscreenContext: CanvasRenderingContext2D; - private _renderLayers: IRenderLayer[]; - // private _charAtlasBitmap;//: ImageBitmap; - - // TODO: This would be better as a large texture atlas rather than a cache of ImageData objects - // private _imageDataCache = {}; - // private _imageData: ImageData; - constructor(private _terminal: ITerminal) { - // Figure out whether boldness affects - // the character width of monospace fonts. - // if (brokenBold === null) { - // brokenBold = checkBoldBroken(this._terminal.element); - // } - - // this._offscreenCanvas = document.createElement('canvas'); - // this._offscreenContext = this._offscreenCanvas.getContext('2d'); - // this._offscreenContext.scale(window.devicePixelRatio, window.devicePixelRatio); - this._renderLayers = [ new BackgroundRenderLayer(this._terminal.element), new ForegroundRenderLayer(this._terminal.element) ]; - - // this._terminal.element.appendChild(this._textCanvasElement); - - // TODO: Pull more DOM interactions into Renderer.constructor, element for - // example should be owned by Renderer (and also exposed by Terminal due to - // to established public API). } public onResize(cols: number, rows: number): void { @@ -73,51 +46,11 @@ export class Renderer { public onCharSizeChanged(charWidth: number, charHeight: number): void { const width = Math.ceil(charWidth) * this._terminal.cols; const height = Math.ceil(charHeight) * this._terminal.rows; - // this._textCanvasElement.width = width * window.devicePixelRatio; - // this._textCanvasElement.height = height * window.devicePixelRatio; - // this._textCanvasElement.style.width = `${width}px`; - // this._textCanvasElement.style.height = `${height}px`; - // this._offscreenCanvas.width = 255 * charWidth * window.devicePixelRatio; - // this._offscreenCanvas.height = (/*default*/1 + /*0-15*/16) * charHeight * window.devicePixelRatio; - // this._refreshCharImageDataAtlas(); for (let i = 0; i < this._renderLayers.length; i++) { this._renderLayers[i].resize(width, height, charWidth, charHeight, true); } } - // private _refreshCharImageDataAtlas(): void { - // const scaledCharWidth = Math.ceil(this._terminal.charMeasure.width) * window.devicePixelRatio; - // const scaledCharHeight = Math.ceil(this._terminal.charMeasure.height) * window.devicePixelRatio; - - // this._offscreenContext.save(); - // this._offscreenContext.fillStyle = '#ffffff'; - // this._offscreenContext.font = `${16 * window.devicePixelRatio}px courier`; - // this._offscreenContext.textBaseline = 'top'; - // // Default color - // for (let i = 0; i < 256; i++) { - // this._offscreenContext.fillText(String.fromCharCode(i), i * scaledCharWidth, 0); - // } - // // Colors 0-15 - // for (let colorIndex = 0; colorIndex < 16; colorIndex++) { - // // colors 8-15 are bold - // if (colorIndex === 8) { - // this._offscreenContext.font = `bold ${this._offscreenContext.font}`; - // } - // for (let i = 0; i < 256; i++) { - // this._offscreenContext.fillStyle = this._colors[colorIndex]; - // this._offscreenContext.fillText(String.fromCharCode(i), i * scaledCharWidth, (colorIndex + 1) * scaledCharHeight); - // } - // } - // this._offscreenContext.restore(); - - // const charAtlasImageData = this._offscreenContext.getImageData(0, 0, this._offscreenCanvas.width, this._offscreenCanvas.height); - // (window).createImageBitmap(charAtlasImageData).then(bitmap => { - // this._charAtlasBitmap = bitmap; - // }); - - // this._offscreenContext.clearRect(0, 0, this._offscreenCanvas.width, this._offscreenCanvas.height); - // } - /** * Queues a refresh between two rows (inclusive), to be done on next animation * frame. @@ -168,39 +101,14 @@ export class Renderer { } this._refreshRowsQueue = []; this._refreshAnimationFrame = null; - // this._refresh(start, end); + + // Render for (let i = 0; i < this._renderLayers.length; i++) { this._renderLayers[i].render(this._terminal, start, end); } this._terminal.emit('refresh', {start, end}); } - /** - * Refreshes (re-renders) terminal content within two rows (inclusive) - * - * Rendering Engine: - * - * In the screen buffer, each character is stored as a an array with a character - * and a 32-bit integer: - * - First value: a utf-16 character. - * - Second value: - * - Next 9 bits: background color (0-511). - * - Next 9 bits: foreground color (0-511). - * - Next 14 bits: a mask for misc. flags: - * - 1=bold - * - 2=underline - * - 4=blink - * - 8=inverse - * - 16=invisible - * - * @param {number} start The row to start from (between 0 and terminal's height terminal - 1) - * @param {number} end The row to end at (between fromRow and terminal's height terminal - 1) - */ - // private _refresh(start: number, end: number): void { - // const scaledCharWidth = Math.ceil(this._terminal.charMeasure.width) * window.devicePixelRatio; - // const scaledCharHeight = Math.ceil(this._terminal.charMeasure.height) * window.devicePixelRatio; - // } - /** * Refreshes the selection in the DOM. * @param start The selection start. @@ -261,19 +169,3 @@ export class Renderer { return element; } } - - -// If bold is broken, we can't use it in the terminal. -// function checkBoldBroken(terminalElement: HTMLElement): boolean { -// const document = terminalElement.ownerDocument; -// const el = document.createElement('span'); -// el.innerHTML = 'hello world'; -// terminalElement.appendChild(el); -// const w1 = el.offsetWidth; -// const h1 = el.offsetHeight; -// el.style.fontWeight = 'bold'; -// const w2 = el.offsetWidth; -// const h2 = el.offsetHeight; -// terminalElement.removeChild(el); -// return w1 !== w2 || h1 !== h2; -// } diff --git a/src/renderer/BackgroundRenderLayer.ts b/src/renderer/BackgroundRenderLayer.ts index 4f8e9ea753..b283ce1580 100644 --- a/src/renderer/BackgroundRenderLayer.ts +++ b/src/renderer/BackgroundRenderLayer.ts @@ -34,13 +34,13 @@ export class BackgroundRenderLayer implements IRenderLayer { const bg = data & 0x1ff; const flags = data >> 18; - // TODO: Draw background if (bg < 16) { this._ctx.save(); this._ctx.fillStyle = TANGO_COLORS[bg]; this._ctx.fillRect(x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); this._ctx.restore(); } else { + // TODO: Only clear if needed this._ctx.clearRect(x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); } } diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index adf6c10a4f..d3b1fb0587 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -50,9 +50,6 @@ export class ForegroundRenderLayer implements IRenderLayer { return; } - // console.log('fill', start, end); - // console.log('fill', start * charHeight, (end - start + 1) * charHeight); - // ctx.fillRect(0, start * charHeight, charWidth * this._terminal.cols, (end - start + 1) * charHeight); this._ctx.fillStyle = '#ffffff'; this._ctx.textBaseline = 'top'; this._ctx.font = `${16 * window.devicePixelRatio}px courier`; @@ -65,7 +62,6 @@ export class ForegroundRenderLayer implements IRenderLayer { let row = y + terminal.buffer.ydisp; let line = terminal.buffer.lines.get(row); for (let x = 0; x < terminal.cols; x++) { - console.log('fg', x, y); this._ctx.save(); let data: number = line[x][0]; @@ -127,7 +123,7 @@ export class ForegroundRenderLayer implements IRenderLayer { } // This draws the atlas (for debugging purposes) - this._ctx.drawImage(this._charAtlas, 0, 0); + // this._ctx.drawImage(this._charAtlas, 0, 0); } } @@ -164,9 +160,13 @@ class CharAtlasGenerator { if (colorIndex === 8) { this._ctx.font = `bold ${this._ctx.font}`; } + const y = (colorIndex + 1) * scaledCharHeight; + // Clear rectangle as some fonts seem to draw over the bottom boundary + this._ctx.clearRect(0, y, this._canvas.width, scaledCharHeight); + // Draw ascii characters for (let i = 0; i < 256; i++) { this._ctx.fillStyle = TANGO_COLORS[colorIndex]; - this._ctx.fillText(String.fromCharCode(i), i * scaledCharWidth, (colorIndex + 1) * scaledCharHeight); + this._ctx.fillText(String.fromCharCode(i), i * scaledCharWidth, y); } } this._ctx.restore(); From 129633a66d68d5d3cd9f349539342c1e16885c5e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 30 Aug 2017 22:09:25 -0700 Subject: [PATCH 010/108] Only draw background when needed --- src/RendererCanvas.ts | 11 +++----- src/renderer/BackgroundRenderLayer.ts | 37 ++++++++++++++++++++------- src/renderer/ForegroundRenderLayer.ts | 4 +-- src/renderer/Interfaces.ts | 2 +- 4 files changed, 35 insertions(+), 19 deletions(-) diff --git a/src/RendererCanvas.ts b/src/RendererCanvas.ts index 27832bed94..bca5f70e08 100644 --- a/src/RendererCanvas.ts +++ b/src/RendererCanvas.ts @@ -33,13 +33,10 @@ export class Renderer { } public onResize(cols: number, rows: number): void { - // TODO: This could be triggered immediately after onCharSizeChanged? - const charWidth = this._terminal.charMeasure.width; - const charHeight = this._terminal.charMeasure.height; - const width = Math.ceil(charWidth) * this._terminal.cols; - const height = Math.ceil(charHeight) * this._terminal.rows; + const width = Math.ceil(this._terminal.charMeasure.width) * this._terminal.cols; + const height = Math.ceil(this._terminal.charMeasure.height) * this._terminal.rows; for (let i = 0; i < this._renderLayers.length; i++) { - this._renderLayers[i].resize(width, height, charWidth, charHeight, false); + this._renderLayers[i].resize(this._terminal, width, height, false); } } @@ -47,7 +44,7 @@ export class Renderer { const width = Math.ceil(charWidth) * this._terminal.cols; const height = Math.ceil(charHeight) * this._terminal.rows; for (let i = 0; i < this._renderLayers.length; i++) { - this._renderLayers[i].resize(width, height, charWidth, charHeight, true); + this._renderLayers[i].resize(this._terminal, width, height, true); } } diff --git a/src/renderer/BackgroundRenderLayer.ts b/src/renderer/BackgroundRenderLayer.ts index b283ce1580..c1d9eac063 100644 --- a/src/renderer/BackgroundRenderLayer.ts +++ b/src/renderer/BackgroundRenderLayer.ts @@ -6,6 +6,7 @@ import { TANGO_COLORS } from './Color'; export class BackgroundRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; private _ctx: CanvasRenderingContext2D; + private _currentState: number[][]; constructor(container: HTMLElement) { this._canvas = document.createElement('canvas'); @@ -13,13 +14,27 @@ export class BackgroundRenderLayer implements IRenderLayer { this._ctx = this._canvas.getContext('2d'); this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); container.appendChild(this._canvas); + + this._currentState = []; } - public resize(canvasWidth: number, canvasHeight: number, charWidth: number, charHeight: number, charSizeChanged: boolean): void { + public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void { this._canvas.width = canvasWidth * window.devicePixelRatio; this._canvas.height = canvasHeight * window.devicePixelRatio; this._canvas.style.width = `${canvasWidth}px`; this._canvas.style.height = `${canvasHeight}px`; + // Initialize current state grid + this._currentState = []; + for (let y = 0; y < terminal.rows; y++) { + if (this._currentState.length <= y) { + this._currentState.push([]); + } + for (let x = this._currentState[y].length; x < terminal.cols; x++) { + this._currentState[y].push(null); + } + this._currentState[y].length = terminal.cols; + } + this._currentState.length = terminal.rows; } public render(terminal: ITerminal, startRow: number, endRow: number): void { @@ -34,14 +49,18 @@ export class BackgroundRenderLayer implements IRenderLayer { const bg = data & 0x1ff; const flags = data >> 18; - if (bg < 16) { - this._ctx.save(); - this._ctx.fillStyle = TANGO_COLORS[bg]; - this._ctx.fillRect(x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); - this._ctx.restore(); - } else { - // TODO: Only clear if needed - this._ctx.clearRect(x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); + const needsRefresh = (bg < 16 && this._currentState[y][x] !== bg) || this._currentState[y][x] !== null; + if (needsRefresh) { + if (bg < 16) { + this._ctx.save(); + this._ctx.fillStyle = TANGO_COLORS[bg]; + this._ctx.fillRect(x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); + this._ctx.restore(); + this._currentState[y][x] = bg; + } else { + this._ctx.clearRect(x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); + this._currentState[y][x] = null; + } } } } diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index d3b1fb0587..da0519bda8 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -20,14 +20,14 @@ export class ForegroundRenderLayer implements IRenderLayer { this._charAtlasGenerator = new CharAtlasGenerator(); } - public resize(canvasWidth: number, canvasHeight: number, charWidth: number, charHeight: number, charSizeChanged: boolean): void { + public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void { this._canvas.width = canvasWidth * window.devicePixelRatio; this._canvas.height = canvasHeight * window.devicePixelRatio; this._canvas.style.width = `${canvasWidth}px`; this._canvas.style.height = `${canvasHeight}px`; if (charSizeChanged) { this._charAtlas = null; - this._charAtlasGenerator.generate(charWidth, charHeight).then(bitmap => { + this._charAtlasGenerator.generate(terminal.charMeasure.width, terminal.charMeasure.height).then(bitmap => { this._charAtlas = bitmap; }); } diff --git a/src/renderer/Interfaces.ts b/src/renderer/Interfaces.ts index f957252567..4d2753751f 100644 --- a/src/renderer/Interfaces.ts +++ b/src/renderer/Interfaces.ts @@ -1,6 +1,6 @@ import { ITerminal } from '../Interfaces'; export interface IRenderLayer { - resize(canvasWidth: number, canvasHeight: number, charWidth: number, charHeight: number, charSizeChanged: boolean): void; + resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void; render(terminal: ITerminal, startRow: number, endRow: number): void; } From d93a80398d831d98d745540431e25af8f1d20a73 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 30 Aug 2017 22:22:41 -0700 Subject: [PATCH 011/108] Draw unicode characters --- src/Buffer.ts | 1 + src/renderer/ForegroundRenderLayer.ts | 76 ++++++++++++--------------- 2 files changed, 35 insertions(+), 42 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index e8ceebf32f..d8311f0675 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -9,6 +9,7 @@ import { LineData, CharData } from './Types'; export const CHAR_DATA_ATTR_INDEX = 0; export const CHAR_DATA_CHAR_INDEX = 1; export const CHAR_DATA_WIDTH_INDEX = 2; +export const CHAR_DATA_CODE_INDEX = 3; /** * This class represents a terminal buffer (an internal state of the terminal), where the diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index da0519bda8..2ed0bd602c 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -1,6 +1,6 @@ import { IRenderLayer } from './Interfaces'; import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; -import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; +import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../Buffer'; import { TANGO_COLORS } from './Color'; import { FLAGS } from './Types'; @@ -62,19 +62,15 @@ export class ForegroundRenderLayer implements IRenderLayer { let row = y + terminal.buffer.ydisp; let line = terminal.buffer.lines.get(row); for (let x = 0; x < terminal.cols; x++) { - this._ctx.save(); + const code: number = line[x][CHAR_DATA_CODE_INDEX]; - let data: number = line[x][0]; - // const ch = line[x][CHAR_DATA_CHAR_INDEX]; - const code: number = line[x][3]; - - // if (ch === ' ') { - // continue; - // } + if (!code) { + continue; + } - // let bg = data & 0x1ff; - let fg = (data >> 9) & 0x1ff; - let flags = data >> 18; + const attr: number = line[x][CHAR_DATA_ATTR_INDEX]; + let fg = (attr >> 9) & 0x1ff; + const flags = attr >> 18; if (flags & FLAGS.BOLD) { this._ctx.font = `bold ${this._ctx.font}`; @@ -84,47 +80,43 @@ export class ForegroundRenderLayer implements IRenderLayer { } } - // if (fg < 16) { - // ctx.fillStyle = this._colors[fg]; - // } else if (fg < 256) { - // // TODO: Support colors 16-255 - // } - let colorIndex = 0; if (fg < 16) { colorIndex = fg + 1; } - // Simulate cache - // let imageData; - // let key = ch + data; - // if (key in this._imageDataCache) { - // imageData = this._imageDataCache[key]; - // } else { - // ctx.fillText(ch, x * scaledCharWidth, y * scaledCharHeight); - // if (flags & FLAGS.UNDERLINE) { - // ctx.fillRect(x * scaledCharWidth, (y + 1) * scaledCharHeight - window.devicePixelRatio, scaledCharWidth, window.devicePixelRatio); - // } - // imageData = ctx.getImageData(x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); - // this._imageDataCache[key] = imageData; - // } - // ctx.putImageData(imageData, x * scaledCharWidth, y * scaledCharHeight); - - // TODO: Try to get atlas working - // This seems too slow :( - // ctx.putImageData(this._charImageDataAtlas, x * scaledCharWidth - ch.charCodeAt(0) * scaledCharWidth, y * scaledCharHeight, ch.charCodeAt(0) * scaledCharWidth, 0, scaledCharWidth, scaledCharHeight); - - // ctx.drawImage(this._offscreenCanvas, code * scaledCharWidth, 0, scaledCharWidth, scaledCharHeight, x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); - - // ImageBitmap's draw about twice as fast as from a canvas - this._ctx.drawImage(this._charAtlas, code * scaledCharWidth, colorIndex * scaledCharHeight, scaledCharWidth, scaledCharHeight, x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); - this._ctx.restore(); + if (code < 256) { + // ImageBitmap's draw about twice as fast as from a canvas + this._ctx.drawImage(this._charAtlas, code * scaledCharWidth, colorIndex * scaledCharHeight, scaledCharWidth, scaledCharHeight, x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); + } else { + // TODO: Evaluate how long it takes to convert from a number + const char: string = line[x][CHAR_DATA_CHAR_INDEX]; + const width: number = line[x][CHAR_DATA_WIDTH_INDEX]; + this._drawUnicodeChar(char, width, fg, x, y, scaledCharWidth, scaledCharHeight); + } } } // This draws the atlas (for debugging purposes) // this._ctx.drawImage(this._charAtlas, 0, 0); } + + private _drawUnicodeChar(char: string, width: number, fg: number, x: number, y: number, scaledCharWidth: number, scaledCharHeight: number) { + this._ctx.save(); + + this._ctx.font = `${16 * window.devicePixelRatio}px courier`; + this._ctx.textBaseline = 'top'; + + if (fg < 16) { + this._ctx.fillStyle = TANGO_COLORS[fg]; + } else { + this._ctx.fillStyle = '#ffffff'; + } + + // TODO: Do we care about width for rendering wide chars? + this._ctx.fillText(char, x * scaledCharWidth, y * scaledCharHeight); + this._ctx.restore(); + } } class CharAtlasGenerator { From 6a0b9bd209ada7d8355e577b0aa9fb19c7da168a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 30 Aug 2017 22:55:17 -0700 Subject: [PATCH 012/108] Only render fg cells if there are changes --- src/Types.ts | 1 + src/renderer/BackgroundRenderLayer.ts | 26 +++++++------------- src/renderer/ForegroundRenderLayer.ts | 34 +++++++++++++++++++-------- src/renderer/GridCache.ts | 20 ++++++++++++++++ 4 files changed, 53 insertions(+), 28 deletions(-) create mode 100644 src/renderer/GridCache.ts diff --git a/src/Types.ts b/src/Types.ts index 0753d80e19..180dc5d67c 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -16,5 +16,6 @@ export type LinkMatcherValidationCallback = (uri: string, element: HTMLElement, export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean; export type Charset = {[key: string]: string}; +// TODO: Add code here? export type CharData = [number, string, number]; export type LineData = CharData[]; diff --git a/src/renderer/BackgroundRenderLayer.ts b/src/renderer/BackgroundRenderLayer.ts index c1d9eac063..8577f58d6e 100644 --- a/src/renderer/BackgroundRenderLayer.ts +++ b/src/renderer/BackgroundRenderLayer.ts @@ -2,11 +2,12 @@ import { IRenderLayer } from './Interfaces'; import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; import { TANGO_COLORS } from './Color'; +import { GridCache } from './GridCache'; export class BackgroundRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; private _ctx: CanvasRenderingContext2D; - private _currentState: number[][]; + private _state: GridCache; constructor(container: HTMLElement) { this._canvas = document.createElement('canvas'); @@ -14,8 +15,7 @@ export class BackgroundRenderLayer implements IRenderLayer { this._ctx = this._canvas.getContext('2d'); this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); container.appendChild(this._canvas); - - this._currentState = []; + this._state = new GridCache(); } public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void { @@ -23,18 +23,7 @@ export class BackgroundRenderLayer implements IRenderLayer { this._canvas.height = canvasHeight * window.devicePixelRatio; this._canvas.style.width = `${canvasWidth}px`; this._canvas.style.height = `${canvasHeight}px`; - // Initialize current state grid - this._currentState = []; - for (let y = 0; y < terminal.rows; y++) { - if (this._currentState.length <= y) { - this._currentState.push([]); - } - for (let x = this._currentState[y].length; x < terminal.cols; x++) { - this._currentState[y].push(null); - } - this._currentState[y].length = terminal.cols; - } - this._currentState.length = terminal.rows; + this._state.resize(terminal.cols, terminal.rows); } public render(terminal: ITerminal, startRow: number, endRow: number): void { @@ -49,17 +38,18 @@ export class BackgroundRenderLayer implements IRenderLayer { const bg = data & 0x1ff; const flags = data >> 18; - const needsRefresh = (bg < 16 && this._currentState[y][x] !== bg) || this._currentState[y][x] !== null; + const cellState = this._state.cache[x][y]; + const needsRefresh = (bg < 16 && cellState !== bg) || cellState !== null; if (needsRefresh) { if (bg < 16) { this._ctx.save(); this._ctx.fillStyle = TANGO_COLORS[bg]; this._ctx.fillRect(x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); this._ctx.restore(); - this._currentState[y][x] = bg; + this._state.cache[x][y] = bg; } else { this._ctx.clearRect(x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); - this._currentState[y][x] = null; + this._state.cache[x][y] = null; } } } diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index 2ed0bd602c..c626324737 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -3,11 +3,14 @@ import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../Buffer'; import { TANGO_COLORS } from './Color'; import { FLAGS } from './Types'; +import { GridCache } from './GridCache'; +import { CharData } from '../Types'; export class ForegroundRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; private _ctx: CanvasRenderingContext2D; private _charAtlas: ImageBitmap; + private _state: GridCache; private _charAtlasGenerator: CharAtlasGenerator; @@ -18,6 +21,7 @@ export class ForegroundRenderLayer implements IRenderLayer { this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); container.appendChild(this._canvas); this._charAtlasGenerator = new CharAtlasGenerator(); + this._state = new GridCache(); } public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void { @@ -25,6 +29,7 @@ export class ForegroundRenderLayer implements IRenderLayer { this._canvas.height = canvasHeight * window.devicePixelRatio; this._canvas.style.width = `${canvasWidth}px`; this._canvas.style.height = `${canvasHeight}px`; + this._state.resize(terminal.cols, terminal.rows); if (charSizeChanged) { this._charAtlas = null; this._charAtlasGenerator.generate(terminal.charMeasure.width, terminal.charMeasure.height).then(bitmap => { @@ -54,21 +59,32 @@ export class ForegroundRenderLayer implements IRenderLayer { this._ctx.textBaseline = 'top'; this._ctx.font = `${16 * window.devicePixelRatio}px courier`; - // Clear out the old data - // TODO: This should be optimised, we don't want to rewrite every character - this._ctx.clearRect(0, startRow * scaledCharHeight, scaledCharWidth * terminal.cols, (endRow - startRow + 1) * scaledCharHeight); - for (let y = startRow; y <= endRow; y++) { let row = y + terminal.buffer.ydisp; let line = terminal.buffer.lines.get(row); for (let x = 0; x < terminal.cols; x++) { - const code: number = line[x][CHAR_DATA_CODE_INDEX]; + 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]; + + // Skip rendering if the character is identical + const state = this._state.cache[x][y]; + if (state && state[CHAR_DATA_CHAR_INDEX] === char && state[CHAR_DATA_ATTR_INDEX] === attr) { + // Skip render, contents are identical + this._state.cache[x][y] = charData; + continue; + } + this._state.cache[x][y] = charData; - if (!code) { + // Clear the old character + this._ctx.clearRect(x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); + + // Skip rendering if the character is invisible + if (!code || code === 32/*' '*/) { continue; } - const attr: number = line[x][CHAR_DATA_ATTR_INDEX]; let fg = (attr >> 9) & 0x1ff; const flags = attr >> 18; @@ -90,8 +106,7 @@ export class ForegroundRenderLayer implements IRenderLayer { this._ctx.drawImage(this._charAtlas, code * scaledCharWidth, colorIndex * scaledCharHeight, scaledCharWidth, scaledCharHeight, x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); } else { // TODO: Evaluate how long it takes to convert from a number - const char: string = line[x][CHAR_DATA_CHAR_INDEX]; - const width: number = line[x][CHAR_DATA_WIDTH_INDEX]; + const width: number = charData[CHAR_DATA_WIDTH_INDEX]; this._drawUnicodeChar(char, width, fg, x, y, scaledCharWidth, scaledCharHeight); } } @@ -103,7 +118,6 @@ export class ForegroundRenderLayer implements IRenderLayer { private _drawUnicodeChar(char: string, width: number, fg: number, x: number, y: number, scaledCharWidth: number, scaledCharHeight: number) { this._ctx.save(); - this._ctx.font = `${16 * window.devicePixelRatio}px courier`; this._ctx.textBaseline = 'top'; diff --git a/src/renderer/GridCache.ts b/src/renderer/GridCache.ts new file mode 100644 index 0000000000..eba3dad6a7 --- /dev/null +++ b/src/renderer/GridCache.ts @@ -0,0 +1,20 @@ +export class GridCache { + public cache: T[][]; + + public constructor() { + this.cache = []; + } + + public resize(width: number, height: number) { + for (let x = 0; x < width; x++) { + if (this.cache.length <= x) { + this.cache.push([]); + } + for (let y = this.cache[x].length; y < height; y++) { + this.cache[x].push(null); + } + this.cache[x].length = height; + } + this.cache.length = width; + } +} From 1291dc5571947241985538e134ba61129881d218 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 30 Aug 2017 23:05:51 -0700 Subject: [PATCH 013/108] Support inverse --- src/renderer/BackgroundRenderLayer.ts | 17 ++++++++++++++--- src/renderer/ForegroundRenderLayer.ts | 20 +++++++++++--------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/renderer/BackgroundRenderLayer.ts b/src/renderer/BackgroundRenderLayer.ts index 8577f58d6e..ea7bea60ac 100644 --- a/src/renderer/BackgroundRenderLayer.ts +++ b/src/renderer/BackgroundRenderLayer.ts @@ -3,6 +3,7 @@ import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; import { TANGO_COLORS } from './Color'; import { GridCache } from './GridCache'; +import { FLAGS } from './Types'; export class BackgroundRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; @@ -34,9 +35,19 @@ export class BackgroundRenderLayer implements IRenderLayer { let row = y + terminal.buffer.ydisp; let line = terminal.buffer.lines.get(row); for (let x = 0; x < terminal.cols; x++) { - const data: number = line[x][CHAR_DATA_ATTR_INDEX]; - const bg = data & 0x1ff; - const flags = data >> 18; + const attr: number = line[x][CHAR_DATA_ATTR_INDEX]; + let bg = attr & 0x1ff; + const flags = attr >> 18; + + + // If inverse flag is on, the background should become the foreground. + if (flags & FLAGS.INVERSE) { + bg = (attr >> 9) & 0x1ff; + // TODO: Is this case still needed + if (bg === 257) { + bg = 15; + } + } const cellState = this._state.cache[x][y]; const needsRefresh = (bg < 16 && cellState !== bg) || cellState !== null; diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index c626324737..05cceb47de 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -42,13 +42,6 @@ export class ForegroundRenderLayer implements IRenderLayer { const scaledCharWidth = Math.ceil(terminal.charMeasure.width) * window.devicePixelRatio; const scaledCharHeight = Math.ceil(terminal.charMeasure.height) * window.devicePixelRatio; - // TODO: Needs to react to terminal resize - // Initialize image data - // if (!this._imageData) { - // this._imageData = textCtx.createImageData(scaledCharWidth * this._terminal.cols * window.devicePixelRatio, scaledCharHeight * this._terminal.rows * window.devicePixelRatio); - // this._imageData.data.set(createBackgroundFillData(this._imageData.width, this._imageData.height, 255, 0, 0, 255)); - // } - // TODO: Ensure that the render is eventually performed // Don't bother render until the atlas bitmap is ready if (!this._charAtlas) { @@ -81,13 +74,22 @@ export class ForegroundRenderLayer implements IRenderLayer { this._ctx.clearRect(x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); // Skip rendering if the character is invisible - if (!code || code === 32/*' '*/) { + if (!code || code === 32 /*' '*/) { continue; } let fg = (attr >> 9) & 0x1ff; const flags = attr >> 18; + // If inverse flag is on, the foreground should become the background. + if (flags & FLAGS.INVERSE) { + fg = attr & 0x1ff; + // TODO: Is this case still needed + if (fg === 257) { + fg = 0; + } + } + if (flags & FLAGS.BOLD) { this._ctx.font = `bold ${this._ctx.font}`; // Convert the FG color to the bold variant @@ -116,7 +118,7 @@ export class ForegroundRenderLayer implements IRenderLayer { // this._ctx.drawImage(this._charAtlas, 0, 0); } - private _drawUnicodeChar(char: string, width: number, fg: number, x: number, y: number, scaledCharWidth: number, scaledCharHeight: number) { + private _drawUnicodeChar(char: string, width: number, fg: number, x: number, y: number, scaledCharWidth: number, scaledCharHeight: number): void { this._ctx.save(); this._ctx.font = `${16 * window.devicePixelRatio}px courier`; this._ctx.textBaseline = 'top'; From a224ed7cc7b8ec1770efe315e8ed1806a75d7b2c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 30 Aug 2017 23:06:55 -0700 Subject: [PATCH 014/108] Move canvas Renderer into folder --- src/Terminal.ts | 2 +- src/{RendererCanvas.ts => renderer/Renderer.ts} | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) rename src/{RendererCanvas.ts => renderer/Renderer.ts} (93%) diff --git a/src/Terminal.ts b/src/Terminal.ts index c076d41066..86c18a4112 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -30,7 +30,7 @@ import { C0 } from './EscapeSequences'; import { InputHandler } from './InputHandler'; import { Parser } from './Parser'; // import { Renderer } from './Renderer'; -import { Renderer } from './RendererCanvas'; +import { Renderer } from './renderer/Renderer'; import { Linkifier } from './Linkifier'; import { SelectionManager } from './SelectionManager'; import { CharMeasure } from './utils/CharMeasure'; diff --git a/src/RendererCanvas.ts b/src/renderer/Renderer.ts similarity index 93% rename from src/RendererCanvas.ts rename to src/renderer/Renderer.ts index bca5f70e08..da1ca14d5d 100644 --- a/src/RendererCanvas.ts +++ b/src/renderer/Renderer.ts @@ -2,13 +2,13 @@ * @license MIT */ -import { ITerminal } from './Interfaces'; -import { DomElementObjectPool } from './utils/DomElementObjectPool'; -import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer'; -import { createBackgroundFillData } from './renderer/Canvas'; -import { IRenderLayer } from './renderer/Interfaces'; -import { BackgroundRenderLayer } from './renderer/BackgroundRenderLayer'; -import { ForegroundRenderLayer } from './renderer/ForegroundRenderLayer'; +import { ITerminal } from '../Interfaces'; +import { DomElementObjectPool } from '../utils/DomElementObjectPool'; +import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; +import { createBackgroundFillData } from './Canvas'; +import { IRenderLayer } from './Interfaces'; +import { BackgroundRenderLayer } from './BackgroundRenderLayer'; +import { ForegroundRenderLayer } from './ForegroundRenderLayer'; /** * The maximum number of refresh frames to skip when the write buffer is non- From 7aff45c2dc19f55779ceeaaee78fc8956db78ab2 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 30 Aug 2017 23:39:33 -0700 Subject: [PATCH 015/108] Remove additional frame skipping as rendering is so fast now --- src/renderer/Renderer.ts | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index da1ca14d5d..61a4b1106e 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -10,17 +10,9 @@ import { IRenderLayer } from './Interfaces'; import { BackgroundRenderLayer } from './BackgroundRenderLayer'; import { ForegroundRenderLayer } from './ForegroundRenderLayer'; -/** - * The maximum number of refresh frames to skip when the write buffer is non- - * empty. Note that these frames may be intermingled with frames that are - * skipped via requestAnimationFrame's mechanism. - */ -const MAX_REFRESH_FRAME_SKIP = 5; - export class Renderer { /** A queue of the rows to be refreshed */ private _refreshRowsQueue: {start: number, end: number}[] = []; - private _refreshFramesSkipped = 0; private _refreshAnimationFrame = null; private _renderLayers: IRenderLayer[]; @@ -66,17 +58,6 @@ export class Renderer { * necessary before queueing up the next one. */ private _refreshLoop(): void { - // Skip MAX_REFRESH_FRAME_SKIP frames if the writeBuffer is non-empty as it - // will need to be immediately refreshed anyway. This saves a lot of - // rendering time as the viewport DOM does not need to be refreshed, no - // scroll events, no layouts, etc. - const skipFrame = this._terminal.writeBuffer.length > 0 && this._refreshFramesSkipped++ <= MAX_REFRESH_FRAME_SKIP; - if (skipFrame) { - this._refreshAnimationFrame = window.requestAnimationFrame(this._refreshLoop.bind(this)); - return; - } - - this._refreshFramesSkipped = 0; let start; let end; if (this._refreshRowsQueue.length > 4) { From 5953bbab42ffa956d122aef0a1d5a9c554386d64 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 30 Aug 2017 23:44:52 -0700 Subject: [PATCH 016/108] Start SelectionRenderLayer --- src/Terminal.ts | 6 +- src/renderer/BackgroundRenderLayer.ts | 1 - src/renderer/ForegroundRenderLayer.ts | 1 + src/renderer/SelectionRenderLayer.ts | 83 +++++++++++++++++++++++++++ src/renderer/Types.ts | 5 ++ 5 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 src/renderer/SelectionRenderLayer.ts diff --git a/src/Terminal.ts b/src/Terminal.ts index 86c18a4112..594a6fb2cb 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -752,9 +752,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.on('resize', () => this.renderer.onResize(this.cols, this.rows)); this.charMeasure.on('charsizechanged', () => this.renderer.onCharSizeChanged(this.charMeasure.width, this.charMeasure.height)); this.selectionManager = new SelectionManager(this, this.buffer, this.rowContainer, this.charMeasure); - this.selectionManager.on('refresh', data => { - this.renderer.refreshSelection(data.start, data.end); - }); + // this.selectionManager.on('refresh', data => { + // this.renderer.refreshSelection(data.start, data.end); + // }); this.selectionManager.on('newselection', text => { // If there's a new selection, put it into the textarea, focus and select it // in order to register it as a selection on the OS. This event is fired diff --git a/src/renderer/BackgroundRenderLayer.ts b/src/renderer/BackgroundRenderLayer.ts index ea7bea60ac..dac476a073 100644 --- a/src/renderer/BackgroundRenderLayer.ts +++ b/src/renderer/BackgroundRenderLayer.ts @@ -39,7 +39,6 @@ export class BackgroundRenderLayer implements IRenderLayer { let bg = attr & 0x1ff; const flags = attr >> 18; - // If inverse flag is on, the background should become the foreground. if (flags & FLAGS.INVERSE) { bg = (attr >> 9) & 0x1ff; diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index 05cceb47de..2b1ff27217 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -55,6 +55,7 @@ export class ForegroundRenderLayer implements IRenderLayer { for (let y = startRow; y <= endRow; y++) { let row = y + terminal.buffer.ydisp; let 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]; diff --git a/src/renderer/SelectionRenderLayer.ts b/src/renderer/SelectionRenderLayer.ts new file mode 100644 index 0000000000..bc31022cd1 --- /dev/null +++ b/src/renderer/SelectionRenderLayer.ts @@ -0,0 +1,83 @@ +import { IRenderLayer } from './Interfaces'; +import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; +import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; +import { TANGO_COLORS } from './Color'; +import { GridCache } from './GridCache'; +import { FLAGS } from './Types'; + +export class BackgroundRenderLayer implements IRenderLayer { + private _canvas: HTMLCanvasElement; + private _ctx: CanvasRenderingContext2D; + private _state: {start: [number, number], end: [number, number]}; + + constructor(container: HTMLElement) { + this._canvas = document.createElement('canvas'); + this._canvas.classList.add('xterm-bg-layer'); + this._ctx = this._canvas.getContext('2d'); + this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); + this._ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'; + container.appendChild(this._canvas); + this._state = { + start: null, + end: null + }; + } + + public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void { + this._canvas.width = canvasWidth * window.devicePixelRatio; + this._canvas.height = canvasHeight * window.devicePixelRatio; + this._canvas.style.width = `${canvasWidth}px`; + this._canvas.style.height = `${canvasHeight}px`; + } + + public render(terminal: ITerminal, startRow: number, endRow: number): void { + const scaledCharWidth = Math.ceil(terminal.charMeasure.width) * window.devicePixelRatio; + const scaledCharHeight = Math.ceil(terminal.charMeasure.height) * window.devicePixelRatio; + + const start = terminal.selectionManager.selectionStart; + const end = terminal.selectionManager.selectionEnd; + + // TODO: Need to redraw selection if the viewport has moved + + // Selection has not changed + if (this._state.start === start || this._state.end === end) { + return; + } + + // Remove all selections + this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); + + // Selection does not exist + if (!start || !end) { + return; + } + + // Translate from buffer position to viewport position + const viewportStartRow = start[1] - terminal.buffer.ydisp; + const viewportEndRow = end[1] - terminal.buffer.ydisp; + const viewportCappedStartRow = Math.max(viewportStartRow, 0); + const viewportCappedEndRow = Math.min(viewportEndRow, terminal.rows - 1); + + // No need to draw the selection + if (viewportCappedStartRow >= terminal.rows || viewportCappedEndRow < 0) { + return; + } + + // Create the selections + // Draw first row + const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0; + const endCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : terminal.cols; + this._ctx.fillRect(startCol * scaledCharWidth, viewportCappedStartRow * scaledCharHeight, (endCol - startCol) * scaledCharWidth, scaledCharHeight); + // 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); + } +} diff --git a/src/renderer/Types.ts b/src/renderer/Types.ts index 09e74439f4..621db0bd8e 100644 --- a/src/renderer/Types.ts +++ b/src/renderer/Types.ts @@ -8,3 +8,8 @@ export enum FLAGS { INVERSE = 8, INVISIBLE = 16 }; + +export type Point = { + x: number, + y: number +}; From 3d1bb2c60fddbc1e0b4e8b7e9585b6e859763d28 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 31 Aug 2017 00:09:23 -0700 Subject: [PATCH 017/108] Support selection --- src/SelectionManager.ts | 5 ++-- src/Terminal.ts | 6 ++-- src/renderer/BackgroundRenderLayer.ts | 4 +-- src/renderer/ForegroundRenderLayer.ts | 4 +-- src/renderer/Interfaces.ts | 7 +++++ src/renderer/Renderer.ts | 32 +++++++++++++++------ src/renderer/SelectionRenderLayer.ts | 40 ++++++++++++--------------- src/xterm.css | 10 ++----- 8 files changed, 59 insertions(+), 49 deletions(-) diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 1aaec14b00..1e3fe9b88d 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -116,8 +116,6 @@ export class SelectionManager extends EventEmitter implements ISelectionManager this._mouseMoveListener = event => this._onMouseMove(event); this._mouseUpListener = event => this._onMouseUp(event); - this._rowContainer.addEventListener('mousedown', event => this._onMouseDown(event)); - // Only adjust the selection on trim, shiftElements is rarely used (only in // reverseIndex) and delete in a splice is only ever used when the same // number of elements was just added. Given this is could actually be @@ -312,7 +310,8 @@ export class SelectionManager extends EventEmitter implements ISelectionManager * Handles te mousedown event, setting up for a new selection. * @param event The mousedown event. */ - private _onMouseDown(event: MouseEvent): void { + public onMouseDown(event: MouseEvent): void { + console.log('mousedown selectionmanager'); // If we have selection, we want the context menu on right click even if the // terminal is in mouse mode. if (event.button === 2 && this.hasSelection) { diff --git a/src/Terminal.ts b/src/Terminal.ts index 594a6fb2cb..f88ebb7f90 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -751,10 +751,10 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.renderer = new Renderer(this); this.on('resize', () => this.renderer.onResize(this.cols, this.rows)); this.charMeasure.on('charsizechanged', () => this.renderer.onCharSizeChanged(this.charMeasure.width, this.charMeasure.height)); + this.selectionManager = new SelectionManager(this, this.buffer, this.rowContainer, this.charMeasure); - // this.selectionManager.on('refresh', data => { - // this.renderer.refreshSelection(data.start, data.end); - // }); + this.element.addEventListener('mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e)); + this.selectionManager.on('refresh', data => this.renderer.onSelectionChanged(data.start, data.end)); this.selectionManager.on('newselection', text => { // If there's a new selection, put it into the textarea, focus and select it // in order to register it as a selection on the OS. This event is fired diff --git a/src/renderer/BackgroundRenderLayer.ts b/src/renderer/BackgroundRenderLayer.ts index dac476a073..162b96a4e4 100644 --- a/src/renderer/BackgroundRenderLayer.ts +++ b/src/renderer/BackgroundRenderLayer.ts @@ -1,11 +1,11 @@ -import { IRenderLayer } from './Interfaces'; +import { IDataRenderLayer } from './Interfaces'; import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; import { TANGO_COLORS } from './Color'; import { GridCache } from './GridCache'; import { FLAGS } from './Types'; -export class BackgroundRenderLayer implements IRenderLayer { +export class BackgroundRenderLayer implements IDataRenderLayer { private _canvas: HTMLCanvasElement; private _ctx: CanvasRenderingContext2D; private _state: GridCache; diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index 2b1ff27217..0f74ff2893 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -1,4 +1,4 @@ -import { IRenderLayer } from './Interfaces'; +import { IDataRenderLayer } from './Interfaces'; import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../Buffer'; import { TANGO_COLORS } from './Color'; @@ -6,7 +6,7 @@ import { FLAGS } from './Types'; import { GridCache } from './GridCache'; import { CharData } from '../Types'; -export class ForegroundRenderLayer implements IRenderLayer { +export class ForegroundRenderLayer implements IDataRenderLayer { private _canvas: HTMLCanvasElement; private _ctx: CanvasRenderingContext2D; private _charAtlas: ImageBitmap; diff --git a/src/renderer/Interfaces.ts b/src/renderer/Interfaces.ts index 4d2753751f..4d6d2d3b9f 100644 --- a/src/renderer/Interfaces.ts +++ b/src/renderer/Interfaces.ts @@ -2,5 +2,12 @@ import { ITerminal } from '../Interfaces'; export interface IRenderLayer { resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void; +} + +export interface IDataRenderLayer extends IRenderLayer { render(terminal: ITerminal, startRow: number, endRow: number): void; } + +export interface ISelectionRenderLayer extends IRenderLayer { + render(terminal: ITerminal, start: [number, number], end: [number, number]): void; +} diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 61a4b1106e..b4d9b153cc 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -6,37 +6,51 @@ import { ITerminal } from '../Interfaces'; import { DomElementObjectPool } from '../utils/DomElementObjectPool'; import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; import { createBackgroundFillData } from './Canvas'; -import { IRenderLayer } from './Interfaces'; +import { IDataRenderLayer, ISelectionRenderLayer } from './Interfaces'; import { BackgroundRenderLayer } from './BackgroundRenderLayer'; import { ForegroundRenderLayer } from './ForegroundRenderLayer'; +import { SelectionRenderLayer } from './SelectionRenderLayer'; export class Renderer { /** A queue of the rows to be refreshed */ private _refreshRowsQueue: {start: number, end: number}[] = []; private _refreshAnimationFrame = null; - private _renderLayers: IRenderLayer[]; + private _dataRenderLayers: IDataRenderLayer[]; + private _selectionRenderLayers: ISelectionRenderLayer[]; constructor(private _terminal: ITerminal) { - this._renderLayers = [ + this._dataRenderLayers = [ new BackgroundRenderLayer(this._terminal.element), new ForegroundRenderLayer(this._terminal.element) ]; + this._selectionRenderLayers = [ + new SelectionRenderLayer(this._terminal.element) + ]; } public onResize(cols: number, rows: number): void { const width = Math.ceil(this._terminal.charMeasure.width) * this._terminal.cols; const height = Math.ceil(this._terminal.charMeasure.height) * this._terminal.rows; - for (let i = 0; i < this._renderLayers.length; i++) { - this._renderLayers[i].resize(this._terminal, width, height, false); + for (let i = 0; i < this._dataRenderLayers.length; i++) { + this._dataRenderLayers[i].resize(this._terminal, width, height, false); } } public onCharSizeChanged(charWidth: number, charHeight: number): void { const width = Math.ceil(charWidth) * this._terminal.cols; const height = Math.ceil(charHeight) * this._terminal.rows; - for (let i = 0; i < this._renderLayers.length; i++) { - this._renderLayers[i].resize(this._terminal, width, height, true); + for (let i = 0; i < this._dataRenderLayers.length; i++) { + this._dataRenderLayers[i].resize(this._terminal, width, height, true); + } + for (let i = 0; i < this._selectionRenderLayers.length; i++) { + this._selectionRenderLayers[i].resize(this._terminal, width, height, true); + } + } + + public onSelectionChanged(start: [number, number], end: [number, number]): void { + for (let i = 0; i < this._selectionRenderLayers.length; i++) { + this._selectionRenderLayers[i].render(this._terminal, start, end); } } @@ -81,8 +95,8 @@ export class Renderer { this._refreshAnimationFrame = null; // Render - for (let i = 0; i < this._renderLayers.length; i++) { - this._renderLayers[i].render(this._terminal, start, end); + for (let i = 0; i < this._dataRenderLayers.length; i++) { + this._dataRenderLayers[i].render(this._terminal, start, end); } this._terminal.emit('refresh', {start, end}); } diff --git a/src/renderer/SelectionRenderLayer.ts b/src/renderer/SelectionRenderLayer.ts index bc31022cd1..b630359e69 100644 --- a/src/renderer/SelectionRenderLayer.ts +++ b/src/renderer/SelectionRenderLayer.ts @@ -1,21 +1,20 @@ -import { IRenderLayer } from './Interfaces'; +import { ISelectionRenderLayer } from './Interfaces'; import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; import { TANGO_COLORS } from './Color'; import { GridCache } from './GridCache'; import { FLAGS } from './Types'; -export class BackgroundRenderLayer implements IRenderLayer { +export class SelectionRenderLayer implements ISelectionRenderLayer { private _canvas: HTMLCanvasElement; private _ctx: CanvasRenderingContext2D; private _state: {start: [number, number], end: [number, number]}; constructor(container: HTMLElement) { this._canvas = document.createElement('canvas'); - this._canvas.classList.add('xterm-bg-layer'); + this._canvas.classList.add('xterm-selection-layer'); this._ctx = this._canvas.getContext('2d'); this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); - this._ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'; container.appendChild(this._canvas); this._state = { start: null, @@ -30,15 +29,10 @@ export class BackgroundRenderLayer implements IRenderLayer { this._canvas.style.height = `${canvasHeight}px`; } - public render(terminal: ITerminal, startRow: number, endRow: number): void { + public render(terminal: ITerminal, start: [number, number], end: [number, number]): void { const scaledCharWidth = Math.ceil(terminal.charMeasure.width) * window.devicePixelRatio; const scaledCharHeight = Math.ceil(terminal.charMeasure.height) * window.devicePixelRatio; - const start = terminal.selectionManager.selectionStart; - const end = terminal.selectionManager.selectionEnd; - - // TODO: Need to redraw selection if the viewport has moved - // Selection has not changed if (this._state.start === start || this._state.end === end) { return; @@ -63,21 +57,21 @@ export class BackgroundRenderLayer implements IRenderLayer { return; } - // Create the selections // Draw first row const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0; - const endCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : terminal.cols; - this._ctx.fillRect(startCol * scaledCharWidth, viewportCappedStartRow * scaledCharHeight, (endCol - startCol) * scaledCharWidth, scaledCharHeight); - // documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow, startCol, endCol)); + const startRowEndCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : terminal.cols; + this._ctx.fillStyle = 'rgba(255,255,255,0.3)'; + this._ctx.fillRect(startCol * scaledCharWidth, viewportCappedStartRow * scaledCharHeight, (startRowEndCol - startCol) * scaledCharWidth, scaledCharHeight); + // 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); + const middleRowsCount = Math.max(viewportCappedEndRow - viewportCappedStartRow - 1, 0); + this._ctx.fillRect(0, (viewportCappedStartRow + 1) * scaledCharHeight, terminal.cols * scaledCharWidth, middleRowsCount * scaledCharHeight); + + // Draw final row + if (viewportCappedStartRow !== viewportCappedEndRow) { + // Only draw viewportEndRow if it's not the same as viewporttartRow + const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : terminal.cols; + this._ctx.fillRect(0, viewportCappedEndRow * scaledCharHeight, endCol * scaledCharWidth, scaledCharHeight); + } } } diff --git a/src/xterm.css b/src/xterm.css index 2ee98e54ec..8a48c6f7c6 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -164,13 +164,9 @@ top: 0; } -.terminal .xterm-bg-layer { - z-index: 0; -} - -.terminal .xterm-fg-layer { - z-index: 1; -} +.terminal .xterm-bg-layer { z-index: 0; } +.terminal .xterm-fg-layer { z-index: 1; } +.terminal .xterm-selection-layer { z-index: 2; } .terminal .xterm-rows { position: absolute; From d601f0aec257d78e9312477513e01c1189afec47 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 31 Aug 2017 00:13:01 -0700 Subject: [PATCH 018/108] Hang on to selection state to avoid redrawing --- src/SelectionManager.ts | 1 - src/renderer/SelectionRenderLayer.ts | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 1e3fe9b88d..dc58858ecb 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -311,7 +311,6 @@ export class SelectionManager extends EventEmitter implements ISelectionManager * @param event The mousedown event. */ public onMouseDown(event: MouseEvent): void { - console.log('mousedown selectionmanager'); // If we have selection, we want the context menu on right click even if the // terminal is in mouse mode. if (event.button === 2 && this.hasSelection) { diff --git a/src/renderer/SelectionRenderLayer.ts b/src/renderer/SelectionRenderLayer.ts index b630359e69..b0a6a5a600 100644 --- a/src/renderer/SelectionRenderLayer.ts +++ b/src/renderer/SelectionRenderLayer.ts @@ -73,5 +73,9 @@ export class SelectionRenderLayer implements ISelectionRenderLayer { const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : terminal.cols; this._ctx.fillRect(0, viewportCappedEndRow * scaledCharHeight, endCol * scaledCharWidth, scaledCharHeight); } + + // Save state for next render + this._state.start = [start[0], start[1]]; + this._state.end = [end[0], end[1]]; } } From dff3d168317a8bc70070e9755047161d8a0fcd01 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 31 Aug 2017 00:25:52 -0700 Subject: [PATCH 019/108] Support 256 color --- src/renderer/BackgroundRenderLayer.ts | 8 +++---- src/renderer/Color.ts | 31 +++++++++++++++++++++++++++ src/renderer/ForegroundRenderLayer.ts | 15 +++++++------ 3 files changed, 43 insertions(+), 11 deletions(-) diff --git a/src/renderer/BackgroundRenderLayer.ts b/src/renderer/BackgroundRenderLayer.ts index 162b96a4e4..d63a561f09 100644 --- a/src/renderer/BackgroundRenderLayer.ts +++ b/src/renderer/BackgroundRenderLayer.ts @@ -1,7 +1,7 @@ import { IDataRenderLayer } from './Interfaces'; import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; -import { TANGO_COLORS } from './Color'; +import { COLORS } from './Color'; import { GridCache } from './GridCache'; import { FLAGS } from './Types'; @@ -49,11 +49,11 @@ export class BackgroundRenderLayer implements IDataRenderLayer { } const cellState = this._state.cache[x][y]; - const needsRefresh = (bg < 16 && cellState !== bg) || cellState !== null; + const needsRefresh = (bg < 256 && cellState !== bg) || cellState !== null; if (needsRefresh) { - if (bg < 16) { + if (bg < 256) { this._ctx.save(); - this._ctx.fillStyle = TANGO_COLORS[bg]; + this._ctx.fillStyle = COLORS[bg]; this._ctx.fillRect(x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); this._ctx.restore(); this._state.cache[x][y] = bg; diff --git a/src/renderer/Color.ts b/src/renderer/Color.ts index 250c5c50ae..4f75664c4c 100644 --- a/src/renderer/Color.ts +++ b/src/renderer/Color.ts @@ -21,3 +21,34 @@ export const TANGO_COLORS = [ '#34e2e2', '#eeeeec' ]; + +export const COLORS: string[] = (function(): string[] { + let colors = TANGO_COLORS.slice(); + let r = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]; + let i; + + // 16-231 + i = 0; + for (; i < 216; i++) { + out(r[(i / 36) % 6 | 0], r[(i / 6) % 6 | 0], r[i % 6]); + } + + // 232-255 (grey) + i = 0; + let c: number; + for (; i < 24; i++) { + c = 8 + i * 10; + out(c, c, c); + } + + function out(r: number, g: number, b: number): void { + colors.push('#' + hex(r) + hex(g) + hex(b)); + } + + function hex(c: number): string { + let s = c.toString(16); + return s.length < 2 ? '0' + s : s; + } + + return colors; +})(); diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index 0f74ff2893..fbfb1a6f93 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -1,7 +1,7 @@ import { IDataRenderLayer } from './Interfaces'; import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../Buffer'; -import { TANGO_COLORS } from './Color'; +import { COLORS } from './Color'; import { FLAGS } from './Types'; import { GridCache } from './GridCache'; import { CharData } from '../Types'; @@ -104,13 +104,13 @@ export class ForegroundRenderLayer implements IDataRenderLayer { colorIndex = fg + 1; } - if (code < 256) { + if (code < 256 && (colorIndex > 0 || fg > 255)) { // ImageBitmap's draw about twice as fast as from a canvas this._ctx.drawImage(this._charAtlas, code * scaledCharWidth, colorIndex * scaledCharHeight, scaledCharWidth, scaledCharHeight, x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); } else { // TODO: Evaluate how long it takes to convert from a number const width: number = charData[CHAR_DATA_WIDTH_INDEX]; - this._drawUnicodeChar(char, width, fg, x, y, scaledCharWidth, scaledCharHeight); + this._drawUncachedChar(char, width, fg, x, y, scaledCharWidth, scaledCharHeight); } } } @@ -119,13 +119,14 @@ export class ForegroundRenderLayer implements IDataRenderLayer { // this._ctx.drawImage(this._charAtlas, 0, 0); } - private _drawUnicodeChar(char: string, width: number, fg: number, x: number, y: number, scaledCharWidth: number, scaledCharHeight: number): void { + private _drawUncachedChar(char: string, width: number, fg: number, x: number, y: number, scaledCharWidth: number, scaledCharHeight: number): void { this._ctx.save(); this._ctx.font = `${16 * window.devicePixelRatio}px courier`; this._ctx.textBaseline = 'top'; - if (fg < 16) { - this._ctx.fillStyle = TANGO_COLORS[fg]; + // 256 color support + if (fg < 256) { + this._ctx.fillStyle = COLORS[fg]; } else { this._ctx.fillStyle = '#ffffff'; } @@ -174,7 +175,7 @@ class CharAtlasGenerator { this._ctx.clearRect(0, y, this._canvas.width, scaledCharHeight); // Draw ascii characters for (let i = 0; i < 256; i++) { - this._ctx.fillStyle = TANGO_COLORS[colorIndex]; + this._ctx.fillStyle = COLORS[colorIndex]; this._ctx.fillText(String.fromCharCode(i), i * scaledCharWidth, y); } } From d3650d9d8068df292560d2e38734e441b309a0cf Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 31 Aug 2017 07:54:16 -0700 Subject: [PATCH 020/108] Draw selection underneath foreground --- src/renderer/Color.ts | 2 +- src/renderer/SelectionRenderLayer.ts | 1 - src/xterm.css | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/renderer/Color.ts b/src/renderer/Color.ts index 4f75664c4c..eb3954eae0 100644 --- a/src/renderer/Color.ts +++ b/src/renderer/Color.ts @@ -1,7 +1,7 @@ // TODO: Ideally colors would be exposed through some theme manager since colors // are moving to JS. -export const TANGO_COLORS = [ +const TANGO_COLORS = [ // dark: '#2e3436', '#cc0000', diff --git a/src/renderer/SelectionRenderLayer.ts b/src/renderer/SelectionRenderLayer.ts index b0a6a5a600..091788f928 100644 --- a/src/renderer/SelectionRenderLayer.ts +++ b/src/renderer/SelectionRenderLayer.ts @@ -1,7 +1,6 @@ import { ISelectionRenderLayer } from './Interfaces'; import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; -import { TANGO_COLORS } from './Color'; import { GridCache } from './GridCache'; import { FLAGS } from './Types'; diff --git a/src/xterm.css b/src/xterm.css index 8a48c6f7c6..dcbd6b6947 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -165,8 +165,8 @@ } .terminal .xterm-bg-layer { z-index: 0; } -.terminal .xterm-fg-layer { z-index: 1; } -.terminal .xterm-selection-layer { z-index: 2; } +.terminal .xterm-selection-layer { z-index: 1; } +.terminal .xterm-fg-layer { z-index: 2; } .terminal .xterm-rows { position: absolute; From fd18f7cba78843fa4094d99b339edfd0f62be4fc Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 31 Aug 2017 17:02:24 -0700 Subject: [PATCH 021/108] Support basic cursor --- src/renderer/CursorRenderLayer.ts | 75 +++++++++++++++++++++++++++ src/renderer/ForegroundRenderLayer.ts | 4 +- src/renderer/Renderer.ts | 4 +- src/xterm.css | 1 + 4 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 src/renderer/CursorRenderLayer.ts diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts new file mode 100644 index 0000000000..d9a21d15fa --- /dev/null +++ b/src/renderer/CursorRenderLayer.ts @@ -0,0 +1,75 @@ +import { IDataRenderLayer } from './Interfaces'; +import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; +import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; +import { COLORS } from './Color'; +import { GridCache } from './GridCache'; +import { FLAGS } from './Types'; + +export class CursorRenderLayer implements IDataRenderLayer { + private _canvas: HTMLCanvasElement; + private _ctx: CanvasRenderingContext2D; + private _state: [number, number]; + + constructor(container: HTMLElement) { + this._canvas = document.createElement('canvas'); + this._canvas.classList.add('xterm-cursor-layer'); + this._ctx = this._canvas.getContext('2d'); + this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); + container.appendChild(this._canvas); + this._state = null; + } + + public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void { + this._canvas.width = canvasWidth * window.devicePixelRatio; + this._canvas.height = canvasHeight * window.devicePixelRatio; + this._canvas.style.width = `${canvasWidth}px`; + this._canvas.style.height = `${canvasHeight}px`; + } + + public render(terminal: ITerminal, startRow: number, endRow: number): void { + // TODO: Track blur/focus somehow, support unfocused cursor + + const scaledCharWidth = Math.ceil(terminal.charMeasure.width) * window.devicePixelRatio; + const scaledCharHeight = Math.ceil(terminal.charMeasure.height) * window.devicePixelRatio; + + // Don't draw the cursor if it's hidden + if (!terminal.cursorState || terminal.cursorHidden) { + this._clearCursor(scaledCharWidth, scaledCharHeight); + return; + } + + const cursorY = terminal.buffer.ybase + terminal.buffer.y; + const viewportRelativeCursorY = cursorY - terminal.buffer.ydisp; + + // Don't draw the cursor if it's off-screen + if (viewportRelativeCursorY < 0 || viewportRelativeCursorY >= terminal.rows) { + this._clearCursor(scaledCharWidth, scaledCharHeight); + return; + } + + if (this._state) { + // The cursor is already in the correct spot, don't redraw + if (this._state[0] === terminal.buffer.x && this._state[1] === viewportRelativeCursorY) { + return; + } + this._clearCursor(scaledCharWidth, scaledCharHeight); + } + + // TODO: Draw text in COLORS[0], using the char atlas if possible + // const charData = terminal.buffer.lines.get(viewportRelativeCursorY)[terminal.buffer.x]; + + this._ctx.save(); + this._ctx.fillStyle = COLORS[7]; + this._ctx.fillRect(terminal.buffer.x * scaledCharWidth, viewportRelativeCursorY * scaledCharHeight, scaledCharWidth, scaledCharHeight); + this._ctx.restore(); + + this._state = [terminal.buffer.x, viewportRelativeCursorY]; + } + + private _clearCursor(scaledCharWidth: number, scaledCharHeight: number): void { + if (this._state) { + this._ctx.clearRect(this._state[0] * scaledCharWidth, this._state[1] * scaledCharHeight, scaledCharWidth, scaledCharHeight); + this._state = null; + } + } +} diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index fbfb1a6f93..db23a041ac 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -53,8 +53,8 @@ export class ForegroundRenderLayer implements IDataRenderLayer { this._ctx.font = `${16 * window.devicePixelRatio}px courier`; for (let y = startRow; y <= endRow; y++) { - let row = y + terminal.buffer.ydisp; - let line = terminal.buffer.lines.get(row); + 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]; diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index b4d9b153cc..b340afba82 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -10,6 +10,7 @@ import { IDataRenderLayer, ISelectionRenderLayer } from './Interfaces'; import { BackgroundRenderLayer } from './BackgroundRenderLayer'; import { ForegroundRenderLayer } from './ForegroundRenderLayer'; import { SelectionRenderLayer } from './SelectionRenderLayer'; +import { CursorRenderLayer } from './CursorRenderLayer'; export class Renderer { /** A queue of the rows to be refreshed */ @@ -22,7 +23,8 @@ export class Renderer { constructor(private _terminal: ITerminal) { this._dataRenderLayers = [ new BackgroundRenderLayer(this._terminal.element), - new ForegroundRenderLayer(this._terminal.element) + new ForegroundRenderLayer(this._terminal.element), + new CursorRenderLayer(this._terminal.element) ]; this._selectionRenderLayers = [ new SelectionRenderLayer(this._terminal.element) diff --git a/src/xterm.css b/src/xterm.css index dcbd6b6947..6b044217af 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -167,6 +167,7 @@ .terminal .xterm-bg-layer { z-index: 0; } .terminal .xterm-selection-layer { z-index: 1; } .terminal .xterm-fg-layer { z-index: 2; } +.terminal .xterm-cursor-layer { z-index: 3; } .terminal .xterm-rows { position: absolute; From a91380a76fdff71122ed7b25883a40f5fae9d0ca Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 31 Aug 2017 17:13:15 -0700 Subject: [PATCH 022/108] Pull common parts into BaseRenderLayer --- src/renderer/BackgroundRenderLayer.ts | 18 +++++------------- src/renderer/BaseRenderLayer.ts | 23 +++++++++++++++++++++++ src/renderer/CursorRenderLayer.ts | 20 ++++---------------- src/renderer/ForegroundRenderLayer.ts | 18 +++++------------- src/renderer/Renderer.ts | 8 ++++---- src/renderer/SelectionRenderLayer.ts | 20 ++++---------------- src/xterm.css | 4 ++-- 7 files changed, 47 insertions(+), 64 deletions(-) create mode 100644 src/renderer/BaseRenderLayer.ts diff --git a/src/renderer/BackgroundRenderLayer.ts b/src/renderer/BackgroundRenderLayer.ts index d63a561f09..a9722eabf0 100644 --- a/src/renderer/BackgroundRenderLayer.ts +++ b/src/renderer/BackgroundRenderLayer.ts @@ -4,26 +4,18 @@ import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; import { COLORS } from './Color'; import { GridCache } from './GridCache'; import { FLAGS } from './Types'; +import { BaseRenderLayer } from './BaseRenderLayer'; -export class BackgroundRenderLayer implements IDataRenderLayer { - private _canvas: HTMLCanvasElement; - private _ctx: CanvasRenderingContext2D; +export class BackgroundRenderLayer extends BaseRenderLayer implements IDataRenderLayer { private _state: GridCache; - constructor(container: HTMLElement) { - this._canvas = document.createElement('canvas'); - this._canvas.classList.add('xterm-bg-layer'); - this._ctx = this._canvas.getContext('2d'); - this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); - container.appendChild(this._canvas); + constructor(container: HTMLElement, zIndex: number) { + super(container, 'bg', zIndex); this._state = new GridCache(); } public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void { - this._canvas.width = canvasWidth * window.devicePixelRatio; - this._canvas.height = canvasHeight * window.devicePixelRatio; - this._canvas.style.width = `${canvasWidth}px`; - this._canvas.style.height = `${canvasHeight}px`; + super.resize(terminal, canvasWidth, canvasHeight, charSizeChanged); this._state.resize(terminal.cols, terminal.rows); } diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts new file mode 100644 index 0000000000..6d49396080 --- /dev/null +++ b/src/renderer/BaseRenderLayer.ts @@ -0,0 +1,23 @@ +import { IRenderLayer } from './Interfaces'; +import { ITerminal } from '../Interfaces'; + +export abstract class BaseRenderLayer implements IRenderLayer { + protected _canvas: HTMLCanvasElement; + protected _ctx: CanvasRenderingContext2D; + + constructor(container: HTMLElement, id: string, zIndex: number) { + this._canvas = document.createElement('canvas'); + this._canvas.id = `xterm-${id}-layer`; + this._canvas.style.zIndex = zIndex.toString(); + this._ctx = this._canvas.getContext('2d'); + this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); + container.appendChild(this._canvas); + } + + public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void { + this._canvas.width = canvasWidth * window.devicePixelRatio; + this._canvas.height = canvasHeight * window.devicePixelRatio; + this._canvas.style.width = `${canvasWidth}px`; + this._canvas.style.height = `${canvasHeight}px`; + } +} diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index d9a21d15fa..114966104d 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -4,28 +4,16 @@ import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; import { COLORS } from './Color'; import { GridCache } from './GridCache'; import { FLAGS } from './Types'; +import { BaseRenderLayer } from './BaseRenderLayer'; -export class CursorRenderLayer implements IDataRenderLayer { - private _canvas: HTMLCanvasElement; - private _ctx: CanvasRenderingContext2D; +export class CursorRenderLayer extends BaseRenderLayer implements IDataRenderLayer { private _state: [number, number]; - constructor(container: HTMLElement) { - this._canvas = document.createElement('canvas'); - this._canvas.classList.add('xterm-cursor-layer'); - this._ctx = this._canvas.getContext('2d'); - this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); - container.appendChild(this._canvas); + constructor(container: HTMLElement, zIndex: number) { + super(container, 'cursor', zIndex); this._state = null; } - public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void { - this._canvas.width = canvasWidth * window.devicePixelRatio; - this._canvas.height = canvasHeight * window.devicePixelRatio; - this._canvas.style.width = `${canvasWidth}px`; - this._canvas.style.height = `${canvasHeight}px`; - } - public render(terminal: ITerminal, startRow: number, endRow: number): void { // TODO: Track blur/focus somehow, support unfocused cursor diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index db23a041ac..a073f69da9 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -5,30 +5,22 @@ import { COLORS } from './Color'; import { FLAGS } from './Types'; import { GridCache } from './GridCache'; import { CharData } from '../Types'; +import { BaseRenderLayer } from './BaseRenderLayer'; -export class ForegroundRenderLayer implements IDataRenderLayer { - private _canvas: HTMLCanvasElement; - private _ctx: CanvasRenderingContext2D; +export class ForegroundRenderLayer extends BaseRenderLayer implements IDataRenderLayer { private _charAtlas: ImageBitmap; private _state: GridCache; private _charAtlasGenerator: CharAtlasGenerator; - constructor(container: HTMLElement) { - this._canvas = document.createElement('canvas'); - this._canvas.classList.add('xterm-fg-layer'); - this._ctx = this._canvas.getContext('2d'); - this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); - container.appendChild(this._canvas); + constructor(container: HTMLElement, zIndex: number) { + super(container, 'fg', zIndex); this._charAtlasGenerator = new CharAtlasGenerator(); this._state = new GridCache(); } public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void { - this._canvas.width = canvasWidth * window.devicePixelRatio; - this._canvas.height = canvasHeight * window.devicePixelRatio; - this._canvas.style.width = `${canvasWidth}px`; - this._canvas.style.height = `${canvasHeight}px`; + super.resize(terminal, canvasWidth, canvasHeight, charSizeChanged); this._state.resize(terminal.cols, terminal.rows); if (charSizeChanged) { this._charAtlas = null; diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index b340afba82..522f7af20b 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -22,12 +22,12 @@ export class Renderer { constructor(private _terminal: ITerminal) { this._dataRenderLayers = [ - new BackgroundRenderLayer(this._terminal.element), - new ForegroundRenderLayer(this._terminal.element), - new CursorRenderLayer(this._terminal.element) + new BackgroundRenderLayer(this._terminal.element, 0), + new ForegroundRenderLayer(this._terminal.element, 2), + new CursorRenderLayer(this._terminal.element, 3) ]; this._selectionRenderLayers = [ - new SelectionRenderLayer(this._terminal.element) + new SelectionRenderLayer(this._terminal.element, 1) ]; } diff --git a/src/renderer/SelectionRenderLayer.ts b/src/renderer/SelectionRenderLayer.ts index 091788f928..048be8e422 100644 --- a/src/renderer/SelectionRenderLayer.ts +++ b/src/renderer/SelectionRenderLayer.ts @@ -3,31 +3,19 @@ import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; import { GridCache } from './GridCache'; import { FLAGS } from './Types'; +import { BaseRenderLayer } from './BaseRenderLayer'; -export class SelectionRenderLayer implements ISelectionRenderLayer { - private _canvas: HTMLCanvasElement; - private _ctx: CanvasRenderingContext2D; +export class SelectionRenderLayer extends BaseRenderLayer implements ISelectionRenderLayer { private _state: {start: [number, number], end: [number, number]}; - constructor(container: HTMLElement) { - this._canvas = document.createElement('canvas'); - this._canvas.classList.add('xterm-selection-layer'); - this._ctx = this._canvas.getContext('2d'); - this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); - container.appendChild(this._canvas); + constructor(container: HTMLElement, zIndex: number) { + super(container, 'selection', zIndex); this._state = { start: null, end: null }; } - public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void { - this._canvas.width = canvasWidth * window.devicePixelRatio; - this._canvas.height = canvasHeight * window.devicePixelRatio; - this._canvas.style.width = `${canvasWidth}px`; - this._canvas.style.height = `${canvasHeight}px`; - } - public render(terminal: ITerminal, start: [number, number], end: [number, number]): void { const scaledCharWidth = Math.ceil(terminal.charMeasure.width) * window.devicePixelRatio; const scaledCharHeight = Math.ceil(terminal.charMeasure.height) * window.devicePixelRatio; diff --git a/src/xterm.css b/src/xterm.css index 6b044217af..f281e527e5 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -164,10 +164,10 @@ top: 0; } -.terminal .xterm-bg-layer { z-index: 0; } +/* .terminal .xterm-bg-layer { z-index: 0; } .terminal .xterm-selection-layer { z-index: 1; } .terminal .xterm-fg-layer { z-index: 2; } -.terminal .xterm-cursor-layer { z-index: 3; } +.terminal .xterm-cursor-layer { z-index: 3; } */ .terminal .xterm-rows { position: absolute; From fe7b424ab83adc5d290249d714800151bbebf694 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 31 Aug 2017 17:20:50 -0700 Subject: [PATCH 023/108] Move char atlas into BaseRenderLayer --- src/renderer/BaseRenderLayer.ts | 76 +++++++++++++++++++++++++++ src/renderer/ForegroundRenderLayer.ts | 66 +---------------------- 2 files changed, 78 insertions(+), 64 deletions(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 6d49396080..66b07fac04 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -1,10 +1,16 @@ import { IRenderLayer } from './Interfaces'; import { ITerminal } from '../Interfaces'; +import { COLORS } from './Color'; export abstract class BaseRenderLayer implements IRenderLayer { protected _canvas: HTMLCanvasElement; protected _ctx: CanvasRenderingContext2D; + protected static _charAtlas: ImageBitmap; + private static _charAtlasCharWidth: number; + private static _charAtlasCharHeight: number; + private static _charAtlasGenerator: CharAtlasGenerator; + constructor(container: HTMLElement, id: string, zIndex: number) { this._canvas = document.createElement('canvas'); this._canvas.id = `xterm-${id}-layer`; @@ -12,6 +18,10 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx = this._canvas.getContext('2d'); this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); container.appendChild(this._canvas); + + if (!BaseRenderLayer._charAtlasGenerator) { + BaseRenderLayer._charAtlasGenerator = new CharAtlasGenerator(); + } } public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void { @@ -19,5 +29,71 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._canvas.height = canvasHeight * window.devicePixelRatio; this._canvas.style.width = `${canvasWidth}px`; this._canvas.style.height = `${canvasHeight}px`; + // Only update the char atlas if the char size changed + if (charSizeChanged) { + // Only update the char atlas if an update for the right dimensions is not + // already in progress + if (BaseRenderLayer._charAtlasCharWidth !== terminal.charMeasure.width || + BaseRenderLayer._charAtlasCharHeight !== terminal.charMeasure.height) { + BaseRenderLayer._charAtlas = null; + BaseRenderLayer._charAtlasCharWidth = terminal.charMeasure.width; + BaseRenderLayer._charAtlasCharHeight = terminal.charMeasure.height; + BaseRenderLayer._charAtlasGenerator.generate(terminal.charMeasure.width, terminal.charMeasure.height).then(bitmap => { + BaseRenderLayer._charAtlas = bitmap; + }); + } + } + } +} + +class CharAtlasGenerator { + private _canvas: HTMLCanvasElement; + private _ctx: CanvasRenderingContext2D; + + constructor() { + this._canvas = document.createElement('canvas'); + this._ctx = this._canvas.getContext('2d'); + this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); + } + + public generate(charWidth: number, charHeight: number): Promise { + const scaledCharWidth = Math.ceil(charWidth) * window.devicePixelRatio; + const scaledCharHeight = Math.ceil(charHeight) * window.devicePixelRatio; + + this._canvas.width = 255 * scaledCharWidth; + this._canvas.height = (/*default*/1 + /*0-15*/16) * scaledCharHeight; + + this._ctx.save(); + this._ctx.fillStyle = '#ffffff'; + this._ctx.font = `${16 * window.devicePixelRatio}px courier`; + this._ctx.textBaseline = 'top'; + + // Default color + for (let i = 0; i < 256; i++) { + this._ctx.fillText(String.fromCharCode(i), i * scaledCharWidth, 0); + } + + // Colors 0-15 + for (let colorIndex = 0; colorIndex < 16; colorIndex++) { + // colors 8-15 are bold + if (colorIndex === 8) { + this._ctx.font = `bold ${this._ctx.font}`; + } + const y = (colorIndex + 1) * scaledCharHeight; + // Clear rectangle as some fonts seem to draw over the bottom boundary + this._ctx.clearRect(0, y, this._canvas.width, scaledCharHeight); + // Draw ascii characters + for (let i = 0; i < 256; i++) { + this._ctx.fillStyle = COLORS[colorIndex]; + this._ctx.fillText(String.fromCharCode(i), i * scaledCharWidth, y); + } + } + this._ctx.restore(); + + const charAtlasImageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); + const promise = window.createImageBitmap(charAtlasImageData); + // Clear the rect while the promise is in progress + this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); + return promise; } } diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index a073f69da9..3870071288 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -8,26 +8,16 @@ import { CharData } from '../Types'; import { BaseRenderLayer } from './BaseRenderLayer'; export class ForegroundRenderLayer extends BaseRenderLayer implements IDataRenderLayer { - private _charAtlas: ImageBitmap; private _state: GridCache; - private _charAtlasGenerator: CharAtlasGenerator; - constructor(container: HTMLElement, zIndex: number) { super(container, 'fg', zIndex); - this._charAtlasGenerator = new CharAtlasGenerator(); this._state = new GridCache(); } public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void { super.resize(terminal, canvasWidth, canvasHeight, charSizeChanged); this._state.resize(terminal.cols, terminal.rows); - if (charSizeChanged) { - this._charAtlas = null; - this._charAtlasGenerator.generate(terminal.charMeasure.width, terminal.charMeasure.height).then(bitmap => { - this._charAtlas = bitmap; - }); - } } public render(terminal: ITerminal, startRow: number, endRow: number): void { @@ -36,7 +26,7 @@ export class ForegroundRenderLayer extends BaseRenderLayer implements IDataRende // TODO: Ensure that the render is eventually performed // Don't bother render until the atlas bitmap is ready - if (!this._charAtlas) { + if (!BaseRenderLayer._charAtlas) { return; } @@ -98,7 +88,7 @@ export class ForegroundRenderLayer extends BaseRenderLayer implements IDataRende if (code < 256 && (colorIndex > 0 || fg > 255)) { // ImageBitmap's draw about twice as fast as from a canvas - this._ctx.drawImage(this._charAtlas, code * scaledCharWidth, colorIndex * scaledCharHeight, scaledCharWidth, scaledCharHeight, x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); + this._ctx.drawImage(BaseRenderLayer._charAtlas, code * scaledCharWidth, colorIndex * scaledCharHeight, scaledCharWidth, scaledCharHeight, x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); } else { // TODO: Evaluate how long it takes to convert from a number const width: number = charData[CHAR_DATA_WIDTH_INDEX]; @@ -128,55 +118,3 @@ export class ForegroundRenderLayer extends BaseRenderLayer implements IDataRende this._ctx.restore(); } } - -class CharAtlasGenerator { - private _canvas: HTMLCanvasElement; - private _ctx: CanvasRenderingContext2D; - - constructor() { - this._canvas = document.createElement('canvas'); - this._ctx = this._canvas.getContext('2d'); - this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); - } - - public generate(charWidth: number, charHeight: number): Promise { - const scaledCharWidth = Math.ceil(charWidth) * window.devicePixelRatio; - const scaledCharHeight = Math.ceil(charHeight) * window.devicePixelRatio; - - this._canvas.width = 255 * scaledCharWidth; - this._canvas.height = (/*default*/1 + /*0-15*/16) * scaledCharHeight; - - this._ctx.save(); - this._ctx.fillStyle = '#ffffff'; - this._ctx.font = `${16 * window.devicePixelRatio}px courier`; - this._ctx.textBaseline = 'top'; - - // Default color - for (let i = 0; i < 256; i++) { - this._ctx.fillText(String.fromCharCode(i), i * scaledCharWidth, 0); - } - - // Colors 0-15 - for (let colorIndex = 0; colorIndex < 16; colorIndex++) { - // colors 8-15 are bold - if (colorIndex === 8) { - this._ctx.font = `bold ${this._ctx.font}`; - } - const y = (colorIndex + 1) * scaledCharHeight; - // Clear rectangle as some fonts seem to draw over the bottom boundary - this._ctx.clearRect(0, y, this._canvas.width, scaledCharHeight); - // Draw ascii characters - for (let i = 0; i < 256; i++) { - this._ctx.fillStyle = COLORS[colorIndex]; - this._ctx.fillText(String.fromCharCode(i), i * scaledCharWidth, y); - } - } - this._ctx.restore(); - - const charAtlasImageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); - const promise = window.createImageBitmap(charAtlasImageData); - // Clear the rect while the promise is in progress - this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); - return promise; - } -} From 1998675dc07de8f347b337545906c15c11c3757b Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 31 Aug 2017 17:28:37 -0700 Subject: [PATCH 024/108] Move char drawing to base and use in cursor layer --- src/renderer/BaseRenderLayer.ts | 34 ++++++++++++++++++++++++++ src/renderer/CursorRenderLayer.ts | 8 +++--- src/renderer/ForegroundRenderLayer.ts | 35 ++------------------------- 3 files changed, 40 insertions(+), 37 deletions(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 66b07fac04..92e95cd23d 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -44,6 +44,40 @@ export abstract class BaseRenderLayer implements IRenderLayer { } } } + + protected drawChar(char: string, code: number, fg: number, x: number, y: number, scaledCharWidth: number, scaledCharHeight: number): void { + let colorIndex = 0; + if (fg < 256) { + colorIndex = fg + 1; + } + if (code < 256 && (colorIndex > 0 || fg > 255)) { + // ImageBitmap's draw about twice as fast as from a canvas + this._ctx.drawImage(BaseRenderLayer._charAtlas, + code * scaledCharWidth, colorIndex * scaledCharHeight, scaledCharWidth, scaledCharHeight, + x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); + } else { + this._drawUncachedChar(char, fg, x, y, scaledCharWidth, scaledCharHeight); + } + // This draws the atlas (for debugging purposes) + // this._ctx.drawImage(BaseRenderLayer._charAtlas, 0, 0); + } + + private _drawUncachedChar(char: string, fg: number, x: number, y: number, scaledCharWidth: number, scaledCharHeight: number): void { + this._ctx.save(); + this._ctx.font = `${16 * window.devicePixelRatio}px courier`; + this._ctx.textBaseline = 'top'; + + // 256 color support + if (fg < 256) { + this._ctx.fillStyle = COLORS[fg]; + } else { + this._ctx.fillStyle = '#ffffff'; + } + + // TODO: Do we care about width for rendering wide chars? + this._ctx.fillText(char, x * scaledCharWidth, y * scaledCharHeight); + this._ctx.restore(); + } } class CharAtlasGenerator { diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index 114966104d..da32aaa826 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -1,6 +1,6 @@ import { IDataRenderLayer } from './Interfaces'; import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; -import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; +import { CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; import { COLORS } from './Color'; import { GridCache } from './GridCache'; import { FLAGS } from './Types'; @@ -43,14 +43,14 @@ export class CursorRenderLayer extends BaseRenderLayer implements IDataRenderLay this._clearCursor(scaledCharWidth, scaledCharHeight); } - // TODO: Draw text in COLORS[0], using the char atlas if possible - // const charData = terminal.buffer.lines.get(viewportRelativeCursorY)[terminal.buffer.x]; - this._ctx.save(); this._ctx.fillStyle = COLORS[7]; this._ctx.fillRect(terminal.buffer.x * scaledCharWidth, viewportRelativeCursorY * scaledCharHeight, scaledCharWidth, scaledCharHeight); this._ctx.restore(); + const charData = terminal.buffer.lines.get(viewportRelativeCursorY)[terminal.buffer.x]; + this.drawChar(charData[CHAR_DATA_CHAR_INDEX], charData[CHAR_DATA_CODE_INDEX], 0, terminal.buffer.x, viewportRelativeCursorY, scaledCharWidth, scaledCharHeight); + this._state = [terminal.buffer.x, viewportRelativeCursorY]; } diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index 3870071288..c3cb833f17 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -26,6 +26,7 @@ export class ForegroundRenderLayer extends BaseRenderLayer implements IDataRende // TODO: Ensure that the render is eventually performed // Don't bother render until the atlas bitmap is ready + // TODO: Move this to BaseRenderLayer? if (!BaseRenderLayer._charAtlas) { return; } @@ -81,40 +82,8 @@ export class ForegroundRenderLayer extends BaseRenderLayer implements IDataRende } } - let colorIndex = 0; - if (fg < 16) { - colorIndex = fg + 1; - } - - if (code < 256 && (colorIndex > 0 || fg > 255)) { - // ImageBitmap's draw about twice as fast as from a canvas - this._ctx.drawImage(BaseRenderLayer._charAtlas, code * scaledCharWidth, colorIndex * scaledCharHeight, scaledCharWidth, scaledCharHeight, x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); - } else { - // TODO: Evaluate how long it takes to convert from a number - const width: number = charData[CHAR_DATA_WIDTH_INDEX]; - this._drawUncachedChar(char, width, fg, x, y, scaledCharWidth, scaledCharHeight); - } + this.drawChar(char, code, fg, x, y, scaledCharWidth, scaledCharHeight); } } - - // This draws the atlas (for debugging purposes) - // this._ctx.drawImage(this._charAtlas, 0, 0); - } - - private _drawUncachedChar(char: string, width: number, fg: number, x: number, y: number, scaledCharWidth: number, scaledCharHeight: number): void { - this._ctx.save(); - this._ctx.font = `${16 * window.devicePixelRatio}px courier`; - this._ctx.textBaseline = 'top'; - - // 256 color support - if (fg < 256) { - this._ctx.fillStyle = COLORS[fg]; - } else { - this._ctx.fillStyle = '#ffffff'; - } - - // TODO: Do we care about width for rendering wide chars? - this._ctx.fillText(char, x * scaledCharWidth, y * scaledCharHeight); - this._ctx.restore(); } } From c88824155f4f0af55bba07b172971f51e723147a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 31 Aug 2017 18:09:07 -0700 Subject: [PATCH 025/108] Simplify color generation code --- src/renderer/BaseRenderLayer.ts | 3 +- src/renderer/Color.ts | 62 +++++++++++++++++++------------ src/renderer/CursorRenderLayer.ts | 7 ++-- 3 files changed, 45 insertions(+), 27 deletions(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 92e95cd23d..5e53c411fa 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -6,6 +6,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { protected _canvas: HTMLCanvasElement; protected _ctx: CanvasRenderingContext2D; + // TODO: This will apply to all terminals, should it be per-terminal? protected static _charAtlas: ImageBitmap; private static _charAtlasCharWidth: number; private static _charAtlasCharHeight: number; @@ -29,7 +30,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._canvas.height = canvasHeight * window.devicePixelRatio; this._canvas.style.width = `${canvasWidth}px`; this._canvas.style.height = `${canvasHeight}px`; - // Only update the char atlas if the char size changed + if (charSizeChanged) { // Only update the char atlas if an update for the right dimensions is not // already in progress diff --git a/src/renderer/Color.ts b/src/renderer/Color.ts index eb3954eae0..446d440b36 100644 --- a/src/renderer/Color.ts +++ b/src/renderer/Color.ts @@ -1,6 +1,25 @@ // TODO: Ideally colors would be exposed through some theme manager since colors // are moving to JS. +export enum COLOR_CODES { + BLACK = 0, + RED = 1, + GREEN = 2, + YELLOW = 3, + BLUE = 4, + MAGENTA = 5, + CYAN = 6, + WHITE = 7, + BRIGHT_BLACK = 8, + BRIGHT_RED = 9, + BRIGHT_GREEN = 10, + BRIGHT_YELLOW = 11, + BRIGHT_BLUE = 12, + BRIGHT_MAGENTA = 13, + BRIGHT_CYAN = 14, + BRIGHT_WHITE = 15 +} + const TANGO_COLORS = [ // dark: '#2e3436', @@ -22,33 +41,30 @@ const TANGO_COLORS = [ '#eeeeec' ]; -export const COLORS: string[] = (function(): string[] { - let colors = TANGO_COLORS.slice(); - let r = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]; - let i; +export const COLORS: string[] = generate256Colors(TANGO_COLORS); - // 16-231 - i = 0; - for (; i < 216; i++) { - out(r[(i / 36) % 6 | 0], r[(i / 6) % 6 | 0], r[i % 6]); - } +function generate256Colors(first16Colors: string[]): string[] { + let colors = first16Colors.slice(); - // 232-255 (grey) - i = 0; - let c: number; - for (; i < 24; i++) { - c = 8 + i * 10; - out(c, c, c); + // Generate colors (16-231) + let v = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]; + for (let i = 0; i < 216; i++) { + const r = toPaddedHex(v[(i / 36) % 6 | 0]); + const g = toPaddedHex(v[(i / 6) % 6 | 0]); + const b = toPaddedHex(v[i % 6]); + colors.push(`#${r}${g}${b}`); } - function out(r: number, g: number, b: number): void { - colors.push('#' + hex(r) + hex(g) + hex(b)); - } - - function hex(c: number): string { - let s = c.toString(16); - return s.length < 2 ? '0' + s : s; + // Generate greys (232-255) + for (let i = 0; i < 24; i++) { + const c = toPaddedHex(8 + i * 10); + colors.push(`#${c}${c}${c}`); } return colors; -})(); +} + +function toPaddedHex(c: number): string { + let s = c.toString(16); + return s.length < 2 ? '0' + s : s; +} diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index da32aaa826..75dd4d8988 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -1,7 +1,7 @@ import { IDataRenderLayer } from './Interfaces'; import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; import { CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; -import { COLORS } from './Color'; +import { COLORS, COLOR_CODES } from './Color'; import { GridCache } from './GridCache'; import { FLAGS } from './Types'; import { BaseRenderLayer } from './BaseRenderLayer'; @@ -17,6 +17,7 @@ export class CursorRenderLayer extends BaseRenderLayer implements IDataRenderLay public render(terminal: ITerminal, startRow: number, endRow: number): void { // TODO: Track blur/focus somehow, support unfocused cursor + // TODO: scaledCharWidth should probably be on Base as a per-terminal thing const scaledCharWidth = Math.ceil(terminal.charMeasure.width) * window.devicePixelRatio; const scaledCharHeight = Math.ceil(terminal.charMeasure.height) * window.devicePixelRatio; @@ -44,12 +45,12 @@ export class CursorRenderLayer extends BaseRenderLayer implements IDataRenderLay } this._ctx.save(); - this._ctx.fillStyle = COLORS[7]; + this._ctx.fillStyle = COLORS[COLOR_CODES.WHITE]; this._ctx.fillRect(terminal.buffer.x * scaledCharWidth, viewportRelativeCursorY * scaledCharHeight, scaledCharWidth, scaledCharHeight); this._ctx.restore(); const charData = terminal.buffer.lines.get(viewportRelativeCursorY)[terminal.buffer.x]; - this.drawChar(charData[CHAR_DATA_CHAR_INDEX], charData[CHAR_DATA_CODE_INDEX], 0, terminal.buffer.x, viewportRelativeCursorY, scaledCharWidth, scaledCharHeight); + this.drawChar(charData[CHAR_DATA_CHAR_INDEX], charData[CHAR_DATA_CODE_INDEX], COLOR_CODES.BLACK, terminal.buffer.x, viewportRelativeCursorY, scaledCharWidth, scaledCharHeight); this._state = [terminal.buffer.x, viewportRelativeCursorY]; } From 15c73ef94f7cacdff3e91cf6ee58bd1f5ed14ff6 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 31 Aug 2017 22:42:39 -0700 Subject: [PATCH 026/108] Add fontSize and fontFamily options --- src/Interfaces.ts | 4 +- src/Terminal.ts | 36 +++++++------- src/renderer/BackgroundRenderLayer.ts | 5 ++ src/renderer/BaseRenderLayer.ts | 19 +++++--- src/renderer/CursorRenderLayer.ts | 8 ++- src/renderer/ForegroundRenderLayer.ts | 11 +++-- src/renderer/GridCache.ts | 8 +++ src/renderer/Interfaces.ts | 8 +++ src/renderer/Renderer.ts | 70 ++++----------------------- src/renderer/SelectionRenderLayer.ts | 10 ++++ src/utils/CharMeasure.test.ts | 12 ++--- src/utils/CharMeasure.ts | 17 ++++--- src/xterm.css | 5 -- typings/xterm.d.ts | 8 +-- 14 files changed, 107 insertions(+), 114 deletions(-) diff --git a/src/Interfaces.ts b/src/Interfaces.ts index fb66f4d7cf..1b1625bca3 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -122,6 +122,8 @@ export interface ITerminalOptions { cursorStyle?: string; debug?: boolean; disableStdin?: boolean; + fontSize?: number; + fontFamily?: string; geometry?: [number, number]; handler?: (data: string) => void; rows?: number; @@ -186,7 +188,7 @@ export interface ICompositionHelper { export interface ICharMeasure { width: number; height: number; - measure(): void; + measure(options: ITerminalOptions): void; } export interface ILinkifier { diff --git a/src/Terminal.ts b/src/Terminal.ts index f88ebb7f90..c9c6466c71 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -151,6 +151,8 @@ const DEFAULT_OPTIONS: ITerminalOptions = { cursorStyle: 'block', bellSound: BellSound, bellStyle: 'none', + fontFamily: 'courier-new, courier, monospace', + fontSize: 15, scrollback: 1000, screenKeys: false, debug: false, @@ -481,6 +483,12 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.element.classList.toggle(`xterm-cursor-style-underline`, value === 'underline'); this.element.classList.toggle(`xterm-cursor-style-bar`, value === 'bar'); break; + case 'fontFamily': + case 'fontSize': + // When the font changes the size of the cells may change which requires a renderer clear + this.renderer.clear(); + this.charMeasure.measure(this.options); + break; case 'scrollback': this.buffers.resize(this.cols, this.rows); this.viewport.syncScrollArea(); @@ -742,15 +750,15 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.parent.appendChild(this.element); this.charMeasure = new CharMeasure(document, this.helperContainer); - this.charMeasure.on('charsizechanged', () => { - this.updateCharSizeStyles(); - }); - this.charMeasure.measure(); this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure); this.renderer = new Renderer(this); this.on('resize', () => this.renderer.onResize(this.cols, this.rows)); - this.charMeasure.on('charsizechanged', () => this.renderer.onCharSizeChanged(this.charMeasure.width, this.charMeasure.height)); + this.charMeasure.on('charsizechanged', () => { + this.renderer.onCharSizeChanged(this.charMeasure.width, this.charMeasure.height); + // Force a refresh for the char size change + this.renderer.queueRefresh(0, this.rows - 1); + }); this.selectionManager = new SelectionManager(this, this.buffer, this.rowContainer, this.charMeasure); this.element.addEventListener('mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e)); @@ -766,6 +774,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.on('scroll', () => this.selectionManager.refresh()); this.viewportElement.addEventListener('scroll', () => this.selectionManager.refresh()); + // Measure the character size + this.charMeasure.measure(this.options); + // Setup loop that draws to screen this.refresh(0, this.rows - 1); @@ -796,17 +807,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT } } - /** - * Updates the helper CSS class with any changes necessary after the terminal's - * character width has been changed. - */ - public updateCharSizeStyles(): void { - this.charSizeStyleElement.textContent = - `.xterm-wide-char{width:${this.charMeasure.width * 2}px;}` + - `.xterm-normal-char{width:${this.charMeasure.width}px;}` + - `.xterm-rows > div{height:${this.charMeasure.height}px;}`; - } - /** * XTerm mouse events * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking @@ -1918,7 +1918,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT if (x === this.cols && y === this.rows) { // Check if we still need to measure the char size (fixes #785). if (!this.charMeasure.width || !this.charMeasure.height) { - this.charMeasure.measure(); + this.charMeasure.measure(this.options); } return; } @@ -1942,7 +1942,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.rows = y; this.buffers.setupTabStops(this.cols); - this.charMeasure.measure(); + this.charMeasure.measure(this.options); this.refresh(0, this.rows - 1); diff --git a/src/renderer/BackgroundRenderLayer.ts b/src/renderer/BackgroundRenderLayer.ts index a9722eabf0..37b9c69e48 100644 --- a/src/renderer/BackgroundRenderLayer.ts +++ b/src/renderer/BackgroundRenderLayer.ts @@ -19,6 +19,11 @@ export class BackgroundRenderLayer extends BaseRenderLayer implements IDataRende this._state.resize(terminal.cols, terminal.rows); } + public clear(terminal: ITerminal): void { + this._state.clear(); + this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); + } + public render(terminal: ITerminal, startRow: number, endRow: number): void { const scaledCharWidth = Math.ceil(terminal.charMeasure.width) * window.devicePixelRatio; const scaledCharHeight = Math.ceil(terminal.charMeasure.height) * window.devicePixelRatio; diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 5e53c411fa..419b814cc0 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -39,14 +39,16 @@ export abstract class BaseRenderLayer implements IRenderLayer { BaseRenderLayer._charAtlas = null; BaseRenderLayer._charAtlasCharWidth = terminal.charMeasure.width; BaseRenderLayer._charAtlasCharHeight = terminal.charMeasure.height; - BaseRenderLayer._charAtlasGenerator.generate(terminal.charMeasure.width, terminal.charMeasure.height).then(bitmap => { + BaseRenderLayer._charAtlasGenerator.generate(terminal, terminal.charMeasure.width, terminal.charMeasure.height).then(bitmap => { BaseRenderLayer._charAtlas = bitmap; }); } } } - protected drawChar(char: string, code: number, fg: number, x: number, y: number, scaledCharWidth: number, scaledCharHeight: number): void { + public abstract clear(terminal: ITerminal): void; + + protected drawChar(terminal: ITerminal, char: string, code: number, fg: number, x: number, y: number, scaledCharWidth: number, scaledCharHeight: number): void { let colorIndex = 0; if (fg < 256) { colorIndex = fg + 1; @@ -57,15 +59,15 @@ export abstract class BaseRenderLayer implements IRenderLayer { code * scaledCharWidth, colorIndex * scaledCharHeight, scaledCharWidth, scaledCharHeight, x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); } else { - this._drawUncachedChar(char, fg, x, y, scaledCharWidth, scaledCharHeight); + this._drawUncachedChar(terminal, char, fg, x, y, scaledCharWidth, scaledCharHeight); } // This draws the atlas (for debugging purposes) // this._ctx.drawImage(BaseRenderLayer._charAtlas, 0, 0); } - private _drawUncachedChar(char: string, fg: number, x: number, y: number, scaledCharWidth: number, scaledCharHeight: number): void { + private _drawUncachedChar(terminal: ITerminal, char: string, fg: number, x: number, y: number, scaledCharWidth: number, scaledCharHeight: number): void { this._ctx.save(); - this._ctx.font = `${16 * window.devicePixelRatio}px courier`; + this._ctx.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`; this._ctx.textBaseline = 'top'; // 256 color support @@ -91,16 +93,17 @@ class CharAtlasGenerator { this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); } - public generate(charWidth: number, charHeight: number): Promise { + public generate(terminal: ITerminal, charWidth: number, charHeight: number): Promise { const scaledCharWidth = Math.ceil(charWidth) * window.devicePixelRatio; const scaledCharHeight = Math.ceil(charHeight) * window.devicePixelRatio; - +console.log('generate'); this._canvas.width = 255 * scaledCharWidth; this._canvas.height = (/*default*/1 + /*0-15*/16) * scaledCharHeight; this._ctx.save(); this._ctx.fillStyle = '#ffffff'; - this._ctx.font = `${16 * window.devicePixelRatio}px courier`; + this._ctx.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`; + console.log(this._ctx.font, scaledCharWidth, scaledCharHeight); this._ctx.textBaseline = 'top'; // Default color diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index 75dd4d8988..7664222916 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -14,6 +14,12 @@ export class CursorRenderLayer extends BaseRenderLayer implements IDataRenderLay this._state = null; } + public clear(terminal: ITerminal): void { + const scaledCharWidth = Math.ceil(terminal.charMeasure.width) * window.devicePixelRatio; + const scaledCharHeight = Math.ceil(terminal.charMeasure.height) * window.devicePixelRatio; + this._clearCursor(scaledCharWidth, scaledCharHeight); + } + public render(terminal: ITerminal, startRow: number, endRow: number): void { // TODO: Track blur/focus somehow, support unfocused cursor @@ -50,7 +56,7 @@ export class CursorRenderLayer extends BaseRenderLayer implements IDataRenderLay this._ctx.restore(); const charData = terminal.buffer.lines.get(viewportRelativeCursorY)[terminal.buffer.x]; - this.drawChar(charData[CHAR_DATA_CHAR_INDEX], charData[CHAR_DATA_CODE_INDEX], COLOR_CODES.BLACK, terminal.buffer.x, viewportRelativeCursorY, scaledCharWidth, scaledCharHeight); + this.drawChar(terminal, charData[CHAR_DATA_CHAR_INDEX], charData[CHAR_DATA_CODE_INDEX], COLOR_CODES.BLACK, terminal.buffer.x, viewportRelativeCursorY, scaledCharWidth, scaledCharHeight); this._state = [terminal.buffer.x, viewportRelativeCursorY]; } diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index c3cb833f17..3cd7ae1eb1 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -20,6 +20,11 @@ export class ForegroundRenderLayer extends BaseRenderLayer implements IDataRende this._state.resize(terminal.cols, terminal.rows); } + public clear(terminal: ITerminal): void { + this._state.clear(); + this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); + } + public render(terminal: ITerminal, startRow: number, endRow: number): void { const scaledCharWidth = Math.ceil(terminal.charMeasure.width) * window.devicePixelRatio; const scaledCharHeight = Math.ceil(terminal.charMeasure.height) * window.devicePixelRatio; @@ -31,10 +36,6 @@ export class ForegroundRenderLayer extends BaseRenderLayer implements IDataRende return; } - this._ctx.fillStyle = '#ffffff'; - this._ctx.textBaseline = 'top'; - this._ctx.font = `${16 * window.devicePixelRatio}px courier`; - for (let y = startRow; y <= endRow; y++) { const row = y + terminal.buffer.ydisp; const line = terminal.buffer.lines.get(row); @@ -82,7 +83,7 @@ export class ForegroundRenderLayer extends BaseRenderLayer implements IDataRende } } - this.drawChar(char, code, fg, x, y, scaledCharWidth, scaledCharHeight); + this.drawChar(terminal, char, code, fg, x, y, scaledCharWidth, scaledCharHeight); } } } diff --git a/src/renderer/GridCache.ts b/src/renderer/GridCache.ts index eba3dad6a7..a4657fcba7 100644 --- a/src/renderer/GridCache.ts +++ b/src/renderer/GridCache.ts @@ -17,4 +17,12 @@ export class GridCache { } this.cache.length = width; } + + public clear() { + for (let x = 0; x < this.cache.length; x++) { + for (let y = 0; y < this.cache[x].length; y++) { + this.cache[x][y] = null; + } + } + } } diff --git a/src/renderer/Interfaces.ts b/src/renderer/Interfaces.ts index 4d6d2d3b9f..8eb3acdde7 100644 --- a/src/renderer/Interfaces.ts +++ b/src/renderer/Interfaces.ts @@ -1,7 +1,15 @@ import { ITerminal } from '../Interfaces'; export interface IRenderLayer { + /** + * Resize the render layer. + */ resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void; + + /** + * Clear the state of the render layer. + */ + clear(terminal: ITerminal): void; } export interface IDataRenderLayer extends IRenderLayer { diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 522f7af20b..26b472cfda 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -40,6 +40,7 @@ export class Renderer { } public onCharSizeChanged(charWidth: number, charHeight: number): void { + console.log('Renderer.onCharSizeChanged', charWidth, charHeight); const width = Math.ceil(charWidth) * this._terminal.cols; const height = Math.ceil(charHeight) * this._terminal.rows; for (let i = 0; i < this._dataRenderLayers.length; i++) { @@ -56,6 +57,15 @@ export class Renderer { } } + public clear(): void { + for (let i = 0; i < this._dataRenderLayers.length; i++) { + this._dataRenderLayers[i].clear(this._terminal); + } + for (let i = 0; i < this._selectionRenderLayers.length; i++) { + this._selectionRenderLayers[i].clear(this._terminal); + } + } + /** * Queues a refresh between two rows (inclusive), to be done on next animation * frame. @@ -102,64 +112,4 @@ export class Renderer { } this._terminal.emit('refresh', {start, 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]): void { - // 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; - } } diff --git a/src/renderer/SelectionRenderLayer.ts b/src/renderer/SelectionRenderLayer.ts index 048be8e422..a2cfb1d62a 100644 --- a/src/renderer/SelectionRenderLayer.ts +++ b/src/renderer/SelectionRenderLayer.ts @@ -16,6 +16,16 @@ export class SelectionRenderLayer extends BaseRenderLayer implements ISelectionR }; } + public clear(terminal: ITerminal): void { + if (this._state.start && this._state.end) { + this._state = { + start: null, + end: null + }; + this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); + } + } + public render(terminal: ITerminal, start: [number, number], end: [number, number]): void { const scaledCharWidth = Math.ceil(terminal.charMeasure.width) * window.devicePixelRatio; const scaledCharHeight = Math.ceil(terminal.charMeasure.height) * window.devicePixelRatio; diff --git a/src/utils/CharMeasure.test.ts b/src/utils/CharMeasure.test.ts index 68e1b6a787..02a8dae9e1 100644 --- a/src/utils/CharMeasure.test.ts +++ b/src/utils/CharMeasure.test.ts @@ -25,13 +25,13 @@ describe('CharMeasure', () => { describe('measure', () => { it('should set _measureElement on first call', () => { - charMeasure.measure(); + charMeasure.measure({}); assert.isDefined((charMeasure)._measureElement, 'CharMeasure.measure should have created _measureElement'); }); it('should be performed async on first call', done => { assert.equal(charMeasure.width, null); - charMeasure.measure(); + charMeasure.measure({}); // Mock getBoundingClientRect since jsdom doesn't have a layout engine (charMeasure)._measureElement.getBoundingClientRect = () => { return { width: 1, height: 1 }; @@ -44,7 +44,7 @@ describe('CharMeasure', () => { }); it('should be performed sync on successive calls', done => { - charMeasure.measure(); + charMeasure.measure({}); // Mock getBoundingClientRect since jsdom doesn't have a layout engine (charMeasure)._measureElement.getBoundingClientRect = () => { return { width: 1, height: 1 }; @@ -55,19 +55,19 @@ describe('CharMeasure', () => { (charMeasure)._measureElement.getBoundingClientRect = () => { return { width: 2, height: 2 }; }; - charMeasure.measure(); + charMeasure.measure({}); assert.equal(charMeasure.width, firstWidth * 2); done(); }, 0); }); it('should NOT do a measure when the parent is hidden', done => { - charMeasure.measure(); + charMeasure.measure({}); setTimeout(() => { const firstWidth = charMeasure.width; container.style.display = 'none'; container.style.fontSize = '2em'; - charMeasure.measure(); + charMeasure.measure({}); assert.equal(charMeasure.width, firstWidth); done(); }, 0); diff --git a/src/utils/CharMeasure.ts b/src/utils/CharMeasure.ts index 45b06f5916..ff777811ec 100644 --- a/src/utils/CharMeasure.ts +++ b/src/utils/CharMeasure.ts @@ -4,11 +4,14 @@ */ import { EventEmitter } from '../EventEmitter.js'; +import { ICharMeasure, ITerminal, ITerminalOptions } from '../Interfaces'; /** - * Utility class that measures the size of a character. + * Utility class that measures the size of a character. Measurements are done in + * the DOM rather than with a canvas context because support for extracting the + * height of characters is patchy across browsers. */ -export class CharMeasure extends EventEmitter { +export class CharMeasure extends EventEmitter implements ICharMeasure { private _document: Document; private _parentElement: HTMLElement; private _measureElement: HTMLElement; @@ -29,7 +32,7 @@ export class CharMeasure extends EventEmitter { return this._height; } - public measure(): void { + public measure(options: ITerminalOptions): void { if (!this._measureElement) { this._measureElement = this._document.createElement('span'); this._measureElement.style.position = 'absolute'; @@ -40,13 +43,15 @@ export class CharMeasure extends EventEmitter { this._parentElement.appendChild(this._measureElement); // Perform _doMeasure async if the element was just attached as sometimes // getBoundingClientRect does not return accurate values without this. - setTimeout(() => this._doMeasure(), 0); + setTimeout(() => this._doMeasure(options), 0); } else { - this._doMeasure(); + this._doMeasure(options); } } - private _doMeasure(): void { + private _doMeasure(options: ITerminalOptions): void { + this._measureElement.style.fontFamily = options.fontFamily; + this._measureElement.style.fontSize = `${options.fontSize}px`; const geometry = this._measureElement.getBoundingClientRect(); // The element is likely currently display:none, we should retain the // previous value. diff --git a/src/xterm.css b/src/xterm.css index f281e527e5..6a7504f3ed 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -164,11 +164,6 @@ top: 0; } -/* .terminal .xterm-bg-layer { z-index: 0; } -.terminal .xterm-selection-layer { z-index: 1; } -.terminal .xterm-fg-layer { z-index: 2; } -.terminal .xterm-cursor-layer { z-index: 3; } */ - .terminal .xterm-rows { position: absolute; left: 0; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index ccc83ed90f..dce15bca4a 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -313,7 +313,7 @@ declare module 'xterm' { * Retrieves an option's value from the terminal. * @param key The option key. */ - getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'termName'): string; + getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'termName'): string; /** * Retrieves an option's value from the terminal. * @param key The option key. @@ -328,7 +328,7 @@ declare module 'xterm' { * Retrieves an option's value from the terminal. * @param key The option key. */ - getOption(key: 'cols' | 'rows' | 'tabStopWidth' | 'scrollback'): number; + getOption(key: 'cols' | 'fontSize' | 'rows' | 'tabStopWidth' | 'scrollback'): number; /** * Retrieves an option's value from the terminal. * @param key The option key. @@ -350,7 +350,7 @@ declare module 'xterm' { * @param key The option key. * @param value The option value. */ - setOption(key: 'termName' | 'bellSound', value: string): void; + setOption(key: 'fontFamily' | 'termName' | 'bellSound', value: string): void; /** * Sets an option on the terminal. * @param key The option key. @@ -380,7 +380,7 @@ declare module 'xterm' { * @param key The option key. * @param value The option value. */ - setOption(key: 'cols' | 'rows' | 'tabStopWidth' | 'scrollback', value: number): void; + setOption(key: 'cols' | 'fontSize' | 'rows' | 'tabStopWidth' | 'scrollback', value: number): void; /** * Sets an option on the terminal. * @param key The option key. From 6588e9e01b53b099f71cb0f13f69f82b32ce37ec Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 31 Aug 2017 22:43:48 -0700 Subject: [PATCH 027/108] Fix cursor which cursor character is rendered --- src/renderer/CursorRenderLayer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index 7664222916..c18bb13088 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -55,7 +55,7 @@ export class CursorRenderLayer extends BaseRenderLayer implements IDataRenderLay this._ctx.fillRect(terminal.buffer.x * scaledCharWidth, viewportRelativeCursorY * scaledCharHeight, scaledCharWidth, scaledCharHeight); this._ctx.restore(); - const charData = terminal.buffer.lines.get(viewportRelativeCursorY)[terminal.buffer.x]; + const charData = terminal.buffer.lines.get(cursorY)[terminal.buffer.x]; this.drawChar(terminal, charData[CHAR_DATA_CHAR_INDEX], charData[CHAR_DATA_CODE_INDEX], COLOR_CODES.BLACK, terminal.buffer.x, viewportRelativeCursorY, scaledCharWidth, scaledCharHeight); this._state = [terminal.buffer.x, viewportRelativeCursorY]; From 9323833fccc85508f10f32892b338300dab7f517 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 31 Aug 2017 22:48:08 -0700 Subject: [PATCH 028/108] Move scaledCharWidth/Height into base render layer --- src/renderer/BackgroundRenderLayer.ts | 7 ++----- src/renderer/BaseRenderLayer.ts | 4 ++++ src/renderer/CursorRenderLayer.ts | 22 ++++++++-------------- src/renderer/ForegroundRenderLayer.ts | 7 ++----- src/renderer/SelectionRenderLayer.ts | 9 +++------ 5 files changed, 19 insertions(+), 30 deletions(-) diff --git a/src/renderer/BackgroundRenderLayer.ts b/src/renderer/BackgroundRenderLayer.ts index 37b9c69e48..1e8169d765 100644 --- a/src/renderer/BackgroundRenderLayer.ts +++ b/src/renderer/BackgroundRenderLayer.ts @@ -25,9 +25,6 @@ export class BackgroundRenderLayer extends BaseRenderLayer implements IDataRende } public render(terminal: ITerminal, startRow: number, endRow: number): void { - const scaledCharWidth = Math.ceil(terminal.charMeasure.width) * window.devicePixelRatio; - const scaledCharHeight = Math.ceil(terminal.charMeasure.height) * window.devicePixelRatio; - for (let y = startRow; y <= endRow; y++) { let row = y + terminal.buffer.ydisp; let line = terminal.buffer.lines.get(row); @@ -51,11 +48,11 @@ export class BackgroundRenderLayer extends BaseRenderLayer implements IDataRende if (bg < 256) { this._ctx.save(); this._ctx.fillStyle = COLORS[bg]; - this._ctx.fillRect(x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); + this._ctx.fillRect(x * this.scaledCharWidth, y * this.scaledCharHeight, this.scaledCharWidth, this.scaledCharHeight); this._ctx.restore(); this._state.cache[x][y] = bg; } else { - this._ctx.clearRect(x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); + this._ctx.clearRect(x * this.scaledCharWidth, y * this.scaledCharHeight, this.scaledCharWidth, this.scaledCharHeight); this._state.cache[x][y] = null; } } diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 419b814cc0..7790e03447 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -5,6 +5,8 @@ import { COLORS } from './Color'; export abstract class BaseRenderLayer implements IRenderLayer { protected _canvas: HTMLCanvasElement; protected _ctx: CanvasRenderingContext2D; + protected scaledCharWidth: number; + protected scaledCharHeight: number; // TODO: This will apply to all terminals, should it be per-terminal? protected static _charAtlas: ImageBitmap; @@ -26,6 +28,8 @@ export abstract class BaseRenderLayer implements IRenderLayer { } public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void { + this.scaledCharWidth = Math.ceil(terminal.charMeasure.width) * window.devicePixelRatio; + this.scaledCharHeight = Math.ceil(terminal.charMeasure.height) * window.devicePixelRatio; this._canvas.width = canvasWidth * window.devicePixelRatio; this._canvas.height = canvasHeight * window.devicePixelRatio; this._canvas.style.width = `${canvasWidth}px`; diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index c18bb13088..bb0ff9284a 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -15,21 +15,15 @@ export class CursorRenderLayer extends BaseRenderLayer implements IDataRenderLay } public clear(terminal: ITerminal): void { - const scaledCharWidth = Math.ceil(terminal.charMeasure.width) * window.devicePixelRatio; - const scaledCharHeight = Math.ceil(terminal.charMeasure.height) * window.devicePixelRatio; - this._clearCursor(scaledCharWidth, scaledCharHeight); + this._clearCursor(); } public render(terminal: ITerminal, startRow: number, endRow: number): void { // TODO: Track blur/focus somehow, support unfocused cursor - // TODO: scaledCharWidth should probably be on Base as a per-terminal thing - const scaledCharWidth = Math.ceil(terminal.charMeasure.width) * window.devicePixelRatio; - const scaledCharHeight = Math.ceil(terminal.charMeasure.height) * window.devicePixelRatio; - // Don't draw the cursor if it's hidden if (!terminal.cursorState || terminal.cursorHidden) { - this._clearCursor(scaledCharWidth, scaledCharHeight); + this._clearCursor(); return; } @@ -38,7 +32,7 @@ export class CursorRenderLayer extends BaseRenderLayer implements IDataRenderLay // Don't draw the cursor if it's off-screen if (viewportRelativeCursorY < 0 || viewportRelativeCursorY >= terminal.rows) { - this._clearCursor(scaledCharWidth, scaledCharHeight); + this._clearCursor(); return; } @@ -47,23 +41,23 @@ export class CursorRenderLayer extends BaseRenderLayer implements IDataRenderLay if (this._state[0] === terminal.buffer.x && this._state[1] === viewportRelativeCursorY) { return; } - this._clearCursor(scaledCharWidth, scaledCharHeight); + this._clearCursor(); } this._ctx.save(); this._ctx.fillStyle = COLORS[COLOR_CODES.WHITE]; - this._ctx.fillRect(terminal.buffer.x * scaledCharWidth, viewportRelativeCursorY * scaledCharHeight, scaledCharWidth, scaledCharHeight); + this._ctx.fillRect(terminal.buffer.x * this.scaledCharWidth, viewportRelativeCursorY * this.scaledCharHeight, this.scaledCharWidth, this.scaledCharHeight); this._ctx.restore(); const charData = terminal.buffer.lines.get(cursorY)[terminal.buffer.x]; - this.drawChar(terminal, charData[CHAR_DATA_CHAR_INDEX], charData[CHAR_DATA_CODE_INDEX], COLOR_CODES.BLACK, terminal.buffer.x, viewportRelativeCursorY, scaledCharWidth, scaledCharHeight); + this.drawChar(terminal, charData[CHAR_DATA_CHAR_INDEX], charData[CHAR_DATA_CODE_INDEX], COLOR_CODES.BLACK, terminal.buffer.x, viewportRelativeCursorY, this.scaledCharWidth, this.scaledCharHeight); this._state = [terminal.buffer.x, viewportRelativeCursorY]; } - private _clearCursor(scaledCharWidth: number, scaledCharHeight: number): void { + private _clearCursor(): void { if (this._state) { - this._ctx.clearRect(this._state[0] * scaledCharWidth, this._state[1] * scaledCharHeight, scaledCharWidth, scaledCharHeight); + this._ctx.clearRect(this._state[0] * this.scaledCharWidth, this._state[1] * this.scaledCharHeight, this.scaledCharWidth, this.scaledCharHeight); this._state = null; } } diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index 3cd7ae1eb1..68d00857a8 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -26,9 +26,6 @@ export class ForegroundRenderLayer extends BaseRenderLayer implements IDataRende } public render(terminal: ITerminal, startRow: number, endRow: number): void { - const scaledCharWidth = Math.ceil(terminal.charMeasure.width) * window.devicePixelRatio; - const scaledCharHeight = Math.ceil(terminal.charMeasure.height) * window.devicePixelRatio; - // TODO: Ensure that the render is eventually performed // Don't bother render until the atlas bitmap is ready // TODO: Move this to BaseRenderLayer? @@ -56,7 +53,7 @@ export class ForegroundRenderLayer extends BaseRenderLayer implements IDataRende this._state.cache[x][y] = charData; // Clear the old character - this._ctx.clearRect(x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); + this._ctx.clearRect(x * this.scaledCharWidth, y * this.scaledCharHeight, this.scaledCharWidth, this.scaledCharHeight); // Skip rendering if the character is invisible if (!code || code === 32 /*' '*/) { @@ -83,7 +80,7 @@ export class ForegroundRenderLayer extends BaseRenderLayer implements IDataRende } } - this.drawChar(terminal, char, code, fg, x, y, scaledCharWidth, scaledCharHeight); + this.drawChar(terminal, char, code, fg, x, y, this.scaledCharWidth, this.scaledCharHeight); } } } diff --git a/src/renderer/SelectionRenderLayer.ts b/src/renderer/SelectionRenderLayer.ts index a2cfb1d62a..602bb486bb 100644 --- a/src/renderer/SelectionRenderLayer.ts +++ b/src/renderer/SelectionRenderLayer.ts @@ -27,9 +27,6 @@ export class SelectionRenderLayer extends BaseRenderLayer implements ISelectionR } public render(terminal: ITerminal, start: [number, number], end: [number, number]): void { - const scaledCharWidth = Math.ceil(terminal.charMeasure.width) * window.devicePixelRatio; - const scaledCharHeight = Math.ceil(terminal.charMeasure.height) * window.devicePixelRatio; - // Selection has not changed if (this._state.start === start || this._state.end === end) { return; @@ -58,17 +55,17 @@ export class SelectionRenderLayer extends BaseRenderLayer implements ISelectionR const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0; const startRowEndCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : terminal.cols; this._ctx.fillStyle = 'rgba(255,255,255,0.3)'; - this._ctx.fillRect(startCol * scaledCharWidth, viewportCappedStartRow * scaledCharHeight, (startRowEndCol - startCol) * scaledCharWidth, scaledCharHeight); + this._ctx.fillRect(startCol * this.scaledCharWidth, viewportCappedStartRow * this.scaledCharHeight, (startRowEndCol - startCol) * this.scaledCharWidth, this.scaledCharHeight); // Draw middle rows const middleRowsCount = Math.max(viewportCappedEndRow - viewportCappedStartRow - 1, 0); - this._ctx.fillRect(0, (viewportCappedStartRow + 1) * scaledCharHeight, terminal.cols * scaledCharWidth, middleRowsCount * scaledCharHeight); + this._ctx.fillRect(0, (viewportCappedStartRow + 1) * this.scaledCharHeight, terminal.cols * this.scaledCharWidth, middleRowsCount * this.scaledCharHeight); // Draw final row if (viewportCappedStartRow !== viewportCappedEndRow) { // Only draw viewportEndRow if it's not the same as viewporttartRow const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : terminal.cols; - this._ctx.fillRect(0, viewportCappedEndRow * scaledCharHeight, endCol * scaledCharWidth, scaledCharHeight); + this._ctx.fillRect(0, viewportCappedEndRow * this.scaledCharHeight, endCol * this.scaledCharWidth, this.scaledCharHeight); } // Save state for next render From 510940389c2714ba985475c9f140b02705aa4646 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 31 Aug 2017 23:01:15 -0700 Subject: [PATCH 029/108] Provide convenience draw methods that deal with cells --- src/renderer/BackgroundRenderLayer.ts | 8 +++---- src/renderer/BaseRenderLayer.ts | 32 ++++++++++++++++++--------- src/renderer/CursorRenderLayer.ts | 8 +++---- src/renderer/ForegroundRenderLayer.ts | 16 ++++++++------ src/renderer/Interfaces.ts | 2 +- src/renderer/Renderer.ts | 4 ++-- src/renderer/SelectionRenderLayer.ts | 12 +++++----- 7 files changed, 47 insertions(+), 35 deletions(-) diff --git a/src/renderer/BackgroundRenderLayer.ts b/src/renderer/BackgroundRenderLayer.ts index 1e8169d765..a37c438752 100644 --- a/src/renderer/BackgroundRenderLayer.ts +++ b/src/renderer/BackgroundRenderLayer.ts @@ -19,9 +19,9 @@ export class BackgroundRenderLayer extends BaseRenderLayer implements IDataRende this._state.resize(terminal.cols, terminal.rows); } - public clear(terminal: ITerminal): void { + public reset(terminal: ITerminal): void { this._state.clear(); - this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); + this.clearAll(); } public render(terminal: ITerminal, startRow: number, endRow: number): void { @@ -48,11 +48,11 @@ export class BackgroundRenderLayer extends BaseRenderLayer implements IDataRende if (bg < 256) { this._ctx.save(); this._ctx.fillStyle = COLORS[bg]; - this._ctx.fillRect(x * this.scaledCharWidth, y * this.scaledCharHeight, this.scaledCharWidth, this.scaledCharHeight); + this.fillCells(x, y, 1, 1); this._ctx.restore(); this._state.cache[x][y] = bg; } else { - this._ctx.clearRect(x * this.scaledCharWidth, y * this.scaledCharHeight, this.scaledCharWidth, this.scaledCharHeight); + this.clearCells(x, y, 1, 1); this._state.cache[x][y] = null; } } diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 7790e03447..e286b22042 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -3,13 +3,13 @@ import { ITerminal } from '../Interfaces'; import { COLORS } from './Color'; export abstract class BaseRenderLayer implements IRenderLayer { - protected _canvas: HTMLCanvasElement; + private _canvas: HTMLCanvasElement; protected _ctx: CanvasRenderingContext2D; - protected scaledCharWidth: number; - protected scaledCharHeight: number; + private scaledCharWidth: number; + private scaledCharHeight: number; // TODO: This will apply to all terminals, should it be per-terminal? - protected static _charAtlas: ImageBitmap; + private static _charAtlas: ImageBitmap; private static _charAtlasCharWidth: number; private static _charAtlasCharHeight: number; private static _charAtlasGenerator: CharAtlasGenerator; @@ -50,9 +50,21 @@ export abstract class BaseRenderLayer implements IRenderLayer { } } - public abstract clear(terminal: ITerminal): void; + public abstract reset(terminal: ITerminal): void; + + protected fillCells(startCol: number, startRow: number, colWidth: number, colHeight: number): void { + this._ctx.fillRect(startCol * this.scaledCharWidth, startRow * this.scaledCharHeight, colWidth * this.scaledCharWidth, colHeight * this.scaledCharHeight); + } + + protected clearAll(): void { + this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); + } + + protected clearCells(startCol: number, startRow: number, colWidth: number, colHeight: number): void { + this._ctx.clearRect(startCol * this.scaledCharWidth, startRow * this.scaledCharHeight, colWidth * this.scaledCharWidth, colHeight * this.scaledCharHeight); + } - protected drawChar(terminal: ITerminal, char: string, code: number, fg: number, x: number, y: number, scaledCharWidth: number, scaledCharHeight: number): void { + protected drawChar(terminal: ITerminal, char: string, code: number, fg: number, x: number, y: number): void { let colorIndex = 0; if (fg < 256) { colorIndex = fg + 1; @@ -60,10 +72,10 @@ export abstract class BaseRenderLayer implements IRenderLayer { if (code < 256 && (colorIndex > 0 || fg > 255)) { // ImageBitmap's draw about twice as fast as from a canvas this._ctx.drawImage(BaseRenderLayer._charAtlas, - code * scaledCharWidth, colorIndex * scaledCharHeight, scaledCharWidth, scaledCharHeight, - x * scaledCharWidth, y * scaledCharHeight, scaledCharWidth, scaledCharHeight); + code * this.scaledCharWidth, colorIndex * this.scaledCharHeight, this.scaledCharWidth, this.scaledCharHeight, + x * this.scaledCharWidth, y * this.scaledCharHeight, this.scaledCharWidth, this.scaledCharHeight); } else { - this._drawUncachedChar(terminal, char, fg, x, y, scaledCharWidth, scaledCharHeight); + this._drawUncachedChar(terminal, char, fg, x, y, this.scaledCharWidth, this.scaledCharHeight); } // This draws the atlas (for debugging purposes) // this._ctx.drawImage(BaseRenderLayer._charAtlas, 0, 0); @@ -100,14 +112,12 @@ class CharAtlasGenerator { public generate(terminal: ITerminal, charWidth: number, charHeight: number): Promise { const scaledCharWidth = Math.ceil(charWidth) * window.devicePixelRatio; const scaledCharHeight = Math.ceil(charHeight) * window.devicePixelRatio; -console.log('generate'); this._canvas.width = 255 * scaledCharWidth; this._canvas.height = (/*default*/1 + /*0-15*/16) * scaledCharHeight; this._ctx.save(); this._ctx.fillStyle = '#ffffff'; this._ctx.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`; - console.log(this._ctx.font, scaledCharWidth, scaledCharHeight); this._ctx.textBaseline = 'top'; // Default color diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index bb0ff9284a..7b5be05224 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -14,7 +14,7 @@ export class CursorRenderLayer extends BaseRenderLayer implements IDataRenderLay this._state = null; } - public clear(terminal: ITerminal): void { + public reset(terminal: ITerminal): void { this._clearCursor(); } @@ -46,18 +46,18 @@ export class CursorRenderLayer extends BaseRenderLayer implements IDataRenderLay this._ctx.save(); this._ctx.fillStyle = COLORS[COLOR_CODES.WHITE]; - this._ctx.fillRect(terminal.buffer.x * this.scaledCharWidth, viewportRelativeCursorY * this.scaledCharHeight, this.scaledCharWidth, this.scaledCharHeight); + this.fillCells(terminal.buffer.x, viewportRelativeCursorY, 1, 1); this._ctx.restore(); const charData = terminal.buffer.lines.get(cursorY)[terminal.buffer.x]; - this.drawChar(terminal, charData[CHAR_DATA_CHAR_INDEX], charData[CHAR_DATA_CODE_INDEX], COLOR_CODES.BLACK, terminal.buffer.x, viewportRelativeCursorY, this.scaledCharWidth, this.scaledCharHeight); + this.drawChar(terminal, charData[CHAR_DATA_CHAR_INDEX], charData[CHAR_DATA_CODE_INDEX], COLOR_CODES.BLACK, terminal.buffer.x, viewportRelativeCursorY); this._state = [terminal.buffer.x, viewportRelativeCursorY]; } private _clearCursor(): void { if (this._state) { - this._ctx.clearRect(this._state[0] * this.scaledCharWidth, this._state[1] * this.scaledCharHeight, this.scaledCharWidth, this.scaledCharHeight); + this.clearCells(this._state[0], this._state[1], 1, 1); this._state = null; } } diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index 68d00857a8..e5b814ce4c 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -20,18 +20,18 @@ export class ForegroundRenderLayer extends BaseRenderLayer implements IDataRende this._state.resize(terminal.cols, terminal.rows); } - public clear(terminal: ITerminal): void { + public reset(terminal: ITerminal): void { this._state.clear(); - this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); + this.clearAll(); } public render(terminal: ITerminal, startRow: number, endRow: number): void { // TODO: Ensure that the render is eventually performed // Don't bother render until the atlas bitmap is ready // TODO: Move this to BaseRenderLayer? - if (!BaseRenderLayer._charAtlas) { - return; - } + // if (!BaseRenderLayer._charAtlas) { + // return; + // } for (let y = startRow; y <= endRow; y++) { const row = y + terminal.buffer.ydisp; @@ -53,7 +53,7 @@ export class ForegroundRenderLayer extends BaseRenderLayer implements IDataRende this._state.cache[x][y] = charData; // Clear the old character - this._ctx.clearRect(x * this.scaledCharWidth, y * this.scaledCharHeight, this.scaledCharWidth, this.scaledCharHeight); + this.clearCells(x, y, 1, 1); // Skip rendering if the character is invisible if (!code || code === 32 /*' '*/) { @@ -72,6 +72,7 @@ export class ForegroundRenderLayer extends BaseRenderLayer implements IDataRende } } + this._ctx.save(); if (flags & FLAGS.BOLD) { this._ctx.font = `bold ${this._ctx.font}`; // Convert the FG color to the bold variant @@ -80,7 +81,8 @@ export class ForegroundRenderLayer extends BaseRenderLayer implements IDataRende } } - this.drawChar(terminal, char, code, fg, x, y, this.scaledCharWidth, this.scaledCharHeight); + this.drawChar(terminal, char, code, fg, x, y); + this._ctx.restore(); } } } diff --git a/src/renderer/Interfaces.ts b/src/renderer/Interfaces.ts index 8eb3acdde7..82b2e77a21 100644 --- a/src/renderer/Interfaces.ts +++ b/src/renderer/Interfaces.ts @@ -9,7 +9,7 @@ export interface IRenderLayer { /** * Clear the state of the render layer. */ - clear(terminal: ITerminal): void; + reset(terminal: ITerminal): void; } export interface IDataRenderLayer extends IRenderLayer { diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 26b472cfda..a512c9834d 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -59,10 +59,10 @@ export class Renderer { public clear(): void { for (let i = 0; i < this._dataRenderLayers.length; i++) { - this._dataRenderLayers[i].clear(this._terminal); + this._dataRenderLayers[i].reset(this._terminal); } for (let i = 0; i < this._selectionRenderLayers.length; i++) { - this._selectionRenderLayers[i].clear(this._terminal); + this._selectionRenderLayers[i].reset(this._terminal); } } diff --git a/src/renderer/SelectionRenderLayer.ts b/src/renderer/SelectionRenderLayer.ts index 602bb486bb..daaf160d6c 100644 --- a/src/renderer/SelectionRenderLayer.ts +++ b/src/renderer/SelectionRenderLayer.ts @@ -16,13 +16,13 @@ export class SelectionRenderLayer extends BaseRenderLayer implements ISelectionR }; } - public clear(terminal: ITerminal): void { + public reset(terminal: ITerminal): void { if (this._state.start && this._state.end) { this._state = { start: null, end: null }; - this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); + this.clearAll(); } } @@ -33,7 +33,7 @@ export class SelectionRenderLayer extends BaseRenderLayer implements ISelectionR } // Remove all selections - this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); + this.clearAll(); // Selection does not exist if (!start || !end) { @@ -55,17 +55,17 @@ export class SelectionRenderLayer extends BaseRenderLayer implements ISelectionR const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0; const startRowEndCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : terminal.cols; this._ctx.fillStyle = 'rgba(255,255,255,0.3)'; - this._ctx.fillRect(startCol * this.scaledCharWidth, viewportCappedStartRow * this.scaledCharHeight, (startRowEndCol - startCol) * this.scaledCharWidth, this.scaledCharHeight); + this.fillCells(startCol, viewportCappedStartRow, startRowEndCol - startCol, 1); // Draw middle rows const middleRowsCount = Math.max(viewportCappedEndRow - viewportCappedStartRow - 1, 0); - this._ctx.fillRect(0, (viewportCappedStartRow + 1) * this.scaledCharHeight, terminal.cols * this.scaledCharWidth, middleRowsCount * this.scaledCharHeight); + this.fillCells(0, viewportCappedStartRow + 1, 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] : terminal.cols; - this._ctx.fillRect(0, viewportCappedEndRow * this.scaledCharHeight, endCol * this.scaledCharWidth, this.scaledCharHeight); + this.fillCells(0, viewportCappedEndRow, endCol, 1); } // Save state for next render From 42e5b3473dddaebe35af22ab9ea66dd356e549c5 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 31 Aug 2017 23:12:42 -0700 Subject: [PATCH 030/108] Don't clear fg char if it's null or ' ' --- src/Terminal.ts | 2 +- src/renderer/ForegroundRenderLayer.ts | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index c9c6466c71..640aafaaa5 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -2046,7 +2046,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT public blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData { const attr = cur ? this.eraseAttr() : this.defAttr; - const ch: CharData = [attr, ' ', 1]; // width defaults to 1 halfwidth character + const ch: CharData = [attr, ' ', 1, 32 /* ' '.charCodeAt(0) */]; // width defaults to 1 halfwidth character const line: LineData = []; // TODO: It is not ideal that this is a property on an array, a buffer line diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index e5b814ce4c..680f1abc5a 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -50,10 +50,12 @@ export class ForegroundRenderLayer extends BaseRenderLayer implements IDataRende this._state.cache[x][y] = charData; continue; } - this._state.cache[x][y] = charData; - // Clear the old character - this.clearCells(x, y, 1, 1); + // Clear the old character if present + if (state && state[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) { + this.clearCells(x, y, 1, 1); + } + this._state.cache[x][y] = charData; // Skip rendering if the character is invisible if (!code || code === 32 /*' '*/) { From 81d93b8fbf817c83cd7dbb918ad33bbd6fbb35b6 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 31 Aug 2017 23:23:04 -0700 Subject: [PATCH 031/108] Ensure CharMeasure exposes integers --- src/renderer/BaseRenderLayer.ts | 10 ++++------ src/renderer/Renderer.ts | 9 ++++----- src/utils/CharMeasure.ts | 4 ++-- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index e286b22042..331bc678ac 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -28,8 +28,8 @@ export abstract class BaseRenderLayer implements IRenderLayer { } public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void { - this.scaledCharWidth = Math.ceil(terminal.charMeasure.width) * window.devicePixelRatio; - this.scaledCharHeight = Math.ceil(terminal.charMeasure.height) * window.devicePixelRatio; + this.scaledCharWidth = terminal.charMeasure.width * window.devicePixelRatio; + this.scaledCharHeight = terminal.charMeasure.height * window.devicePixelRatio; this._canvas.width = canvasWidth * window.devicePixelRatio; this._canvas.height = canvasHeight * window.devicePixelRatio; this._canvas.style.width = `${canvasWidth}px`; @@ -43,7 +43,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { BaseRenderLayer._charAtlas = null; BaseRenderLayer._charAtlasCharWidth = terminal.charMeasure.width; BaseRenderLayer._charAtlasCharHeight = terminal.charMeasure.height; - BaseRenderLayer._charAtlasGenerator.generate(terminal, terminal.charMeasure.width, terminal.charMeasure.height).then(bitmap => { + BaseRenderLayer._charAtlasGenerator.generate(terminal, this.scaledCharWidth, this.scaledCharHeight).then(bitmap => { BaseRenderLayer._charAtlas = bitmap; }); } @@ -109,9 +109,7 @@ class CharAtlasGenerator { this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); } - public generate(terminal: ITerminal, charWidth: number, charHeight: number): Promise { - const scaledCharWidth = Math.ceil(charWidth) * window.devicePixelRatio; - const scaledCharHeight = Math.ceil(charHeight) * window.devicePixelRatio; + public generate(terminal: ITerminal, scaledCharWidth: number, scaledCharHeight: number): Promise { this._canvas.width = 255 * scaledCharWidth; this._canvas.height = (/*default*/1 + /*0-15*/16) * scaledCharHeight; diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index a512c9834d..2cce350b2f 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -32,17 +32,16 @@ export class Renderer { } public onResize(cols: number, rows: number): void { - const width = Math.ceil(this._terminal.charMeasure.width) * this._terminal.cols; - const height = Math.ceil(this._terminal.charMeasure.height) * this._terminal.rows; + const width = this._terminal.charMeasure.width * this._terminal.cols; + const height = this._terminal.charMeasure.height * this._terminal.rows; for (let i = 0; i < this._dataRenderLayers.length; i++) { this._dataRenderLayers[i].resize(this._terminal, width, height, false); } } public onCharSizeChanged(charWidth: number, charHeight: number): void { - console.log('Renderer.onCharSizeChanged', charWidth, charHeight); - const width = Math.ceil(charWidth) * this._terminal.cols; - const height = Math.ceil(charHeight) * this._terminal.rows; + const width = charWidth * this._terminal.cols; + const height = charHeight * this._terminal.rows; for (let i = 0; i < this._dataRenderLayers.length; i++) { this._dataRenderLayers[i].resize(this._terminal, width, height, true); } diff --git a/src/utils/CharMeasure.ts b/src/utils/CharMeasure.ts index ff777811ec..1366bb631f 100644 --- a/src/utils/CharMeasure.ts +++ b/src/utils/CharMeasure.ts @@ -59,8 +59,8 @@ export class CharMeasure extends EventEmitter implements ICharMeasure { return; } if (this._width !== geometry.width || this._height !== geometry.height) { - this._width = geometry.width; - this._height = geometry.height; + this._width = Math.ceil(geometry.width); + this._height = Math.ceil(geometry.height); this.emit('charsizechanged'); } } From 848c85c1ebf667080ea44664f25f04e87e56cc16 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 31 Aug 2017 23:28:42 -0700 Subject: [PATCH 032/108] Fix more cases where ' ' doesn't have code --- src/Terminal.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 640aafaaa5..92fc54402a 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1983,7 +1983,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT if (!line) { return; } - const ch: CharData = [this.eraseAttr(), ' ', 1]; // xterm + const ch: CharData = [this.eraseAttr(), ' ', 1, 32 /* ' '.charCodeAt(0) */]; // xterm for (; x < this.cols; x++) { line[x] = ch; } @@ -2000,7 +2000,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT if (!line) { return; } - const ch: CharData = [this.eraseAttr(), ' ', 1]; // xterm + const ch: CharData = [this.eraseAttr(), ' ', 1, 32 /* ' '.charCodeAt(0) */]; // xterm x++; while (x--) { line[x] = ch; @@ -2068,7 +2068,10 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * @param cur */ public ch(cur?: boolean): CharData { - return cur ? [this.eraseAttr(), ' ', 1] : [this.defAttr, ' ', 1]; + if (cur) { + return [this.eraseAttr(), ' ', 1, 32 /* ' '.charCodeAt(0) */]; + } + return [this.defAttr, ' ', 1, 32 /* ' '.charCodeAt(0) */]; } /** From 882d7e4a301c50eea93a41a40604bb49e8352301 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 1 Sep 2017 11:34:52 -0700 Subject: [PATCH 033/108] Add basic support for cursor blinking --- src/Terminal.ts | 1 + src/renderer/BaseRenderLayer.ts | 22 +++++++- src/renderer/CursorRenderLayer.ts | 88 ++++++++++++++++++++++++++++--- src/renderer/Interfaces.ts | 10 +++- src/renderer/Renderer.ts | 9 ++++ 5 files changed, 121 insertions(+), 9 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 92fc54402a..0c6eb65dc5 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -497,6 +497,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT case 'bellSound': case 'bellStyle': this.syncBellSound(); break; } + this.renderer.onOptionsChanged(); } private restartCursorBlinking(): void { diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 331bc678ac..047269c374 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -1,5 +1,5 @@ import { IRenderLayer } from './Interfaces'; -import { ITerminal } from '../Interfaces'; +import { ITerminal, ITerminalOptions } from '../Interfaces'; import { COLORS } from './Color'; export abstract class BaseRenderLayer implements IRenderLayer { @@ -27,6 +27,10 @@ export abstract class BaseRenderLayer implements IRenderLayer { } } + public onOptionsChanged(options: ITerminal): void { + // TODO: Should this do anything? + } + public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void { this.scaledCharWidth = terminal.charMeasure.width * window.devicePixelRatio; this.scaledCharHeight = terminal.charMeasure.height * window.devicePixelRatio; @@ -56,6 +60,22 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.fillRect(startCol * this.scaledCharWidth, startRow * this.scaledCharHeight, colWidth * this.scaledCharWidth, colHeight * this.scaledCharHeight); } + protected fillBottomLineAtCell(x: number, y: number): void { + this._ctx.fillRect( + x * this.scaledCharWidth, + (y + 1) * this.scaledCharHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */, + this.scaledCharWidth, + window.devicePixelRatio); + } + + protected fillLeftLineAtCell(x: number, y: number): void { + this._ctx.fillRect( + x * this.scaledCharWidth, + y * this.scaledCharHeight, + window.devicePixelRatio, + this.scaledCharHeight); + } + protected clearAll(): void { this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); } diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index 7b5be05224..89707590e3 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -1,28 +1,57 @@ import { IDataRenderLayer } from './Interfaces'; -import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; +import { IBuffer, ICharMeasure, ITerminal, ITerminalOptions } from '../Interfaces'; import { CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; import { COLORS, COLOR_CODES } from './Color'; import { GridCache } from './GridCache'; import { FLAGS } from './Types'; import { BaseRenderLayer } from './BaseRenderLayer'; +import { CharData } from '../Types'; + +/** + * The time between cursor blinks. + */ +const BLINK_INTERVAL = 600; export class CursorRenderLayer extends BaseRenderLayer implements IDataRenderLayer { private _state: [number, number]; + private _cursorRenderers: {[key: string]: (terminal: ITerminal, x: number, y: number, charData: CharData) => void}; + private _animationFrame: number; + private _blinkInterval: number; + private _isVisible: boolean; constructor(container: HTMLElement, zIndex: number) { super(container, 'cursor', zIndex); this._state = null; + this._isVisible = true; + this._cursorRenderers = { + 'bar': this._renderBarCursor.bind(this), + 'block': this._renderBlockCursor.bind(this), + 'underline': this._renderUnderlineCursor.bind(this) + }; } public reset(terminal: ITerminal): void { this._clearCursor(); + this._isVisible = true; + } + + public onOptionsChanged(terminal: ITerminal): void { + super.onOptionsChanged(terminal); + this._refreshBlinkState(terminal); } public render(terminal: ITerminal, startRow: number, endRow: number): void { + // Only render if the animation frame is not active + if (!this._blinkInterval) { + this._render(terminal, false); + } + } + + private _render(terminal: ITerminal, triggeredByAnimationFrame: boolean): void { // TODO: Track blur/focus somehow, support unfocused cursor // Don't draw the cursor if it's hidden - if (!terminal.cursorState || terminal.cursorHidden) { + if (!terminal.cursorState || terminal.cursorHidden || !this._isVisible) { this._clearCursor(); return; } @@ -44,14 +73,11 @@ export class CursorRenderLayer extends BaseRenderLayer implements IDataRenderLay this._clearCursor(); } + const charData = terminal.buffer.lines.get(cursorY)[terminal.buffer.x]; this._ctx.save(); this._ctx.fillStyle = COLORS[COLOR_CODES.WHITE]; - this.fillCells(terminal.buffer.x, viewportRelativeCursorY, 1, 1); + this._cursorRenderers[terminal.options.cursorStyle || 'block'](terminal, terminal.buffer.x, viewportRelativeCursorY, charData); this._ctx.restore(); - - const charData = terminal.buffer.lines.get(cursorY)[terminal.buffer.x]; - this.drawChar(terminal, charData[CHAR_DATA_CHAR_INDEX], charData[CHAR_DATA_CODE_INDEX], COLOR_CODES.BLACK, terminal.buffer.x, viewportRelativeCursorY); - this._state = [terminal.buffer.x, viewportRelativeCursorY]; } @@ -61,4 +87,52 @@ export class CursorRenderLayer extends BaseRenderLayer implements IDataRenderLay this._state = null; } } + + private _renderBarCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void { + this.fillLeftLineAtCell(x, y); + } + + private _renderBlockCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void { + this.fillCells(x, y, 1, 1); + this.drawChar(terminal, charData[CHAR_DATA_CHAR_INDEX], charData[CHAR_DATA_CODE_INDEX], COLOR_CODES.BLACK, x, y); + } + + private _renderUnderlineCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void { + this.fillBottomLineAtCell(x, y); + } + + private _refreshBlinkState(terminal: ITerminal): void { + if (terminal.options.cursorBlink) { + if (!this._blinkInterval) { + this._blinkInterval = setInterval(() => { + this._isVisible = !this._isVisible; + this._animationFrame = window.requestAnimationFrame(() => { + this._render(terminal, true); + this._animationFrame = null; + }); + }, BLINK_INTERVAL); + } + } else { + if (this._animationFrame) { + window.clearInterval(this._blinkInterval); + this._blinkInterval = null; + window.cancelAnimationFrame(this._animationFrame); + this._animationFrame = null; + this._isVisible = true; + } + } + } + + private _restartBlinkAnimation(terminal: ITerminal): void { + // TODO: Restart the blink animation when input is received + // How can this be done efficiently, without thrashing with restarting the timers? + } + + private _pauseBlinkAnimation(): void { + // TODO: Pause the blink animation on blur + } + + private _resumeBlinkAnimation(): void { + // TODO: Resume the blink animation on focus + } } diff --git a/src/renderer/Interfaces.ts b/src/renderer/Interfaces.ts index 82b2e77a21..55cffbc540 100644 --- a/src/renderer/Interfaces.ts +++ b/src/renderer/Interfaces.ts @@ -1,6 +1,8 @@ -import { ITerminal } from '../Interfaces'; +import { ITerminal, ITerminalOptions } from '../Interfaces'; export interface IRenderLayer { + onOptionsChanged(options: ITerminal): void; + /** * Resize the render layer. */ @@ -12,10 +14,16 @@ export interface IRenderLayer { reset(terminal: ITerminal): void; } +/** + * A render layer that renders when there is a data change. + */ export interface IDataRenderLayer extends IRenderLayer { render(terminal: ITerminal, startRow: number, endRow: number): void; } +/** + * A render layer that renders when there is a selection change. + */ export interface ISelectionRenderLayer extends IRenderLayer { render(terminal: ITerminal, start: [number, number], end: [number, number]): void; } diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 2cce350b2f..2903460a3f 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -56,6 +56,15 @@ export class Renderer { } } + public onOptionsChanged(): void { + for (let i = 0; i < this._dataRenderLayers.length; i++) { + this._dataRenderLayers[i].onOptionsChanged(this._terminal); + } + for (let i = 0; i < this._selectionRenderLayers.length; i++) { + this._selectionRenderLayers[i].onOptionsChanged(this._terminal); + } + } + public clear(): void { for (let i = 0; i < this._dataRenderLayers.length; i++) { this._dataRenderLayers[i].reset(this._terminal); From 3fc500b291de87731e160e3323b6de5070faffde Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 1 Sep 2017 12:11:07 -0700 Subject: [PATCH 034/108] Pull cursor animation state management into a helper class --- src/Interfaces.ts | 1 + src/renderer/CursorRenderLayer.ts | 78 ++++++++++++++++++++++--------- 2 files changed, 57 insertions(+), 22 deletions(-) diff --git a/src/Interfaces.ts b/src/Interfaces.ts index 1b1625bca3..afa2de58d7 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -47,6 +47,7 @@ export interface ITerminal extends IEventEmitter { reset(): void; showCursor(): void; blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData; + refresh(start: number, end: number): void; } /** diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index 89707590e3..11e56347a0 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -19,6 +19,15 @@ export class CursorRenderLayer extends BaseRenderLayer implements IDataRenderLay private _blinkInterval: number; private _isVisible: boolean; + /** + * The time at which the animation frame was restarted, this is used on the + * next render to restart the timers so they don't need to restart the timers + * multiple times over a short period. + */ + private _animationTimeRestarted: number; + + private _cursorBlinkStateManager: CursorBlinkStateManager; + constructor(container: HTMLElement, zIndex: number) { super(container, 'cursor', zIndex); this._state = null; @@ -28,6 +37,7 @@ export class CursorRenderLayer extends BaseRenderLayer implements IDataRenderLay 'block': this._renderBlockCursor.bind(this), 'underline': this._renderUnderlineCursor.bind(this) }; + // TODO: Consider initial options? Maybe onOptionsChanged should be called at the end of open? } public reset(terminal: ITerminal): void { @@ -37,21 +47,36 @@ export class CursorRenderLayer extends BaseRenderLayer implements IDataRenderLay public onOptionsChanged(terminal: ITerminal): void { super.onOptionsChanged(terminal); - this._refreshBlinkState(terminal); + if (terminal.options.cursorBlink) { + if (!this._cursorBlinkStateManager) { + this._cursorBlinkStateManager = new CursorBlinkStateManager(terminal, () => { + this._render(terminal, true); + }); + } + } else { + if (this._cursorBlinkStateManager) { + this._cursorBlinkStateManager.dispose(); + this._cursorBlinkStateManager = null; + } + // Request a refresh from the terminal as management of rendering is being + // moved back to the terminal + terminal.refresh(terminal.buffer.y, terminal.buffer.y); + } } public render(terminal: ITerminal, startRow: number, endRow: number): void { // Only render if the animation frame is not active - if (!this._blinkInterval) { + if (!this._cursorBlinkStateManager) { this._render(terminal, false); } } private _render(terminal: ITerminal, triggeredByAnimationFrame: boolean): void { // TODO: Track blur/focus somehow, support unfocused cursor - // Don't draw the cursor if it's hidden - if (!terminal.cursorState || terminal.cursorHidden || !this._isVisible) { + if (!terminal.cursorState || + terminal.cursorHidden || + (this._cursorBlinkStateManager && !this._cursorBlinkStateManager.isCursorVisible)) { this._clearCursor(); return; } @@ -100,32 +125,41 @@ export class CursorRenderLayer extends BaseRenderLayer implements IDataRenderLay private _renderUnderlineCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void { this.fillBottomLineAtCell(x, y); } +} - private _refreshBlinkState(terminal: ITerminal): void { - if (terminal.options.cursorBlink) { - if (!this._blinkInterval) { - this._blinkInterval = setInterval(() => { - this._isVisible = !this._isVisible; - this._animationFrame = window.requestAnimationFrame(() => { - this._render(terminal, true); - this._animationFrame = null; - }); - }, BLINK_INTERVAL); - } - } else { - if (this._animationFrame) { - window.clearInterval(this._blinkInterval); - this._blinkInterval = null; - window.cancelAnimationFrame(this._animationFrame); +class CursorBlinkStateManager { + public isCursorVisible: boolean; + + private _animationFrame: number; + private _blinkInterval: number; + + constructor( + terminal: ITerminal, + private renderCallback: () => void + ) { + this.isCursorVisible = true; + this._blinkInterval = setInterval(() => { + this.isCursorVisible = !this.isCursorVisible; + this._animationFrame = window.requestAnimationFrame(() => { + this.renderCallback(); this._animationFrame = null; - this._isVisible = true; - } + }); + }, BLINK_INTERVAL); + } + + public dispose(): void { + window.clearInterval(this._blinkInterval); + this._blinkInterval = null; + if (this._animationFrame) { + window.cancelAnimationFrame(this._animationFrame); + this._animationFrame = null; } } private _restartBlinkAnimation(terminal: ITerminal): void { // TODO: Restart the blink animation when input is received // How can this be done efficiently, without thrashing with restarting the timers? + // Could record the time it was restarted and diff that on next render? } private _pauseBlinkAnimation(): void { From 076156b66e927814db251d5a52f04f5e95f1b833 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 1 Sep 2017 12:59:18 -0700 Subject: [PATCH 035/108] Properly support blinking cursors --- src/Buffer.ts | 4 +- src/Parser.ts | 10 +++ src/Terminal.ts | 4 ++ src/renderer/BaseRenderLayer.ts | 4 +- src/renderer/CursorRenderLayer.ts | 109 +++++++++++++++++++++++------- src/renderer/Interfaces.ts | 1 + src/renderer/Renderer.ts | 9 +++ 7 files changed, 113 insertions(+), 28 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index d8311f0675..101244f1f8 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -34,8 +34,8 @@ export class Buffer implements IBuffer { /** * Create a new Buffer. * @param _terminal The terminal the Buffer will belong to. - * @param _hasScrollback Whether the buffer should respecr the scrollback of - * the terminal.. + * @param _hasScrollback Whether the buffer should respect the scrollback of + * the terminal. */ constructor( private _terminal: ITerminal, diff --git a/src/Parser.ts b/src/Parser.ts index 983b3536e6..b047f91f7a 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -189,6 +189,9 @@ export class Parser { let code; let low; + const cursorStartX = this._terminal.buffer.x; + const cursorStartY = this._terminal.buffer.y; + if (this._terminal.debug) { this._terminal.log('data: ' + data); } @@ -580,6 +583,13 @@ export class Parser { break; } } + + // Fire the cursormove event if it's moved. This is done inside the parser + // as a render cannot happen in the middle of a parsing round. + if (this._terminal.buffer.x !== cursorStartX || this._terminal.buffer.y !== cursorStartY) { + this._terminal.emit('cursormove'); + } + return this._state; } diff --git a/src/Terminal.ts b/src/Terminal.ts index 0c6eb65dc5..17fd0d9766 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -754,6 +754,10 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure); this.renderer = new Renderer(this); + this.on('cursormove', () => { + console.log('cursormove fired'); + this.renderer.onCursorMove(); + }); this.on('resize', () => this.renderer.onResize(this.cols, this.rows)); this.charMeasure.on('charsizechanged', () => { this.renderer.onCharSizeChanged(this.charMeasure.width, this.charMeasure.height); diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 047269c374..378ac1a16d 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -27,9 +27,9 @@ export abstract class BaseRenderLayer implements IRenderLayer { } } - public onOptionsChanged(options: ITerminal): void { // TODO: Should this do anything? - } + public onOptionsChanged(options: ITerminal): void {} + public onCursorMove(options: ITerminal): void {} public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void { this.scaledCharWidth = terminal.charMeasure.width * window.devicePixelRatio; diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index 11e56347a0..3ccf2ec260 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -15,23 +15,11 @@ const BLINK_INTERVAL = 600; export class CursorRenderLayer extends BaseRenderLayer implements IDataRenderLayer { private _state: [number, number]; private _cursorRenderers: {[key: string]: (terminal: ITerminal, x: number, y: number, charData: CharData) => void}; - private _animationFrame: number; - private _blinkInterval: number; - private _isVisible: boolean; - - /** - * The time at which the animation frame was restarted, this is used on the - * next render to restart the timers so they don't need to restart the timers - * multiple times over a short period. - */ - private _animationTimeRestarted: number; - private _cursorBlinkStateManager: CursorBlinkStateManager; constructor(container: HTMLElement, zIndex: number) { super(container, 'cursor', zIndex); this._state = null; - this._isVisible = true; this._cursorRenderers = { 'bar': this._renderBarCursor.bind(this), 'block': this._renderBlockCursor.bind(this), @@ -42,7 +30,11 @@ export class CursorRenderLayer extends BaseRenderLayer implements IDataRenderLay public reset(terminal: ITerminal): void { this._clearCursor(); - this._isVisible = true; + if (this._cursorBlinkStateManager) { + this._cursorBlinkStateManager.dispose(); + this._cursorBlinkStateManager = null; + this.onOptionsChanged(terminal); + } } public onOptionsChanged(terminal: ITerminal): void { @@ -64,6 +56,12 @@ export class CursorRenderLayer extends BaseRenderLayer implements IDataRenderLay } } + public onCursorMove(terminal: ITerminal): void { + if (this._cursorBlinkStateManager) { + this._cursorBlinkStateManager.restartBlinkAnimation(terminal); + } + } + public render(terminal: ITerminal, startRow: number, endRow: number): void { // Only render if the animation frame is not active if (!this._cursorBlinkStateManager) { @@ -131,20 +129,22 @@ class CursorBlinkStateManager { public isCursorVisible: boolean; private _animationFrame: number; + private _blinkStartTimeout: number; private _blinkInterval: number; + /** + * The time at which the animation frame was restarted, this is used on the + * next render to restart the timers so they don't need to restart the timers + * multiple times over a short period. + */ + private _animationTimeRestarted: number; + constructor( terminal: ITerminal, private renderCallback: () => void ) { this.isCursorVisible = true; - this._blinkInterval = setInterval(() => { - this.isCursorVisible = !this.isCursorVisible; - this._animationFrame = window.requestAnimationFrame(() => { - this.renderCallback(); - this._animationFrame = null; - }); - }, BLINK_INTERVAL); + this._restartInterval(); } public dispose(): void { @@ -156,10 +156,71 @@ class CursorBlinkStateManager { } } - private _restartBlinkAnimation(terminal: ITerminal): void { - // TODO: Restart the blink animation when input is received - // How can this be done efficiently, without thrashing with restarting the timers? - // Could record the time it was restarted and diff that on next render? + public restartBlinkAnimation(terminal: ITerminal): void { + console.log('restartBlinkAnimation'); + // Save a timestamp so that the restart can be done on the next interval + this._animationTimeRestarted = Date.now(); + // Force a cursor render to ensure it's visible and in the correct position + this.isCursorVisible = true; + if (!this._animationFrame) { + this._animationFrame = window.requestAnimationFrame(() => { + this.renderCallback(); + this._animationFrame = null; + }); + } + } + + private _restartInterval(timeToStart: number = BLINK_INTERVAL): void { + // Clear any existing interval + if (this._blinkInterval) { + window.clearInterval(this._blinkInterval); + } + + console.log('restartInterval'); + // Setup the initial timeout which will hide the cursor, this is done before + // the regular interval is setup in order to support restarting the blink + // animation in a lightweight way (without thrashing clearInterval and + // setInterval). + this._blinkStartTimeout = setTimeout(() => { + // Check if another animation restart was requested while this was being + // started + if (this._animationTimeRestarted) { + const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted); + this._animationTimeRestarted = null; + this._restartInterval(time); + return; + } + + console.log('timeout'); + // Hide the cursor + this.isCursorVisible = false; + this._animationFrame = window.requestAnimationFrame(() => { + this.renderCallback(); + this._animationFrame = null; + }); + + // Setup the blink interval + this._blinkInterval = setInterval(() => { + console.log('interval'); + // Adjust the animation time if it was restarted + if (this._animationTimeRestarted) { + // calc time diff + // Make restart interval do a setTimeout initially? + const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted); + this._animationTimeRestarted = null; + console.log(' restart in ', time); + this._restartInterval(time); + return; + } + + // Invert visibility and render + this.isCursorVisible = !this.isCursorVisible; + this._animationFrame = window.requestAnimationFrame(() => { + this.renderCallback(); + this._animationFrame = null; + }); + }, BLINK_INTERVAL); + }, timeToStart); } private _pauseBlinkAnimation(): void { diff --git a/src/renderer/Interfaces.ts b/src/renderer/Interfaces.ts index 55cffbc540..5da13109ef 100644 --- a/src/renderer/Interfaces.ts +++ b/src/renderer/Interfaces.ts @@ -1,6 +1,7 @@ import { ITerminal, ITerminalOptions } from '../Interfaces'; export interface IRenderLayer { + onCursorMove(options: ITerminal): void; onOptionsChanged(options: ITerminal): void; /** diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 2903460a3f..7c6f6d4009 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -56,6 +56,15 @@ export class Renderer { } } + public onCursorMove(): void { + for (let i = 0; i < this._dataRenderLayers.length; i++) { + this._dataRenderLayers[i].onCursorMove(this._terminal); + } + for (let i = 0; i < this._selectionRenderLayers.length; i++) { + this._selectionRenderLayers[i].onCursorMove(this._terminal); + } + } + public onOptionsChanged(): void { for (let i = 0; i < this._dataRenderLayers.length; i++) { this._dataRenderLayers[i].onOptionsChanged(this._terminal); From 62fea4d3edfb7f80f7d23f89253cacef26077fd2 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 1 Sep 2017 15:50:18 -0700 Subject: [PATCH 036/108] Add theme support --- src/Interfaces.ts | 22 ++++++ src/Terminal.ts | 9 ++- src/renderer/BackgroundRenderLayer.ts | 9 +-- src/renderer/BaseRenderLayer.ts | 102 +++++++------------------ src/renderer/Color.ts | 70 ----------------- src/renderer/ColorManager.ts | 104 ++++++++++++++++++++++++++ src/renderer/CursorRenderLayer.ts | 15 ++-- src/renderer/ForegroundRenderLayer.ts | 7 +- src/renderer/Interfaces.ts | 12 ++- src/renderer/Renderer.ts | 32 ++++++-- src/renderer/SelectionRenderLayer.ts | 6 +- src/renderer/Types.ts | 5 -- src/utils/CharAtlas.ts | 66 ++++++++++++++++ 13 files changed, 278 insertions(+), 181 deletions(-) delete mode 100644 src/renderer/Color.ts create mode 100644 src/renderer/ColorManager.ts create mode 100644 src/utils/CharAtlas.ts diff --git a/src/Interfaces.ts b/src/Interfaces.ts index afa2de58d7..3a0c99297b 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -294,3 +294,25 @@ export interface IInputHandler { /** CSI s */ saveCursor(params?: number[]): void; /** CSI u */ restoreCursor(params?: number[]): void; } + +export interface ITheme { + foreground?: string; + background?: string; + // cursor?: string; + black?: string; + red?: string; + green?: string; + yellow?: string; + blue?: string; + magenta?: string; + cyan?: string; + white?: string; + brightBlack?: string; + brightRed?: string; + brightGreen?: string; + brightYellow?: string; + brightBlue?: string; + brightMagenta?: string; + brightCyan?: string; + brightWhite?: string; +} diff --git a/src/Terminal.ts b/src/Terminal.ts index 17fd0d9766..a663323521 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -39,7 +39,7 @@ import * as Mouse from './utils/Mouse'; import { CHARSETS } from './Charsets'; import { getRawByteCoords } from './utils/Mouse'; import { CustomKeyEventHandler, Charset, LinkMatcherHandler, LinkMatcherValidationCallback, CharData, LineData } from './Types'; -import { ITerminal, IBrowser, ITerminalOptions, IInputHandlingTerminal, ILinkMatcherOptions, IViewport, ICompositionHelper } from './Interfaces'; +import { ITerminal, IBrowser, ITerminalOptions, IInputHandlingTerminal, ILinkMatcherOptions, IViewport, ICompositionHelper, ITheme } from './Interfaces'; import { BellSound } from './utils/Sounds'; // Declare for RequireJS in loadAddon @@ -412,6 +412,13 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.textarea.focus(); } + public setTheme(theme: ITheme): void { + // TODO: Allow setting of theme before renderer is ready + if (this.renderer) { + this.renderer.setTheme(theme); + } + } + /** * Retrieves an option's value from the terminal. * @param {string} key The option key. diff --git a/src/renderer/BackgroundRenderLayer.ts b/src/renderer/BackgroundRenderLayer.ts index a37c438752..cf122cc23d 100644 --- a/src/renderer/BackgroundRenderLayer.ts +++ b/src/renderer/BackgroundRenderLayer.ts @@ -1,7 +1,6 @@ -import { IDataRenderLayer } from './Interfaces'; +import { IDataRenderLayer, IColorSet } from './Interfaces'; import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; -import { COLORS } from './Color'; import { GridCache } from './GridCache'; import { FLAGS } from './Types'; import { BaseRenderLayer } from './BaseRenderLayer'; @@ -9,8 +8,8 @@ import { BaseRenderLayer } from './BaseRenderLayer'; export class BackgroundRenderLayer extends BaseRenderLayer implements IDataRenderLayer { private _state: GridCache; - constructor(container: HTMLElement, zIndex: number) { - super(container, 'bg', zIndex); + constructor(container: HTMLElement, zIndex: number, colors: IColorSet) { + super(container, 'bg', zIndex, colors); this._state = new GridCache(); } @@ -47,7 +46,7 @@ export class BackgroundRenderLayer extends BaseRenderLayer implements IDataRende if (needsRefresh) { if (bg < 256) { this._ctx.save(); - this._ctx.fillStyle = COLORS[bg]; + this._ctx.fillStyle = this.colors.ansi[bg]; this.fillCells(x, y, 1, 1); this._ctx.restore(); this._state.cache[x][y] = bg; diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 378ac1a16d..d131bee0fb 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -1,6 +1,6 @@ -import { IRenderLayer } from './Interfaces'; +import { IRenderLayer, IColorSet } from './Interfaces'; import { ITerminal, ITerminalOptions } from '../Interfaces'; -import { COLORS } from './Color'; +import { acquireCharAtlas } from '../utils/CharAtlas'; export abstract class BaseRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; @@ -8,28 +8,34 @@ export abstract class BaseRenderLayer implements IRenderLayer { private scaledCharWidth: number; private scaledCharHeight: number; - // TODO: This will apply to all terminals, should it be per-terminal? - private static _charAtlas: ImageBitmap; - private static _charAtlasCharWidth: number; - private static _charAtlasCharHeight: number; - private static _charAtlasGenerator: CharAtlasGenerator; + // TODO: This should be shared between terminals, but not for static as some + // terminals may have different styles + private _charAtlas: ImageBitmap; - constructor(container: HTMLElement, id: string, zIndex: number) { + constructor( + container: HTMLElement, + id: string, + zIndex: number, + protected colors: IColorSet + ) { this._canvas = document.createElement('canvas'); this._canvas.id = `xterm-${id}-layer`; this._canvas.style.zIndex = zIndex.toString(); this._ctx = this._canvas.getContext('2d'); this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); container.appendChild(this._canvas); - - if (!BaseRenderLayer._charAtlasGenerator) { - BaseRenderLayer._charAtlasGenerator = new CharAtlasGenerator(); - } } - // TODO: Should this do anything? - public onOptionsChanged(options: ITerminal): void {} - public onCursorMove(options: ITerminal): void {} + // TODO: Should this do anything? + public onOptionsChanged(terminal: ITerminal): void {} + public onCursorMove(terminal: ITerminal): void {} + + public onThemeChanged(terminal: ITerminal, colorSet: IColorSet): void { + this._charAtlas = null; + acquireCharAtlas(terminal, this.colors).then(bitmap => { + this._charAtlas = bitmap; + }); + } public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void { this.scaledCharWidth = terminal.charMeasure.width * window.devicePixelRatio; @@ -40,17 +46,9 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._canvas.style.height = `${canvasHeight}px`; if (charSizeChanged) { - // Only update the char atlas if an update for the right dimensions is not - // already in progress - if (BaseRenderLayer._charAtlasCharWidth !== terminal.charMeasure.width || - BaseRenderLayer._charAtlasCharHeight !== terminal.charMeasure.height) { - BaseRenderLayer._charAtlas = null; - BaseRenderLayer._charAtlasCharWidth = terminal.charMeasure.width; - BaseRenderLayer._charAtlasCharHeight = terminal.charMeasure.height; - BaseRenderLayer._charAtlasGenerator.generate(terminal, this.scaledCharWidth, this.scaledCharHeight).then(bitmap => { - BaseRenderLayer._charAtlas = bitmap; - }); - } + acquireCharAtlas(terminal, this.colors).then(bitmap => { + this._charAtlas = bitmap; + }); } } @@ -91,7 +89,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { } if (code < 256 && (colorIndex > 0 || fg > 255)) { // ImageBitmap's draw about twice as fast as from a canvas - this._ctx.drawImage(BaseRenderLayer._charAtlas, + this._ctx.drawImage(this._charAtlas, code * this.scaledCharWidth, colorIndex * this.scaledCharHeight, this.scaledCharWidth, this.scaledCharHeight, x * this.scaledCharWidth, y * this.scaledCharHeight, this.scaledCharWidth, this.scaledCharHeight); } else { @@ -108,7 +106,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { // 256 color support if (fg < 256) { - this._ctx.fillStyle = COLORS[fg]; + this._ctx.fillStyle = this.colors.ansi[fg]; } else { this._ctx.fillStyle = '#ffffff'; } @@ -119,51 +117,3 @@ export abstract class BaseRenderLayer implements IRenderLayer { } } -class CharAtlasGenerator { - private _canvas: HTMLCanvasElement; - private _ctx: CanvasRenderingContext2D; - - constructor() { - this._canvas = document.createElement('canvas'); - this._ctx = this._canvas.getContext('2d'); - this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); - } - - public generate(terminal: ITerminal, scaledCharWidth: number, scaledCharHeight: number): Promise { - this._canvas.width = 255 * scaledCharWidth; - this._canvas.height = (/*default*/1 + /*0-15*/16) * scaledCharHeight; - - this._ctx.save(); - this._ctx.fillStyle = '#ffffff'; - this._ctx.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`; - this._ctx.textBaseline = 'top'; - - // Default color - for (let i = 0; i < 256; i++) { - this._ctx.fillText(String.fromCharCode(i), i * scaledCharWidth, 0); - } - - // Colors 0-15 - for (let colorIndex = 0; colorIndex < 16; colorIndex++) { - // colors 8-15 are bold - if (colorIndex === 8) { - this._ctx.font = `bold ${this._ctx.font}`; - } - const y = (colorIndex + 1) * scaledCharHeight; - // Clear rectangle as some fonts seem to draw over the bottom boundary - this._ctx.clearRect(0, y, this._canvas.width, scaledCharHeight); - // Draw ascii characters - for (let i = 0; i < 256; i++) { - this._ctx.fillStyle = COLORS[colorIndex]; - this._ctx.fillText(String.fromCharCode(i), i * scaledCharWidth, y); - } - } - this._ctx.restore(); - - const charAtlasImageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); - const promise = window.createImageBitmap(charAtlasImageData); - // Clear the rect while the promise is in progress - this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); - return promise; - } -} diff --git a/src/renderer/Color.ts b/src/renderer/Color.ts deleted file mode 100644 index 446d440b36..0000000000 --- a/src/renderer/Color.ts +++ /dev/null @@ -1,70 +0,0 @@ -// TODO: Ideally colors would be exposed through some theme manager since colors -// are moving to JS. - -export enum COLOR_CODES { - BLACK = 0, - RED = 1, - GREEN = 2, - YELLOW = 3, - BLUE = 4, - MAGENTA = 5, - CYAN = 6, - WHITE = 7, - BRIGHT_BLACK = 8, - BRIGHT_RED = 9, - BRIGHT_GREEN = 10, - BRIGHT_YELLOW = 11, - BRIGHT_BLUE = 12, - BRIGHT_MAGENTA = 13, - BRIGHT_CYAN = 14, - BRIGHT_WHITE = 15 -} - -const TANGO_COLORS = [ - // dark: - '#2e3436', - '#cc0000', - '#4e9a06', - '#c4a000', - '#3465a4', - '#75507b', - '#06989a', - '#d3d7cf', - // bright: - '#555753', - '#ef2929', - '#8ae234', - '#fce94f', - '#729fcf', - '#ad7fa8', - '#34e2e2', - '#eeeeec' -]; - -export const COLORS: string[] = generate256Colors(TANGO_COLORS); - -function generate256Colors(first16Colors: string[]): string[] { - let colors = first16Colors.slice(); - - // Generate colors (16-231) - let v = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]; - for (let i = 0; i < 216; i++) { - const r = toPaddedHex(v[(i / 36) % 6 | 0]); - const g = toPaddedHex(v[(i / 6) % 6 | 0]); - const b = toPaddedHex(v[i % 6]); - colors.push(`#${r}${g}${b}`); - } - - // Generate greys (232-255) - for (let i = 0; i < 24; i++) { - const c = toPaddedHex(8 + i * 10); - colors.push(`#${c}${c}${c}`); - } - - return colors; -} - -function toPaddedHex(c: number): string { - let s = c.toString(16); - return s.length < 2 ? '0' + s : s; -} diff --git a/src/renderer/ColorManager.ts b/src/renderer/ColorManager.ts new file mode 100644 index 0000000000..ca8015a2b4 --- /dev/null +++ b/src/renderer/ColorManager.ts @@ -0,0 +1,104 @@ +import { IColorSet } from './Interfaces'; +import { ITheme } from '../Interfaces'; + +// TODO: Ideally colors would be exposed through some theme manager since colors +// are moving to JS. + +export enum COLOR_CODES { + BLACK = 0, + RED = 1, + GREEN = 2, + YELLOW = 3, + BLUE = 4, + MAGENTA = 5, + CYAN = 6, + WHITE = 7, + BRIGHT_BLACK = 8, + BRIGHT_RED = 9, + BRIGHT_GREEN = 10, + BRIGHT_YELLOW = 11, + BRIGHT_BLUE = 12, + BRIGHT_MAGENTA = 13, + BRIGHT_CYAN = 14, + BRIGHT_WHITE = 15 +} + +const DEFAULT_ANSI_COLORS = [ + // dark: + '#2e3436', + '#cc0000', + '#4e9a06', + '#c4a000', + '#3465a4', + '#75507b', + '#06989a', + '#d3d7cf', + // bright: + '#555753', + '#ef2929', + '#8ae234', + '#fce94f', + '#729fcf', + '#ad7fa8', + '#34e2e2', + '#eeeeec' +]; + +function generate256Colors(first16Colors: string[]): string[] { + let colors = first16Colors.slice(); + + // Generate colors (16-231) + let v = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]; + for (let i = 0; i < 216; i++) { + const r = toPaddedHex(v[(i / 36) % 6 | 0]); + const g = toPaddedHex(v[(i / 6) % 6 | 0]); + const b = toPaddedHex(v[i % 6]); + colors.push(`#${r}${g}${b}`); + } + + // Generate greys (232-255) + for (let i = 0; i < 24; i++) { + const c = toPaddedHex(8 + i * 10); + colors.push(`#${c}${c}${c}`); + } + + return colors; +} + +function toPaddedHex(c: number): string { + let s = c.toString(16); + return s.length < 2 ? '0' + s : s; +} + +export class ColorManager { + public colors: IColorSet; + + constructor() { + this.colors = { + foreground: '#ffffff', + background: '#000000', + ansi: generate256Colors(DEFAULT_ANSI_COLORS) + }; + } + + public setTheme(theme: ITheme): void { + if (theme.foreground) this.colors.foreground = theme.foreground; + if (theme.background) this.colors.background = theme.background; + if (theme.black) this.colors.ansi[0] = theme.black; + if (theme.red) this.colors.ansi[1] = theme.red; + if (theme.green) this.colors.ansi[2] = theme.green; + if (theme.yellow) this.colors.ansi[3] = theme.yellow; + if (theme.blue) this.colors.ansi[4] = theme.blue; + if (theme.magenta) this.colors.ansi[5] = theme.magenta; + if (theme.cyan) this.colors.ansi[6] = theme.cyan; + if (theme.white) this.colors.ansi[7] = theme.white; + if (theme.brightBlack) this.colors.ansi[8] = theme.brightBlack; + if (theme.brightRed) this.colors.ansi[9] = theme.brightRed; + if (theme.brightGreen) this.colors.ansi[10] = theme.brightGreen; + if (theme.brightYellow) this.colors.ansi[11] = theme.brightYellow; + if (theme.brightBlue) this.colors.ansi[12] = theme.brightBlue; + if (theme.brightMagenta) this.colors.ansi[13] = theme.brightMagenta; + if (theme.brightCyan) this.colors.ansi[14] = theme.brightCyan; + if (theme.brightWhite) this.colors.ansi[15] = theme.brightWhite; + } +} diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index 3ccf2ec260..bfec1d8e94 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -1,11 +1,11 @@ -import { IDataRenderLayer } from './Interfaces'; +import { IDataRenderLayer, IColorSet } from './Interfaces'; import { IBuffer, ICharMeasure, ITerminal, ITerminalOptions } from '../Interfaces'; import { CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; -import { COLORS, COLOR_CODES } from './Color'; import { GridCache } from './GridCache'; import { FLAGS } from './Types'; import { BaseRenderLayer } from './BaseRenderLayer'; import { CharData } from '../Types'; +import { COLOR_CODES } from './ColorManager'; /** * The time between cursor blinks. @@ -17,8 +17,8 @@ export class CursorRenderLayer extends BaseRenderLayer implements IDataRenderLay private _cursorRenderers: {[key: string]: (terminal: ITerminal, x: number, y: number, charData: CharData) => void}; private _cursorBlinkStateManager: CursorBlinkStateManager; - constructor(container: HTMLElement, zIndex: number) { - super(container, 'cursor', zIndex); + constructor(container: HTMLElement, zIndex: number, colors: IColorSet) { + super(container, 'cursor', zIndex, colors); this._state = null; this._cursorRenderers = { 'bar': this._renderBarCursor.bind(this), @@ -98,7 +98,7 @@ export class CursorRenderLayer extends BaseRenderLayer implements IDataRenderLay const charData = terminal.buffer.lines.get(cursorY)[terminal.buffer.x]; this._ctx.save(); - this._ctx.fillStyle = COLORS[COLOR_CODES.WHITE]; + this._ctx.fillStyle = this.colors.ansi[COLOR_CODES.WHITE]; this._cursorRenderers[terminal.options.cursorStyle || 'block'](terminal, terminal.buffer.x, viewportRelativeCursorY, charData); this._ctx.restore(); this._state = [terminal.buffer.x, viewportRelativeCursorY]; @@ -157,7 +157,6 @@ class CursorBlinkStateManager { } public restartBlinkAnimation(terminal: ITerminal): void { - console.log('restartBlinkAnimation'); // Save a timestamp so that the restart can be done on the next interval this._animationTimeRestarted = Date.now(); // Force a cursor render to ensure it's visible and in the correct position @@ -176,7 +175,6 @@ class CursorBlinkStateManager { window.clearInterval(this._blinkInterval); } - console.log('restartInterval'); // Setup the initial timeout which will hide the cursor, this is done before // the regular interval is setup in order to support restarting the blink // animation in a lightweight way (without thrashing clearInterval and @@ -191,7 +189,6 @@ class CursorBlinkStateManager { return; } - console.log('timeout'); // Hide the cursor this.isCursorVisible = false; this._animationFrame = window.requestAnimationFrame(() => { @@ -201,14 +198,12 @@ class CursorBlinkStateManager { // Setup the blink interval this._blinkInterval = setInterval(() => { - console.log('interval'); // Adjust the animation time if it was restarted if (this._animationTimeRestarted) { // calc time diff // Make restart interval do a setTimeout initially? const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted); this._animationTimeRestarted = null; - console.log(' restart in ', time); this._restartInterval(time); return; } diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index 680f1abc5a..fa4ef80074 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -1,7 +1,6 @@ -import { IDataRenderLayer } from './Interfaces'; +import { IDataRenderLayer, IColorSet } from './Interfaces'; import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../Buffer'; -import { COLORS } from './Color'; import { FLAGS } from './Types'; import { GridCache } from './GridCache'; import { CharData } from '../Types'; @@ -10,8 +9,8 @@ import { BaseRenderLayer } from './BaseRenderLayer'; export class ForegroundRenderLayer extends BaseRenderLayer implements IDataRenderLayer { private _state: GridCache; - constructor(container: HTMLElement, zIndex: number) { - super(container, 'fg', zIndex); + constructor(container: HTMLElement, zIndex: number, colors: IColorSet) { + super(container, 'fg', zIndex, colors); this._state = new GridCache(); } diff --git a/src/renderer/Interfaces.ts b/src/renderer/Interfaces.ts index 5da13109ef..4b8ce816f2 100644 --- a/src/renderer/Interfaces.ts +++ b/src/renderer/Interfaces.ts @@ -1,8 +1,9 @@ import { ITerminal, ITerminalOptions } from '../Interfaces'; export interface IRenderLayer { - onCursorMove(options: ITerminal): void; - onOptionsChanged(options: ITerminal): void; + onCursorMove(terminal: ITerminal): void; + onOptionsChanged(terminal: ITerminal): void; + onThemeChanged(terminal: ITerminal, colorSet: IColorSet): void; /** * Resize the render layer. @@ -28,3 +29,10 @@ export interface IDataRenderLayer extends IRenderLayer { export interface ISelectionRenderLayer extends IRenderLayer { render(terminal: ITerminal, start: [number, number], end: [number, number]): void; } + + +export interface IColorSet { + foreground: string; + background: string; + ansi: string[]; +} diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 7c6f6d4009..653e2cbafe 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -2,7 +2,7 @@ * @license MIT */ -import { ITerminal } from '../Interfaces'; +import { ITerminal, ITheme } from '../Interfaces'; import { DomElementObjectPool } from '../utils/DomElementObjectPool'; import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; import { createBackgroundFillData } from './Canvas'; @@ -11,6 +11,8 @@ import { BackgroundRenderLayer } from './BackgroundRenderLayer'; import { ForegroundRenderLayer } from './ForegroundRenderLayer'; import { SelectionRenderLayer } from './SelectionRenderLayer'; import { CursorRenderLayer } from './CursorRenderLayer'; +import { ColorManager } from './ColorManager'; +import { BaseRenderLayer } from './BaseRenderLayer'; export class Renderer { /** A queue of the rows to be refreshed */ @@ -20,17 +22,37 @@ export class Renderer { private _dataRenderLayers: IDataRenderLayer[]; private _selectionRenderLayers: ISelectionRenderLayer[]; + private _colorManager: ColorManager; + constructor(private _terminal: ITerminal) { + this._colorManager = new ColorManager(); this._dataRenderLayers = [ - new BackgroundRenderLayer(this._terminal.element, 0), - new ForegroundRenderLayer(this._terminal.element, 2), - new CursorRenderLayer(this._terminal.element, 3) + new BackgroundRenderLayer(this._terminal.element, 0, this._colorManager.colors), + new ForegroundRenderLayer(this._terminal.element, 2, this._colorManager.colors), + new CursorRenderLayer(this._terminal.element, 3, this._colorManager.colors) ]; this._selectionRenderLayers = [ - new SelectionRenderLayer(this._terminal.element, 1) + new SelectionRenderLayer(this._terminal.element, 1, this._colorManager.colors) ]; } + public setTheme(theme: ITheme): void { + console.log('setTheme'); + this._colorManager.setTheme(theme); + // Clear layers and force a full render + for (let i = 0; i < this._dataRenderLayers.length; i++) { + this._dataRenderLayers[i].onThemeChanged(this._terminal, this._colorManager.colors); + this._dataRenderLayers[i].reset(this._terminal); + } + for (let i = 0; i < this._selectionRenderLayers.length; i++) { + this._selectionRenderLayers[i].onThemeChanged(this._terminal, this._colorManager.colors); + this._selectionRenderLayers[i].reset(this._terminal); + } + + // TODO: This is currently done for every single terminal, but it's static so it's wasting time + this._terminal.refresh(0, this._terminal.rows - 1); + } + public onResize(cols: number, rows: number): void { const width = this._terminal.charMeasure.width * this._terminal.cols; const height = this._terminal.charMeasure.height * this._terminal.rows; diff --git a/src/renderer/SelectionRenderLayer.ts b/src/renderer/SelectionRenderLayer.ts index daaf160d6c..4ec967644b 100644 --- a/src/renderer/SelectionRenderLayer.ts +++ b/src/renderer/SelectionRenderLayer.ts @@ -1,4 +1,4 @@ -import { ISelectionRenderLayer } from './Interfaces'; +import { ISelectionRenderLayer, IColorSet } from './Interfaces'; import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; import { GridCache } from './GridCache'; @@ -8,8 +8,8 @@ import { BaseRenderLayer } from './BaseRenderLayer'; export class SelectionRenderLayer extends BaseRenderLayer implements ISelectionRenderLayer { private _state: {start: [number, number], end: [number, number]}; - constructor(container: HTMLElement, zIndex: number) { - super(container, 'selection', zIndex); + constructor(container: HTMLElement, zIndex: number, colors: IColorSet) { + super(container, 'selection', zIndex, colors); this._state = { start: null, end: null diff --git a/src/renderer/Types.ts b/src/renderer/Types.ts index 621db0bd8e..09e74439f4 100644 --- a/src/renderer/Types.ts +++ b/src/renderer/Types.ts @@ -8,8 +8,3 @@ export enum FLAGS { INVERSE = 8, INVISIBLE = 16 }; - -export type Point = { - x: number, - y: number -}; diff --git a/src/utils/CharAtlas.ts b/src/utils/CharAtlas.ts new file mode 100644 index 0000000000..a375d46829 --- /dev/null +++ b/src/utils/CharAtlas.ts @@ -0,0 +1,66 @@ +import { ITerminal } from '../Interfaces'; +import { IColorSet } from '../renderer/Interfaces'; + +export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet): Promise { + const scaledCharWidth = terminal.charMeasure.width * window.devicePixelRatio; + const scaledCharHeight = terminal.charMeasure.height * window.devicePixelRatio; + + // TODO: Check to see if the atlas already exists in a cache + + return generator.generate(scaledCharWidth, scaledCharHeight, terminal.options.fontSize, terminal.options.fontFamily, colors.foreground, colors.ansi); +} + +export function releaseCharAtlas(terminal: ITerminal): void { + // TODO: Release the char atlas if it's no longer needed +} + +class CharAtlasGenerator { + private _canvas: HTMLCanvasElement; + private _ctx: CanvasRenderingContext2D; + + constructor() { + this._canvas = document.createElement('canvas'); + this._ctx = this._canvas.getContext('2d'); + this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); + } + + public generate(scaledCharWidth: number, scaledCharHeight: number, fontSize: number, fontFamily: string, foreground: string, ansiColors: string[]): Promise { + this._canvas.width = 255 * scaledCharWidth; + this._canvas.height = (/*default*/1 + /*0-15*/16) * scaledCharHeight; + + this._ctx.save(); + this._ctx.fillStyle = foreground; + this._ctx.font = `${fontSize * window.devicePixelRatio}px ${fontFamily}`; + this._ctx.textBaseline = 'top'; + + // Default color + for (let i = 0; i < 256; i++) { + this._ctx.fillText(String.fromCharCode(i), i * scaledCharWidth, 0); + } + + // Colors 0-15 + for (let colorIndex = 0; colorIndex < 16; colorIndex++) { + // colors 8-15 are bold + if (colorIndex === 8) { + this._ctx.font = `bold ${this._ctx.font}`; + } + const y = (colorIndex + 1) * scaledCharHeight; + // Clear rectangle as some fonts seem to draw over the bottom boundary + this._ctx.clearRect(0, y, this._canvas.width, scaledCharHeight); + // Draw ascii characters + for (let i = 0; i < 256; i++) { + this._ctx.fillStyle = ansiColors[colorIndex]; + this._ctx.fillText(String.fromCharCode(i), i * scaledCharWidth, y); + } + } + this._ctx.restore(); + + const charAtlasImageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); + const promise = window.createImageBitmap(charAtlasImageData); + // Clear the rect while the promise is in progress + this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); + return promise; + } +} + +const generator = new CharAtlasGenerator(); From bc3bdacdaa3b220e400c9a597e5f59303e902196 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 1 Sep 2017 16:43:45 -0700 Subject: [PATCH 037/108] Add a char atlas cache to allow different styles+reuse across terminals --- src/renderer/BaseRenderLayer.ts | 8 +--- src/utils/CharAtlas.ts | 85 +++++++++++++++++++++++++++++++-- src/utils/TestUtils.test.ts | 3 ++ 3 files changed, 85 insertions(+), 11 deletions(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index d131bee0fb..e996a2a894 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -32,9 +32,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { public onThemeChanged(terminal: ITerminal, colorSet: IColorSet): void { this._charAtlas = null; - acquireCharAtlas(terminal, this.colors).then(bitmap => { - this._charAtlas = bitmap; - }); + acquireCharAtlas(terminal, this.colors).then(bitmap => this._charAtlas = bitmap); } public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void { @@ -46,9 +44,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._canvas.style.height = `${canvasHeight}px`; if (charSizeChanged) { - acquireCharAtlas(terminal, this.colors).then(bitmap => { - this._charAtlas = bitmap; - }); + acquireCharAtlas(terminal, this.colors).then(bitmap => this._charAtlas = bitmap); } } diff --git a/src/utils/CharAtlas.ts b/src/utils/CharAtlas.ts index a375d46829..18398efc74 100644 --- a/src/utils/CharAtlas.ts +++ b/src/utils/CharAtlas.ts @@ -1,17 +1,92 @@ -import { ITerminal } from '../Interfaces'; +import { ITerminal, ITheme } from '../Interfaces'; import { IColorSet } from '../renderer/Interfaces'; +interface ICharAtlasConfig { + fontSize: number; + fontFamily: string; + scaledCharWidth: number; + scaledCharHeight: number; + colors: IColorSet; +} + +interface ICharAtlasCacheEntry { + bitmap: Promise; + config: ICharAtlasConfig; + ownedBy: ITerminal[]; +} + +let charAtlasCache: ICharAtlasCacheEntry[] = []; + export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet): Promise { const scaledCharWidth = terminal.charMeasure.width * window.devicePixelRatio; const scaledCharHeight = terminal.charMeasure.height * window.devicePixelRatio; + const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, terminal, colors); - // TODO: Check to see if the atlas already exists in a cache + // Check to see if the terminal already owns this config + for (let i = 0; i < charAtlasCache.length; i++) { + const entry = charAtlasCache[i]; + const ownedByIndex = entry.ownedBy.indexOf(terminal); + if (ownedByIndex >= 0) { + if (configEquals(entry.config, newConfig)) { + return entry.bitmap; + } else { + // The configs differ, release the terminal from the entry + if (entry.ownedBy.length === 1) { + charAtlasCache.splice(i, 1); + } else { + entry.ownedBy.splice(ownedByIndex, 1); + } + break; + } + } + } - return generator.generate(scaledCharWidth, scaledCharHeight, terminal.options.fontSize, terminal.options.fontFamily, colors.foreground, colors.ansi); + // Try match a char atlas from the cache + for (let i = 0; i < charAtlasCache.length; i++) { + const entry = charAtlasCache[i]; + if (configEquals(entry.config, newConfig)) { + // Add the terminal to the cache entry and return + entry.ownedBy.push(terminal); + return entry.bitmap; + } + } + + const newEntry: ICharAtlasCacheEntry = { + bitmap: generator.generate(scaledCharWidth, scaledCharHeight, terminal.options.fontSize, terminal.options.fontFamily, colors.foreground, colors.ansi), + config: newConfig, + ownedBy: [terminal] + }; + charAtlasCache.push(newEntry); + return newEntry.bitmap; } -export function releaseCharAtlas(terminal: ITerminal): void { - // TODO: Release the char atlas if it's no longer needed +function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: ITerminal, colors: IColorSet): ICharAtlasConfig { + const clonedColors = { + foreground: colors.foreground, + background: colors.background, + ansi: colors.ansi.slice(0, 16) + }; + return { + scaledCharWidth, + scaledCharHeight, + fontFamily: terminal.options.fontFamily, + fontSize: terminal.options.fontSize, + colors: clonedColors + }; +} + +function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean { + for (let i = 0; i < a.colors.ansi.length; i++) { + if (a.colors.ansi[i] !== b.colors.ansi[i]) { + return false; + } + } + return a.fontFamily === b.fontFamily && + a.fontSize === b.fontSize && + a.scaledCharWidth === b.scaledCharWidth && + a.scaledCharHeight === b.scaledCharHeight && + a.colors.foreground === b.colors.foreground && + a.colors.background === b.colors.background; } class CharAtlasGenerator { diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index ae83aed4fb..3a66d49d05 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -52,6 +52,9 @@ export class MockTerminal implements ITerminal { showCursor(): void { throw new Error('Method not implemented.'); } + refresh(start: number, end: number): void { + throw new Error('Method not implemented.'); + } blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData { const line: LineData = []; cols = cols || this.cols; From de4fcb9241e23b31d2a7bcc2f0cd6d8277d509c5 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 1 Sep 2017 16:54:26 -0700 Subject: [PATCH 038/108] Merge render layer interface types --- src/Terminal.ts | 5 +-- src/renderer/BackgroundRenderLayer.ts | 6 +-- src/renderer/BaseRenderLayer.ts | 2 + src/renderer/CursorRenderLayer.ts | 6 +-- src/renderer/ForegroundRenderLayer.ts | 6 +-- src/renderer/Interfaces.ts | 16 +------ src/renderer/Renderer.ts | 63 +++++++-------------------- src/renderer/SelectionRenderLayer.ts | 6 +-- 8 files changed, 32 insertions(+), 78 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index a663323521..13f3886037 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -761,10 +761,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure); this.renderer = new Renderer(this); - this.on('cursormove', () => { - console.log('cursormove fired'); - this.renderer.onCursorMove(); - }); + this.on('cursormove', () => this.renderer.onCursorMove()); this.on('resize', () => this.renderer.onResize(this.cols, this.rows)); this.charMeasure.on('charsizechanged', () => { this.renderer.onCharSizeChanged(this.charMeasure.width, this.charMeasure.height); diff --git a/src/renderer/BackgroundRenderLayer.ts b/src/renderer/BackgroundRenderLayer.ts index cf122cc23d..271dc2ac8b 100644 --- a/src/renderer/BackgroundRenderLayer.ts +++ b/src/renderer/BackgroundRenderLayer.ts @@ -1,11 +1,11 @@ -import { IDataRenderLayer, IColorSet } from './Interfaces'; +import { IColorSet } from './Interfaces'; import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; import { GridCache } from './GridCache'; import { FLAGS } from './Types'; import { BaseRenderLayer } from './BaseRenderLayer'; -export class BackgroundRenderLayer extends BaseRenderLayer implements IDataRenderLayer { +export class BackgroundRenderLayer extends BaseRenderLayer { private _state: GridCache; constructor(container: HTMLElement, zIndex: number, colors: IColorSet) { @@ -23,7 +23,7 @@ export class BackgroundRenderLayer extends BaseRenderLayer implements IDataRende this.clearAll(); } - public render(terminal: ITerminal, startRow: number, endRow: number): void { + public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void { for (let y = startRow; y <= endRow; y++) { let row = y + terminal.buffer.ydisp; let line = terminal.buffer.lines.get(row); diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index e996a2a894..20425e413a 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -29,6 +29,8 @@ export abstract class BaseRenderLayer implements IRenderLayer { // TODO: Should this do anything? public onOptionsChanged(terminal: ITerminal): void {} public onCursorMove(terminal: ITerminal): void {} + public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void {} + public onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number]): void {} public onThemeChanged(terminal: ITerminal, colorSet: IColorSet): void { this._charAtlas = null; diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index bfec1d8e94..1e5cf64785 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -1,4 +1,4 @@ -import { IDataRenderLayer, IColorSet } from './Interfaces'; +import { IColorSet } from './Interfaces'; import { IBuffer, ICharMeasure, ITerminal, ITerminalOptions } from '../Interfaces'; import { CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; import { GridCache } from './GridCache'; @@ -12,7 +12,7 @@ import { COLOR_CODES } from './ColorManager'; */ const BLINK_INTERVAL = 600; -export class CursorRenderLayer extends BaseRenderLayer implements IDataRenderLayer { +export class CursorRenderLayer extends BaseRenderLayer { private _state: [number, number]; private _cursorRenderers: {[key: string]: (terminal: ITerminal, x: number, y: number, charData: CharData) => void}; private _cursorBlinkStateManager: CursorBlinkStateManager; @@ -62,7 +62,7 @@ export class CursorRenderLayer extends BaseRenderLayer implements IDataRenderLay } } - public render(terminal: ITerminal, startRow: number, endRow: number): void { + public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void { // Only render if the animation frame is not active if (!this._cursorBlinkStateManager) { this._render(terminal, false); diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index fa4ef80074..5a1d41f6c6 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -1,4 +1,4 @@ -import { IDataRenderLayer, IColorSet } from './Interfaces'; +import { IColorSet } from './Interfaces'; import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../Buffer'; import { FLAGS } from './Types'; @@ -6,7 +6,7 @@ import { GridCache } from './GridCache'; import { CharData } from '../Types'; import { BaseRenderLayer } from './BaseRenderLayer'; -export class ForegroundRenderLayer extends BaseRenderLayer implements IDataRenderLayer { +export class ForegroundRenderLayer extends BaseRenderLayer { private _state: GridCache; constructor(container: HTMLElement, zIndex: number, colors: IColorSet) { @@ -24,7 +24,7 @@ export class ForegroundRenderLayer extends BaseRenderLayer implements IDataRende this.clearAll(); } - public render(terminal: ITerminal, startRow: number, endRow: number): void { + public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void { // TODO: Ensure that the render is eventually performed // Don't bother render until the atlas bitmap is ready // TODO: Move this to BaseRenderLayer? diff --git a/src/renderer/Interfaces.ts b/src/renderer/Interfaces.ts index 4b8ce816f2..5626f13db1 100644 --- a/src/renderer/Interfaces.ts +++ b/src/renderer/Interfaces.ts @@ -4,6 +4,8 @@ export interface IRenderLayer { onCursorMove(terminal: ITerminal): void; onOptionsChanged(terminal: ITerminal): void; onThemeChanged(terminal: ITerminal, colorSet: IColorSet): void; + onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void; + onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number]): void; /** * Resize the render layer. @@ -16,20 +18,6 @@ export interface IRenderLayer { reset(terminal: ITerminal): void; } -/** - * A render layer that renders when there is a data change. - */ -export interface IDataRenderLayer extends IRenderLayer { - render(terminal: ITerminal, startRow: number, endRow: number): void; -} - -/** - * A render layer that renders when there is a selection change. - */ -export interface ISelectionRenderLayer extends IRenderLayer { - render(terminal: ITerminal, start: [number, number], end: [number, number]): void; -} - export interface IColorSet { foreground: string; diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 653e2cbafe..96e899f660 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -6,48 +6,41 @@ import { ITerminal, ITheme } from '../Interfaces'; import { DomElementObjectPool } from '../utils/DomElementObjectPool'; import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; import { createBackgroundFillData } from './Canvas'; -import { IDataRenderLayer, ISelectionRenderLayer } from './Interfaces'; import { BackgroundRenderLayer } from './BackgroundRenderLayer'; import { ForegroundRenderLayer } from './ForegroundRenderLayer'; import { SelectionRenderLayer } from './SelectionRenderLayer'; import { CursorRenderLayer } from './CursorRenderLayer'; import { ColorManager } from './ColorManager'; import { BaseRenderLayer } from './BaseRenderLayer'; +import { IRenderLayer } from './Interfaces'; export class Renderer { /** A queue of the rows to be refreshed */ private _refreshRowsQueue: {start: number, end: number}[] = []; private _refreshAnimationFrame = null; - private _dataRenderLayers: IDataRenderLayer[]; - private _selectionRenderLayers: ISelectionRenderLayer[]; + private _renderLayers: IRenderLayer[]; private _colorManager: ColorManager; constructor(private _terminal: ITerminal) { this._colorManager = new ColorManager(); - this._dataRenderLayers = [ + this._renderLayers = [ new BackgroundRenderLayer(this._terminal.element, 0, this._colorManager.colors), + new SelectionRenderLayer(this._terminal.element, 1, this._colorManager.colors), new ForegroundRenderLayer(this._terminal.element, 2, this._colorManager.colors), new CursorRenderLayer(this._terminal.element, 3, this._colorManager.colors) ]; - this._selectionRenderLayers = [ - new SelectionRenderLayer(this._terminal.element, 1, this._colorManager.colors) - ]; } public setTheme(theme: ITheme): void { console.log('setTheme'); this._colorManager.setTheme(theme); // Clear layers and force a full render - for (let i = 0; i < this._dataRenderLayers.length; i++) { - this._dataRenderLayers[i].onThemeChanged(this._terminal, this._colorManager.colors); - this._dataRenderLayers[i].reset(this._terminal); - } - for (let i = 0; i < this._selectionRenderLayers.length; i++) { - this._selectionRenderLayers[i].onThemeChanged(this._terminal, this._colorManager.colors); - this._selectionRenderLayers[i].reset(this._terminal); - } + this._renderLayers.forEach(l => { + l.onThemeChanged(this._terminal, this._colorManager.colors); + l.reset(this._terminal); + }); // TODO: This is currently done for every single terminal, but it's static so it's wasting time this._terminal.refresh(0, this._terminal.rows - 1); @@ -56,53 +49,29 @@ export class Renderer { public onResize(cols: number, rows: number): void { const width = this._terminal.charMeasure.width * this._terminal.cols; const height = this._terminal.charMeasure.height * this._terminal.rows; - for (let i = 0; i < this._dataRenderLayers.length; i++) { - this._dataRenderLayers[i].resize(this._terminal, width, height, false); - } + this._renderLayers.forEach(l => l.resize(this._terminal, width, height, false)); } public onCharSizeChanged(charWidth: number, charHeight: number): void { const width = charWidth * this._terminal.cols; const height = charHeight * this._terminal.rows; - for (let i = 0; i < this._dataRenderLayers.length; i++) { - this._dataRenderLayers[i].resize(this._terminal, width, height, true); - } - for (let i = 0; i < this._selectionRenderLayers.length; i++) { - this._selectionRenderLayers[i].resize(this._terminal, width, height, true); - } + this._renderLayers.forEach(l => l.resize(this._terminal, width, height, true)); } public onSelectionChanged(start: [number, number], end: [number, number]): void { - for (let i = 0; i < this._selectionRenderLayers.length; i++) { - this._selectionRenderLayers[i].render(this._terminal, start, end); - } + this._renderLayers.forEach(l => l.onSelectionChanged(this._terminal, start, end)); } public onCursorMove(): void { - for (let i = 0; i < this._dataRenderLayers.length; i++) { - this._dataRenderLayers[i].onCursorMove(this._terminal); - } - for (let i = 0; i < this._selectionRenderLayers.length; i++) { - this._selectionRenderLayers[i].onCursorMove(this._terminal); - } + this._renderLayers.forEach(l => l.onCursorMove(this._terminal)); } public onOptionsChanged(): void { - for (let i = 0; i < this._dataRenderLayers.length; i++) { - this._dataRenderLayers[i].onOptionsChanged(this._terminal); - } - for (let i = 0; i < this._selectionRenderLayers.length; i++) { - this._selectionRenderLayers[i].onOptionsChanged(this._terminal); - } + this._renderLayers.forEach(l => l.onOptionsChanged(this._terminal)); } public clear(): void { - for (let i = 0; i < this._dataRenderLayers.length; i++) { - this._dataRenderLayers[i].reset(this._terminal); - } - for (let i = 0; i < this._selectionRenderLayers.length; i++) { - this._selectionRenderLayers[i].reset(this._terminal); - } + this._renderLayers.forEach(l => l.reset(this._terminal)); } /** @@ -146,9 +115,7 @@ export class Renderer { this._refreshAnimationFrame = null; // Render - for (let i = 0; i < this._dataRenderLayers.length; i++) { - this._dataRenderLayers[i].render(this._terminal, start, end); - } + this._renderLayers.forEach(l => l.onGridChanged(this._terminal, start, end)); this._terminal.emit('refresh', {start, end}); } } diff --git a/src/renderer/SelectionRenderLayer.ts b/src/renderer/SelectionRenderLayer.ts index 4ec967644b..1dcb8ab6a6 100644 --- a/src/renderer/SelectionRenderLayer.ts +++ b/src/renderer/SelectionRenderLayer.ts @@ -1,11 +1,11 @@ -import { ISelectionRenderLayer, IColorSet } from './Interfaces'; +import { IColorSet } from './Interfaces'; import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; import { GridCache } from './GridCache'; import { FLAGS } from './Types'; import { BaseRenderLayer } from './BaseRenderLayer'; -export class SelectionRenderLayer extends BaseRenderLayer implements ISelectionRenderLayer { +export class SelectionRenderLayer extends BaseRenderLayer { private _state: {start: [number, number], end: [number, number]}; constructor(container: HTMLElement, zIndex: number, colors: IColorSet) { @@ -26,7 +26,7 @@ export class SelectionRenderLayer extends BaseRenderLayer implements ISelectionR } } - public render(terminal: ITerminal, start: [number, number], end: [number, number]): void { + public onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number]): void { // Selection has not changed if (this._state.start === start || this._state.end === end) { return; From 2c9dbc6191158e09e46f0633acf1683a7b501433 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 1 Sep 2017 16:58:02 -0700 Subject: [PATCH 039/108] Add some jsdoc --- src/renderer/CursorRenderLayer.ts | 1 - src/renderer/Interfaces.ts | 20 ++++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index 1e5cf64785..1533c4f229 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -38,7 +38,6 @@ export class CursorRenderLayer extends BaseRenderLayer { } public onOptionsChanged(terminal: ITerminal): void { - super.onOptionsChanged(terminal); if (terminal.options.cursorBlink) { if (!this._cursorBlinkStateManager) { this._cursorBlinkStateManager = new CursorBlinkStateManager(terminal, () => { diff --git a/src/renderer/Interfaces.ts b/src/renderer/Interfaces.ts index 5626f13db1..45822e6530 100644 --- a/src/renderer/Interfaces.ts +++ b/src/renderer/Interfaces.ts @@ -1,10 +1,30 @@ import { ITerminal, ITerminalOptions } from '../Interfaces'; export interface IRenderLayer { + /** + * Called when the cursor is moved. + */ onCursorMove(terminal: ITerminal): void; + + /** + * Called when options change. + */ onOptionsChanged(terminal: ITerminal): void; + + /** + * Called when the theme changes. + */ onThemeChanged(terminal: ITerminal, colorSet: IColorSet): void; + + /** + * Called when the data in the grid has changed (or needs to be rendered + * again). + */ onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void; + + /** + * Calls when the selection changes. + */ onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number]): void; /** From bf9533327518bf162da192c82ef8963db247bdac Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 1 Sep 2017 20:44:41 -0700 Subject: [PATCH 040/108] Fix the fit addon --- demo/main.js | 39 +++++++++++++-------------- src/addons/fit/fit.js | 33 +++++++++-------------- src/renderer/BackgroundRenderLayer.ts | 4 +++ src/renderer/ForegroundRenderLayer.ts | 5 ++++ src/renderer/GridCache.ts | 2 +- 5 files changed, 42 insertions(+), 41 deletions(-) diff --git a/demo/main.js b/demo/main.js index cb346de2af..2884070985 100644 --- a/demo/main.js +++ b/demo/main.js @@ -92,27 +92,26 @@ function createTerminal() { term.open(terminalContainer); term.fit(); - var initialGeometry = term.proposeGeometry(), - cols = initialGeometry.cols, - rows = initialGeometry.rows; - - colsElement.value = cols; - rowsElement.value = rows; - - fetch('/terminals?cols=' + cols + '&rows=' + rows, {method: 'POST'}).then(function (res) { - - charWidth = Math.ceil(term.element.offsetWidth / cols); - charHeight = Math.ceil(term.element.offsetHeight / rows); - - res.text().then(function (pid) { - window.pid = pid; - socketURL += pid; - socket = new WebSocket(socketURL); - socket.onopen = runRealTerminal; - socket.onclose = runFakeTerminal; - socket.onerror = runFakeTerminal; + // fit is called within a setTimeout, cols and rows need this. + setTimeout(() => { + colsElement.value = term.cols; + rowsElement.value = term.rows; + + fetch('/terminals?cols=' + cols + '&rows=' + rows, {method: 'POST'}).then(function (res) { + + charWidth = Math.ceil(term.element.offsetWidth / cols); + charHeight = Math.ceil(term.element.offsetHeight / rows); + + res.text().then(function (pid) { + window.pid = pid; + socketURL += pid; + socket = new WebSocket(socketURL); + socket.onopen = runRealTerminal; + socket.onclose = runFakeTerminal; + socket.onerror = runFakeTerminal; + }); }); - }); + }, 0); } function runRealTerminal() { diff --git a/src/addons/fit/fit.js b/src/addons/fit/fit.js index da66802d12..1e46932cdd 100644 --- a/src/addons/fit/fit.js +++ b/src/addons/fit/fit.js @@ -45,33 +45,26 @@ availableWidth = parentElementWidth - elementPaddingHor, container = term.rowContainer, subjectRow = term.rowContainer.firstElementChild, - contentBuffer = subjectRow.innerHTML, - characterHeight, - rows, - characterWidth, - cols, - geometry; + contentBuffer = subjectRow.innerHTML; - subjectRow.style.display = 'inline'; - subjectRow.innerHTML = 'W'; // Common character for measuring width, although on monospace - characterWidth = subjectRow.getBoundingClientRect().width; - subjectRow.style.display = ''; // Revert style before calculating height, since they differ. - characterHeight = subjectRow.getBoundingClientRect().height; - subjectRow.innerHTML = contentBuffer; + var geometry = { + cols: parseInt(availableWidth / term.charMeasure.width, 10), + rows: parseInt(availableHeight / term.charMeasure.height, 10) + }; - rows = parseInt(availableHeight / characterHeight); - cols = parseInt(availableWidth / characterWidth); - - geometry = {cols: cols, rows: rows}; return geometry; }; exports.fit = function (term) { - var geometry = exports.proposeGeometry(term); + // Wrap fit in a setTimeout as charMeasure needs time to get initialized + // after calling Terminal.open + setTimeout(() => { + var geometry = exports.proposeGeometry(term); - if (geometry) { - term.resize(geometry.cols, geometry.rows); - } + if (geometry) { + term.resize(geometry.cols, geometry.rows); + } + }, 0); }; Terminal.prototype.proposeGeometry = function () { diff --git a/src/renderer/BackgroundRenderLayer.ts b/src/renderer/BackgroundRenderLayer.ts index 271dc2ac8b..33550af05d 100644 --- a/src/renderer/BackgroundRenderLayer.ts +++ b/src/renderer/BackgroundRenderLayer.ts @@ -24,6 +24,10 @@ export class BackgroundRenderLayer extends BaseRenderLayer { } public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void { + // Resize has not been called yet + if (this._state.cache.length === 0) { + return; + } for (let y = startRow; y <= endRow; y++) { let row = y + terminal.buffer.ydisp; let line = terminal.buffer.lines.get(row); diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index 5a1d41f6c6..9df1f49b1e 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -32,6 +32,11 @@ export class ForegroundRenderLayer extends BaseRenderLayer { // return; // } + // Resize has not been called yet + if (this._state.cache.length === 0) { + return; + } + for (let y = startRow; y <= endRow; y++) { const row = y + terminal.buffer.ydisp; const line = terminal.buffer.lines.get(row); diff --git a/src/renderer/GridCache.ts b/src/renderer/GridCache.ts index a4657fcba7..56294ddcbe 100644 --- a/src/renderer/GridCache.ts +++ b/src/renderer/GridCache.ts @@ -18,7 +18,7 @@ export class GridCache { this.cache.length = width; } - public clear() { + public clear(): void { for (let x = 0; x < this.cache.length; x++) { for (let y = 0; y < this.cache[x].length; y++) { this.cache[x][y] = null; From 1f444b19130410677d1a92f7fa19ea4efac5ae5f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 1 Sep 2017 20:51:18 -0700 Subject: [PATCH 041/108] Support invisible attr --- src/renderer/BaseRenderLayer.ts | 2 +- src/renderer/CursorRenderLayer.ts | 2 +- src/renderer/ForegroundRenderLayer.ts | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 20425e413a..207f87ee94 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -80,7 +80,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.clearRect(startCol * this.scaledCharWidth, startRow * this.scaledCharHeight, colWidth * this.scaledCharWidth, colHeight * this.scaledCharHeight); } - protected drawChar(terminal: ITerminal, char: string, code: number, fg: number, x: number, y: number): void { + protected drawChar(terminal: ITerminal, char: string, code: number, x: number, y: number, fg: number): void { let colorIndex = 0; if (fg < 256) { colorIndex = fg + 1; diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index 1533c4f229..cd11f1dc8d 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -116,7 +116,7 @@ export class CursorRenderLayer extends BaseRenderLayer { private _renderBlockCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void { this.fillCells(x, y, 1, 1); - this.drawChar(terminal, charData[CHAR_DATA_CHAR_INDEX], charData[CHAR_DATA_CODE_INDEX], COLOR_CODES.BLACK, x, y); + this.drawChar(terminal, charData[CHAR_DATA_CHAR_INDEX], charData[CHAR_DATA_CODE_INDEX], x, y, COLOR_CODES.BLACK); } private _renderUnderlineCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void { diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index 9df1f49b1e..53660fa0e3 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -61,13 +61,14 @@ export class ForegroundRenderLayer extends BaseRenderLayer { } this._state.cache[x][y] = charData; + const flags = attr >> 18; + // Skip rendering if the character is invisible - if (!code || code === 32 /*' '*/) { + if (!code || code === 32 /*' '*/ || (flags & FLAGS.INVISIBLE)) { continue; } let fg = (attr >> 9) & 0x1ff; - const flags = attr >> 18; // If inverse flag is on, the foreground should become the background. if (flags & FLAGS.INVERSE) { @@ -87,7 +88,7 @@ export class ForegroundRenderLayer extends BaseRenderLayer { } } - this.drawChar(terminal, char, code, fg, x, y); + this.drawChar(terminal, char, code, x, y, fg); this._ctx.restore(); } } From 70764a74afa893e6cc91bb5cd813c38e6a79437f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 1 Sep 2017 21:09:07 -0700 Subject: [PATCH 042/108] Use the proper bg/fg colors for inverse attr --- src/renderer/BackgroundRenderLayer.ts | 7 +++---- src/renderer/BaseRenderLayer.ts | 10 +++++++--- src/renderer/ForegroundRenderLayer.ts | 6 +++--- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/renderer/BackgroundRenderLayer.ts b/src/renderer/BackgroundRenderLayer.ts index 33550af05d..729a8ec5fc 100644 --- a/src/renderer/BackgroundRenderLayer.ts +++ b/src/renderer/BackgroundRenderLayer.ts @@ -3,7 +3,7 @@ import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; import { GridCache } from './GridCache'; import { FLAGS } from './Types'; -import { BaseRenderLayer } from './BaseRenderLayer'; +import { BaseRenderLayer, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer'; export class BackgroundRenderLayer extends BaseRenderLayer { private _state: GridCache; @@ -39,9 +39,8 @@ export class BackgroundRenderLayer extends BaseRenderLayer { // If inverse flag is on, the background should become the foreground. if (flags & FLAGS.INVERSE) { bg = (attr >> 9) & 0x1ff; - // TODO: Is this case still needed if (bg === 257) { - bg = 15; + bg = INVERTED_DEFAULT_COLOR; } } @@ -50,7 +49,7 @@ export class BackgroundRenderLayer extends BaseRenderLayer { if (needsRefresh) { if (bg < 256) { this._ctx.save(); - this._ctx.fillStyle = this.colors.ansi[bg]; + this._ctx.fillStyle = (bg === INVERTED_DEFAULT_COLOR ? this.colors.foreground : this.colors.ansi[bg]); this.fillCells(x, y, 1, 1); this._ctx.restore(); this._state.cache[x][y] = bg; diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 207f87ee94..858f15b863 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -2,6 +2,8 @@ import { IRenderLayer, IColorSet } from './Interfaces'; import { ITerminal, ITerminalOptions } from '../Interfaces'; import { acquireCharAtlas } from '../utils/CharAtlas'; +export const INVERTED_DEFAULT_COLOR = -1; + export abstract class BaseRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; protected _ctx: CanvasRenderingContext2D; @@ -102,11 +104,13 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`; this._ctx.textBaseline = 'top'; - // 256 color support - if (fg < 256) { + if (fg === INVERTED_DEFAULT_COLOR) { + this._ctx.fillStyle = this.colors.background; + } else if (fg < 256) { + // 256 color support this._ctx.fillStyle = this.colors.ansi[fg]; } else { - this._ctx.fillStyle = '#ffffff'; + this._ctx.fillStyle = this.colors.foreground; } // TODO: Do we care about width for rendering wide chars? diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index 53660fa0e3..7d1cec1fd5 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -4,7 +4,7 @@ import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_ import { FLAGS } from './Types'; import { GridCache } from './GridCache'; import { CharData } from '../Types'; -import { BaseRenderLayer } from './BaseRenderLayer'; +import { BaseRenderLayer, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer'; export class ForegroundRenderLayer extends BaseRenderLayer { private _state: GridCache; @@ -74,8 +74,8 @@ export class ForegroundRenderLayer extends BaseRenderLayer { if (flags & FLAGS.INVERSE) { fg = attr & 0x1ff; // TODO: Is this case still needed - if (fg === 257) { - fg = 0; + if (fg === 256) { + fg = INVERTED_DEFAULT_COLOR; } } From b0653fec6823d7a425c40a4c3a86b6f5a0aa7061 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 1 Sep 2017 21:24:17 -0700 Subject: [PATCH 043/108] Support underline --- src/renderer/BaseRenderLayer.ts | 3 +-- src/renderer/ForegroundRenderLayer.ts | 13 +++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 858f15b863..523edbda09 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -82,7 +82,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.clearRect(startCol * this.scaledCharWidth, startRow * this.scaledCharHeight, colWidth * this.scaledCharWidth, colHeight * this.scaledCharHeight); } - protected drawChar(terminal: ITerminal, char: string, code: number, x: number, y: number, fg: number): void { + protected drawChar(terminal: ITerminal, char: string, code: number, x: number, y: number, fg: number, underline: boolean = false): void { let colorIndex = 0; if (fg < 256) { colorIndex = fg + 1; @@ -113,7 +113,6 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.fillStyle = this.colors.foreground; } - // TODO: Do we care about width for rendering wide chars? this._ctx.fillText(char, x * scaledCharWidth, y * scaledCharHeight); this._ctx.restore(); } diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index 7d1cec1fd5..30f68f4c5a 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -88,7 +88,20 @@ export class ForegroundRenderLayer extends BaseRenderLayer { } } + if (flags & FLAGS.UNDERLINE) { + if (fg === INVERTED_DEFAULT_COLOR) { + this._ctx.fillStyle = this.colors.background; + } else if (fg < 256) { + // 256 color support + this._ctx.fillStyle = this.colors.ansi[fg]; + } else { + this._ctx.fillStyle = this.colors.foreground; + } + this.fillBottomLineAtCell(x, y); + } + this.drawChar(terminal, char, code, x, y, fg); + this._ctx.restore(); } } From 5cd189e2e072ea45f41058dfc8e7235d45645d93 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 1 Sep 2017 21:36:20 -0700 Subject: [PATCH 044/108] Fix demo pty size --- demo/main.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/demo/main.js b/demo/main.js index 2884070985..f05a8cef16 100644 --- a/demo/main.js +++ b/demo/main.js @@ -97,10 +97,10 @@ function createTerminal() { colsElement.value = term.cols; rowsElement.value = term.rows; - fetch('/terminals?cols=' + cols + '&rows=' + rows, {method: 'POST'}).then(function (res) { + fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, {method: 'POST'}).then(function (res) { - charWidth = Math.ceil(term.element.offsetWidth / cols); - charHeight = Math.ceil(term.element.offsetHeight / rows); + charWidth = Math.ceil(term.element.offsetWidth / term.cols); + charHeight = Math.ceil(term.element.offsetHeight / term.rows); res.text().then(function (pid) { window.pid = pid; From 6ee8e9126a5c1f7fe13de8de90c6d37411ae15a4 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 1 Sep 2017 21:38:35 -0700 Subject: [PATCH 045/108] Remove some old dead code --- src/Renderer.ts | 423 ------------------------- src/renderer/Renderer.ts | 1 - src/utils/DomElementObjectPool.test.ts | 51 --- src/utils/DomElementObjectPool.ts | 75 ----- 4 files changed, 550 deletions(-) delete mode 100644 src/Renderer.ts delete mode 100644 src/utils/DomElementObjectPool.test.ts delete mode 100644 src/utils/DomElementObjectPool.ts diff --git a/src/Renderer.ts b/src/Renderer.ts deleted file mode 100644 index f4a275bbe2..0000000000 --- a/src/Renderer.ts +++ /dev/null @@ -1,423 +0,0 @@ -/** - * @license MIT - */ - -import { ITerminal } from './Interfaces'; -import { DomElementObjectPool } from './utils/DomElementObjectPool'; -import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer'; - -/** - * The maximum number of refresh frames to skip when the write buffer is non- - * empty. Note that these frames may be intermingled with frames that are - * skipped via requestAnimationFrame's mechanism. - */ -const MAX_REFRESH_FRAME_SKIP = 5; - -/** - * Flags used to render terminal text properly. - */ -enum FLAGS { - BOLD = 1, - UNDERLINE = 2, - BLINK = 4, - INVERSE = 8, - INVISIBLE = 16 -}; - -let brokenBold: boolean = null; - -export class Renderer { - /** A queue of the rows to be refreshed */ - private _refreshRowsQueue: {start: number, end: number}[] = []; - 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 - // to established public API). - } - - /** - * Queues a refresh between two rows (inclusive), to be done on next animation - * frame. - * @param {number} start The start row. - * @param {number} end The end row. - */ - public queueRefresh(start: number, end: number): void { - this._refreshRowsQueue.push({ start: start, end: end }); - if (!this._refreshAnimationFrame) { - this._refreshAnimationFrame = window.requestAnimationFrame(this._refreshLoop.bind(this)); - } - } - - /** - * Performs the refresh loop callback, calling refresh only if a refresh is - * necessary before queueing up the next one. - */ - private _refreshLoop(): void { - // Skip MAX_REFRESH_FRAME_SKIP frames if the writeBuffer is non-empty as it - // will need to be immediately refreshed anyway. This saves a lot of - // rendering time as the viewport DOM does not need to be refreshed, no - // scroll events, no layouts, etc. - const skipFrame = this._terminal.writeBuffer.length > 0 && this._refreshFramesSkipped++ <= MAX_REFRESH_FRAME_SKIP; - if (skipFrame) { - this._refreshAnimationFrame = window.requestAnimationFrame(this._refreshLoop.bind(this)); - return; - } - - this._refreshFramesSkipped = 0; - let start; - let end; - if (this._refreshRowsQueue.length > 4) { - // Just do a full refresh when 5+ refreshes are queued - start = 0; - end = this._terminal.rows - 1; - } else { - // Get start and end rows that need refreshing - start = this._refreshRowsQueue[0].start; - end = this._refreshRowsQueue[0].end; - for (let i = 1; i < this._refreshRowsQueue.length; i++) { - if (this._refreshRowsQueue[i].start < start) { - start = this._refreshRowsQueue[i].start; - } - if (this._refreshRowsQueue[i].end > end) { - end = this._refreshRowsQueue[i].end; - } - } - } - this._refreshRowsQueue = []; - this._refreshAnimationFrame = null; - this._refresh(start, end); - } - - /** - * Refreshes (re-renders) terminal content within two rows (inclusive) - * - * Rendering Engine: - * - * In the screen buffer, each character is stored as a an array with a character - * and a 32-bit integer: - * - First value: a utf-16 character. - * - Second value: - * - Next 9 bits: background color (0-511). - * - Next 9 bits: foreground color (0-511). - * - Next 14 bits: a mask for misc. flags: - * - 1=bold - * - 2=underline - * - 4=blink - * - 8=inverse - * - 16=invisible - * - * @param {number} start The row to start from (between 0 and terminal's height terminal - 1) - * @param {number} end The row to end at (between fromRow and terminal's height terminal - 1) - */ - private _refresh(start: number, end: number): void { - // 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) { - this._terminal.element.removeChild(this._terminal.rowContainer); - } - } - - 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.'); - end = this._terminal.rows - 1; - } - - for (; y <= end; y++) { - let row = y + this._terminal.buffer.ydisp; - - let line = this._terminal.buffer.lines.get(row); - - 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; - } - - let attr = this._terminal.defAttr; - - const documentFragment = document.createDocumentFragment(); - let innerHTML = ''; - let currentElement; - - // 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][CHAR_DATA_CHAR_INDEX]; - const ch_width: any = line[i][CHAR_DATA_WIDTH_INDEX]; - const isCursor: boolean = i === x; - if (!ch_width) { - continue; - } - - 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 || isCursor) { - if (innerHTML && !currentElement) { - currentElement = this._spanElementObjectPool.acquire(); - } - if (currentElement) { - if (innerHTML) { - currentElement.innerHTML = innerHTML; - innerHTML = ''; - } - documentFragment.appendChild(currentElement); - } - currentElement = this._spanElementObjectPool.acquire(); - - let bg = data & 0x1ff; - let fg = (data >> 9) & 0x1ff; - let flags = data >> 18; - - if (isCursor) { - currentElement.classList.add('reverse-video'); - currentElement.classList.add('terminal-cursor'); - } - - if (flags & FLAGS.BOLD) { - if (!brokenBold) { - currentElement.classList.add('xterm-bold'); - } - // See: XTerm*boldColors - if (fg < 8) { - fg += 8; - } - } - - if (flags & FLAGS.UNDERLINE) { - currentElement.classList.add('xterm-underline'); - } - - if (flags & FLAGS.BLINK) { - currentElement.classList.add('xterm-blink'); - } - - // 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; - } - } - - if (flags & FLAGS.INVISIBLE && !isCursor) { - currentElement.classList.add('xterm-hidden'); - } - - /** - * 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 (bg < 256) { - currentElement.classList.add(`xterm-bg-color-${bg}`); - } - - if (fg < 256) { - currentElement.classList.add(`xterm-color-${fg}`); - } - - } - } - - if (ch_width === 2) { - // 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; - } - } - - // 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 (innerHTML && !currentElement) { - currentElement = this._spanElementObjectPool.acquire(); - } - if (currentElement) { - if (innerHTML) { - currentElement.innerHTML = innerHTML; - innerHTML = ''; - } - documentFragment.appendChild(currentElement); - currentElement = null; - } - - this._terminal.children[y].appendChild(documentFragment); - } - - if (parent) { - this._terminal.element.appendChild(this._terminal.rowContainer); - } - - this._terminal.emit('refresh', {start, end}); - }; - - private _imageDataCache = {}; - private _colors = [ - // dark: - '#2e3436', - '#cc0000', - '#4e9a06', - '#c4a000', - '#3465a4', - '#75507b', - '#06989a', - '#d3d7cf', - // bright: - '#555753', - '#ef2929', - '#8ae234', - '#fce94f', - '#729fcf', - '#ad7fa8', - '#34e2e2', - '#eeeeec' - ]; - - /** - * Refreshes the selection in the DOM. - * @param start The selection start. - * @param end The selection end. - */ - public refreshSelection(start: [number, number], end: [number, number]): void { - // 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. -function checkBoldBroken(terminalElement: HTMLElement): boolean { - const document = terminalElement.ownerDocument; - const el = document.createElement('span'); - el.innerHTML = 'hello world'; - terminalElement.appendChild(el); - const w1 = el.offsetWidth; - const h1 = el.offsetHeight; - el.style.fontWeight = 'bold'; - const w2 = el.offsetWidth; - const h2 = el.offsetHeight; - terminalElement.removeChild(el); - return w1 !== w2 || h1 !== h2; -} diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 96e899f660..119399b06a 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -3,7 +3,6 @@ */ import { ITerminal, ITheme } from '../Interfaces'; -import { DomElementObjectPool } from '../utils/DomElementObjectPool'; import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; import { createBackgroundFillData } from './Canvas'; import { BackgroundRenderLayer } from './BackgroundRenderLayer'; diff --git a/src/utils/DomElementObjectPool.test.ts b/src/utils/DomElementObjectPool.test.ts deleted file mode 100644 index 5a5d8a7841..0000000000 --- a/src/utils/DomElementObjectPool.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * @license MIT - */ - -import { assert } from 'chai'; -import { DomElementObjectPool } from './DomElementObjectPool'; - -class MockDocument { - private _attr: {[key: string]: string} = {}; - constructor() {} - public getAttribute(key: string): string { return this._attr[key]; }; - public setAttribute(key: string, value: string): void { this._attr[key] = value; } -} - -describe('DomElementObjectPool', () => { - let pool: DomElementObjectPool; - - beforeEach(() => { - pool = new DomElementObjectPool('span'); - (global).document = { - createElement: () => new MockDocument() - }; - }); - - it('should acquire distinct elements', () => { - const element1 = pool.acquire(); - const element2 = pool.acquire(); - assert.notEqual(element1, element2); - }); - - it('should acquire released elements', () => { - const element = pool.acquire(); - pool.release(element); - assert.equal(pool.acquire(), element); - }); - - it('should handle a series of acquisitions and releases', () => { - const element1 = pool.acquire(); - const element2 = pool.acquire(); - pool.release(element1); - assert.equal(pool.acquire(), element1); - pool.release(element1); - pool.release(element2); - assert.equal(pool.acquire(), element2); - assert.equal(pool.acquire(), element1); - }); - - it('should throw when releasing an element that was not acquired', () => { - assert.throws(() => pool.release(document.createElement('span'))); - }); -}); diff --git a/src/utils/DomElementObjectPool.ts b/src/utils/DomElementObjectPool.ts deleted file mode 100644 index 7707ad9ab6..0000000000 --- a/src/utils/DomElementObjectPool.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * @module xterm/utils/DomElementObjectPool - * @license MIT - */ - -/** - * An object pool that manages acquisition and releasing of DOM elements for - * when reuse is desirable. - */ -export class DomElementObjectPool { - private static readonly OBJECT_ID_ATTRIBUTE = 'data-obj-id'; - - private static _objectCount = 0; - - private _type: string; - private _pool: HTMLElement[]; - private _inUse: {[key: string]: HTMLElement}; - - /** - * @param type The DOM element type (div, span, etc.). - */ - constructor(private type: string) { - this._type = type; - this._pool = []; - this._inUse = {}; - } - - /** - * Acquire an element from the pool, creating it if the pool is empty. - */ - public acquire(): HTMLElement { - let element: HTMLElement; - if (this._pool.length === 0) { - element = this._createNew(); - } else { - element = this._pool.pop(); - } - this._inUse[element.getAttribute(DomElementObjectPool.OBJECT_ID_ATTRIBUTE)] = element; - return element; - } - - /** - * Release an element back into the pool. It's up to the caller of this - * function to ensure that all external references to the element have been - * removed. - * @param element The element being released. - */ - public release(element: HTMLElement): void { - if (!this._inUse[element.getAttribute(DomElementObjectPool.OBJECT_ID_ATTRIBUTE)]) { - throw new Error('Could not release an element not yet acquired'); - } - delete this._inUse[element.getAttribute(DomElementObjectPool.OBJECT_ID_ATTRIBUTE)]; - this._cleanElement(element); - this._pool.push(element); - } - - /** - * Creates a new element for the pool. - */ - private _createNew(): HTMLElement { - const element = document.createElement(this._type); - const id = DomElementObjectPool._objectCount++; - element.setAttribute(DomElementObjectPool.OBJECT_ID_ATTRIBUTE, id.toString(10)); - return element; - } - - /** - * Resets an element back to a "clean state". - * @param element The element to be cleaned. - */ - private _cleanElement(element: HTMLElement): void { - element.className = ''; - element.innerHTML = ''; - } -} From b57b94ad0bf839f9e24d3699cd285f44cc26fb47 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 1 Sep 2017 22:02:12 -0700 Subject: [PATCH 046/108] Improve focus/blur state, fire only once Fixes #681 --- src/Terminal.ts | 46 ++++++++++++++++++++-------------------------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 13f3886037..616c29459e 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -532,17 +532,14 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT /** * Binds the desired focus behavior on a given terminal object. */ - private bindFocus(): void { - globalOn(this.textarea, 'focus', (ev) => { - if (this.sendFocus) { - this.send(C0.ESC + '[I'); - } - this.element.classList.add('focus'); - this.showCursor(); - this.restartCursorBlinking.apply(this); - // TODO: Why pass terminal here? - this.emit('focus'); - }); + private _onTextAreaFocus(): void { + if (this.sendFocus) { + this.send(C0.ESC + '[I'); + } + this.element.classList.add('focus'); + this.showCursor(); + this.restartCursorBlinking.apply(this); + this.emit('focus'); }; /** @@ -556,17 +553,14 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT /** * Binds the desired blur behavior on a given terminal object. */ - private bindBlur(): void { - on(this.textarea, 'blur', (ev) => { - this.refresh(this.buffer.y, this.buffer.y); - if (this.sendFocus) { - this.send(C0.ESC + '[O'); - } - this.element.classList.remove('focus'); - this.clearCursorBlinkingInterval.apply(this); - // TODO: Why pass terminal here? - this.emit('blur'); - }); + private _onTextAreaBlur(): void { + this.refresh(this.buffer.y, this.buffer.y); + if (this.sendFocus) { + this.send(C0.ESC + '[O'); + } + this.element.classList.remove('focus'); + this.clearCursorBlinkingInterval.apply(this); + this.emit('blur'); } /** @@ -574,8 +568,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT */ private initGlobal(): void { this.bindKeys(); - this.bindFocus(); - this.bindBlur(); // Bind clipboard functionality on(this.element, 'copy', (event: ClipboardEvent) => { @@ -740,8 +732,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.textarea.setAttribute('autocapitalize', 'off'); this.textarea.setAttribute('spellcheck', 'false'); this.textarea.tabIndex = 0; - this.textarea.addEventListener('focus', () => this.emit('focus')); - this.textarea.addEventListener('blur', () => this.emit('blur')); + this.textarea.addEventListener('focus', () => this._onTextAreaFocus()); + this.textarea.addEventListener('blur', () => this._onTextAreaBlur()); this.helperContainer.appendChild(this.textarea); this.compositionView = document.createElement('div'); @@ -763,6 +755,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.renderer = new Renderer(this); this.on('cursormove', () => this.renderer.onCursorMove()); this.on('resize', () => this.renderer.onResize(this.cols, this.rows)); + this.on('blur', () => this.renderer.onBlur()); + this.on('focus', () => this.renderer.onFocus()); this.charMeasure.on('charsizechanged', () => { this.renderer.onCharSizeChanged(this.charMeasure.width, this.charMeasure.height); // Force a refresh for the char size change From dc1e7276138e7a6d7666ed5a21d1405df29ea0cf Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 1 Sep 2017 22:42:31 -0700 Subject: [PATCH 047/108] Support blurred cursor --- src/Interfaces.ts | 1 + src/Terminal.ts | 4 +++ src/renderer/BaseRenderLayer.ts | 16 +++++++-- src/renderer/CursorRenderLayer.ts | 47 +++++++++++++++++++++++---- src/renderer/ForegroundRenderLayer.ts | 2 +- src/renderer/Interfaces.ts | 10 ++++++ src/renderer/Renderer.ts | 8 +++++ 7 files changed, 78 insertions(+), 10 deletions(-) diff --git a/src/Interfaces.ts b/src/Interfaces.ts index 3a0c99297b..745dd87a46 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -35,6 +35,7 @@ export interface ITerminal extends IEventEmitter { options: ITerminalOptions; buffers: IBufferSet; buffer: IBuffer; + isFocused: boolean; /** * Emit the 'data' event and populate the given data. diff --git a/src/Terminal.ts b/src/Terminal.ts index 616c29459e..fc5af887b5 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -412,6 +412,10 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.textarea.focus(); } + public get isFocused(): boolean { + return document.activeElement === this.textarea; + } + public setTheme(theme: ITheme): void { // TODO: Allow setting of theme before renderer is ready if (this.renderer) { diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 523edbda09..3da7aadcdf 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -30,6 +30,8 @@ export abstract class BaseRenderLayer implements IRenderLayer { // TODO: Should this do anything? public onOptionsChanged(terminal: ITerminal): void {} + public onBlur(terminal: ITerminal): void {} + public onFocus(terminal: ITerminal): void {} public onCursorMove(terminal: ITerminal): void {} public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void {} public onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number]): void {} @@ -58,7 +60,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.fillRect(startCol * this.scaledCharWidth, startRow * this.scaledCharHeight, colWidth * this.scaledCharWidth, colHeight * this.scaledCharHeight); } - protected fillBottomLineAtCell(x: number, y: number): void { + protected drawBottomLineAtCell(x: number, y: number): void { this._ctx.fillRect( x * this.scaledCharWidth, (y + 1) * this.scaledCharHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */, @@ -66,7 +68,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { window.devicePixelRatio); } - protected fillLeftLineAtCell(x: number, y: number): void { + protected drawLeftLineAtCell(x: number, y: number): void { this._ctx.fillRect( x * this.scaledCharWidth, y * this.scaledCharHeight, @@ -74,6 +76,16 @@ export abstract class BaseRenderLayer implements IRenderLayer { this.scaledCharHeight); } + protected drawSquareAtCell(x: number, y: number, color: string): void { + this._ctx.strokeStyle = color; + this._ctx.lineWidth = window.devicePixelRatio; + this._ctx.strokeRect( + x * this.scaledCharWidth + window.devicePixelRatio / 2, + y * this.scaledCharHeight + (window.devicePixelRatio / 2), + this.scaledCharWidth - window.devicePixelRatio, + this.scaledCharHeight - window.devicePixelRatio); + } + protected clearAll(): void { this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); } diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index cd11f1dc8d..941d6d3b74 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -16,6 +16,7 @@ export class CursorRenderLayer extends BaseRenderLayer { private _state: [number, number]; private _cursorRenderers: {[key: string]: (terminal: ITerminal, x: number, y: number, charData: CharData) => void}; private _cursorBlinkStateManager: CursorBlinkStateManager; + private _isFocused: boolean; constructor(container: HTMLElement, zIndex: number, colors: IColorSet) { super(container, 'cursor', zIndex, colors); @@ -37,6 +38,20 @@ export class CursorRenderLayer extends BaseRenderLayer { } } + public onBlur(terminal: ITerminal): void { + if (this._cursorBlinkStateManager) { + // this._cursorBlinkStateManager.pause(); + } + terminal.refresh(terminal.buffer.y, terminal.buffer.y); + } + + public onFocus(terminal: ITerminal): void { + if (this._cursorBlinkStateManager) { + // this._cursorBlinkStateManager.resume(); + } + terminal.refresh(terminal.buffer.y, terminal.buffer.y); + } + public onOptionsChanged(terminal: ITerminal): void { if (terminal.options.cursorBlink) { if (!this._cursorBlinkStateManager) { @@ -69,11 +84,8 @@ export class CursorRenderLayer extends BaseRenderLayer { } private _render(terminal: ITerminal, triggeredByAnimationFrame: boolean): void { - // TODO: Track blur/focus somehow, support unfocused cursor // Don't draw the cursor if it's hidden - if (!terminal.cursorState || - terminal.cursorHidden || - (this._cursorBlinkStateManager && !this._cursorBlinkStateManager.isCursorVisible)) { + if (!terminal.cursorState || terminal.cursorHidden) { this._clearCursor(); return; } @@ -87,6 +99,23 @@ export class CursorRenderLayer extends BaseRenderLayer { return; } + const charData = terminal.buffer.lines.get(cursorY)[terminal.buffer.x]; + + if (!terminal.isFocused) { + this._clearCursor(); + this._ctx.save(); + this._ctx.fillStyle = this.colors.ansi[COLOR_CODES.WHITE]; + this._renderBlurCursor(terminal, terminal.buffer.x, viewportRelativeCursorY, charData); + this._ctx.restore(); + return; + } + + // Don't draw the cursor if it's blinking + if (this._cursorBlinkStateManager && !this._cursorBlinkStateManager.isCursorVisible) { + this._clearCursor(); + return; + } + if (this._state) { // The cursor is already in the correct spot, don't redraw if (this._state[0] === terminal.buffer.x && this._state[1] === viewportRelativeCursorY) { @@ -95,7 +124,6 @@ export class CursorRenderLayer extends BaseRenderLayer { this._clearCursor(); } - const charData = terminal.buffer.lines.get(cursorY)[terminal.buffer.x]; this._ctx.save(); this._ctx.fillStyle = this.colors.ansi[COLOR_CODES.WHITE]; this._cursorRenderers[terminal.options.cursorStyle || 'block'](terminal, terminal.buffer.x, viewportRelativeCursorY, charData); @@ -111,7 +139,7 @@ export class CursorRenderLayer extends BaseRenderLayer { } private _renderBarCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void { - this.fillLeftLineAtCell(x, y); + this.drawLeftLineAtCell(x, y); } private _renderBlockCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void { @@ -120,7 +148,12 @@ export class CursorRenderLayer extends BaseRenderLayer { } private _renderUnderlineCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void { - this.fillBottomLineAtCell(x, y); + this.drawBottomLineAtCell(x, y); + } + + private _renderBlurCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void { + // TODO: Support cursor colors + this.drawSquareAtCell(x, y, this.colors.foreground); } } diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index 30f68f4c5a..f4bc1e7778 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -97,7 +97,7 @@ export class ForegroundRenderLayer extends BaseRenderLayer { } else { this._ctx.fillStyle = this.colors.foreground; } - this.fillBottomLineAtCell(x, y); + this.drawBottomLineAtCell(x, y); } this.drawChar(terminal, char, code, x, y, fg); diff --git a/src/renderer/Interfaces.ts b/src/renderer/Interfaces.ts index 45822e6530..b47a75b9a5 100644 --- a/src/renderer/Interfaces.ts +++ b/src/renderer/Interfaces.ts @@ -1,6 +1,16 @@ import { ITerminal, ITerminalOptions } from '../Interfaces'; export interface IRenderLayer { + /** + * Called when the terminal loses focus. + */ + onBlur(terminal: ITerminal): void; + + /** + * * Called when the terminal gets focus. + */ + onFocus(terminal: ITerminal): void; + /** * Called when the cursor is moved. */ diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 119399b06a..474699c5bc 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -57,6 +57,14 @@ export class Renderer { this._renderLayers.forEach(l => l.resize(this._terminal, width, height, true)); } + public onBlur(): void { + this._renderLayers.forEach(l => l.onBlur(this._terminal)); + } + + public onFocus(): void { + this._renderLayers.forEach(l => l.onFocus(this._terminal)); + } + public onSelectionChanged(start: [number, number], end: [number, number]): void { this._renderLayers.forEach(l => l.onSelectionChanged(this._terminal, start, end)); } From 4919c40b57a3c05515f1b176751831905d121e90 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 1 Sep 2017 22:53:21 -0700 Subject: [PATCH 048/108] Add cursor to ITheme --- src/Interfaces.ts | 2 +- src/renderer/BaseRenderLayer.ts | 17 +++++++++++++---- src/{utils => renderer}/CharAtlas.ts | 6 +++--- src/renderer/ColorManager.ts | 2 ++ src/renderer/CursorRenderLayer.ts | 14 +++++++++++--- src/renderer/Interfaces.ts | 1 + src/utils/TestUtils.test.ts | 1 + 7 files changed, 32 insertions(+), 11 deletions(-) rename src/{utils => renderer}/CharAtlas.ts (97%) diff --git a/src/Interfaces.ts b/src/Interfaces.ts index 745dd87a46..b6ba08db1c 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -299,7 +299,7 @@ export interface IInputHandler { export interface ITheme { foreground?: string; background?: string; - // cursor?: string; + cursor?: string; black?: string; red?: string; green?: string; diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 3da7aadcdf..fb78e1d2a9 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -1,6 +1,6 @@ import { IRenderLayer, IColorSet } from './Interfaces'; import { ITerminal, ITerminalOptions } from '../Interfaces'; -import { acquireCharAtlas } from '../utils/CharAtlas'; +import { acquireCharAtlas } from './CharAtlas'; export const INVERTED_DEFAULT_COLOR = -1; @@ -94,6 +94,15 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.clearRect(startCol * this.scaledCharWidth, startRow * this.scaledCharHeight, colWidth * this.scaledCharWidth, colHeight * this.scaledCharHeight); } + protected drawCharTrueColor(terminal: ITerminal, char: string, code: number, x: number, y: number, color: string): void { + this._ctx.save(); + this._ctx.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`; + this._ctx.textBaseline = 'top'; + this._ctx.fillStyle = color; + this._ctx.fillText(char, x * this.scaledCharWidth, y * this.scaledCharHeight); + this._ctx.restore(); + } + protected drawChar(terminal: ITerminal, char: string, code: number, x: number, y: number, fg: number, underline: boolean = false): void { let colorIndex = 0; if (fg < 256) { @@ -105,13 +114,13 @@ export abstract class BaseRenderLayer implements IRenderLayer { code * this.scaledCharWidth, colorIndex * this.scaledCharHeight, this.scaledCharWidth, this.scaledCharHeight, x * this.scaledCharWidth, y * this.scaledCharHeight, this.scaledCharWidth, this.scaledCharHeight); } else { - this._drawUncachedChar(terminal, char, fg, x, y, this.scaledCharWidth, this.scaledCharHeight); + this._drawUncachedChar(terminal, char, fg, x, y); } // This draws the atlas (for debugging purposes) // this._ctx.drawImage(BaseRenderLayer._charAtlas, 0, 0); } - private _drawUncachedChar(terminal: ITerminal, char: string, fg: number, x: number, y: number, scaledCharWidth: number, scaledCharHeight: number): void { + private _drawUncachedChar(terminal: ITerminal, char: string, fg: number, x: number, y: number): void { this._ctx.save(); this._ctx.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`; this._ctx.textBaseline = 'top'; @@ -125,7 +134,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.fillStyle = this.colors.foreground; } - this._ctx.fillText(char, x * scaledCharWidth, y * scaledCharHeight); + this._ctx.fillText(char, x * this.scaledCharWidth, y * this.scaledCharHeight); this._ctx.restore(); } } diff --git a/src/utils/CharAtlas.ts b/src/renderer/CharAtlas.ts similarity index 97% rename from src/utils/CharAtlas.ts rename to src/renderer/CharAtlas.ts index 18398efc74..e71d8d5a58 100644 --- a/src/utils/CharAtlas.ts +++ b/src/renderer/CharAtlas.ts @@ -63,7 +63,8 @@ export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet): Promis function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: ITerminal, colors: IColorSet): ICharAtlasConfig { const clonedColors = { foreground: colors.foreground, - background: colors.background, + background: null, + cursor: null, ansi: colors.ansi.slice(0, 16) }; return { @@ -85,8 +86,7 @@ function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean { a.fontSize === b.fontSize && a.scaledCharWidth === b.scaledCharWidth && a.scaledCharHeight === b.scaledCharHeight && - a.colors.foreground === b.colors.foreground && - a.colors.background === b.colors.background; + a.colors.foreground === b.colors.foreground; } class CharAtlasGenerator { diff --git a/src/renderer/ColorManager.ts b/src/renderer/ColorManager.ts index ca8015a2b4..341e4ca588 100644 --- a/src/renderer/ColorManager.ts +++ b/src/renderer/ColorManager.ts @@ -77,6 +77,7 @@ export class ColorManager { this.colors = { foreground: '#ffffff', background: '#000000', + cursor: '#ffffff', ansi: generate256Colors(DEFAULT_ANSI_COLORS) }; } @@ -84,6 +85,7 @@ export class ColorManager { public setTheme(theme: ITheme): void { if (theme.foreground) this.colors.foreground = theme.foreground; if (theme.background) this.colors.background = theme.background; + if (theme.cursor) this.colors.cursor = theme.cursor; if (theme.black) this.colors.ansi[0] = theme.black; if (theme.red) this.colors.ansi[1] = theme.red; if (theme.green) this.colors.ansi[2] = theme.green; diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index 941d6d3b74..10f3ad8ce0 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -139,21 +139,29 @@ export class CursorRenderLayer extends BaseRenderLayer { } private _renderBarCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void { + this._ctx.save(); + this._ctx.fillStyle = this.colors.cursor; this.drawLeftLineAtCell(x, y); + this._ctx.restore(); } private _renderBlockCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void { + this._ctx.save(); + this._ctx.fillStyle = this.colors.cursor; this.fillCells(x, y, 1, 1); - this.drawChar(terminal, charData[CHAR_DATA_CHAR_INDEX], charData[CHAR_DATA_CODE_INDEX], x, y, COLOR_CODES.BLACK); + this._ctx.restore(); + this.drawCharTrueColor(terminal, charData[CHAR_DATA_CHAR_INDEX], charData[CHAR_DATA_CODE_INDEX], x, y, this.colors.background); } private _renderUnderlineCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void { + this._ctx.save(); + this._ctx.fillStyle = this.colors.cursor; this.drawBottomLineAtCell(x, y); + this._ctx.restore(); } private _renderBlurCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void { - // TODO: Support cursor colors - this.drawSquareAtCell(x, y, this.colors.foreground); + this.drawSquareAtCell(x, y, this.colors.cursor); } } diff --git a/src/renderer/Interfaces.ts b/src/renderer/Interfaces.ts index b47a75b9a5..fd94d59d7f 100644 --- a/src/renderer/Interfaces.ts +++ b/src/renderer/Interfaces.ts @@ -52,5 +52,6 @@ export interface IRenderLayer { export interface IColorSet { foreground: string; background: string; + cursor: string; ansi: string[]; } diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index 3a66d49d05..f4eae64db9 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -7,6 +7,7 @@ import { LineData } from '../Types'; import * as Browser from './Browser'; export class MockTerminal implements ITerminal { + isFocused: boolean; options: ITerminalOptions = {}; element: HTMLElement; rowContainer: HTMLElement; From f8ece108c6f6d6ff7dd11c9f4eff434368ea0cdc Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 1 Sep 2017 23:00:29 -0700 Subject: [PATCH 049/108] Rerender cursor if state changes --- src/Terminal.ts | 48 ------------------------------- src/renderer/CursorRenderLayer.ts | 9 ++++-- 2 files changed, 6 insertions(+), 51 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index fc5af887b5..7d2e8fd9c0 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -61,13 +61,6 @@ const WRITE_BUFFER_PAUSE_THRESHOLD = 5; */ const WRITE_BATCH_SIZE = 300; -/** - * The time between cursor blinks. This is driven by JS rather than a CSS - * animation due to a bug in Chromium that causes it to use excessive CPU time. - * See https://github.com/Microsoft/vscode/issues/22900 - */ -const CURSOR_BLINK_INTERVAL = 600; - // TODO: Most of the color code should be removed after truecolor is implemented // Colors 0-15 const tangoColors: string[] = [ @@ -197,10 +190,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT private sendDataQueue: string; private customKeyEventHandler: CustomKeyEventHandler; - // The ID from a setInterval that tracks the blink animation. This animation - // is done in JS due to a Chromium bug with CSS animations that thrashed the - // CPU. - private cursorBlinkInterval: NodeJS.Timer; // modes public applicationKeypad: boolean; @@ -341,7 +330,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.cursorHidden = false; this.sendDataQueue = ''; this.customKeyEventHandler = null; - this.cursorBlinkInterval = null; // modes this.applicationKeypad = false; @@ -488,12 +476,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this[key] = value; this.options[key] = value; switch (key) { - case 'cursorBlink': this.setCursorBlinking(value); break; - case 'cursorStyle': - this.element.classList.toggle(`xterm-cursor-style-block`, value === 'block'); - this.element.classList.toggle(`xterm-cursor-style-underline`, value === 'underline'); - this.element.classList.toggle(`xterm-cursor-style-bar`, value === 'bar'); - break; case 'fontFamily': case 'fontSize': // When the font changes the size of the cells may change which requires a renderer clear @@ -511,28 +493,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.renderer.onOptionsChanged(); } - private restartCursorBlinking(): void { - this.setCursorBlinking(this.options.cursorBlink); - } - - private setCursorBlinking(enabled: boolean): void { - this.element.classList.toggle('xterm-cursor-blink', enabled); - this.clearCursorBlinkingInterval(); - if (enabled) { - this.cursorBlinkInterval = setInterval(() => { - this.element.classList.toggle('xterm-cursor-blink-on'); - }, CURSOR_BLINK_INTERVAL); - } - } - - private clearCursorBlinkingInterval(): void { - this.element.classList.remove('xterm-cursor-blink-on'); - if (this.cursorBlinkInterval) { - clearInterval(this.cursorBlinkInterval); - this.cursorBlinkInterval = null; - } - } - /** * Binds the desired focus behavior on a given terminal object. */ @@ -542,7 +502,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT } this.element.classList.add('focus'); this.showCursor(); - this.restartCursorBlinking.apply(this); this.emit('focus'); }; @@ -563,7 +522,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.send(C0.ESC + '[O'); } this.element.classList.remove('focus'); - this.clearCursorBlinkingInterval.apply(this); this.emit('blur'); } @@ -696,8 +654,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.element = this.document.createElement('div'); this.element.classList.add('terminal'); this.element.classList.add('xterm'); - this.element.classList.add(`xterm-cursor-style-${this.options.cursorStyle}`); - this.setCursorBlinking(this.options.cursorBlink); this.element.setAttribute('tabindex', '0'); @@ -1462,8 +1418,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT return false; } - this.restartCursorBlinking(); - if (!this.compositionHelper.keydown(ev)) { if (this.buffer.ybase !== this.buffer.ydisp) { this.scrollToBottom(); @@ -2171,12 +2125,10 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.options.rows = this.rows; this.options.cols = this.cols; const customKeyEventHandler = this.customKeyEventHandler; - const cursorBlinkInterval = this.cursorBlinkInterval; const inputHandler = this.inputHandler; const buffers = this.buffers; this.setup(); this.customKeyEventHandler = customKeyEventHandler; - this.cursorBlinkInterval = cursorBlinkInterval; this.inputHandler = inputHandler; this.buffers = buffers; this.refresh(0, this.rows - 1); diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index 10f3ad8ce0..752b42ddf8 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -13,7 +13,7 @@ import { COLOR_CODES } from './ColorManager'; const BLINK_INTERVAL = 600; export class CursorRenderLayer extends BaseRenderLayer { - private _state: [number, number]; + private _state: [number, number, string]; private _cursorRenderers: {[key: string]: (terminal: ITerminal, x: number, y: number, charData: CharData) => void}; private _cursorBlinkStateManager: CursorBlinkStateManager; private _isFocused: boolean; @@ -118,7 +118,10 @@ export class CursorRenderLayer extends BaseRenderLayer { if (this._state) { // The cursor is already in the correct spot, don't redraw - if (this._state[0] === terminal.buffer.x && this._state[1] === viewportRelativeCursorY) { + if (this._state[0] === terminal.buffer.x && + this._state[1] === viewportRelativeCursorY && + this._state[2] === terminal.options.cursorStyle) { + // TODO: Ideally cursorStyle would be stored as a number here to prevent the string compare return; } this._clearCursor(); @@ -128,7 +131,7 @@ export class CursorRenderLayer extends BaseRenderLayer { this._ctx.fillStyle = this.colors.ansi[COLOR_CODES.WHITE]; this._cursorRenderers[terminal.options.cursorStyle || 'block'](terminal, terminal.buffer.x, viewportRelativeCursorY, charData); this._ctx.restore(); - this._state = [terminal.buffer.x, viewportRelativeCursorY]; + this._state = [terminal.buffer.x, viewportRelativeCursorY, terminal.options.cursorStyle]; } private _clearCursor(): void { From 06b54585a18ed0941829c538a2a843f4ddd2a41a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 1 Sep 2017 23:11:39 -0700 Subject: [PATCH 050/108] Fix cursor render when after focusing underline/bar --- src/renderer/CursorRenderLayer.ts | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index 752b42ddf8..9a2ce3fdd8 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -13,7 +13,7 @@ import { COLOR_CODES } from './ColorManager'; const BLINK_INTERVAL = 600; export class CursorRenderLayer extends BaseRenderLayer { - private _state: [number, number, string]; + private _state: [number, number, boolean, string]; private _cursorRenderers: {[key: string]: (terminal: ITerminal, x: number, y: number, charData: CharData) => void}; private _cursorBlinkStateManager: CursorBlinkStateManager; private _isFocused: boolean; @@ -40,14 +40,14 @@ export class CursorRenderLayer extends BaseRenderLayer { public onBlur(terminal: ITerminal): void { if (this._cursorBlinkStateManager) { - // this._cursorBlinkStateManager.pause(); + this._cursorBlinkStateManager.pause(); } terminal.refresh(terminal.buffer.y, terminal.buffer.y); } public onFocus(terminal: ITerminal): void { if (this._cursorBlinkStateManager) { - // this._cursorBlinkStateManager.resume(); + this._cursorBlinkStateManager.resume(); } terminal.refresh(terminal.buffer.y, terminal.buffer.y); } @@ -107,6 +107,7 @@ export class CursorRenderLayer extends BaseRenderLayer { this._ctx.fillStyle = this.colors.ansi[COLOR_CODES.WHITE]; this._renderBlurCursor(terminal, terminal.buffer.x, viewportRelativeCursorY, charData); this._ctx.restore(); + this._state = [terminal.buffer.x, viewportRelativeCursorY, terminal.isFocused, terminal.options.cursorStyle]; return; } @@ -120,7 +121,8 @@ export class CursorRenderLayer extends BaseRenderLayer { // The cursor is already in the correct spot, don't redraw if (this._state[0] === terminal.buffer.x && this._state[1] === viewportRelativeCursorY && - this._state[2] === terminal.options.cursorStyle) { + this._state[2] === terminal.isFocused && + this._state[3] === terminal.options.cursorStyle) { // TODO: Ideally cursorStyle would be stored as a number here to prevent the string compare return; } @@ -131,7 +133,7 @@ export class CursorRenderLayer extends BaseRenderLayer { this._ctx.fillStyle = this.colors.ansi[COLOR_CODES.WHITE]; this._cursorRenderers[terminal.options.cursorStyle || 'block'](terminal, terminal.buffer.x, viewportRelativeCursorY, charData); this._ctx.restore(); - this._state = [terminal.buffer.x, viewportRelativeCursorY, terminal.options.cursorStyle]; + this._state = [terminal.buffer.x, viewportRelativeCursorY, terminal.isFocused, terminal.options.cursorStyle]; } private _clearCursor(): void { @@ -261,11 +263,18 @@ class CursorBlinkStateManager { }, timeToStart); } - private _pauseBlinkAnimation(): void { - // TODO: Pause the blink animation on blur + public pause(): void { + this.isCursorVisible = true; + window.clearInterval(this._blinkInterval); + this._blinkInterval = null; + if (this._animationFrame) { + window.cancelAnimationFrame(this._animationFrame); + this._animationFrame = null; + } } - private _resumeBlinkAnimation(): void { - // TODO: Resume the blink animation on focus + public resume(): void { + console.log('resume'); + this._restartInterval(); } } From cf7237d2a4f0885f9ac8167a79cf5105d9325b5a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 1 Sep 2017 23:47:17 -0700 Subject: [PATCH 051/108] Fix remaining cursor animation state issues --- src/renderer/CursorRenderLayer.ts | 48 ++++++++++++++++++++++--------- src/renderer/Renderer.ts | 1 - 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index 9a2ce3fdd8..0894498f28 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -42,14 +42,14 @@ export class CursorRenderLayer extends BaseRenderLayer { if (this._cursorBlinkStateManager) { this._cursorBlinkStateManager.pause(); } - terminal.refresh(terminal.buffer.y, terminal.buffer.y); + terminal.emit('cursormove'); } public onFocus(terminal: ITerminal): void { if (this._cursorBlinkStateManager) { this._cursorBlinkStateManager.resume(); } - terminal.refresh(terminal.buffer.y, terminal.buffer.y); + terminal.emit('cursormove'); } public onOptionsChanged(terminal: ITerminal): void { @@ -78,7 +78,7 @@ export class CursorRenderLayer extends BaseRenderLayer { public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void { // Only render if the animation frame is not active - if (!this._cursorBlinkStateManager) { + if (!this._cursorBlinkStateManager || this._cursorBlinkStateManager.isPaused) { this._render(terminal, false); } } @@ -107,7 +107,7 @@ export class CursorRenderLayer extends BaseRenderLayer { this._ctx.fillStyle = this.colors.ansi[COLOR_CODES.WHITE]; this._renderBlurCursor(terminal, terminal.buffer.x, viewportRelativeCursorY, charData); this._ctx.restore(); - this._state = [terminal.buffer.x, viewportRelativeCursorY, terminal.isFocused, terminal.options.cursorStyle]; + this._state = [terminal.buffer.x, viewportRelativeCursorY, false, terminal.options.cursorStyle]; return; } @@ -123,7 +123,6 @@ export class CursorRenderLayer extends BaseRenderLayer { this._state[1] === viewportRelativeCursorY && this._state[2] === terminal.isFocused && this._state[3] === terminal.options.cursorStyle) { - // TODO: Ideally cursorStyle would be stored as a number here to prevent the string compare return; } this._clearCursor(); @@ -133,7 +132,7 @@ export class CursorRenderLayer extends BaseRenderLayer { this._ctx.fillStyle = this.colors.ansi[COLOR_CODES.WHITE]; this._cursorRenderers[terminal.options.cursorStyle || 'block'](terminal, terminal.buffer.x, viewportRelativeCursorY, charData); this._ctx.restore(); - this._state = [terminal.buffer.x, viewportRelativeCursorY, terminal.isFocused, terminal.options.cursorStyle]; + this._state = [terminal.buffer.x, viewportRelativeCursorY, true, terminal.options.cursorStyle]; } private _clearCursor(): void { @@ -189,12 +188,22 @@ class CursorBlinkStateManager { private renderCallback: () => void ) { this.isCursorVisible = true; - this._restartInterval(); + if (terminal.isFocused) { + this._restartInterval(); + } } + public get isPaused(): boolean { return !(this._blinkStartTimeout || this._blinkInterval); } + public dispose(): void { - window.clearInterval(this._blinkInterval); - this._blinkInterval = null; + if (this._blinkInterval) { + window.clearInterval(this._blinkInterval); + this._blinkInterval = null; + } + if (this._blinkStartTimeout) { + window.clearTimeout(this._blinkStartTimeout); + this._blinkStartTimeout = null; + } if (this._animationFrame) { window.cancelAnimationFrame(this._animationFrame); this._animationFrame = null; @@ -202,6 +211,9 @@ class CursorBlinkStateManager { } public restartBlinkAnimation(terminal: ITerminal): void { + if (this.isPaused) { + return; + } // Save a timestamp so that the restart can be done on the next interval this._animationTimeRestarted = Date.now(); // Force a cursor render to ensure it's visible and in the correct position @@ -230,8 +242,10 @@ class CursorBlinkStateManager { if (this._animationTimeRestarted) { const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted); this._animationTimeRestarted = null; - this._restartInterval(time); - return; + if (time > 0) { + this._restartInterval(time); + return; + } } // Hide the cursor @@ -265,8 +279,14 @@ class CursorBlinkStateManager { public pause(): void { this.isCursorVisible = true; - window.clearInterval(this._blinkInterval); - this._blinkInterval = null; + if (this._blinkInterval) { + window.clearInterval(this._blinkInterval); + this._blinkInterval = null; + } + if (this._blinkStartTimeout) { + window.clearTimeout(this._blinkStartTimeout); + this._blinkStartTimeout = null; + } if (this._animationFrame) { window.cancelAnimationFrame(this._animationFrame); this._animationFrame = null; @@ -274,7 +294,7 @@ class CursorBlinkStateManager { } public resume(): void { - console.log('resume'); + this._animationTimeRestarted = null; this._restartInterval(); } } diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 474699c5bc..cc85c7e76c 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -33,7 +33,6 @@ export class Renderer { } public setTheme(theme: ITheme): void { - console.log('setTheme'); this._colorManager.setTheme(theme); // Clear layers and force a full render this._renderLayers.forEach(l => { From 36a723c4ccc51fa49b294e239170f33a9612c5fb Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 1 Sep 2017 23:58:34 -0700 Subject: [PATCH 052/108] Fix 256 FG chars --- src/renderer/BaseRenderLayer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index fb78e1d2a9..ecd064f3fe 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -108,7 +108,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { if (fg < 256) { colorIndex = fg + 1; } - if (code < 256 && (colorIndex > 0 || fg > 255)) { + if (code < 256 && colorIndex > 0 && fg < 16) { // ImageBitmap's draw about twice as fast as from a canvas this._ctx.drawImage(this._charAtlas, code * this.scaledCharWidth, colorIndex * this.scaledCharHeight, this.scaledCharWidth, this.scaledCharHeight, From 6dd6d0391f9d8bc86e68413f34516331050aeb11 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 2 Sep 2017 00:05:31 -0700 Subject: [PATCH 053/108] Add cell spacing to char atlas --- src/renderer/BaseRenderLayer.ts | 8 +++++--- src/renderer/CharAtlas.ts | 16 +++++++++------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index ecd064f3fe..bd8e489663 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -1,6 +1,6 @@ import { IRenderLayer, IColorSet } from './Interfaces'; import { ITerminal, ITerminalOptions } from '../Interfaces'; -import { acquireCharAtlas } from './CharAtlas'; +import { acquireCharAtlas, CHAR_ATLAS_CELL_SPACING } from './CharAtlas'; export const INVERTED_DEFAULT_COLOR = -1; @@ -110,14 +110,16 @@ export abstract class BaseRenderLayer implements IRenderLayer { } if (code < 256 && colorIndex > 0 && fg < 16) { // ImageBitmap's draw about twice as fast as from a canvas + const charAtlasCellWidth = this.scaledCharWidth + CHAR_ATLAS_CELL_SPACING; + const charAtlasCellHeight = this.scaledCharHeight + CHAR_ATLAS_CELL_SPACING; this._ctx.drawImage(this._charAtlas, - code * this.scaledCharWidth, colorIndex * this.scaledCharHeight, this.scaledCharWidth, this.scaledCharHeight, + code * charAtlasCellWidth, colorIndex * charAtlasCellHeight, this.scaledCharWidth, this.scaledCharHeight, x * this.scaledCharWidth, y * this.scaledCharHeight, this.scaledCharWidth, this.scaledCharHeight); } else { this._drawUncachedChar(terminal, char, fg, x, y); } // This draws the atlas (for debugging purposes) - // this._ctx.drawImage(BaseRenderLayer._charAtlas, 0, 0); + // this._ctx.drawImage(this._charAtlas, 0, 0); } private _drawUncachedChar(terminal: ITerminal, char: string, fg: number, x: number, y: number): void { diff --git a/src/renderer/CharAtlas.ts b/src/renderer/CharAtlas.ts index e71d8d5a58..a9fe8bd301 100644 --- a/src/renderer/CharAtlas.ts +++ b/src/renderer/CharAtlas.ts @@ -1,6 +1,8 @@ import { ITerminal, ITheme } from '../Interfaces'; import { IColorSet } from '../renderer/Interfaces'; +export const CHAR_ATLAS_CELL_SPACING = 1; + interface ICharAtlasConfig { fontSize: number; fontFamily: string; @@ -100,8 +102,10 @@ class CharAtlasGenerator { } public generate(scaledCharWidth: number, scaledCharHeight: number, fontSize: number, fontFamily: string, foreground: string, ansiColors: string[]): Promise { - this._canvas.width = 255 * scaledCharWidth; - this._canvas.height = (/*default*/1 + /*0-15*/16) * scaledCharHeight; + const cellWidth = scaledCharWidth + CHAR_ATLAS_CELL_SPACING; + const cellHeight = scaledCharHeight + CHAR_ATLAS_CELL_SPACING; + this._canvas.width = 255 * cellWidth; + this._canvas.height = (/*default*/1 + /*0-15*/16) * cellHeight; this._ctx.save(); this._ctx.fillStyle = foreground; @@ -110,7 +114,7 @@ class CharAtlasGenerator { // Default color for (let i = 0; i < 256; i++) { - this._ctx.fillText(String.fromCharCode(i), i * scaledCharWidth, 0); + this._ctx.fillText(String.fromCharCode(i), i * cellWidth, 0); } // Colors 0-15 @@ -119,13 +123,11 @@ class CharAtlasGenerator { if (colorIndex === 8) { this._ctx.font = `bold ${this._ctx.font}`; } - const y = (colorIndex + 1) * scaledCharHeight; - // Clear rectangle as some fonts seem to draw over the bottom boundary - this._ctx.clearRect(0, y, this._canvas.width, scaledCharHeight); + const y = (colorIndex + 1) * cellHeight; // Draw ascii characters for (let i = 0; i < 256; i++) { this._ctx.fillStyle = ansiColors[colorIndex]; - this._ctx.fillText(String.fromCharCode(i), i * scaledCharWidth, y); + this._ctx.fillText(String.fromCharCode(i), i * cellWidth, y); } } this._ctx.restore(); From ae72e1c51ed31bb151101b4e3ec0c028c96cc394 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 2 Sep 2017 09:49:05 -0700 Subject: [PATCH 054/108] Fix uncached chars bleeding into other cells --- src/renderer/BaseRenderLayer.ts | 11 ++++++++++- src/renderer/CursorRenderLayer.ts | 2 ++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index bd8e489663..7e88359863 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -108,7 +108,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { if (fg < 256) { colorIndex = fg + 1; } - if (code < 256 && colorIndex > 0 && fg < 16) { + if (code < 256 && (colorIndex > 0 || fg >= 256)) { // ImageBitmap's draw about twice as fast as from a canvas const charAtlasCellWidth = this.scaledCharWidth + CHAR_ATLAS_CELL_SPACING; const charAtlasCellHeight = this.scaledCharHeight + CHAR_ATLAS_CELL_SPACING; @@ -136,6 +136,15 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.fillStyle = this.colors.foreground; } + // Since uncached characters are not coming off the char atlas with source + // coordinates, it means that text drawn to the canvas (particularly '_') + // can bleed into other cells. This code will clip the following fillText, + // ensuring that its contents don't go beyond the cell bounds. + this._ctx.beginPath(); + this._ctx.rect(x * this.scaledCharWidth, y * this.scaledCharHeight, this.scaledCharWidth, this.scaledCharHeight); + this._ctx.clip(); + + // Draw the character this._ctx.fillText(char, x * this.scaledCharWidth, y * this.scaledCharHeight); this._ctx.restore(); } diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index 0894498f28..96e4b8fb9e 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -165,7 +165,9 @@ export class CursorRenderLayer extends BaseRenderLayer { } private _renderBlurCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void { + this._ctx.save(); this.drawSquareAtCell(x, y, this.colors.cursor); + this._ctx.restore(); } } From 32a3c9812d517684521db9a0f246e5e0b01c6e45 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 2 Sep 2017 10:12:30 -0700 Subject: [PATCH 055/108] Support wide character drawing and cursors --- src/renderer/BaseRenderLayer.ts | 29 ++++++++---- src/renderer/CursorRenderLayer.ts | 68 ++++++++++++++++++++------- src/renderer/ForegroundRenderLayer.ts | 4 +- 3 files changed, 73 insertions(+), 28 deletions(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 7e88359863..2650af3acc 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -1,6 +1,8 @@ import { IRenderLayer, IColorSet } from './Interfaces'; import { ITerminal, ITerminalOptions } from '../Interfaces'; import { acquireCharAtlas, CHAR_ATLAS_CELL_SPACING } from './CharAtlas'; +import { CharData } from '../Types'; +import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; export const INVERTED_DEFAULT_COLOR = -1; @@ -76,14 +78,14 @@ export abstract class BaseRenderLayer implements IRenderLayer { this.scaledCharHeight); } - protected drawSquareAtCell(x: number, y: number, color: string): void { + protected drawRectAtCell(x: number, y: number, width: number, height: number, color: string): void { this._ctx.strokeStyle = color; this._ctx.lineWidth = window.devicePixelRatio; this._ctx.strokeRect( x * this.scaledCharWidth + window.devicePixelRatio / 2, y * this.scaledCharHeight + (window.devicePixelRatio / 2), - this.scaledCharWidth - window.devicePixelRatio, - this.scaledCharHeight - window.devicePixelRatio); + (width * this.scaledCharWidth) - window.devicePixelRatio, + (height * this.scaledCharHeight) - window.devicePixelRatio); } protected clearAll(): void { @@ -94,16 +96,25 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.clearRect(startCol * this.scaledCharWidth, startRow * this.scaledCharHeight, colWidth * this.scaledCharWidth, colHeight * this.scaledCharHeight); } - protected drawCharTrueColor(terminal: ITerminal, char: string, code: number, x: number, y: number, color: string): void { + protected drawCharTrueColor(terminal: ITerminal, charData: CharData, x: number, y: number, color: string): void { this._ctx.save(); this._ctx.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`; this._ctx.textBaseline = 'top'; this._ctx.fillStyle = color; - this._ctx.fillText(char, x * this.scaledCharWidth, y * this.scaledCharHeight); + + // Since uncached characters are not coming off the char atlas with source + // coordinates, it means that text drawn to the canvas (particularly '_') + // can bleed into other cells. This code will clip the following fillText, + // ensuring that its contents don't go beyond the cell bounds. + this._ctx.beginPath(); + this._ctx.rect(x * this.scaledCharWidth, y * this.scaledCharHeight, charData[CHAR_DATA_WIDTH_INDEX] * this.scaledCharWidth, this.scaledCharHeight); + this._ctx.clip(); + + this._ctx.fillText(charData[CHAR_DATA_CHAR_INDEX], x * this.scaledCharWidth, y * this.scaledCharHeight); this._ctx.restore(); } - protected drawChar(terminal: ITerminal, char: string, code: number, x: number, y: number, fg: number, underline: boolean = false): void { + protected drawChar(terminal: ITerminal, char: string, code: number, width: number, x: number, y: number, fg: number, underline: boolean = false): void { let colorIndex = 0; if (fg < 256) { colorIndex = fg + 1; @@ -116,13 +127,13 @@ export abstract class BaseRenderLayer implements IRenderLayer { code * charAtlasCellWidth, colorIndex * charAtlasCellHeight, this.scaledCharWidth, this.scaledCharHeight, x * this.scaledCharWidth, y * this.scaledCharHeight, this.scaledCharWidth, this.scaledCharHeight); } else { - this._drawUncachedChar(terminal, char, fg, x, y); + this._drawUncachedChar(terminal, char, width, fg, x, y); } // This draws the atlas (for debugging purposes) // this._ctx.drawImage(this._charAtlas, 0, 0); } - private _drawUncachedChar(terminal: ITerminal, char: string, fg: number, x: number, y: number): void { + private _drawUncachedChar(terminal: ITerminal, char: string, width: number, fg: number, x: number, y: number): void { this._ctx.save(); this._ctx.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`; this._ctx.textBaseline = 'top'; @@ -141,7 +152,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { // can bleed into other cells. This code will clip the following fillText, // ensuring that its contents don't go beyond the cell bounds. this._ctx.beginPath(); - this._ctx.rect(x * this.scaledCharWidth, y * this.scaledCharHeight, this.scaledCharWidth, this.scaledCharHeight); + this._ctx.rect(x * this.scaledCharWidth, y * this.scaledCharHeight, width * this.scaledCharWidth, this.scaledCharHeight); this._ctx.clip(); // Draw the character diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index 96e4b8fb9e..cc443b4979 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -1,26 +1,40 @@ import { IColorSet } from './Interfaces'; import { IBuffer, ICharMeasure, ITerminal, ITerminalOptions } from '../Interfaces'; -import { CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; +import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; import { GridCache } from './GridCache'; import { FLAGS } from './Types'; import { BaseRenderLayer } from './BaseRenderLayer'; import { CharData } from '../Types'; import { COLOR_CODES } from './ColorManager'; +interface CursorState { + x: number; + y: number; + isFocused: boolean; + style: string; + width: number; +} + /** * The time between cursor blinks. */ const BLINK_INTERVAL = 600; export class CursorRenderLayer extends BaseRenderLayer { - private _state: [number, number, boolean, string]; + private _state: CursorState; private _cursorRenderers: {[key: string]: (terminal: ITerminal, x: number, y: number, charData: CharData) => void}; private _cursorBlinkStateManager: CursorBlinkStateManager; private _isFocused: boolean; constructor(container: HTMLElement, zIndex: number, colors: IColorSet) { super(container, 'cursor', zIndex, colors); - this._state = null; + this._state = { + x: null, + y: null, + isFocused: null, + style: null, + width: null, + }; this._cursorRenderers = { 'bar': this._renderBarCursor.bind(this), 'block': this._renderBlockCursor.bind(this), @@ -42,14 +56,15 @@ export class CursorRenderLayer extends BaseRenderLayer { if (this._cursorBlinkStateManager) { this._cursorBlinkStateManager.pause(); } - terminal.emit('cursormove'); + terminal.refresh(terminal.buffer.y, terminal.buffer.y); } public onFocus(terminal: ITerminal): void { if (this._cursorBlinkStateManager) { - this._cursorBlinkStateManager.resume(); + this._cursorBlinkStateManager.resume(terminal); + } else { + terminal.refresh(terminal.buffer.y, terminal.buffer.y); } - terminal.emit('cursormove'); } public onOptionsChanged(terminal: ITerminal): void { @@ -107,7 +122,11 @@ export class CursorRenderLayer extends BaseRenderLayer { this._ctx.fillStyle = this.colors.ansi[COLOR_CODES.WHITE]; this._renderBlurCursor(terminal, terminal.buffer.x, viewportRelativeCursorY, charData); this._ctx.restore(); - this._state = [terminal.buffer.x, viewportRelativeCursorY, false, terminal.options.cursorStyle]; + this._state.x = terminal.buffer.x; + this._state.y = viewportRelativeCursorY; + this._state.isFocused = false; + this._state.style = terminal.options.cursorStyle; + this._state.width = charData[CHAR_DATA_WIDTH_INDEX]; return; } @@ -119,10 +138,11 @@ export class CursorRenderLayer extends BaseRenderLayer { if (this._state) { // The cursor is already in the correct spot, don't redraw - if (this._state[0] === terminal.buffer.x && - this._state[1] === viewportRelativeCursorY && - this._state[2] === terminal.isFocused && - this._state[3] === terminal.options.cursorStyle) { + if (this._state.x === terminal.buffer.x && + this._state.y === viewportRelativeCursorY && + this._state.isFocused === terminal.isFocused && + this._state.style === terminal.options.cursorStyle && + this._state.width === charData[CHAR_DATA_WIDTH_INDEX]) { return; } this._clearCursor(); @@ -132,13 +152,24 @@ export class CursorRenderLayer extends BaseRenderLayer { this._ctx.fillStyle = this.colors.ansi[COLOR_CODES.WHITE]; this._cursorRenderers[terminal.options.cursorStyle || 'block'](terminal, terminal.buffer.x, viewportRelativeCursorY, charData); this._ctx.restore(); - this._state = [terminal.buffer.x, viewportRelativeCursorY, true, terminal.options.cursorStyle]; + + this._state.x = terminal.buffer.x; + this._state.y = viewportRelativeCursorY; + this._state.isFocused = false; + this._state.style = terminal.options.cursorStyle; + this._state.width = charData[CHAR_DATA_WIDTH_INDEX]; } private _clearCursor(): void { if (this._state) { - this.clearCells(this._state[0], this._state[1], 1, 1); - this._state = null; + this.clearCells(this._state.x, this._state.y, this._state.width, 1); + this._state = { + x: null, + y: null, + isFocused: null, + style: null, + width: null, + }; } } @@ -152,9 +183,9 @@ export class CursorRenderLayer extends BaseRenderLayer { private _renderBlockCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void { this._ctx.save(); this._ctx.fillStyle = this.colors.cursor; - this.fillCells(x, y, 1, 1); + this.fillCells(x, y, charData[CHAR_DATA_WIDTH_INDEX], 1); this._ctx.restore(); - this.drawCharTrueColor(terminal, charData[CHAR_DATA_CHAR_INDEX], charData[CHAR_DATA_CODE_INDEX], x, y, this.colors.background); + this.drawCharTrueColor(terminal, charData, x, y, this.colors.background); } private _renderUnderlineCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void { @@ -166,7 +197,7 @@ export class CursorRenderLayer extends BaseRenderLayer { private _renderBlurCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void { this._ctx.save(); - this.drawSquareAtCell(x, y, this.colors.cursor); + this.drawRectAtCell(x, y, charData[CHAR_DATA_WIDTH_INDEX], 1, this.colors.cursor); this._ctx.restore(); } } @@ -295,8 +326,9 @@ class CursorBlinkStateManager { } } - public resume(): void { + public resume(terminal: ITerminal): void { this._animationTimeRestarted = null; this._restartInterval(); + this.restartBlinkAnimation(terminal); } } diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index f4bc1e7778..182c9eadff 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -100,7 +100,9 @@ export class ForegroundRenderLayer extends BaseRenderLayer { this.drawBottomLineAtCell(x, y); } - this.drawChar(terminal, char, code, x, y, fg); + const width: number = charData[CHAR_DATA_WIDTH_INDEX]; + + this.drawChar(terminal, char, code, width, x, y, fg); this._ctx.restore(); } From b8732a25a3a9b3560454ded301bdd2f92009fea5 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 2 Sep 2017 10:37:51 -0700 Subject: [PATCH 056/108] Support Safari Safari lacks support for window.createImageBitmap, fallback to HTMLCanvasElement in thi case. --- src/renderer/BaseRenderLayer.ts | 15 ++++++++++++--- src/renderer/CharAtlas.ts | 18 +++++++++++++++--- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 2650af3acc..20768c8ad2 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -14,7 +14,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { // TODO: This should be shared between terminals, but not for static as some // terminals may have different styles - private _charAtlas: ImageBitmap; + private _charAtlas: HTMLCanvasElement | ImageBitmap; constructor( container: HTMLElement, @@ -39,8 +39,17 @@ export abstract class BaseRenderLayer implements IRenderLayer { public onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number]): void {} public onThemeChanged(terminal: ITerminal, colorSet: IColorSet): void { + this._refreshCharAtlas(terminal, colorSet); + } + + private _refreshCharAtlas(terminal: ITerminal, colorSet: IColorSet): void { this._charAtlas = null; - acquireCharAtlas(terminal, this.colors).then(bitmap => this._charAtlas = bitmap); + const result = acquireCharAtlas(terminal, this.colors); + if (result instanceof HTMLCanvasElement) { + this._charAtlas = result; + } else { + result.then(bitmap => this._charAtlas = bitmap); + } } public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void { @@ -52,7 +61,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._canvas.style.height = `${canvasHeight}px`; if (charSizeChanged) { - acquireCharAtlas(terminal, this.colors).then(bitmap => this._charAtlas = bitmap); + this._refreshCharAtlas(terminal, this.colors); } } diff --git a/src/renderer/CharAtlas.ts b/src/renderer/CharAtlas.ts index a9fe8bd301..d25520d115 100644 --- a/src/renderer/CharAtlas.ts +++ b/src/renderer/CharAtlas.ts @@ -12,14 +12,14 @@ interface ICharAtlasConfig { } interface ICharAtlasCacheEntry { - bitmap: Promise; + bitmap: HTMLCanvasElement | Promise; config: ICharAtlasConfig; ownedBy: ITerminal[]; } let charAtlasCache: ICharAtlasCacheEntry[] = []; -export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet): Promise { +export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet): HTMLCanvasElement | Promise { const scaledCharWidth = terminal.charMeasure.width * window.devicePixelRatio; const scaledCharHeight = terminal.charMeasure.height * window.devicePixelRatio; const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, terminal, colors); @@ -101,7 +101,7 @@ class CharAtlasGenerator { this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); } - public generate(scaledCharWidth: number, scaledCharHeight: number, fontSize: number, fontFamily: string, foreground: string, ansiColors: string[]): Promise { + public generate(scaledCharWidth: number, scaledCharHeight: number, fontSize: number, fontFamily: string, foreground: string, ansiColors: string[]): HTMLCanvasElement | Promise { const cellWidth = scaledCharWidth + CHAR_ATLAS_CELL_SPACING; const cellHeight = scaledCharHeight + CHAR_ATLAS_CELL_SPACING; this._canvas.width = 255 * cellWidth; @@ -133,6 +133,18 @@ class CharAtlasGenerator { this._ctx.restore(); const charAtlasImageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); + + // Support is patchy for createImageBitmap at the moment, pass a canvas back + // if support is lacking as drawImage works there too. + if (!('createImageBitmap' in window)) { + // Regenerate canvas and context as they are now owned by the char atlas + const result = this._canvas; + this._canvas = document.createElement('canvas'); + this._ctx = this._canvas.getContext('2d'); + this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); + return result; + } + const promise = window.createImageBitmap(charAtlasImageData); // Clear the rect while the promise is in progress this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); From ee4b71c8e76a0eb09bb8c4480bfe7ab8a310ccf7 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 2 Sep 2017 10:51:08 -0700 Subject: [PATCH 057/108] Remove Terminal.options.colors It wasn't being used --- src/Interfaces.ts | 1 - src/Terminal.ts | 14 -------------- 2 files changed, 15 deletions(-) diff --git a/src/Interfaces.ts b/src/Interfaces.ts index b6ba08db1c..0b50161cd0 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -117,7 +117,6 @@ export interface ITerminalOptions { bellSound?: string; bellStyle?: string; cancelEvents?: boolean; - colors?: string[]; cols?: number; convertEol?: boolean; cursorBlink?: boolean; diff --git a/src/Terminal.ts b/src/Terminal.ts index 7d2e8fd9c0..c572fefb6e 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -136,7 +136,6 @@ const vcolors: number[][] = (function(): number[][] { })(); const DEFAULT_OPTIONS: ITerminalOptions = { - colors: defaultColors, convertEol: false, termName: 'xterm', geometry: [80, 24], @@ -300,19 +299,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this[key] = this.options[key]; }); - if (this.options.colors.length === 8) { - this.options.colors = this.options.colors.concat(_colors.slice(8)); - } else if (this.options.colors.length === 16) { - this.options.colors = this.options.colors.concat(_colors.slice(16)); - } else if (this.options.colors.length === 10) { - this.options.colors = this.options.colors.slice(0, -2).concat( - _colors.slice(8, -2), this.options.colors.slice(-2)); - } else if (this.options.colors.length === 18) { - this.options.colors = this.options.colors.concat( - _colors.slice(16, -2), this.options.colors.slice(-2)); - } - this.colors = this.options.colors; - // this.context = options.context || window; // this.document = options.document || document; // TODO: WHy not document.body? From 9cabb93b8b11e41f6eeac45943b597cbd0d0f691 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 2 Sep 2017 11:08:43 -0700 Subject: [PATCH 058/108] Organize temporary color handling code --- src/Terminal.ts | 202 +++++++++++--------------- src/renderer/BaseRenderLayer.ts | 5 +- src/renderer/ColorManager.ts | 2 +- src/renderer/ForegroundRenderLayer.ts | 1 - 4 files changed, 91 insertions(+), 119 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index c572fefb6e..59d39e5af3 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -41,6 +41,7 @@ import { getRawByteCoords } from './utils/Mouse'; import { CustomKeyEventHandler, Charset, LinkMatcherHandler, LinkMatcherValidationCallback, CharData, LineData } from './Types'; import { ITerminal, IBrowser, ITerminalOptions, IInputHandlingTerminal, ILinkMatcherOptions, IViewport, ICompositionHelper, ITheme } from './Interfaces'; import { BellSound } from './utils/Sounds'; +import { DEFAULT_ANSI_COLORS } from './renderer/ColorManager'; // Declare for RequireJS in loadAddon declare var define: any; @@ -61,80 +62,6 @@ const WRITE_BUFFER_PAUSE_THRESHOLD = 5; */ const WRITE_BATCH_SIZE = 300; -// TODO: Most of the color code should be removed after truecolor is implemented -// Colors 0-15 -const tangoColors: string[] = [ - // dark: - '#2e3436', - '#cc0000', - '#4e9a06', - '#c4a000', - '#3465a4', - '#75507b', - '#06989a', - '#d3d7cf', - // bright: - '#555753', - '#ef2929', - '#8ae234', - '#fce94f', - '#729fcf', - '#ad7fa8', - '#34e2e2', - '#eeeeec' -]; - -// Colors 0-15 + 16-255 -// Much thanks to TooTallNate for writing this. -const defaultColors: string[] = (function(): string[] { - let colors = tangoColors.slice(); - let r = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]; - let i; - - // 16-231 - i = 0; - for (; i < 216; i++) { - out(r[(i / 36) % 6 | 0], r[(i / 6) % 6 | 0], r[i % 6]); - } - - // 232-255 (grey) - i = 0; - let c: number; - for (; i < 24; i++) { - c = 8 + i * 10; - out(c, c, c); - } - - function out(r: number, g: number, b: number): void { - colors.push('#' + hex(r) + hex(g) + hex(b)); - } - - function hex(c: number): string { - let s = c.toString(16); - return s.length < 2 ? '0' + s : s; - } - - return colors; -})(); - -const _colors: string[] = defaultColors.slice(); - -const vcolors: number[][] = (function(): number[][] { - const out: number[][] = []; - let color; - - for (let i = 0; i < 256; i++) { - color = parseInt(defaultColors[i].substring(1), 16); - out.push([ - (color >> 16) & 0xff, - (color >> 8) & 0xff, - color & 0xff - ]); - } - - return out; -})(); - const DEFAULT_OPTIONS: ITerminalOptions = { convertEol: false, termName: 'xterm', @@ -2139,44 +2066,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT return false; } - // Expose to InputHandler - // TODO: Revise when truecolor is introduced. + // TODO: Remove when true color is implemented public matchColor(r1: number, g1: number, b1: number): number { - const hash = (r1 << 16) | (g1 << 8) | b1; - - if (matchColorCache[hash] != null) { - return matchColorCache[hash]; - } - - let ldiff = Infinity; - let li = -1; - let i = 0; - let c: number[]; - let r2: number; - let g2: number; - let b2: number; - let diff: number; - - for (; i < vcolors.length; i++) { - c = vcolors[i]; - r2 = c[0]; - g2 = c[1]; - b2 = c[2]; - - diff = matchColorDistance(r1, g1, b1, r2, g2, b2); - - if (diff === 0) { - li = i; - break; - } - - if (diff < ldiff) { - ldiff = diff; - li = i; - } - } - - return matchColorCache[hash] = li; + return matchColor_(r1, g1, b1); } private visualBell(): boolean { @@ -2235,17 +2127,95 @@ function isThirdLevelShift(browser: IBrowser, ev: KeyboardEvent): boolean { return thirdLevelKey && (!ev.keyCode || ev.keyCode > 47); } +function wasMondifierKeyOnlyEvent(ev: KeyboardEvent): boolean { + return ev.keyCode === 16 || // Shift + ev.keyCode === 17 || // Ctrl + ev.keyCode === 18; // Alt +} + +/** + * TODO: + * The below color-related code can be removed when true color is implemented. + * It's only purpose is to match true color requests with the closest matching + * ANSI color code. + */ + +// Colors 0-15 + 16-255 +// Much thanks to TooTallNate for writing this. +const vcolors: number[][] = (function(): number[][] { + const result = DEFAULT_ANSI_COLORS.map(c => { + c = c.substring(1); + return [ + parseInt(c.substring(0, 2), 16), + parseInt(c.substring(2, 4), 16), + parseInt(c.substring(4, 6), 16) + ]; + }); + const r = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]; + + // 16-231 + for (let i = 0; i < 216; i++) { + result.push([ + r[(i / 36) % 6 | 0], + r[(i / 6) % 6 | 0], + r[i % 6] + ]); + } + + // 232-255 (grey) + let c: number; + for (let i = 0; i < 24; i++) { + c = 8 + i * 10; + result.push([c, c, c]); + } + + return result; +})(); + const matchColorCache: {[colorRGBHash: number]: number} = {}; // http://stackoverflow.com/questions/1633828 -const matchColorDistance = function(r1: number, g1: number, b1: number, r2: number, g2: number, b2: number): number { +function matchColorDistance(r1: number, g1: number, b1: number, r2: number, g2: number, b2: number): number { return Math.pow(30 * (r1 - r2), 2) + Math.pow(59 * (g1 - g2), 2) + Math.pow(11 * (b1 - b2), 2); }; -function wasMondifierKeyOnlyEvent(ev: KeyboardEvent): boolean { - return ev.keyCode === 16 || // Shift - ev.keyCode === 17 || // Ctrl - ev.keyCode === 18; // Alt + +function matchColor_(r1: number, g1: number, b1: number): number { + const hash = (r1 << 16) | (g1 << 8) | b1; + + if (matchColorCache[hash] != null) { + return matchColorCache[hash]; + } + + let ldiff = Infinity; + let li = -1; + let i = 0; + let c: number[]; + let r2: number; + let g2: number; + let b2: number; + let diff: number; + + for (; i < vcolors.length; i++) { + c = vcolors[i]; + r2 = c[0]; + g2 = c[1]; + b2 = c[2]; + + diff = matchColorDistance(r1, g1, b1, r2, g2, b2); + + if (diff === 0) { + li = i; + break; + } + + if (diff < ldiff) { + ldiff = diff; + li = i; + } + } + + return matchColorCache[hash] = li; } diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 20768c8ad2..5742712224 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -128,7 +128,10 @@ export abstract class BaseRenderLayer implements IRenderLayer { if (fg < 256) { colorIndex = fg + 1; } - if (code < 256 && (colorIndex > 0 || fg >= 256)) { + const isAscii = code < 256; + const isBasicColor = (colorIndex > 0 && fg < 16); + const isDefaultColor = fg >= 256; + if (isAscii && (isBasicColor || isDefaultColor)) { // ImageBitmap's draw about twice as fast as from a canvas const charAtlasCellWidth = this.scaledCharWidth + CHAR_ATLAS_CELL_SPACING; const charAtlasCellHeight = this.scaledCharHeight + CHAR_ATLAS_CELL_SPACING; diff --git a/src/renderer/ColorManager.ts b/src/renderer/ColorManager.ts index 341e4ca588..6b7f88c1f1 100644 --- a/src/renderer/ColorManager.ts +++ b/src/renderer/ColorManager.ts @@ -23,7 +23,7 @@ export enum COLOR_CODES { BRIGHT_WHITE = 15 } -const DEFAULT_ANSI_COLORS = [ +export const DEFAULT_ANSI_COLORS = [ // dark: '#2e3436', '#cc0000', diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index 182c9eadff..1f579f91af 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -101,7 +101,6 @@ export class ForegroundRenderLayer extends BaseRenderLayer { } const width: number = charData[CHAR_DATA_WIDTH_INDEX]; - this.drawChar(terminal, char, code, width, x, y, fg); this._ctx.restore(); From c92a2f964a403d35e28229f0bacc2019f9cb2510 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 2 Sep 2017 12:27:09 -0700 Subject: [PATCH 059/108] Support lineHeight --- src/Interfaces.ts | 1 + src/SelectionManager.ts | 4 ++-- src/Terminal.ts | 28 ++++++++++++++++++++-------- src/Viewport.ts | 15 ++++++++------- src/addons/fit/fit.js | 4 +++- src/renderer/BaseRenderLayer.ts | 26 +++++++++++++++----------- src/renderer/Renderer.ts | 2 +- src/utils/Mouse.ts | 8 ++++---- typings/xterm.d.ts | 4 ++-- 9 files changed, 56 insertions(+), 36 deletions(-) diff --git a/src/Interfaces.ts b/src/Interfaces.ts index 0b50161cd0..8af0793482 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -127,6 +127,7 @@ export interface ITerminalOptions { fontFamily?: string; geometry?: [number, number]; handler?: (data: string) => void; + lineHeight?: number; rows?: number; screenKeys?: boolean; scrollback?: number; diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index dc58858ecb..1425aedc10 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -273,7 +273,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager * @param event The mouse event. */ private _getMouseBufferCoords(event: MouseEvent): [number, number] { - const coords = Mouse.getCoords(event, this._rowContainer, this._charMeasure, this._terminal.cols, this._terminal.rows, true); + const coords = Mouse.getCoords(event, this._rowContainer, this._charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows, true); if (!coords) { return null; } @@ -293,7 +293,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager */ private _getMouseEventScrollAmount(event: MouseEvent): number { let offset = Mouse.getCoordsRelativeToElement(event, this._rowContainer)[1]; - const terminalHeight = this._terminal.rows * this._charMeasure.height; + const terminalHeight = this._terminal.rows * Math.ceil(this._charMeasure.height * this._terminal.options.lineHeight); if (offset >= 0 && offset <= terminalHeight) { return 0; } diff --git a/src/Terminal.ts b/src/Terminal.ts index 59d39e5af3..9dba67017d 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -72,6 +72,7 @@ const DEFAULT_OPTIONS: ITerminalOptions = { bellStyle: 'none', fontFamily: 'courier-new, courier, monospace', fontSize: 15, + lineHeight: 1.0, scrollback: 1000, screenKeys: false, debug: false, @@ -356,19 +357,24 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT } break; case 'cursorStyle': - if (!value) { - value = 'block'; - } - break; + if (!value) { + value = 'block'; + } + break; + case 'lineHeight': + if (value < 1) { + console.warn(`${key} cannot be less than 1, value: ${value}`); + return; + } case 'tabStopWidth': if (value < 1) { - console.warn(`tabStopWidth cannot be less than 1, value: ${value}`); + console.warn(`${key} cannot be less than 1, value: ${value}`); return; } break; case 'scrollback': if (value < 0) { - console.warn(`scrollback cannot be less than 0, value: ${value}`); + console.warn(`${key} cannot be less than 0, value: ${value}`); return; } if (this.options[key] !== value) { @@ -395,6 +401,12 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.renderer.clear(); this.charMeasure.measure(this.options); break; + case 'lineHeight': + // When the font changes the size of the cells may change which requires a renderer clear + this.renderer.clear(); + this.renderer.onResize(this.cols, this.rows); + this.refresh(0, this.rows - 1); + // this.charMeasure.measure(this.options); case 'scrollback': this.buffers.resize(this.cols, this.rows); this.viewport.syncScrollArea(); @@ -709,7 +721,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT button = getButton(ev); // get mouse coordinates - pos = getRawByteCoords(ev, self.rowContainer, self.charMeasure, self.cols, self.rows); + pos = getRawByteCoords(ev, self.rowContainer, self.charMeasure, self.options.lineHeight, self.cols, self.rows); if (!pos) return; sendEvent(button, pos); @@ -735,7 +747,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7< function sendMove(ev: MouseEvent): void { let button = pressed; - let pos = getRawByteCoords(ev, self.rowContainer, self.charMeasure, self.cols, self.rows); + let pos = getRawByteCoords(ev, self.rowContainer, self.charMeasure, self.options.lineHeight, self.cols, self.rows); if (!pos) return; // buttons marked as motions diff --git a/src/Viewport.ts b/src/Viewport.ts index 4ad4ddec47..b061eeb6cf 100644 --- a/src/Viewport.ts +++ b/src/Viewport.ts @@ -46,19 +46,20 @@ export class Viewport implements IViewport { */ private refresh(): void { if (this.charMeasure.height > 0) { - const rowHeightChanged = this.charMeasure.height !== this.currentRowHeight; + const lineHeight = Math.ceil(this.charMeasure.height * this.terminal.options.lineHeight); + const rowHeightChanged = lineHeight !== this.currentRowHeight; if (rowHeightChanged) { - this.currentRowHeight = this.charMeasure.height; - this.viewportElement.style.lineHeight = this.charMeasure.height + 'px'; - this.terminal.rowContainer.style.lineHeight = this.charMeasure.height + 'px'; + this.currentRowHeight = lineHeight; + this.viewportElement.style.lineHeight = lineHeight + 'px'; + this.terminal.rowContainer.style.lineHeight = lineHeight + 'px'; } const viewportHeightChanged = this.lastRecordedViewportHeight !== this.terminal.rows; if (rowHeightChanged || viewportHeightChanged) { this.lastRecordedViewportHeight = this.terminal.rows; - this.viewportElement.style.height = this.charMeasure.height * this.terminal.rows + 'px'; + this.viewportElement.style.height = lineHeight * this.terminal.rows + 'px'; this.terminal.selectionContainer.style.height = this.viewportElement.style.height; } - this.scrollArea.style.height = (this.charMeasure.height * this.lastRecordedBufferLength) + 'px'; + this.scrollArea.style.height = (lineHeight * this.lastRecordedBufferLength) + 'px'; } } @@ -75,7 +76,7 @@ export class Viewport implements IViewport { this.refresh(); } else { // If size has changed, refresh viewport - if (this.charMeasure.height !== this.currentRowHeight) { + if (Math.ceil(this.charMeasure.height * this.terminal.options.lineHeight) !== this.currentRowHeight) { this.refresh(); } } diff --git a/src/addons/fit/fit.js b/src/addons/fit/fit.js index 1e46932cdd..b774cebfde 100644 --- a/src/addons/fit/fit.js +++ b/src/addons/fit/fit.js @@ -49,7 +49,7 @@ var geometry = { cols: parseInt(availableWidth / term.charMeasure.width, 10), - rows: parseInt(availableHeight / term.charMeasure.height, 10) + rows: parseInt(availableHeight / (term.charMeasure.height * term.getOption('lineHeight')), 10) }; return geometry; @@ -62,6 +62,8 @@ var geometry = exports.proposeGeometry(term); if (geometry) { + // Force a full render + term.renderer.clear(); term.resize(geometry.cols, geometry.rows); } }, 0); diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 5742712224..912d351f9e 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -11,6 +11,8 @@ export abstract class BaseRenderLayer implements IRenderLayer { protected _ctx: CanvasRenderingContext2D; private scaledCharWidth: number; private scaledCharHeight: number; + private scaledLineHeight: number; + private scaledLineDrawY: number; // TODO: This should be shared between terminals, but not for static as some // terminals may have different styles @@ -55,6 +57,8 @@ export abstract class BaseRenderLayer implements IRenderLayer { public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void { this.scaledCharWidth = terminal.charMeasure.width * window.devicePixelRatio; this.scaledCharHeight = terminal.charMeasure.height * window.devicePixelRatio; + this.scaledLineHeight = Math.ceil(this.scaledCharHeight * terminal.options.lineHeight); + this.scaledLineDrawY = terminal.options.lineHeight === 1 ? 0 : Math.round((this.scaledLineHeight - this.scaledCharHeight) / 2); this._canvas.width = canvasWidth * window.devicePixelRatio; this._canvas.height = canvasHeight * window.devicePixelRatio; this._canvas.style.width = `${canvasWidth}px`; @@ -68,13 +72,13 @@ export abstract class BaseRenderLayer implements IRenderLayer { public abstract reset(terminal: ITerminal): void; protected fillCells(startCol: number, startRow: number, colWidth: number, colHeight: number): void { - this._ctx.fillRect(startCol * this.scaledCharWidth, startRow * this.scaledCharHeight, colWidth * this.scaledCharWidth, colHeight * this.scaledCharHeight); + this._ctx.fillRect(startCol * this.scaledCharWidth, startRow * this.scaledLineHeight, colWidth * this.scaledCharWidth, colHeight * this.scaledLineHeight); } protected drawBottomLineAtCell(x: number, y: number): void { this._ctx.fillRect( x * this.scaledCharWidth, - (y + 1) * this.scaledCharHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */, + (y + 1) * this.scaledLineHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */, this.scaledCharWidth, window.devicePixelRatio); } @@ -82,9 +86,9 @@ export abstract class BaseRenderLayer implements IRenderLayer { protected drawLeftLineAtCell(x: number, y: number): void { this._ctx.fillRect( x * this.scaledCharWidth, - y * this.scaledCharHeight, + y * this.scaledLineHeight, window.devicePixelRatio, - this.scaledCharHeight); + this.scaledLineHeight); } protected drawRectAtCell(x: number, y: number, width: number, height: number, color: string): void { @@ -92,9 +96,9 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.lineWidth = window.devicePixelRatio; this._ctx.strokeRect( x * this.scaledCharWidth + window.devicePixelRatio / 2, - y * this.scaledCharHeight + (window.devicePixelRatio / 2), + y * this.scaledLineHeight + (window.devicePixelRatio / 2), (width * this.scaledCharWidth) - window.devicePixelRatio, - (height * this.scaledCharHeight) - window.devicePixelRatio); + (height * this.scaledLineHeight) - window.devicePixelRatio); } protected clearAll(): void { @@ -102,7 +106,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { } protected clearCells(startCol: number, startRow: number, colWidth: number, colHeight: number): void { - this._ctx.clearRect(startCol * this.scaledCharWidth, startRow * this.scaledCharHeight, colWidth * this.scaledCharWidth, colHeight * this.scaledCharHeight); + this._ctx.clearRect(startCol * this.scaledCharWidth, startRow * this.scaledLineHeight, colWidth * this.scaledCharWidth, colHeight * this.scaledLineHeight); } protected drawCharTrueColor(terminal: ITerminal, charData: CharData, x: number, y: number, color: string): void { @@ -116,7 +120,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { // can bleed into other cells. This code will clip the following fillText, // ensuring that its contents don't go beyond the cell bounds. this._ctx.beginPath(); - this._ctx.rect(x * this.scaledCharWidth, y * this.scaledCharHeight, charData[CHAR_DATA_WIDTH_INDEX] * this.scaledCharWidth, this.scaledCharHeight); + this._ctx.rect(x * this.scaledCharWidth, y * this.scaledLineHeight + this.scaledLineDrawY, charData[CHAR_DATA_WIDTH_INDEX] * this.scaledCharWidth, this.scaledCharHeight); this._ctx.clip(); this._ctx.fillText(charData[CHAR_DATA_CHAR_INDEX], x * this.scaledCharWidth, y * this.scaledCharHeight); @@ -137,7 +141,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { const charAtlasCellHeight = this.scaledCharHeight + CHAR_ATLAS_CELL_SPACING; this._ctx.drawImage(this._charAtlas, code * charAtlasCellWidth, colorIndex * charAtlasCellHeight, this.scaledCharWidth, this.scaledCharHeight, - x * this.scaledCharWidth, y * this.scaledCharHeight, this.scaledCharWidth, this.scaledCharHeight); + x * this.scaledCharWidth, y * this.scaledLineHeight + this.scaledLineDrawY, this.scaledCharWidth, this.scaledCharHeight); } else { this._drawUncachedChar(terminal, char, width, fg, x, y); } @@ -164,11 +168,11 @@ export abstract class BaseRenderLayer implements IRenderLayer { // can bleed into other cells. This code will clip the following fillText, // ensuring that its contents don't go beyond the cell bounds. this._ctx.beginPath(); - this._ctx.rect(x * this.scaledCharWidth, y * this.scaledCharHeight, width * this.scaledCharWidth, this.scaledCharHeight); + this._ctx.rect(x * this.scaledCharWidth, y * this.scaledLineHeight + this.scaledLineDrawY, width * this.scaledCharWidth, this.scaledCharHeight); this._ctx.clip(); // Draw the character - this._ctx.fillText(char, x * this.scaledCharWidth, y * this.scaledCharHeight); + this._ctx.fillText(char, x * this.scaledCharWidth, y * this.scaledLineHeight + this.scaledLineDrawY); this._ctx.restore(); } } diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index cc85c7e76c..b3df2b7a02 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -46,7 +46,7 @@ export class Renderer { public onResize(cols: number, rows: number): void { const width = this._terminal.charMeasure.width * this._terminal.cols; - const height = this._terminal.charMeasure.height * this._terminal.rows; + const height = Math.ceil(this._terminal.charMeasure.height * this._terminal.options.lineHeight) * this._terminal.rows; this._renderLayers.forEach(l => l.resize(this._terminal, width, height, false)); } diff --git a/src/utils/Mouse.ts b/src/utils/Mouse.ts index c61624e9ff..f25a6b2e9b 100644 --- a/src/utils/Mouse.ts +++ b/src/utils/Mouse.ts @@ -36,7 +36,7 @@ export function getCoordsRelativeToElement(event: MouseEvent, element: HTMLEleme * apply an offset to the x value such that the left half of the cell will * select that cell and the right half will select the next cell. */ -export function getCoords(event: MouseEvent, rowContainer: HTMLElement, charMeasure: CharMeasure, colCount: number, rowCount: number, isSelection?: boolean): [number, number] { +export function getCoords(event: MouseEvent, rowContainer: HTMLElement, charMeasure: CharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number] { // Coordinates cannot be measured if charMeasure has not been initialized if (!charMeasure.width || !charMeasure.height) { return null; @@ -49,7 +49,7 @@ export function getCoords(event: MouseEvent, rowContainer: HTMLElement, charMeas // Convert to cols/rows. coords[0] = Math.ceil((coords[0] + (isSelection ? charMeasure.width / 2 : 0)) / charMeasure.width); - coords[1] = Math.ceil(coords[1] / charMeasure.height); + coords[1] = Math.ceil(coords[1] / Math.ceil(charMeasure.height * lineHeight)); // Ensure coordinates are within the terminal viewport. coords[0] = Math.min(Math.max(coords[0], 1), colCount + 1); @@ -68,8 +68,8 @@ export function getCoords(event: MouseEvent, rowContainer: HTMLElement, charMeas * @param colCount The number of columns in the terminal. * @param rowCount The number of rows in the terminal. */ -export function getRawByteCoords(event: MouseEvent, rowContainer: HTMLElement, charMeasure: CharMeasure, colCount: number, rowCount: number): { x: number, y: number } { - const coords = getCoords(event, rowContainer, charMeasure, colCount, rowCount); +export function getRawByteCoords(event: MouseEvent, rowContainer: HTMLElement, charMeasure: CharMeasure, lineHeight: number, colCount: number, rowCount: number): { x: number, y: number } { + const coords = getCoords(event, rowContainer, charMeasure, lineHeight, colCount, rowCount); let x = coords[0]; let y = coords[1]; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index dce15bca4a..2956c93786 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -328,7 +328,7 @@ declare module 'xterm' { * Retrieves an option's value from the terminal. * @param key The option key. */ - getOption(key: 'cols' | 'fontSize' | 'rows' | 'tabStopWidth' | 'scrollback'): number; + getOption(key: 'cols' | 'fontSize' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback'): number; /** * Retrieves an option's value from the terminal. * @param key The option key. @@ -380,7 +380,7 @@ declare module 'xterm' { * @param key The option key. * @param value The option value. */ - setOption(key: 'cols' | 'fontSize' | 'rows' | 'tabStopWidth' | 'scrollback', value: number): void; + setOption(key: 'cols' | 'fontSize' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback', value: number): void; /** * Sets an option on the terminal. * @param key The option key. From 66d41feab610cfcfb72b18bd6c52443736d61419 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 2 Sep 2017 12:31:26 -0700 Subject: [PATCH 060/108] Expose setTheme through .d.ts --- typings/xterm.d.ts | 50 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 2956c93786..72206c824e 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -58,6 +58,50 @@ interface ITerminalOptions { tabStopWidth?: number; } +/** + * Contains colors to theme the terminal with. + */ +interface ITheme { + /** The default foreground color */ + foreground?: string, + /** The default background color */ + background?: string, + /** The cursor color */ + cursor?: string, + /** ANSI black (eg. `\x1b[30m`) */ + black?: string, + /** ANSI red (eg. `\x1b[31m`) */ + red?: string, + /** ANSI green (eg. `\x1b[32m`) */ + green?: string, + /** ANSI yellow (eg. `\x1b[33m`) */ + yellow?: string, + /** ANSI blue (eg. `\x1b[34m`) */ + blue?: string, + /** ANSI magenta (eg. `\x1b[35m`) */ + magenta?: string, + /** ANSI cyan (eg. `\x1b[36m`) */ + cyan?: string, + /** ANSI white (eg. `\x1b[37m`) */ + white?: string, + /** ANSI bright black (eg. `\x1b[1;30m`) */ + brightBlack?: string, + /** ANSI bright red (eg. `\x1b[1;31m`) */ + brightRed?: string, + /** ANSI bright green (eg. `\x1b[1;32m`) */ + brightGreen?: string, + /** ANSI bright yellow (eg. `\x1b[1;33m`) */ + brightYellow?: string, + /** ANSI bright blue (eg. `\x1b[1;34m`) */ + brightBlue?: string, + /** ANSI bright magenta (eg. `\x1b[1;35m`) */ + brightMagenta?: string, + /** ANSI bright cyan (eg. `\x1b[1;36m`) */ + brightCyan?: string, + /** ANSI bright white (eg. `\x1b[1;37m`) */ + brightWhite?: string +} + /** * An object containing options for a link matcher. */ @@ -400,6 +444,12 @@ declare module 'xterm' { */ setOption(key: string, value: any): void; + /** + * Sets the theme of the terminal. + * @param theme The theme to use. + */ + setTheme(theme: ITheme): void; + /** * Tells the renderer to refresh terminal content between two rows * (inclusive) at the next opportunity. From 24a98f2fa860d19b5a91d8cff26b1189c2a876dc Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 2 Sep 2017 13:26:05 -0700 Subject: [PATCH 061/108] Remove rowContainer and children, disable Linkifier --- src/Interfaces.ts | 2 - src/SelectionManager.test.ts | 6 +- src/SelectionManager.ts | 13 ++-- src/Terminal.ts | 113 +++++++++++------------------------ src/Viewport.test.ts | 12 ---- src/Viewport.ts | 2 +- src/addons/fit/fit.js | 20 +++---- src/utils/Mouse.ts | 12 ++-- 8 files changed, 57 insertions(+), 123 deletions(-) diff --git a/src/Interfaces.ts b/src/Interfaces.ts index 8af0793482..41fc8bdad9 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -19,7 +19,6 @@ export interface IBrowser { export interface ITerminal extends IEventEmitter { element: HTMLElement; - rowContainer: HTMLElement; selectionContainer: HTMLElement; selectionManager: ISelectionManager; charMeasure: ICharMeasure; @@ -28,7 +27,6 @@ export interface ITerminal extends IEventEmitter { cols: number; browser: IBrowser; writeBuffer: string[]; - children: HTMLElement[]; cursorHidden: boolean; cursorState: number; defAttr: number; diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index 4774b3bb9d..b5a9d5529c 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -17,10 +17,9 @@ class TestSelectionManager extends SelectionManager { constructor( terminal: ITerminal, buffer: IBuffer, - rowContainer: HTMLElement, charMeasure: CharMeasure ) { - super(terminal, buffer, rowContainer, charMeasure); + super(terminal, buffer, charMeasure); } public get model(): SelectionModel { return this._model; } @@ -48,7 +47,6 @@ describe('SelectionManager', () => { dom = new jsdom.JSDOM(''); window = dom.window; document = window.document; - rowContainer = document.createElement('div'); terminal = new MockTerminal(); terminal.cols = 80; terminal.rows = 2; @@ -56,7 +54,7 @@ describe('SelectionManager', () => { terminal.buffers = new BufferSet(terminal); terminal.buffer = terminal.buffers.active; buffer = terminal.buffer; - selectionManager = new TestSelectionManager(terminal, buffer, rowContainer, null); + selectionManager = new TestSelectionManager(terminal, buffer, null); }); function stringToRow(text: string): LineData { diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 1425aedc10..e84bde6869 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -98,7 +98,6 @@ export class SelectionManager extends EventEmitter implements ISelectionManager constructor( private _terminal: ITerminal, private _buffer: IBuffer, - private _rowContainer: HTMLElement, private _charMeasure: CharMeasure ) { super(); @@ -273,7 +272,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager * @param event The mouse event. */ private _getMouseBufferCoords(event: MouseEvent): [number, number] { - const coords = Mouse.getCoords(event, this._rowContainer, this._charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows, true); + const coords = Mouse.getCoords(event, this._terminal.element, this._charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows, true); if (!coords) { return null; } @@ -292,7 +291,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager * @param event The mouse event. */ private _getMouseEventScrollAmount(event: MouseEvent): number { - let offset = Mouse.getCoordsRelativeToElement(event, this._rowContainer)[1]; + let offset = Mouse.getCoordsRelativeToElement(event, this._terminal.element)[1]; const terminalHeight = this._terminal.rows * Math.ceil(this._charMeasure.height * this._terminal.options.lineHeight); if (offset >= 0 && offset <= terminalHeight) { return 0; @@ -361,8 +360,8 @@ export class SelectionManager extends EventEmitter implements ISelectionManager */ private _addMouseDownListeners(): void { // Listen on the document so that dragging outside of viewport works - this._rowContainer.ownerDocument.addEventListener('mousemove', this._mouseMoveListener); - this._rowContainer.ownerDocument.addEventListener('mouseup', this._mouseUpListener); + this._terminal.element.ownerDocument.addEventListener('mousemove', this._mouseMoveListener); + this._terminal.element.ownerDocument.addEventListener('mouseup', this._mouseUpListener); this._dragScrollIntervalTimer = setInterval(() => this._dragScroll(), DRAG_SCROLL_INTERVAL); } @@ -370,8 +369,8 @@ export class SelectionManager extends EventEmitter implements ISelectionManager * Removes the listeners that are registered when mousedown is triggered. */ private _removeMouseDownListeners(): void { - this._rowContainer.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener); - this._rowContainer.ownerDocument.removeEventListener('mouseup', this._mouseUpListener); + this._terminal.element.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener); + this._terminal.element.ownerDocument.removeEventListener('mouseup', this._mouseUpListener); clearInterval(this._dragScrollIntervalTimer); this._dragScrollIntervalTimer = null; } diff --git a/src/Terminal.ts b/src/Terminal.ts index 9dba67017d..fec7fdcaab 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -31,7 +31,7 @@ import { InputHandler } from './InputHandler'; import { Parser } from './Parser'; // import { Renderer } from './Renderer'; import { Renderer } from './renderer/Renderer'; -import { Linkifier } from './Linkifier'; +// import { Linkifier } from './Linkifier'; import { SelectionManager } from './SelectionManager'; import { CharMeasure } from './utils/CharMeasure'; import * as Browser from './utils/Browser'; @@ -87,7 +87,6 @@ const DEFAULT_OPTIONS: ITerminalOptions = { export class Terminal extends EventEmitter implements ITerminal, IInputHandlingTerminal { public textarea: HTMLTextAreaElement; public element: HTMLElement; - public rowContainer: HTMLElement; /** * The HTMLElement that the terminal is created in, set by Terminal.open. @@ -145,7 +144,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT public urxvtMouse: boolean; // misc - public children: HTMLElement[]; private refreshStart: number; private refreshEnd: number; public savedCols: number; @@ -187,7 +185,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT private parser: Parser; private renderer: Renderer; public selectionManager: SelectionManager; - private linkifier: Linkifier; + // private linkifier: Linkifier; public buffers: BufferSet; public buffer: Buffer; public viewport: IViewport; @@ -284,7 +282,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // Reuse renderer if the Terminal is being recreated via a reset call. this.renderer = this.renderer || null; this.selectionManager = this.selectionManager || null; - this.linkifier = this.linkifier || new Linkifier(); + // this.linkifier = this.linkifier || new Linkifier(); // Create the terminal's buffers and set the current buffer this.buffers = new BufferSet(this); @@ -539,22 +537,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.on('refresh', (data) => this.queueLinkification(data.start, data.end)); } - /** - * Insert the given row to the terminal or produce a new one - * if no row argument is passed. Return the inserted row. - * @param {HTMLElement} row (optional) The row to append to the terminal. - */ - private insertRow(row?: HTMLElement): HTMLElement { - if (typeof row !== 'object') { - row = document.createElement('div'); - } - - this.rowContainer.appendChild(row); - this.children.push(row); - - return row; - }; - /** * Opens the terminal within an element. * @@ -597,13 +579,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.selectionContainer.classList.add('xterm-selection'); this.element.appendChild(this.selectionContainer); - // Create the container that will hold the lines of the terminal and then - // produce the lines the lines. - this.rowContainer = document.createElement('div'); - this.rowContainer.classList.add('xterm-rows'); - this.element.appendChild(this.rowContainer); - this.children = []; - this.linkifier.attachToDom(document, this.children); + // TODO: Re-enable linkifier + // this.linkifier.attachToDom(document, this.children); // Create the container that will hold helpers like the textarea for // capturing DOM Events. Then produce the helpers. @@ -629,9 +606,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.charSizeStyleElement = document.createElement('style'); this.helperContainer.appendChild(this.charSizeStyleElement); - for (; i < this.rows; i++) { - this.insertRow(); - } this.parent.appendChild(this.element); this.charMeasure = new CharMeasure(document, this.helperContainer); @@ -648,7 +622,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.renderer.queueRefresh(0, this.rows - 1); }); - this.selectionManager = new SelectionManager(this, this.buffer, this.rowContainer, this.charMeasure); + this.selectionManager = new SelectionManager(this, this.buffer, this.charMeasure); this.element.addEventListener('mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e)); this.selectionManager.on('refresh', data => this.renderer.onSelectionChanged(data.start, data.end)); this.selectionManager.on('newselection', text => { @@ -721,7 +695,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT button = getButton(ev); // get mouse coordinates - pos = getRawByteCoords(ev, self.rowContainer, self.charMeasure, self.options.lineHeight, self.cols, self.rows); + pos = getRawByteCoords(ev, self.element, self.charMeasure, self.options.lineHeight, self.cols, self.rows); if (!pos) return; sendEvent(button, pos); @@ -747,7 +721,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7< function sendMove(ev: MouseEvent): void { let button = pressed; - let pos = getRawByteCoords(ev, self.rowContainer, self.charMeasure, self.options.lineHeight, self.cols, self.rows); + let pos = getRawByteCoords(ev, self.element, self.charMeasure, self.options.lineHeight, self.cols, self.rows); if (!pos) return; // buttons marked as motions @@ -1033,12 +1007,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * @param {number} end The row to end at (between start and this.rows - 1). */ private queueLinkification(start: number, end: number): void { - if (this.linkifier) { - this.linkifier.linkifyRows(0, this.rows); - // for (let i = start; i <= end; i++) { - // this.linkifier.linkifyRow(i); - // } - } + // if (this.linkifier) { + // this.linkifier.linkifyRows(0, this.rows); + // } } /** @@ -1246,12 +1217,12 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * @param handler The handler callback function. */ public setHypertextLinkHandler(handler: LinkMatcherHandler): void { - if (!this.linkifier) { - throw new Error('Cannot attach a hypertext link handler before Terminal.open is called'); - } - this.linkifier.setHypertextLinkHandler(handler); - // Refresh to force links to refresh - this.refresh(0, this.rows - 1); + // if (!this.linkifier) { + // throw new Error('Cannot attach a hypertext link handler before Terminal.open is called'); + // } + // this.linkifier.setHypertextLinkHandler(handler); + // // Refresh to force links to refresh + // this.refresh(0, this.rows - 1); } /** @@ -1261,12 +1232,12 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * be cleared with null. */ public setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void { - if (!this.linkifier) { - throw new Error('Cannot attach a hypertext validation callback before Terminal.open is called'); - } - this.linkifier.setHypertextValidationCallback(callback); - // Refresh to force links to refresh - this.refresh(0, this.rows - 1); + // if (!this.linkifier) { + // throw new Error('Cannot attach a hypertext validation callback before Terminal.open is called'); + // } + // this.linkifier.setHypertextValidationCallback(callback); + // // Refresh to force links to refresh + // this.refresh(0, this.rows - 1); } /** @@ -1280,11 +1251,12 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * @return The ID of the new matcher, this can be used to deregister. */ public registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number { - if (this.linkifier) { - const matcherId = this.linkifier.registerLinkMatcher(regex, handler, options); - this.refresh(0, this.rows - 1); - return matcherId; - } + // if (this.linkifier) { + // const matcherId = this.linkifier.registerLinkMatcher(regex, handler, options); + // this.refresh(0, this.rows - 1); + // return matcherId; + // } + return 0; } /** @@ -1292,11 +1264,11 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * @param matcherId The link matcher's ID (returned after register) */ public deregisterLinkMatcher(matcherId: number): void { - if (this.linkifier) { - if (this.linkifier.deregisterLinkMatcher(matcherId)) { - this.refresh(0, this.rows - 1); - } - } + // if (this.linkifier) { + // if (this.linkifier.deregisterLinkMatcher(matcherId)) { + // this.refresh(0, this.rows - 1); + // } + // } } /** @@ -1794,13 +1766,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT return; } - let line; - let el; - let i; - let j; - let ch; - let addToY; - if (x === this.cols && y === this.rows) { // Check if we still need to measure the char size (fixes #785). if (!this.charMeasure.width || !this.charMeasure.height) { @@ -1814,16 +1779,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.buffers.resize(x, y); - // Adjust rows in the DOM to accurately reflect the new dimensions - while (this.children.length < y) { - this.insertRow(); - } - while (this.children.length > y) { - el = this.children.shift(); - if (!el) continue; - el.parentNode.removeChild(el); - } - this.cols = x; this.rows = y; this.buffers.setupTabStops(this.cols); diff --git a/src/Viewport.test.ts b/src/Viewport.test.ts index 428ef09871..8a960fc7dc 100644 --- a/src/Viewport.test.ts +++ b/src/Viewport.test.ts @@ -55,18 +55,6 @@ describe('Viewport', () => { }); describe('refresh', () => { - it('should set the line-height of the terminal', done => { - // Allow CharMeasure to be initialized - setTimeout(() => { - assert.equal(viewportElement.style.lineHeight, CHARACTER_HEIGHT + 'px'); - assert.equal(terminal.rowContainer.style.lineHeight, CHARACTER_HEIGHT + 'px'); - charMeasure.height = 1; - viewport.refresh(); - assert.equal(viewportElement.style.lineHeight, '1px'); - assert.equal(terminal.rowContainer.style.lineHeight, '1px'); - done(); - }, 0); - }); it('should set the height of the viewport when the line-height changed', () => { terminal.buffer.lines.push(''); terminal.buffer.lines.push(''); diff --git a/src/Viewport.ts b/src/Viewport.ts index b061eeb6cf..10418fa77d 100644 --- a/src/Viewport.ts +++ b/src/Viewport.ts @@ -51,7 +51,7 @@ export class Viewport implements IViewport { if (rowHeightChanged) { this.currentRowHeight = lineHeight; this.viewportElement.style.lineHeight = lineHeight + 'px'; - this.terminal.rowContainer.style.lineHeight = lineHeight + 'px'; + this.terminal.element.style.lineHeight = lineHeight + 'px'; } const viewportHeightChanged = this.lastRecordedViewportHeight !== this.terminal.rows; if (rowHeightChanged || viewportHeightChanged) { diff --git a/src/addons/fit/fit.js b/src/addons/fit/fit.js index b774cebfde..4f55457970 100644 --- a/src/addons/fit/fit.js +++ b/src/addons/fit/fit.js @@ -35,18 +35,14 @@ if (!term.element.parentElement) { return null; } - var parentElementStyle = window.getComputedStyle(term.element.parentElement), - parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')), - parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')) - 17), - elementStyle = window.getComputedStyle(term.element), - elementPaddingVer = parseInt(elementStyle.getPropertyValue('padding-top')) + parseInt(elementStyle.getPropertyValue('padding-bottom')), - elementPaddingHor = parseInt(elementStyle.getPropertyValue('padding-right')) + parseInt(elementStyle.getPropertyValue('padding-left')), - availableHeight = parentElementHeight - elementPaddingVer, - availableWidth = parentElementWidth - elementPaddingHor, - container = term.rowContainer, - subjectRow = term.rowContainer.firstElementChild, - contentBuffer = subjectRow.innerHTML; - + var parentElementStyle = window.getComputedStyle(term.element.parentElement); + var parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')); + var parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')) - 17); + var elementStyle = window.getComputedStyle(term.element); + var elementPaddingVer = parseInt(elementStyle.getPropertyValue('padding-top')) + parseInt(elementStyle.getPropertyValue('padding-bottom')); + var elementPaddingHor = parseInt(elementStyle.getPropertyValue('padding-right')) + parseInt(elementStyle.getPropertyValue('padding-left')); + var availableHeight = parentElementHeight - elementPaddingVer; + var availableWidth = parentElementWidth - elementPaddingHor; var geometry = { cols: parseInt(availableWidth / term.charMeasure.width, 10), rows: parseInt(availableHeight / (term.charMeasure.height * term.getOption('lineHeight')), 10) diff --git a/src/utils/Mouse.ts b/src/utils/Mouse.ts index f25a6b2e9b..77c422d5e7 100644 --- a/src/utils/Mouse.ts +++ b/src/utils/Mouse.ts @@ -28,7 +28,7 @@ export function getCoordsRelativeToElement(event: MouseEvent, element: HTMLEleme * is returned as an array in the form [x, y] instead of an object as it's a * little faster and this function is used in some low level code. * @param event The mouse event. - * @param rowContainer The terminal's row container. + * @param element The terminal's container element. * @param charMeasure The char measure object used to determine character sizes. * @param colCount The number of columns in the terminal. * @param rowCount The number of rows n the terminal. @@ -36,13 +36,13 @@ export function getCoordsRelativeToElement(event: MouseEvent, element: HTMLEleme * apply an offset to the x value such that the left half of the cell will * select that cell and the right half will select the next cell. */ -export function getCoords(event: MouseEvent, rowContainer: HTMLElement, charMeasure: CharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number] { +export function getCoords(event: MouseEvent, element: HTMLElement, charMeasure: CharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number] { // Coordinates cannot be measured if charMeasure has not been initialized if (!charMeasure.width || !charMeasure.height) { return null; } - const coords = getCoordsRelativeToElement(event, rowContainer); + const coords = getCoordsRelativeToElement(event, element); if (!coords) { return null; } @@ -63,13 +63,13 @@ export function getCoords(event: MouseEvent, rowContainer: HTMLElement, charMeas * them to the bounds of the terminal and adding 32 to both the x and y values * as expected by xterm. * @param event The mouse event. - * @param rowContainer The terminal's row container. + * @param element The terminal's container element. * @param charMeasure The char measure object used to determine character sizes. * @param colCount The number of columns in the terminal. * @param rowCount The number of rows in the terminal. */ -export function getRawByteCoords(event: MouseEvent, rowContainer: HTMLElement, charMeasure: CharMeasure, lineHeight: number, colCount: number, rowCount: number): { x: number, y: number } { - const coords = getCoords(event, rowContainer, charMeasure, lineHeight, colCount, rowCount); +export function getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: CharMeasure, lineHeight: number, colCount: number, rowCount: number): { x: number, y: number } { + const coords = getCoords(event, element, charMeasure, lineHeight, colCount, rowCount); let x = coords[0]; let y = coords[1]; From 8039fd35e6df96d146528b1043677d8868ce6af6 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 2 Sep 2017 13:34:01 -0700 Subject: [PATCH 062/108] Remove unused CSS, selectionElement --- src/Interfaces.ts | 1 - src/Terminal.ts | 6 - src/Viewport.ts | 1 - src/xterm.css | 2169 +-------------------------------------------- 4 files changed, 5 insertions(+), 2172 deletions(-) diff --git a/src/Interfaces.ts b/src/Interfaces.ts index 41fc8bdad9..861720a3a1 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -19,7 +19,6 @@ export interface IBrowser { export interface ITerminal extends IEventEmitter { element: HTMLElement; - selectionContainer: HTMLElement; selectionManager: ISelectionManager; charMeasure: ICharMeasure; textarea: HTMLTextAreaElement; diff --git a/src/Terminal.ts b/src/Terminal.ts index fec7fdcaab..8003dce338 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -97,7 +97,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT private body: HTMLBodyElement; private viewportScrollArea: HTMLElement; private viewportElement: HTMLElement; - public selectionContainer: HTMLElement; private helperContainer: HTMLElement; private compositionView: HTMLElement; private charSizeStyleElement: HTMLStyleElement; @@ -574,11 +573,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // preload audio this.syncBellSound(); - // Create the selection container. - this.selectionContainer = document.createElement('div'); - this.selectionContainer.classList.add('xterm-selection'); - this.element.appendChild(this.selectionContainer); - // TODO: Re-enable linkifier // this.linkifier.attachToDom(document, this.children); diff --git a/src/Viewport.ts b/src/Viewport.ts index 10418fa77d..cedcef2031 100644 --- a/src/Viewport.ts +++ b/src/Viewport.ts @@ -57,7 +57,6 @@ export class Viewport implements IViewport { if (rowHeightChanged || viewportHeightChanged) { this.lastRecordedViewportHeight = this.terminal.rows; this.viewportElement.style.height = lineHeight * this.terminal.rows + 'px'; - this.terminal.selectionContainer.style.height = this.viewportElement.style.height; } this.scrollArea.style.height = (lineHeight * this.lastRecordedBufferLength) + 'px'; } diff --git a/src/xterm.css b/src/xterm.css index 6a7504f3ed..30bce803fa 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -31,11 +31,12 @@ * other features. */ -/* - * Default style for xterm.js +/** + * Default styles for xterm.js */ .terminal { + /** TODO: Remove colors from xterm.css */ background-color: #000; color: #fff; font-family: courier-new, courier, monospace; @@ -74,66 +75,6 @@ resize: none; } -.terminal a { - color: inherit; - text-decoration: none; -} - -.terminal a:hover { - cursor: pointer; - text-decoration: underline; -} - -.terminal a.xterm-invalid-link:hover { - cursor: text; - text-decoration: none; -} - -.terminal .terminal-cursor { - position: relative; - transition: opacity 150ms ease; -} - -.terminal:not(.focus) .terminal-cursor { - outline: 1px solid #fff; - outline-offset: -1px; -} - -.terminal.xterm-cursor-style-block.focus:not(.xterm-cursor-blink-on) .terminal-cursor { - background-color: #fff; - color: #000; -} - -.terminal.focus.xterm-cursor-style-bar:not(.xterm-cursor-blink-on) .terminal-cursor::before, -.terminal.focus.xterm-cursor-style-underline:not(.xterm-cursor-blink-on) .terminal-cursor::before { - content: ''; - position: absolute; - background-color: #fff; - transition: opacity 150ms ease; -} - -.terminal.focus.xterm-cursor-style-bar:not(.xterm-cursor-blink-on) .terminal-cursor::before { - top: 0; - left: 0; - bottom: 0; - width: 1px; -} - -.terminal.focus.xterm-cursor-style-underline:not(.xterm-cursor-blink-on) .terminal-cursor::before { - bottom: 0; - left: 0; - right: 0; - height: 1px; -} - -.terminal.focus.xterm-cursor-style-block.visual-bell-active .terminal-cursor { - opacity: 0.5; -} -.terminal.focus.xterm-cursor-style-bar.visual-bell-active .terminal-cursor::before, -.terminal.focus.xterm-cursor-style-underline.visual-bell-active .terminal-cursor::before { - opacity: 0; -} - .terminal .composition-view { background: #000; color: #FFF; @@ -153,31 +94,12 @@ overflow-y: scroll; } -.terminal .xterm-wide-char, -.terminal .xterm-normal-char { - display: inline-block; -} - .terminal canvas { position: absolute; left: 0; top: 0; } -.terminal .xterm-rows { - position: absolute; - left: 0; - top: 0; - - /* Hide temporarily, this should be removed eventually */ - visibility: hidden; -} - -.terminal .xterm-rows > div { - /* Lines containing spans and text nodes ocassionally wrap despite being the same width (#327) */ - white-space: nowrap; -} - .terminal .xterm-scroll-area { visibility: hidden; } @@ -194,2087 +116,6 @@ cursor: default; } -.terminal .xterm-selection { - position: absolute; - top: 0; - left: 0; - z-index: 1; - opacity: 0.3; - pointer-events: none; -} - -.terminal .xterm-selection div { - position: absolute; - background-color: #fff; -} - -/* - * Determine default colors for xterm.js - */ -.terminal .xterm-bold { - font-weight: bold; -} - -.terminal .xterm-underline { - text-decoration: underline; -} - -.terminal .xterm-blink { - text-decoration: blink; -} - -.terminal .xterm-blink.xterm-underline { - text-decoration: blink underline; -} - -.terminal .xterm-hidden { - visibility: hidden; -} - -.terminal .xterm-color-0 { - color: #2e3436; -} - -.terminal .xterm-bg-color-0 { - background-color: #2e3436; -} - -.terminal .xterm-color-1 { - color: #cc0000; -} - -.terminal .xterm-bg-color-1 { - background-color: #cc0000; -} - -.terminal .xterm-color-2 { - color: #4e9a06; -} - -.terminal .xterm-bg-color-2 { - background-color: #4e9a06; -} - -.terminal .xterm-color-3 { - color: #c4a000; -} - -.terminal .xterm-bg-color-3 { - background-color: #c4a000; -} - -.terminal .xterm-color-4 { - color: #3465a4; -} - -.terminal .xterm-bg-color-4 { - background-color: #3465a4; -} - -.terminal .xterm-color-5 { - color: #75507b; -} - -.terminal .xterm-bg-color-5 { - background-color: #75507b; -} - -.terminal .xterm-color-6 { - color: #06989a; -} - -.terminal .xterm-bg-color-6 { - background-color: #06989a; -} - -.terminal .xterm-color-7 { - color: #d3d7cf; -} - -.terminal .xterm-bg-color-7 { - background-color: #d3d7cf; -} - -.terminal .xterm-color-8 { - color: #555753; -} - -.terminal .xterm-bg-color-8 { - background-color: #555753; -} - -.terminal .xterm-color-9 { - color: #ef2929; -} - -.terminal .xterm-bg-color-9 { - background-color: #ef2929; -} - -.terminal .xterm-color-10 { - color: #8ae234; -} - -.terminal .xterm-bg-color-10 { - background-color: #8ae234; -} - -.terminal .xterm-color-11 { - color: #fce94f; -} - -.terminal .xterm-bg-color-11 { - background-color: #fce94f; -} - -.terminal .xterm-color-12 { - color: #729fcf; -} - -.terminal .xterm-bg-color-12 { - background-color: #729fcf; -} - -.terminal .xterm-color-13 { - color: #ad7fa8; -} - -.terminal .xterm-bg-color-13 { - background-color: #ad7fa8; -} - -.terminal .xterm-color-14 { - color: #34e2e2; -} - -.terminal .xterm-bg-color-14 { - background-color: #34e2e2; -} - -.terminal .xterm-color-15 { - color: #eeeeec; -} - -.terminal .xterm-bg-color-15 { - background-color: #eeeeec; -} - -.terminal .xterm-color-16 { - color: #000000; -} - -.terminal .xterm-bg-color-16 { - background-color: #000000; -} - -.terminal .xterm-color-17 { - color: #00005f; -} - -.terminal .xterm-bg-color-17 { - background-color: #00005f; -} - -.terminal .xterm-color-18 { - color: #000087; -} - -.terminal .xterm-bg-color-18 { - background-color: #000087; -} - -.terminal .xterm-color-19 { - color: #0000af; -} - -.terminal .xterm-bg-color-19 { - background-color: #0000af; -} - -.terminal .xterm-color-20 { - color: #0000d7; -} - -.terminal .xterm-bg-color-20 { - background-color: #0000d7; -} - -.terminal .xterm-color-21 { - color: #0000ff; -} - -.terminal .xterm-bg-color-21 { - background-color: #0000ff; -} - -.terminal .xterm-color-22 { - color: #005f00; -} - -.terminal .xterm-bg-color-22 { - background-color: #005f00; -} - -.terminal .xterm-color-23 { - color: #005f5f; -} - -.terminal .xterm-bg-color-23 { - background-color: #005f5f; -} - -.terminal .xterm-color-24 { - color: #005f87; -} - -.terminal .xterm-bg-color-24 { - background-color: #005f87; -} - -.terminal .xterm-color-25 { - color: #005faf; -} - -.terminal .xterm-bg-color-25 { - background-color: #005faf; -} - -.terminal .xterm-color-26 { - color: #005fd7; -} - -.terminal .xterm-bg-color-26 { - background-color: #005fd7; -} - -.terminal .xterm-color-27 { - color: #005fff; -} - -.terminal .xterm-bg-color-27 { - background-color: #005fff; -} - -.terminal .xterm-color-28 { - color: #008700; -} - -.terminal .xterm-bg-color-28 { - background-color: #008700; -} - -.terminal .xterm-color-29 { - color: #00875f; -} - -.terminal .xterm-bg-color-29 { - background-color: #00875f; -} - -.terminal .xterm-color-30 { - color: #008787; -} - -.terminal .xterm-bg-color-30 { - background-color: #008787; -} - -.terminal .xterm-color-31 { - color: #0087af; -} - -.terminal .xterm-bg-color-31 { - background-color: #0087af; -} - -.terminal .xterm-color-32 { - color: #0087d7; -} - -.terminal .xterm-bg-color-32 { - background-color: #0087d7; -} - -.terminal .xterm-color-33 { - color: #0087ff; -} - -.terminal .xterm-bg-color-33 { - background-color: #0087ff; -} - -.terminal .xterm-color-34 { - color: #00af00; -} - -.terminal .xterm-bg-color-34 { - background-color: #00af00; -} - -.terminal .xterm-color-35 { - color: #00af5f; -} - -.terminal .xterm-bg-color-35 { - background-color: #00af5f; -} - -.terminal .xterm-color-36 { - color: #00af87; -} - -.terminal .xterm-bg-color-36 { - background-color: #00af87; -} - -.terminal .xterm-color-37 { - color: #00afaf; -} - -.terminal .xterm-bg-color-37 { - background-color: #00afaf; -} - -.terminal .xterm-color-38 { - color: #00afd7; -} - -.terminal .xterm-bg-color-38 { - background-color: #00afd7; -} - -.terminal .xterm-color-39 { - color: #00afff; -} - -.terminal .xterm-bg-color-39 { - background-color: #00afff; -} - -.terminal .xterm-color-40 { - color: #00d700; -} - -.terminal .xterm-bg-color-40 { - background-color: #00d700; -} - -.terminal .xterm-color-41 { - color: #00d75f; -} - -.terminal .xterm-bg-color-41 { - background-color: #00d75f; -} - -.terminal .xterm-color-42 { - color: #00d787; -} - -.terminal .xterm-bg-color-42 { - background-color: #00d787; -} - -.terminal .xterm-color-43 { - color: #00d7af; -} - -.terminal .xterm-bg-color-43 { - background-color: #00d7af; -} - -.terminal .xterm-color-44 { - color: #00d7d7; -} - -.terminal .xterm-bg-color-44 { - background-color: #00d7d7; -} - -.terminal .xterm-color-45 { - color: #00d7ff; -} - -.terminal .xterm-bg-color-45 { - background-color: #00d7ff; -} - -.terminal .xterm-color-46 { - color: #00ff00; -} - -.terminal .xterm-bg-color-46 { - background-color: #00ff00; -} - -.terminal .xterm-color-47 { - color: #00ff5f; -} - -.terminal .xterm-bg-color-47 { - background-color: #00ff5f; -} - -.terminal .xterm-color-48 { - color: #00ff87; -} - -.terminal .xterm-bg-color-48 { - background-color: #00ff87; -} - -.terminal .xterm-color-49 { - color: #00ffaf; -} - -.terminal .xterm-bg-color-49 { - background-color: #00ffaf; -} - -.terminal .xterm-color-50 { - color: #00ffd7; -} - -.terminal .xterm-bg-color-50 { - background-color: #00ffd7; -} - -.terminal .xterm-color-51 { - color: #00ffff; -} - -.terminal .xterm-bg-color-51 { - background-color: #00ffff; -} - -.terminal .xterm-color-52 { - color: #5f0000; -} - -.terminal .xterm-bg-color-52 { - background-color: #5f0000; -} - -.terminal .xterm-color-53 { - color: #5f005f; -} - -.terminal .xterm-bg-color-53 { - background-color: #5f005f; -} - -.terminal .xterm-color-54 { - color: #5f0087; -} - -.terminal .xterm-bg-color-54 { - background-color: #5f0087; -} - -.terminal .xterm-color-55 { - color: #5f00af; -} - -.terminal .xterm-bg-color-55 { - background-color: #5f00af; -} - -.terminal .xterm-color-56 { - color: #5f00d7; -} - -.terminal .xterm-bg-color-56 { - background-color: #5f00d7; -} - -.terminal .xterm-color-57 { - color: #5f00ff; -} - -.terminal .xterm-bg-color-57 { - background-color: #5f00ff; -} - -.terminal .xterm-color-58 { - color: #5f5f00; -} - -.terminal .xterm-bg-color-58 { - background-color: #5f5f00; -} - -.terminal .xterm-color-59 { - color: #5f5f5f; -} - -.terminal .xterm-bg-color-59 { - background-color: #5f5f5f; -} - -.terminal .xterm-color-60 { - color: #5f5f87; -} - -.terminal .xterm-bg-color-60 { - background-color: #5f5f87; -} - -.terminal .xterm-color-61 { - color: #5f5faf; -} - -.terminal .xterm-bg-color-61 { - background-color: #5f5faf; -} - -.terminal .xterm-color-62 { - color: #5f5fd7; -} - -.terminal .xterm-bg-color-62 { - background-color: #5f5fd7; -} - -.terminal .xterm-color-63 { - color: #5f5fff; -} - -.terminal .xterm-bg-color-63 { - background-color: #5f5fff; -} - -.terminal .xterm-color-64 { - color: #5f8700; -} - -.terminal .xterm-bg-color-64 { - background-color: #5f8700; -} - -.terminal .xterm-color-65 { - color: #5f875f; -} - -.terminal .xterm-bg-color-65 { - background-color: #5f875f; -} - -.terminal .xterm-color-66 { - color: #5f8787; -} - -.terminal .xterm-bg-color-66 { - background-color: #5f8787; -} - -.terminal .xterm-color-67 { - color: #5f87af; -} - -.terminal .xterm-bg-color-67 { - background-color: #5f87af; -} - -.terminal .xterm-color-68 { - color: #5f87d7; -} - -.terminal .xterm-bg-color-68 { - background-color: #5f87d7; -} - -.terminal .xterm-color-69 { - color: #5f87ff; -} - -.terminal .xterm-bg-color-69 { - background-color: #5f87ff; -} - -.terminal .xterm-color-70 { - color: #5faf00; -} - -.terminal .xterm-bg-color-70 { - background-color: #5faf00; -} - -.terminal .xterm-color-71 { - color: #5faf5f; -} - -.terminal .xterm-bg-color-71 { - background-color: #5faf5f; -} - -.terminal .xterm-color-72 { - color: #5faf87; -} - -.terminal .xterm-bg-color-72 { - background-color: #5faf87; -} - -.terminal .xterm-color-73 { - color: #5fafaf; -} - -.terminal .xterm-bg-color-73 { - background-color: #5fafaf; -} - -.terminal .xterm-color-74 { - color: #5fafd7; -} - -.terminal .xterm-bg-color-74 { - background-color: #5fafd7; -} - -.terminal .xterm-color-75 { - color: #5fafff; -} - -.terminal .xterm-bg-color-75 { - background-color: #5fafff; -} - -.terminal .xterm-color-76 { - color: #5fd700; -} - -.terminal .xterm-bg-color-76 { - background-color: #5fd700; -} - -.terminal .xterm-color-77 { - color: #5fd75f; -} - -.terminal .xterm-bg-color-77 { - background-color: #5fd75f; -} - -.terminal .xterm-color-78 { - color: #5fd787; -} - -.terminal .xterm-bg-color-78 { - background-color: #5fd787; -} - -.terminal .xterm-color-79 { - color: #5fd7af; -} - -.terminal .xterm-bg-color-79 { - background-color: #5fd7af; -} - -.terminal .xterm-color-80 { - color: #5fd7d7; -} - -.terminal .xterm-bg-color-80 { - background-color: #5fd7d7; -} - -.terminal .xterm-color-81 { - color: #5fd7ff; -} - -.terminal .xterm-bg-color-81 { - background-color: #5fd7ff; -} - -.terminal .xterm-color-82 { - color: #5fff00; -} - -.terminal .xterm-bg-color-82 { - background-color: #5fff00; -} - -.terminal .xterm-color-83 { - color: #5fff5f; -} - -.terminal .xterm-bg-color-83 { - background-color: #5fff5f; -} - -.terminal .xterm-color-84 { - color: #5fff87; -} - -.terminal .xterm-bg-color-84 { - background-color: #5fff87; -} - -.terminal .xterm-color-85 { - color: #5fffaf; -} - -.terminal .xterm-bg-color-85 { - background-color: #5fffaf; -} - -.terminal .xterm-color-86 { - color: #5fffd7; -} - -.terminal .xterm-bg-color-86 { - background-color: #5fffd7; -} - -.terminal .xterm-color-87 { - color: #5fffff; -} - -.terminal .xterm-bg-color-87 { - background-color: #5fffff; -} - -.terminal .xterm-color-88 { - color: #870000; -} - -.terminal .xterm-bg-color-88 { - background-color: #870000; -} - -.terminal .xterm-color-89 { - color: #87005f; -} - -.terminal .xterm-bg-color-89 { - background-color: #87005f; -} - -.terminal .xterm-color-90 { - color: #870087; -} - -.terminal .xterm-bg-color-90 { - background-color: #870087; -} - -.terminal .xterm-color-91 { - color: #8700af; -} - -.terminal .xterm-bg-color-91 { - background-color: #8700af; -} - -.terminal .xterm-color-92 { - color: #8700d7; -} - -.terminal .xterm-bg-color-92 { - background-color: #8700d7; -} - -.terminal .xterm-color-93 { - color: #8700ff; -} - -.terminal .xterm-bg-color-93 { - background-color: #8700ff; -} - -.terminal .xterm-color-94 { - color: #875f00; -} - -.terminal .xterm-bg-color-94 { - background-color: #875f00; -} - -.terminal .xterm-color-95 { - color: #875f5f; -} - -.terminal .xterm-bg-color-95 { - background-color: #875f5f; -} - -.terminal .xterm-color-96 { - color: #875f87; -} - -.terminal .xterm-bg-color-96 { - background-color: #875f87; -} - -.terminal .xterm-color-97 { - color: #875faf; -} - -.terminal .xterm-bg-color-97 { - background-color: #875faf; -} - -.terminal .xterm-color-98 { - color: #875fd7; -} - -.terminal .xterm-bg-color-98 { - background-color: #875fd7; -} - -.terminal .xterm-color-99 { - color: #875fff; -} - -.terminal .xterm-bg-color-99 { - background-color: #875fff; -} - -.terminal .xterm-color-100 { - color: #878700; -} - -.terminal .xterm-bg-color-100 { - background-color: #878700; -} - -.terminal .xterm-color-101 { - color: #87875f; -} - -.terminal .xterm-bg-color-101 { - background-color: #87875f; -} - -.terminal .xterm-color-102 { - color: #878787; -} - -.terminal .xterm-bg-color-102 { - background-color: #878787; -} - -.terminal .xterm-color-103 { - color: #8787af; -} - -.terminal .xterm-bg-color-103 { - background-color: #8787af; -} - -.terminal .xterm-color-104 { - color: #8787d7; -} - -.terminal .xterm-bg-color-104 { - background-color: #8787d7; -} - -.terminal .xterm-color-105 { - color: #8787ff; -} - -.terminal .xterm-bg-color-105 { - background-color: #8787ff; -} - -.terminal .xterm-color-106 { - color: #87af00; -} - -.terminal .xterm-bg-color-106 { - background-color: #87af00; -} - -.terminal .xterm-color-107 { - color: #87af5f; -} - -.terminal .xterm-bg-color-107 { - background-color: #87af5f; -} - -.terminal .xterm-color-108 { - color: #87af87; -} - -.terminal .xterm-bg-color-108 { - background-color: #87af87; -} - -.terminal .xterm-color-109 { - color: #87afaf; -} - -.terminal .xterm-bg-color-109 { - background-color: #87afaf; -} - -.terminal .xterm-color-110 { - color: #87afd7; -} - -.terminal .xterm-bg-color-110 { - background-color: #87afd7; -} - -.terminal .xterm-color-111 { - color: #87afff; -} - -.terminal .xterm-bg-color-111 { - background-color: #87afff; -} - -.terminal .xterm-color-112 { - color: #87d700; -} - -.terminal .xterm-bg-color-112 { - background-color: #87d700; -} - -.terminal .xterm-color-113 { - color: #87d75f; -} - -.terminal .xterm-bg-color-113 { - background-color: #87d75f; -} - -.terminal .xterm-color-114 { - color: #87d787; -} - -.terminal .xterm-bg-color-114 { - background-color: #87d787; -} - -.terminal .xterm-color-115 { - color: #87d7af; -} - -.terminal .xterm-bg-color-115 { - background-color: #87d7af; -} - -.terminal .xterm-color-116 { - color: #87d7d7; -} - -.terminal .xterm-bg-color-116 { - background-color: #87d7d7; -} - -.terminal .xterm-color-117 { - color: #87d7ff; -} - -.terminal .xterm-bg-color-117 { - background-color: #87d7ff; -} - -.terminal .xterm-color-118 { - color: #87ff00; -} - -.terminal .xterm-bg-color-118 { - background-color: #87ff00; -} - -.terminal .xterm-color-119 { - color: #87ff5f; -} - -.terminal .xterm-bg-color-119 { - background-color: #87ff5f; -} - -.terminal .xterm-color-120 { - color: #87ff87; -} - -.terminal .xterm-bg-color-120 { - background-color: #87ff87; -} - -.terminal .xterm-color-121 { - color: #87ffaf; -} - -.terminal .xterm-bg-color-121 { - background-color: #87ffaf; -} - -.terminal .xterm-color-122 { - color: #87ffd7; -} - -.terminal .xterm-bg-color-122 { - background-color: #87ffd7; -} - -.terminal .xterm-color-123 { - color: #87ffff; -} - -.terminal .xterm-bg-color-123 { - background-color: #87ffff; -} - -.terminal .xterm-color-124 { - color: #af0000; -} - -.terminal .xterm-bg-color-124 { - background-color: #af0000; -} - -.terminal .xterm-color-125 { - color: #af005f; -} - -.terminal .xterm-bg-color-125 { - background-color: #af005f; -} - -.terminal .xterm-color-126 { - color: #af0087; -} - -.terminal .xterm-bg-color-126 { - background-color: #af0087; -} - -.terminal .xterm-color-127 { - color: #af00af; -} - -.terminal .xterm-bg-color-127 { - background-color: #af00af; -} - -.terminal .xterm-color-128 { - color: #af00d7; -} - -.terminal .xterm-bg-color-128 { - background-color: #af00d7; -} - -.terminal .xterm-color-129 { - color: #af00ff; -} - -.terminal .xterm-bg-color-129 { - background-color: #af00ff; -} - -.terminal .xterm-color-130 { - color: #af5f00; -} - -.terminal .xterm-bg-color-130 { - background-color: #af5f00; -} - -.terminal .xterm-color-131 { - color: #af5f5f; -} - -.terminal .xterm-bg-color-131 { - background-color: #af5f5f; -} - -.terminal .xterm-color-132 { - color: #af5f87; -} - -.terminal .xterm-bg-color-132 { - background-color: #af5f87; -} - -.terminal .xterm-color-133 { - color: #af5faf; -} - -.terminal .xterm-bg-color-133 { - background-color: #af5faf; -} - -.terminal .xterm-color-134 { - color: #af5fd7; -} - -.terminal .xterm-bg-color-134 { - background-color: #af5fd7; -} - -.terminal .xterm-color-135 { - color: #af5fff; -} - -.terminal .xterm-bg-color-135 { - background-color: #af5fff; -} - -.terminal .xterm-color-136 { - color: #af8700; -} - -.terminal .xterm-bg-color-136 { - background-color: #af8700; -} - -.terminal .xterm-color-137 { - color: #af875f; -} - -.terminal .xterm-bg-color-137 { - background-color: #af875f; -} - -.terminal .xterm-color-138 { - color: #af8787; -} - -.terminal .xterm-bg-color-138 { - background-color: #af8787; -} - -.terminal .xterm-color-139 { - color: #af87af; -} - -.terminal .xterm-bg-color-139 { - background-color: #af87af; -} - -.terminal .xterm-color-140 { - color: #af87d7; -} - -.terminal .xterm-bg-color-140 { - background-color: #af87d7; -} - -.terminal .xterm-color-141 { - color: #af87ff; -} - -.terminal .xterm-bg-color-141 { - background-color: #af87ff; -} - -.terminal .xterm-color-142 { - color: #afaf00; -} - -.terminal .xterm-bg-color-142 { - background-color: #afaf00; -} - -.terminal .xterm-color-143 { - color: #afaf5f; -} - -.terminal .xterm-bg-color-143 { - background-color: #afaf5f; -} - -.terminal .xterm-color-144 { - color: #afaf87; -} - -.terminal .xterm-bg-color-144 { - background-color: #afaf87; -} - -.terminal .xterm-color-145 { - color: #afafaf; -} - -.terminal .xterm-bg-color-145 { - background-color: #afafaf; -} - -.terminal .xterm-color-146 { - color: #afafd7; -} - -.terminal .xterm-bg-color-146 { - background-color: #afafd7; -} - -.terminal .xterm-color-147 { - color: #afafff; -} - -.terminal .xterm-bg-color-147 { - background-color: #afafff; -} - -.terminal .xterm-color-148 { - color: #afd700; -} - -.terminal .xterm-bg-color-148 { - background-color: #afd700; -} - -.terminal .xterm-color-149 { - color: #afd75f; -} - -.terminal .xterm-bg-color-149 { - background-color: #afd75f; -} - -.terminal .xterm-color-150 { - color: #afd787; -} - -.terminal .xterm-bg-color-150 { - background-color: #afd787; -} - -.terminal .xterm-color-151 { - color: #afd7af; -} - -.terminal .xterm-bg-color-151 { - background-color: #afd7af; -} - -.terminal .xterm-color-152 { - color: #afd7d7; -} - -.terminal .xterm-bg-color-152 { - background-color: #afd7d7; -} - -.terminal .xterm-color-153 { - color: #afd7ff; -} - -.terminal .xterm-bg-color-153 { - background-color: #afd7ff; -} - -.terminal .xterm-color-154 { - color: #afff00; -} - -.terminal .xterm-bg-color-154 { - background-color: #afff00; -} - -.terminal .xterm-color-155 { - color: #afff5f; -} - -.terminal .xterm-bg-color-155 { - background-color: #afff5f; -} - -.terminal .xterm-color-156 { - color: #afff87; -} - -.terminal .xterm-bg-color-156 { - background-color: #afff87; -} - -.terminal .xterm-color-157 { - color: #afffaf; -} - -.terminal .xterm-bg-color-157 { - background-color: #afffaf; -} - -.terminal .xterm-color-158 { - color: #afffd7; -} - -.terminal .xterm-bg-color-158 { - background-color: #afffd7; -} - -.terminal .xterm-color-159 { - color: #afffff; -} - -.terminal .xterm-bg-color-159 { - background-color: #afffff; -} - -.terminal .xterm-color-160 { - color: #d70000; -} - -.terminal .xterm-bg-color-160 { - background-color: #d70000; -} - -.terminal .xterm-color-161 { - color: #d7005f; -} - -.terminal .xterm-bg-color-161 { - background-color: #d7005f; -} - -.terminal .xterm-color-162 { - color: #d70087; -} - -.terminal .xterm-bg-color-162 { - background-color: #d70087; -} - -.terminal .xterm-color-163 { - color: #d700af; -} - -.terminal .xterm-bg-color-163 { - background-color: #d700af; -} - -.terminal .xterm-color-164 { - color: #d700d7; -} - -.terminal .xterm-bg-color-164 { - background-color: #d700d7; -} - -.terminal .xterm-color-165 { - color: #d700ff; -} - -.terminal .xterm-bg-color-165 { - background-color: #d700ff; -} - -.terminal .xterm-color-166 { - color: #d75f00; -} - -.terminal .xterm-bg-color-166 { - background-color: #d75f00; -} - -.terminal .xterm-color-167 { - color: #d75f5f; -} - -.terminal .xterm-bg-color-167 { - background-color: #d75f5f; -} - -.terminal .xterm-color-168 { - color: #d75f87; -} - -.terminal .xterm-bg-color-168 { - background-color: #d75f87; -} - -.terminal .xterm-color-169 { - color: #d75faf; -} - -.terminal .xterm-bg-color-169 { - background-color: #d75faf; -} - -.terminal .xterm-color-170 { - color: #d75fd7; -} - -.terminal .xterm-bg-color-170 { - background-color: #d75fd7; -} - -.terminal .xterm-color-171 { - color: #d75fff; -} - -.terminal .xterm-bg-color-171 { - background-color: #d75fff; -} - -.terminal .xterm-color-172 { - color: #d78700; -} - -.terminal .xterm-bg-color-172 { - background-color: #d78700; -} - -.terminal .xterm-color-173 { - color: #d7875f; -} - -.terminal .xterm-bg-color-173 { - background-color: #d7875f; -} - -.terminal .xterm-color-174 { - color: #d78787; -} - -.terminal .xterm-bg-color-174 { - background-color: #d78787; -} - -.terminal .xterm-color-175 { - color: #d787af; -} - -.terminal .xterm-bg-color-175 { - background-color: #d787af; -} - -.terminal .xterm-color-176 { - color: #d787d7; -} - -.terminal .xterm-bg-color-176 { - background-color: #d787d7; -} - -.terminal .xterm-color-177 { - color: #d787ff; -} - -.terminal .xterm-bg-color-177 { - background-color: #d787ff; -} - -.terminal .xterm-color-178 { - color: #d7af00; -} - -.terminal .xterm-bg-color-178 { - background-color: #d7af00; -} - -.terminal .xterm-color-179 { - color: #d7af5f; -} - -.terminal .xterm-bg-color-179 { - background-color: #d7af5f; -} - -.terminal .xterm-color-180 { - color: #d7af87; -} - -.terminal .xterm-bg-color-180 { - background-color: #d7af87; -} - -.terminal .xterm-color-181 { - color: #d7afaf; -} - -.terminal .xterm-bg-color-181 { - background-color: #d7afaf; -} - -.terminal .xterm-color-182 { - color: #d7afd7; -} - -.terminal .xterm-bg-color-182 { - background-color: #d7afd7; -} - -.terminal .xterm-color-183 { - color: #d7afff; -} - -.terminal .xterm-bg-color-183 { - background-color: #d7afff; -} - -.terminal .xterm-color-184 { - color: #d7d700; -} - -.terminal .xterm-bg-color-184 { - background-color: #d7d700; -} - -.terminal .xterm-color-185 { - color: #d7d75f; -} - -.terminal .xterm-bg-color-185 { - background-color: #d7d75f; -} - -.terminal .xterm-color-186 { - color: #d7d787; -} - -.terminal .xterm-bg-color-186 { - background-color: #d7d787; -} - -.terminal .xterm-color-187 { - color: #d7d7af; -} - -.terminal .xterm-bg-color-187 { - background-color: #d7d7af; -} - -.terminal .xterm-color-188 { - color: #d7d7d7; -} - -.terminal .xterm-bg-color-188 { - background-color: #d7d7d7; -} - -.terminal .xterm-color-189 { - color: #d7d7ff; -} - -.terminal .xterm-bg-color-189 { - background-color: #d7d7ff; -} - -.terminal .xterm-color-190 { - color: #d7ff00; -} - -.terminal .xterm-bg-color-190 { - background-color: #d7ff00; -} - -.terminal .xterm-color-191 { - color: #d7ff5f; -} - -.terminal .xterm-bg-color-191 { - background-color: #d7ff5f; -} - -.terminal .xterm-color-192 { - color: #d7ff87; -} - -.terminal .xterm-bg-color-192 { - background-color: #d7ff87; -} - -.terminal .xterm-color-193 { - color: #d7ffaf; -} - -.terminal .xterm-bg-color-193 { - background-color: #d7ffaf; -} - -.terminal .xterm-color-194 { - color: #d7ffd7; -} - -.terminal .xterm-bg-color-194 { - background-color: #d7ffd7; -} - -.terminal .xterm-color-195 { - color: #d7ffff; -} - -.terminal .xterm-bg-color-195 { - background-color: #d7ffff; -} - -.terminal .xterm-color-196 { - color: #ff0000; -} - -.terminal .xterm-bg-color-196 { - background-color: #ff0000; -} - -.terminal .xterm-color-197 { - color: #ff005f; -} - -.terminal .xterm-bg-color-197 { - background-color: #ff005f; -} - -.terminal .xterm-color-198 { - color: #ff0087; -} - -.terminal .xterm-bg-color-198 { - background-color: #ff0087; -} - -.terminal .xterm-color-199 { - color: #ff00af; -} - -.terminal .xterm-bg-color-199 { - background-color: #ff00af; -} - -.terminal .xterm-color-200 { - color: #ff00d7; -} - -.terminal .xterm-bg-color-200 { - background-color: #ff00d7; -} - -.terminal .xterm-color-201 { - color: #ff00ff; -} - -.terminal .xterm-bg-color-201 { - background-color: #ff00ff; -} - -.terminal .xterm-color-202 { - color: #ff5f00; -} - -.terminal .xterm-bg-color-202 { - background-color: #ff5f00; -} - -.terminal .xterm-color-203 { - color: #ff5f5f; -} - -.terminal .xterm-bg-color-203 { - background-color: #ff5f5f; -} - -.terminal .xterm-color-204 { - color: #ff5f87; -} - -.terminal .xterm-bg-color-204 { - background-color: #ff5f87; -} - -.terminal .xterm-color-205 { - color: #ff5faf; -} - -.terminal .xterm-bg-color-205 { - background-color: #ff5faf; -} - -.terminal .xterm-color-206 { - color: #ff5fd7; -} - -.terminal .xterm-bg-color-206 { - background-color: #ff5fd7; -} - -.terminal .xterm-color-207 { - color: #ff5fff; -} - -.terminal .xterm-bg-color-207 { - background-color: #ff5fff; -} - -.terminal .xterm-color-208 { - color: #ff8700; -} - -.terminal .xterm-bg-color-208 { - background-color: #ff8700; -} - -.terminal .xterm-color-209 { - color: #ff875f; -} - -.terminal .xterm-bg-color-209 { - background-color: #ff875f; -} - -.terminal .xterm-color-210 { - color: #ff8787; -} - -.terminal .xterm-bg-color-210 { - background-color: #ff8787; -} - -.terminal .xterm-color-211 { - color: #ff87af; -} - -.terminal .xterm-bg-color-211 { - background-color: #ff87af; -} - -.terminal .xterm-color-212 { - color: #ff87d7; -} - -.terminal .xterm-bg-color-212 { - background-color: #ff87d7; -} - -.terminal .xterm-color-213 { - color: #ff87ff; -} - -.terminal .xterm-bg-color-213 { - background-color: #ff87ff; -} - -.terminal .xterm-color-214 { - color: #ffaf00; -} - -.terminal .xterm-bg-color-214 { - background-color: #ffaf00; -} - -.terminal .xterm-color-215 { - color: #ffaf5f; -} - -.terminal .xterm-bg-color-215 { - background-color: #ffaf5f; -} - -.terminal .xterm-color-216 { - color: #ffaf87; -} - -.terminal .xterm-bg-color-216 { - background-color: #ffaf87; -} - -.terminal .xterm-color-217 { - color: #ffafaf; -} - -.terminal .xterm-bg-color-217 { - background-color: #ffafaf; -} - -.terminal .xterm-color-218 { - color: #ffafd7; -} - -.terminal .xterm-bg-color-218 { - background-color: #ffafd7; -} - -.terminal .xterm-color-219 { - color: #ffafff; -} - -.terminal .xterm-bg-color-219 { - background-color: #ffafff; -} - -.terminal .xterm-color-220 { - color: #ffd700; -} - -.terminal .xterm-bg-color-220 { - background-color: #ffd700; -} - -.terminal .xterm-color-221 { - color: #ffd75f; -} - -.terminal .xterm-bg-color-221 { - background-color: #ffd75f; -} - -.terminal .xterm-color-222 { - color: #ffd787; -} - -.terminal .xterm-bg-color-222 { - background-color: #ffd787; -} - -.terminal .xterm-color-223 { - color: #ffd7af; -} - -.terminal .xterm-bg-color-223 { - background-color: #ffd7af; -} - -.terminal .xterm-color-224 { - color: #ffd7d7; -} - -.terminal .xterm-bg-color-224 { - background-color: #ffd7d7; -} - -.terminal .xterm-color-225 { - color: #ffd7ff; -} - -.terminal .xterm-bg-color-225 { - background-color: #ffd7ff; -} - -.terminal .xterm-color-226 { - color: #ffff00; -} - -.terminal .xterm-bg-color-226 { - background-color: #ffff00; -} - -.terminal .xterm-color-227 { - color: #ffff5f; -} - -.terminal .xterm-bg-color-227 { - background-color: #ffff5f; -} - -.terminal .xterm-color-228 { - color: #ffff87; -} - -.terminal .xterm-bg-color-228 { - background-color: #ffff87; -} - -.terminal .xterm-color-229 { - color: #ffffaf; -} - -.terminal .xterm-bg-color-229 { - background-color: #ffffaf; -} - -.terminal .xterm-color-230 { - color: #ffffd7; -} - -.terminal .xterm-bg-color-230 { - background-color: #ffffd7; -} - -.terminal .xterm-color-231 { - color: #ffffff; -} - -.terminal .xterm-bg-color-231 { - background-color: #ffffff; -} - -.terminal .xterm-color-232 { - color: #080808; -} - -.terminal .xterm-bg-color-232 { - background-color: #080808; -} - -.terminal .xterm-color-233 { - color: #121212; -} - -.terminal .xterm-bg-color-233 { - background-color: #121212; -} - -.terminal .xterm-color-234 { - color: #1c1c1c; -} - -.terminal .xterm-bg-color-234 { - background-color: #1c1c1c; -} - -.terminal .xterm-color-235 { - color: #262626; -} - -.terminal .xterm-bg-color-235 { - background-color: #262626; -} - -.terminal .xterm-color-236 { - color: #303030; -} - -.terminal .xterm-bg-color-236 { - background-color: #303030; -} - -.terminal .xterm-color-237 { - color: #3a3a3a; -} - -.terminal .xterm-bg-color-237 { - background-color: #3a3a3a; -} - -.terminal .xterm-color-238 { - color: #444444; -} - -.terminal .xterm-bg-color-238 { - background-color: #444444; -} - -.terminal .xterm-color-239 { - color: #4e4e4e; -} - -.terminal .xterm-bg-color-239 { - background-color: #4e4e4e; -} - -.terminal .xterm-color-240 { - color: #585858; -} - -.terminal .xterm-bg-color-240 { - background-color: #585858; -} - -.terminal .xterm-color-241 { - color: #626262; -} - -.terminal .xterm-bg-color-241 { - background-color: #626262; -} - -.terminal .xterm-color-242 { - color: #6c6c6c; -} - -.terminal .xterm-bg-color-242 { - background-color: #6c6c6c; -} - -.terminal .xterm-color-243 { - color: #767676; -} - -.terminal .xterm-bg-color-243 { - background-color: #767676; -} - -.terminal .xterm-color-244 { - color: #808080; -} - -.terminal .xterm-bg-color-244 { - background-color: #808080; -} - -.terminal .xterm-color-245 { - color: #8a8a8a; -} - -.terminal .xterm-bg-color-245 { - background-color: #8a8a8a; -} - -.terminal .xterm-color-246 { - color: #949494; -} - -.terminal .xterm-bg-color-246 { - background-color: #949494; -} - -.terminal .xterm-color-247 { - color: #9e9e9e; -} - -.terminal .xterm-bg-color-247 { - background-color: #9e9e9e; -} - -.terminal .xterm-color-248 { - color: #a8a8a8; -} - -.terminal .xterm-bg-color-248 { - background-color: #a8a8a8; -} - -.terminal .xterm-color-249 { - color: #b2b2b2; -} - -.terminal .xterm-bg-color-249 { - background-color: #b2b2b2; -} - -.terminal .xterm-color-250 { - color: #bcbcbc; -} - -.terminal .xterm-bg-color-250 { - background-color: #bcbcbc; -} - -.terminal .xterm-color-251 { - color: #c6c6c6; -} - -.terminal .xterm-bg-color-251 { - background-color: #c6c6c6; -} - -.terminal .xterm-color-252 { - color: #d0d0d0; -} - -.terminal .xterm-bg-color-252 { - background-color: #d0d0d0; -} - -.terminal .xterm-color-253 { - color: #dadada; -} - -.terminal .xterm-bg-color-253 { - background-color: #dadada; -} - -.terminal .xterm-color-254 { - color: #e4e4e4; -} - -.terminal .xterm-bg-color-254 { - background-color: #e4e4e4; -} - -.terminal .xterm-color-255 { - color: #eeeeee; -} - -.terminal .xterm-bg-color-255 { - background-color: #eeeeee; +.terminal:not(.enable-mouse-events) { + cursor: text; } From 4f81cdfe8c6da79b0dc6651eef86d6027834b3ff Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 2 Sep 2017 13:58:21 -0700 Subject: [PATCH 063/108] Make the terminal background theme aware --- demo/style.css | 2 -- src/Interfaces.ts | 2 ++ src/Terminal.ts | 5 ++++- src/Viewport.ts | 5 +++++ src/renderer/Renderer.ts | 6 ++++-- src/utils/TestUtils.test.ts | 3 +++ src/xterm.css | 4 +--- 7 files changed, 19 insertions(+), 8 deletions(-) diff --git a/demo/style.css b/demo/style.css index 7138962123..ee00eca032 100644 --- a/demo/style.css +++ b/demo/style.css @@ -16,7 +16,5 @@ h1 { } #terminal-container .terminal { - background-color: #111; - color: #fafafa; padding: 2px; } diff --git a/src/Interfaces.ts b/src/Interfaces.ts index 861720a3a1..2b4909c9db 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -4,6 +4,7 @@ import { ILinkMatcherOptions } from './Interfaces'; import { LinkMatcherHandler, LinkMatcherValidationCallback, Charset, LineData } from './Types'; +import { IColorSet } from './renderer/Interfaces'; export interface IBrowser { isNode: boolean; @@ -163,6 +164,7 @@ export interface IViewport { onWheel(ev: WheelEvent): void; onTouchStart(ev: TouchEvent): void; onTouchMove(ev: TouchEvent): void; + onThemeChanged(colors: IColorSet): void; } export interface ISelectionManager { diff --git a/src/Terminal.ts b/src/Terminal.ts index 8003dce338..d47c6bf85a 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -318,7 +318,10 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT public setTheme(theme: ITheme): void { // TODO: Allow setting of theme before renderer is ready if (this.renderer) { - this.renderer.setTheme(theme); + const colors = this.renderer.setTheme(theme); + if (this.viewport) { + this.viewport.onThemeChanged(colors); + } } } diff --git a/src/Viewport.ts b/src/Viewport.ts index cedcef2031..88433ed5a2 100644 --- a/src/Viewport.ts +++ b/src/Viewport.ts @@ -4,6 +4,7 @@ import { ITerminal, IViewport } from './Interfaces'; import { CharMeasure } from './utils/CharMeasure'; +import { IColorSet } from './renderer/Interfaces'; /** * Represents the viewport of a terminal, the visible area within the larger buffer of output. @@ -40,6 +41,10 @@ export class Viewport implements IViewport { setTimeout(() => this.syncScrollArea(), 0); } + public onThemeChanged(colors: IColorSet): void { + this.viewportElement.style.backgroundColor = colors.background; + } + /** * Refreshes row height, setting line-height, viewport height and scroll area height if * necessary. diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index b3df2b7a02..adf5bbd0f1 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -11,7 +11,7 @@ import { SelectionRenderLayer } from './SelectionRenderLayer'; import { CursorRenderLayer } from './CursorRenderLayer'; import { ColorManager } from './ColorManager'; import { BaseRenderLayer } from './BaseRenderLayer'; -import { IRenderLayer } from './Interfaces'; +import { IRenderLayer, IColorSet } from './Interfaces'; export class Renderer { /** A queue of the rows to be refreshed */ @@ -32,7 +32,7 @@ export class Renderer { ]; } - public setTheme(theme: ITheme): void { + public setTheme(theme: ITheme): IColorSet { this._colorManager.setTheme(theme); // Clear layers and force a full render this._renderLayers.forEach(l => { @@ -42,6 +42,8 @@ export class Renderer { // TODO: This is currently done for every single terminal, but it's static so it's wasting time this._terminal.refresh(0, this._terminal.rows - 1); + + return this._colorManager.colors; } public onResize(cols: number, rows: number): void { diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index f4eae64db9..46a226a230 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -202,6 +202,9 @@ export class MockBuffer implements IBuffer { } export class MockViewport implements IViewport { + onThemeChanged(colors: IColorSet): void { + throw new Error('Method not implemented.'); + } onWheel(ev: WheelEvent): void { throw new Error('Method not implemented.'); } diff --git a/src/xterm.css b/src/xterm.css index 30bce803fa..0ddfe8b575 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -36,9 +36,6 @@ */ .terminal { - /** TODO: Remove colors from xterm.css */ - background-color: #000; - color: #fff; font-family: courier-new, courier, monospace; font-feature-settings: "liga" 0; position: relative; @@ -76,6 +73,7 @@ } .terminal .composition-view { + /* TODO: Composition position got messed up somewhere */ background: #000; color: #FFF; display: none; From c5a6d96a62c3900f89d7be072c838388f775d995 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 2 Sep 2017 14:11:21 -0700 Subject: [PATCH 064/108] Remove padding from terminal, not support atm --- demo/style.css | 4 ---- 1 file changed, 4 deletions(-) diff --git a/demo/style.css b/demo/style.css index ee00eca032..1ab5f61caf 100644 --- a/demo/style.css +++ b/demo/style.css @@ -14,7 +14,3 @@ h1 { margin: 0 auto; padding: 2px; } - -#terminal-container .terminal { - padding: 2px; -} From 7ccac09b6bbac4e93030c50aa5b0d5aaff41e50d Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 2 Sep 2017 18:41:53 -0700 Subject: [PATCH 065/108] Fix the composition element's position --- src/Buffer.ts | 6 ++++++ src/CompositionHelper.ts | 23 ++++++++++++----------- src/Interfaces.ts | 1 + src/xterm.css | 5 +++++ 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 101244f1f8..6931eea453 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -52,6 +52,12 @@ export class Buffer implements IBuffer { return this._hasScrollback && this.lines.maxLength > this._terminal.rows; } + public get isCursorInViewport(): boolean { + const absoluteY = this.ybase + this.y; + const relativeY = absoluteY - this.ydisp; + return (relativeY >= 0 && relativeY < this._terminal.rows); + } + /** * Gets the correct buffer length based on the rows provided, the terminal's * scrollback and whether this buffer is flagged to have scrollback or not. diff --git a/src/CompositionHelper.ts b/src/CompositionHelper.ts index 223a90e7c2..345041d667 100644 --- a/src/CompositionHelper.ts +++ b/src/CompositionHelper.ts @@ -63,6 +63,7 @@ export class CompositionHelper { * @param {CompositionEvent} ev The event. */ public compositionupdate(ev: CompositionEvent): void { + console.log('compositionupdate'); this.compositionView.textContent = ev.data; this.updateCompositionElements(); setTimeout(() => { @@ -193,26 +194,26 @@ export class CompositionHelper { if (!this.isComposing) { return; } - const cursor = this.terminal.element.querySelector('.terminal-cursor'); - if (cursor) { - // Take .xterm-rows offsetTop into account as well in case it's positioned absolutely within - // the .xterm element. - const xtermRows = this.terminal.element.querySelector('.xterm-rows'); - const cursorTop = xtermRows.offsetTop + cursor.offsetTop; - - this.compositionView.style.left = cursor.offsetLeft + 'px'; + + if (this.terminal.buffer.isCursorInViewport) { + const cellHeight = Math.ceil(this.terminal.charMeasure.height * this.terminal.options.lineHeight); + const cursorTop = this.terminal.buffer.y * cellHeight; + const cursorLeft = this.terminal.buffer.x * this.terminal.charMeasure.width; + + this.compositionView.style.left = cursorLeft + 'px'; this.compositionView.style.top = cursorTop + 'px'; - this.compositionView.style.height = cursor.offsetHeight + 'px'; - this.compositionView.style.lineHeight = cursor.offsetHeight + 'px'; + this.compositionView.style.height = cellHeight + 'px'; + this.compositionView.style.lineHeight = cellHeight + 'px'; // Sync the textarea to the exact position of the composition view so the IME knows where the // text is. const compositionViewBounds = this.compositionView.getBoundingClientRect(); - this.textarea.style.left = cursor.offsetLeft + 'px'; + this.textarea.style.left = cursorLeft + 'px'; this.textarea.style.top = cursorTop + 'px'; this.textarea.style.width = compositionViewBounds.width + 'px'; this.textarea.style.height = compositionViewBounds.height + 'px'; this.textarea.style.lineHeight = compositionViewBounds.height + 'px'; } + if (!dontRecurse) { setTimeout(() => this.updateCompositionElements(true), 0); } diff --git a/src/Interfaces.ts b/src/Interfaces.ts index 2b4909c9db..7ebf698378 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -145,6 +145,7 @@ export interface IBuffer { scrollTop: number; savedY: number; savedX: number; + isCursorInViewport: boolean; translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string; nextStop(x?: number): number; prevStop(x?: number): number; diff --git a/src/xterm.css b/src/xterm.css index 0ddfe8b575..df3aff5908 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -52,6 +52,11 @@ .terminal .xterm-helpers { position: absolute; top: 0; + /** + * The z-index of the helpers must be higher than the canvases in order for + * IMEs to appear on top. + */ + z-index: 10; } .terminal .xterm-helper-textarea { From c94e87428deb687dd3739fa3527486d4a6cdc802 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 2 Sep 2017 19:04:07 -0700 Subject: [PATCH 066/108] Fix CharMeasure getting taller on successive calls --- src/Viewport.ts | 1 - src/addons/fit/fit.js | 10 ++++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Viewport.ts b/src/Viewport.ts index 88433ed5a2..2de698a0e0 100644 --- a/src/Viewport.ts +++ b/src/Viewport.ts @@ -56,7 +56,6 @@ export class Viewport implements IViewport { if (rowHeightChanged) { this.currentRowHeight = lineHeight; this.viewportElement.style.lineHeight = lineHeight + 'px'; - this.terminal.element.style.lineHeight = lineHeight + 'px'; } const viewportHeightChanged = this.lastRecordedViewportHeight !== this.terminal.rows; if (rowHeightChanged || viewportHeightChanged) { diff --git a/src/addons/fit/fit.js b/src/addons/fit/fit.js index 4f55457970..60214dad9e 100644 --- a/src/addons/fit/fit.js +++ b/src/addons/fit/fit.js @@ -44,8 +44,8 @@ var availableHeight = parentElementHeight - elementPaddingVer; var availableWidth = parentElementWidth - elementPaddingHor; var geometry = { - cols: parseInt(availableWidth / term.charMeasure.width, 10), - rows: parseInt(availableHeight / (term.charMeasure.height * term.getOption('lineHeight')), 10) + cols: Math.floor(availableWidth / term.charMeasure.width), + rows: Math.floor(availableHeight / Math.ceil(term.charMeasure.height * term.getOption('lineHeight'))) }; return geometry; @@ -59,8 +59,10 @@ if (geometry) { // Force a full render - term.renderer.clear(); - term.resize(geometry.cols, geometry.rows); + if (term.rows !== geometry.rows || term.cols !== geometry.cols) { + term.renderer.clear(); + term.resize(geometry.cols, geometry.rows); + } } }, 0); }; From 7973b26e7a956ed6e59bd4e9d80a2b0195ad3cc5 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 2 Sep 2017 19:45:44 -0700 Subject: [PATCH 067/108] Include new options in ITerminalOptions --- typings/xterm.d.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 72206c824e..b1cea81691 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -41,6 +41,21 @@ interface ITerminalOptions { */ disableStdin?: boolean; + /** + * The font size used to render text. + */ + fontSize?: number; + + /** + * The font family used to render text. + */ + fontFamily?: string; + + /** + * The line height used to render text. + */ + lineHeight?: number; + /** * The number of rows in the terminal. */ From 8427f7490dd7732bdd0e812feaea11a1d98f24f3 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 2 Sep 2017 21:13:44 -0700 Subject: [PATCH 068/108] Clear state when resizing as canvases get cleared --- src/renderer/BackgroundRenderLayer.ts | 2 ++ src/renderer/CursorRenderLayer.ts | 12 ++++++++++++ src/renderer/ForegroundRenderLayer.ts | 2 ++ src/renderer/SelectionRenderLayer.ts | 9 +++++++++ src/utils/TestUtils.test.ts | 2 ++ 5 files changed, 27 insertions(+) diff --git a/src/renderer/BackgroundRenderLayer.ts b/src/renderer/BackgroundRenderLayer.ts index 729a8ec5fc..292847c2db 100644 --- a/src/renderer/BackgroundRenderLayer.ts +++ b/src/renderer/BackgroundRenderLayer.ts @@ -15,6 +15,8 @@ export class BackgroundRenderLayer extends BaseRenderLayer { public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void { super.resize(terminal, canvasWidth, canvasHeight, charSizeChanged); + // Resizing the canvas discards the contents of the canvas so clear state + this._state.clear(); this._state.resize(terminal.cols, terminal.rows); } diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index cc443b4979..482d4e1482 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -43,6 +43,18 @@ export class CursorRenderLayer extends BaseRenderLayer { // TODO: Consider initial options? Maybe onOptionsChanged should be called at the end of open? } + public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void { + super.resize(terminal, canvasWidth, canvasHeight, charSizeChanged); + // Resizing the canvas discards the contents of the canvas so clear state + this._state = { + x: null, + y: null, + isFocused: null, + style: null, + width: null, + }; + } + public reset(terminal: ITerminal): void { this._clearCursor(); if (this._cursorBlinkStateManager) { diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index 1f579f91af..e8835dea60 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -16,6 +16,8 @@ export class ForegroundRenderLayer extends BaseRenderLayer { public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void { super.resize(terminal, canvasWidth, canvasHeight, charSizeChanged); + // Resizing the canvas discards the contents of the canvas so clear state + this._state.clear(); this._state.resize(terminal.cols, terminal.rows); } diff --git a/src/renderer/SelectionRenderLayer.ts b/src/renderer/SelectionRenderLayer.ts index 1dcb8ab6a6..7f131da802 100644 --- a/src/renderer/SelectionRenderLayer.ts +++ b/src/renderer/SelectionRenderLayer.ts @@ -16,6 +16,15 @@ export class SelectionRenderLayer extends BaseRenderLayer { }; } + public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void { + super.resize(terminal, canvasWidth, canvasHeight, charSizeChanged); + // Resizing the canvas discards the contents of the canvas so clear state + this._state = { + start: null, + end: null + }; + } + public reset(terminal: ITerminal): void { if (this._state.start && this._state.end) { this._state = { diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index 46a226a230..d4dc16ab0a 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -5,6 +5,7 @@ import { ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, IListenerType, IInputHandlingTerminal, IViewport, ICircularList, ICompositionHelper } from '../Interfaces'; import { LineData } from '../Types'; import * as Browser from './Browser'; +import { IColorSet } from '../renderer/Interfaces'; export class MockTerminal implements ITerminal { isFocused: boolean; @@ -180,6 +181,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { } export class MockBuffer implements IBuffer { + isCursorInViewport: boolean; lines: ICircularList<[number, string, number][]>; ydisp: number; ybase: number; From fd4e9d8a1bcf2fa4e6b4f275d48725e7670c534e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 2 Sep 2017 23:13:22 -0700 Subject: [PATCH 069/108] Fix graphical glitch wide chars would leave behind Wide chars now 'own' the character next to them and are entirely responsible for drawing and clean up --- src/renderer/BaseRenderLayer.ts | 5 +++++ src/renderer/ForegroundRenderLayer.ts | 21 +++++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 912d351f9e..686a1d252b 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -128,6 +128,11 @@ export abstract class BaseRenderLayer implements IRenderLayer { } protected drawChar(terminal: ITerminal, char: string, code: number, width: number, x: number, y: number, fg: number, underline: boolean = false): void { + // Clear the cell next to this character if it's wide + if (width === 2) { + this.clearCells(x + 1, y, 1, 1); + } + let colorIndex = 0; if (fg < 256) { colorIndex = fg + 1; diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index e8835dea60..8ee81f02db 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -48,6 +48,14 @@ export class ForegroundRenderLayer extends BaseRenderLayer { const code: number = charData[CHAR_DATA_CODE_INDEX]; const char: string = charData[CHAR_DATA_CHAR_INDEX]; const attr: number = charData[CHAR_DATA_ATTR_INDEX]; + const 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) { + this._state.cache[x][y] = null; + continue; + } // Skip rendering if the character is identical const state = this._state.cache[x][y]; @@ -59,7 +67,7 @@ export class ForegroundRenderLayer extends BaseRenderLayer { // Clear the old character if present if (state && state[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) { - this.clearCells(x, y, 1, 1); + this.clearChar(x, y); } this._state.cache[x][y] = charData; @@ -102,11 +110,20 @@ export class ForegroundRenderLayer extends BaseRenderLayer { this.drawBottomLineAtCell(x, y); } - const width: number = charData[CHAR_DATA_WIDTH_INDEX]; this.drawChar(terminal, char, code, width, x, y, fg); this._ctx.restore(); } } } + + private clearChar(x: number, y: number): void { + let colsToClear = 1; + // Clear the adjacent character if it was wide + const state = this._state.cache[x][y]; + if (state && state[CHAR_DATA_WIDTH_INDEX] === 2) { + colsToClear = 2; + } + this.clearCells(x, y, colsToClear, 1); + } } From 5a4851630e527ebbe1d002e109a5c62588d77af3 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 3 Sep 2017 07:27:52 -0700 Subject: [PATCH 070/108] Prevent updating of invalid terminal row ranges --- src/renderer/Renderer.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index adf5bbd0f1..bc1a7a1b82 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -123,6 +123,8 @@ export class Renderer { this._refreshAnimationFrame = null; // Render + start = Math.max(start, 0); + end = Math.min(end, this._terminal.rows - 1); this._renderLayers.forEach(l => l.onGridChanged(this._terminal, start, end)); this._terminal.emit('refresh', {start, end}); } From deb47e29bf884bc7c696e3e135928d3f558dfa75 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 3 Sep 2017 07:41:18 -0700 Subject: [PATCH 071/108] Fix canvas resizing with non-1 lineHeight --- src/renderer/Renderer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index bc1a7a1b82..58f31ff76a 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -48,13 +48,13 @@ export class Renderer { public onResize(cols: number, rows: number): void { const width = this._terminal.charMeasure.width * this._terminal.cols; - const height = Math.ceil(this._terminal.charMeasure.height * this._terminal.options.lineHeight) * this._terminal.rows; + const height = Math.floor(this._terminal.charMeasure.height * this._terminal.options.lineHeight) * this._terminal.rows; this._renderLayers.forEach(l => l.resize(this._terminal, width, height, false)); } public onCharSizeChanged(charWidth: number, charHeight: number): void { const width = charWidth * this._terminal.cols; - const height = charHeight * this._terminal.rows; + const height = Math.floor(charHeight * this._terminal.options.lineHeight) * this._terminal.rows; this._renderLayers.forEach(l => l.resize(this._terminal, width, height, true)); } From a3297b001773c00d68fafe5309db74fc63397762 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 3 Sep 2017 07:57:16 -0700 Subject: [PATCH 072/108] Fix viewport/background size after char size changed --- src/Terminal.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Terminal.ts b/src/Terminal.ts index d47c6bf85a..4373f5ec8b 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -608,6 +608,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.charMeasure = new CharMeasure(document, this.helperContainer); this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure); + this.charMeasure.on('charsizechanged', () => this.viewport.syncScrollArea()); this.renderer = new Renderer(this); this.on('cursormove', () => this.renderer.onCursorMove()); this.on('resize', () => this.renderer.onResize(this.cols, this.rows)); From bc3470c272e4b32f7daec09711d2892828766db4 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 3 Sep 2017 08:06:27 -0700 Subject: [PATCH 073/108] Fix default bold text --- src/renderer/BaseRenderLayer.ts | 10 ++++++++-- src/renderer/CharAtlas.ts | 12 ++++++++++-- src/renderer/ForegroundRenderLayer.ts | 2 +- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 686a1d252b..53c48b6966 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -127,7 +127,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.restore(); } - protected drawChar(terminal: ITerminal, char: string, code: number, width: number, x: number, y: number, fg: number, underline: boolean = false): void { + protected drawChar(terminal: ITerminal, char: string, code: number, width: number, x: number, y: number, fg: number, bold: boolean): void { // Clear the cell next to this character if it's wide if (width === 2) { this.clearCells(x + 1, y, 1, 1); @@ -135,7 +135,12 @@ export abstract class BaseRenderLayer implements IRenderLayer { let colorIndex = 0; if (fg < 256) { - colorIndex = fg + 1; + colorIndex = fg + 2; + } else { + // If default color and bold + if (bold) { + colorIndex = 1; + } } const isAscii = code < 256; const isBasicColor = (colorIndex > 0 && fg < 16); @@ -151,6 +156,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._drawUncachedChar(terminal, char, width, fg, x, y); } // This draws the atlas (for debugging purposes) + // this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); // this._ctx.drawImage(this._charAtlas, 0, 0); } diff --git a/src/renderer/CharAtlas.ts b/src/renderer/CharAtlas.ts index d25520d115..a92037a883 100644 --- a/src/renderer/CharAtlas.ts +++ b/src/renderer/CharAtlas.ts @@ -105,7 +105,7 @@ class CharAtlasGenerator { const cellWidth = scaledCharWidth + CHAR_ATLAS_CELL_SPACING; const cellHeight = scaledCharHeight + CHAR_ATLAS_CELL_SPACING; this._canvas.width = 255 * cellWidth; - this._canvas.height = (/*default*/1 + /*0-15*/16) * cellHeight; + this._canvas.height = (/*default+default bold*/2 + /*0-15*/16) * cellHeight; this._ctx.save(); this._ctx.fillStyle = foreground; @@ -116,14 +116,22 @@ class CharAtlasGenerator { for (let i = 0; i < 256; i++) { this._ctx.fillText(String.fromCharCode(i), i * cellWidth, 0); } + // Default color bold + this._ctx.save(); + this._ctx.font = `bold ${this._ctx.font}`; + for (let i = 0; i < 256; i++) { + this._ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight); + } + this._ctx.restore(); // Colors 0-15 + this._ctx.font = `${fontSize * window.devicePixelRatio}px ${fontFamily}`; for (let colorIndex = 0; colorIndex < 16; colorIndex++) { // colors 8-15 are bold if (colorIndex === 8) { this._ctx.font = `bold ${this._ctx.font}`; } - const y = (colorIndex + 1) * cellHeight; + const y = (colorIndex + 2) * cellHeight; // Draw ascii characters for (let i = 0; i < 256; i++) { this._ctx.fillStyle = ansiColors[colorIndex]; diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index 8ee81f02db..907f686d8a 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -110,7 +110,7 @@ export class ForegroundRenderLayer extends BaseRenderLayer { this.drawBottomLineAtCell(x, y); } - this.drawChar(terminal, char, code, width, x, y, fg); + this.drawChar(terminal, char, code, width, x, y, fg, !!(flags & FLAGS.BOLD)); this._ctx.restore(); } From 48b37453ea3796a9dc9de81deccef2a5349cbd49 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 3 Sep 2017 08:40:00 -0700 Subject: [PATCH 074/108] Ensure char measure's line height isn't influenced by outside --- src/utils/CharMeasure.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/CharMeasure.ts b/src/utils/CharMeasure.ts index 1366bb631f..37d6697aa9 100644 --- a/src/utils/CharMeasure.ts +++ b/src/utils/CharMeasure.ts @@ -38,6 +38,7 @@ export class CharMeasure extends EventEmitter implements ICharMeasure { this._measureElement.style.position = 'absolute'; this._measureElement.style.top = '0'; this._measureElement.style.left = '-9999em'; + this._measureElement.style.lineHeight = 'normal'; this._measureElement.textContent = 'W'; this._measureElement.setAttribute('aria-hidden', 'true'); this._parentElement.appendChild(this._measureElement); From 2d1a1bfb76974219e6a9401e709c2acad501dd15 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 3 Sep 2017 09:05:26 -0700 Subject: [PATCH 075/108] Fix inconsistency with lineHeight --- src/renderer/BaseRenderLayer.ts | 4 ++-- src/renderer/Renderer.ts | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 53c48b6966..462c4eca96 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -57,7 +57,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void { this.scaledCharWidth = terminal.charMeasure.width * window.devicePixelRatio; this.scaledCharHeight = terminal.charMeasure.height * window.devicePixelRatio; - this.scaledLineHeight = Math.ceil(this.scaledCharHeight * terminal.options.lineHeight); + this.scaledLineHeight = Math.floor(this.scaledCharHeight * terminal.options.lineHeight); this.scaledLineDrawY = terminal.options.lineHeight === 1 ? 0 : Math.round((this.scaledLineHeight - this.scaledCharHeight) / 2); this._canvas.width = canvasWidth * window.devicePixelRatio; this._canvas.height = canvasHeight * window.devicePixelRatio; @@ -143,7 +143,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { } } const isAscii = code < 256; - const isBasicColor = (colorIndex > 0 && fg < 16); + const isBasicColor = (colorIndex > 1 && fg < 16); const isDefaultColor = fg >= 256; if (isAscii && (isBasicColor || isDefaultColor)) { // ImageBitmap's draw about twice as fast as from a canvas diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 58f31ff76a..bc0717dc46 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -47,6 +47,9 @@ export class Renderer { } public onResize(cols: number, rows: number): void { + if (!this._terminal.charMeasure.width || !this._terminal.charMeasure.height) { + return; + } const width = this._terminal.charMeasure.width * this._terminal.cols; const height = Math.floor(this._terminal.charMeasure.height * this._terminal.options.lineHeight) * this._terminal.rows; this._renderLayers.forEach(l => l.resize(this._terminal, width, height, false)); From 5619abe12dd4cdaf7a496391bd58a3ee3cb75700 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 3 Sep 2017 11:20:29 -0700 Subject: [PATCH 076/108] Basic link support using mouse zone manager --- src/Linkifier.ts | 234 ++++++++-------------------------- src/Terminal.ts | 59 +++++---- src/Types.ts | 3 +- src/input/Interfaces.ts | 11 ++ src/input/MouseZoneManager.ts | 85 ++++++++++++ 5 files changed, 184 insertions(+), 208 deletions(-) create mode 100644 src/input/Interfaces.ts create mode 100644 src/input/MouseZoneManager.ts diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 5a0fc1e12b..bfac87abbe 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -2,8 +2,10 @@ * @license MIT */ -import { ILinkMatcherOptions } from './Interfaces'; -import { LinkMatcher, LinkMatcherHandler, LinkMatcherValidationCallback } from './Types'; +import { ILinkMatcherOptions, ITerminal } from './Interfaces'; +import { LinkMatcher, LinkMatcherHandler, LinkMatcherValidationCallback, LineData } from './Types'; +import { IMouseZoneManager } from './input/Interfaces'; +import { MouseZone } from './input/MouseZoneManager'; const INVALID_LINK_CLASS = 'xterm-invalid-link'; @@ -44,13 +46,14 @@ export class Linkifier { protected _linkMatchers: LinkMatcher[]; - private _document: Document; - private _rows: HTMLElement[]; + private _mouseZoneManager: IMouseZoneManager; private _rowTimeoutIds: number[]; private _rowsTimeoutId: number; private _nextLinkMatcherId = HYPERTEXT_LINK_MATCHER_ID; - constructor() { + constructor( + private _terminal: ITerminal + ) { this._rowTimeoutIds = []; this._linkMatchers = []; this.registerLinkMatcher(strictUrlRegex, null, { matchIndex: 1 }); @@ -61,22 +64,21 @@ export class Linkifier { * @param document The document object. * @param rows The array of rows to apply links to. */ - public attachToDom(document: Document, rows: HTMLElement[]): void { - this._document = document; - this._rows = rows; + public attachToDom(mouseZoneManager: IMouseZoneManager): void { + console.log('attachToDom', mouseZoneManager); + this._mouseZoneManager = mouseZoneManager; } public linkifyRows(start: number, end: number): void { - // for (let i = start; i <= end; i++) { - // this.linkifyRow(i); - // } - - // Don't attempt linkify if not yet attached to DOM - if (!this._document) { + if (!this._mouseZoneManager) { return; } + // Clear out any existing links + this._mouseZoneManager.clearAll(); + // TODO: Cancel any validation callbacks + if (this._rowsTimeoutId) { clearTimeout(this._rowsTimeoutId); } @@ -95,7 +97,7 @@ export class Linkifier { */ public linkifyRow(rowIndex: number): void { // Don't attempt linkify if not yet attached to DOM - if (!this._document) { + if (!this._mouseZoneManager) { return; } @@ -194,194 +196,68 @@ export class Linkifier { * @param {number} rowIndex The index of the row to linkify. */ private _linkifyRow(rowIndex: number): void { - const row = this._rows[rowIndex]; - if (!row) { + const absoluteRowIndex = this._terminal.buffer.ydisp + rowIndex; + if (absoluteRowIndex >= this._terminal.buffer.lines.length) { return; } - const text = row.textContent; + const text = this._terminal.buffer.translateBufferLineToString(absoluteRowIndex, false); for (let i = 0; i < this._linkMatchers.length; i++) { - const matcher = this._linkMatchers[i]; - const linkElements = this._doLinkifyRow(row, matcher); - if (linkElements.length > 0) { - // Fire validation callback - if (matcher.validationCallback) { - for (let j = 0; j < linkElements.length; j++) { - const element = linkElements[j]; - matcher.validationCallback(element.textContent, element, isValid => { - if (!isValid) { - element.classList.add(INVALID_LINK_CLASS); - } - }); - } - } - // Only allow a single LinkMatcher to trigger on any given row. - return; - } + this._doLinkifyRow(rowIndex, text, this._linkMatchers[i]); } } /** * Linkifies a row given a specific handler. - * @param {HTMLElement} row The row to linkify. + * @param rowIndex The row index to linkify. + * @param text The text of the row. * @param {LinkMatcher} matcher The link matcher for this line. * @return The link element(s) that were added. */ - private _doLinkifyRow(row: HTMLElement, matcher: LinkMatcher): HTMLElement[] { + private _doLinkifyRow(rowIndex: number, text: string, matcher: LinkMatcher): void { // Iterate over nodes as we want to consider text nodes let result = []; const isHttpLinkMatcher = matcher.id === HYPERTEXT_LINK_MATCHER_ID; - const nodes = row.childNodes; - +console.log('linkifyRow: ', text); // Find the first match - let match = row.textContent.match(matcher.regex); + let match = text.match(matcher.regex); if (!match || match.length === 0) { - return result; + return; } let uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex]; - // Set the next searches start index - let rowStartIndex = match.index + uri.length; - - for (let i = 0; i < nodes.length; i++) { - const node = nodes[i]; - const searchIndex = node.textContent.indexOf(uri); - if (searchIndex >= 0) { - const linkElement = this._createAnchorElement(uri, matcher.handler, isHttpLinkMatcher); - if (node.textContent.length === uri.length) { - // Matches entire string - if (node.nodeType === 3 /*Node.TEXT_NODE*/) { - this._replaceNode(node, linkElement); - } else { - const element = (node); - if (element.nodeName === 'A') { - // This row has already been linkified - return result; - } - element.innerHTML = ''; - element.appendChild(linkElement); - } - } else if (node.childNodes.length > 1) { - // Matches part of string in an element with multiple child nodes - for (let j = 0; j < node.childNodes.length; j++) { - const childNode = node.childNodes[j]; - const childSearchIndex = childNode.textContent.indexOf(uri); - if (childSearchIndex !== -1) { - // Match found in currentNode - this._replaceNodeSubstringWithNode(childNode, linkElement, uri, childSearchIndex); - // Don't need to count nodesAdded by replacing the node as this - // is a child node, not a top-level node. - break; - } - } - } else { - // Matches part of string in a single text node - const nodesAdded = this._replaceNodeSubstringWithNode(node, linkElement, uri, searchIndex); - // No need to consider the new nodes - i += nodesAdded; - } - result.push(linkElement); - - // Find the next match - match = row.textContent.substring(rowStartIndex).match(matcher.regex); - if (!match || match.length === 0) { - return result; - } - uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex]; - rowStartIndex += match.index + uri.length; - } - } - return result; - } - /** - * Creates a link anchor element. - * @param {string} uri The uri of the link. - * @return {HTMLAnchorElement} The link. - */ - private _createAnchorElement(uri: string, handler: LinkMatcherHandler, isHypertextLinkHandler: boolean): HTMLAnchorElement { - const element = this._document.createElement('a'); - element.textContent = uri; - element.draggable = false; - if (isHypertextLinkHandler) { - element.href = uri; - // Force link on another tab so work is not lost - element.target = '_blank'; - element.addEventListener('click', (event: MouseEvent) => { - if (handler) { - return handler(event, uri); + // TODO: Match more than one link per row + // Set the next searches start index + // let rowStartIndex = match.index + uri.length; +console.log('matcher', matcher); + // Ensure the link is valid before registering + if (matcher.validationCallback) { + matcher.validationCallback(text, isValid => { + if (isValid) { + // TODO: Discard link if the line has already changed? + this._addLink(match.index, match.length, uri, matcher); } }); } else { - element.addEventListener('click', (event: MouseEvent) => { - // Don't execute the handler if the link is flagged as invalid - if (element.classList.contains(INVALID_LINK_CLASS)) { - return; - } - return handler(event, uri); - }); - } - return element; - } - - /** - * Replace a node with 1 or more other nodes. - * @param {Node} oldNode The node to replace. - * @param {Node[]} newNodes The new nodes to insert in order. - */ - private _replaceNode(oldNode: Node, ...newNodes: Node[]): void { - const parent = oldNode.parentNode; - for (let i = 0; i < newNodes.length; i++) { - parent.insertBefore(newNodes[i], oldNode); + this._addLink(match.index, match.length, uri, matcher); } - parent.removeChild(oldNode); } - /** - * Replace a substring within a node with a new node. - * @param {Node} targetNode The target node; either a text node or a - * containing a single text node. - * @param {Node} newNode The new node to insert. - * @param {string} substring The substring to replace. - * @param {number} substringIndex The index of the substring within the string. - * @return The number of nodes to skip when searching for the next uri. - */ - private _replaceNodeSubstringWithNode(targetNode: Node, newNode: Node, substring: string, substringIndex: number): number { - // If the targetNode is a non-text node with a single child, make the child - // the new targetNode. - if (targetNode.childNodes.length === 1) { - targetNode = targetNode.childNodes[0]; - } - - // The targetNode will be either a text node or a . The text node - // (targetNode or its only-child) needs to be replaced with newNode plus new - // text nodes potentially on either side. - if (targetNode.nodeType !== 3/*Node.TEXT_NODE*/) { - throw new Error('targetNode must be a text node or only contain a single text node'); - } - - const fullText = targetNode.textContent; - - if (substringIndex === 0) { - // Replace with - const rightText = fullText.substring(substring.length); - const rightTextNode = this._document.createTextNode(rightText); - this._replaceNode(targetNode, newNode, rightTextNode); - return 0; - } - - if (substringIndex === targetNode.textContent.length - substring.length) { - // Replace with - const leftText = fullText.substring(0, substringIndex); - const leftTextNode = this._document.createTextNode(leftText); - this._replaceNode(targetNode, leftTextNode, newNode); - return 0; - } - - // Replace with - const leftText = fullText.substring(0, substringIndex); - const leftTextNode = this._document.createTextNode(leftText); - const rightText = fullText.substring(substringIndex + substring.length); - const rightTextNode = this._document.createTextNode(rightText); - this._replaceNode(targetNode, leftTextNode, newNode, rightTextNode); - return 1; + private _addLink(x: number, length: number, uri: string, matcher: LinkMatcher): void { + this._mouseZoneManager.add(new MouseZone( + x, + x + length, + e => { + if (matcher.hoverCallback) { + return matcher.hoverCallback(e, uri); + } + console.log('hover', uri); + }, + e => { + if (matcher.handler) { + return matcher.handler(e, uri); + } + window.open(uri, '_blink'); + } + )); } } diff --git a/src/Terminal.ts b/src/Terminal.ts index 4373f5ec8b..6f93ca2df3 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -29,9 +29,8 @@ import { CircularList } from './utils/CircularList'; import { C0 } from './EscapeSequences'; import { InputHandler } from './InputHandler'; import { Parser } from './Parser'; -// import { Renderer } from './Renderer'; import { Renderer } from './renderer/Renderer'; -// import { Linkifier } from './Linkifier'; +import { Linkifier } from './Linkifier'; import { SelectionManager } from './SelectionManager'; import { CharMeasure } from './utils/CharMeasure'; import * as Browser from './utils/Browser'; @@ -42,6 +41,8 @@ import { CustomKeyEventHandler, Charset, LinkMatcherHandler, LinkMatcherValidati import { ITerminal, IBrowser, ITerminalOptions, IInputHandlingTerminal, ILinkMatcherOptions, IViewport, ICompositionHelper, ITheme } from './Interfaces'; import { BellSound } from './utils/Sounds'; import { DEFAULT_ANSI_COLORS } from './renderer/ColorManager'; +import { IMouseZoneManager } from './input/Interfaces'; +import { MouseZoneManager } from './input/MouseZoneManager'; // Declare for RequireJS in loadAddon declare var define: any; @@ -184,12 +185,13 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT private parser: Parser; private renderer: Renderer; public selectionManager: SelectionManager; - // private linkifier: Linkifier; + private linkifier: Linkifier; public buffers: BufferSet; public buffer: Buffer; public viewport: IViewport; private compositionHelper: ICompositionHelper; public charMeasure: CharMeasure; + private _mouseZoneManager: IMouseZoneManager; public cols: number; public rows: number; @@ -281,7 +283,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // Reuse renderer if the Terminal is being recreated via a reset call. this.renderer = this.renderer || null; this.selectionManager = this.selectionManager || null; - // this.linkifier = this.linkifier || new Linkifier(); + this.linkifier = this.linkifier || new Linkifier(this); + this._mouseZoneManager = this._mouseZoneManager || null; // Create the terminal's buffers and set the current buffer this.buffers = new BufferSet(this); @@ -576,8 +579,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // preload audio this.syncBellSound(); - // TODO: Re-enable linkifier - // this.linkifier.attachToDom(document, this.children); + this._mouseZoneManager = new MouseZoneManager(this); + this.linkifier.attachToDom(this._mouseZoneManager); // Create the container that will hold helpers like the textarea for // capturing DOM Events. Then produce the helpers. @@ -1005,9 +1008,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * @param {number} end The row to end at (between start and this.rows - 1). */ private queueLinkification(start: number, end: number): void { - // if (this.linkifier) { - // this.linkifier.linkifyRows(0, this.rows); - // } + if (this.linkifier) { + this.linkifier.linkifyRows(0, this.rows); + } } /** @@ -1215,11 +1218,11 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * @param handler The handler callback function. */ public setHypertextLinkHandler(handler: LinkMatcherHandler): void { - // if (!this.linkifier) { - // throw new Error('Cannot attach a hypertext link handler before Terminal.open is called'); - // } - // this.linkifier.setHypertextLinkHandler(handler); - // // Refresh to force links to refresh + if (!this.linkifier) { + throw new Error('Cannot attach a hypertext link handler before Terminal.open is called'); + } + this.linkifier.setHypertextLinkHandler(handler); + // Refresh to force links to refresh // this.refresh(0, this.rows - 1); } @@ -1230,10 +1233,10 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * be cleared with null. */ public setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void { - // if (!this.linkifier) { - // throw new Error('Cannot attach a hypertext validation callback before Terminal.open is called'); - // } - // this.linkifier.setHypertextValidationCallback(callback); + if (!this.linkifier) { + throw new Error('Cannot attach a hypertext validation callback before Terminal.open is called'); + } + this.linkifier.setHypertextValidationCallback(callback); // // Refresh to force links to refresh // this.refresh(0, this.rows - 1); } @@ -1249,11 +1252,11 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * @return The ID of the new matcher, this can be used to deregister. */ public registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number { - // if (this.linkifier) { - // const matcherId = this.linkifier.registerLinkMatcher(regex, handler, options); - // this.refresh(0, this.rows - 1); - // return matcherId; - // } + if (this.linkifier) { + const matcherId = this.linkifier.registerLinkMatcher(regex, handler, options); + // this.refresh(0, this.rows - 1); + return matcherId; + } return 0; } @@ -1262,11 +1265,11 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * @param matcherId The link matcher's ID (returned after register) */ public deregisterLinkMatcher(matcherId: number): void { - // if (this.linkifier) { - // if (this.linkifier.deregisterLinkMatcher(matcherId)) { - // this.refresh(0, this.rows - 1); - // } - // } + if (this.linkifier) { + if (this.linkifier.deregisterLinkMatcher(matcherId)) { + // this.refresh(0, this.rows - 1); + } + } } /** diff --git a/src/Types.ts b/src/Types.ts index 180dc5d67c..378a66acb4 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -6,12 +6,13 @@ export type LinkMatcher = { id: number, regex: RegExp, handler: LinkMatcherHandler, + hoverCallback?: LinkMatcherHandler, matchIndex?: number, validationCallback?: LinkMatcherValidationCallback, priority?: number }; export type LinkMatcherHandler = (event: MouseEvent, uri: string) => boolean | void; -export type LinkMatcherValidationCallback = (uri: string, element: HTMLElement, callback: (isValid: boolean) => void) => void; +export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void; export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean; export type Charset = {[key: string]: string}; diff --git a/src/input/Interfaces.ts b/src/input/Interfaces.ts new file mode 100644 index 0000000000..9bb90fed67 --- /dev/null +++ b/src/input/Interfaces.ts @@ -0,0 +1,11 @@ +export interface IMouseZoneManager { + add(zone: IMouseZone): void; + clearAll(): void; +} + +export interface IMouseZone { + x1: number; + x2: number; + hoverCallback: (e: MouseEvent) => any; + clickCallback: (e: MouseEvent) => any; +} diff --git a/src/input/MouseZoneManager.ts b/src/input/MouseZoneManager.ts new file mode 100644 index 0000000000..7e63ee0bb0 --- /dev/null +++ b/src/input/MouseZoneManager.ts @@ -0,0 +1,85 @@ +import { IMouseZoneManager, IMouseZone } from './Interfaces'; +import { ITerminal } from '../Interfaces'; + +/** + * The MouseZoneManager allows components to register zones within the terminal + * that trigger hover and click callbacks. + * + * This class was intentionally made not so robust initially as the only case it + * needed to support was single-line links which never overlap. Improvements can + * be made in the future. + */ +export class MouseZoneManager implements IMouseZoneManager { + private _zones: IMouseZone[] = []; + + private _areZonesActive: boolean = false; + private _mouseMoveListener: (e: MouseEvent) => any; + private _mouseDownListener: (e: MouseEvent) => any; + private _clickListener: (e: MouseEvent) => any; + + constructor( + private _terminal: ITerminal + ) { + // These events are expensive, only listen to it when mouse zones are active + this._mouseMoveListener = e => this._onMouseMove(e); + this._clickListener = e => this._onClick(e); + } + + public add(zone: IMouseZone): void { + console.log('add zone', zone); + this._zones.push(zone); + if (this._zones.length === 1) { + this._activate(); + } + } + + public clearAll(): void { + this._zones.length = 0; + this._deactivate(); + } + + private _activate(): void { + if (!this._areZonesActive) { + console.log('_addMoveListener'); + this._areZonesActive = true; + this._terminal.element.addEventListener('mousemove', this._mouseMoveListener); + this._terminal.element.addEventListener('click', this._clickListener); + } + } + + private _deactivate(): void { + if (this._areZonesActive) { + console.log('_removeMoveListener'); + this._areZonesActive = false; + this._terminal.element.removeEventListener('mousemove', this._mouseMoveListener); + this._terminal.element.removeEventListener('click', this._clickListener); + } + } + + private _onMouseMove(e: MouseEvent): void { + console.log('move'); + // TODO: Handle hover + } + + private _onClick(e: MouseEvent): void { + const zone = this._findZoneEventAt(e); + if (zone) { + zone.clickCallback(e); + e.preventDefault(); + } + } + + private _findZoneEventAt(e: MouseEvent): IMouseZone { + return this._zones[0]; + } +} + +export class MouseZone implements IMouseZone { + constructor( + public x1: number, + public x2: number, + public hoverCallback: (e: MouseEvent) => any, + public clickCallback: (e: MouseEvent) => any + ) { + } +} From 923ea7247c1e2a80ad29c9078c27e50356f8a471 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 3 Sep 2017 11:39:17 -0700 Subject: [PATCH 077/108] Get links working on proper coords --- src/Linkifier.ts | 19 +++++++++++-------- src/input/Interfaces.ts | 1 + src/input/MouseZoneManager.ts | 15 ++++++++++----- src/utils/Mouse.ts | 6 +++--- 4 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/Linkifier.ts b/src/Linkifier.ts index bfac87abbe..f92badcf7c 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -217,7 +217,7 @@ export class Linkifier { // Iterate over nodes as we want to consider text nodes let result = []; const isHttpLinkMatcher = matcher.id === HYPERTEXT_LINK_MATCHER_ID; -console.log('linkifyRow: ', text); + // Find the first match let match = text.match(matcher.regex); if (!match || match.length === 0) { @@ -228,29 +228,32 @@ console.log('linkifyRow: ', text); // TODO: Match more than one link per row // Set the next searches start index // let rowStartIndex = match.index + uri.length; -console.log('matcher', matcher); + + // Get index, match.index is for the outer match which includes negated chars + const index = text.indexOf(uri); + // Ensure the link is valid before registering if (matcher.validationCallback) { matcher.validationCallback(text, isValid => { if (isValid) { // TODO: Discard link if the line has already changed? - this._addLink(match.index, match.length, uri, matcher); + this._addLink(index, rowIndex, uri, matcher); } }); } else { - this._addLink(match.index, match.length, uri, matcher); + this._addLink(index, rowIndex, uri, matcher); } } - private _addLink(x: number, length: number, uri: string, matcher: LinkMatcher): void { + private _addLink(x: number, y: number, uri: string, matcher: LinkMatcher): void { this._mouseZoneManager.add(new MouseZone( - x, - x + length, + x + 1, + x + 1 + uri.length, + y + 1, e => { if (matcher.hoverCallback) { return matcher.hoverCallback(e, uri); } - console.log('hover', uri); }, e => { if (matcher.handler) { diff --git a/src/input/Interfaces.ts b/src/input/Interfaces.ts index 9bb90fed67..d0c0e48044 100644 --- a/src/input/Interfaces.ts +++ b/src/input/Interfaces.ts @@ -6,6 +6,7 @@ export interface IMouseZoneManager { export interface IMouseZone { x1: number; x2: number; + y: number; hoverCallback: (e: MouseEvent) => any; clickCallback: (e: MouseEvent) => any; } diff --git a/src/input/MouseZoneManager.ts b/src/input/MouseZoneManager.ts index 7e63ee0bb0..6d8f017a4e 100644 --- a/src/input/MouseZoneManager.ts +++ b/src/input/MouseZoneManager.ts @@ -1,5 +1,6 @@ import { IMouseZoneManager, IMouseZone } from './Interfaces'; import { ITerminal } from '../Interfaces'; +import { getCoords } from '../utils/Mouse'; /** * The MouseZoneManager allows components to register zones within the terminal @@ -26,7 +27,6 @@ export class MouseZoneManager implements IMouseZoneManager { } public add(zone: IMouseZone): void { - console.log('add zone', zone); this._zones.push(zone); if (this._zones.length === 1) { this._activate(); @@ -40,7 +40,6 @@ export class MouseZoneManager implements IMouseZoneManager { private _activate(): void { if (!this._areZonesActive) { - console.log('_addMoveListener'); this._areZonesActive = true; this._terminal.element.addEventListener('mousemove', this._mouseMoveListener); this._terminal.element.addEventListener('click', this._clickListener); @@ -49,7 +48,6 @@ export class MouseZoneManager implements IMouseZoneManager { private _deactivate(): void { if (this._areZonesActive) { - console.log('_removeMoveListener'); this._areZonesActive = false; this._terminal.element.removeEventListener('mousemove', this._mouseMoveListener); this._terminal.element.removeEventListener('click', this._clickListener); @@ -57,7 +55,6 @@ export class MouseZoneManager implements IMouseZoneManager { } private _onMouseMove(e: MouseEvent): void { - console.log('move'); // TODO: Handle hover } @@ -70,7 +67,14 @@ export class MouseZoneManager implements IMouseZoneManager { } private _findZoneEventAt(e: MouseEvent): IMouseZone { - return this._zones[0]; + const coords = getCoords(e, this._terminal.element, this._terminal.charMeasure,this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows); + for (let i = 0; i < this._zones.length; i++) { + const zone = this._zones[i]; + if (zone.y === coords[1] && zone.x1 <= coords[0] && zone.x2 > coords[0]) { + return zone; + } + }; + return null; } } @@ -78,6 +82,7 @@ export class MouseZone implements IMouseZone { constructor( public x1: number, public x2: number, + public y: number, public hoverCallback: (e: MouseEvent) => any, public clickCallback: (e: MouseEvent) => any ) { diff --git a/src/utils/Mouse.ts b/src/utils/Mouse.ts index 77c422d5e7..0d433cd6f6 100644 --- a/src/utils/Mouse.ts +++ b/src/utils/Mouse.ts @@ -2,7 +2,7 @@ * @license MIT */ -import { CharMeasure } from './CharMeasure'; +import { ICharMeasure } from '../Interfaces'; export function getCoordsRelativeToElement(event: MouseEvent, element: HTMLElement): [number, number] { // Ignore browsers that don't support MouseEvent.pageX @@ -36,7 +36,7 @@ export function getCoordsRelativeToElement(event: MouseEvent, element: HTMLEleme * apply an offset to the x value such that the left half of the cell will * select that cell and the right half will select the next cell. */ -export function getCoords(event: MouseEvent, element: HTMLElement, charMeasure: CharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number] { +export function getCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number] { // Coordinates cannot be measured if charMeasure has not been initialized if (!charMeasure.width || !charMeasure.height) { return null; @@ -68,7 +68,7 @@ export function getCoords(event: MouseEvent, element: HTMLElement, charMeasure: * @param colCount The number of columns in the terminal. * @param rowCount The number of rows in the terminal. */ -export function getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: CharMeasure, lineHeight: number, colCount: number, rowCount: number): { x: number, y: number } { +export function getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number): { x: number, y: number } { const coords = getCoords(event, element, charMeasure, lineHeight, colCount, rowCount); let x = coords[0]; let y = coords[1]; From b30cb58ff42419e406c6abb1492101f7fb4e6c21 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 3 Sep 2017 12:47:04 -0700 Subject: [PATCH 078/108] Support hover callback in links --- src/Interfaces.ts | 4 ++++ src/Linkifier.ts | 12 ++++++------ src/Terminal.ts | 8 ++++---- src/input/Interfaces.ts | 2 +- src/input/MouseZoneManager.ts | 27 +++++++++++++++++++++++---- 5 files changed, 38 insertions(+), 15 deletions(-) diff --git a/src/Interfaces.ts b/src/Interfaces.ts index 7ebf698378..ace4012e5e 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -236,6 +236,10 @@ export interface ILinkMatcherOptions { * false if invalid. */ validationCallback?: LinkMatcherValidationCallback; + /** + * A callback that fired when the mouse hovers over a link. + */ + hoverCallback?: LinkMatcherHandler; /** * The priority of the link matcher, this defines the order in which the link * matcher is evaluated relative to others, from highest to lowest. The diff --git a/src/Linkifier.ts b/src/Linkifier.ts index f92badcf7c..20c5b713e2 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -65,7 +65,6 @@ export class Linkifier { * @param rows The array of rows to apply links to. */ public attachToDom(mouseZoneManager: IMouseZoneManager): void { - console.log('attachToDom', mouseZoneManager); this._mouseZoneManager = mouseZoneManager; } @@ -147,6 +146,7 @@ export class Linkifier { handler, matchIndex: options.matchIndex, validationCallback: options.validationCallback, + hoverCallback: options.hoverCallback, priority: options.priority || 0 }; this._addLinkMatcherToList(matcher); @@ -250,16 +250,16 @@ export class Linkifier { x + 1, x + 1 + uri.length, y + 1, - e => { - if (matcher.hoverCallback) { - return matcher.hoverCallback(e, uri); - } - }, e => { if (matcher.handler) { return matcher.handler(e, uri); } window.open(uri, '_blink'); + }, + e => { + if (matcher.hoverCallback) { + return matcher.hoverCallback(e, uri); + } } )); } diff --git a/src/Terminal.ts b/src/Terminal.ts index 6f93ca2df3..c3d360bb93 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1223,7 +1223,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT } this.linkifier.setHypertextLinkHandler(handler); // Refresh to force links to refresh - // this.refresh(0, this.rows - 1); + this.refresh(0, this.rows - 1); } /** @@ -1238,7 +1238,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT } this.linkifier.setHypertextValidationCallback(callback); // // Refresh to force links to refresh - // this.refresh(0, this.rows - 1); + this.refresh(0, this.rows - 1); } /** @@ -1254,7 +1254,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT public registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number { if (this.linkifier) { const matcherId = this.linkifier.registerLinkMatcher(regex, handler, options); - // this.refresh(0, this.rows - 1); + this.refresh(0, this.rows - 1); return matcherId; } return 0; @@ -1267,7 +1267,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT public deregisterLinkMatcher(matcherId: number): void { if (this.linkifier) { if (this.linkifier.deregisterLinkMatcher(matcherId)) { - // this.refresh(0, this.rows - 1); + this.refresh(0, this.rows - 1); } } } diff --git a/src/input/Interfaces.ts b/src/input/Interfaces.ts index d0c0e48044..15dcbae9b1 100644 --- a/src/input/Interfaces.ts +++ b/src/input/Interfaces.ts @@ -7,6 +7,6 @@ export interface IMouseZone { x1: number; x2: number; y: number; - hoverCallback: (e: MouseEvent) => any; clickCallback: (e: MouseEvent) => any; + hoverCallback?: (e: MouseEvent) => any; } diff --git a/src/input/MouseZoneManager.ts b/src/input/MouseZoneManager.ts index 6d8f017a4e..37a6647d35 100644 --- a/src/input/MouseZoneManager.ts +++ b/src/input/MouseZoneManager.ts @@ -2,6 +2,8 @@ import { IMouseZoneManager, IMouseZone } from './Interfaces'; import { ITerminal } from '../Interfaces'; import { getCoords } from '../utils/Mouse'; +const HOVER_DURATION = 500; + /** * The MouseZoneManager allows components to register zones within the terminal * that trigger hover and click callbacks. @@ -18,6 +20,9 @@ export class MouseZoneManager implements IMouseZoneManager { private _mouseDownListener: (e: MouseEvent) => any; private _clickListener: (e: MouseEvent) => any; + private _hoverTimeout: number; + private _lastHoverCoords: [number, number] = [null, null]; + constructor( private _terminal: ITerminal ) { @@ -55,7 +60,21 @@ export class MouseZoneManager implements IMouseZoneManager { } private _onMouseMove(e: MouseEvent): void { - // TODO: Handle hover + if (this._lastHoverCoords[0] !== e.pageX && this._lastHoverCoords[1] !== e.pageY) { + if (this._hoverTimeout) { + clearTimeout(this._hoverTimeout); + } + this._hoverTimeout = setTimeout(() => this._onHover(e), HOVER_DURATION); + this._lastHoverCoords = [e.pageX, e.pageY]; + } + } + + private _onHover(e: MouseEvent): void { + const coords = getCoords(e, this._terminal.element, this._terminal.charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows); + const zone = this._findZoneEventAt(e); + if (zone && zone.hoverCallback) { + zone.hoverCallback(e); + } } private _onClick(e: MouseEvent): void { @@ -67,7 +86,7 @@ export class MouseZoneManager implements IMouseZoneManager { } private _findZoneEventAt(e: MouseEvent): IMouseZone { - const coords = getCoords(e, this._terminal.element, this._terminal.charMeasure,this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows); + const coords = getCoords(e, this._terminal.element, this._terminal.charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows); for (let i = 0; i < this._zones.length; i++) { const zone = this._zones[i]; if (zone.y === coords[1] && zone.x1 <= coords[0] && zone.x2 > coords[0]) { @@ -83,8 +102,8 @@ export class MouseZone implements IMouseZone { public x1: number, public x2: number, public y: number, - public hoverCallback: (e: MouseEvent) => any, - public clickCallback: (e: MouseEvent) => any + public clickCallback: (e: MouseEvent) => any, + public hoverCallback?: (e: MouseEvent) => any ) { } } From 17731c3de36037baa27c33c705b632984cd69fe0 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 3 Sep 2017 13:56:53 -0700 Subject: [PATCH 079/108] Support multiple links in a line, fix Linkifier tests --- src/Buffer.ts | 2 +- src/InputHandler.ts | 8 +- src/Interfaces.ts | 7 +- src/Linkifier.test.ts | 163 +++++++++++++++++++---------------- src/Linkifier.ts | 21 +++-- src/SelectionManager.test.ts | 32 +++---- src/Types.ts | 3 +- src/utils/TestUtils.test.ts | 9 +- typings/xterm.d.ts | 5 ++ 9 files changed, 136 insertions(+), 114 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 6931eea453..65b7d58e53 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -114,7 +114,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]; // does xterm use the default attr? + const ch: CharData = [this._terminal.defAttr, ' ', 1, 32]; // does xterm use the default attr? for (let i = 0; i < this._lines.length; i++) { // TODO: This should be removed, with tests setup for the case that was // causing the underlying bug, see https://github.com/sourcelair/xterm.js/issues/824 diff --git a/src/InputHandler.ts b/src/InputHandler.ts index e9346c1a0c..3bf16a1c38 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -189,7 +189,7 @@ export class InputHandler implements IInputHandler { const row = this._terminal.buffer.y + this._terminal.buffer.ybase; let j = this._terminal.buffer.x; - const ch: CharData = [this._terminal.eraseAttr(), ' ', 1]; // xterm + const ch: CharData = [this._terminal.eraseAttr(), ' ', 1, 32]; // xterm while (param-- && j < this._terminal.cols) { this._terminal.buffer.lines.get(row).splice(j++, 0, ch); @@ -488,7 +488,7 @@ export class InputHandler implements IInputHandler { } const row = this._terminal.buffer.y + this._terminal.buffer.ybase; - const ch: CharData = [this._terminal.eraseAttr(), ' ', 1]; // xterm + const ch: CharData = [this._terminal.eraseAttr(), ' ', 1, 32]; // xterm while (param--) { this._terminal.buffer.lines.get(row).splice(this._terminal.buffer.x, 1); @@ -536,7 +536,7 @@ export class InputHandler implements IInputHandler { const row = this._terminal.buffer.y + this._terminal.buffer.ybase; let j = this._terminal.buffer.x; - const ch: CharData = [this._terminal.eraseAttr(), ' ', 1]; // xterm + const ch: CharData = [this._terminal.eraseAttr(), ' ', 1, 32]; // xterm while (param-- && j < this._terminal.cols) { this._terminal.buffer.lines.get(row)[j++] = ch; @@ -590,7 +590,7 @@ export class InputHandler implements IInputHandler { public repeatPrecedingCharacter(params: number[]): void { let param = params[0] || 1; const line = this._terminal.buffer.lines.get(this._terminal.buffer.ybase + this._terminal.buffer.y); - const ch = line[this._terminal.buffer.x - 1] || [this._terminal.defAttr, ' ', 1]; + const ch = line[this._terminal.buffer.x - 1] || [this._terminal.defAttr, ' ', 1, 32]; while (param--) { line[this._terminal.buffer.x++] = ch; diff --git a/src/Interfaces.ts b/src/Interfaces.ts index ace4012e5e..9ce8ffd482 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -18,7 +18,11 @@ export interface IBrowser { isMSWindows: boolean; } -export interface ITerminal extends IEventEmitter { +export interface IBufferAccessor { + buffer: IBuffer; +} + +export interface ITerminal extends IBufferAccessor, IEventEmitter { element: HTMLElement; selectionManager: ISelectionManager; charMeasure: ICharMeasure; @@ -32,7 +36,6 @@ export interface ITerminal extends IEventEmitter { defAttr: number; options: ITerminalOptions; buffers: IBufferSet; - buffer: IBuffer; isFocused: boolean; /** diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index a4ed70db26..6b28acb94c 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -2,42 +2,84 @@ * @license MIT */ -import jsdom = require('jsdom'); import { assert } from 'chai'; -import { ITerminal, ILinkifier } from './Interfaces'; +import { ITerminal, ILinkifier, IBuffer, IBufferAccessor } from './Interfaces'; import { Linkifier } from './Linkifier'; -import { LinkMatcher } from './Types'; +import { LinkMatcher, LineData } from './Types'; +import { IMouseZoneManager, IMouseZone } from './input/Interfaces'; +import { MockBuffer } from './utils/TestUtils.test'; +import { CircularList } from './utils/CircularList'; class TestLinkifier extends Linkifier { - constructor() { + constructor(bufferAccessor: IBufferAccessor) { Linkifier.TIME_BEFORE_LINKIFY = 0; - super(); + super(bufferAccessor); } public get linkMatchers(): LinkMatcher[] { return this._linkMatchers; } } -describe('Linkifier', () => { - let dom: jsdom.JSDOM; - let window: Window; - let document: Document; +class TestMouseZoneManager implements IMouseZoneManager { + public clears: number = 0; + public zones: IMouseZone[] = []; + add(zone: IMouseZone): void { + this.zones.push(zone); + } + clearAll(): void { + this.clears++; + } +} - let container: HTMLElement; - let rows: HTMLElement[]; +describe('Linkifier', () => { + let bufferAccessor: IBufferAccessor; let linkifier: TestLinkifier; + let mouseZoneManager: TestMouseZoneManager; beforeEach(() => { - dom = new jsdom.JSDOM(''); - window = dom.window; - document = window.document; - linkifier = new TestLinkifier(); + bufferAccessor = { buffer: new MockBuffer() }; + bufferAccessor.buffer.lines = new CircularList(20); + bufferAccessor.buffer.ydisp = 0; + linkifier = new TestLinkifier(bufferAccessor); + mouseZoneManager = new TestMouseZoneManager(); }); - function addRow(html: string): void { - const element = document.createElement('div'); - element.innerHTML = html; - container.appendChild(element); - rows.push(element); + function stringToRow(text: string): LineData { + let result: LineData = []; + for (let i = 0; i < text.length; i++) { + result.push([0, text.charAt(i), 1, text.charCodeAt(i)]); + } + return result; + } + + function addRow(text: string): void { + bufferAccessor.buffer.lines.push(stringToRow(text)); + } + + function assertLinkifiesEntireRow(uri: string, done: MochaDone): void { + addRow(uri); + linkifier.linkifyRow(0); + setTimeout(() => { + assert.equal(mouseZoneManager.zones[0].x1, 1); + assert.equal(mouseZoneManager.zones[0].x2, uri.length + 1); + assert.equal(mouseZoneManager.zones[0].y, bufferAccessor.buffer.lines.length); + done(); + }, 0); + } + + function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, links: {x: number, length: number}[], done: MochaDone): void { + addRow(rowText); + linkifier.registerLinkMatcher(linkMatcherRegex, () => {}); + linkifier.linkifyRow(0); + // Allow linkify to happen + setTimeout(() => { + assert.equal(mouseZoneManager.zones.length, links.length); + links.forEach((l, i) => { + assert.equal(mouseZoneManager.zones[i].x1, l.x + 1); + assert.equal(mouseZoneManager.zones[i].x2, l.x + l.length + 1); + assert.equal(mouseZoneManager.zones[i].y, bufferAccessor.buffer.lines.length); + }); + done(); + }, 0); } describe('before attachToDom', () => { @@ -52,78 +94,42 @@ describe('Linkifier', () => { describe('after attachToDom', () => { beforeEach(() => { - rows = []; - linkifier.attachToDom(document, rows); - container = document.createElement('div'); - document.body.appendChild(container); + linkifier.attachToDom(mouseZoneManager); }); - function clickElement(element: Node): void { - const event = document.createEvent('MouseEvent'); - event.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); - element.dispatchEvent(event); - } - - function assertLinkifiesEntireRow(uri: string, done: MochaDone): void { - addRow(uri); - linkifier.linkifyRow(0); - setTimeout(() => { - assert.equal((rows[0].firstChild).tagName, 'A'); - assert.equal((rows[0].firstChild).textContent, uri); - done(); - }, 0); - } - describe('http links', () => { - function assertLinkifiesEntireRow(uri: string, done: MochaDone): void { - addRow(uri); - linkifier.linkifyRow(0); - setTimeout(() => { - assert.equal((rows[0].firstChild).tagName, 'A'); - assert.equal((rows[0].firstChild).textContent, uri); - done(); - }, 0); - } - it('should allow ~ character in URI path', done => assertLinkifiesEntireRow('http://foo.com/a~b#c~d?e~f', done)); + it('should allow ~ character in URI path', (done) => { + assertLinkifiesEntireRow('http://foo.com/a~b#c~d?e~f', done); + }); }); describe('link matcher', () => { - function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, expectedHtml: string, done: MochaDone): void { - addRow(rowText); - linkifier.registerLinkMatcher(linkMatcherRegex, () => {}); - linkifier.linkifyRow(0); - // Allow linkify to happen - setTimeout(() => { - assert.equal(rows[0].innerHTML, expectedHtml); - done(); - }, 0); - } it('should match a single link', done => { - assertLinkifiesRow('foo', /foo/, 'foo', done); + assertLinkifiesRow('foo', /foo/, [{x: 0, length: 3}], done); }); it('should match a single link at the start of a text node', done => { - assertLinkifiesRow('foo bar', /foo/, 'foo bar', done); + assertLinkifiesRow('foo bar', /foo/, [{x: 0, length: 3}], done); }); it('should match a single link in the middle of a text node', done => { - assertLinkifiesRow('foo bar baz', /bar/, 'foo bar baz', done); + assertLinkifiesRow('foo bar baz', /bar/, [{x: 4, length: 3}], done); }); it('should match a single link at the end of a text node', done => { - assertLinkifiesRow('foo bar', /bar/, 'foo bar', done); + assertLinkifiesRow('foo bar', /bar/, [{x: 4, length: 3}], done); }); it('should match a link after a link at the start of a text node', done => { - assertLinkifiesRow('foo bar', /foo|bar/, 'foo bar', done); + assertLinkifiesRow('foo bar', /foo|bar/, [{x: 0, length: 3}, {x: 4, length: 3}], done); }); it('should match a link after a link in the middle of a text node', done => { - assertLinkifiesRow('foo bar baz', /bar|baz/, 'foo bar baz', done); + assertLinkifiesRow('foo bar baz', /bar|baz/, [{x: 4, length: 3}, {x: 8, length: 3}], done); }); it('should match a link immediately after a link at the end of a text node', done => { - assertLinkifiesRow('foo barbaz', /bar|baz/, 'foo barbaz', done); + assertLinkifiesRow('foo barbaz', /bar|baz/, [{x: 4, length: 3}, {x: 7, length: 3}], done); }); it('should not duplicate text after a unicode character (wrapped in a span)', done => { // This is a regression test for an issue that came about when using // an oh-my-zsh theme that added the large blue diamond unicode // character (U+1F537) which caused the path to be duplicated. See #642. - assertLinkifiesRow('echo \'🔷foo\'', /foo/, 'echo \'🔷foo\'', done); + assertLinkifiesRow('echo \'🔷foo\'', /foo/, [{x: 8, length: 3}], done); }); }); @@ -131,10 +137,15 @@ describe('Linkifier', () => { it('should enable link if true', done => { addRow('test'); linkifier.registerLinkMatcher(/test/, () => done(), { - validationCallback: (url, element, cb) => { + validationCallback: (url, cb) => { + assert.equal(mouseZoneManager.zones.length, 0); cb(true); - assert.equal((rows[0].firstChild).tagName, 'A'); - setTimeout(() => clickElement(rows[0].firstChild), 0); + assert.equal(mouseZoneManager.zones.length, 1); + assert.equal(mouseZoneManager.zones[0].x1, 1); + assert.equal(mouseZoneManager.zones[0].x2, 5); + assert.equal(mouseZoneManager.zones[0].y, 1); + // Fires done() + mouseZoneManager.zones[0].clickCallback({}); } }); linkifier.linkifyRow(0); @@ -143,14 +154,14 @@ describe('Linkifier', () => { it('should disable link if false', done => { addRow('test'); linkifier.registerLinkMatcher(/test/, () => assert.fail(), { - validationCallback: (url, element, cb) => { + validationCallback: (url, cb) => { + assert.equal(mouseZoneManager.zones.length, 0); cb(false); - assert.equal((rows[0].firstChild).tagName, 'A'); - setTimeout(() => clickElement(rows[0].firstChild), 0); + assert.equal(mouseZoneManager.zones.length, 0); } }); linkifier.linkifyRow(0); - // Allow time for the click to be performed + // Allow time for the validation callback to be performed setTimeout(() => done(), 10); }); @@ -158,7 +169,7 @@ describe('Linkifier', () => { addRow('test test'); let count = 0; linkifier.registerLinkMatcher(/test/, () => assert.fail(), { - validationCallback: (url, element, cb) => { + validationCallback: (url, cb) => { count += 1; if (count === 2) { done(); diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 20c5b713e2..cae0d6625a 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -2,7 +2,7 @@ * @license MIT */ -import { ILinkMatcherOptions, ITerminal } from './Interfaces'; +import { ILinkMatcherOptions, ITerminal, IBufferAccessor } from './Interfaces'; import { LinkMatcher, LinkMatcherHandler, LinkMatcherValidationCallback, LineData } from './Types'; import { IMouseZoneManager } from './input/Interfaces'; import { MouseZone } from './input/MouseZoneManager'; @@ -52,7 +52,7 @@ export class Linkifier { private _nextLinkMatcherId = HYPERTEXT_LINK_MATCHER_ID; constructor( - private _terminal: ITerminal + private _terminal: IBufferAccessor ) { this._rowTimeoutIds = []; this._linkMatchers = []; @@ -213,7 +213,7 @@ export class Linkifier { * @param {LinkMatcher} matcher The link matcher for this line. * @return The link element(s) that were added. */ - private _doLinkifyRow(rowIndex: number, text: string, matcher: LinkMatcher): void { + private _doLinkifyRow(rowIndex: number, text: string, matcher: LinkMatcher, offset: number = 0): void { // Iterate over nodes as we want to consider text nodes let result = []; const isHttpLinkMatcher = matcher.id === HYPERTEXT_LINK_MATCHER_ID; @@ -225,10 +225,6 @@ export class Linkifier { } let uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex]; - // TODO: Match more than one link per row - // Set the next searches start index - // let rowStartIndex = match.index + uri.length; - // Get index, match.index is for the outer match which includes negated chars const index = text.indexOf(uri); @@ -237,11 +233,18 @@ export class Linkifier { matcher.validationCallback(text, isValid => { if (isValid) { // TODO: Discard link if the line has already changed? - this._addLink(index, rowIndex, uri, matcher); + this._addLink(offset + index, rowIndex, uri, matcher); } }); } else { - this._addLink(index, rowIndex, uri, matcher); + this._addLink(offset + index, rowIndex, uri, matcher); + } + + // Recursively check for links in the rest of the text + const remainingStartIndex = index + uri.length; + const remainingText = text.substr(remainingStartIndex); + if (remainingText.length > 0) { + this._doLinkifyRow(rowIndex, remainingText, matcher, offset + remainingStartIndex); } } diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index b5a9d5529c..8c90693e99 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -60,7 +60,7 @@ describe('SelectionManager', () => { function stringToRow(text: string): LineData { let result: LineData = []; for (let i = 0; i < text.length; i++) { - result.push([0, text.charAt(i), 1]); + result.push([0, text.charAt(i), 1, text.charCodeAt(i)]); } return result; } @@ -99,21 +99,21 @@ describe('SelectionManager', () => { it('should expand selection for wide characters', () => { // Wide characters use a special format buffer.lines.set(0, [ - [null, '中', 2], - [null, '', 0], - [null, 'æ–‡', 2], - [null, '', 0], - [null, ' ', 1], - [null, 'a', 1], - [null, '中', 2], - [null, '', 0], - [null, 'æ–‡', 2], - [null, '', 0], - [null, 'b', 1], - [null, ' ', 1], - [null, 'f', 1], - [null, 'o', 1], - [null, 'o', 1] + [null, '中', 2, '中'.charCodeAt(0)], + [null, '', 0, null], + [null, 'æ–‡', 2, 'æ–‡'.charCodeAt(0)], + [null, '', 0, null], + [null, ' ', 1, ' '.charCodeAt(0)], + [null, 'a', 1, 'a'.charCodeAt(0)], + [null, '中', 2, '中'.charCodeAt(0)], + [null, '', 0, null], + [null, 'æ–‡', 2, 'æ–‡'.charCodeAt(0)], + [null, '', 0, ''.charCodeAt(0)], + [null, 'b', 1, 'b'.charCodeAt(0)], + [null, ' ', 1, ' '.charCodeAt(0)], + [null, 'f', 1, 'f'.charCodeAt(0)], + [null, 'o', 1, 'o'.charCodeAt(0)], + [null, 'o', 1, 'o'.charCodeAt(0)] ]); // Ensure wide characters take up 2 columns selectionManager.selectWordAt([0, 0]); diff --git a/src/Types.ts b/src/Types.ts index 378a66acb4..1ced3d73a8 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -17,6 +17,5 @@ export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: bo export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean; export type Charset = {[key: string]: string}; -// TODO: Add code here? -export type CharData = [number, string, number]; +export type CharData = [number, string, number, number]; export type LineData = CharData[]; diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index d4dc16ab0a..0388e9da10 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -4,6 +4,7 @@ import { ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, IListenerType, IInputHandlingTerminal, IViewport, ICircularList, ICompositionHelper } from '../Interfaces'; import { LineData } from '../Types'; +import { Buffer } from '../Buffer'; import * as Browser from './Browser'; import { IColorSet } from '../renderer/Interfaces'; @@ -61,7 +62,7 @@ export class MockTerminal implements ITerminal { const line: LineData = []; cols = cols || this.cols; for (let i = 0; i < cols; i++) { - line.push([0, ' ', 1]); + line.push([0, ' ', 1, 32]); } return line; } @@ -130,7 +131,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { eraseLeft(x: number, y: number): void { throw new Error('Method not implemented.'); } - blankLine(cur?: boolean, isWrapped?: boolean): [number, string, number][] { + blankLine(cur?: boolean, isWrapped?: boolean): [number, string, number, number][] { throw new Error('Method not implemented.'); } prevStop(x?: number): number { @@ -182,7 +183,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { export class MockBuffer implements IBuffer { isCursorInViewport: boolean; - lines: ICircularList<[number, string, number][]>; + lines: ICircularList<[number, string, number, number][]>; ydisp: number; ybase: number; y: number; @@ -193,7 +194,7 @@ export class MockBuffer implements IBuffer { savedY: number; savedX: number; translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string { - throw new Error('Method not implemented.'); + return Buffer.prototype.translateBufferLineToString.apply(this, arguments); } nextStop(x?: number): number { throw new Error('Method not implemented.'); diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index b1cea81691..d612797858 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -133,6 +133,11 @@ interface ILinkMatcherOptions { */ validationCallback?: (uri: string, element: HTMLElement, callback: (isValid: boolean) => void) => void; + /** + * A callback that fired when the mouse hovers over a link. + */ + hoverCallback?: LinkMatcherHandler; + /** * The priority of the link matcher, this defines the order in which the link * matcher is evaluated relative to others, from highest to lowest. The From e518ea04c59c3122ff99dfee427009bba511229b Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 3 Sep 2017 14:21:30 -0700 Subject: [PATCH 080/108] Get tests reporting failures again --- src/Terminal.ts | 3 +++ src/renderer/CharAtlas.ts | 16 +++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index c3d360bb93..620340171e 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -43,6 +43,7 @@ import { BellSound } from './utils/Sounds'; import { DEFAULT_ANSI_COLORS } from './renderer/ColorManager'; import { IMouseZoneManager } from './input/Interfaces'; import { MouseZoneManager } from './input/MouseZoneManager'; +import { initialize as initializeCharAtlas } from './renderer/CharAtlas'; // Declare for RequireJS in loadAddon declare var define: any; @@ -562,6 +563,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.document = this.parent.ownerDocument; this.body = this.document.body; + initializeCharAtlas(this.document); + // Create main element container this.element = this.document.createElement('div'); this.element.classList.add('terminal'); diff --git a/src/renderer/CharAtlas.ts b/src/renderer/CharAtlas.ts index a92037a883..4fcedd1a79 100644 --- a/src/renderer/CharAtlas.ts +++ b/src/renderer/CharAtlas.ts @@ -91,12 +91,20 @@ function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean { a.colors.foreground === b.colors.foreground; } +let generator: CharAtlasGenerator; + +export function initialize(document: Document): void { + if (!generator) { + generator = new CharAtlasGenerator(document); + } +} + class CharAtlasGenerator { private _canvas: HTMLCanvasElement; private _ctx: CanvasRenderingContext2D; - constructor() { - this._canvas = document.createElement('canvas'); + constructor(private _document: Document) { + this._canvas = this._document.createElement('canvas'); this._ctx = this._canvas.getContext('2d'); this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); } @@ -147,7 +155,7 @@ class CharAtlasGenerator { if (!('createImageBitmap' in window)) { // Regenerate canvas and context as they are now owned by the char atlas const result = this._canvas; - this._canvas = document.createElement('canvas'); + this._canvas = this._document.createElement('canvas'); this._ctx = this._canvas.getContext('2d'); this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); return result; @@ -159,5 +167,3 @@ class CharAtlasGenerator { return promise; } } - -const generator = new CharAtlasGenerator(); From 36778186e1bfafa302523ee8c874865c00169bd4 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 3 Sep 2017 14:34:50 -0700 Subject: [PATCH 081/108] Fix tests --- fixtures/typings-test/typings-test.ts | 4 ++-- src/CompositionHelper.test.ts | 10 ++++++++++ src/Terminal.test.ts | 3 ++- src/Terminal.ts | 3 ++- src/Viewport.test.ts | 3 ++- src/renderer/Interfaces.ts | 15 ++++++++++++++- src/renderer/Renderer.ts | 4 ++-- src/utils/TestUtils.test.ts | 17 +++++++++++++++-- typings/xterm.d.ts | 6 +++--- 9 files changed, 52 insertions(+), 13 deletions(-) diff --git a/fixtures/typings-test/typings-test.ts b/fixtures/typings-test/typings-test.ts index ffe6afd496..90958f83f0 100644 --- a/fixtures/typings-test/typings-test.ts +++ b/fixtures/typings-test/typings-test.ts @@ -210,8 +210,8 @@ namespace methods_experimental { t.registerLinkMatcher(/foo/, () => true, { matchIndex: 1, priority: 1, - validationCallback: (uri: string, element: HTMLElement, callback: (isValid: boolean) => void) => { - console.log(uri, element, callback); + validationCallback: (uri: string, callback: (isValid: boolean) => void) => { + console.log(uri, callback); } }); t.deregisterLinkMatcher(1); diff --git a/src/CompositionHelper.test.ts b/src/CompositionHelper.test.ts index 0adb1719a6..7d78a259bb 100644 --- a/src/CompositionHelper.test.ts +++ b/src/CompositionHelper.test.ts @@ -42,6 +42,16 @@ describe('CompositionHelper', () => { }, handler: (text: string) => { handledText += text; + }, + buffer: { + isCursorInViewport: true + }, + charMeasure: { + height: 10, + width: 10 + }, + options: { + lineHeight: 1 } }; handledText = ''; diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index 0c333741fc..7a44ee81e9 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -1,6 +1,6 @@ import { assert, expect } from 'chai'; import { Terminal } from './Terminal'; -import { MockViewport, MockCompositionHelper } from './utils/TestUtils.test'; +import { MockViewport, MockCompositionHelper, MockRenderer } from './utils/TestUtils.test'; import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from './Buffer'; const INIT_COLS = 80; @@ -21,6 +21,7 @@ describe('term.js addons', () => { rows: INIT_ROWS }); term.refresh = () => {}; + (term).renderer = new MockRenderer(); term.viewport = new MockViewport(); (term).compositionHelper = new MockCompositionHelper(); // Force synchronous writes diff --git a/src/Terminal.ts b/src/Terminal.ts index 620340171e..cab25b03b6 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -44,6 +44,7 @@ import { DEFAULT_ANSI_COLORS } from './renderer/ColorManager'; import { IMouseZoneManager } from './input/Interfaces'; import { MouseZoneManager } from './input/MouseZoneManager'; import { initialize as initializeCharAtlas } from './renderer/CharAtlas'; +import { IRenderer } from './renderer/Interfaces'; // Declare for RequireJS in loadAddon declare var define: any; @@ -184,7 +185,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT private inputHandler: InputHandler; private parser: Parser; - private renderer: Renderer; + private renderer: IRenderer; public selectionManager: SelectionManager; private linkifier: Linkifier; public buffers: BufferSet; diff --git a/src/Viewport.test.ts b/src/Viewport.test.ts index 8a960fc7dc..6d23e19a1f 100644 --- a/src/Viewport.test.ts +++ b/src/Viewport.test.ts @@ -31,7 +31,8 @@ describe('Viewport', () => { } }, options: { - scrollback: 10 + scrollback: 10, + lineHeight: 1 } }; terminal.buffers = new BufferSet(terminal); diff --git a/src/renderer/Interfaces.ts b/src/renderer/Interfaces.ts index fd94d59d7f..f7645abf8e 100644 --- a/src/renderer/Interfaces.ts +++ b/src/renderer/Interfaces.ts @@ -1,4 +1,17 @@ -import { ITerminal, ITerminalOptions } from '../Interfaces'; +import { ITerminal, ITerminalOptions, ITheme } from '../Interfaces'; + +export interface IRenderer { + setTheme(theme: ITheme): IColorSet; + onResize(cols: number, rows: number): void; + onCharSizeChanged(charWidth: number, charHeight: number): void; + onBlur(): void; + onFocus(): void; + onSelectionChanged(start: [number, number], end: [number, number]): void; + onCursorMove(): void; + onOptionsChanged(): void; + clear(): void; + queueRefresh(start: number, end: number): void; +} export interface IRenderLayer { /** diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index bc0717dc46..6b12188eda 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -11,9 +11,9 @@ import { SelectionRenderLayer } from './SelectionRenderLayer'; import { CursorRenderLayer } from './CursorRenderLayer'; import { ColorManager } from './ColorManager'; import { BaseRenderLayer } from './BaseRenderLayer'; -import { IRenderLayer, IColorSet } from './Interfaces'; +import { IRenderLayer, IColorSet, IRenderer } from './Interfaces'; -export class Renderer { +export class Renderer implements IRenderer { /** A queue of the rows to be refreshed */ private _refreshRowsQueue: {start: number, end: number}[] = []; private _refreshAnimationFrame = null; diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index 0388e9da10..51f191a836 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -2,11 +2,11 @@ * @license MIT */ -import { ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, IListenerType, IInputHandlingTerminal, IViewport, ICircularList, ICompositionHelper } from '../Interfaces'; +import { ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, IListenerType, IInputHandlingTerminal, IViewport, ICircularList, ICompositionHelper, ITheme } from '../Interfaces'; import { LineData } from '../Types'; import { Buffer } from '../Buffer'; import * as Browser from './Browser'; -import { IColorSet } from '../renderer/Interfaces'; +import { IColorSet, IRenderer } from '../renderer/Interfaces'; export class MockTerminal implements ITerminal { isFocused: boolean; @@ -204,6 +204,19 @@ export class MockBuffer implements IBuffer { } } +export class MockRenderer implements IRenderer { + setTheme(theme: ITheme): IColorSet { return {}; } + onResize(cols: number, rows: number): void {} + onCharSizeChanged(charWidth: number, charHeight: number): void {} + onBlur(): void {} + onFocus(): void {} + onSelectionChanged(start: [number, number], end: [number, number]): void {} + onCursorMove(): void {} + onOptionsChanged(): void {} + clear(): void {} + queueRefresh(start: number, end: number): void {} +} + export class MockViewport implements IViewport { onThemeChanged(colors: IColorSet): void { throw new Error('Method not implemented.'); diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index d612797858..866021e87d 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -131,12 +131,12 @@ interface ILinkMatcherOptions { * A callback that validates an individual link, returning true if valid and * false if invalid. */ - validationCallback?: (uri: string, element: HTMLElement, callback: (isValid: boolean) => void) => void; + validationCallback?: (uri: string, callback: (isValid: boolean) => void) => void; /** * A callback that fired when the mouse hovers over a link. */ - hoverCallback?: LinkMatcherHandler; + hoverCallback?: (event: MouseEvent, uri: string) => boolean | void; /** * The priority of the link matcher, this defines the order in which the link @@ -290,7 +290,7 @@ declare module 'xterm' { * @param options Options for the link matcher. * @return The ID of the new matcher, this can be used to deregister. */ - registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void , options?: ILinkMatcherOptions): number; + registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number; /** * (EXPERIMENTAL) Deregisters a link matcher if it has been registered. From e123dd1849b63fd15cdead4d7ec89774180e799e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 3 Sep 2017 21:15:14 -0700 Subject: [PATCH 082/108] Null check if clicking on scroll bar --- src/SelectionManager.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index e84bde6869..4fe09c8c0f 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -409,6 +409,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager return; } + // Return early if the click event is not in the buffer (eg. in scroll bar) + if (line.length >= this._model.selectionStart[0]) { + return; + } + // If the mouse is over the second half of a wide character, adjust the // selection to cover the whole character const char = line[this._model.selectionStart[0]]; From 7934e050836953d967ae2ccad64e5a395178228d Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 3 Sep 2017 21:34:48 -0700 Subject: [PATCH 083/108] jsdoc Linkifier --- src/Linkifier.test.ts | 15 +++++---- src/Linkifier.ts | 78 ++++++++++++++++++++----------------------- 2 files changed, 45 insertions(+), 48 deletions(-) diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index 6b28acb94c..f165590f94 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -11,12 +11,13 @@ import { MockBuffer } from './utils/TestUtils.test'; import { CircularList } from './utils/CircularList'; class TestLinkifier extends Linkifier { - constructor(bufferAccessor: IBufferAccessor) { + constructor(private _bufferAccessor: IBufferAccessor) { + super(_bufferAccessor); Linkifier.TIME_BEFORE_LINKIFY = 0; - super(bufferAccessor); } public get linkMatchers(): LinkMatcher[] { return this._linkMatchers; } + public linkifyRows(): void { super.linkifyRows(0, this._bufferAccessor.buffer.lines.length - 1); } } class TestMouseZoneManager implements IMouseZoneManager { @@ -57,7 +58,7 @@ describe('Linkifier', () => { function assertLinkifiesEntireRow(uri: string, done: MochaDone): void { addRow(uri); - linkifier.linkifyRow(0); + linkifier.linkifyRows(); setTimeout(() => { assert.equal(mouseZoneManager.zones[0].x1, 1); assert.equal(mouseZoneManager.zones[0].x2, uri.length + 1); @@ -69,7 +70,7 @@ describe('Linkifier', () => { function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, links: {x: number, length: number}[], done: MochaDone): void { addRow(rowText); linkifier.registerLinkMatcher(linkMatcherRegex, () => {}); - linkifier.linkifyRow(0); + linkifier.linkifyRows(); // Allow linkify to happen setTimeout(() => { assert.equal(mouseZoneManager.zones.length, links.length); @@ -148,7 +149,7 @@ describe('Linkifier', () => { mouseZoneManager.zones[0].clickCallback({}); } }); - linkifier.linkifyRow(0); + linkifier.linkifyRows(); }); it('should disable link if false', done => { @@ -160,7 +161,7 @@ describe('Linkifier', () => { assert.equal(mouseZoneManager.zones.length, 0); } }); - linkifier.linkifyRow(0); + linkifier.linkifyRows(); // Allow time for the validation callback to be performed setTimeout(() => done(), 10); }); @@ -177,7 +178,7 @@ describe('Linkifier', () => { cb(false); } }); - linkifier.linkifyRow(0); + linkifier.linkifyRows(); }); }); diff --git a/src/Linkifier.ts b/src/Linkifier.ts index cae0d6625a..d3150b4f64 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -44,30 +44,31 @@ export class Linkifier { */ protected static TIME_BEFORE_LINKIFY = 200; - protected _linkMatchers: LinkMatcher[]; + protected _linkMatchers: LinkMatcher[] = []; private _mouseZoneManager: IMouseZoneManager; - private _rowTimeoutIds: number[]; private _rowsTimeoutId: number; private _nextLinkMatcherId = HYPERTEXT_LINK_MATCHER_ID; constructor( private _terminal: IBufferAccessor ) { - this._rowTimeoutIds = []; - this._linkMatchers = []; this.registerLinkMatcher(strictUrlRegex, null, { matchIndex: 1 }); } /** * Attaches the linkifier to the DOM, enabling linkification. - * @param document The document object. - * @param rows The array of rows to apply links to. + * @param mouseZoneManager The mouse zone manager to register link zones with. */ public attachToDom(mouseZoneManager: IMouseZoneManager): void { this._mouseZoneManager = mouseZoneManager; } + /** + * Queue linkification on a set of rows. + * @param start The row to linkify from (inclusive). + * @param end The row to linkify to (inclusive). + */ public linkifyRows(start: number, end: number): void { // Don't attempt linkify if not yet attached to DOM if (!this._mouseZoneManager) { @@ -84,6 +85,11 @@ export class Linkifier { this._rowsTimeoutId = setTimeout(this._linkifyRows.bind(this, start, end), Linkifier.TIME_BEFORE_LINKIFY); } + /** + * Linkifies + * @param start The row to start at. + * @param end The row to end at. + */ private _linkifyRows(start: number, end: number): void { for (let i = start; i <= end; i++) { this._linkifyRow(i); @@ -91,27 +97,9 @@ export class Linkifier { } /** - * Queues a row for linkification. - * @param {number} rowIndex The index of the row to linkify. - */ - public linkifyRow(rowIndex: number): void { - // Don't attempt linkify if not yet attached to DOM - if (!this._mouseZoneManager) { - return; - } - - const timeoutId = this._rowTimeoutIds[rowIndex]; - if (timeoutId) { - clearTimeout(timeoutId); - } - this._rowTimeoutIds[rowIndex] = setTimeout(this._linkifyRow.bind(this, rowIndex), Linkifier.TIME_BEFORE_LINKIFY); - } - - /** - * Attaches a handler for hypertext links, overriding default behavior - * for standard http(s) links. - * @param {LinkHandler} handler The handler to use, this can be cleared with - * null. + * Attaches a handler for hypertext links, overriding default behavior for + * tandard http(s) links. + * @param handler The handler to use, this can be cleared with null. */ public setHypertextLinkHandler(handler: LinkMatcherHandler): void { this._linkMatchers[HYPERTEXT_LINK_MATCHER_ID].handler = handler; @@ -119,8 +107,7 @@ export class Linkifier { /** * Attaches a validation callback for hypertext links. - * @param {LinkMatcherValidationCallback} callback The callback to use, this - * can be cleared with null. + * @param callback The callback to use, this can be cleared with null. */ public setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void { this._linkMatchers[HYPERTEXT_LINK_MATCHER_ID].validationCallback = callback; @@ -129,12 +116,12 @@ export class Linkifier { /** * Registers a link matcher, allowing custom link patterns to be matched and * handled. - * @param {RegExp} regex The regular expression to search for, specifically - * this searches the textContent of the rows. You will want to use \s to match - * a space ' ' character for example. - * @param {LinkHandler} handler The callback when the link is called. - * @param {ILinkMatcherOptions} [options] Options for the link matcher. - * @return {number} The ID of the new matcher, this can be used to deregister. + * @param regex The regular expression to search for. Specifically, this + * searches the textContent of the rows. You will want to use \s to match a + * space ' ' character for example. + * @param handler The callback when the link is called. + * @param options Options for the link matcher. + * @return The ID of the new matcher, this can be used to deregister. */ public registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options: ILinkMatcherOptions = {}): number { if (this._nextLinkMatcherId !== HYPERTEXT_LINK_MATCHER_ID && !handler) { @@ -177,8 +164,8 @@ export class Linkifier { /** * Deregisters a link matcher if it has been registered. - * @param {number} matcherId The link matcher's ID (returned after register) - * @return {boolean} Whether a link matcher was found and deregistered. + * @param matcherId The link matcher's ID (returned after register) + * @return Whether a link matcher was found and deregistered. */ public deregisterLinkMatcher(matcherId: number): boolean { // ID 0 is the hypertext link matcher which cannot be deregistered @@ -193,7 +180,7 @@ export class Linkifier { /** * Linkifies a row. - * @param {number} rowIndex The index of the row to linkify. + * @param rowIndex The index of the row to linkify. */ private _linkifyRow(rowIndex: number): void { const absoluteRowIndex = this._terminal.buffer.ydisp + rowIndex; @@ -209,8 +196,10 @@ export class Linkifier { /** * Linkifies a row given a specific handler. * @param rowIndex The row index to linkify. - * @param text The text of the row. - * @param {LinkMatcher} matcher The link matcher for this line. + * @param text The text of the row (excludes text in the row that's already + * linkified). + * @param matcher The link matcher for this line. + * @param offset The how much of the row has already been linkified. * @return The link element(s) that were added. */ private _doLinkifyRow(rowIndex: number, text: string, matcher: LinkMatcher, offset: number = 0): void { @@ -248,6 +237,13 @@ export class Linkifier { } } + /** + * Registers a link to the mouse zone manager. + * @param x The column the link starts. + * @param y The row the link is on. + * @param uri The URI of the link. + * @param matcher The link matcher for the link. + */ private _addLink(x: number, y: number, uri: string, matcher: LinkMatcher): void { this._mouseZoneManager.add(new MouseZone( x + 1, @@ -257,7 +253,7 @@ export class Linkifier { if (matcher.handler) { return matcher.handler(e, uri); } - window.open(uri, '_blink'); + window.open(uri, '_blank'); }, e => { if (matcher.hoverCallback) { From e77f1a928759bfe880c124908b6d81d074ff18a6 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 3 Sep 2017 21:52:47 -0700 Subject: [PATCH 084/108] Use default colors if not defined in a theme Previous behavior didn't touch them, meaning partial themes could be inconsistent --- src/renderer/ColorManager.ts | 55 ++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/src/renderer/ColorManager.ts b/src/renderer/ColorManager.ts index 6b7f88c1f1..2b16b1c396 100644 --- a/src/renderer/ColorManager.ts +++ b/src/renderer/ColorManager.ts @@ -1,9 +1,6 @@ import { IColorSet } from './Interfaces'; import { ITheme } from '../Interfaces'; -// TODO: Ideally colors would be exposed through some theme manager since colors -// are moving to JS. - export enum COLOR_CODES { BLACK = 0, RED = 1, @@ -23,6 +20,9 @@ export enum COLOR_CODES { BRIGHT_WHITE = 15 } +const DEFAULT_FOREGROUND = '#ffffff'; +const DEFAULT_BACKGROUND = '#000000'; +const DEFAULT_CURSOR = '#ffffff'; export const DEFAULT_ANSI_COLORS = [ // dark: '#2e3436', @@ -75,32 +75,37 @@ export class ColorManager { constructor() { this.colors = { - foreground: '#ffffff', - background: '#000000', - cursor: '#ffffff', + foreground: DEFAULT_FOREGROUND, + background: DEFAULT_BACKGROUND, + cursor: DEFAULT_CURSOR, ansi: generate256Colors(DEFAULT_ANSI_COLORS) }; } + /** + * Sets the terminal's theme. + * @param theme The theme to use. If a partial theme is provided then default + * colors will be used where colors are not defined. + */ public setTheme(theme: ITheme): void { - if (theme.foreground) this.colors.foreground = theme.foreground; - if (theme.background) this.colors.background = theme.background; - if (theme.cursor) this.colors.cursor = theme.cursor; - if (theme.black) this.colors.ansi[0] = theme.black; - if (theme.red) this.colors.ansi[1] = theme.red; - if (theme.green) this.colors.ansi[2] = theme.green; - if (theme.yellow) this.colors.ansi[3] = theme.yellow; - if (theme.blue) this.colors.ansi[4] = theme.blue; - if (theme.magenta) this.colors.ansi[5] = theme.magenta; - if (theme.cyan) this.colors.ansi[6] = theme.cyan; - if (theme.white) this.colors.ansi[7] = theme.white; - if (theme.brightBlack) this.colors.ansi[8] = theme.brightBlack; - if (theme.brightRed) this.colors.ansi[9] = theme.brightRed; - if (theme.brightGreen) this.colors.ansi[10] = theme.brightGreen; - if (theme.brightYellow) this.colors.ansi[11] = theme.brightYellow; - if (theme.brightBlue) this.colors.ansi[12] = theme.brightBlue; - if (theme.brightMagenta) this.colors.ansi[13] = theme.brightMagenta; - if (theme.brightCyan) this.colors.ansi[14] = theme.brightCyan; - if (theme.brightWhite) this.colors.ansi[15] = theme.brightWhite; + this.colors.foreground = theme.foreground || DEFAULT_FOREGROUND; + this.colors.background = theme.background || DEFAULT_BACKGROUND; + this.colors.cursor = theme.cursor || DEFAULT_CURSOR; + this.colors.ansi[0] = theme.black || DEFAULT_ANSI_COLORS[0]; + this.colors.ansi[1] = theme.red || DEFAULT_ANSI_COLORS[1]; + this.colors.ansi[2] = theme.green || DEFAULT_ANSI_COLORS[2]; + this.colors.ansi[3] = theme.yellow || DEFAULT_ANSI_COLORS[3]; + this.colors.ansi[4] = theme.blue || DEFAULT_ANSI_COLORS[4]; + this.colors.ansi[5] = theme.magenta || DEFAULT_ANSI_COLORS[5]; + this.colors.ansi[6] = theme.cyan || DEFAULT_ANSI_COLORS[6]; + this.colors.ansi[7] = theme.white || DEFAULT_ANSI_COLORS[7]; + this.colors.ansi[8] = theme.brightBlack || DEFAULT_ANSI_COLORS[8]; + this.colors.ansi[9] = theme.brightRed || DEFAULT_ANSI_COLORS[9]; + this.colors.ansi[10] = theme.brightGreen || DEFAULT_ANSI_COLORS[10]; + this.colors.ansi[11] = theme.brightYellow || DEFAULT_ANSI_COLORS[11]; + this.colors.ansi[12] = theme.brightBlue || DEFAULT_ANSI_COLORS[12]; + this.colors.ansi[13] = theme.brightMagenta || DEFAULT_ANSI_COLORS[13]; + this.colors.ansi[14] = theme.brightCyan || DEFAULT_ANSI_COLORS[14]; + this.colors.ansi[15] = theme.brightWhite || DEFAULT_ANSI_COLORS[15]; } } From 29a7ee30f33f8712d9320b6465c5a678c1680dad Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 3 Sep 2017 22:39:47 -0700 Subject: [PATCH 085/108] Fix lint --- src/renderer/GridCache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/GridCache.ts b/src/renderer/GridCache.ts index 56294ddcbe..3b3ae8dfab 100644 --- a/src/renderer/GridCache.ts +++ b/src/renderer/GridCache.ts @@ -5,7 +5,7 @@ export class GridCache { this.cache = []; } - public resize(width: number, height: number) { + public resize(width: number, height: number): void { for (let x = 0; x < width; x++) { if (this.cache.length <= x) { this.cache.push([]); From 0bfdff0b39010b8a49a2396aa1e389de132c3d35 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 4 Sep 2017 08:09:43 -0700 Subject: [PATCH 086/108] Theme unfocused cursor using cursor color --- src/renderer/ColorManager.ts | 19 ------------------- src/renderer/CursorRenderLayer.ts | 4 +--- 2 files changed, 1 insertion(+), 22 deletions(-) diff --git a/src/renderer/ColorManager.ts b/src/renderer/ColorManager.ts index 2b16b1c396..581f09a9ff 100644 --- a/src/renderer/ColorManager.ts +++ b/src/renderer/ColorManager.ts @@ -1,25 +1,6 @@ import { IColorSet } from './Interfaces'; import { ITheme } from '../Interfaces'; -export enum COLOR_CODES { - BLACK = 0, - RED = 1, - GREEN = 2, - YELLOW = 3, - BLUE = 4, - MAGENTA = 5, - CYAN = 6, - WHITE = 7, - BRIGHT_BLACK = 8, - BRIGHT_RED = 9, - BRIGHT_GREEN = 10, - BRIGHT_YELLOW = 11, - BRIGHT_BLUE = 12, - BRIGHT_MAGENTA = 13, - BRIGHT_CYAN = 14, - BRIGHT_WHITE = 15 -} - const DEFAULT_FOREGROUND = '#ffffff'; const DEFAULT_BACKGROUND = '#000000'; const DEFAULT_CURSOR = '#ffffff'; diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index 482d4e1482..00abfb9332 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -5,7 +5,6 @@ import { GridCache } from './GridCache'; import { FLAGS } from './Types'; import { BaseRenderLayer } from './BaseRenderLayer'; import { CharData } from '../Types'; -import { COLOR_CODES } from './ColorManager'; interface CursorState { x: number; @@ -131,7 +130,7 @@ export class CursorRenderLayer extends BaseRenderLayer { if (!terminal.isFocused) { this._clearCursor(); this._ctx.save(); - this._ctx.fillStyle = this.colors.ansi[COLOR_CODES.WHITE]; + this._ctx.fillStyle = this.colors.cursor; this._renderBlurCursor(terminal, terminal.buffer.x, viewportRelativeCursorY, charData); this._ctx.restore(); this._state.x = terminal.buffer.x; @@ -161,7 +160,6 @@ export class CursorRenderLayer extends BaseRenderLayer { } this._ctx.save(); - this._ctx.fillStyle = this.colors.ansi[COLOR_CODES.WHITE]; this._cursorRenderers[terminal.options.cursorStyle || 'block'](terminal, terminal.buffer.x, viewportRelativeCursorY, charData); this._ctx.restore(); From ff31f345516bd6b6a7b09e3251ba0c7c1047331f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 4 Sep 2017 08:14:40 -0700 Subject: [PATCH 087/108] Add selection to ITheme --- src/Interfaces.ts | 1 + src/renderer/CharAtlas.ts | 1 + src/renderer/ColorManager.ts | 3 +++ src/renderer/Interfaces.ts | 1 + src/renderer/SelectionRenderLayer.ts | 2 +- typings/xterm.d.ts | 2 ++ 6 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Interfaces.ts b/src/Interfaces.ts index 9ce8ffd482..da6d272728 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -307,6 +307,7 @@ export interface ITheme { foreground?: string; background?: string; cursor?: string; + selection?: string; black?: string; red?: string; green?: string; diff --git a/src/renderer/CharAtlas.ts b/src/renderer/CharAtlas.ts index 4fcedd1a79..05eb9a8418 100644 --- a/src/renderer/CharAtlas.ts +++ b/src/renderer/CharAtlas.ts @@ -67,6 +67,7 @@ function generateConfig(scaledCharWidth: number, scaledCharHeight: number, termi foreground: colors.foreground, background: null, cursor: null, + selection: null, ansi: colors.ansi.slice(0, 16) }; return { diff --git a/src/renderer/ColorManager.ts b/src/renderer/ColorManager.ts index 581f09a9ff..17aee39285 100644 --- a/src/renderer/ColorManager.ts +++ b/src/renderer/ColorManager.ts @@ -4,6 +4,7 @@ import { ITheme } from '../Interfaces'; const DEFAULT_FOREGROUND = '#ffffff'; const DEFAULT_BACKGROUND = '#000000'; const DEFAULT_CURSOR = '#ffffff'; +const DEFAULT_SELECTION = 'rgba(255, 255, 255, 0.9)'; export const DEFAULT_ANSI_COLORS = [ // dark: '#2e3436', @@ -59,6 +60,7 @@ export class ColorManager { foreground: DEFAULT_FOREGROUND, background: DEFAULT_BACKGROUND, cursor: DEFAULT_CURSOR, + selection: DEFAULT_SELECTION, ansi: generate256Colors(DEFAULT_ANSI_COLORS) }; } @@ -72,6 +74,7 @@ export class ColorManager { this.colors.foreground = theme.foreground || DEFAULT_FOREGROUND; this.colors.background = theme.background || DEFAULT_BACKGROUND; this.colors.cursor = theme.cursor || DEFAULT_CURSOR; + this.colors.selection = theme.selection || DEFAULT_SELECTION; this.colors.ansi[0] = theme.black || DEFAULT_ANSI_COLORS[0]; this.colors.ansi[1] = theme.red || DEFAULT_ANSI_COLORS[1]; this.colors.ansi[2] = theme.green || DEFAULT_ANSI_COLORS[2]; diff --git a/src/renderer/Interfaces.ts b/src/renderer/Interfaces.ts index f7645abf8e..04e24e19b5 100644 --- a/src/renderer/Interfaces.ts +++ b/src/renderer/Interfaces.ts @@ -66,5 +66,6 @@ export interface IColorSet { foreground: string; background: string; cursor: string; + selection: string; ansi: string[]; } diff --git a/src/renderer/SelectionRenderLayer.ts b/src/renderer/SelectionRenderLayer.ts index 7f131da802..91e62a0c6d 100644 --- a/src/renderer/SelectionRenderLayer.ts +++ b/src/renderer/SelectionRenderLayer.ts @@ -63,7 +63,7 @@ export class SelectionRenderLayer extends BaseRenderLayer { // Draw first row const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0; const startRowEndCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : terminal.cols; - this._ctx.fillStyle = 'rgba(255,255,255,0.3)'; + this._ctx.fillStyle = this.colors.selection; this.fillCells(startCol, viewportCappedStartRow, startRowEndCol - startCol, 1); // Draw middle rows diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 866021e87d..eff26c9bbc 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -83,6 +83,8 @@ interface ITheme { background?: string, /** The cursor color */ cursor?: string, + /** The selection color (can be transparent) */ + selection?: string, /** ANSI black (eg. `\x1b[30m`) */ black?: string, /** ANSI red (eg. `\x1b[31m`) */ From a17950290c9951dde0af2862e4eb530c639b0097 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 4 Sep 2017 09:04:50 -0700 Subject: [PATCH 088/108] Support emoji --- src/renderer/ForegroundRenderLayer.ts | 58 ++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index 907f686d8a..71168f26a7 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -6,6 +6,13 @@ import { GridCache } from './GridCache'; import { CharData } from '../Types'; import { BaseRenderLayer, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer'; +/** + * This CharData looks like a null character, which will forc a clear and render + * when the character changes (a regular space ' ' character may not as it's + * drawn state is a cleared cell). + */ +const EMOJI_OWNED_CHAR_DATA: CharData = [null, '', 0, -1]; + export class ForegroundRenderLayer extends BaseRenderLayer { private _state: GridCache; @@ -27,13 +34,6 @@ export class ForegroundRenderLayer extends BaseRenderLayer { } public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void { - // TODO: Ensure that the render is eventually performed - // Don't bother render until the atlas bitmap is ready - // TODO: Move this to BaseRenderLayer? - // if (!BaseRenderLayer._charAtlas) { - // return; - // } - // Resize has not been called yet if (this._state.cache.length === 0) { return; @@ -48,7 +48,7 @@ export class ForegroundRenderLayer extends BaseRenderLayer { const code: number = charData[CHAR_DATA_CODE_INDEX]; const char: string = charData[CHAR_DATA_CHAR_INDEX]; const attr: number = charData[CHAR_DATA_ATTR_INDEX]; - const width: number = charData[CHAR_DATA_WIDTH_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 @@ -57,6 +57,18 @@ export class ForegroundRenderLayer extends BaseRenderLayer { continue; } + // If the character is a space and the character to the left is an + // emoji, skip the character and allow the emoji char to take full + // control over this character's cell. + if (code === 32 /*' '*/) { + if (x > 0) { + const previousChar: CharData = line[x - 1]; + if (this._isEmoji(previousChar[CHAR_DATA_CHAR_INDEX])) { + continue; + } + } + } + // Skip rendering if the character is identical const state = this._state.cache[x][y]; if (state && state[CHAR_DATA_CHAR_INDEX] === char && state[CHAR_DATA_ATTR_INDEX] === attr) { @@ -67,7 +79,7 @@ export class ForegroundRenderLayer extends BaseRenderLayer { // Clear the old character if present if (state && state[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) { - this.clearChar(x, y); + this._clearChar(x, y); } this._state.cache[x][y] = charData; @@ -78,6 +90,19 @@ export class ForegroundRenderLayer extends BaseRenderLayer { continue; } + // If the character is an emoji and the character to the right is a + // space, take ownership of the cell to the right. + if (this._isEmoji(char)) { + if (x < line.length && line[x + 1][CHAR_DATA_CODE_INDEX] === 32 /*' '*/) { + width = 2; + this._clearChar(x + 1, y); + // The emoji owned char data will force a clear and render when the + // emoji is no longer to the left of the character and also when the + // space changes to another character. + this._state.cache[x + 1][y] = EMOJI_OWNED_CHAR_DATA; + } + } + let fg = (attr >> 9) & 0x1ff; // If inverse flag is on, the foreground should become the background. @@ -117,7 +142,20 @@ export class ForegroundRenderLayer extends BaseRenderLayer { } } - private clearChar(x: number, y: number): void { + /** + * Whether the character is an emoji. + * @param char The character to search. + */ + private _isEmoji(char: string): boolean { + return char.search(/([\uD800-\uDBFF][\uDC00-\uDFFF])/g) >= 0; + } + + /** + * Clear the charcater at the cell specified. + * @param x The column of the char. + * @param y The row of the char. + */ + private _clearChar(x: number, y: number): void { let colsToClear = 1; // Clear the adjacent character if it was wide const state = this._state.cache[x][y]; From 1c3028c59f9afb4623c93d142aedb9ffc49b4cd6 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 4 Sep 2017 09:23:14 -0700 Subject: [PATCH 089/108] Work around an emoji bug --- src/renderer/ForegroundRenderLayer.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index 71168f26a7..ef9c3c600b 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -93,6 +93,13 @@ export class ForegroundRenderLayer extends BaseRenderLayer { // If the character is an emoji and the character to the right is a // space, take ownership of the cell to the right. if (this._isEmoji(char)) { + // If the character is an emoji, we want to force a re-render on every + // frame. This is specifically to work around the case where two + // emoji's `a` and `b` are adjacent, the cursor is moved to b and a + // space is added. Without this, the first half of `b` would never + // get removed, and `a` would not re-render because it thinks it's + // already in the correct state. + this._state.cache[x][y] = EMOJI_OWNED_CHAR_DATA; if (x < line.length && line[x + 1][CHAR_DATA_CODE_INDEX] === 32 /*' '*/) { width = 2; this._clearChar(x + 1, y); From a35cf02208345f5b7790d5659069ab9eae3c5a12 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 4 Sep 2017 09:39:44 -0700 Subject: [PATCH 090/108] Add GridCache tests --- src/renderer/GridCache.test.ts | 61 ++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 src/renderer/GridCache.test.ts diff --git a/src/renderer/GridCache.test.ts b/src/renderer/GridCache.test.ts new file mode 100644 index 0000000000..37e55ee143 --- /dev/null +++ b/src/renderer/GridCache.test.ts @@ -0,0 +1,61 @@ +/** + * @license MIT + */ + +import { assert } from 'chai'; +import { GridCache } from './GridCache'; + +describe('GridCache', () => { + let grid: GridCache; + + beforeEach(() => { + grid = new GridCache(); + }); + + describe('constructor', () => { + it('should create an empty cache', () => { + assert.equal(grid.cache.length, 0); + }); + }); + + describe('resize', () => { + it('should fill all new elements with null', () => { + grid.resize(2, 2); + assert.equal(grid.cache.length, 2); + assert.equal(grid.cache[0].length, 2); + assert.equal(grid.cache[0][0], null); + assert.equal(grid.cache[0][1], null); + assert.equal(grid.cache[1].length, 2); + assert.equal(grid.cache[1][0], null); + assert.equal(grid.cache[1][1], null); + grid.resize(3, 2); + assert.equal(grid.cache.length, 3); + assert.equal(grid.cache[2][0], null); + assert.equal(grid.cache[2][1], null); + }); + + it('should remove rows/cols from the cache when reduced', () => { + grid.resize(2, 2); + grid.resize(1, 1); + assert.equal(grid.cache.length, 1); + assert.equal(grid.cache[0].length, 1); + }); + + it('should not touch existing cache entries if they fit in the new cache', () => { + grid.resize(1, 1); + assert.equal(grid.cache[0][0], null); + grid.cache[0][0] = 1; + grid.resize(2, 1); + assert.equal(grid.cache[0][0], 1); + }); + }); + + describe('clear', () => { + it('should make all entries null', () => { + grid.resize(1, 1); + grid.cache[0][0] = 1; + grid.clear(); + assert.equal(grid.cache[0][0], null); + }); + }); +}); From 6f6f17f5dc55411af1db457761cde79d3f1270e1 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 4 Sep 2017 09:52:35 -0700 Subject: [PATCH 091/108] Add tests for ColorManager --- src/renderer/ColorManager.test.ts | 288 ++++++++++++++++++++++++++++++ src/renderer/ColorManager.ts | 2 +- 2 files changed, 289 insertions(+), 1 deletion(-) create mode 100644 src/renderer/ColorManager.test.ts diff --git a/src/renderer/ColorManager.test.ts b/src/renderer/ColorManager.test.ts new file mode 100644 index 0000000000..6bd9c97775 --- /dev/null +++ b/src/renderer/ColorManager.test.ts @@ -0,0 +1,288 @@ +/** + * @license MIT + */ + +import { assert } from 'chai'; +import { ColorManager } from './ColorManager'; + +describe('ColorManager', () => { + let cm: ColorManager; + + beforeEach(() => { + cm = new ColorManager(); + }); + + describe('constructor', () => { + it('should fill all colors with values', () => { + for (let key in cm.colors) { + if (typeof key === 'string') { + // A #rrggbb or rgba(...) + assert.ok(cm.colors[key].length >= 7); + } + } + assert.equal(cm.colors.ansi.length, 256); + }); + + it('should fill 240 colors with expected values', () => { + assert.equal(cm.colors.ansi[16], '#000000'); + assert.equal(cm.colors.ansi[17], '#00005f'); + assert.equal(cm.colors.ansi[18], '#000087'); + assert.equal(cm.colors.ansi[19], '#0000af'); + assert.equal(cm.colors.ansi[20], '#0000d7'); + assert.equal(cm.colors.ansi[21], '#0000ff'); + assert.equal(cm.colors.ansi[22], '#005f00'); + assert.equal(cm.colors.ansi[23], '#005f5f'); + assert.equal(cm.colors.ansi[24], '#005f87'); + assert.equal(cm.colors.ansi[25], '#005faf'); + assert.equal(cm.colors.ansi[26], '#005fd7'); + assert.equal(cm.colors.ansi[27], '#005fff'); + assert.equal(cm.colors.ansi[28], '#008700'); + assert.equal(cm.colors.ansi[29], '#00875f'); + assert.equal(cm.colors.ansi[30], '#008787'); + assert.equal(cm.colors.ansi[31], '#0087af'); + assert.equal(cm.colors.ansi[32], '#0087d7'); + assert.equal(cm.colors.ansi[33], '#0087ff'); + assert.equal(cm.colors.ansi[34], '#00af00'); + assert.equal(cm.colors.ansi[35], '#00af5f'); + assert.equal(cm.colors.ansi[36], '#00af87'); + assert.equal(cm.colors.ansi[37], '#00afaf'); + assert.equal(cm.colors.ansi[38], '#00afd7'); + assert.equal(cm.colors.ansi[39], '#00afff'); + assert.equal(cm.colors.ansi[40], '#00d700'); + assert.equal(cm.colors.ansi[41], '#00d75f'); + assert.equal(cm.colors.ansi[42], '#00d787'); + assert.equal(cm.colors.ansi[43], '#00d7af'); + assert.equal(cm.colors.ansi[44], '#00d7d7'); + assert.equal(cm.colors.ansi[45], '#00d7ff'); + assert.equal(cm.colors.ansi[46], '#00ff00'); + assert.equal(cm.colors.ansi[47], '#00ff5f'); + assert.equal(cm.colors.ansi[48], '#00ff87'); + assert.equal(cm.colors.ansi[49], '#00ffaf'); + assert.equal(cm.colors.ansi[50], '#00ffd7'); + assert.equal(cm.colors.ansi[51], '#00ffff'); + assert.equal(cm.colors.ansi[52], '#5f0000'); + assert.equal(cm.colors.ansi[53], '#5f005f'); + assert.equal(cm.colors.ansi[54], '#5f0087'); + assert.equal(cm.colors.ansi[55], '#5f00af'); + assert.equal(cm.colors.ansi[56], '#5f00d7'); + assert.equal(cm.colors.ansi[57], '#5f00ff'); + assert.equal(cm.colors.ansi[58], '#5f5f00'); + assert.equal(cm.colors.ansi[59], '#5f5f5f'); + assert.equal(cm.colors.ansi[60], '#5f5f87'); + assert.equal(cm.colors.ansi[61], '#5f5faf'); + assert.equal(cm.colors.ansi[62], '#5f5fd7'); + assert.equal(cm.colors.ansi[63], '#5f5fff'); + assert.equal(cm.colors.ansi[64], '#5f8700'); + assert.equal(cm.colors.ansi[65], '#5f875f'); + assert.equal(cm.colors.ansi[66], '#5f8787'); + assert.equal(cm.colors.ansi[67], '#5f87af'); + assert.equal(cm.colors.ansi[68], '#5f87d7'); + assert.equal(cm.colors.ansi[69], '#5f87ff'); + assert.equal(cm.colors.ansi[70], '#5faf00'); + assert.equal(cm.colors.ansi[71], '#5faf5f'); + assert.equal(cm.colors.ansi[72], '#5faf87'); + assert.equal(cm.colors.ansi[73], '#5fafaf'); + assert.equal(cm.colors.ansi[74], '#5fafd7'); + assert.equal(cm.colors.ansi[75], '#5fafff'); + assert.equal(cm.colors.ansi[76], '#5fd700'); + assert.equal(cm.colors.ansi[77], '#5fd75f'); + assert.equal(cm.colors.ansi[78], '#5fd787'); + assert.equal(cm.colors.ansi[79], '#5fd7af'); + assert.equal(cm.colors.ansi[80], '#5fd7d7'); + assert.equal(cm.colors.ansi[81], '#5fd7ff'); + assert.equal(cm.colors.ansi[82], '#5fff00'); + assert.equal(cm.colors.ansi[83], '#5fff5f'); + assert.equal(cm.colors.ansi[84], '#5fff87'); + assert.equal(cm.colors.ansi[85], '#5fffaf'); + assert.equal(cm.colors.ansi[86], '#5fffd7'); + assert.equal(cm.colors.ansi[87], '#5fffff'); + assert.equal(cm.colors.ansi[88], '#870000'); + assert.equal(cm.colors.ansi[89], '#87005f'); + assert.equal(cm.colors.ansi[90], '#870087'); + assert.equal(cm.colors.ansi[91], '#8700af'); + assert.equal(cm.colors.ansi[92], '#8700d7'); + assert.equal(cm.colors.ansi[93], '#8700ff'); + assert.equal(cm.colors.ansi[94], '#875f00'); + assert.equal(cm.colors.ansi[95], '#875f5f'); + assert.equal(cm.colors.ansi[96], '#875f87'); + assert.equal(cm.colors.ansi[97], '#875faf'); + assert.equal(cm.colors.ansi[98], '#875fd7'); + assert.equal(cm.colors.ansi[99], '#875fff'); + assert.equal(cm.colors.ansi[100], '#878700'); + assert.equal(cm.colors.ansi[101], '#87875f'); + assert.equal(cm.colors.ansi[102], '#878787'); + assert.equal(cm.colors.ansi[103], '#8787af'); + assert.equal(cm.colors.ansi[104], '#8787d7'); + assert.equal(cm.colors.ansi[105], '#8787ff'); + assert.equal(cm.colors.ansi[106], '#87af00'); + assert.equal(cm.colors.ansi[107], '#87af5f'); + assert.equal(cm.colors.ansi[108], '#87af87'); + assert.equal(cm.colors.ansi[109], '#87afaf'); + assert.equal(cm.colors.ansi[110], '#87afd7'); + assert.equal(cm.colors.ansi[111], '#87afff'); + assert.equal(cm.colors.ansi[112], '#87d700'); + assert.equal(cm.colors.ansi[113], '#87d75f'); + assert.equal(cm.colors.ansi[114], '#87d787'); + assert.equal(cm.colors.ansi[115], '#87d7af'); + assert.equal(cm.colors.ansi[116], '#87d7d7'); + assert.equal(cm.colors.ansi[117], '#87d7ff'); + assert.equal(cm.colors.ansi[118], '#87ff00'); + assert.equal(cm.colors.ansi[119], '#87ff5f'); + assert.equal(cm.colors.ansi[120], '#87ff87'); + assert.equal(cm.colors.ansi[121], '#87ffaf'); + assert.equal(cm.colors.ansi[122], '#87ffd7'); + assert.equal(cm.colors.ansi[123], '#87ffff'); + assert.equal(cm.colors.ansi[124], '#af0000'); + assert.equal(cm.colors.ansi[125], '#af005f'); + assert.equal(cm.colors.ansi[126], '#af0087'); + assert.equal(cm.colors.ansi[127], '#af00af'); + assert.equal(cm.colors.ansi[128], '#af00d7'); + assert.equal(cm.colors.ansi[129], '#af00ff'); + assert.equal(cm.colors.ansi[130], '#af5f00'); + assert.equal(cm.colors.ansi[131], '#af5f5f'); + assert.equal(cm.colors.ansi[132], '#af5f87'); + assert.equal(cm.colors.ansi[133], '#af5faf'); + assert.equal(cm.colors.ansi[134], '#af5fd7'); + assert.equal(cm.colors.ansi[135], '#af5fff'); + assert.equal(cm.colors.ansi[136], '#af8700'); + assert.equal(cm.colors.ansi[137], '#af875f'); + assert.equal(cm.colors.ansi[138], '#af8787'); + assert.equal(cm.colors.ansi[139], '#af87af'); + assert.equal(cm.colors.ansi[140], '#af87d7'); + assert.equal(cm.colors.ansi[141], '#af87ff'); + assert.equal(cm.colors.ansi[142], '#afaf00'); + assert.equal(cm.colors.ansi[143], '#afaf5f'); + assert.equal(cm.colors.ansi[144], '#afaf87'); + assert.equal(cm.colors.ansi[145], '#afafaf'); + assert.equal(cm.colors.ansi[146], '#afafd7'); + assert.equal(cm.colors.ansi[147], '#afafff'); + assert.equal(cm.colors.ansi[148], '#afd700'); + assert.equal(cm.colors.ansi[149], '#afd75f'); + assert.equal(cm.colors.ansi[150], '#afd787'); + assert.equal(cm.colors.ansi[151], '#afd7af'); + assert.equal(cm.colors.ansi[152], '#afd7d7'); + assert.equal(cm.colors.ansi[153], '#afd7ff'); + assert.equal(cm.colors.ansi[154], '#afff00'); + assert.equal(cm.colors.ansi[155], '#afff5f'); + assert.equal(cm.colors.ansi[156], '#afff87'); + assert.equal(cm.colors.ansi[157], '#afffaf'); + assert.equal(cm.colors.ansi[158], '#afffd7'); + assert.equal(cm.colors.ansi[159], '#afffff'); + assert.equal(cm.colors.ansi[160], '#d70000'); + assert.equal(cm.colors.ansi[161], '#d7005f'); + assert.equal(cm.colors.ansi[162], '#d70087'); + assert.equal(cm.colors.ansi[163], '#d700af'); + assert.equal(cm.colors.ansi[164], '#d700d7'); + assert.equal(cm.colors.ansi[165], '#d700ff'); + assert.equal(cm.colors.ansi[166], '#d75f00'); + assert.equal(cm.colors.ansi[167], '#d75f5f'); + assert.equal(cm.colors.ansi[168], '#d75f87'); + assert.equal(cm.colors.ansi[169], '#d75faf'); + assert.equal(cm.colors.ansi[170], '#d75fd7'); + assert.equal(cm.colors.ansi[171], '#d75fff'); + assert.equal(cm.colors.ansi[172], '#d78700'); + assert.equal(cm.colors.ansi[173], '#d7875f'); + assert.equal(cm.colors.ansi[174], '#d78787'); + assert.equal(cm.colors.ansi[175], '#d787af'); + assert.equal(cm.colors.ansi[176], '#d787d7'); + assert.equal(cm.colors.ansi[177], '#d787ff'); + assert.equal(cm.colors.ansi[178], '#d7af00'); + assert.equal(cm.colors.ansi[179], '#d7af5f'); + assert.equal(cm.colors.ansi[180], '#d7af87'); + assert.equal(cm.colors.ansi[181], '#d7afaf'); + assert.equal(cm.colors.ansi[182], '#d7afd7'); + assert.equal(cm.colors.ansi[183], '#d7afff'); + assert.equal(cm.colors.ansi[184], '#d7d700'); + assert.equal(cm.colors.ansi[185], '#d7d75f'); + assert.equal(cm.colors.ansi[186], '#d7d787'); + assert.equal(cm.colors.ansi[187], '#d7d7af'); + assert.equal(cm.colors.ansi[188], '#d7d7d7'); + assert.equal(cm.colors.ansi[189], '#d7d7ff'); + assert.equal(cm.colors.ansi[190], '#d7ff00'); + assert.equal(cm.colors.ansi[191], '#d7ff5f'); + assert.equal(cm.colors.ansi[192], '#d7ff87'); + assert.equal(cm.colors.ansi[193], '#d7ffaf'); + assert.equal(cm.colors.ansi[194], '#d7ffd7'); + assert.equal(cm.colors.ansi[195], '#d7ffff'); + assert.equal(cm.colors.ansi[196], '#ff0000'); + assert.equal(cm.colors.ansi[197], '#ff005f'); + assert.equal(cm.colors.ansi[198], '#ff0087'); + assert.equal(cm.colors.ansi[199], '#ff00af'); + assert.equal(cm.colors.ansi[200], '#ff00d7'); + assert.equal(cm.colors.ansi[201], '#ff00ff'); + assert.equal(cm.colors.ansi[202], '#ff5f00'); + assert.equal(cm.colors.ansi[203], '#ff5f5f'); + assert.equal(cm.colors.ansi[204], '#ff5f87'); + assert.equal(cm.colors.ansi[205], '#ff5faf'); + assert.equal(cm.colors.ansi[206], '#ff5fd7'); + assert.equal(cm.colors.ansi[207], '#ff5fff'); + assert.equal(cm.colors.ansi[208], '#ff8700'); + assert.equal(cm.colors.ansi[209], '#ff875f'); + assert.equal(cm.colors.ansi[210], '#ff8787'); + assert.equal(cm.colors.ansi[211], '#ff87af'); + assert.equal(cm.colors.ansi[212], '#ff87d7'); + assert.equal(cm.colors.ansi[213], '#ff87ff'); + assert.equal(cm.colors.ansi[214], '#ffaf00'); + assert.equal(cm.colors.ansi[215], '#ffaf5f'); + assert.equal(cm.colors.ansi[216], '#ffaf87'); + assert.equal(cm.colors.ansi[217], '#ffafaf'); + assert.equal(cm.colors.ansi[218], '#ffafd7'); + assert.equal(cm.colors.ansi[219], '#ffafff'); + assert.equal(cm.colors.ansi[220], '#ffd700'); + assert.equal(cm.colors.ansi[221], '#ffd75f'); + assert.equal(cm.colors.ansi[222], '#ffd787'); + assert.equal(cm.colors.ansi[223], '#ffd7af'); + assert.equal(cm.colors.ansi[224], '#ffd7d7'); + assert.equal(cm.colors.ansi[225], '#ffd7ff'); + assert.equal(cm.colors.ansi[226], '#ffff00'); + assert.equal(cm.colors.ansi[227], '#ffff5f'); + assert.equal(cm.colors.ansi[228], '#ffff87'); + assert.equal(cm.colors.ansi[229], '#ffffaf'); + assert.equal(cm.colors.ansi[230], '#ffffd7'); + assert.equal(cm.colors.ansi[231], '#ffffff'); + assert.equal(cm.colors.ansi[232], '#080808'); + assert.equal(cm.colors.ansi[233], '#121212'); + assert.equal(cm.colors.ansi[234], '#1c1c1c'); + assert.equal(cm.colors.ansi[235], '#262626'); + assert.equal(cm.colors.ansi[236], '#303030'); + assert.equal(cm.colors.ansi[237], '#3a3a3a'); + assert.equal(cm.colors.ansi[238], '#444444'); + assert.equal(cm.colors.ansi[239], '#4e4e4e'); + assert.equal(cm.colors.ansi[240], '#585858'); + assert.equal(cm.colors.ansi[241], '#626262'); + assert.equal(cm.colors.ansi[242], '#6c6c6c'); + assert.equal(cm.colors.ansi[243], '#767676'); + assert.equal(cm.colors.ansi[244], '#808080'); + assert.equal(cm.colors.ansi[245], '#8a8a8a'); + assert.equal(cm.colors.ansi[246], '#949494'); + assert.equal(cm.colors.ansi[247], '#9e9e9e'); + assert.equal(cm.colors.ansi[248], '#a8a8a8'); + assert.equal(cm.colors.ansi[249], '#b2b2b2'); + assert.equal(cm.colors.ansi[250], '#bcbcbc'); + assert.equal(cm.colors.ansi[251], '#c6c6c6'); + assert.equal(cm.colors.ansi[252], '#d0d0d0'); + assert.equal(cm.colors.ansi[253], '#dadada'); + assert.equal(cm.colors.ansi[254], '#e4e4e4'); + assert.equal(cm.colors.ansi[255], '#eeeeee'); + }); + }); + + describe('setTheme', () => { + it('should set a partial set of colors, using the default if not present', () => { + assert.equal(cm.colors.background, '#000000'); + assert.equal(cm.colors.foreground, '#ffffff'); + cm.setTheme({ + background: '#FF0000', + foreground: '#00FF00' + }); + assert.equal(cm.colors.background, '#FF0000'); + assert.equal(cm.colors.foreground, '#00FF00'); + cm.setTheme({ + background: '#0000FF' + }); + assert.equal(cm.colors.background, '#0000FF'); + // FG reverts back to default + assert.equal(cm.colors.foreground, '#ffffff'); + }); + }); +}); diff --git a/src/renderer/ColorManager.ts b/src/renderer/ColorManager.ts index 17aee39285..2ae785ddeb 100644 --- a/src/renderer/ColorManager.ts +++ b/src/renderer/ColorManager.ts @@ -4,7 +4,7 @@ import { ITheme } from '../Interfaces'; const DEFAULT_FOREGROUND = '#ffffff'; const DEFAULT_BACKGROUND = '#000000'; const DEFAULT_CURSOR = '#ffffff'; -const DEFAULT_SELECTION = 'rgba(255, 255, 255, 0.9)'; +const DEFAULT_SELECTION = 'rgba(255, 255, 255, 0.3)'; export const DEFAULT_ANSI_COLORS = [ // dark: '#2e3436', From 1f38c2f4679c30cc34cb66991706fe509e1e000d Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 4 Sep 2017 09:53:53 -0700 Subject: [PATCH 092/108] Remove unused file --- src/renderer/Canvas.ts | 14 -------------- src/renderer/Renderer.ts | 1 - 2 files changed, 15 deletions(-) delete mode 100644 src/renderer/Canvas.ts diff --git a/src/renderer/Canvas.ts b/src/renderer/Canvas.ts deleted file mode 100644 index 97e7f9ea8a..0000000000 --- a/src/renderer/Canvas.ts +++ /dev/null @@ -1,14 +0,0 @@ -export function createBackgroundFillData(width: number, height: number, r: number, g: number, b: number, a: number): Uint8ClampedArray { - const data = new Uint8ClampedArray(width * height * 4); - let offset = 0; - for (let i = 0; i < height; i++) { - for (let j = 0; j < width; j++) { - data[offset] = r; - data[offset + 1] = g; - data[offset + 2] = b; - data[offset + 3] = a; - offset += 4; - } - } - return data; -} diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 6b12188eda..66fa6a1502 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -4,7 +4,6 @@ import { ITerminal, ITheme } from '../Interfaces'; import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; -import { createBackgroundFillData } from './Canvas'; import { BackgroundRenderLayer } from './BackgroundRenderLayer'; import { ForegroundRenderLayer } from './ForegroundRenderLayer'; import { SelectionRenderLayer } from './SelectionRenderLayer'; From 9506c01824a4a348cbd637945ffd7d3ba3e89ef9 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 4 Sep 2017 10:27:07 -0700 Subject: [PATCH 093/108] Add tests for utils/Mouse --- src/utils/Mouse.test.ts | 62 +++++++++++++++++++++++++++++++++++++ src/utils/Mouse.ts | 6 ++-- src/utils/TestUtils.test.ts | 8 +++++ 3 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 src/utils/Mouse.test.ts diff --git a/src/utils/Mouse.test.ts b/src/utils/Mouse.test.ts new file mode 100644 index 0000000000..15b68ef2d1 --- /dev/null +++ b/src/utils/Mouse.test.ts @@ -0,0 +1,62 @@ +/** + * @license MIT + */ + +import jsdom = require('jsdom'); +import { assert } from 'chai'; +import { getCoords } from './Mouse'; +import { MockCharMeasure } from './TestUtils.test'; + +const CHAR_WIDTH = 10; +const CHAR_HEIGHT = 20; + +describe('getCoords', () => { + let dom: jsdom.JSDOM; + let window: Window; + let document: Document; + + let charMeasure: MockCharMeasure; + + beforeEach(() => { + dom = new jsdom.JSDOM(''); + window = dom.window; + document = window.document; + charMeasure = new MockCharMeasure(); + charMeasure.width = CHAR_WIDTH; + charMeasure.height = CHAR_HEIGHT; + }); + + describe('when charMeasure is not initialized', () => { + it('should return null', () => { + charMeasure = new MockCharMeasure(); + assert.equal(getCoords({ pageX: 0, pageY: 0 }, document.createElement('div'), charMeasure, 1, 10, 10), null); + }); + }); + + describe('when pageX/pageY are not supported', () => { + it('should return null', () => { + assert.equal(getCoords({ pageX: undefined, pageY: undefined }, document.createElement('div'), charMeasure, 1, 10, 10), null); + }); + }); + + it('should return the cell that was clicked', () => { + let coords: [number, number]; + coords = getCoords({ pageX: CHAR_WIDTH / 2, pageY: CHAR_HEIGHT / 2 }, document.createElement('div'), charMeasure, 1, 10, 10); + assert.deepEqual(coords, [1, 1]); + coords = getCoords({ pageX: CHAR_WIDTH, pageY: CHAR_HEIGHT }, document.createElement('div'), charMeasure, 1, 10, 10); + assert.deepEqual(coords, [1, 1]); + coords = getCoords({ pageX: CHAR_WIDTH, pageY: CHAR_HEIGHT + 1 }, document.createElement('div'), charMeasure, 1, 10, 10); + assert.deepEqual(coords, [1, 2]); + coords = getCoords({ pageX: CHAR_WIDTH + 1, pageY: CHAR_HEIGHT }, document.createElement('div'), charMeasure, 1, 10, 10); + assert.deepEqual(coords, [2, 1]); + }); + + it('should ensure the coordinates are returned within the terminal bounds', () => { + let coords: [number, number]; + coords = getCoords({ pageX: -1, pageY: -1 }, document.createElement('div'), charMeasure, 1, 10, 10); + assert.deepEqual(coords, [1, 1]); + // Event are double the cols/rows + coords = getCoords({ pageX: CHAR_WIDTH * 20, pageY: CHAR_HEIGHT * 20 }, document.createElement('div'), charMeasure, 1, 10, 10); + assert.deepEqual(coords, [11, 11], 'coordinates should never come back as larger than the terminal'); + }); +}); diff --git a/src/utils/Mouse.ts b/src/utils/Mouse.ts index 0d433cd6f6..3bbad855ab 100644 --- a/src/utils/Mouse.ts +++ b/src/utils/Mouse.ts @@ -4,7 +4,7 @@ import { ICharMeasure } from '../Interfaces'; -export function getCoordsRelativeToElement(event: MouseEvent, element: HTMLElement): [number, number] { +export function getCoordsRelativeToElement(event: {pageX: number, pageY: number}, element: HTMLElement): [number, number] { // Ignore browsers that don't support MouseEvent.pageX if (event.pageX == null) { return null; @@ -15,7 +15,7 @@ export function getCoordsRelativeToElement(event: MouseEvent, element: HTMLEleme // Converts the coordinates from being relative to the document to being // relative to the terminal. - while (element && element !== self.document.documentElement) { + while (element) { x -= element.offsetLeft; y -= element.offsetTop; element = 'offsetParent' in element ? element.offsetParent : element.parentElement; @@ -36,7 +36,7 @@ export function getCoordsRelativeToElement(event: MouseEvent, element: HTMLEleme * apply an offset to the x value such that the left half of the cell will * select that cell and the right half will select the next cell. */ -export function getCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number] { +export function getCoords(event: {pageX: number, pageY: number}, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number] { // Coordinates cannot be measured if charMeasure has not been initialized if (!charMeasure.width || !charMeasure.height) { return null; diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index 51f191a836..ed0a19fbe4 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -68,6 +68,14 @@ export class MockTerminal implements ITerminal { } } +export class MockCharMeasure implements ICharMeasure { + width: number; + height: number; + measure(options: ITerminalOptions): void { + throw new Error('Method not implemented.'); + } +} + export class MockInputHandlingTerminal implements IInputHandlingTerminal { element: HTMLElement; options: ITerminalOptions = {}; From da1e3b40cca8017d3d311f57a7e1edc1032b359c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 4 Sep 2017 12:59:59 -0700 Subject: [PATCH 094/108] Don't use ImageBitmap on Firefox --- src/renderer/CharAtlas.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/renderer/CharAtlas.ts b/src/renderer/CharAtlas.ts index 05eb9a8418..e256287303 100644 --- a/src/renderer/CharAtlas.ts +++ b/src/renderer/CharAtlas.ts @@ -1,5 +1,6 @@ import { ITerminal, ITheme } from '../Interfaces'; import { IColorSet } from '../renderer/Interfaces'; +import { isFirefox } from '../utils/Browser'; export const CHAR_ATLAS_CELL_SPACING = 1; @@ -152,8 +153,10 @@ class CharAtlasGenerator { const charAtlasImageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); // Support is patchy for createImageBitmap at the moment, pass a canvas back - // if support is lacking as drawImage works there too. - if (!('createImageBitmap' in window)) { + // if support is lacking as drawImage works there too. Firefox is also + // included here as ImageBitmap appears both buggy and has horrible + // performance (tested on v55). + if (!('createImageBitmap' in window) || isFirefox) { // Regenerate canvas and context as they are now owned by the char atlas const result = this._canvas; this._canvas = this._document.createElement('canvas'); From 8e07af04e37d7765da9e63c015ea33f9401a2b75 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 4 Sep 2017 13:39:33 -0700 Subject: [PATCH 095/108] Drop support for IE/Opera and indicate latest Opera uses Blink under the hood so supporting Chrome is basically supporting Opera --- README.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index edfa1169cb..f6b805ef15 100644 --- a/README.md +++ b/README.md @@ -97,12 +97,10 @@ xterm.fit(); Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Here is a list of the versions we aim to support: -- Chrome 48+ -- Edge 13+ -- Firefox 44+ -- Internet Explorer 11+ -- Opera 35+ -- Safari 8+ +- Chrome latest +- Edge latest +- Firefox latest +- Safari latest Xterm.js works seamlessly in Electron apps and may even work on earlier versions of the browsers but these are the browsers we strive to keep working. From 0dc24cf800e4f4efdfbc7e4182c8262833610b59 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 4 Sep 2017 13:41:56 -0700 Subject: [PATCH 096/108] Move theme setting into setOption API --- src/Interfaces.ts | 1 + src/Terminal.ts | 46 ++++++++++++++++++++++++++++++++++------------ typings/xterm.d.ts | 15 ++++++++++----- 3 files changed, 45 insertions(+), 17 deletions(-) diff --git a/src/Interfaces.ts b/src/Interfaces.ts index da6d272728..a0d75a54c7 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -134,6 +134,7 @@ export interface ITerminalOptions { scrollback?: number; tabStopWidth?: number; termName?: string; + theme?: ITheme; useFlowControl?: boolean; } diff --git a/src/Terminal.ts b/src/Terminal.ts index cab25b03b6..15c6335a4c 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -82,7 +82,8 @@ const DEFAULT_OPTIONS: ITerminalOptions = { cancelEvents: false, disableStdin: false, useFlowControl: false, - tabStopWidth: 8 + tabStopWidth: 8, + theme: null // programFeatures: false, // focusKeys: false, }; @@ -320,16 +321,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT return document.activeElement === this.textarea; } - public setTheme(theme: ITheme): void { - // TODO: Allow setting of theme before renderer is ready - if (this.renderer) { - const colors = this.renderer.setTheme(theme); - if (this.viewport) { - this.viewport.onThemeChanged(colors); - } - } - } - /** * Retrieves an option's value from the terminal. * @param {string} key The option key. @@ -377,6 +368,14 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT return; } break; + case 'theme': + // If open has been called we do not want to set options.theme as the + // source of truth is owned by the renderer. + if (this.renderer) { + this._setTheme(value); + return; + } + break; case 'scrollback': if (value < 0) { console.warn(`${key} cannot be less than 0, value: ${value}`); @@ -420,7 +419,10 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT case 'bellSound': case 'bellStyle': this.syncBellSound(); break; } - this.renderer.onOptionsChanged(); + // Inform renderer of changes + if (this.renderer) { + this.renderer.onOptionsChanged(); + } } /** @@ -644,6 +646,15 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // Measure the character size this.charMeasure.measure(this.options); + // Set the theme if it was set via setOption/constructor before open. This + // must be run after CharMeasure.measure as it depends on char dimensions. + setTimeout(() => { + if (this.options.theme) { + this._setTheme(this.options.theme); + this.options.theme = null; + } + }, 0); + // Setup loop that draws to screen this.refresh(0, this.rows - 1); @@ -655,6 +666,17 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.bindMouse(); } + /** + * Sets the theme on the renderer. The renderer must have been initialized. + * @param theme The theme to ste. + */ + private _setTheme(theme: ITheme): void { + const colors = this.renderer.setTheme(theme); + if (this.viewport) { + this.viewport.onThemeChanged(colors); + } + } + /** * Attempts to load an add-on using CommonJS or RequireJS (whichever is available). * @param {string} addon The name of the addon to load diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index eff26c9bbc..d98f6c5d3f 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -71,6 +71,11 @@ interface ITerminalOptions { * The size of tab stops in the terminal. */ tabStopWidth?: number; + + /** + * The color theme of the terminal. + */ + theme?: ITheme; } /** @@ -464,13 +469,13 @@ declare module 'xterm' { * @param key The option key. * @param value The option value. */ - setOption(key: string, value: any): void; - + setOption(key: 'theme', value: ITheme): void; /** - * Sets the theme of the terminal. - * @param theme The theme to use. + * Sets an option on the terminal. + * @param key The option key. + * @param value The option value. */ - setTheme(theme: ITheme): void; + setOption(key: string, value: any): void; /** * Tells the renderer to refresh terminal content between two rows From 46b0857ef6c7d85420160ca8b19cef63f7336896 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 4 Sep 2017 14:13:41 -0700 Subject: [PATCH 097/108] Resolve TODOs --- src/Linkifier.ts | 7 +++++-- src/renderer/BaseRenderLayer.ts | 3 --- src/renderer/Renderer.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Linkifier.ts b/src/Linkifier.ts index d3150b4f64..e04ad767d9 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -77,7 +77,6 @@ export class Linkifier { // Clear out any existing links this._mouseZoneManager.clearAll(); - // TODO: Cancel any validation callbacks if (this._rowsTimeoutId) { clearTimeout(this._rowsTimeoutId); @@ -91,6 +90,7 @@ export class Linkifier { * @param end The row to end at. */ private _linkifyRows(start: number, end: number): void { + this._rowsTimeoutId = null; for (let i = start; i <= end; i++) { this._linkifyRow(i); } @@ -220,8 +220,11 @@ export class Linkifier { // Ensure the link is valid before registering if (matcher.validationCallback) { matcher.validationCallback(text, isValid => { + // Discard link if the line has already changed + if (this._rowsTimeoutId) { + return; + } if (isValid) { - // TODO: Discard link if the line has already changed? this._addLink(offset + index, rowIndex, uri, matcher); } }); diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 462c4eca96..08a229edb3 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -14,8 +14,6 @@ export abstract class BaseRenderLayer implements IRenderLayer { private scaledLineHeight: number; private scaledLineDrawY: number; - // TODO: This should be shared between terminals, but not for static as some - // terminals may have different styles private _charAtlas: HTMLCanvasElement | ImageBitmap; constructor( @@ -32,7 +30,6 @@ export abstract class BaseRenderLayer implements IRenderLayer { container.appendChild(this._canvas); } - // TODO: Should this do anything? public onOptionsChanged(terminal: ITerminal): void {} public onBlur(terminal: ITerminal): void {} public onFocus(terminal: ITerminal): void {} diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 66fa6a1502..a6d679a2d0 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -33,13 +33,13 @@ export class Renderer implements IRenderer { public setTheme(theme: ITheme): IColorSet { this._colorManager.setTheme(theme); + // Clear layers and force a full render this._renderLayers.forEach(l => { l.onThemeChanged(this._terminal, this._colorManager.colors); l.reset(this._terminal); }); - // TODO: This is currently done for every single terminal, but it's static so it's wasting time this._terminal.refresh(0, this._terminal.rows - 1); return this._colorManager.colors; From 1e728d453b7c5981853faa6978025ea23f9cfb7e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 4 Sep 2017 14:19:17 -0700 Subject: [PATCH 098/108] Add new API to typings test --- fixtures/typings-test/typings-test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/fixtures/typings-test/typings-test.ts b/fixtures/typings-test/typings-test.ts index 90958f83f0..df79d2d04e 100644 --- a/fixtures/typings-test/typings-test.ts +++ b/fixtures/typings-test/typings-test.ts @@ -168,6 +168,10 @@ namespace methods_core { t.setOption('bellStyle', 'visual'); t.setOption('bellStyle', 'sound'); t.setOption('bellStyle', 'both'); + t.setOption('fontSize', 1); + t.setOption('lineHeight', 1); + t.setOption('fontFamily', 'foo'); + t.setOption('theme', {background: '#ff0000'}); } } namespace scrolling { @@ -212,6 +216,9 @@ namespace methods_experimental { priority: 1, validationCallback: (uri: string, callback: (isValid: boolean) => void) => { console.log(uri, callback); + }, + hoverCallback: (e: MouseEvent, uri: string) => { + console.log(e, uri); } }); t.deregisterLinkMatcher(1); From 5c2d5dcd274335f1e7067f45b4a74960f9d9aa4f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 4 Sep 2017 19:34:57 -0700 Subject: [PATCH 099/108] Document methods of BaseRenderLayer --- src/renderer/BaseRenderLayer.ts | 94 +++++++++++++++++++++++---- src/renderer/CursorRenderLayer.ts | 10 +-- src/renderer/ForegroundRenderLayer.ts | 2 +- 3 files changed, 88 insertions(+), 18 deletions(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 08a229edb3..7bb7e9c5a7 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -41,6 +41,11 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._refreshCharAtlas(terminal, colorSet); } + /** + * Refreshes the char atlas, aquiring a new one if necessary. + * @param terminal The terminal. + * @param colorSet The color set to use for the char atlas. + */ private _refreshCharAtlas(terminal: ITerminal, colorSet: IColorSet): void { this._charAtlas = null; const result = acquireCharAtlas(terminal, this.colors); @@ -68,11 +73,24 @@ export abstract class BaseRenderLayer implements IRenderLayer { public abstract reset(terminal: ITerminal): void; - protected fillCells(startCol: number, startRow: number, colWidth: number, colHeight: number): void { - this._ctx.fillRect(startCol * this.scaledCharWidth, startRow * this.scaledLineHeight, colWidth * this.scaledCharWidth, colHeight * this.scaledLineHeight); + /** + * Fills 1+ cells completely. This uses the existing fillStyle on the context. + * @param x The column to start at. + * @param y The row to start at + * @param width The number of columns to fill. + * @param height The number of rows to fill. + */ + protected fillCells(x: number, y: number, width: number, height: number): void { + this._ctx.fillRect(x * this.scaledCharWidth, y * this.scaledLineHeight, width * this.scaledCharWidth, height * this.scaledLineHeight); } - protected drawBottomLineAtCell(x: number, y: number): void { + /** + * Fills a 1px line (2px on HDPI) at the bottom of the cell. This uses the + * existing fillStyle on the context. + * @param x The column to fill. + * @param y The row to fill. + */ + protected fillBottomLineAtCell(x: number, y: number): void { this._ctx.fillRect( x * this.scaledCharWidth, (y + 1) * this.scaledLineHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */, @@ -80,7 +98,13 @@ export abstract class BaseRenderLayer implements IRenderLayer { window.devicePixelRatio); } - protected drawLeftLineAtCell(x: number, y: number): void { + /** + * Fills a 1px line (2px on HDPI) at the left of the cell. This uses the + * existing fillStyle on the context. + * @param x The column to fill. + * @param y The row to fill. + */ + protected fillLeftLineAtCell(x: number, y: number): void { this._ctx.fillRect( x * this.scaledCharWidth, y * this.scaledLineHeight, @@ -88,8 +112,13 @@ export abstract class BaseRenderLayer implements IRenderLayer { this.scaledLineHeight); } - protected drawRectAtCell(x: number, y: number, width: number, height: number, color: string): void { - this._ctx.strokeStyle = color; + /** + * Strokes a 1px rectangle (2px on HDPI) around a cell. This uses the existing + * strokeStyle on the context. + * @param x The column to fill. + * @param y The row to fill. + */ + protected strokeRectAtCell(x: number, y: number, width: number, height: number): void { this._ctx.lineWidth = window.devicePixelRatio; this._ctx.strokeRect( x * this.scaledCharWidth + window.devicePixelRatio / 2, @@ -98,19 +127,37 @@ export abstract class BaseRenderLayer implements IRenderLayer { (height * this.scaledLineHeight) - window.devicePixelRatio); } + /** + * Clears the entire canvas. + */ protected clearAll(): void { this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); } - protected clearCells(startCol: number, startRow: number, colWidth: number, colHeight: number): void { - this._ctx.clearRect(startCol * this.scaledCharWidth, startRow * this.scaledLineHeight, colWidth * this.scaledCharWidth, colHeight * this.scaledLineHeight); + /** + * Clears 1+ cells completely. + * @param x The column to start at. + * @param y The row to start at. + * @param width The number of columns to clear. + * @param height The number of rows to clear. + */ + protected clearCells(x: number, y: number, width: number, height: number): void { + this._ctx.clearRect(x * this.scaledCharWidth, y * this.scaledLineHeight, width * this.scaledCharWidth, height * this.scaledLineHeight); } - protected drawCharTrueColor(terminal: ITerminal, charData: CharData, x: number, y: number, color: string): void { - this._ctx.save(); + /** + * Draws a truecolor character at the cell. The character will be clipped to + * ensure that it fits with the cell, including the cell to the right if it's + * a wide character. This uses the existing fillStyle on the context. + * @param terminal The terminal. + * @param charData The char data for the character to draw. + * @param x The column to draw at. + * @param y The row to draw at. + * @param color The color of the character. + */ + protected fillCharTrueColor(terminal: ITerminal, charData: CharData, x: number, y: number): void { this._ctx.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`; this._ctx.textBaseline = 'top'; - this._ctx.fillStyle = color; // Since uncached characters are not coming off the char atlas with source // coordinates, it means that text drawn to the canvas (particularly '_') @@ -119,11 +166,21 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.beginPath(); this._ctx.rect(x * this.scaledCharWidth, y * this.scaledLineHeight + this.scaledLineDrawY, charData[CHAR_DATA_WIDTH_INDEX] * this.scaledCharWidth, this.scaledCharHeight); this._ctx.clip(); - this._ctx.fillText(charData[CHAR_DATA_CHAR_INDEX], x * this.scaledCharWidth, y * this.scaledCharHeight); - this._ctx.restore(); } + /** + * Draws a character at a cell. If possible this will draw using the character + * atlas to reduce draw time. + * @param terminal The terminal. + * @param char The character. + * @param code The character code. + * @param width The width of the character. + * @param x The column to draw at. + * @param y The row to draw at. + * @param fg The foreground color, in the format stored within the attributes. + * @param bold Whether the text is bold. + */ protected drawChar(terminal: ITerminal, char: string, code: number, width: number, x: number, y: number, fg: number, bold: boolean): void { // Clear the cell next to this character if it's wide if (width === 2) { @@ -157,6 +214,17 @@ export abstract class BaseRenderLayer implements IRenderLayer { // this._ctx.drawImage(this._charAtlas, 0, 0); } + /** + * Draws a character at a cell. The character will be clipped to + * ensure that it fits with the cell, including the cell to the right if it's + * a wide character. + * @param terminal The terminal. + * @param char The character. + * @param width The width of the character. + * @param fg The foreground color, in the format stored within the attributes. + * @param x The column to draw at. + * @param y The row to draw at. + */ private _drawUncachedChar(terminal: ITerminal, char: string, width: number, fg: number, x: number, y: number): void { this._ctx.save(); this._ctx.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`; diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index 00abfb9332..9f5136ff75 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -186,7 +186,7 @@ export class CursorRenderLayer extends BaseRenderLayer { private _renderBarCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void { this._ctx.save(); this._ctx.fillStyle = this.colors.cursor; - this.drawLeftLineAtCell(x, y); + this.fillLeftLineAtCell(x, y); this._ctx.restore(); } @@ -194,20 +194,22 @@ export class CursorRenderLayer extends BaseRenderLayer { this._ctx.save(); this._ctx.fillStyle = this.colors.cursor; this.fillCells(x, y, charData[CHAR_DATA_WIDTH_INDEX], 1); + this._ctx.fillStyle = this.colors.background; + this.fillCharTrueColor(terminal, charData, x, y); this._ctx.restore(); - this.drawCharTrueColor(terminal, charData, x, y, this.colors.background); } private _renderUnderlineCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void { this._ctx.save(); this._ctx.fillStyle = this.colors.cursor; - this.drawBottomLineAtCell(x, y); + this.fillBottomLineAtCell(x, y); this._ctx.restore(); } private _renderBlurCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void { this._ctx.save(); - this.drawRectAtCell(x, y, charData[CHAR_DATA_WIDTH_INDEX], 1, this.colors.cursor); + this._ctx.strokeStyle = this.colors.cursor; + this.strokeRectAtCell(x, y, charData[CHAR_DATA_WIDTH_INDEX], 1); this._ctx.restore(); } } diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index ef9c3c600b..05141aedfc 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -139,7 +139,7 @@ export class ForegroundRenderLayer extends BaseRenderLayer { } else { this._ctx.fillStyle = this.colors.foreground; } - this.drawBottomLineAtCell(x, y); + this.fillBottomLineAtCell(x, y); } this.drawChar(terminal, char, code, width, x, y, fg, !!(flags & FLAGS.BOLD)); From 7b199ac17f52146296fd30986a1996bb96258074 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 4 Sep 2017 19:40:49 -0700 Subject: [PATCH 100/108] Some more documentation --- src/renderer/CharAtlas.ts | 10 ++++++++++ src/renderer/ColorManager.ts | 7 +++++++ 2 files changed, 17 insertions(+) diff --git a/src/renderer/CharAtlas.ts b/src/renderer/CharAtlas.ts index e256287303..b4264567ad 100644 --- a/src/renderer/CharAtlas.ts +++ b/src/renderer/CharAtlas.ts @@ -20,6 +20,12 @@ interface ICharAtlasCacheEntry { let charAtlasCache: ICharAtlasCacheEntry[] = []; +/** + * Acquires a char atlas, either generating a new one or returning an existing + * one that is in use by another terminal. + * @param terminal The terminal. + * @param colors The colors to use. + */ export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet): HTMLCanvasElement | Promise { const scaledCharWidth = terminal.charMeasure.width * window.devicePixelRatio; const scaledCharHeight = terminal.charMeasure.height * window.devicePixelRatio; @@ -95,6 +101,10 @@ function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean { let generator: CharAtlasGenerator; +/** + * Initializes the char atlas generator. + * @param document The document. + */ export function initialize(document: Document): void { if (!generator) { generator = new CharAtlasGenerator(document); diff --git a/src/renderer/ColorManager.ts b/src/renderer/ColorManager.ts index 2ae785ddeb..057292f77c 100644 --- a/src/renderer/ColorManager.ts +++ b/src/renderer/ColorManager.ts @@ -26,6 +26,10 @@ export const DEFAULT_ANSI_COLORS = [ '#eeeeec' ]; +/** + * Fills an existing 16 length string with the remaining 240 ANSI colors. + * @param first16Colors The first 16 ANSI colors. + */ function generate256Colors(first16Colors: string[]): string[] { let colors = first16Colors.slice(); @@ -52,6 +56,9 @@ function toPaddedHex(c: number): string { return s.length < 2 ? '0' + s : s; } +/** + * Manages the source of truth for a terminal's colors. + */ export class ColorManager { public colors: IColorSet; From b69ff5d31aa7b68039f8338abf708981fbc08134 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 6 Sep 2017 10:22:33 -0700 Subject: [PATCH 101/108] Redraw terminal when devicePixelRatio changes --- src/Terminal.ts | 11 ++++------- src/renderer/Interfaces.ts | 5 +++-- src/renderer/Renderer.ts | 28 ++++++++++++++++++++-------- src/utils/TestUtils.test.ts | 5 +++-- 4 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 15c6335a4c..d42978f734 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -408,7 +408,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT case 'lineHeight': // When the font changes the size of the cells may change which requires a renderer clear this.renderer.clear(); - this.renderer.onResize(this.cols, this.rows); + this.renderer.onResize(this.cols, this.rows, false); this.refresh(0, this.rows - 1); // this.charMeasure.measure(this.options); case 'scrollback': @@ -620,14 +620,11 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.charMeasure.on('charsizechanged', () => this.viewport.syncScrollArea()); this.renderer = new Renderer(this); this.on('cursormove', () => this.renderer.onCursorMove()); - this.on('resize', () => this.renderer.onResize(this.cols, this.rows)); + this.on('resize', () => this.renderer.onResize(this.cols, this.rows, false)); this.on('blur', () => this.renderer.onBlur()); this.on('focus', () => this.renderer.onFocus()); - this.charMeasure.on('charsizechanged', () => { - this.renderer.onCharSizeChanged(this.charMeasure.width, this.charMeasure.height); - // Force a refresh for the char size change - this.renderer.queueRefresh(0, this.rows - 1); - }); + window.addEventListener('resize', () => this.renderer.onWindowResize(window.devicePixelRatio)); + this.charMeasure.on('charsizechanged', () => this.renderer.onResize(this.cols, this.rows, true)); this.selectionManager = new SelectionManager(this, this.buffer, this.charMeasure); this.element.addEventListener('mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e)); diff --git a/src/renderer/Interfaces.ts b/src/renderer/Interfaces.ts index 04e24e19b5..e6b09843df 100644 --- a/src/renderer/Interfaces.ts +++ b/src/renderer/Interfaces.ts @@ -2,8 +2,9 @@ import { ITerminal, ITerminalOptions, ITheme } from '../Interfaces'; export interface IRenderer { setTheme(theme: ITheme): IColorSet; - onResize(cols: number, rows: number): void; - onCharSizeChanged(charWidth: number, charHeight: number): void; + onWindowResize(devicePixelRatio: number): void; + onResize(cols: number, rows: number, didCharSizeChange: boolean): void; + onCharSizeChanged(): void; onBlur(): void; onFocus(): void; onSelectionChanged(start: [number, number], end: [number, number]): void; diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index a6d679a2d0..c032bc8f8f 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -18,6 +18,7 @@ export class Renderer implements IRenderer { private _refreshAnimationFrame = null; private _renderLayers: IRenderLayer[]; + private _devicePixelRatio: number; private _colorManager: ColorManager; @@ -29,6 +30,16 @@ export class Renderer implements IRenderer { new ForegroundRenderLayer(this._terminal.element, 2, this._colorManager.colors), new CursorRenderLayer(this._terminal.element, 3, this._colorManager.colors) ]; + this._devicePixelRatio = window.devicePixelRatio; + } + + public onWindowResize(devicePixelRatio: number): void { + // If the device pixel ratio changed, the char atlas needs to be regenerated + // and the terminal needs to refreshed + if (this._devicePixelRatio !== devicePixelRatio) { + this._devicePixelRatio = devicePixelRatio; + this.onResize(this._terminal.cols, this._terminal.rows, true); + } } public setTheme(theme: ITheme): IColorSet { @@ -45,19 +56,20 @@ export class Renderer implements IRenderer { return this._colorManager.colors; } - public onResize(cols: number, rows: number): void { + public onResize(cols: number, rows: number, didCharSizeChange: boolean): void { if (!this._terminal.charMeasure.width || !this._terminal.charMeasure.height) { return; } - const width = this._terminal.charMeasure.width * this._terminal.cols; - const height = Math.floor(this._terminal.charMeasure.height * this._terminal.options.lineHeight) * this._terminal.rows; - this._renderLayers.forEach(l => l.resize(this._terminal, width, height, false)); + const width = this._terminal.charMeasure.width * cols; + const height = Math.floor(this._terminal.charMeasure.height * this._terminal.options.lineHeight) * rows; + // Resize all render layers + this._renderLayers.forEach(l => l.resize(this._terminal, width, height, didCharSizeChange)); + // Force a refresh + this._terminal.refresh(0, this._terminal.rows - 1); } - public onCharSizeChanged(charWidth: number, charHeight: number): void { - const width = charWidth * this._terminal.cols; - const height = Math.floor(charHeight * this._terminal.options.lineHeight) * this._terminal.rows; - this._renderLayers.forEach(l => l.resize(this._terminal, width, height, true)); + public onCharSizeChanged(): void { + this.onResize(this._terminal.cols, this._terminal.rows, true); } public onBlur(): void { diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index ed0a19fbe4..652c3aa33b 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -214,13 +214,14 @@ export class MockBuffer implements IBuffer { export class MockRenderer implements IRenderer { setTheme(theme: ITheme): IColorSet { return {}; } - onResize(cols: number, rows: number): void {} - onCharSizeChanged(charWidth: number, charHeight: number): void {} + onResize(cols: number, rows: number, didCharSizeChange: boolean): void {} + onCharSizeChanged(): void {} onBlur(): void {} onFocus(): void {} onSelectionChanged(start: [number, number], end: [number, number]): void {} onCursorMove(): void {} onOptionsChanged(): void {} + onWindowResize(devicePixelRatio: number): void {} clear(): void {} queueRefresh(start: number, end: number): void {} } From fb90c98f819848c0339a45dfa4bf6dcd59afa8d7 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 6 Sep 2017 10:36:34 -0700 Subject: [PATCH 102/108] Fix blurry text by supporting floating pt devicePixelRatios --- src/renderer/BaseRenderLayer.ts | 29 ++++++++++++++++++++++++----- src/renderer/CharAtlas.ts | 4 +--- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 7bb7e9c5a7..0e657ae777 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -48,7 +48,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { */ private _refreshCharAtlas(terminal: ITerminal, colorSet: IColorSet): void { this._charAtlas = null; - const result = acquireCharAtlas(terminal, this.colors); + const result = acquireCharAtlas(terminal, this.colors, this.scaledCharWidth, this.scaledCharHeight); if (result instanceof HTMLCanvasElement) { this._charAtlas = result; } else { @@ -57,12 +57,31 @@ export abstract class BaseRenderLayer implements IRenderLayer { } public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void { - this.scaledCharWidth = terminal.charMeasure.width * window.devicePixelRatio; - this.scaledCharHeight = terminal.charMeasure.height * window.devicePixelRatio; + // Calculate the scaled character dimensions, if devicePixelRatio is a + // floating point number then the value is ceiled to ensure there is enough + // space to draw the character to the cell + this.scaledCharWidth = Math.ceil(terminal.charMeasure.width * window.devicePixelRatio); + this.scaledCharHeight = Math.ceil(terminal.charMeasure.height * window.devicePixelRatio); + + // Calculate the scaled line height, if lineHeight is not 1 then the value + // will be floored because since lineHeight can never be lower then 1, there + // is a guarentee that the scaled line height will always be larger than + // scaled char height. this.scaledLineHeight = Math.floor(this.scaledCharHeight * terminal.options.lineHeight); + + // Calculate the y coordinate within a cell that text should draw from in + // order to draw in the center of a cell. this.scaledLineDrawY = terminal.options.lineHeight === 1 ? 0 : Math.round((this.scaledLineHeight - this.scaledCharHeight) / 2); - this._canvas.width = canvasWidth * window.devicePixelRatio; - this._canvas.height = canvasHeight * window.devicePixelRatio; + + // Recalcualte the canvas dimensions; width/height define the actual number + // of pixels in the canvas, style.width/height define the size of the canvas + // on the page. It's very important that this rounds to nearest integer and + // not ceils as browsers often set window.devicePixelRatio as something like + // 1.100000023841858, when it's actually 1.1. Ceiling causes blurriness as + // the backing canvas image is 1 pixel too large for the canvas element + // size. + this._canvas.width = Math.round(canvasWidth * window.devicePixelRatio); + this._canvas.height = Math.round(canvasHeight * window.devicePixelRatio); this._canvas.style.width = `${canvasWidth}px`; this._canvas.style.height = `${canvasHeight}px`; diff --git a/src/renderer/CharAtlas.ts b/src/renderer/CharAtlas.ts index b4264567ad..434b263baa 100644 --- a/src/renderer/CharAtlas.ts +++ b/src/renderer/CharAtlas.ts @@ -26,9 +26,7 @@ let charAtlasCache: ICharAtlasCacheEntry[] = []; * @param terminal The terminal. * @param colors The colors to use. */ -export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet): HTMLCanvasElement | Promise { - const scaledCharWidth = terminal.charMeasure.width * window.devicePixelRatio; - const scaledCharHeight = terminal.charMeasure.height * window.devicePixelRatio; +export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledCharWidth: number, scaledCharHeight: number): HTMLCanvasElement | Promise { const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, terminal, colors); // Check to see if the terminal already owns this config From 66c891df5b0243cd276740ffb3bf169120317680 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 6 Sep 2017 15:36:21 -0700 Subject: [PATCH 103/108] Fix resize adding right padding on demo --- demo/main.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/demo/main.js b/demo/main.js index f05a8cef16..f4fb0a2a01 100644 --- a/demo/main.js +++ b/demo/main.js @@ -2,9 +2,7 @@ var term, protocol, socketURL, socket, - pid, - charWidth, - charHeight; + pid; var terminalContainer = document.getElementById('terminal-container'), actionElements = { @@ -21,11 +19,13 @@ var terminalContainer = document.getElementById('terminal-container'), colsElement = document.getElementById('cols'), rowsElement = document.getElementById('rows'); -function setTerminalSize () { - var cols = parseInt(colsElement.value, 10), - rows = parseInt(rowsElement.value, 10), - width = (cols * charWidth).toString() + 'px', - height = (rows * charHeight).toString() + 'px'; +function setTerminalSize() { + var cols = parseInt(colsElement.value, 10); + var rows = parseInt(rowsElement.value, 10); + var viewportElement = document.querySelector('.xterm-viewport'); + var scrollBarWidth = viewportElement.offsetWidth - viewportElement.clientWidth; + var width = (cols * term.charMeasure.width + 20 /*room for scrollbar*/).toString() + 'px'; + var height = (rows * term.charMeasure.height).toString() + 'px'; terminalContainer.style.width = width; terminalContainer.style.height = height; @@ -97,10 +97,10 @@ function createTerminal() { colsElement.value = term.cols; rowsElement.value = term.rows; - fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, {method: 'POST'}).then(function (res) { + // Set terminal size again to set the specific dimensions on the demo + setTerminalSize(); - charWidth = Math.ceil(term.element.offsetWidth / term.cols); - charHeight = Math.ceil(term.element.offsetHeight / term.rows); + fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, {method: 'POST'}).then(function (res) { res.text().then(function (pid) { window.pid = pid; From e960a0a82ce8dd82329eaa99c06db73ff8e53357 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 6 Sep 2017 16:05:09 -0700 Subject: [PATCH 104/108] Add ILinkMatcherOptions.hoverEndCallback --- src/Interfaces.ts | 8 ++++++-- src/Linkifier.ts | 12 +++++++++--- src/Types.ts | 3 ++- src/input/Interfaces.ts | 3 ++- src/input/MouseZoneManager.ts | 22 ++++++++++++++++++---- typings/xterm.d.ts | 9 +++++++-- 6 files changed, 44 insertions(+), 13 deletions(-) diff --git a/src/Interfaces.ts b/src/Interfaces.ts index a0d75a54c7..790bf4191e 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -241,9 +241,13 @@ export interface ILinkMatcherOptions { */ validationCallback?: LinkMatcherValidationCallback; /** - * A callback that fired when the mouse hovers over a link. + * A callback that fires when the mouse hovers over a link. */ - hoverCallback?: LinkMatcherHandler; + hoverStartCallback?: LinkMatcherHandler; + /** + * A callback that fires when the mouse leaves a link that was hovered. + */ + hoverEndCallback?: () => void; /** * The priority of the link matcher, this defines the order in which the link * matcher is evaluated relative to others, from highest to lowest. The diff --git a/src/Linkifier.ts b/src/Linkifier.ts index e04ad767d9..2adb6f63b9 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -133,7 +133,8 @@ export class Linkifier { handler, matchIndex: options.matchIndex, validationCallback: options.validationCallback, - hoverCallback: options.hoverCallback, + hoverStartCallback: options.hoverStartCallback, + hoverEndCallback: options.hoverEndCallback, priority: options.priority || 0 }; this._addLinkMatcherToList(matcher); @@ -259,8 +260,13 @@ export class Linkifier { window.open(uri, '_blank'); }, e => { - if (matcher.hoverCallback) { - return matcher.hoverCallback(e, uri); + if (matcher.hoverStartCallback) { + matcher.hoverStartCallback(e, uri); + } + }, + () => { + if (matcher.hoverEndCallback) { + matcher.hoverEndCallback(); } } )); diff --git a/src/Types.ts b/src/Types.ts index 1ced3d73a8..1af21e7886 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -6,7 +6,8 @@ export type LinkMatcher = { id: number, regex: RegExp, handler: LinkMatcherHandler, - hoverCallback?: LinkMatcherHandler, + hoverStartCallback?: LinkMatcherHandler, + hoverEndCallback?: () => void, matchIndex?: number, validationCallback?: LinkMatcherValidationCallback, priority?: number diff --git a/src/input/Interfaces.ts b/src/input/Interfaces.ts index 15dcbae9b1..8756e5401e 100644 --- a/src/input/Interfaces.ts +++ b/src/input/Interfaces.ts @@ -8,5 +8,6 @@ export interface IMouseZone { x2: number; y: number; clickCallback: (e: MouseEvent) => any; - hoverCallback?: (e: MouseEvent) => any; + hoverStartCallback?: (e: MouseEvent) => any; + hoverEndCallback?: () => any; } diff --git a/src/input/MouseZoneManager.ts b/src/input/MouseZoneManager.ts index 37a6647d35..7789082e65 100644 --- a/src/input/MouseZoneManager.ts +++ b/src/input/MouseZoneManager.ts @@ -20,7 +20,8 @@ export class MouseZoneManager implements IMouseZoneManager { private _mouseDownListener: (e: MouseEvent) => any; private _clickListener: (e: MouseEvent) => any; - private _hoverTimeout: number; + private _hoverTimeout: number = null; + private _currentZone: IMouseZone = null; private _lastHoverCoords: [number, number] = [null, null]; constructor( @@ -60,11 +61,22 @@ export class MouseZoneManager implements IMouseZoneManager { } private _onMouseMove(e: MouseEvent): void { + // TODO: Ideally this would only clear the hover state when the mouse moves + // outside of the mouse zone if (this._lastHoverCoords[0] !== e.pageX && this._lastHoverCoords[1] !== e.pageY) { + // Restart the timeout if (this._hoverTimeout) { clearTimeout(this._hoverTimeout); } this._hoverTimeout = setTimeout(() => this._onHover(e), HOVER_DURATION); + + // Fire the hover end callback if a zone was being hovered + if (this._currentZone) { + this._currentZone.hoverEndCallback(); + this._currentZone = null; + } + + // Record the current coordinates this._lastHoverCoords = [e.pageX, e.pageY]; } } @@ -72,8 +84,9 @@ export class MouseZoneManager implements IMouseZoneManager { private _onHover(e: MouseEvent): void { const coords = getCoords(e, this._terminal.element, this._terminal.charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows); const zone = this._findZoneEventAt(e); - if (zone && zone.hoverCallback) { - zone.hoverCallback(e); + if (zone && zone.hoverStartCallback) { + this._currentZone = zone; + zone.hoverStartCallback(e); } } @@ -103,7 +116,8 @@ export class MouseZone implements IMouseZone { public x2: number, public y: number, public clickCallback: (e: MouseEvent) => any, - public hoverCallback?: (e: MouseEvent) => any + public hoverStartCallback?: (e: MouseEvent) => any, + public hoverEndCallback?: () => void ) { } } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index d98f6c5d3f..b219732a3b 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -141,9 +141,14 @@ interface ILinkMatcherOptions { validationCallback?: (uri: string, callback: (isValid: boolean) => void) => void; /** - * A callback that fired when the mouse hovers over a link. + * A callback that fires when the mouse hovers over a link. */ - hoverCallback?: (event: MouseEvent, uri: string) => boolean | void; + hoverStartCallback?: (event: MouseEvent, uri: string) => boolean | void; + + /** + * A callback that fires when the mouse leaves a link that was hovered. + */ + hoverEndCallback?: (event: MouseEvent, uri: string) => boolean | void; /** * The priority of the link matcher, this defines the order in which the link From 67962a5e5530ee872f8c24182cbd69a4063315f7 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 7 Sep 2017 07:19:48 -0700 Subject: [PATCH 105/108] typescript@2.4 For string enums --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 21ab9b8936..f32918dfac 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "nodemon": "1.10.2", "sorcery": "^0.10.0", "tslint": "^4.0.2", - "typescript": "~2.3.0", + "typescript": "~2.4.0", "vinyl-buffer": "^1.0.0", "vinyl-source-stream": "^1.1.0" }, From ea8dbbd29fd9d61ffa0e4adf510363773daafdc9 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 7 Sep 2017 10:20:48 -0700 Subject: [PATCH 106/108] Underline links on hover --- src/Interfaces.ts | 22 +++++++--- src/Linkifier.ts | 31 ++++++++------ src/Terminal.ts | 4 +- src/Types.ts | 16 ++++++- src/input/Interfaces.ts | 5 ++- src/input/MouseZoneManager.ts | 60 ++++++++++++++++++--------- src/renderer/BaseRenderLayer.ts | 4 +- src/renderer/CursorRenderLayer.ts | 2 +- src/renderer/ForegroundRenderLayer.ts | 2 +- src/renderer/LinkRenderLayer.ts | 44 ++++++++++++++++++++ src/renderer/Renderer.ts | 4 +- src/utils/TestUtils.test.ts | 3 +- src/xterm.css | 2 +- typings/xterm.d.ts | 9 ++-- 14 files changed, 153 insertions(+), 55 deletions(-) create mode 100644 src/renderer/LinkRenderLayer.ts diff --git a/src/Interfaces.ts b/src/Interfaces.ts index 790bf4191e..b777968886 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -5,6 +5,7 @@ import { ILinkMatcherOptions } from './Interfaces'; import { LinkMatcherHandler, LinkMatcherValidationCallback, Charset, LineData } from './Types'; import { IColorSet } from './renderer/Interfaces'; +import { IMouseZoneManager } from './input/Interfaces'; export interface IBrowser { isNode: boolean; @@ -22,8 +23,15 @@ export interface IBufferAccessor { buffer: IBuffer; } -export interface ITerminal extends IBufferAccessor, IEventEmitter { +export interface IElementAccessor { element: HTMLElement; +} + +export interface ILinkifierAccessor { + linkifier: ILinkifier; +} + +export interface ITerminal extends ILinkifierAccessor, IBufferAccessor, IElementAccessor, IEventEmitter { selectionManager: ISelectionManager; charMeasure: ICharMeasure; textarea: HTMLTextAreaElement; @@ -197,9 +205,11 @@ export interface ICharMeasure { measure(options: ITerminalOptions): void; } -export interface ILinkifier { - linkifyRow(rowIndex: number): void; - attachHypertextLinkHandler(handler: LinkMatcherHandler): void; +export interface ILinkifier extends IEventEmitter { + attachToDom(mouseZoneManager: IMouseZoneManager): void; + linkifyRows(start: number, end: number): void; + setHypertextLinkHandler(handler: LinkMatcherHandler): void; + setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void; registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number; deregisterLinkMatcher(matcherId: number): boolean; } @@ -243,11 +253,11 @@ export interface ILinkMatcherOptions { /** * A callback that fires when the mouse hovers over a link. */ - hoverStartCallback?: LinkMatcherHandler; + tooltipCallback?: LinkMatcherHandler; /** * A callback that fires when the mouse leaves a link that was hovered. */ - hoverEndCallback?: () => void; + leaveCallback?: () => void; /** * The priority of the link matcher, this defines the order in which the link * matcher is evaluated relative to others, from highest to lowest. The diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 2adb6f63b9..55d8d68d95 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -2,12 +2,11 @@ * @license MIT */ -import { ILinkMatcherOptions, ITerminal, IBufferAccessor } from './Interfaces'; -import { LinkMatcher, LinkMatcherHandler, LinkMatcherValidationCallback, LineData } from './Types'; +import { ILinkMatcherOptions, ITerminal, IBufferAccessor, ILinkifier, IElementAccessor } from './Interfaces'; +import { LinkMatcher, LinkMatcherHandler, LinkMatcherValidationCallback, LineData, LinkHoverEvent, LinkHoverEventTypes } from './Types'; import { IMouseZoneManager } from './input/Interfaces'; import { MouseZone } from './input/MouseZoneManager'; - -const INVALID_LINK_CLASS = 'xterm-invalid-link'; +import { EventEmitter } from './EventEmitter'; const protocolClause = '(https?:\\/\\/)'; const domainCharacterSet = '[\\da-z\\.-]+'; @@ -36,7 +35,7 @@ const HYPERTEXT_LINK_MATCHER_ID = 0; /** * The Linkifier applies links to rows shortly after they have been refreshed. */ -export class Linkifier { +export class Linkifier extends EventEmitter implements ILinkifier { /** * The time to wait after a row is changed before it is linkified. This prevents * the costly operation of searching every row multiple times, potentially a @@ -51,8 +50,9 @@ export class Linkifier { private _nextLinkMatcherId = HYPERTEXT_LINK_MATCHER_ID; constructor( - private _terminal: IBufferAccessor + private _terminal: IBufferAccessor & IElementAccessor ) { + super(); this.registerLinkMatcher(strictUrlRegex, null, { matchIndex: 1 }); } @@ -133,8 +133,8 @@ export class Linkifier { handler, matchIndex: options.matchIndex, validationCallback: options.validationCallback, - hoverStartCallback: options.hoverStartCallback, - hoverEndCallback: options.hoverEndCallback, + hoverTooltipCallback: options.tooltipCallback, + hoverLeaveCallback: options.leaveCallback, priority: options.priority || 0 }; this._addLinkMatcherToList(matcher); @@ -260,13 +260,20 @@ export class Linkifier { window.open(uri, '_blank'); }, e => { - if (matcher.hoverStartCallback) { - matcher.hoverStartCallback(e, uri); + this.emit(LinkHoverEventTypes.HOVER, { x, y, length: uri.length}); + this._terminal.element.style.cursor = 'pointer'; + }, + e => { + this.emit(LinkHoverEventTypes.TOOLTIP, { x, y, length: uri.length}); + if (matcher.hoverTooltipCallback) { + matcher.hoverTooltipCallback(e, uri); } }, () => { - if (matcher.hoverEndCallback) { - matcher.hoverEndCallback(); + this.emit(LinkHoverEventTypes.LEAVE, { x, y, length: uri.length}); + this._terminal.element.style.cursor = ''; + if (matcher.hoverLeaveCallback) { + matcher.hoverLeaveCallback(); } } )); diff --git a/src/Terminal.ts b/src/Terminal.ts index d42978f734..1a180cc152 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -38,7 +38,7 @@ import * as Mouse from './utils/Mouse'; import { CHARSETS } from './Charsets'; import { getRawByteCoords } from './utils/Mouse'; import { CustomKeyEventHandler, Charset, LinkMatcherHandler, LinkMatcherValidationCallback, CharData, LineData } from './Types'; -import { ITerminal, IBrowser, ITerminalOptions, IInputHandlingTerminal, ILinkMatcherOptions, IViewport, ICompositionHelper, ITheme } from './Interfaces'; +import { ITerminal, IBrowser, ITerminalOptions, IInputHandlingTerminal, ILinkMatcherOptions, IViewport, ICompositionHelper, ITheme, ILinkifier } from './Interfaces'; import { BellSound } from './utils/Sounds'; import { DEFAULT_ANSI_COLORS } from './renderer/ColorManager'; import { IMouseZoneManager } from './input/Interfaces'; @@ -188,7 +188,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT private parser: Parser; private renderer: IRenderer; public selectionManager: SelectionManager; - private linkifier: Linkifier; + public linkifier: ILinkifier; public buffers: BufferSet; public buffer: Buffer; public viewport: IViewport; diff --git a/src/Types.ts b/src/Types.ts index 1af21e7886..06d495026c 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -6,8 +6,8 @@ export type LinkMatcher = { id: number, regex: RegExp, handler: LinkMatcherHandler, - hoverStartCallback?: LinkMatcherHandler, - hoverEndCallback?: () => void, + hoverTooltipCallback?: LinkMatcherHandler, + hoverLeaveCallback?: () => void, matchIndex?: number, validationCallback?: LinkMatcherValidationCallback, priority?: number @@ -20,3 +20,15 @@ export type Charset = {[key: string]: string}; export type CharData = [number, string, number, number]; export type LineData = CharData[]; + +export type LinkHoverEvent = { + x: number, + y: number, + length: number +}; + +export enum LinkHoverEventTypes { + HOVER = 'linkhover', + TOOLTIP = 'linktooltip', + LEAVE = 'linkleave' +}; diff --git a/src/input/Interfaces.ts b/src/input/Interfaces.ts index 8756e5401e..19125d0801 100644 --- a/src/input/Interfaces.ts +++ b/src/input/Interfaces.ts @@ -8,6 +8,7 @@ export interface IMouseZone { x2: number; y: number; clickCallback: (e: MouseEvent) => any; - hoverStartCallback?: (e: MouseEvent) => any; - hoverEndCallback?: () => any; + hoverCallback?: (e: MouseEvent) => any; + tooltipCallback?: (e: MouseEvent) => any; + leaveCallback?: () => any; } diff --git a/src/input/MouseZoneManager.ts b/src/input/MouseZoneManager.ts index 7789082e65..1a6cedada9 100644 --- a/src/input/MouseZoneManager.ts +++ b/src/input/MouseZoneManager.ts @@ -20,7 +20,7 @@ export class MouseZoneManager implements IMouseZoneManager { private _mouseDownListener: (e: MouseEvent) => any; private _clickListener: (e: MouseEvent) => any; - private _hoverTimeout: number = null; + private _tooltipTimeout: number = null; private _currentZone: IMouseZone = null; private _lastHoverCoords: [number, number] = [null, null]; @@ -63,30 +63,49 @@ export class MouseZoneManager implements IMouseZoneManager { private _onMouseMove(e: MouseEvent): void { // TODO: Ideally this would only clear the hover state when the mouse moves // outside of the mouse zone - if (this._lastHoverCoords[0] !== e.pageX && this._lastHoverCoords[1] !== e.pageY) { - // Restart the timeout - if (this._hoverTimeout) { - clearTimeout(this._hoverTimeout); - } - this._hoverTimeout = setTimeout(() => this._onHover(e), HOVER_DURATION); - - // Fire the hover end callback if a zone was being hovered - if (this._currentZone) { - this._currentZone.hoverEndCallback(); - this._currentZone = null; - } - + if (this._lastHoverCoords[0] !== e.pageX || this._lastHoverCoords[1] !== e.pageY) { + this._onHover(e); // Record the current coordinates this._lastHoverCoords = [e.pageX, e.pageY]; } } private _onHover(e: MouseEvent): void { - const coords = getCoords(e, this._terminal.element, this._terminal.charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows); const zone = this._findZoneEventAt(e); - if (zone && zone.hoverStartCallback) { - this._currentZone = zone; - zone.hoverStartCallback(e); + + // Do nothing if the zone is the same + if (zone === this._currentZone) { + return; + } + + // Fire the hover end callback if a zone was being hovered + if (this._currentZone) { + this._currentZone.leaveCallback(); + this._currentZone = null; + } + + // Exit if there is not zone + if (!zone) { + return; + } + this._currentZone = zone; + + // Trigger the hover callback + if (zone.hoverCallback) { + zone.hoverCallback(e); + } + + // Restart the timeout + if (this._tooltipTimeout) { + clearTimeout(this._tooltipTimeout); + } + this._tooltipTimeout = setTimeout(() => this._onTooltip(e), HOVER_DURATION); + } + + private _onTooltip(e: MouseEvent): void { + const zone = this._findZoneEventAt(e); + if (zone && zone.tooltipCallback) { + zone.tooltipCallback(e); } } @@ -116,8 +135,9 @@ export class MouseZone implements IMouseZone { public x2: number, public y: number, public clickCallback: (e: MouseEvent) => any, - public hoverStartCallback?: (e: MouseEvent) => any, - public hoverEndCallback?: () => void + public hoverCallback?: (e: MouseEvent) => any, + public tooltipCallback?: (e: MouseEvent) => any, + public leaveCallback?: () => void ) { } } diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 0e657ae777..4efc54812d 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -109,11 +109,11 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param x The column to fill. * @param y The row to fill. */ - protected fillBottomLineAtCell(x: number, y: number): void { + protected fillBottomLineAtCells(x: number, y: number, width: number = 1): void { this._ctx.fillRect( x * this.scaledCharWidth, (y + 1) * this.scaledLineHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */, - this.scaledCharWidth, + width * this.scaledCharWidth, window.devicePixelRatio); } diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index 9f5136ff75..a888d54fa5 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -202,7 +202,7 @@ export class CursorRenderLayer extends BaseRenderLayer { private _renderUnderlineCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void { this._ctx.save(); this._ctx.fillStyle = this.colors.cursor; - this.fillBottomLineAtCell(x, y); + this.fillBottomLineAtCells(x, y); this._ctx.restore(); } diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index 05141aedfc..bef57a0076 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -139,7 +139,7 @@ export class ForegroundRenderLayer extends BaseRenderLayer { } else { this._ctx.fillStyle = this.colors.foreground; } - this.fillBottomLineAtCell(x, y); + this.fillBottomLineAtCells(x, y); } this.drawChar(terminal, char, code, width, x, y, fg, !!(flags & FLAGS.BOLD)); diff --git a/src/renderer/LinkRenderLayer.ts b/src/renderer/LinkRenderLayer.ts new file mode 100644 index 0000000000..b948b70dc5 --- /dev/null +++ b/src/renderer/LinkRenderLayer.ts @@ -0,0 +1,44 @@ +import { IColorSet } from './Interfaces'; +import { IBuffer, ICharMeasure, ITerminal, ILinkifierAccessor } from '../Interfaces'; +import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; +import { GridCache } from './GridCache'; +import { FLAGS } from './Types'; +import { BaseRenderLayer, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer'; +import { LinkHoverEvent, LinkHoverEventTypes } from '../Types'; + +export class LinkRenderLayer extends BaseRenderLayer { + private _state: LinkHoverEvent = null; + + constructor(container: HTMLElement, zIndex: number, colors: IColorSet, terminal: ILinkifierAccessor) { + super(container, 'link', zIndex, colors); + terminal.linkifier.on(LinkHoverEventTypes.HOVER, (e: LinkHoverEvent) => this._onLinkHover(e)); + terminal.linkifier.on(LinkHoverEventTypes.LEAVE, (e: LinkHoverEvent) => this._onLinkLeave(e)); + } + + public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void { + super.resize(terminal, canvasWidth, canvasHeight, charSizeChanged); + // Resizing the canvas discards the contents of the canvas so clear state + this._state = null; + } + + public reset(terminal: ITerminal): void { + this._clearCurrentLink(); + } + + private _clearCurrentLink(): void { + if (this._state) { + this.clearCells(this._state.x, this._state.y, this._state.length, 1); + this._state = null; + } + } + + private _onLinkHover(e: LinkHoverEvent): void { + this._ctx.fillStyle = this.colors.foreground; + this.fillBottomLineAtCells(e.x, e.y, e.length); + this._state = e; + } + + private _onLinkLeave(e: LinkHoverEvent): void { + this._clearCurrentLink(); + } +} diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index c032bc8f8f..ebdd0bdbcf 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -11,6 +11,7 @@ import { CursorRenderLayer } from './CursorRenderLayer'; import { ColorManager } from './ColorManager'; import { BaseRenderLayer } from './BaseRenderLayer'; import { IRenderLayer, IColorSet, IRenderer } from './Interfaces'; +import { LinkRenderLayer } from './LinkRenderLayer'; export class Renderer implements IRenderer { /** A queue of the rows to be refreshed */ @@ -28,7 +29,8 @@ export class Renderer implements IRenderer { new BackgroundRenderLayer(this._terminal.element, 0, this._colorManager.colors), new SelectionRenderLayer(this._terminal.element, 1, this._colorManager.colors), new ForegroundRenderLayer(this._terminal.element, 2, this._colorManager.colors), - new CursorRenderLayer(this._terminal.element, 3, this._colorManager.colors) + new LinkRenderLayer(this._terminal.element, 3, this._colorManager.colors, this._terminal), + new CursorRenderLayer(this._terminal.element, 4, this._colorManager.colors) ]; this._devicePixelRatio = window.devicePixelRatio; } diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index 652c3aa33b..e229fc8782 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -2,13 +2,14 @@ * @license MIT */ -import { ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, IListenerType, IInputHandlingTerminal, IViewport, ICircularList, ICompositionHelper, ITheme } from '../Interfaces'; +import { ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, IListenerType, IInputHandlingTerminal, IViewport, ICircularList, ICompositionHelper, ITheme, ILinkifier } from '../Interfaces'; import { LineData } from '../Types'; import { Buffer } from '../Buffer'; import * as Browser from './Browser'; import { IColorSet, IRenderer } from '../renderer/Interfaces'; export class MockTerminal implements ITerminal { + linkifier: ILinkifier; isFocused: boolean; options: ITerminalOptions = {}; element: HTMLElement; diff --git a/src/xterm.css b/src/xterm.css index df3aff5908..c26d459d72 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -120,5 +120,5 @@ } .terminal:not(.enable-mouse-events) { - cursor: text; + cursor: text; } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index b219732a3b..7a4d6940a5 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -141,14 +141,15 @@ interface ILinkMatcherOptions { validationCallback?: (uri: string, callback: (isValid: boolean) => void) => void; /** - * A callback that fires when the mouse hovers over a link. + * A callback that fires when the mouse hovers over a link for a moment. */ - hoverStartCallback?: (event: MouseEvent, uri: string) => boolean | void; + tooltipCallback?: (event: MouseEvent, uri: string) => boolean | void; /** - * A callback that fires when the mouse leaves a link that was hovered. + * A callback that fires when the mouse leaves a link. Note that this can + * happen even when tooltipCallback hasn't fired for the link yet. */ - hoverEndCallback?: (event: MouseEvent, uri: string) => boolean | void; + leaveCallback?: (event: MouseEvent, uri: string) => boolean | void; /** * The priority of the link matcher, this defines the order in which the link From 75b91c618e880845d984fba1eb1e492ac38596d1 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 8 Sep 2017 07:50:43 -0700 Subject: [PATCH 107/108] Update license text --- src/Buffer.test.ts | 1 + src/Buffer.ts | 1 + src/BufferSet.test.ts | 1 + src/BufferSet.ts | 1 + src/Charsets.ts | 1 + src/CompositionHelper.test.ts | 1 + src/CompositionHelper.ts | 1 + src/EscapeSequences.ts | 1 + src/EventEmitter.test.ts | 1 + src/EventEmitter.ts | 1 + src/InputHandler.test.ts | 1 + src/InputHandler.ts | 2 ++ src/Interfaces.ts | 1 + src/Linkifier.test.ts | 1 + src/Linkifier.ts | 1 + src/Parser.ts | 2 ++ src/SelectionManager.test.ts | 1 + src/SelectionManager.ts | 1 + src/SelectionModel.test.ts | 1 + src/SelectionModel.ts | 1 + src/Terminal.integration.ts | 1 + src/Terminal.test.ts | 7 ++++++- src/Terminal.ts | 6 ++++-- src/Types.ts | 1 + src/Viewport.test.ts | 1 + src/Viewport.ts | 1 + src/addons/attach/attach.js | 5 +++-- src/addons/fit/fit.js | 14 ++++++++------ src/addons/fullscreen/fullscreen.js | 4 ++-- src/addons/search/SearchHelper.ts | 1 + src/addons/search/search.ts | 1 + src/addons/terminado/terminado.js | 7 ++++--- src/handlers/Clipboard.test.ts | 5 +++++ src/handlers/Clipboard.ts | 4 +--- src/input/Interfaces.ts | 5 +++++ src/input/MouseZoneManager.ts | 5 +++++ src/renderer/BackgroundRenderLayer.ts | 5 +++++ src/renderer/BaseRenderLayer.ts | 5 +++++ src/renderer/CharAtlas.ts | 5 +++++ src/renderer/ColorManager.test.ts | 1 + src/renderer/ColorManager.ts | 5 +++++ src/renderer/CursorRenderLayer.ts | 5 +++++ src/renderer/ForegroundRenderLayer.ts | 5 +++++ src/renderer/GridCache.test.ts | 1 + src/renderer/GridCache.ts | 5 +++++ src/renderer/Interfaces.ts | 5 +++++ src/renderer/LinkRenderLayer.ts | 5 +++++ src/renderer/Renderer.ts | 1 + src/renderer/SelectionRenderLayer.ts | 5 +++++ src/renderer/Types.ts | 5 +++++ src/utils/Browser.ts | 3 +-- src/utils/CharMeasure.test.ts | 1 + src/utils/CharMeasure.ts | 2 +- src/utils/CircularList.test.ts | 1 + src/utils/CircularList.ts | 8 +++++--- src/utils/Generic.ts | 3 +-- src/utils/Mouse.test.ts | 1 + src/utils/Mouse.ts | 1 + src/utils/Sounds.ts | 1 + src/utils/TestUtils.test.ts | 1 + src/xterm.css | 4 ++-- src/xterm.ts | 1 + 62 files changed, 146 insertions(+), 29 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 6baa230dd4..210d297184 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -1,4 +1,5 @@ /** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/Buffer.ts b/src/Buffer.ts index 65b7d58e53..2d23980dc9 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -1,4 +1,5 @@ /** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/BufferSet.test.ts b/src/BufferSet.test.ts index 320241ca52..b9c1824dbe 100644 --- a/src/BufferSet.test.ts +++ b/src/BufferSet.test.ts @@ -1,4 +1,5 @@ /** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/BufferSet.ts b/src/BufferSet.ts index 64b364e92f..e31d2278f1 100644 --- a/src/BufferSet.ts +++ b/src/BufferSet.ts @@ -1,4 +1,5 @@ /** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/Charsets.ts b/src/Charsets.ts index c2c92e8d4a..0ee91d326d 100644 --- a/src/Charsets.ts +++ b/src/Charsets.ts @@ -1,4 +1,5 @@ /** + * Copyright (c) 2016 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/CompositionHelper.test.ts b/src/CompositionHelper.test.ts index 7d78a259bb..09a59f729f 100644 --- a/src/CompositionHelper.test.ts +++ b/src/CompositionHelper.test.ts @@ -1,4 +1,5 @@ /** + * Copyright (c) 2016 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/CompositionHelper.ts b/src/CompositionHelper.ts index 345041d667..cec8fb3738 100644 --- a/src/CompositionHelper.ts +++ b/src/CompositionHelper.ts @@ -1,4 +1,5 @@ /** + * Copyright (c) 2016 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/EscapeSequences.ts b/src/EscapeSequences.ts index 9542658faa..cb97c3680e 100644 --- a/src/EscapeSequences.ts +++ b/src/EscapeSequences.ts @@ -1,4 +1,5 @@ /** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/EventEmitter.test.ts b/src/EventEmitter.test.ts index d61f994ab5..c1f0a0aba8 100644 --- a/src/EventEmitter.test.ts +++ b/src/EventEmitter.test.ts @@ -1,4 +1,5 @@ /** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/EventEmitter.ts b/src/EventEmitter.ts index a1768d2d4a..414eac8956 100644 --- a/src/EventEmitter.ts +++ b/src/EventEmitter.ts @@ -1,4 +1,5 @@ /** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index f6e07f7efd..a8b2ff1264 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -1,4 +1,5 @@ /** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 3bf16a1c38..88a86a4417 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -1,4 +1,6 @@ /** + * Copyright (c) 2014 The xterm.js authors. All rights reserved. + * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) * @license MIT */ diff --git a/src/Interfaces.ts b/src/Interfaces.ts index b777968886..d022dc3ebd 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -1,4 +1,5 @@ /** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index f165590f94..17793fca54 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -1,4 +1,5 @@ /** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 55d8d68d95..d3839cb5b3 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -1,4 +1,5 @@ /** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/Parser.ts b/src/Parser.ts index b047f91f7a..b6493fea52 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -1,4 +1,6 @@ /** + * Copyright (c) 2014 The xterm.js authors. All rights reserved. + * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) * @license MIT */ diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index 8c90693e99..e5bdc117c5 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -1,4 +1,5 @@ /** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 4fe09c8c0f..29f12169b5 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -1,4 +1,5 @@ /** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/SelectionModel.test.ts b/src/SelectionModel.test.ts index 6c750ed240..e195d6bde1 100644 --- a/src/SelectionModel.test.ts +++ b/src/SelectionModel.test.ts @@ -1,4 +1,5 @@ /** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/SelectionModel.ts b/src/SelectionModel.ts index 8c599fd7ae..1fbf40fe89 100644 --- a/src/SelectionModel.ts +++ b/src/SelectionModel.ts @@ -1,4 +1,5 @@ /** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/Terminal.integration.ts b/src/Terminal.integration.ts index 124e89cb8e..00db4dd68c 100644 --- a/src/Terminal.integration.ts +++ b/src/Terminal.integration.ts @@ -1,4 +1,5 @@ /** + * Copyright (c) 2016 The xterm.js authors. All rights reserved. * @license MIT * * This file contains integration tests for xterm.js. diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index 7a44ee81e9..fd173831fa 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -1,4 +1,9 @@ -import { assert, expect } from 'chai'; +/** + * Copyright (c) 2016 The xterm.js authors. All rights reserved. + * @license MIT + */ + + import { assert, expect } from 'chai'; import { Terminal } from './Terminal'; import { MockViewport, MockCompositionHelper, MockRenderer } from './utils/TestUtils.test'; import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from './Buffer'; diff --git a/src/Terminal.ts b/src/Terminal.ts index 1a180cc152..e19887cbc2 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1,5 +1,8 @@ /** - * xterm.js: xterm, in the browser + * Copyright (c) 2014 The xterm.js authors. All rights reserved. + * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) + * @license MIT + * * Originally forked from (with the author's permission): * Fabrice Bellard's javascript vt100 for jslinux: * http://bellard.org/jslinux/ @@ -7,7 +10,6 @@ * The original design remains. The terminal itself * has been extended to include xterm CSI codes, among * other features. - * @license MIT * * Terminal Emulation References: * http://vt100.net/ diff --git a/src/Types.ts b/src/Types.ts index 06d495026c..a0c03fdd7d 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -1,4 +1,5 @@ /** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/Viewport.test.ts b/src/Viewport.test.ts index 6d23e19a1f..6ce798d327 100644 --- a/src/Viewport.test.ts +++ b/src/Viewport.test.ts @@ -1,4 +1,5 @@ /** + * Copyright (c) 2016 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/Viewport.ts b/src/Viewport.ts index 2de698a0e0..1f693cfeb6 100644 --- a/src/Viewport.ts +++ b/src/Viewport.ts @@ -1,4 +1,5 @@ /** + * Copyright (c) 2016 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/addons/attach/attach.js b/src/addons/attach/attach.js index 34dc4f681f..834ccd3c5d 100644 --- a/src/addons/attach/attach.js +++ b/src/addons/attach/attach.js @@ -1,7 +1,8 @@ /** - * Implements the attach method, that attaches the terminal to a WebSocket stream. - * @module xterm/addons/attach/attach + * Copyright (c) 2014 The xterm.js authors. All rights reserved. * @license MIT + * + * Implements the attach method, that attaches the terminal to a WebSocket stream. */ (function (attach) { diff --git a/src/addons/fit/fit.js b/src/addons/fit/fit.js index 60214dad9e..59f0fd52e0 100644 --- a/src/addons/fit/fit.js +++ b/src/addons/fit/fit.js @@ -1,14 +1,16 @@ /** + * Copyright (c) 2014 The xterm.js authors. All rights reserved. + * @license MIT + * * Fit terminal columns and rows to the dimensions of its DOM element. * * ## Approach - * - Rows: Truncate the division of the terminal parent element height by the terminal row height. * - * - Columns: Truncate the division of the terminal parent element width by the terminal character - * width (apply display: inline at the terminal row and truncate its width with the current - * number of columns). - * @module xterm/addons/fit/fit - * @license MIT + * Rows: Truncate the division of the terminal parent element height by the + * terminal row height. + * Columns: Truncate the division of the terminal parent element width by the + * terminal character width (apply display: inline at the terminal + * row and truncate its width with the current number of columns). */ (function (fit) { diff --git a/src/addons/fullscreen/fullscreen.js b/src/addons/fullscreen/fullscreen.js index 344669f6df..1474d2db6f 100644 --- a/src/addons/fullscreen/fullscreen.js +++ b/src/addons/fullscreen/fullscreen.js @@ -1,8 +1,8 @@ /** - * Fullscreen addon for xterm.js - * @module xterm/addons/fullscreen/fullscreen + * Copyright (c) 2014 The xterm.js authors. All rights reserved. * @license MIT */ + (function (fullscreen) { if (typeof exports === 'object' && typeof module === 'object') { /* diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 26e4e8a7d2..1747e747e5 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -1,4 +1,5 @@ /** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/addons/search/search.ts b/src/addons/search/search.ts index 34223c4ea7..69c5737b53 100644 --- a/src/addons/search/search.ts +++ b/src/addons/search/search.ts @@ -1,4 +1,5 @@ /** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/addons/terminado/terminado.js b/src/addons/terminado/terminado.js index d1414c864b..54af76337a 100644 --- a/src/addons/terminado/terminado.js +++ b/src/addons/terminado/terminado.js @@ -1,8 +1,9 @@ /** - * This module provides methods for attaching a terminal to a terminado WebSocket stream. - * - * @module xterm/addons/terminado/terminado + * Copyright (c) 2016 The xterm.js authors. All rights reserved. * @license MIT + * + * This module provides methods for attaching a terminal to a terminado + * WebSocket stream. */ (function (attach) { diff --git a/src/handlers/Clipboard.test.ts b/src/handlers/Clipboard.test.ts index bd2cea0366..6901b73006 100644 --- a/src/handlers/Clipboard.test.ts +++ b/src/handlers/Clipboard.test.ts @@ -1,3 +1,8 @@ +/** + * Copyright (c) 2016 The xterm.js authors. All rights reserved. + * @license MIT + */ + import { assert } from 'chai'; import * as Terminal from '../Terminal'; import * as Clipboard from './Clipboard'; diff --git a/src/handlers/Clipboard.ts b/src/handlers/Clipboard.ts index ac9a7b8714..a973535203 100644 --- a/src/handlers/Clipboard.ts +++ b/src/handlers/Clipboard.ts @@ -1,7 +1,5 @@ /** - * Clipboard handler module: exports methods for handling all clipboard-related events in the - * terminal. - * @module xterm/handlers/Clipboard + * Copyright (c) 2016 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/input/Interfaces.ts b/src/input/Interfaces.ts index 19125d0801..e3ed4eef2c 100644 --- a/src/input/Interfaces.ts +++ b/src/input/Interfaces.ts @@ -1,3 +1,8 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + export interface IMouseZoneManager { add(zone: IMouseZone): void; clearAll(): void; diff --git a/src/input/MouseZoneManager.ts b/src/input/MouseZoneManager.ts index 1a6cedada9..d1bc8a758b 100644 --- a/src/input/MouseZoneManager.ts +++ b/src/input/MouseZoneManager.ts @@ -1,3 +1,8 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + import { IMouseZoneManager, IMouseZone } from './Interfaces'; import { ITerminal } from '../Interfaces'; import { getCoords } from '../utils/Mouse'; diff --git a/src/renderer/BackgroundRenderLayer.ts b/src/renderer/BackgroundRenderLayer.ts index 292847c2db..0c09727db7 100644 --- a/src/renderer/BackgroundRenderLayer.ts +++ b/src/renderer/BackgroundRenderLayer.ts @@ -1,3 +1,8 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + import { IColorSet } from './Interfaces'; import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 4efc54812d..cc02bcea90 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -1,3 +1,8 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + import { IRenderLayer, IColorSet } from './Interfaces'; import { ITerminal, ITerminalOptions } from '../Interfaces'; import { acquireCharAtlas, CHAR_ATLAS_CELL_SPACING } from './CharAtlas'; diff --git a/src/renderer/CharAtlas.ts b/src/renderer/CharAtlas.ts index 434b263baa..bf080cce57 100644 --- a/src/renderer/CharAtlas.ts +++ b/src/renderer/CharAtlas.ts @@ -1,3 +1,8 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + import { ITerminal, ITheme } from '../Interfaces'; import { IColorSet } from '../renderer/Interfaces'; import { isFirefox } from '../utils/Browser'; diff --git a/src/renderer/ColorManager.test.ts b/src/renderer/ColorManager.test.ts index 6bd9c97775..e00f6b6d31 100644 --- a/src/renderer/ColorManager.test.ts +++ b/src/renderer/ColorManager.test.ts @@ -1,4 +1,5 @@ /** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/renderer/ColorManager.ts b/src/renderer/ColorManager.ts index 057292f77c..b9afd0e1a6 100644 --- a/src/renderer/ColorManager.ts +++ b/src/renderer/ColorManager.ts @@ -1,3 +1,8 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + import { IColorSet } from './Interfaces'; import { ITheme } from '../Interfaces'; diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index a888d54fa5..b136a13a18 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -1,3 +1,8 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + import { IColorSet } from './Interfaces'; import { IBuffer, ICharMeasure, ITerminal, ITerminalOptions } from '../Interfaces'; import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index bef57a0076..883be5d94a 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -1,3 +1,8 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + import { IColorSet } from './Interfaces'; import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../Buffer'; diff --git a/src/renderer/GridCache.test.ts b/src/renderer/GridCache.test.ts index 37e55ee143..c4b1c220ee 100644 --- a/src/renderer/GridCache.test.ts +++ b/src/renderer/GridCache.test.ts @@ -1,4 +1,5 @@ /** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/renderer/GridCache.ts b/src/renderer/GridCache.ts index 3b3ae8dfab..dd188c6269 100644 --- a/src/renderer/GridCache.ts +++ b/src/renderer/GridCache.ts @@ -1,3 +1,8 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + export class GridCache { public cache: T[][]; diff --git a/src/renderer/Interfaces.ts b/src/renderer/Interfaces.ts index e6b09843df..05f98300d7 100644 --- a/src/renderer/Interfaces.ts +++ b/src/renderer/Interfaces.ts @@ -1,3 +1,8 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + import { ITerminal, ITerminalOptions, ITheme } from '../Interfaces'; export interface IRenderer { diff --git a/src/renderer/LinkRenderLayer.ts b/src/renderer/LinkRenderLayer.ts index b948b70dc5..9b2cabb32d 100644 --- a/src/renderer/LinkRenderLayer.ts +++ b/src/renderer/LinkRenderLayer.ts @@ -1,3 +1,8 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + import { IColorSet } from './Interfaces'; import { IBuffer, ICharMeasure, ITerminal, ILinkifierAccessor } from '../Interfaces'; import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index ebdd0bdbcf..131a8b7849 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -1,4 +1,5 @@ /** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/renderer/SelectionRenderLayer.ts b/src/renderer/SelectionRenderLayer.ts index 91e62a0c6d..39569c9beb 100644 --- a/src/renderer/SelectionRenderLayer.ts +++ b/src/renderer/SelectionRenderLayer.ts @@ -1,3 +1,8 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + import { IColorSet } from './Interfaces'; import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; diff --git a/src/renderer/Types.ts b/src/renderer/Types.ts index 09e74439f4..b1a930f9d0 100644 --- a/src/renderer/Types.ts +++ b/src/renderer/Types.ts @@ -1,4 +1,9 @@ /** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + + /** * Flags used to render terminal text properly. */ export enum FLAGS { diff --git a/src/utils/Browser.ts b/src/utils/Browser.ts index 68148aeb65..48c0c3748f 100644 --- a/src/utils/Browser.ts +++ b/src/utils/Browser.ts @@ -1,6 +1,5 @@ /** - * Attributes and methods to help with identifying the current browser and platform. - * @module xterm/utils/Browser + * Copyright (c) 2016 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/utils/CharMeasure.test.ts b/src/utils/CharMeasure.test.ts index 02a8dae9e1..5d2358f4bf 100644 --- a/src/utils/CharMeasure.test.ts +++ b/src/utils/CharMeasure.test.ts @@ -1,4 +1,5 @@ /** + * Copyright (c) 2016 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/utils/CharMeasure.ts b/src/utils/CharMeasure.ts index 37d6697aa9..e8fcff8626 100644 --- a/src/utils/CharMeasure.ts +++ b/src/utils/CharMeasure.ts @@ -1,5 +1,5 @@ /** - * @module xterm/utils/CharMeasure + * Copyright (c) 2016 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/utils/CircularList.test.ts b/src/utils/CircularList.test.ts index f404b6f28e..6bb35113ac 100644 --- a/src/utils/CircularList.test.ts +++ b/src/utils/CircularList.test.ts @@ -1,4 +1,5 @@ /** + * Copyright (c) 2016 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/utils/CircularList.ts b/src/utils/CircularList.ts index e188fb5d86..97a32b790b 100644 --- a/src/utils/CircularList.ts +++ b/src/utils/CircularList.ts @@ -1,13 +1,15 @@ /** - * Represents a circular list; a list with a maximum size that wraps around when push is called, - * overriding values at the start of the list. - * @module xterm/utils/CircularList + * Copyright (c) 2016 The xterm.js authors. All rights reserved. * @license MIT */ import { EventEmitter } from '../EventEmitter'; import { ICircularList } from '../Interfaces'; +/** + * Represents a circular list; a list with a maximum size that wraps around when push is called, + * overriding values at the start of the list. + */ export class CircularList extends EventEmitter implements ICircularList { protected _array: T[]; private _startIndex: number; diff --git a/src/utils/Generic.ts b/src/utils/Generic.ts index 9807232605..823736178f 100644 --- a/src/utils/Generic.ts +++ b/src/utils/Generic.ts @@ -1,6 +1,5 @@ /** - * Generic utilities module with methods that can be helpful at different parts of the code base. - * @module xterm/utils/Generic + * Copyright (c) 2016 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/utils/Mouse.test.ts b/src/utils/Mouse.test.ts index 15b68ef2d1..4163263be3 100644 --- a/src/utils/Mouse.test.ts +++ b/src/utils/Mouse.test.ts @@ -1,4 +1,5 @@ /** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/utils/Mouse.ts b/src/utils/Mouse.ts index 3bbad855ab..aef76d5bab 100644 --- a/src/utils/Mouse.ts +++ b/src/utils/Mouse.ts @@ -1,4 +1,5 @@ /** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/utils/Sounds.ts b/src/utils/Sounds.ts index 6d20c67122..03f36f4267 100644 --- a/src/utils/Sounds.ts +++ b/src/utils/Sounds.ts @@ -1,4 +1,5 @@ /** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index e229fc8782..eb2f25017b 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -1,4 +1,5 @@ /** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/xterm.css b/src/xterm.css index c26d459d72..399f56ceb5 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -1,8 +1,8 @@ /** - * xterm.js: xterm, in the browser - * Copyright (c) 2014-2016, SourceLair Private Company (www.sourcelair.com (MIT License) + * Copyright (c) 2014 The xterm.js authors. All rights reserved. * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) * https://github.com/chjj/term.js + * @license MIT * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/xterm.ts b/src/xterm.ts index 50ac559287..6df3a4c065 100644 --- a/src/xterm.ts +++ b/src/xterm.ts @@ -1,4 +1,5 @@ /** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. * @license MIT * * This file is the entry point for browserify. From 0c19bc9c2812b4564b554e048a993598622b6d67 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 8 Sep 2017 11:06:15 -0700 Subject: [PATCH 108/108] Fix tests --- fixtures/typings-test/typings-test.ts | 5 +++-- src/Linkifier.test.ts | 27 +++++++++++++++------------ src/Linkifier.ts | 2 +- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/fixtures/typings-test/typings-test.ts b/fixtures/typings-test/typings-test.ts index df79d2d04e..928ca23896 100644 --- a/fixtures/typings-test/typings-test.ts +++ b/fixtures/typings-test/typings-test.ts @@ -217,9 +217,10 @@ namespace methods_experimental { validationCallback: (uri: string, callback: (isValid: boolean) => void) => { console.log(uri, callback); }, - hoverCallback: (e: MouseEvent, uri: string) => { + tooltipCallback: (e: MouseEvent, uri: string) => { console.log(e, uri); - } + }, + leaveCallback: () => {} }); t.deregisterLinkMatcher(1); } diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index 17793fca54..df9e459cd2 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -4,7 +4,7 @@ */ import { assert } from 'chai'; -import { ITerminal, ILinkifier, IBuffer, IBufferAccessor } from './Interfaces'; +import { ITerminal, ILinkifier, IBuffer, IBufferAccessor, IElementAccessor } from './Interfaces'; import { Linkifier } from './Linkifier'; import { LinkMatcher, LineData } from './Types'; import { IMouseZoneManager, IMouseZone } from './input/Interfaces'; @@ -12,13 +12,13 @@ import { MockBuffer } from './utils/TestUtils.test'; import { CircularList } from './utils/CircularList'; class TestLinkifier extends Linkifier { - constructor(private _bufferAccessor: IBufferAccessor) { - super(_bufferAccessor); + constructor(_terminal: IBufferAccessor & IElementAccessor) { + super(_terminal); Linkifier.TIME_BEFORE_LINKIFY = 0; } public get linkMatchers(): LinkMatcher[] { return this._linkMatchers; } - public linkifyRows(): void { super.linkifyRows(0, this._bufferAccessor.buffer.lines.length - 1); } + public linkifyRows(): void { super.linkifyRows(0, this._terminal.buffer.lines.length - 1); } } class TestMouseZoneManager implements IMouseZoneManager { @@ -33,15 +33,18 @@ class TestMouseZoneManager implements IMouseZoneManager { } describe('Linkifier', () => { - let bufferAccessor: IBufferAccessor; + let terminal: IBufferAccessor & IElementAccessor; let linkifier: TestLinkifier; let mouseZoneManager: TestMouseZoneManager; beforeEach(() => { - bufferAccessor = { buffer: new MockBuffer() }; - bufferAccessor.buffer.lines = new CircularList(20); - bufferAccessor.buffer.ydisp = 0; - linkifier = new TestLinkifier(bufferAccessor); + terminal = { + buffer: new MockBuffer(), + element: {} + }; + terminal.buffer.lines = new CircularList(20); + terminal.buffer.ydisp = 0; + linkifier = new TestLinkifier(terminal); mouseZoneManager = new TestMouseZoneManager(); }); @@ -54,7 +57,7 @@ describe('Linkifier', () => { } function addRow(text: string): void { - bufferAccessor.buffer.lines.push(stringToRow(text)); + terminal.buffer.lines.push(stringToRow(text)); } function assertLinkifiesEntireRow(uri: string, done: MochaDone): void { @@ -63,7 +66,7 @@ describe('Linkifier', () => { setTimeout(() => { assert.equal(mouseZoneManager.zones[0].x1, 1); assert.equal(mouseZoneManager.zones[0].x2, uri.length + 1); - assert.equal(mouseZoneManager.zones[0].y, bufferAccessor.buffer.lines.length); + assert.equal(mouseZoneManager.zones[0].y, terminal.buffer.lines.length); done(); }, 0); } @@ -78,7 +81,7 @@ describe('Linkifier', () => { links.forEach((l, i) => { assert.equal(mouseZoneManager.zones[i].x1, l.x + 1); assert.equal(mouseZoneManager.zones[i].x2, l.x + l.length + 1); - assert.equal(mouseZoneManager.zones[i].y, bufferAccessor.buffer.lines.length); + assert.equal(mouseZoneManager.zones[i].y, terminal.buffer.lines.length); }); done(); }, 0); diff --git a/src/Linkifier.ts b/src/Linkifier.ts index d3839cb5b3..9479542956 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -51,7 +51,7 @@ export class Linkifier extends EventEmitter implements ILinkifier { private _nextLinkMatcherId = HYPERTEXT_LINK_MATCHER_ID; constructor( - private _terminal: IBufferAccessor & IElementAccessor + protected _terminal: IBufferAccessor & IElementAccessor ) { super(); this.registerLinkMatcher(strictUrlRegex, null, { matchIndex: 1 });