From 877e1e5eb30cdd8a9f4a9073a97b81c81a59dac4 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 21 Aug 2017 14:30:52 -0700 Subject: [PATCH 001/156] Prevent scrollback setOption < 0 Fixes #911 --- src/Terminal.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Terminal.ts b/src/Terminal.ts index a17cf94a9..03babac50 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -454,6 +454,10 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT } break; case 'scrollback': + if (value < 0) { + console.warn(`scrollback cannot be less than 0, value: ${value}`); + return; + } if (this.options[key] !== value) { const newBufferLength = this.rows + value; if (this.buffer.lines.length > newBufferLength) { From b706383687353fd90f78c18171db82b7df01e43f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 24 Aug 2017 11:15:25 -0700 Subject: [PATCH 002/156] Allow mousedown even to propagate on right click Fixes #913 --- src/SelectionManager.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index f3aa5f820..1aaec14b0 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -316,7 +316,6 @@ export class SelectionManager extends EventEmitter implements ISelectionManager // 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) { - event.stopPropagation(); return; } From 9a8b7a4d8de143ce81c2211fa32772f65c1299fb Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 25 Aug 2017 11:26:46 -0700 Subject: [PATCH 003/156] Fix scroll region scrolling with 0 scrollback --- src/Buffer.ts | 3 +- src/Terminal.test.ts | 140 ++++++++++++++++++++++++++++++++++++++++++- src/Terminal.ts | 118 ++++++++++++++++++++++++++---------- 3 files changed, 228 insertions(+), 33 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index fd6f8aefa..7af090ac9 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -78,10 +78,9 @@ export class Buffer implements IBuffer { this.ybase = 0; this.y = 0; this.x = 0; - this.scrollBottom = 0; - this.scrollTop = 0; this.tabs = {}; this._lines = new CircularList(this._getCorrectBufferLength(this._terminal.rows)); + this.scrollTop = 0; this.scrollBottom = this._terminal.rows - 1; } diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index 771835bbe..0c333741f 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -3,6 +3,9 @@ import { Terminal } from './Terminal'; import { MockViewport, MockCompositionHelper } from './utils/TestUtils.test'; import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from './Buffer'; +const INIT_COLS = 80; +const INIT_ROWS = 24; + class TestTerminal extends Terminal { public evaluateKeyEscapeSequence(ev: any): {cancel: boolean, key: string, scrollDisp: number} { return this._evaluateKeyEscapeSequence(ev); } public keyDown(ev: any): boolean { return this._keyDown(ev); } @@ -13,7 +16,10 @@ describe('term.js addons', () => { let term: TestTerminal; beforeEach(() => { - term = new TestTerminal(); + term = new TestTerminal({ + cols: INIT_COLS, + rows: INIT_ROWS + }); term.refresh = () => {}; term.viewport = new MockViewport(); (term).compositionHelper = new MockCompositionHelper(); @@ -283,6 +289,138 @@ describe('term.js addons', () => { assert.equal(term.buffer.ydisp, startYDisp - 1); }); }); + + describe('scroll() function', () => { + describe('when scrollback > 0', () => { + it('should create a new line and scroll', () => { + term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a'; + term.buffer.lines.get(INIT_ROWS - 1)[0][CHAR_DATA_CHAR_INDEX] = 'b'; + term.buffer.y = INIT_ROWS - 1; // Move cursor to last line + term.scroll(); + assert.equal(term.buffer.lines.length, INIT_ROWS + 1); + assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a'); + assert.equal(term.buffer.lines.get(INIT_ROWS - 1)[0][CHAR_DATA_CHAR_INDEX], 'b'); + assert.equal(term.buffer.lines.get(INIT_ROWS)[0][CHAR_DATA_CHAR_INDEX], ' '); + }); + + it('should properly scroll inside a scroll region (scrollTop set)', () => { + term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a'; + term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b'; + term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c'; + term.buffer.y = INIT_ROWS - 1; // Move cursor to last line + term.buffer.scrollTop = 1; + term.scroll(); + assert.equal(term.buffer.lines.length, INIT_ROWS); + assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a'); + assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c'); + }); + + it('should properly scroll inside a scroll region (scrollBottom set)', () => { + term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a'; + term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b'; + term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c'; + term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd'; + term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e'; + term.buffer.y = 3; + term.buffer.scrollBottom = 3; + term.scroll(); + assert.equal(term.buffer.lines.length, INIT_ROWS + 1); + assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a', '\'a\' should be pushed to the scrollback'); + assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'b'); + assert.equal(term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX], 'c'); + assert.equal(term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX], 'd'); + assert.equal(term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index'); + assert.equal(term.buffer.lines.get(5)[0][CHAR_DATA_CHAR_INDEX], 'e'); + }); + + it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => { + term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a'; + term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b'; + term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c'; + term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd'; + term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e'; + term.buffer.y = INIT_ROWS - 1; // Move cursor to last line + term.buffer.scrollTop = 1; + term.buffer.scrollBottom = 3; + term.scroll(); + assert.equal(term.buffer.lines.length, INIT_ROWS); + assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a'); + assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer'); + assert.equal(term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX], 'd'); + assert.equal(term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index'); + assert.equal(term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX], 'e'); + }); + }); + + describe('when scrollback === 0', () => { + beforeEach(() => { + term.setOption('scrollback', 0); + assert.equal(term.buffer.lines.maxLength, INIT_ROWS); + }); + + it('should create a new line and shift everything up', () => { + term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a'; + term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b'; + term.buffer.lines.get(INIT_ROWS - 1)[0][CHAR_DATA_CHAR_INDEX] = 'c'; + term.buffer.y = INIT_ROWS - 1; // Move cursor to last line + assert.equal(term.buffer.lines.length, INIT_ROWS); + term.scroll(); + assert.equal(term.buffer.lines.length, INIT_ROWS); + // 'a' gets pushed out of buffer + assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'b'); + assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], ' '); + assert.equal(term.buffer.lines.get(INIT_ROWS - 2)[0][CHAR_DATA_CHAR_INDEX], 'c'); + assert.equal(term.buffer.lines.get(INIT_ROWS - 1)[0][CHAR_DATA_CHAR_INDEX], ' '); + }); + + it('should properly scroll inside a scroll region (scrollTop set)', () => { + term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a'; + term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b'; + term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c'; + term.buffer.y = INIT_ROWS - 1; // Move cursor to last line + term.buffer.scrollTop = 1; + term.scroll(); + assert.equal(term.buffer.lines.length, INIT_ROWS); + assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a'); + assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c'); + }); + + it('should properly scroll inside a scroll region (scrollBottom set)', () => { + term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a'; + term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b'; + term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c'; + term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd'; + term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e'; + term.buffer.y = 3; + term.buffer.scrollBottom = 3; + term.scroll(); + assert.equal(term.buffer.lines.length, INIT_ROWS); + assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'b'); + assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c'); + assert.equal(term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX], 'd'); + assert.equal(term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index'); + assert.equal(term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX], 'e'); + }); + + it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => { + term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a'; + term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b'; + term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c'; + term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd'; + term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e'; + term.buffer.y = INIT_ROWS - 1; // Move cursor to last line + term.buffer.scrollTop = 1; + term.buffer.scrollBottom = 3; + term.scroll(); + assert.equal(term.buffer.lines.length, INIT_ROWS); + assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a'); + assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer'); + assert.equal(term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX], 'd'); + assert.equal(term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index'); + assert.equal(term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX], 'e'); + }); + }); + }); }); describe('evaluateKeyEscapeSequence', () => { diff --git a/src/Terminal.ts b/src/Terminal.ts index 03babac50..a083d9d42 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1167,46 +1167,104 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * line. */ public scroll(isWrapped?: boolean): void { - let row; + // The problem is caused when scrollback = 0. This section was written + // around the assumption that there would be buffer to trim when necessary. - // Make room for the new row in lines - const bufferNeedsTrimming = this.buffer.lines.length === this.buffer.lines.maxLength; - if (bufferNeedsTrimming) { - this.buffer.lines.trimStart(1); - this.buffer.ybase--; - this.buffer.ydisp = Math.max(this.buffer.ydisp - 1, 0); + + // shiftElements should be used here + + // If scrollTop is set (non-zero), we should shiftElements + // scrollBottom defines the position at which the new element is inserted + + // Note that both scrollBottom and scrollTop need to be handled individually + // because they can be set to terminal.rows or 0 respectively + + // Don't mess with ybase temporarily, asking for trouble + + // This should work with minimal effort utilizing the CiruclarList + + // TODO: Need to hold onto top given the if? + const bottomRow = this.buffer.ybase + this.buffer.scrollBottom; + console.log('bottomRow', bottomRow); + console.log('this.buffer.scrollTop: ' + this.buffer.scrollTop); + if (this.buffer.scrollTop === 0) { + const willBufferBeTrimmed = this.buffer.lines.length === this.buffer.lines.maxLength; + + if (bottomRow === this.buffer.lines.length - 1) { + // Pushing when possible is faster than splicing + this.buffer.lines.push(this.blankLine(undefined, isWrapped)); + console.log('push'); + } else { + // Insert a row *below* the bottomRow, pushing the top row into the scrollback + this.buffer.lines.splice(bottomRow + 1, 0, this.blankLine(undefined, isWrapped)); + console.log('splice'); + } + + // Adjust ydisp and ybase to accommodate the changes after the buffer was + // trimmed. + if (!willBufferBeTrimmed) { + console.log('increment ydisp/ybase'); + this.buffer.ybase++; + this.buffer.ydisp++; + } + console.log('this.buffer.ybase: ' + this.buffer.ybase); + console.log('this.buffer.ybase: ' + this.buffer.ydisp); + } else { + const topRow = this.buffer.ybase + this.buffer.scrollTop; + const scrollRegionHeight = bottomRow - topRow + 1/*as it's zero-based*/; +console.log('shiftElements'); +console.log('topRow: ' + topRow + '(' + this.buffer.lines.get(topRow)[0][1] + ')'); +console.log('scrollRegionHeight: ' + scrollRegionHeight); + this.buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1); + this.buffer.lines.set(bottomRow, this.blankLine(undefined, isWrapped)); } - this.buffer.ybase++; - - // Scroll the viewport down to the bottom if the user is not scrolling if (!this.userScrolling) { this.buffer.ydisp = this.buffer.ybase; } - // last line - row = this.buffer.ybase + this.rows - 1; - // subtract the bottom scroll region - row -= this.rows - 1 - this.buffer.scrollBottom; + // Make room for the new row in lines + // const bufferNeedsTrimming = this.buffer.lines.length === this.buffer.lines.maxLength; + // if (bufferNeedsTrimming) { + // this.buffer.lines.trimStart(1); + // this.buffer.ybase--; + // this.buffer.ydisp = Math.max(this.buffer.ydisp - 1, 0); + // } - if (row === this.buffer.lines.length) { - // Optimization: pushing is faster than splicing when they amount to the same behavior - this.buffer.lines.push(this.blankLine(undefined, isWrapped)); - } else { - // add our new line - this.buffer.lines.splice(row, 0, this.blankLine(undefined, isWrapped)); - } + // this.buffer.ybase++; - if (this.buffer.scrollTop !== 0) { - if (this.buffer.ybase !== 0) { - this.buffer.ybase--; - if (!this.userScrolling) { - this.buffer.ydisp = this.buffer.ybase; - } - } - this.buffer.lines.splice(this.buffer.ybase + this.buffer.scrollTop, 1); - } + // // Scroll the viewport down to the bottom if the user is not scrolling + // if (!this.userScrolling) { + // this.buffer.ydisp = this.buffer.ybase; + // } + + // // last line + // let row = this.buffer.ybase + this.rows - 1; + + // // subtract the bottom scroll region + // row -= this.rows - 1 - this.buffer.scrollBottom; + + // // Same as this? + // // row = this.buffer.ybase + this.buffer.scrollBottom; + + // if (row === this.buffer.lines.length) { + // // Optimization: pushing is faster than splicing when they amount to the same behavior + // this.buffer.lines.push(this.blankLine(undefined, isWrapped)); + // } else { + // // add our new line + // this.buffer.lines.splice(row, 0, this.blankLine(undefined, isWrapped)); + // } + + // if (this.buffer.scrollTop !== 0) { + // if (this.buffer.ybase !== 0) { + // this.buffer.ybase--; + // if (!this.userScrolling) { + // this.buffer.ydisp = this.buffer.ybase; + // } + // } + // this.buffer.lines.splice(this.buffer.ybase + this.buffer.scrollTop, 1); + // } // this.maxRange(); this.updateRange(this.buffer.scrollTop); From a2161532db2deb3b05da3f1238790bc929797552 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 25 Aug 2017 12:11:20 -0700 Subject: [PATCH 004/156] Fix reverseIndex to work with scroll regions and 0 scrollback --- src/Buffer.ts | 4 ++++ src/InputHandler.ts | 3 +++ src/Terminal.ts | 48 +++++++++++++++++++++++++++++++++------------ 3 files changed, 43 insertions(+), 12 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 7af090ac9..53645837d 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -46,6 +46,10 @@ export class Buffer implements IBuffer { return this._lines; } + public get hasScrollback(): boolean { + return this._hasScrollback && this.lines.maxLength > 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/InputHandler.ts b/src/InputHandler.ts index f2d977511..e6ab55402 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -464,10 +464,13 @@ export class InputHandler implements IInputHandler { let j: number; j = this._terminal.rows - 1 - this._terminal.buffer.scrollBottom; j = this._terminal.rows - 1 + this._terminal.buffer.ybase - j; + console.log('deleteLines', params); while (param--) { // test: echo -e '\e[44m\e[1M\e[0m' // blankLine(true) - xterm/linux behavior + console.log('splice delete row: ' + (row - 1) + '(' + this._terminal.buffer.lines.get(row - 1)[0][1] + ')'); this._terminal.buffer.lines.splice(row - 1, 1); + console.log('splice add blank row: ' + (j) + '(' + this._terminal.buffer.lines.get(j)[0][1] + ')'); this._terminal.buffer.lines.splice(j, 0, this._terminal.blankLine(true)); } diff --git a/src/Terminal.ts b/src/Terminal.ts index a083d9d42..fa3161f0e 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1184,39 +1184,62 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // This should work with minimal effort utilizing the CiruclarList // TODO: Need to hold onto top given the if? - const bottomRow = this.buffer.ybase + this.buffer.scrollBottom; + const newLine = this.blankLine(undefined, isWrapped); + const topRow = this.buffer.ybase + this.buffer.scrollTop; + let bottomRow = this.buffer.ybase + this.buffer.scrollBottom; + + + +// TODO: There's a problem with ybase going beyond scrollback? + + console.log('bottomRow', bottomRow); console.log('this.buffer.scrollTop: ' + this.buffer.scrollTop); if (this.buffer.scrollTop === 0) { const willBufferBeTrimmed = this.buffer.lines.length === this.buffer.lines.maxLength; + // Remove the line first if there is no scrollback so the list is not + // trimmed + if (!this.buffer.hasScrollback) { + this.buffer.lines.splice(topRow, 1); + // Adjust bottomRow to make up for the deleted row + // bottomRow--; + } + if (bottomRow === this.buffer.lines.length - 1) { // Pushing when possible is faster than splicing - this.buffer.lines.push(this.blankLine(undefined, isWrapped)); + this.buffer.lines.push(newLine); console.log('push'); } else { // Insert a row *below* the bottomRow, pushing the top row into the scrollback - this.buffer.lines.splice(bottomRow + 1, 0, this.blankLine(undefined, isWrapped)); + if (!this.buffer.hasScrollback) { + // A line is deleted in this case so bottomRow is pushed up + this.buffer.lines.splice(bottomRow, 0, newLine); + } else { + this.buffer.lines.splice(bottomRow + 1, 0, newLine); + } console.log('splice'); } - // Adjust ydisp and ybase to accommodate the changes after the buffer was - // trimmed. + // Only adjust ybase and ydisp when the buffer is not trimmed if (!willBufferBeTrimmed) { console.log('increment ydisp/ybase'); this.buffer.ybase++; this.buffer.ydisp++; } + console.log('this.buffer.ybase: ' + this.buffer.ybase); - console.log('this.buffer.ybase: ' + this.buffer.ydisp); + console.log('this.buffer.ydisp: ' + this.buffer.ydisp); + } else { - const topRow = this.buffer.ybase + this.buffer.scrollTop; const scrollRegionHeight = bottomRow - topRow + 1/*as it's zero-based*/; -console.log('shiftElements'); -console.log('topRow: ' + topRow + '(' + this.buffer.lines.get(topRow)[0][1] + ')'); -console.log('scrollRegionHeight: ' + scrollRegionHeight); + + console.log('shiftElements'); + console.log('topRow: ' + topRow + '(' + this.buffer.lines.get(topRow)[0][1] + ')'); + console.log('scrollRegionHeight: ' + scrollRegionHeight); + this.buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1); - this.buffer.lines.set(bottomRow, this.blankLine(undefined, isWrapped)); + this.buffer.lines.set(bottomRow, newLine); } if (!this.userScrolling) { @@ -2247,7 +2270,8 @@ console.log('scrollRegionHeight: ' + scrollRegionHeight); // possibly move the code below to term.reverseScroll(); // test: echo -ne '\e[1;1H\e[44m\eM\e[0m' // blankLine(true) is xterm/linux behavior - this.buffer.lines.shiftElements(this.buffer.y + this.buffer.ybase, this.rows - 1, 1); + const scrollRegionHeight = this.buffer.scrollBottom - this.buffer.scrollTop; + this.buffer.lines.shiftElements(this.buffer.y + this.buffer.ybase, scrollRegionHeight, 1); this.buffer.lines.set(this.buffer.y + this.buffer.ybase, this.blankLine(true)); this.updateRange(this.buffer.scrollTop); this.updateRange(this.buffer.scrollBottom); From 41463d8eb5ae6e604eb524c9be7787935f5a75d7 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 25 Aug 2017 12:41:53 -0700 Subject: [PATCH 005/156] Make reverseIndex scroll region-aware This was causing a issue when scrollback was 0 as the start of the buffer was getting copied --- src/InputHandler.ts | 3 -- src/Terminal.ts | 107 ++++---------------------------------------- test/bar | 0 test/foo | 0 4 files changed, 9 insertions(+), 101 deletions(-) create mode 100644 test/bar create mode 100644 test/foo diff --git a/src/InputHandler.ts b/src/InputHandler.ts index e6ab55402..f2d977511 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -464,13 +464,10 @@ export class InputHandler implements IInputHandler { let j: number; j = this._terminal.rows - 1 - this._terminal.buffer.scrollBottom; j = this._terminal.rows - 1 + this._terminal.buffer.ybase - j; - console.log('deleteLines', params); while (param--) { // test: echo -e '\e[44m\e[1M\e[0m' // blankLine(true) - xterm/linux behavior - console.log('splice delete row: ' + (row - 1) + '(' + this._terminal.buffer.lines.get(row - 1)[0][1] + ')'); this._terminal.buffer.lines.splice(row - 1, 1); - console.log('splice add blank row: ' + (j) + '(' + this._terminal.buffer.lines.get(j)[0][1] + ')'); this._terminal.buffer.lines.splice(j, 0, this._terminal.blankLine(true)); } diff --git a/src/Terminal.ts b/src/Terminal.ts index fa3161f0e..ea3800d0c 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1163,133 +1163,44 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT /** * Scroll the terminal down 1 row, creating a blank line. - * @param {boolean} isWrapped Whether the new line is wrapped from the previous - * line. + * @param isWrapped Whether the new line is wrapped from the previous line. */ public scroll(isWrapped?: boolean): void { - // The problem is caused when scrollback = 0. This section was written - // around the assumption that there would be buffer to trim when necessary. - - - // shiftElements should be used here - - // If scrollTop is set (non-zero), we should shiftElements - // scrollBottom defines the position at which the new element is inserted - - // Note that both scrollBottom and scrollTop need to be handled individually - // because they can be set to terminal.rows or 0 respectively - - // Don't mess with ybase temporarily, asking for trouble - - // This should work with minimal effort utilizing the CiruclarList - - // TODO: Need to hold onto top given the if? const newLine = this.blankLine(undefined, isWrapped); const topRow = this.buffer.ybase + this.buffer.scrollTop; let bottomRow = this.buffer.ybase + this.buffer.scrollBottom; - - -// TODO: There's a problem with ybase going beyond scrollback? - - - console.log('bottomRow', bottomRow); - console.log('this.buffer.scrollTop: ' + this.buffer.scrollTop); if (this.buffer.scrollTop === 0) { + // Determine whether the buffer is going to be trimmed after insertion. const willBufferBeTrimmed = this.buffer.lines.length === this.buffer.lines.maxLength; - // Remove the line first if there is no scrollback so the list is not - // trimmed - if (!this.buffer.hasScrollback) { - this.buffer.lines.splice(topRow, 1); - // Adjust bottomRow to make up for the deleted row - // bottomRow--; - } - + // Insert the line using the fastest method if (bottomRow === this.buffer.lines.length - 1) { - // Pushing when possible is faster than splicing this.buffer.lines.push(newLine); - console.log('push'); } else { - // Insert a row *below* the bottomRow, pushing the top row into the scrollback - if (!this.buffer.hasScrollback) { - // A line is deleted in this case so bottomRow is pushed up - this.buffer.lines.splice(bottomRow, 0, newLine); - } else { - this.buffer.lines.splice(bottomRow + 1, 0, newLine); - } - console.log('splice'); + this.buffer.lines.splice(bottomRow + 1, 0, newLine); } // Only adjust ybase and ydisp when the buffer is not trimmed if (!willBufferBeTrimmed) { - console.log('increment ydisp/ybase'); this.buffer.ybase++; this.buffer.ydisp++; } - - console.log('this.buffer.ybase: ' + this.buffer.ybase); - console.log('this.buffer.ydisp: ' + this.buffer.ydisp); - } else { + // scrollTop is non-zero which means no line will be going to the + // scrollback, instead we can just shift them in-place. const scrollRegionHeight = bottomRow - topRow + 1/*as it's zero-based*/; - - console.log('shiftElements'); - console.log('topRow: ' + topRow + '(' + this.buffer.lines.get(topRow)[0][1] + ')'); - console.log('scrollRegionHeight: ' + scrollRegionHeight); - this.buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1); this.buffer.lines.set(bottomRow, newLine); } + // Move the viewport to the bottom of the buffer unless the user is + // scrolling. if (!this.userScrolling) { this.buffer.ydisp = this.buffer.ybase; } - - // Make room for the new row in lines - // const bufferNeedsTrimming = this.buffer.lines.length === this.buffer.lines.maxLength; - // if (bufferNeedsTrimming) { - // this.buffer.lines.trimStart(1); - // this.buffer.ybase--; - // this.buffer.ydisp = Math.max(this.buffer.ydisp - 1, 0); - // } - - // this.buffer.ybase++; - - // // Scroll the viewport down to the bottom if the user is not scrolling - // if (!this.userScrolling) { - // this.buffer.ydisp = this.buffer.ybase; - // } - - // // last line - // let row = this.buffer.ybase + this.rows - 1; - - // // subtract the bottom scroll region - // row -= this.rows - 1 - this.buffer.scrollBottom; - - // // Same as this? - // // row = this.buffer.ybase + this.buffer.scrollBottom; - - // if (row === this.buffer.lines.length) { - // // Optimization: pushing is faster than splicing when they amount to the same behavior - // this.buffer.lines.push(this.blankLine(undefined, isWrapped)); - // } else { - // // add our new line - // this.buffer.lines.splice(row, 0, this.blankLine(undefined, isWrapped)); - // } - - // if (this.buffer.scrollTop !== 0) { - // if (this.buffer.ybase !== 0) { - // this.buffer.ybase--; - // if (!this.userScrolling) { - // this.buffer.ydisp = this.buffer.ybase; - // } - // } - // this.buffer.lines.splice(this.buffer.ybase + this.buffer.scrollTop, 1); - // } - - // this.maxRange(); + // Flag rows that need updating this.updateRange(this.buffer.scrollTop); this.updateRange(this.buffer.scrollBottom); diff --git a/test/bar b/test/bar new file mode 100644 index 000000000..e69de29bb diff --git a/test/foo b/test/foo new file mode 100644 index 000000000..e69de29bb From 54cf30ae68f8b80b183f346241752dc2b99e2508 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 25 Aug 2017 12:47:45 -0700 Subject: [PATCH 006/156] Remove test files --- test/bar | 0 test/foo | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 test/bar delete mode 100644 test/foo diff --git a/test/bar b/test/bar deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/foo b/test/foo deleted file mode 100644 index e69de29bb..000000000 From bc86664726bf2f1f0ca2e700f70545a125f3b369 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 25 Aug 2017 13:14:30 -0700 Subject: [PATCH 007/156] Improve Buffer.resize to work with uninitialized buffers Fixes #924 --- src/Buffer.ts | 138 +++++++++++++++++++++++++------------------------- 1 file changed, 69 insertions(+), 69 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 53645837d..4f81025fe 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -94,11 +94,6 @@ export class Buffer implements IBuffer { * @param newRows The new number of rows. */ public resize(newCols: number, newRows: number): void { - // Don't resize the buffer if it's empty and hasn't been used yet. - if (this._lines.length === 0) { - return; - } - // Increase max length if needed before adjustments to allow space to fill // as required. const newMaxLength = this._getCorrectBufferLength(newRows); @@ -106,83 +101,88 @@ export class Buffer implements IBuffer { this._lines.maxLength = newMaxLength; } - // 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? - 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 - if (this._lines.get(i) === undefined) { - this._lines.set(i, this._terminal.blankLine(undefined, undefined, newCols)); - } - while (this._lines.get(i).length < newCols) { - this._lines.get(i).push(ch); + // The following adjustments should only happen if the buffer has been + // initialized/filled. + 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? + 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 + if (this._lines.get(i) === undefined) { + this._lines.set(i, this._terminal.blankLine(undefined, undefined, newCols)); + } + while (this._lines.get(i).length < newCols) { + this._lines.get(i).push(ch); + } } } - } - // Resize rows in both directions as needed - let addToY = 0; - if (this._terminal.rows < newRows) { - for (let y = this._terminal.rows; y < newRows; y++) { - if (this._lines.length < newRows + this.ybase) { - if (this.ybase > 0 && this._lines.length <= this.ybase + this.y + addToY + 1) { - // There is room above the buffer and there are no empty elements below the line, - // scroll up - this.ybase--; - addToY++; - if (this.ydisp > 0) { - // Viewport is at the top of the buffer, must increase downwards - this.ydisp--; + // Resize rows in both directions as needed + let addToY = 0; + if (this._terminal.rows < newRows) { + for (let y = this._terminal.rows; y < newRows; y++) { + if (this._lines.length < newRows + this.ybase) { + if (this.ybase > 0 && this._lines.length <= this.ybase + this.y + addToY + 1) { + // There is room above the buffer and there are no empty elements below the line, + // scroll up + this.ybase--; + addToY++; + if (this.ydisp > 0) { + // Viewport is at the top of the buffer, must increase downwards + this.ydisp--; + } + } else { + // Add a blank line if there is no buffer left at the top to scroll to, or if there + // are blank lines after the cursor + this._lines.push(this._terminal.blankLine(undefined, undefined, newCols)); + } + } + } + } else { // (this._terminal.rows >= newRows) + for (let y = this._terminal.rows; y > newRows; y--) { + if (this._lines.length > newRows + this.ybase) { + if (this._lines.length > this.ybase + this.y + 1) { + // The line is a blank line below the cursor, remove it + this._lines.pop(); + } else { + // The line is the cursor, scroll down + this.ybase++; + this.ydisp++; } - } else { - // Add a blank line if there is no buffer left at the top to scroll to, or if there - // are blank lines after the cursor - this._lines.push(this._terminal.blankLine(undefined, undefined, newCols)); } } } - } else { // (this._terminal.rows >= newRows) - for (let y = this._terminal.rows; y > newRows; y--) { - if (this._lines.length > newRows + this.ybase) { - if (this._lines.length > this.ybase + this.y + 1) { - // The line is a blank line below the cursor, remove it - this._lines.pop(); - } else { - // The line is the cursor, scroll down - this.ybase++; - this.ydisp++; - } + + // Reduce max length if needed after adjustments, this is done after as it + // would otherwise cut data from the bottom of the buffer. + if (newMaxLength < this._lines.maxLength) { + // Trim from the top of the buffer and adjust ybase and ydisp. + const amountToTrim = this._lines.length - newMaxLength; + if (amountToTrim > 0) { + this._lines.trimStart(amountToTrim); + this.ybase = Math.max(this.ybase - amountToTrim, 0); + this.ydisp = Math.max(this.ydisp - amountToTrim, 0); } + this._lines.maxLength = newMaxLength; } - } - // Reduce max length if needed after adjustments, this is done after as it - // would otherwise cut data from the bottom of the buffer. - if (newMaxLength < this._lines.maxLength) { - // Trim from the top of the buffer and adjust ybase and ydisp. - const amountToTrim = this._lines.length - newMaxLength; - if (amountToTrim > 0) { - this._lines.trimStart(amountToTrim); - this.ybase = Math.max(this.ybase - amountToTrim, 0); - this.ydisp = Math.max(this.ydisp - amountToTrim, 0); + // Make sure that the cursor stays on screen + if (this.y >= newRows) { + this.y = newRows - 1; } - this._lines.maxLength = newMaxLength; + if (addToY) { + this.y += addToY; + } + + if (this.x >= newCols) { + this.x = newCols - 1; + } + + this.scrollTop = 0; } - // Make sure that the cursor stays on screen - if (this.y >= newRows) { - this.y = newRows - 1; - } - if (addToY) { - this.y += addToY; - } - - if (this.x >= newCols) { - this.x = newCols - 1; - } - - this.scrollTop = 0; this.scrollBottom = newRows - 1; } From 6b8b32015dac0f5afedbc06e10dc370d24f1a4cb Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 27 Aug 2017 10:39:13 -0700 Subject: [PATCH 008/156] Don't include Terminal in search addon Fixes #927 --- gulpfile.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gulpfile.js b/gulpfile.js index ea5688bc5..6a20c7292 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -7,6 +7,7 @@ const buffer = require('vinyl-buffer'); const coveralls = require('gulp-coveralls'); const fs = require('fs-extra'); const gulp = require('gulp'); +const path = require('path'); const istanbul = require('gulp-istanbul'); const merge = require('merge-stream'); const mocha = require('gulp-mocha'); @@ -96,6 +97,7 @@ gulp.task('browserify-addons', ['tsc'], function() { packageCache: {} }; let searchBundle = browserify(searchOptions) + .external(path.join(outDir, 'Terminal.js')) .bundle() .pipe(source('./addons/search/search.js')) .pipe(buffer()) From a57e511fa3f95ac42a1e1296048399d55e8b0d05 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 29 Aug 2017 21:08:11 -0700 Subject: [PATCH 009/156] Properly setup and maintain tab stops for the alt buffer Fixes #930 --- src/Buffer.ts | 45 ++++++++++++++++++++++++++++++++++++- src/BufferSet.ts | 11 +++++++++ src/InputHandler.ts | 6 ++--- src/Interfaces.ts | 4 ++-- src/Terminal.ts | 45 ++----------------------------------- src/utils/TestUtils.test.ts | 6 +++++ 6 files changed, 68 insertions(+), 49 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 4f81025fe..4c7300639 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -82,10 +82,10 @@ export class Buffer implements IBuffer { this.ybase = 0; this.y = 0; this.x = 0; - this.tabs = {}; this._lines = new CircularList(this._getCorrectBufferLength(this._terminal.rows)); this.scrollTop = 0; this.scrollBottom = this._terminal.rows - 1; + this.setupTabStops(); } /** @@ -233,4 +233,47 @@ export class Buffer implements IBuffer { return lineString.substring(widthAdjustedStartCol, finalEndCol); } + + /** + * Setup the tab stops. + * @param i The index to start setting up tab stops from. + */ + public setupTabStops(i?: number): void { + if (i != null) { + if (!this.tabs[i]) { + i = this.prevStop(i); + } + } else { + this.tabs = {}; + i = 0; + } + + for (; i < this._terminal.cols; i += this._terminal.options.tabStopWidth) { + this.tabs[i] = true; + } + } + + /** + * Move the cursor to the previous tab stop from the given position (default is current). + * @param x The position to move the cursor to the previous tab stop. + */ + public prevStop(x?: number): number { + if (x == null) { + x = this.x; + } + while (!this.tabs[--x] && x > 0); + return x >= this._terminal.cols ? this._terminal.cols - 1 : x < 0 ? 0 : x; + } + + /** + * Move the cursor one tab stop forward from the given position (default is current). + * @param x The position to move the cursor one tab stop forward. + */ + public nextStop(x?: number): number { + if (x == null) { + x = this.x; + } + while (!this.tabs[++x] && x < this._terminal.cols); + return x >= this._terminal.cols ? this._terminal.cols - 1 : x < 0 ? 0 : x; + } } diff --git a/src/BufferSet.ts b/src/BufferSet.ts index d37a650f9..64b364e92 100644 --- a/src/BufferSet.ts +++ b/src/BufferSet.ts @@ -28,6 +28,8 @@ export class BufferSet extends EventEmitter implements IBufferSet { // See http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer this._alt = new Buffer(this._terminal, false); this._activeBuffer = this._normal; + + this.setupTabStops(); } /** @@ -87,4 +89,13 @@ export class BufferSet extends EventEmitter implements IBufferSet { this._normal.resize(newCols, newRows); this._alt.resize(newCols, newRows); } + + /** + * Setup the tab stops. + * @param i The index to start setting up tab stops from. + */ + public setupTabStops(i?: number): void { + this._normal.setupTabStops(i); + this._alt.setupTabStops(i); + } } diff --git a/src/InputHandler.ts b/src/InputHandler.ts index f2d977511..abfdfa0e6 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -157,7 +157,7 @@ export class InputHandler implements IInputHandler { * Horizontal Tab (HT) (Ctrl-I). */ public tab(): void { - this._terminal.buffer.x = this._terminal.nextStop(); + this._terminal.buffer.x = this._terminal.buffer.nextStop(); } /** @@ -349,7 +349,7 @@ export class InputHandler implements IInputHandler { public cursorForwardTab(params: number[]): void { let param = params[0] || 1; while (param--) { - this._terminal.buffer.x = this._terminal.nextStop(); + this._terminal.buffer.x = this._terminal.buffer.nextStop(); } } @@ -548,7 +548,7 @@ export class InputHandler implements IInputHandler { public cursorBackwardTab(params: number[]): void { let param = params[0] || 1; while (param--) { - this._terminal.buffer.x = this._terminal.prevStop(); + this._terminal.buffer.x = this._terminal.buffer.prevStop(); } } diff --git a/src/Interfaces.ts b/src/Interfaces.ts index 943003aec..fb66f4d7c 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -92,14 +92,12 @@ export interface IInputHandlingTerminal extends IEventEmitter { convertEol: boolean; updateRange(y: number): void; scroll(isWrapped?: boolean): void; - nextStop(x?: number): number; setgLevel(g: number): void; eraseAttr(): number; eraseRight(x: number, y: number): void; eraseLine(y: number): void; eraseLeft(x: number, y: number): void; blankLine(cur?: boolean, isWrapped?: boolean): LineData; - prevStop(x?: number): number; is(term: string): boolean; send(data: string): void; setgCharset(g: number, charset: Charset): void; @@ -146,6 +144,8 @@ export interface IBuffer { savedY: number; savedX: number; translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string; + nextStop(x?: number): number; + prevStop(x?: number): number; } export interface IBufferSet { diff --git a/src/Terminal.ts b/src/Terminal.ts index ea3800d0c..827273ff5 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -392,8 +392,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT if (this.selectionManager) { this.selectionManager.setBuffer(this.buffer); } - - this.setupStops(); } /** @@ -486,7 +484,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.buffers.resize(this.cols, this.rows); this.viewport.syncScrollArea(); break; - case 'tabStopWidth': this.setupStops(); break; + case 'tabStopWidth': this.buffers.setupTabStops(); break; case 'bellSound': case 'bellStyle': this.syncBellSound(); break; } @@ -1938,7 +1936,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.cols = x; this.rows = y; - this.setupStops(this.cols); + this.buffers.setupTabStops(this.cols); this.charMeasure.measure(); @@ -1971,45 +1969,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.refreshEnd = this.rows - 1; } - /** - * Setup the tab stops. - * @param {number} i - */ - public setupStops(i?: number): void { - if (i != null) { - if (!this.buffer.tabs[i]) { - i = this.prevStop(i); - } - } else { - this.buffer.tabs = {}; - i = 0; - } - - for (; i < this.cols; i += this.getOption('tabStopWidth')) { - this.buffer.tabs[i] = true; - } - } - - /** - * Move the cursor to the previous tab stop from the given position (default is current). - * @param {number} x The position to move the cursor to the previous tab stop. - */ - public prevStop(x?: number): number { - if (x == null) x = this.buffer.x; - while (!this.buffer.tabs[--x] && x > 0); - return x >= this.cols ? this.cols - 1 : x < 0 ? 0 : x; - } - - /** - * Move the cursor one tab stop forward from the given position (default is current). - * @param {number} x The position to move the cursor one tab stop forward. - */ - public nextStop(x?: number): number { - if (x == null) x = this.buffer.x; - while (!this.buffer.tabs[++x] && x < this.cols); - return x >= this.cols ? this.cols - 1 : x < 0 ? 0 : x; - } - /** * Erase in the identified line everything from "x" to the end of the line (right). * @param {number} x The column from which to start erasing to the end of the line. diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index 8d4755609..ae83aed4f 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -189,6 +189,12 @@ export class MockBuffer implements IBuffer { translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string { throw new Error('Method not implemented.'); } + nextStop(x?: number): number { + throw new Error('Method not implemented.'); + } + prevStop(x?: number): number { + throw new Error('Method not implemented.'); + } } export class MockViewport implements IViewport { From c6d4c73c8a8c23ce102918fc0342cf33d2986711 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 30 Aug 2017 14:50:07 -0700 Subject: [PATCH 010/156] 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 fb66f4d7c..b643de45c 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 b51a9ff93..1586d8fef 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 827273ff5..0bb00ecf6 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 011/156] 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 1586d8fef..8c1550965 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 000000000..703d14643 --- /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 0bb00ecf6..2ddba9a28 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 012/156] progress --- src/RendererCanvas.ts | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/RendererCanvas.ts b/src/RendererCanvas.ts index 703d14643..0665b880d 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 013/156] 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 abfdfa0e6..e9346c1a0 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 b643de45c..fb66f4d7c 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 8c1550965..f4a275bbe 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 0665b880d..9d4156c41 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 2ddba9a28..429092989 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 000000000..97e7f9ea8 --- /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 014/156] 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 2323f2267..5a0fc1e12 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 429092989..c076d4106 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 015/156] 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 4c7300639..e8ceebf32 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 9d4156c41..934662211 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._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._renderLayers = [ + new BackgroundRenderLayer(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 @@ -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 000000000..a624ef2ef --- /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 000000000..6e0c39336 --- /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 14b0a58d4..321cf6d64 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 016/156] 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 9b550d676..21ab9b893 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 017/156] Move fg rendering into IRenderLayer --- src/RendererCanvas.ts | 286 +++++++------------------- 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, 300 insertions(+), 234 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 934662211..bf952b6c6 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); - } + // 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); - - 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; + // 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(); + // 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; - }); + // 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); - } + // 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 a624ef2ef..4f8e9ea75 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 000000000..250c5c50a --- /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 000000000..adf6c10a4 --- /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 6e0c39336..f95725256 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 000000000..09e74439f --- /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 321cf6d64..2ee98e54e 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 018/156] 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 bf952b6c6..27832bed9 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 4f8e9ea75..b283ce158 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 adf6c10a4..d3b1fb058 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 019/156] 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 27832bed9..bca5f70e0 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 b283ce158..c1d9eac06 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 d3b1fb058..da0519bda 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 f95725256..4d2753751 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 020/156] 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 e8ceebf32..d8311f067 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 da0519bda..2ed0bd602 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 (!code) { + continue; + } - // if (ch === ' ') { - // 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 021/156] 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 0753d80e1..180dc5d67 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 c1d9eac06..8577f58d6 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 2ed0bd602..c62632473 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]; - if (!code) { + // 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; + + // 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 000000000..eba3dad6a --- /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 022/156] 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 8577f58d6..ea7bea60a 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 c62632473..05cceb47d 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 023/156] 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 c076d4106..86c18a411 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 bca5f70e0..da1ca14d5 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 024/156] 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 da1ca14d5..61a4b1106 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 025/156] 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 86c18a411..594a6fb2c 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 ea7bea60a..dac476a07 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 05cceb47d..2b1ff2721 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 000000000..bc31022cd --- /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 09e74439f..621db0bd8 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 026/156] 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 1aaec14b0..1e3fe9b88 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 594a6fb2c..f88ebb7f9 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 dac476a07..162b96a4e 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 2b1ff2721..0f74ff289 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 4d2753751..4d6d2d3b9 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 61a4b1106..b4d9b153c 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 bc31022cd..b630359e6 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 2ee98e54e..8a48c6f7c 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 027/156] 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 1e3fe9b88..dc58858ec 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 b630359e6..b0a6a5a60 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 028/156] 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 162b96a4e..d63a561f0 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 250c5c50a..4f75664c4 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 0f74ff289..fbfb1a6f9 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 029/156] 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 4f75664c4..eb3954eae 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 b0a6a5a60..091788f92 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 8a48c6f7c..dcbd6b694 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 030/156] 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 000000000..d9a21d15f --- /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 fbfb1a6f9..db23a041a 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 b4d9b153c..b340afba8 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 dcbd6b694..6b044217a 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 031/156] 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 d63a561f0..a9722eabf 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 000000000..6d4939608 --- /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 d9a21d15f..114966104 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 db23a041a..a073f69da 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 b340afba8..522f7af20 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 091788f92..048be8e42 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 6b044217a..f281e527e 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 032/156] 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 6d4939608..66b07fac0 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 a073f69da..387007128 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 033/156] 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 66b07fac0..92e95cd23 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 114966104..da32aaa82 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 387007128..c3cb833f1 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 034/156] Simplify color generation code --- src/renderer/BaseRenderLayer.ts | 3 +- src/renderer/Color.ts | 64 +++++++++++++++++++------------ src/renderer/CursorRenderLayer.ts | 7 ++-- 3 files changed, 46 insertions(+), 28 deletions(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 92e95cd23..5e53c411f 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 eb3954eae..446d440b3 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(); + + // 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}`); } - // 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; + // 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 da32aaa82..75dd4d898 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 035/156] 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 fb66f4d7c..1b1625bca 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 f88ebb7f9..c9c6466c7 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 a9722eabf..37b9c69e4 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 5e53c411f..419b814cc 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 75dd4d898..766422291 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 c3cb833f1..3cd7ae1eb 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 eba3dad6a..a4657fcba 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 4d6d2d3b9..8eb3acdde 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 522f7af20..26b472cfd 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 048be8e42..a2cfb1d62 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 68e1b6a78..02a8dae9e 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 45b06f591..ff777811e 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 f281e527e..6a7504f3e 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 ccc83ed90..dce15bca4 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 036/156] 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 766422291..c18bb1308 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 037/156] 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 37b9c69e4..1e8169d76 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 419b814cc..7790e0344 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 c18bb1308..bb0ff9284 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 3cd7ae1eb..68d00857a 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 a2cfb1d62..602bb486b 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 038/156] 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 1e8169d76..a37c43875 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 7790e0344..e286b2204 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 drawChar(terminal: ITerminal, char: string, code: number, fg: number, x: number, y: number, scaledCharWidth: number, scaledCharHeight: number): 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): 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 bb0ff9284..7b5be0522 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 68d00857a..e5b814ce4 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 8eb3acdde..82b2e77a2 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 26b472cfd..a512c9834 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 602bb486b..daaf160d6 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 039/156] 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 c9c6466c7..640aafaaa 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 e5b814ce4..680f1abc5 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 040/156] 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 e286b2204..331bc678a 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 a512c9834..2cce350b2 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 ff777811e..1366bb631 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 041/156] 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 640aafaaa..92fc54402 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 042/156] 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 92fc54402..0c6eb65dc 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 331bc678a..047269c37 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 7b5be0522..89707590e 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 82b2e77a2..55cffbc54 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 2cce350b2..2903460a3 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 043/156] 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 1b1625bca..afa2de58d 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 89707590e..11e56347a 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 044/156] 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 d8311f067..101244f1f 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 983b3536e..b047f91f7 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 0c6eb65dc..17fd0d976 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 047269c37..378ac1a16 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 11e56347a..3ccf2ec26 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 55cffbc54..5da13109e 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 2903460a3..7c6f6d400 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 045/156] 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 afa2de58d..3a0c99297 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 17fd0d976..a66332352 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 a37c43875..cf122cc23 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 378ac1a16..d131bee0f 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 446d440b3..000000000 --- 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 000000000..ca8015a2b --- /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 3ccf2ec26..bfec1d8e9 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 680f1abc5..fa4ef8007 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 5da13109e..4b8ce816f 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 7c6f6d400..653e2cbaf 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 daaf160d6..4ec967644 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 621db0bd8..09e74439f 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 000000000..a375d4682 --- /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 046/156] 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 d131bee0f..e996a2a89 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 a375d4682..18398efc7 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 ae83aed4f..3a66d49d0 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 047/156] 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 a66332352..13f388603 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 cf122cc23..271dc2ac8 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 e996a2a89..20425e413 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 bfec1d8e9..1e5cf6478 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 fa4ef8007..5a1d41f6c 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 4b8ce816f..5626f13db 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 653e2cbaf..96e899f66 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 4ec967644..1dcb8ab6a 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 048/156] 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 1e5cf6478..1533c4f22 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 5626f13db..45822e653 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 049/156] Fix the fit addon --- demo/main.js | 33 +++++++++++++-------------- src/addons/fit/fit.js | 33 +++++++++++---------------- src/renderer/BackgroundRenderLayer.ts | 4 ++++ src/renderer/ForegroundRenderLayer.ts | 5 ++++ src/renderer/GridCache.ts | 2 +- 5 files changed, 39 insertions(+), 38 deletions(-) diff --git a/demo/main.js b/demo/main.js index cb346de2a..288407098 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; + // fit is called within a setTimeout, cols and rows need this. + setTimeout(() => { + colsElement.value = term.cols; + rowsElement.value = term.rows; - colsElement.value = cols; - rowsElement.value = rows; + fetch('/terminals?cols=' + cols + '&rows=' + rows, {method: 'POST'}).then(function (res) { - 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); - 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; + 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 da66802d1..1e46932cd 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 271dc2ac8..33550af05 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 5a1d41f6c..9df1f49b1 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 a4657fcba..56294ddcb 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 050/156] 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 20425e413..207f87ee9 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 1533c4f22..cd11f1dc8 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 9df1f49b1..53660fa0e 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 051/156] 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 33550af05..729a8ec5f 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 207f87ee9..858f15b86 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 53660fa0e..7d1cec1fd 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 052/156] 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 858f15b86..523edbda0 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 7d1cec1fd..30f68f4c5 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 053/156] 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 288407098..f05a8cef1 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 054/156] 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 f4a275bbe..000000000 --- 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 96e899f66..119399b06 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 5a5d8a784..000000000 --- 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 7707ad9ab..000000000 --- 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 055/156] 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 13f388603..616c29459 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 056/156] 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 3a0c99297..745dd87a4 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 616c29459..fc5af887b 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 523edbda0..3da7aadcd 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 cd11f1dc8..941d6d3b7 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 30f68f4c5..f4bc1e777 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 45822e653..b47a75b9a 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 119399b06..474699c5b 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 057/156] 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 745dd87a4..b6ba08db1 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 3da7aadcd..fb78e1d2a 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 18398efc7..e71d8d5a5 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 ca8015a2b..341e4ca58 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 941d6d3b7..10f3ad8ce 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 b47a75b9a..fd94d59d7 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 3a66d49d0..f4eae64db 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 058/156] 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 fc5af887b..7d2e8fd9c 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 10f3ad8ce..752b42ddf 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 059/156] 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 752b42ddf..9a2ce3fdd 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 060/156] 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 9a2ce3fdd..0894498f2 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 474699c5b..cc85c7e76 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 061/156] 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 fb78e1d2a..ecd064f3f 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 062/156] 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 ecd064f3f..bd8e48966 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 e71d8d5a5..a9fe8bd30 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 063/156] 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 bd8e48966..7e8835986 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 0894498f2..96e4b8fb9 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 064/156] 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 7e8835986..2650af3ac 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 96e4b8fb9..cc443b497 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 f4bc1e777..182c9eadf 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 065/156] 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 2650af3ac..20768c8ad 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 a9fe8bd30..d25520d11 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 066/156] 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 b6ba08db1..0b50161cd 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 7d2e8fd9c..c572fefb6 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 067/156] Organize temporary color handling code --- src/Terminal.ts | 210 +++++++++++--------------- src/renderer/BaseRenderLayer.ts | 5 +- src/renderer/ColorManager.ts | 2 +- src/renderer/ForegroundRenderLayer.ts | 1 - 4 files changed, 95 insertions(+), 123 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index c572fefb6..59d39e5af 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); } -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 { - 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 } + +/** + * 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 +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 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 20768c8ad..574271222 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 341e4ca58..6b7f88c1f 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 182c9eadf..1f579f91a 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 068/156] 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 0b50161cd..8af079348 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 dc58858ec..1425aedc1 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 59d39e5af..9dba67017 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 4ad4ddec4..b061eeb6c 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 1e46932cd..b774cebfd 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 574271222..912d351f9 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 cc85c7e76..b3df2b7a0 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 c61624e9f..f25a6b2e9 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 dce15bca4..2956c9378 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 069/156] 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 2956c9378..72206c824 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 070/156] 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 8af079348..41fc8bdad 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 4774b3bb9..b5a9d5529 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 1425aedc1..e84bde686 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 9dba67017..fec7fdcaa 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 428ef0987..8a960fc7d 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 b061eeb6c..10418fa77 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 b774cebfd..4f5545797 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 f25a6b2e9..77c422d5e 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 071/156] 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 41fc8bdad..861720a3a 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 fec7fdcaa..8003dce33 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 10418fa77..cedcef203 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 6a7504f3e..30bce803f 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 072/156] 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 713896212..ee00eca03 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 861720a3a..2b4909c9d 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 8003dce33..d47c6bf85 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 cedcef203..88433ed5a 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 b3df2b7a0..adf5bbd0f 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 f4eae64db..46a226a23 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 30bce803f..0ddfe8b57 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 073/156] 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 ee00eca03..1ab5f61ca 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 074/156] Fix the composition element's position --- src/Buffer.ts | 6 ++++++ src/CompositionHelper.ts | 21 +++++++++++---------- src/Interfaces.ts | 1 + src/xterm.css | 5 +++++ 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 101244f1f..6931eea45 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 223a90e7c..345041d66 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 2b4909c9d..7ebf69837 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 0ddfe8b57..df3aff590 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 075/156] 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 88433ed5a..2de698a0e 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 4f5545797..60214dad9 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 076/156] 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 72206c824..b1cea8169 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 077/156] 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 729a8ec5f..292847c2d 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 cc443b497..482d4e148 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 1f579f91a..e8835dea6 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 1dcb8ab6a..7f131da80 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 46a226a23..d4dc16ab0 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 078/156] 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 912d351f9..686a1d252 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 e8835dea6..8ee81f02d 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 079/156] 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 adf5bbd0f..bc1a7a1b8 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 080/156] 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 bc1a7a1b8..58f31ff76 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 081/156] 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 d47c6bf85..4373f5ec8 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 082/156] 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 686a1d252..53c48b696 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 d25520d11..a92037a88 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 8ee81f02d..907f686d8 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 083/156] 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 1366bb631..37d6697aa 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 084/156] 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 53c48b696..462c4eca9 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 58f31ff76..bc0717dc4 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 085/156] Basic link support using mouse zone manager --- src/Linkifier.ts | 238 ++++++++-------------------------- src/Terminal.ts | 59 +++++---- src/Types.ts | 3 +- src/input/Interfaces.ts | 11 ++ src/input/MouseZoneManager.ts | 85 ++++++++++++ 5 files changed, 186 insertions(+), 210 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 5a0fc1e12..bfac87abb 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]; + + // TODO: Match more than one link per row // 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); + // 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; + this._addLink(match.index, match.length, uri, matcher); + } + } + + 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); } - 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); - } - 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; + 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 4373f5ec8..6f93ca2df 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 180dc5d67..378a66acb 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 000000000..9bb90fed6 --- /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 000000000..7e63ee0bb --- /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 086/156] 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 bfac87abb..f92badcf7 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 9bb90fed6..d0c0e4804 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 7e63ee0bb..6d8f017a4 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 77c422d5e..0d433cd6f 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 087/156] 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 7ebf69837..ace4012e5 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 f92badcf7..20c5b713e 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 6f93ca2df..c3d360bb9 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 d0c0e4804..15dcbae9b 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 6d8f017a4..37a6647d3 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 088/156] 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 6931eea45..65b7d58e5 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 e9346c1a0..3bf16a1c3 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 ace4012e5..9ce8ffd48 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 a4ed70db2..6b28acb94 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 20c5b713e..cae0d6625 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 b5a9d5529..8c90693e9 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 378a66acb..1ced3d73a 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 d4dc16ab0..0388e9da1 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 b1cea8169..d61279785 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 089/156] 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 c3d360bb9..620340171 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 a92037a88..4fcedd1a7 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 090/156] 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 ffe6afd49..90958f83f 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 0adb1719a..7d78a259b 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 0c333741f..7a44ee81e 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 620340171..cab25b03b 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 8a960fc7d..6d23e19a1 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 fd94d59d7..f7645abf8 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 bc0717dc4..6b12188ed 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 0388e9da1..51f191a83 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 d61279785..866021e87 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 091/156] 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 e84bde686..4fe09c8c0 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 092/156] 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 6b28acb94..f165590f9 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 cae0d6625..d3150b4f6 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 093/156] 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 6b7f88c1f..2b16b1c39 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 094/156] 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 56294ddcb..3b3ae8dfa 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 095/156] 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 2b16b1c39..581f09a9f 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 482d4e148..00abfb933 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 096/156] 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 9ce8ffd48..da6d27272 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 4fcedd1a7..05eb9a841 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 581f09a9f..17aee3928 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 f7645abf8..04e24e19b 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 7f131da80..91e62a0c6 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 866021e87..eff26c9bb 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 097/156] 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 907f686d8..71168f26a 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 098/156] 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 71168f26a..ef9c3c600 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 099/156] 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 000000000..37e55ee14 --- /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 100/156] 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 000000000..6bd9c9777 --- /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 17aee3928..2ae785dde 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 101/156] 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 97e7f9ea8..000000000 --- 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 6b12188ed..66fa6a150 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 102/156] 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 000000000..15b68ef2d --- /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 0d433cd6f..3bbad855a 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 51f191a83..ed0a19fbe 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 103/156] 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 05eb9a841..e25628730 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 104/156] 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 edfa1169c..f6b805ef1 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 105/156] 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 da6d27272..a0d75a54c 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 cab25b03b..15c6335a4 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 eff26c9bb..d98f6c5d3 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 106/156] 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 d3150b4f6..e04ad767d 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 462c4eca9..08a229edb 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 66fa6a150..a6d679a2d 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 107/156] 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 90958f83f..df79d2d04 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 108/156] 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 08a229edb..7bb7e9c5a 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 00abfb933..9f5136ff7 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 ef9c3c600..05141aedf 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 109/156] 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 e25628730..b4264567a 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 2ae785dde..057292f77 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 2f168da1dc753dfec7ddbf4ad07a837284ffc79c Mon Sep 17 00:00:00 2001 From: Vincent Woo Date: Tue, 5 Sep 2017 19:14:11 -0700 Subject: [PATCH 110/156] Delete unused timer variables --- src/addons/attach/attach.js | 2 -- src/addons/terminado/terminado.js | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/addons/attach/attach.js b/src/addons/attach/attach.js index 34dc4f681..81b704f00 100644 --- a/src/addons/attach/attach.js +++ b/src/addons/attach/attach.js @@ -44,8 +44,6 @@ term._flushBuffer = function () { term.write(term._attachSocketBuffer); term._attachSocketBuffer = null; - clearTimeout(term._attachSocketBufferTimer); - term._attachSocketBufferTimer = null; }; term._pushToBuffer = function (data) { diff --git a/src/addons/terminado/terminado.js b/src/addons/terminado/terminado.js index d1414c864..8b9f1cc0b 100644 --- a/src/addons/terminado/terminado.js +++ b/src/addons/terminado/terminado.js @@ -45,8 +45,6 @@ term._flushBuffer = function () { term.write(term._attachSocketBuffer); term._attachSocketBuffer = null; - clearTimeout(term._attachSocketBufferTimer); - term._attachSocketBufferTimer = null; }; term._pushToBuffer = function (data) { From b69ff5d31aa7b68039f8338abf708981fbc08134 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 6 Sep 2017 10:22:33 -0700 Subject: [PATCH 111/156] 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 15c6335a4..d42978f73 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 04e24e19b..e6b09843d 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 a6d679a2d..c032bc8f8 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 ed0a19fbe..652c3aa33 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 112/156] 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 7bb7e9c5a..0e657ae77 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 b4264567a..434b263ba 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 113/156] 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 f05a8cef1..f4fb0a2a0 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 114/156] 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 a0d75a54c..790bf4191 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 e04ad767d..2adb6f63b 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 1ced3d73a..1af21e788 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 15dcbae9b..8756e5401 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 37a6647d3..7789082e6 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 d98f6c5d3..b219732a3 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 115/156] 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 21ab9b893..f32918dfa 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 4eb356f6bef63bb87d3c57e76ee6776493308095 Mon Sep 17 00:00:00 2001 From: Thomas Zilz Date: Thu, 7 Sep 2017 17:13:15 +0200 Subject: [PATCH 116/156] Fix CSI Ps M to delete the correct line --- src/InputHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index abfdfa0e6..3bc87d961 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -467,7 +467,7 @@ export class InputHandler implements IInputHandler { while (param--) { // test: echo -e '\e[44m\e[1M\e[0m' // blankLine(true) - xterm/linux behavior - this._terminal.buffer.lines.splice(row - 1, 1); + this._terminal.buffer.lines.splice(row, 1); this._terminal.buffer.lines.splice(j, 0, this._terminal.blankLine(true)); } From ea8dbbd29fd9d61ffa0e4adf510363773daafdc9 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 7 Sep 2017 10:20:48 -0700 Subject: [PATCH 117/156] 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 790bf4191..b77796888 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 2adb6f63b..55d8d68d9 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 d42978f73..1a180cc15 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 1af21e788..06d495026 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 8756e5401..19125d080 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 7789082e6..1a6cedada 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 0e657ae77..4efc54812 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 9f5136ff7..a888d54fa 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 05141aedf..bef57a007 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 000000000..b948b70dc --- /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 c032bc8f8..ebdd0bdbc 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 652c3aa33..e229fc878 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 df3aff590..c26d459d7 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 b219732a3..7a4d6940a 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 e3051ea1dc82717df1f31dee2d0bc8dabcb29d3f Mon Sep 17 00:00:00 2001 From: Marc Dumais Date: Fri, 8 Sep 2017 11:20:01 -0400 Subject: [PATCH 118/156] Add Theia as a "Real World" use of xterm.js --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index edfa1169c..41a38bcc5 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,8 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**SSHy**](https://github.com/stuicey/SSHy): HTML5 Based SSHv2 Web Client with E2E encryption utilising `xterm.js`, SJCL & websockets. - [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages. +- [**Theia**](https://github.com/theia-ide/theia): Theia is a cloud & desktop IDE framework implemented in TypeScript. + Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it in our list. From 75b91c618e880845d984fba1eb1e492ac38596d1 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 8 Sep 2017 07:50:43 -0700 Subject: [PATCH 119/156] 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 6baa230dd..210d29718 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 65b7d58e5..2d23980dc 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 320241ca5..b9c1824db 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 64b364e92..e31d2278f 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 c2c92e8d4..0ee91d326 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 7d78a259b..09a59f729 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 345041d66..cec8fb373 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 9542658fa..cb97c3680 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 d61f994ab..c1f0a0aba 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 a1768d2d4..414eac895 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 f6e07f7ef..a8b2ff126 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 3bf16a1c3..88a86a441 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 b77796888..d022dc3eb 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 f165590f9..17793fca5 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 55d8d68d9..d3839cb5b 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 b047f91f7..b6493fea5 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 8c90693e9..e5bdc117c 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 4fe09c8c0..29f12169b 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 6c750ed24..e195d6bde 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 8c599fd7a..1fbf40fe8 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 124e89cb8..00db4dd68 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 7a44ee81e..fd173831f 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 1a180cc15..e19887cbc 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 06d495026..a0c03fdd7 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 6d23e19a1..6ce798d32 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 2de698a0e..1f693cfeb 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 34dc4f681..834ccd3c5 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 60214dad9..59f0fd52e 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 344669f6d..1474d2db6 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 26e4e8a7d..1747e747e 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 34223c4ea..69c5737b5 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 d1414c864..54af76337 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 bd2cea036..6901b7300 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 ac9a7b871..a97353520 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 19125d080..e3ed4eef2 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 1a6cedada..d1bc8a758 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 292847c2d..0c09727db 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 4efc54812..cc02bcea9 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 434b263ba..bf080cce5 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 6bd9c9777..e00f6b6d3 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 057292f77..b9afd0e1a 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 a888d54fa..b136a13a1 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 bef57a007..883be5d94 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 37e55ee14..c4b1c220e 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 3b3ae8dfa..dd188c626 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 e6b09843d..05f98300d 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 b948b70dc..9b2cabb32 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 ebdd0bdbc..131a8b784 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 91e62a0c6..39569c9be 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 09e74439f..b1a930f9d 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 68148aeb6..48c0c3748 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 02a8dae9e..5d2358f4b 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 37d6697aa..e8fcff862 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 f404b6f28..6bb35113a 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 e188fb5d8..97a32b790 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 980723260..823736178 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 15b68ef2d..4163263be 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 3bbad855a..aef76d5ba 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 6d20c6712..03f36f426 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 e229fc878..eb2f25017 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 c26d459d7..399f56ceb 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 50ac55928..6df3a4c06 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 120/156] 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 df79d2d04..928ca2389 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 17793fca5..df9e459cd 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 d3839cb5b..947954295 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 }); From 0e3518409e8d2ec20b2e3fbc24bef8ebb4b34990 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 9 Sep 2017 19:06:30 -0700 Subject: [PATCH 121/156] Fix issues with link state - Scrolling will now clear the link renderer - Refreshing a single line will now recalculate only that line Fixes #959 Fixes #960 --- src/Linkifier.ts | 31 +++++++++++++++++++++++-------- src/Terminal.ts | 8 ++++++-- src/input/Interfaces.ts | 2 +- src/input/MouseZoneManager.ts | 25 ++++++++++++++++++++++--- 4 files changed, 52 insertions(+), 14 deletions(-) diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 947954295..fa47271c2 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -49,11 +49,16 @@ export class Linkifier extends EventEmitter implements ILinkifier { private _mouseZoneManager: IMouseZoneManager; private _rowsTimeoutId: number; private _nextLinkMatcherId = HYPERTEXT_LINK_MATCHER_ID; + private _rowsToLinkify: {start: number, end: number}; constructor( protected _terminal: IBufferAccessor & IElementAccessor ) { super(); + this._rowsToLinkify = { + start: null, + end: null + }; this.registerLinkMatcher(strictUrlRegex, null, { matchIndex: 1 }); } @@ -76,25 +81,35 @@ export class Linkifier extends EventEmitter implements ILinkifier { return; } - // Clear out any existing links - this._mouseZoneManager.clearAll(); + // Increase range to linkify + if (!this._rowsToLinkify.start) { + this._rowsToLinkify.start = start; + this._rowsToLinkify.end = end; + } else { + this._rowsToLinkify.start = this._rowsToLinkify.start < start ? this._rowsToLinkify.start : start; + this._rowsToLinkify.end = this._rowsToLinkify.end < end ? this._rowsToLinkify.end : end; + } + // Clear out any existing links on this row range + this._mouseZoneManager.clearAll(start, end); + + // Restart timer if (this._rowsTimeoutId) { clearTimeout(this._rowsTimeoutId); } - this._rowsTimeoutId = setTimeout(this._linkifyRows.bind(this, start, end), Linkifier.TIME_BEFORE_LINKIFY); + this._rowsTimeoutId = setTimeout(() => this._linkifyRows(), Linkifier.TIME_BEFORE_LINKIFY); } /** - * Linkifies - * @param start The row to start at. - * @param end The row to end at. + * Linkifies the rows requested. */ - private _linkifyRows(start: number, end: number): void { + private _linkifyRows(): void { this._rowsTimeoutId = null; - for (let i = start; i <= end; i++) { + for (let i = this._rowsToLinkify.start; i <= this._rowsToLinkify.end; i++) { this._linkifyRow(i); } + this._rowsToLinkify.start = null; + this._rowsToLinkify.end = null; } /** diff --git a/src/Terminal.ts b/src/Terminal.ts index e19887cbc..817eed409 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -48,8 +48,11 @@ import { MouseZoneManager } from './input/MouseZoneManager'; import { initialize as initializeCharAtlas } from './renderer/CharAtlas'; import { IRenderer } from './renderer/Interfaces'; -// Declare for RequireJS in loadAddon +// Declares required for loadAddon +declare var exports: any; +declare var module: any; declare var define: any; +declare var require: any; // Let it work inside Node.js for automated testing purposes. const document = (typeof window !== 'undefined') ? window.document : null; @@ -588,6 +591,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.syncBellSound(); this._mouseZoneManager = new MouseZoneManager(this); + this.on('scroll', () => this._mouseZoneManager.clearAll()); this.linkifier.attachToDom(this._mouseZoneManager); // Create the container that will hold helpers like the textarea for @@ -1034,7 +1038,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT */ private queueLinkification(start: number, end: number): void { if (this.linkifier) { - this.linkifier.linkifyRows(0, this.rows); + this.linkifier.linkifyRows(start, end); } } diff --git a/src/input/Interfaces.ts b/src/input/Interfaces.ts index e3ed4eef2..21514a53c 100644 --- a/src/input/Interfaces.ts +++ b/src/input/Interfaces.ts @@ -5,7 +5,7 @@ export interface IMouseZoneManager { add(zone: IMouseZone): void; - clearAll(): void; + clearAll(start?: number, end?: number): void; } export interface IMouseZone { diff --git a/src/input/MouseZoneManager.ts b/src/input/MouseZoneManager.ts index d1bc8a758..f3ba90c1d 100644 --- a/src/input/MouseZoneManager.ts +++ b/src/input/MouseZoneManager.ts @@ -44,9 +44,28 @@ export class MouseZoneManager implements IMouseZoneManager { } } - public clearAll(): void { - this._zones.length = 0; - this._deactivate(); + public clearAll(start?: number, end?: number): void { + // Exit if there's nothing to clear + if (this._zones.length === 0) { + return; + } + + // Iterate through zones and clear them out if they're within the range + for (let i = 0; i < this._zones.length; i++) { + const zone = this._zones[i]; + if (zone.y >= start && zone.y <= end) { + if (this._currentZone && this._currentZone === zone) { + this._currentZone.leaveCallback(); + this._currentZone = null; + } + this._zones.splice(i--, 1); + } + } + + // Deactivate the mouse zone manager if all the zones have been removed + if (this._zones.length === 0) { + this._deactivate(); + } } private _activate(): void { From 6c06d83525908d12d0c0ba245cfbfba0115f1868 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 10 Sep 2017 08:01:16 -0700 Subject: [PATCH 122/156] Support theming cursor accent color Fixes #963 --- src/Interfaces.ts | 1 + src/renderer/CharAtlas.ts | 1 + src/renderer/ColorManager.ts | 3 +++ src/renderer/CursorRenderLayer.ts | 2 +- src/renderer/Interfaces.ts | 1 + typings/xterm.d.ts | 2 ++ 6 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Interfaces.ts b/src/Interfaces.ts index d022dc3eb..f6c200187 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -323,6 +323,7 @@ export interface ITheme { foreground?: string; background?: string; cursor?: string; + cursorAccent?: string; selection?: string; black?: string; red?: string; diff --git a/src/renderer/CharAtlas.ts b/src/renderer/CharAtlas.ts index bf080cce5..45562a256 100644 --- a/src/renderer/CharAtlas.ts +++ b/src/renderer/CharAtlas.ts @@ -77,6 +77,7 @@ function generateConfig(scaledCharWidth: number, scaledCharHeight: number, termi foreground: colors.foreground, background: null, cursor: null, + cursorAccent: null, selection: null, ansi: colors.ansi.slice(0, 16) }; diff --git a/src/renderer/ColorManager.ts b/src/renderer/ColorManager.ts index b9afd0e1a..afd5c243f 100644 --- a/src/renderer/ColorManager.ts +++ b/src/renderer/ColorManager.ts @@ -9,6 +9,7 @@ import { ITheme } from '../Interfaces'; const DEFAULT_FOREGROUND = '#ffffff'; const DEFAULT_BACKGROUND = '#000000'; const DEFAULT_CURSOR = '#ffffff'; +const DEFAULT_CURSOR_ACCENT = '#000000'; const DEFAULT_SELECTION = 'rgba(255, 255, 255, 0.3)'; export const DEFAULT_ANSI_COLORS = [ // dark: @@ -72,6 +73,7 @@ export class ColorManager { foreground: DEFAULT_FOREGROUND, background: DEFAULT_BACKGROUND, cursor: DEFAULT_CURSOR, + cursorAccent: DEFAULT_CURSOR_ACCENT, selection: DEFAULT_SELECTION, ansi: generate256Colors(DEFAULT_ANSI_COLORS) }; @@ -86,6 +88,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.cursorAccent = theme.cursorAccent || DEFAULT_CURSOR_ACCENT; 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]; diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index b136a13a1..bc6fbcb13 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -199,7 +199,7 @@ 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._ctx.fillStyle = this.colors.cursorAccent; this.fillCharTrueColor(terminal, charData, x, y); this._ctx.restore(); } diff --git a/src/renderer/Interfaces.ts b/src/renderer/Interfaces.ts index 05f98300d..4b0028b8a 100644 --- a/src/renderer/Interfaces.ts +++ b/src/renderer/Interfaces.ts @@ -72,6 +72,7 @@ export interface IColorSet { foreground: string; background: string; cursor: string; + cursorAccent: string; selection: string; ansi: string[]; } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 7a4d6940a..de2a261b3 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -88,6 +88,8 @@ interface ITheme { background?: string, /** The cursor color */ cursor?: string, + /** The accent color of the cursor (used as the foreground color for a block cursor) */ + cursorAccent?: string, /** The selection color (can be transparent) */ selection?: string, /** ANSI black (eg. `\x1b[30m`) */ From ae741348a635839ec04f7c65fc02594804f6d0c4 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 10 Sep 2017 10:31:17 -0700 Subject: [PATCH 123/156] Fix several issues with line height Fixes #966 Fixes #967 --- src/Interfaces.ts | 3 +- src/Terminal.ts | 9 +++-- src/Viewport.ts | 21 +++++----- src/addons/fit/fit.js | 2 +- src/renderer/BackgroundRenderLayer.ts | 6 +-- src/renderer/BaseRenderLayer.ts | 39 +++++------------- src/renderer/CursorRenderLayer.ts | 6 +-- src/renderer/ForegroundRenderLayer.ts | 6 +-- src/renderer/Interfaces.ts | 19 +++++++-- src/renderer/LinkRenderLayer.ts | 6 +-- src/renderer/Renderer.ts | 57 ++++++++++++++++++++++++--- src/renderer/SelectionRenderLayer.ts | 6 +-- src/utils/TestUtils.test.ts | 13 +++++- 13 files changed, 125 insertions(+), 68 deletions(-) diff --git a/src/Interfaces.ts b/src/Interfaces.ts index d022dc3eb..97fa3fd89 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -5,7 +5,7 @@ import { ILinkMatcherOptions } from './Interfaces'; import { LinkMatcherHandler, LinkMatcherValidationCallback, Charset, LineData } from './Types'; -import { IColorSet } from './renderer/Interfaces'; +import { IColorSet, IRenderer } from './renderer/Interfaces'; import { IMouseZoneManager } from './input/Interfaces'; export interface IBrowser { @@ -36,6 +36,7 @@ export interface ITerminal extends ILinkifierAccessor, IBufferAccessor, IElement selectionManager: ISelectionManager; charMeasure: ICharMeasure; textarea: HTMLTextAreaElement; + renderer: IRenderer; rows: number; cols: number; browser: IBrowser; diff --git a/src/Terminal.ts b/src/Terminal.ts index e19887cbc..67026ab06 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -188,7 +188,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT private inputHandler: InputHandler; private parser: Parser; - private renderer: IRenderer; + public renderer: IRenderer; public selectionManager: SelectionManager; public linkifier: ILinkifier; public buffers: BufferSet; @@ -619,7 +619,6 @@ 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, false)); @@ -627,6 +626,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.on('focus', () => this.renderer.onFocus()); window.addEventListener('resize', () => this.renderer.onWindowResize(window.devicePixelRatio)); this.charMeasure.on('charsizechanged', () => this.renderer.onResize(this.cols, this.rows, true)); + this.renderer.on('resize', (dimensions) => this.viewport.syncScrollArea()); this.selectionManager = new SelectionManager(this, this.buffer, this.charMeasure); this.element.addEventListener('mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e)); @@ -639,7 +639,10 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.textarea.focus(); this.textarea.select(); }); - this.on('scroll', () => this.selectionManager.refresh()); + this.on('scroll', () => { + this.viewport.syncScrollArea(); + this.selectionManager.refresh(); + }); this.viewportElement.addEventListener('scroll', () => this.selectionManager.refresh()); // Measure the character size diff --git a/src/Viewport.ts b/src/Viewport.ts index 1f693cfeb..b7622ba75 100644 --- a/src/Viewport.ts +++ b/src/Viewport.ts @@ -34,8 +34,6 @@ export class Viewport implements IViewport { this.lastRecordedBufferLength = 0; this.lastRecordedViewportHeight = 0; - this.terminal.on('scroll', this.syncScrollArea.bind(this)); - this.terminal.on('resize', this.syncScrollArea.bind(this)); this.viewportElement.addEventListener('scroll', this.onScroll.bind(this)); // Perform this async to ensure the CharMeasure is ready. @@ -52,18 +50,20 @@ export class Viewport implements IViewport { */ private refresh(): void { if (this.charMeasure.height > 0) { - const lineHeight = Math.ceil(this.charMeasure.height * this.terminal.options.lineHeight); + const lineHeight = (this.terminal).renderer.dimensions.scaledLineHeight / window.devicePixelRatio; const rowHeightChanged = lineHeight !== this.currentRowHeight; + // TODO: Do we need lineHeight anymore?? if (rowHeightChanged) { this.currentRowHeight = lineHeight; this.viewportElement.style.lineHeight = lineHeight + 'px'; } - const viewportHeightChanged = this.lastRecordedViewportHeight !== this.terminal.rows; - if (rowHeightChanged || viewportHeightChanged) { - this.lastRecordedViewportHeight = this.terminal.rows; - this.viewportElement.style.height = lineHeight * this.terminal.rows + 'px'; + // const viewportHeightChanged = this.lastRecordedViewportHeight !== this.terminal.rows; + const viewportHeightChanged = this.lastRecordedViewportHeight !== this.terminal.renderer.dimensions.canvasHeight; + if (viewportHeightChanged) { + this.lastRecordedViewportHeight = this.terminal.renderer.dimensions.canvasHeight; + this.viewportElement.style.height = this.lastRecordedViewportHeight + 'px'; } - this.scrollArea.style.height = (lineHeight * this.lastRecordedBufferLength) + 'px'; + this.scrollArea.style.height = Math.round(lineHeight * this.lastRecordedBufferLength) + 'px'; } } @@ -75,12 +75,13 @@ export class Viewport implements IViewport { // If buffer height changed this.lastRecordedBufferLength = this.terminal.buffer.lines.length; this.refresh(); - } else if (this.lastRecordedViewportHeight !== this.terminal.rows) { + } else if (this.lastRecordedViewportHeight !== (this.terminal).renderer.dimensions.canvasHeight) { // If viewport height changed this.refresh(); } else { // If size has changed, refresh viewport - if (Math.ceil(this.charMeasure.height * this.terminal.options.lineHeight) !== this.currentRowHeight) { + console.log(this.terminal.renderer.dimensions.scaledLineHeight / window.devicePixelRatio); + if (this.terminal.renderer.dimensions.scaledLineHeight / window.devicePixelRatio !== this.currentRowHeight) { this.refresh(); } } diff --git a/src/addons/fit/fit.js b/src/addons/fit/fit.js index 59f0fd52e..3e61a7efc 100644 --- a/src/addons/fit/fit.js +++ b/src/addons/fit/fit.js @@ -47,7 +47,7 @@ var availableWidth = parentElementWidth - elementPaddingHor; var geometry = { cols: Math.floor(availableWidth / term.charMeasure.width), - rows: Math.floor(availableHeight / Math.ceil(term.charMeasure.height * term.getOption('lineHeight'))) + rows: Math.floor(availableHeight / Math.floor(term.charMeasure.height * term.getOption('lineHeight'))) }; return geometry; diff --git a/src/renderer/BackgroundRenderLayer.ts b/src/renderer/BackgroundRenderLayer.ts index 0c09727db..279aee6db 100644 --- a/src/renderer/BackgroundRenderLayer.ts +++ b/src/renderer/BackgroundRenderLayer.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IColorSet } from './Interfaces'; +import { IColorSet, IRenderDimensions } from './Interfaces'; import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; import { GridCache } from './GridCache'; @@ -18,8 +18,8 @@ export class BackgroundRenderLayer extends BaseRenderLayer { this._state = new GridCache(); } - public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void { - super.resize(terminal, canvasWidth, canvasHeight, charSizeChanged); + public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void { + super.resize(terminal, dim, 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/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index cc02bcea9..38457e13a 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IRenderLayer, IColorSet } from './Interfaces'; +import { IRenderLayer, IColorSet, IRenderDimensions } from './Interfaces'; import { ITerminal, ITerminalOptions } from '../Interfaces'; import { acquireCharAtlas, CHAR_ATLAS_CELL_SPACING } from './CharAtlas'; import { CharData } from '../Types'; @@ -61,34 +61,15 @@ export abstract class BaseRenderLayer implements IRenderLayer { } } - public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void { - // 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); - - // 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`; + public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void { + this.scaledCharWidth = dim.scaledCharWidth; + this.scaledCharHeight = dim.scaledCharHeight; + this.scaledLineHeight = dim.scaledLineHeight; + this.scaledLineDrawY = dim.scaledLineDrawY; + this._canvas.width = dim.scaledCanvasWidth; + this._canvas.height = dim.scaledCanvasHeight; + this._canvas.style.width = `${dim.canvasWidth}px`; + this._canvas.style.height = `${dim.canvasHeight}px`; if (charSizeChanged) { this._refreshCharAtlas(terminal, this.colors); diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index b136a13a1..8e25d7c16 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IColorSet } from './Interfaces'; +import { IColorSet, IRenderDimensions } from './Interfaces'; import { IBuffer, ICharMeasure, ITerminal, ITerminalOptions } from '../Interfaces'; import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; import { GridCache } from './GridCache'; @@ -47,8 +47,8 @@ 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); + public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void { + super.resize(terminal, dim, charSizeChanged); // Resizing the canvas discards the contents of the canvas so clear state this._state = { x: null, diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index 883be5d94..8e9e23fc3 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IColorSet } from './Interfaces'; +import { IColorSet, IRenderDimensions } 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'; @@ -26,8 +26,8 @@ export class ForegroundRenderLayer extends BaseRenderLayer { this._state = new GridCache(); } - public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void { - super.resize(terminal, canvasWidth, canvasHeight, charSizeChanged); + public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void { + super.resize(terminal, dim, 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/Interfaces.ts b/src/renderer/Interfaces.ts index 05f98300d..890b01f5a 100644 --- a/src/renderer/Interfaces.ts +++ b/src/renderer/Interfaces.ts @@ -3,9 +3,11 @@ * @license MIT */ -import { ITerminal, ITerminalOptions, ITheme } from '../Interfaces'; +import { ITerminal, ITerminalOptions, ITheme, IEventEmitter } from '../Interfaces'; + +export interface IRenderer extends IEventEmitter { + dimensions: IRenderDimensions; -export interface IRenderer { setTheme(theme: ITheme): IColorSet; onWindowResize(devicePixelRatio: number): void; onResize(cols: number, rows: number, didCharSizeChange: boolean): void; @@ -59,7 +61,7 @@ export interface IRenderLayer { /** * Resize the render layer. */ - resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void; + resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void; /** * Clear the state of the render layer. @@ -75,3 +77,14 @@ export interface IColorSet { selection: string; ansi: string[]; } + +export interface IRenderDimensions { + scaledCharWidth: number; + scaledCharHeight: number; + scaledLineHeight: number; + scaledLineDrawY: number; + scaledCanvasWidth: number; + scaledCanvasHeight: number; + canvasWidth: number; + canvasHeight: number; +} diff --git a/src/renderer/LinkRenderLayer.ts b/src/renderer/LinkRenderLayer.ts index 9b2cabb32..ad1789334 100644 --- a/src/renderer/LinkRenderLayer.ts +++ b/src/renderer/LinkRenderLayer.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IColorSet } from './Interfaces'; +import { IColorSet, IRenderDimensions } from './Interfaces'; import { IBuffer, ICharMeasure, ITerminal, ILinkifierAccessor } from '../Interfaces'; import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; import { GridCache } from './GridCache'; @@ -20,8 +20,8 @@ export class LinkRenderLayer extends BaseRenderLayer { 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); + public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void { + super.resize(terminal, dim, charSizeChanged); // Resizing the canvas discards the contents of the canvas so clear state this._state = null; } diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 131a8b784..b610fd696 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -11,10 +11,11 @@ import { SelectionRenderLayer } from './SelectionRenderLayer'; import { CursorRenderLayer } from './CursorRenderLayer'; import { ColorManager } from './ColorManager'; import { BaseRenderLayer } from './BaseRenderLayer'; -import { IRenderLayer, IColorSet, IRenderer } from './Interfaces'; +import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions } from './Interfaces'; import { LinkRenderLayer } from './LinkRenderLayer'; +import { EventEmitter } from '../EventEmitter'; -export class Renderer implements IRenderer { +export class Renderer extends EventEmitter implements IRenderer { /** A queue of the rows to be refreshed */ private _refreshRowsQueue: {start: number, end: number}[] = []; private _refreshAnimationFrame = null; @@ -23,8 +24,10 @@ export class Renderer implements IRenderer { private _devicePixelRatio: number; private _colorManager: ColorManager; + public dimensions: IRenderDimensions; constructor(private _terminal: ITerminal) { + super(); this._colorManager = new ColorManager(); this._renderLayers = [ new BackgroundRenderLayer(this._terminal.element, 0, this._colorManager.colors), @@ -33,6 +36,16 @@ export class Renderer implements IRenderer { new LinkRenderLayer(this._terminal.element, 3, this._colorManager.colors, this._terminal), new CursorRenderLayer(this._terminal.element, 4, this._colorManager.colors) ]; + this.dimensions = { + scaledCharWidth: null, + scaledCharHeight: null, + scaledLineHeight: null, + scaledLineDrawY: null, + scaledCanvasWidth: null, + scaledCanvasHeight: null, + canvasWidth: null, + canvasHeight: null + }; this._devicePixelRatio = window.devicePixelRatio; } @@ -63,12 +76,46 @@ export class Renderer implements IRenderer { if (!this._terminal.charMeasure.width || !this._terminal.charMeasure.height) { return; } - const width = this._terminal.charMeasure.width * cols; - const height = Math.floor(this._terminal.charMeasure.height * this._terminal.options.lineHeight) * rows; + + // 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.dimensions.scaledCharWidth = Math.ceil(this._terminal.charMeasure.width * window.devicePixelRatio); + this.dimensions.scaledCharHeight = Math.ceil(this._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.dimensions.scaledLineHeight = Math.floor(this.dimensions.scaledCharHeight * this._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.dimensions.scaledLineDrawY = this._terminal.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledLineHeight - this.dimensions.scaledCharHeight) / 2); + + // Recalculate the canvas dimensions; scaled* define the actual number of + // pixel in the canvas + this.dimensions.scaledCanvasHeight = this._terminal.rows * this.dimensions.scaledLineHeight; + this.dimensions.scaledCanvasWidth = this._terminal.cols * this.dimensions.scaledCharWidth; + + // The 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.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / window.devicePixelRatio); + this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / window.devicePixelRatio); + // Resize all render layers - this._renderLayers.forEach(l => l.resize(this._terminal, width, height, didCharSizeChange)); + this._renderLayers.forEach(l => l.resize(this._terminal, this.dimensions, didCharSizeChange)); + // Force a refresh this._terminal.refresh(0, this._terminal.rows - 1); + + this.emit('resize', { + width: this.dimensions.canvasWidth, + height: this.dimensions.canvasHeight + }); } public onCharSizeChanged(): void { diff --git a/src/renderer/SelectionRenderLayer.ts b/src/renderer/SelectionRenderLayer.ts index 39569c9be..5339c7a40 100644 --- a/src/renderer/SelectionRenderLayer.ts +++ b/src/renderer/SelectionRenderLayer.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IColorSet } from './Interfaces'; +import { IColorSet, IRenderDimensions } from './Interfaces'; import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; import { GridCache } from './GridCache'; @@ -21,8 +21,8 @@ export class SelectionRenderLayer extends BaseRenderLayer { }; } - public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void { - super.resize(terminal, canvasWidth, canvasHeight, charSizeChanged); + public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void { + super.resize(terminal, dim, charSizeChanged); // Resizing the canvas discards the contents of the canvas so clear state this._state = { start: null, diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index eb2f25017..45cf169ac 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -7,9 +7,10 @@ import { ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManag import { LineData } from '../Types'; import { Buffer } from '../Buffer'; import * as Browser from './Browser'; -import { IColorSet, IRenderer } from '../renderer/Interfaces'; +import { IColorSet, IRenderer, IRenderDimensions } from '../renderer/Interfaces'; export class MockTerminal implements ITerminal { + renderer: IRenderer; linkifier: ILinkifier; isFocused: boolean; options: ITerminalOptions = {}; @@ -215,6 +216,16 @@ export class MockBuffer implements IBuffer { } export class MockRenderer implements IRenderer { + on(type: string, listener: IListenerType): void { + throw new Error('Method not implemented.'); + } + off(type: string, listener: IListenerType): void { + throw new Error('Method not implemented.'); + } + emit(type: string, data?: any): void { + throw new Error('Method not implemented.'); + } + dimensions: IRenderDimensions; setTheme(theme: ITheme): IColorSet { return {}; } onResize(cols: number, rows: number, didCharSizeChange: boolean): void {} onCharSizeChanged(): void {} From ac23be134b1b5926aee87654e92493cb691cd375 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 10 Sep 2017 10:38:08 -0700 Subject: [PATCH 124/156] Simplify viewport --- src/Viewport.ts | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/src/Viewport.ts b/src/Viewport.ts index b7622ba75..064076a23 100644 --- a/src/Viewport.ts +++ b/src/Viewport.ts @@ -12,9 +12,10 @@ import { IColorSet } from './renderer/Interfaces'; * Logic for the virtual scroll bar is included in this object. */ export class Viewport implements IViewport { - private currentRowHeight: number; - private lastRecordedBufferLength: number; - private lastRecordedViewportHeight: number; + private currentRowHeight: number = 0; + private lastRecordedBufferLength: number = 0; + private lastRecordedViewportHeight: number = 0; + private lastRecordedBufferHeight: number = 0; private lastTouchY: number; /** @@ -30,10 +31,6 @@ export class Viewport implements IViewport { private scrollArea: HTMLElement, private charMeasure: CharMeasure ) { - this.currentRowHeight = 0; - this.lastRecordedBufferLength = 0; - this.lastRecordedViewportHeight = 0; - this.viewportElement.addEventListener('scroll', this.onScroll.bind(this)); // Perform this async to ensure the CharMeasure is ready. @@ -50,20 +47,18 @@ export class Viewport implements IViewport { */ private refresh(): void { if (this.charMeasure.height > 0) { - const lineHeight = (this.terminal).renderer.dimensions.scaledLineHeight / window.devicePixelRatio; - const rowHeightChanged = lineHeight !== this.currentRowHeight; - // TODO: Do we need lineHeight anymore?? - if (rowHeightChanged) { - this.currentRowHeight = lineHeight; - this.viewportElement.style.lineHeight = lineHeight + 'px'; - } - // const viewportHeightChanged = this.lastRecordedViewportHeight !== this.terminal.rows; - const viewportHeightChanged = this.lastRecordedViewportHeight !== this.terminal.renderer.dimensions.canvasHeight; - if (viewportHeightChanged) { + this.currentRowHeight = this.terminal.renderer.dimensions.scaledLineHeight / window.devicePixelRatio; + + if (this.lastRecordedViewportHeight !== this.terminal.renderer.dimensions.canvasHeight) { this.lastRecordedViewportHeight = this.terminal.renderer.dimensions.canvasHeight; this.viewportElement.style.height = this.lastRecordedViewportHeight + 'px'; } - this.scrollArea.style.height = Math.round(lineHeight * this.lastRecordedBufferLength) + 'px'; + + const newBufferHeight = Math.round(this.currentRowHeight * this.lastRecordedBufferLength); + if (this.lastRecordedBufferHeight !== newBufferHeight) { + this.lastRecordedBufferHeight = newBufferHeight; + this.scrollArea.style.height = this.lastRecordedBufferHeight + 'px'; + } } } @@ -80,7 +75,6 @@ export class Viewport implements IViewport { this.refresh(); } else { // If size has changed, refresh viewport - console.log(this.terminal.renderer.dimensions.scaledLineHeight / window.devicePixelRatio); if (this.terminal.renderer.dimensions.scaledLineHeight / window.devicePixelRatio !== this.currentRowHeight) { this.refresh(); } From 903ed1917e38c52c372d6b340d6a65ac0dcbef78 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 10 Sep 2017 10:43:07 -0700 Subject: [PATCH 125/156] Remove viewport test It's not tightly linked to renderer and depends on devicePixelRatio. --- src/Viewport.test.ts | 90 -------------------------------------------- 1 file changed, 90 deletions(-) delete mode 100644 src/Viewport.test.ts diff --git a/src/Viewport.test.ts b/src/Viewport.test.ts deleted file mode 100644 index 6ce798d32..000000000 --- a/src/Viewport.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -/** - * Copyright (c) 2016 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { assert } from 'chai'; -import { Viewport } from './Viewport'; -import { BufferSet } from './BufferSet'; - -describe('Viewport', () => { - let terminal; - let viewportElement; - let charMeasure; - let viewport; - let scrollAreaElement; - - const CHARACTER_HEIGHT = 10; - - beforeEach(() => { - terminal = { - rows: 0, - ydisp: 0, - on: () => {}, - rowContainer: { - style: { - lineHeight: 0 - } - }, - selectionContainer: { - style: { - height: 0 - } - }, - options: { - scrollback: 10, - lineHeight: 1 - } - }; - terminal.buffers = new BufferSet(terminal); - terminal.buffer = terminal.buffers.active; - viewportElement = { - addEventListener: () => {}, - style: { - height: 0, - lineHeight: 0 - } - }; - scrollAreaElement = { - style: { - height: 0 - } - }; - charMeasure = { - height: CHARACTER_HEIGHT - }; - viewport = new Viewport(terminal, viewportElement, scrollAreaElement, charMeasure); - }); - - describe('refresh', () => { - it('should set the height of the viewport when the line-height changed', () => { - terminal.buffer.lines.push(''); - terminal.buffer.lines.push(''); - terminal.rows = 1; - viewport.refresh(); - assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px'); - charMeasure.height = 2 * CHARACTER_HEIGHT; - viewport.refresh(); - assert.equal(viewportElement.style.height, 2 * CHARACTER_HEIGHT + 'px'); - }); - }); - - describe('syncScrollArea', () => { - it('should sync the scroll area', done => { - // Allow CharMeasure to be initialized - setTimeout(() => { - terminal.buffer.lines.push(''); - terminal.rows = 1; - assert.equal(scrollAreaElement.style.height, 0 * CHARACTER_HEIGHT + 'px'); - viewport.syncScrollArea(); - assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px'); - assert.equal(scrollAreaElement.style.height, 1 * CHARACTER_HEIGHT + 'px'); - terminal.buffer.lines.push(''); - viewport.syncScrollArea(); - assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px'); - assert.equal(scrollAreaElement.style.height, 2 * CHARACTER_HEIGHT + 'px'); - done(); - }, 0); - }); - }); -}); From 2114bc4837b6cf7dd8b362e5791fc4c0ed762008 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 11 Sep 2017 11:12:51 -0700 Subject: [PATCH 126/156] Ensure leave is called when leaving a MouseZone Fixes #975 --- src/input/MouseZoneManager.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/input/MouseZoneManager.ts b/src/input/MouseZoneManager.ts index d1bc8a758..d84b7218d 100644 --- a/src/input/MouseZoneManager.ts +++ b/src/input/MouseZoneManager.ts @@ -83,10 +83,14 @@ export class MouseZoneManager implements IMouseZoneManager { return; } - // Fire the hover end callback if a zone was being hovered + // Fire the hover end callback and cancel any existing timer if a new zone + // is being hovered if (this._currentZone) { this._currentZone.leaveCallback(); this._currentZone = null; + if (this._tooltipTimeout) { + clearTimeout(this._tooltipTimeout); + } } // Exit if there is not zone @@ -100,14 +104,12 @@ export class MouseZoneManager implements IMouseZoneManager { zone.hoverCallback(e); } - // Restart the timeout - if (this._tooltipTimeout) { - clearTimeout(this._tooltipTimeout); - } + // Restart the tooltip timeout this._tooltipTimeout = setTimeout(() => this._onTooltip(e), HOVER_DURATION); } private _onTooltip(e: MouseEvent): void { + this._tooltipTimeout = null; const zone = this._findZoneEventAt(e); if (zone && zone.tooltipCallback) { zone.tooltipCallback(e); From 6899e01fc1e5c0b5639faefddbe6f1c63f5f1ede Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Sep 2017 15:05:47 -0700 Subject: [PATCH 127/156] Improve scrolling while user has scrolled up Fixes #953 --- src/Terminal.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index e19887cbc..f4d10dc4b 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1071,7 +1071,16 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // Only adjust ybase and ydisp when the buffer is not trimmed if (!willBufferBeTrimmed) { this.buffer.ybase++; - this.buffer.ydisp++; + // Only scroll the ydisp with ybase if the user has not scrolled up + if (!this.userScrolling) { + this.buffer.ydisp++; + } + } else { + // When the buffer is full and the user has scrolled up, keep the text + // stable unless ydisp is right at the top + if (this.userScrolling) { + this.buffer.ydisp = Math.max(this.buffer.ydisp - 1, 0); + } } } else { // scrollTop is non-zero which means no line will be going to the From 403fd3cd4f205d74fcd51513433d9dbf8ba50921 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Sep 2017 18:55:59 -0700 Subject: [PATCH 128/156] Draw text using decimal numbers Fixes #972 --- src/renderer/BaseRenderLayer.ts | 36 ++++++++++++++++++++++++++------- src/renderer/Renderer.ts | 17 +++++++++++----- src/utils/CharMeasure.ts | 2 +- 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 38457e13a..3bc0a9175 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -78,6 +78,16 @@ export abstract class BaseRenderLayer implements IRenderLayer { public abstract reset(terminal: ITerminal): void; + /** + * Gets the left position of a cell. Since character width is stored as a + * float in order to prevent bad letter spacing, drawing shapes in the cell + * need to be rounded. + * @param x The column of the cell. + */ + private _getCellLeft(x: number): number { + return Math.round(x * this.scaledCharWidth); + } + /** * Fills 1+ cells completely. This uses the existing fillStyle on the context. * @param x The column to start at. @@ -86,7 +96,12 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @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); + const cellLeft = this._getCellLeft(x); + this._ctx.fillRect( + cellLeft, + y * this.scaledLineHeight, + this._getCellLeft(x + width) - cellLeft, + height * this.scaledLineHeight); } /** @@ -96,10 +111,11 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param y The row to fill. */ protected fillBottomLineAtCells(x: number, y: number, width: number = 1): void { + const cellLeft = this._getCellLeft(x); this._ctx.fillRect( - x * this.scaledCharWidth, + cellLeft, (y + 1) * this.scaledLineHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */, - width * this.scaledCharWidth, + this._getCellLeft(x + width) - cellLeft, window.devicePixelRatio); } @@ -111,7 +127,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { */ protected fillLeftLineAtCell(x: number, y: number): void { this._ctx.fillRect( - x * this.scaledCharWidth, + this._getCellLeft(x), y * this.scaledLineHeight, window.devicePixelRatio, this.scaledLineHeight); @@ -124,11 +140,12 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param y The row to fill. */ protected strokeRectAtCell(x: number, y: number, width: number, height: number): void { + const cellLeft = this._getCellLeft(x); this._ctx.lineWidth = window.devicePixelRatio; this._ctx.strokeRect( - x * this.scaledCharWidth + window.devicePixelRatio / 2, + cellLeft + window.devicePixelRatio / 2, y * this.scaledLineHeight + (window.devicePixelRatio / 2), - (width * this.scaledCharWidth) - window.devicePixelRatio, + this._getCellLeft(x + width) - cellLeft - window.devicePixelRatio, (height * this.scaledLineHeight) - window.devicePixelRatio); } @@ -147,7 +164,12 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @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); + const cellLeft = this._getCellLeft(x); + this._ctx.clearRect( + cellLeft, + y * this.scaledLineHeight, + this._getCellLeft(x + width) - cellLeft, + height * this.scaledLineHeight); } /** diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index b610fd696..a2b8a7fec 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -77,10 +77,17 @@ export class Renderer extends EventEmitter implements IRenderer { return; } - // 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.dimensions.scaledCharWidth = Math.ceil(this._terminal.charMeasure.width * window.devicePixelRatio); + // Calculate the scaled character width. Width is kept as a decimal to + // provide better letter spacing, otherwise the text can look odd. + // Characters drawn using this decimal number do have the potential to + // overlap, but only by a single pixel. As such, it's not a big deal when + // they do as that pixel is always cleared as necessary before drawing the + // character. + this.dimensions.scaledCharWidth = this._terminal.charMeasure.width * window.devicePixelRatio; + + // Calculate the scaled character height. Height is ceiled in case + // devicePixelRatio is a floating point number in order to ensure there is + // enough space to draw the character to the cell. this.dimensions.scaledCharHeight = Math.ceil(this._terminal.charMeasure.height * window.devicePixelRatio); // Calculate the scaled line height, if lineHeight is not 1 then the value @@ -96,7 +103,7 @@ export class Renderer extends EventEmitter implements IRenderer { // Recalculate the canvas dimensions; scaled* define the actual number of // pixel in the canvas this.dimensions.scaledCanvasHeight = this._terminal.rows * this.dimensions.scaledLineHeight; - this.dimensions.scaledCanvasWidth = this._terminal.cols * this.dimensions.scaledCharWidth; + this.dimensions.scaledCanvasWidth = Math.round(this._terminal.cols * this.dimensions.scaledCharWidth); // The 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 diff --git a/src/utils/CharMeasure.ts b/src/utils/CharMeasure.ts index e8fcff862..e5b03f871 100644 --- a/src/utils/CharMeasure.ts +++ b/src/utils/CharMeasure.ts @@ -60,7 +60,7 @@ export class CharMeasure extends EventEmitter implements ICharMeasure { return; } if (this._width !== geometry.width || this._height !== geometry.height) { - this._width = Math.ceil(geometry.width); + this._width = geometry.width; this._height = Math.ceil(geometry.height); this.emit('charsizechanged'); } From cf324529175712873522b85ceb5834d5cd4d00d6 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Sep 2017 19:23:26 -0700 Subject: [PATCH 129/156] Treat \u279C arrow as ambiguous width char (like emoji) Fixes #974 --- src/renderer/ForegroundRenderLayer.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index 8e9e23fc3..9334f1093 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -159,6 +159,11 @@ export class ForegroundRenderLayer extends BaseRenderLayer { * @param char The character to search. */ private _isEmoji(char: string): boolean { + // Check special ambiguous width characters + if (char === '➜') { + return true; + } + // Check emoji unicode range return char.search(/([\uD800-\uDBFF][\uDC00-\uDFFF])/g) >= 0; } From 73d316442637c627e3cee4e54f48cdcef296f534 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Sep 2017 20:00:25 -0700 Subject: [PATCH 130/156] Fix alt+selection while mouse events are enabled Fixes #970 --- src/SelectionManager.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 29f12169b..28ed15227 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -454,6 +454,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager * @param event The mousemove event. */ private _onMouseMove(event: MouseEvent): void { + // If the mousemove listener is active it means that a selection is + // currently being made, we should stop propogation to prevent mouse events + // to be sent to the pty. + event.stopImmediatePropagation(); + // Record the previous position so we know whether to redraw the selection // at the end. const previousSelectionEnd = this._model.selectionEnd ? [this._model.selectionEnd[0], this._model.selectionEnd[1]] : null; From 30cac210036278150ecf7d1a8a62e79737bdd73e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 13 Sep 2017 10:34:58 -0700 Subject: [PATCH 131/156] Remove log --- src/CompositionHelper.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/CompositionHelper.ts b/src/CompositionHelper.ts index cec8fb373..ca5b4f4b4 100644 --- a/src/CompositionHelper.ts +++ b/src/CompositionHelper.ts @@ -64,7 +64,6 @@ export class CompositionHelper { * @param {CompositionEvent} ev The event. */ public compositionupdate(ev: CompositionEvent): void { - console.log('compositionupdate'); this.compositionView.textContent = ev.data; this.updateCompositionElements(); setTimeout(() => { From ead45fe4d027eabc01e580006e4751d0b3ded091 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 13 Sep 2017 10:59:55 -0700 Subject: [PATCH 132/156] Add a TODO --- src/renderer/ForegroundRenderLayer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/ForegroundRenderLayer.ts index 9334f1093..19625466e 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/ForegroundRenderLayer.ts @@ -159,6 +159,7 @@ export class ForegroundRenderLayer extends BaseRenderLayer { * @param char The character to search. */ private _isEmoji(char: string): boolean { + // TODO: We need a generic solution for handling characters like this // Check special ambiguous width characters if (char === '➜') { return true; From a52b9feeea7d3ba877b30f1aa1601ed41317d66c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 13 Sep 2017 14:03:40 -0700 Subject: [PATCH 133/156] Ensure mouse coordinates are always within rows/cols Fixes #986 --- src/utils/Mouse.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/Mouse.ts b/src/utils/Mouse.ts index aef76d5ba..7bc39b7fc 100644 --- a/src/utils/Mouse.ts +++ b/src/utils/Mouse.ts @@ -53,8 +53,8 @@ export function getCoords(event: {pageX: number, pageY: number}, element: HTMLEl 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); - coords[1] = Math.min(Math.max(coords[1], 1), rowCount + 1); + coords[0] = Math.min(Math.max(coords[0], 1), colCount); + coords[1] = Math.min(Math.max(coords[1], 1), rowCount); return coords; } From 5538481833fe7c0add2ded86eec757267f2bf8de Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 13 Sep 2017 15:00:36 -0700 Subject: [PATCH 134/156] Draw characters to a grid --- src/renderer/Renderer.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index a2b8a7fec..63782afdf 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -77,13 +77,11 @@ export class Renderer extends EventEmitter implements IRenderer { return; } - // Calculate the scaled character width. Width is kept as a decimal to - // provide better letter spacing, otherwise the text can look odd. - // Characters drawn using this decimal number do have the potential to - // overlap, but only by a single pixel. As such, it's not a big deal when - // they do as that pixel is always cleared as necessary before drawing the - // character. - this.dimensions.scaledCharWidth = this._terminal.charMeasure.width * window.devicePixelRatio; + // Calculate the scaled character width. Width is floored as it must be + // drawn to an integer grid in order for the CharAtlas "stamps" to not be + // blurry. When text is drawn to the grid not using the CharAtlas, it is + // clipped to ensure there is no overlap with the next cell. + this.dimensions.scaledCharWidth = Math.floor(this._terminal.charMeasure.width * window.devicePixelRatio); // Calculate the scaled character height. Height is ceiled in case // devicePixelRatio is a floating point number in order to ensure there is @@ -103,7 +101,7 @@ export class Renderer extends EventEmitter implements IRenderer { // Recalculate the canvas dimensions; scaled* define the actual number of // pixel in the canvas this.dimensions.scaledCanvasHeight = this._terminal.rows * this.dimensions.scaledLineHeight; - this.dimensions.scaledCanvasWidth = Math.round(this._terminal.cols * this.dimensions.scaledCharWidth); + this.dimensions.scaledCanvasWidth = this._terminal.cols * this.dimensions.scaledCharWidth; // The 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 From 365661704424f9a33b3a2cb4b85ad144948a86a9 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 13 Sep 2017 15:21:40 -0700 Subject: [PATCH 135/156] Start merging fg/bg layers, draw using SP-AA --- src/renderer/BaseRenderLayer.ts | 3 +- src/renderer/CharAtlas.ts | 2 +- src/renderer/CursorRenderLayer.ts | 2 +- src/renderer/LinkRenderLayer.ts | 2 +- src/renderer/Renderer.ts | 10 +++--- src/renderer/SelectionRenderLayer.ts | 2 +- ...roundRenderLayer.ts => TextRenderLayer.ts} | 35 +++++++++++-------- 7 files changed, 30 insertions(+), 26 deletions(-) rename src/renderer/{ForegroundRenderLayer.ts => TextRenderLayer.ts} (86%) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 3bc0a9175..22d424070 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -25,12 +25,13 @@ export abstract class BaseRenderLayer implements IRenderLayer { container: HTMLElement, id: string, zIndex: number, + alpha: boolean, 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 = this._canvas.getContext('2d', {alpha}); this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); container.appendChild(this._canvas); } diff --git a/src/renderer/CharAtlas.ts b/src/renderer/CharAtlas.ts index bf080cce5..45835a27b 100644 --- a/src/renderer/CharAtlas.ts +++ b/src/renderer/CharAtlas.ts @@ -120,7 +120,7 @@ class CharAtlasGenerator { constructor(private _document: Document) { this._canvas = this._document.createElement('canvas'); - this._ctx = this._canvas.getContext('2d'); + this._ctx = this._canvas.getContext('2d', {alpha: false}); this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); } diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index 8e25d7c16..faed9bfe3 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -31,7 +31,7 @@ export class CursorRenderLayer extends BaseRenderLayer { private _isFocused: boolean; constructor(container: HTMLElement, zIndex: number, colors: IColorSet) { - super(container, 'cursor', zIndex, colors); + super(container, 'cursor', zIndex, true, colors); this._state = { x: null, y: null, diff --git a/src/renderer/LinkRenderLayer.ts b/src/renderer/LinkRenderLayer.ts index ad1789334..980840349 100644 --- a/src/renderer/LinkRenderLayer.ts +++ b/src/renderer/LinkRenderLayer.ts @@ -15,7 +15,7 @@ export class LinkRenderLayer extends BaseRenderLayer { private _state: LinkHoverEvent = null; constructor(container: HTMLElement, zIndex: number, colors: IColorSet, terminal: ILinkifierAccessor) { - super(container, 'link', zIndex, colors); + super(container, 'link', zIndex, true, colors); terminal.linkifier.on(LinkHoverEventTypes.HOVER, (e: LinkHoverEvent) => this._onLinkHover(e)); terminal.linkifier.on(LinkHoverEventTypes.LEAVE, (e: LinkHoverEvent) => this._onLinkLeave(e)); } diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 63782afdf..8e8934d68 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -5,8 +5,7 @@ import { ITerminal, ITheme } from '../Interfaces'; import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; -import { BackgroundRenderLayer } from './BackgroundRenderLayer'; -import { ForegroundRenderLayer } from './ForegroundRenderLayer'; +import { TextRenderLayer } from './TextRenderLayer'; import { SelectionRenderLayer } from './SelectionRenderLayer'; import { CursorRenderLayer } from './CursorRenderLayer'; import { ColorManager } from './ColorManager'; @@ -30,11 +29,10 @@ export class Renderer extends EventEmitter implements IRenderer { super(); this._colorManager = new ColorManager(); this._renderLayers = [ - new BackgroundRenderLayer(this._terminal.element, 0, this._colorManager.colors), + new TextRenderLayer(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 LinkRenderLayer(this._terminal.element, 3, this._colorManager.colors, this._terminal), - new CursorRenderLayer(this._terminal.element, 4, this._colorManager.colors) + new LinkRenderLayer(this._terminal.element, 2, this._colorManager.colors, this._terminal), + new CursorRenderLayer(this._terminal.element, 3, this._colorManager.colors) ]; this.dimensions = { scaledCharWidth: null, diff --git a/src/renderer/SelectionRenderLayer.ts b/src/renderer/SelectionRenderLayer.ts index 5339c7a40..7a0aff839 100644 --- a/src/renderer/SelectionRenderLayer.ts +++ b/src/renderer/SelectionRenderLayer.ts @@ -14,7 +14,7 @@ export class SelectionRenderLayer extends BaseRenderLayer { private _state: {start: [number, number], end: [number, number]}; constructor(container: HTMLElement, zIndex: number, colors: IColorSet) { - super(container, 'selection', zIndex, colors); + super(container, 'selection', zIndex, true, colors); this._state = { start: null, end: null diff --git a/src/renderer/ForegroundRenderLayer.ts b/src/renderer/TextRenderLayer.ts similarity index 86% rename from src/renderer/ForegroundRenderLayer.ts rename to src/renderer/TextRenderLayer.ts index 19625466e..4b06ef427 100644 --- a/src/renderer/ForegroundRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -18,29 +18,34 @@ import { BaseRenderLayer, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer'; */ const EMOJI_OWNED_CHAR_DATA: CharData = [null, '', 0, -1]; -export class ForegroundRenderLayer extends BaseRenderLayer { - private _state: GridCache; +export class TextRenderLayer extends BaseRenderLayer { + private _fgState: GridCache; + private _bgState: GridCache; constructor(container: HTMLElement, zIndex: number, colors: IColorSet) { - super(container, 'fg', zIndex, colors); - this._state = new GridCache(); + super(container, 'text', zIndex, false, colors); + this._fgState = new GridCache(); + this._bgState = new GridCache(); } public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void { super.resize(terminal, dim, charSizeChanged); // Resizing the canvas discards the contents of the canvas so clear state - this._state.clear(); - this._state.resize(terminal.cols, terminal.rows); + this._fgState.clear(); + this._bgState.clear(); + this._fgState.resize(terminal.cols, terminal.rows); + this._bgState.resize(terminal.cols, terminal.rows); } public reset(terminal: ITerminal): void { - this._state.clear(); + this._fgState.clear(); + this._bgState.clear(); this.clearAll(); } public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void { // Resize has not been called yet - if (this._state.cache.length === 0) { + if (this._fgState.cache.length === 0) { return; } @@ -58,7 +63,7 @@ export class ForegroundRenderLayer extends BaseRenderLayer { // 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; + this._fgState.cache[x][y] = null; continue; } @@ -75,10 +80,10 @@ export class ForegroundRenderLayer extends BaseRenderLayer { } // Skip rendering if the character is identical - const state = this._state.cache[x][y]; + const state = this._fgState.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; + this._fgState.cache[x][y] = charData; continue; } @@ -86,7 +91,7 @@ export class ForegroundRenderLayer extends BaseRenderLayer { if (state && state[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) { this._clearChar(x, y); } - this._state.cache[x][y] = charData; + this._fgState.cache[x][y] = charData; const flags = attr >> 18; @@ -104,14 +109,14 @@ export class ForegroundRenderLayer extends BaseRenderLayer { // 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; + this._fgState.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); // 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; + this._fgState.cache[x + 1][y] = EMOJI_OWNED_CHAR_DATA; } } @@ -176,7 +181,7 @@ export class ForegroundRenderLayer extends BaseRenderLayer { 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]; + const state = this._fgState.cache[x][y]; if (state && state[CHAR_DATA_WIDTH_INDEX] === 2) { colsToClear = 2; } From 402cebc76ce45f1784a57521530c110abfeaa0ff Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 13 Sep 2017 22:50:48 -0700 Subject: [PATCH 136/156] Support bold in non cached text drawing Fixes #987 --- src/renderer/BaseRenderLayer.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 22d424070..3f5b2bb61 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -235,7 +235,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { code * charAtlasCellWidth, colorIndex * charAtlasCellHeight, 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); + this._drawUncachedChar(terminal, char, width, fg, x, y, bold); } // This draws the atlas (for debugging purposes) // this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); @@ -253,9 +253,12 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @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 { + private _drawUncachedChar(terminal: ITerminal, char: string, width: number, fg: number, x: number, y: number, bold: boolean): void { this._ctx.save(); this._ctx.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`; + if (bold) { + this._ctx.font = `bold ${this._ctx.font}`; + } this._ctx.textBaseline = 'top'; if (fg === INVERTED_DEFAULT_COLOR) { From 872c007a295a92add19f2b9f7ed5510e8a7fffe8 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 13 Sep 2017 23:15:21 -0700 Subject: [PATCH 137/156] Finish merging bg and fg layers --- src/renderer/BackgroundRenderLayer.ts | 71 --------------------------- src/renderer/BaseRenderLayer.ts | 7 ++- src/renderer/TextRenderLayer.ts | 58 +++++++++++++--------- 3 files changed, 39 insertions(+), 97 deletions(-) delete mode 100644 src/renderer/BackgroundRenderLayer.ts diff --git a/src/renderer/BackgroundRenderLayer.ts b/src/renderer/BackgroundRenderLayer.ts deleted file mode 100644 index 279aee6db..000000000 --- a/src/renderer/BackgroundRenderLayer.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { IColorSet, IRenderDimensions } 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, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer'; - -export class BackgroundRenderLayer extends BaseRenderLayer { - private _state: GridCache; - - constructor(container: HTMLElement, zIndex: number, colors: IColorSet) { - super(container, 'bg', zIndex, colors); - this._state = new GridCache(); - } - - public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void { - super.resize(terminal, dim, charSizeChanged); - // Resizing the canvas discards the contents of the canvas so clear state - this._state.clear(); - this._state.resize(terminal.cols, terminal.rows); - } - - public reset(terminal: ITerminal): void { - this._state.clear(); - this.clearAll(); - } - - 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); - for (let x = 0; x < terminal.cols; x++) { - 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; - if (bg === 257) { - bg = INVERTED_DEFAULT_COLOR; - } - } - - const cellState = this._state.cache[x][y]; - const needsRefresh = (bg < 256 && cellState !== bg) || cellState !== null; - if (needsRefresh) { - if (bg < 256) { - this._ctx.save(); - 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; - } else { - 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 3f5b2bb61..38e7b9d45 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -207,9 +207,11 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @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 bg The background color, in the format stored within the attributes. + * This is used to validate whether a cached image can be used. * @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 { + protected drawChar(terminal: ITerminal, char: string, code: number, width: number, x: number, y: number, fg: number, bg: 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); @@ -227,7 +229,8 @@ export abstract class BaseRenderLayer implements IRenderLayer { const isAscii = code < 256; const isBasicColor = (colorIndex > 1 && fg < 16); const isDefaultColor = fg >= 256; - if (isAscii && (isBasicColor || isDefaultColor)) { + const isDefaultBackground = bg >= 256; + if (isAscii && (isBasicColor || isDefaultColor) && isDefaultBackground) { // 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/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index 4b06ef427..41fcdeee6 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -19,33 +19,28 @@ import { BaseRenderLayer, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer'; const EMOJI_OWNED_CHAR_DATA: CharData = [null, '', 0, -1]; export class TextRenderLayer extends BaseRenderLayer { - private _fgState: GridCache; - private _bgState: GridCache; + private _state: GridCache; constructor(container: HTMLElement, zIndex: number, colors: IColorSet) { super(container, 'text', zIndex, false, colors); - this._fgState = new GridCache(); - this._bgState = new GridCache(); + this._state = new GridCache(); } public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void { super.resize(terminal, dim, charSizeChanged); // Resizing the canvas discards the contents of the canvas so clear state - this._fgState.clear(); - this._bgState.clear(); - this._fgState.resize(terminal.cols, terminal.rows); - this._bgState.resize(terminal.cols, terminal.rows); + this._state.clear(); + this._state.resize(terminal.cols, terminal.rows); } public reset(terminal: ITerminal): void { - this._fgState.clear(); - this._bgState.clear(); + this._state.clear(); this.clearAll(); } public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void { // Resize has not been called yet - if (this._fgState.cache.length === 0) { + if (this._state.cache.length === 0) { return; } @@ -63,7 +58,7 @@ export class TextRenderLayer extends BaseRenderLayer { // The character to the left is a wide character, drawing is owned by // the char at x-1 if (width === 0) { - this._fgState.cache[x][y] = null; + this._state.cache[x][y] = null; continue; } @@ -80,23 +75,26 @@ export class TextRenderLayer extends BaseRenderLayer { } // Skip rendering if the character is identical - const state = this._fgState.cache[x][y]; + 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._fgState.cache[x][y] = charData; + this._state.cache[x][y] = charData; continue; } - // Clear the old character if present - if (state && state[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) { + // Clear the old character was not a space with the default background + if (state && !(state[CHAR_DATA_CODE_INDEX] === 32 /*' '*/ && (state[CHAR_DATA_ATTR_INDEX] & 0x1ff) >= 256)) { this._clearChar(x, y); } - this._fgState.cache[x][y] = charData; + this._state.cache[x][y] = charData; const flags = attr >> 18; + let bg = attr & 0x1ff; // Skip rendering if the character is invisible - if (!code || code === 32 /*' '*/ || (flags & FLAGS.INVISIBLE)) { + const isDefaultBackground = bg >= 256; + const isInvisible = flags & FLAGS.INVISIBLE; + if (!code || (code === 32 /*' '*/ && isDefaultBackground) || isInvisible) { continue; } @@ -109,14 +107,14 @@ export class TextRenderLayer extends BaseRenderLayer { // 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._fgState.cache[x][y] = EMOJI_OWNED_CHAR_DATA; + 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); // 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._fgState.cache[x + 1][y] = EMOJI_OWNED_CHAR_DATA; + this._state.cache[x + 1][y] = EMOJI_OWNED_CHAR_DATA; } } @@ -124,11 +122,23 @@ export class TextRenderLayer extends BaseRenderLayer { // If inverse flag is on, the foreground should become the background. if (flags & FLAGS.INVERSE) { - fg = attr & 0x1ff; - // TODO: Is this case still needed + const temp = bg; + bg = fg; + fg = bg; if (fg === 256) { fg = INVERTED_DEFAULT_COLOR; } + if (bg === 257) { + bg = INVERTED_DEFAULT_COLOR; + } + } + + // Draw background + if (bg < 256) { + this._ctx.save(); + this._ctx.fillStyle = (bg === INVERTED_DEFAULT_COLOR ? this.colors.foreground : this.colors.ansi[bg]); + this.fillCells(x, y, width, 1); + this._ctx.restore(); } this._ctx.save(); @@ -152,7 +162,7 @@ export class TextRenderLayer extends BaseRenderLayer { this.fillBottomLineAtCells(x, y); } - this.drawChar(terminal, char, code, width, x, y, fg, !!(flags & FLAGS.BOLD)); + this.drawChar(terminal, char, code, width, x, y, fg, bg, !!(flags & FLAGS.BOLD)); this._ctx.restore(); } @@ -181,7 +191,7 @@ export class TextRenderLayer extends BaseRenderLayer { private _clearChar(x: number, y: number): void { let colsToClear = 1; // Clear the adjacent character if it was wide - const state = this._fgState.cache[x][y]; + const state = this._state.cache[x][y]; if (state && state[CHAR_DATA_WIDTH_INDEX] === 2) { colsToClear = 2; } From b225866c270a554781ac2116844fc8dbb5acfce9 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 13 Sep 2017 23:21:24 -0700 Subject: [PATCH 138/156] Clear cells properly for text layer --- src/renderer/BaseRenderLayer.ts | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 38e7b9d45..88dfc26c3 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -25,7 +25,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { container: HTMLElement, id: string, zIndex: number, - alpha: boolean, + private alpha: boolean, protected colors: IColorSet ) { this._canvas = document.createElement('canvas'); @@ -154,7 +154,12 @@ export abstract class BaseRenderLayer implements IRenderLayer { * Clears the entire canvas. */ protected clearAll(): void { - this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); + if (this.alpha) { + this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); + } else { + this._ctx.fillStyle = this.colors.background; + this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height); + } } /** @@ -166,11 +171,20 @@ export abstract class BaseRenderLayer implements IRenderLayer { */ protected clearCells(x: number, y: number, width: number, height: number): void { const cellLeft = this._getCellLeft(x); - this._ctx.clearRect( - cellLeft, - y * this.scaledLineHeight, - this._getCellLeft(x + width) - cellLeft, - height * this.scaledLineHeight); + if (this.alpha) { + this._ctx.clearRect( + cellLeft, + y * this.scaledLineHeight, + this._getCellLeft(x + width) - cellLeft, + height * this.scaledLineHeight); + } else { + this._ctx.fillStyle = this.colors.background; + this._ctx.fillRect( + cellLeft, + y * this.scaledLineHeight, + this._getCellLeft(x + width) - cellLeft, + height * this.scaledLineHeight); + } } /** From 0a58628f74b57953d7fec4feef55031f7acb5a5a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 13 Sep 2017 23:49:34 -0700 Subject: [PATCH 139/156] Clear text layer to the background color on creation Prevents a black flash before the first draw --- src/renderer/BaseRenderLayer.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 88dfc26c3..d06e0ee77 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -33,6 +33,10 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._canvas.style.zIndex = zIndex.toString(); this._ctx = this._canvas.getContext('2d', {alpha}); this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); + // Draw the background if this is an opaque layer + if (!alpha) { + this.clearAll(); + } container.appendChild(this._canvas); } From 068c0743a1507f64f55a81dcf46e4aa3980882e4 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 13 Sep 2017 23:54:45 -0700 Subject: [PATCH 140/156] Support non-#000 background colors in char atlas --- src/renderer/CharAtlas.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/renderer/CharAtlas.ts b/src/renderer/CharAtlas.ts index 45835a27b..34404e123 100644 --- a/src/renderer/CharAtlas.ts +++ b/src/renderer/CharAtlas.ts @@ -64,7 +64,7 @@ export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledC } const newEntry: ICharAtlasCacheEntry = { - bitmap: generator.generate(scaledCharWidth, scaledCharHeight, terminal.options.fontSize, terminal.options.fontFamily, colors.foreground, colors.ansi), + bitmap: generator.generate(scaledCharWidth, scaledCharHeight, terminal.options.fontSize, terminal.options.fontFamily, colors.background, colors.foreground, colors.ansi), config: newConfig, ownedBy: [terminal] }; @@ -75,7 +75,7 @@ export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledC function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: ITerminal, colors: IColorSet): ICharAtlasConfig { const clonedColors = { foreground: colors.foreground, - background: null, + background: colors.background, cursor: null, selection: null, ansi: colors.ansi.slice(0, 16) @@ -99,7 +99,8 @@ 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.foreground === b.colors.foreground && + a.colors.background === b.colors.background; } let generator: CharAtlasGenerator; @@ -124,12 +125,15 @@ class CharAtlasGenerator { this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); } - public generate(scaledCharWidth: number, scaledCharHeight: number, fontSize: number, fontFamily: string, foreground: string, ansiColors: string[]): HTMLCanvasElement | Promise { + public generate(scaledCharWidth: number, scaledCharHeight: number, fontSize: number, fontFamily: string, background: 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; this._canvas.height = (/*default+default bold*/2 + /*0-15*/16) * cellHeight; + this._ctx.fillStyle = background; + this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height); + this._ctx.save(); this._ctx.fillStyle = foreground; this._ctx.font = `${fontSize * window.devicePixelRatio}px ${fontFamily}`; From c2b765f3ffc39f11a1663abfd6c9ce67c2544d17 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 13 Sep 2017 23:56:35 -0700 Subject: [PATCH 141/156] Redraw background on text layer after a resize --- src/renderer/BaseRenderLayer.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index d06e0ee77..3f9965458 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -76,6 +76,11 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._canvas.style.width = `${dim.canvasWidth}px`; this._canvas.style.height = `${dim.canvasHeight}px`; + // Draw the background if this is an opaque layer + if (!this.alpha) { + this.clearAll(); + } + if (charSizeChanged) { this._refreshCharAtlas(terminal, this.colors); } From aca7ab63e87899b1e8160f047b49812756eb3574 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 14 Sep 2017 00:22:57 -0700 Subject: [PATCH 142/156] Set background color on Renderer immediately Prevents black flash on init --- src/Terminal.ts | 14 ++--- src/renderer/BaseRenderLayer.ts | 86 ++++++++++++++-------------- src/renderer/ColorManager.ts | 4 +- src/renderer/CursorRenderLayer.ts | 12 ++-- src/renderer/Interfaces.ts | 4 ++ src/renderer/LinkRenderLayer.ts | 2 +- src/renderer/Renderer.ts | 23 ++++---- src/renderer/SelectionRenderLayer.ts | 2 +- src/renderer/TextRenderLayer.ts | 8 +-- 9 files changed, 79 insertions(+), 76 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index ef4b21eb7..443203570 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -622,8 +622,11 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.charMeasure = new CharMeasure(document, this.helperContainer); + this.renderer = new Renderer(this, this.options.theme); + this.options.theme = null; this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure); - this.renderer = new Renderer(this); + this.viewport.onThemeChanged(this.renderer.colorManager.colors); + this.on('cursormove', () => this.renderer.onCursorMove()); this.on('resize', () => this.renderer.onResize(this.cols, this.rows, false)); this.on('blur', () => this.renderer.onBlur()); @@ -652,15 +655,6 @@ 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); diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 3f9965458..b55b5aaa3 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -14,10 +14,10 @@ export const INVERTED_DEFAULT_COLOR = -1; export abstract class BaseRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; protected _ctx: CanvasRenderingContext2D; - private scaledCharWidth: number; - private scaledCharHeight: number; - private scaledLineHeight: number; - private scaledLineDrawY: number; + private _scaledCharWidth: number; + private _scaledCharHeight: number; + private _scaledLineHeight: number; + private _scaledLineDrawY: number; private _charAtlas: HTMLCanvasElement | ImageBitmap; @@ -25,16 +25,17 @@ export abstract class BaseRenderLayer implements IRenderLayer { container: HTMLElement, id: string, zIndex: number, - private alpha: boolean, - protected colors: IColorSet + private _alpha: boolean, + 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', {alpha}); + this._ctx = this._canvas.getContext('2d', {_alpha}); this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); // Draw the background if this is an opaque layer - if (!alpha) { + if (!_alpha) { + console.log('clearAll!'); this.clearAll(); } container.appendChild(this._canvas); @@ -58,7 +59,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { */ private _refreshCharAtlas(terminal: ITerminal, colorSet: IColorSet): void { this._charAtlas = null; - const result = acquireCharAtlas(terminal, this.colors, this.scaledCharWidth, this.scaledCharHeight); + const result = acquireCharAtlas(terminal, this._colors, this._scaledCharWidth, this._scaledCharHeight); if (result instanceof HTMLCanvasElement) { this._charAtlas = result; } else { @@ -67,22 +68,22 @@ export abstract class BaseRenderLayer implements IRenderLayer { } public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void { - this.scaledCharWidth = dim.scaledCharWidth; - this.scaledCharHeight = dim.scaledCharHeight; - this.scaledLineHeight = dim.scaledLineHeight; - this.scaledLineDrawY = dim.scaledLineDrawY; + this._scaledCharWidth = dim.scaledCharWidth; + this._scaledCharHeight = dim.scaledCharHeight; + this._scaledLineHeight = dim.scaledLineHeight; + this._scaledLineDrawY = dim.scaledLineDrawY; this._canvas.width = dim.scaledCanvasWidth; this._canvas.height = dim.scaledCanvasHeight; this._canvas.style.width = `${dim.canvasWidth}px`; this._canvas.style.height = `${dim.canvasHeight}px`; // Draw the background if this is an opaque layer - if (!this.alpha) { + if (!this._alpha) { this.clearAll(); } if (charSizeChanged) { - this._refreshCharAtlas(terminal, this.colors); + this._refreshCharAtlas(terminal, this._colors); } } @@ -95,7 +96,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param x The column of the cell. */ private _getCellLeft(x: number): number { - return Math.round(x * this.scaledCharWidth); + return Math.round(x * this._scaledCharWidth); } /** @@ -109,9 +110,9 @@ export abstract class BaseRenderLayer implements IRenderLayer { const cellLeft = this._getCellLeft(x); this._ctx.fillRect( cellLeft, - y * this.scaledLineHeight, + y * this._scaledLineHeight, this._getCellLeft(x + width) - cellLeft, - height * this.scaledLineHeight); + height * this._scaledLineHeight); } /** @@ -124,7 +125,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { const cellLeft = this._getCellLeft(x); this._ctx.fillRect( cellLeft, - (y + 1) * this.scaledLineHeight - 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._getCellLeft(x + width) - cellLeft, window.devicePixelRatio); } @@ -138,9 +139,9 @@ export abstract class BaseRenderLayer implements IRenderLayer { protected fillLeftLineAtCell(x: number, y: number): void { this._ctx.fillRect( this._getCellLeft(x), - y * this.scaledLineHeight, + y * this._scaledLineHeight, window.devicePixelRatio, - this.scaledLineHeight); + this._scaledLineHeight); } /** @@ -154,19 +155,20 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.lineWidth = window.devicePixelRatio; this._ctx.strokeRect( cellLeft + window.devicePixelRatio / 2, - y * this.scaledLineHeight + (window.devicePixelRatio / 2), + y * this._scaledLineHeight + (window.devicePixelRatio / 2), this._getCellLeft(x + width) - cellLeft - window.devicePixelRatio, - (height * this.scaledLineHeight) - window.devicePixelRatio); + (height * this._scaledLineHeight) - window.devicePixelRatio); } /** * Clears the entire canvas. */ protected clearAll(): void { - if (this.alpha) { + if (this._alpha) { this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); } else { - this._ctx.fillStyle = this.colors.background; + console.log('fill with', this._colors.background); + this._ctx.fillStyle = this._colors.background; this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height); } } @@ -180,19 +182,19 @@ export abstract class BaseRenderLayer implements IRenderLayer { */ protected clearCells(x: number, y: number, width: number, height: number): void { const cellLeft = this._getCellLeft(x); - if (this.alpha) { + if (this._alpha) { this._ctx.clearRect( cellLeft, - y * this.scaledLineHeight, + y * this._scaledLineHeight, this._getCellLeft(x + width) - cellLeft, - height * this.scaledLineHeight); + height * this._scaledLineHeight); } else { - this._ctx.fillStyle = this.colors.background; + this._ctx.fillStyle = this._colors.background; this._ctx.fillRect( cellLeft, - y * this.scaledLineHeight, + y * this._scaledLineHeight, this._getCellLeft(x + width) - cellLeft, - height * this.scaledLineHeight); + height * this._scaledLineHeight); } } @@ -215,9 +217,9 @@ 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.scaledLineHeight + this.scaledLineDrawY, 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); + this._ctx.fillText(charData[CHAR_DATA_CHAR_INDEX], x * this._scaledCharWidth, y * this._scaledCharHeight); } /** @@ -255,11 +257,11 @@ export abstract class BaseRenderLayer implements IRenderLayer { const isDefaultBackground = bg >= 256; if (isAscii && (isBasicColor || isDefaultColor) && isDefaultBackground) { // 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; + const charAtlasCellWidth = this._scaledCharWidth + CHAR_ATLAS_CELL_SPACING; + 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.scaledLineHeight + this.scaledLineDrawY, this.scaledCharWidth, this.scaledCharHeight); + code * charAtlasCellWidth, colorIndex * charAtlasCellHeight, 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, bold); } @@ -288,12 +290,12 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.textBaseline = 'top'; if (fg === INVERTED_DEFAULT_COLOR) { - this._ctx.fillStyle = this.colors.background; + this._ctx.fillStyle = this._colors.background; } else if (fg < 256) { // 256 color support - this._ctx.fillStyle = this.colors.ansi[fg]; + this._ctx.fillStyle = this._colors.ansi[fg]; } else { - this._ctx.fillStyle = this.colors.foreground; + this._ctx.fillStyle = this._colors.foreground; } // Since uncached characters are not coming off the char atlas with source @@ -301,11 +303,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.scaledLineHeight + this.scaledLineDrawY, 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.scaledLineHeight + this.scaledLineDrawY); + this._ctx.fillText(char, x * this._scaledCharWidth, y * this._scaledLineHeight + this._scaledLineDrawY); this._ctx.restore(); } } diff --git a/src/renderer/ColorManager.ts b/src/renderer/ColorManager.ts index b9afd0e1a..746970bb0 100644 --- a/src/renderer/ColorManager.ts +++ b/src/renderer/ColorManager.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IColorSet } from './Interfaces'; +import { IColorSet, IColorManager } from './Interfaces'; import { ITheme } from '../Interfaces'; const DEFAULT_FOREGROUND = '#ffffff'; @@ -64,7 +64,7 @@ function toPaddedHex(c: number): string { /** * Manages the source of truth for a terminal's colors. */ -export class ColorManager { +export class ColorManager implements IColorManager { public colors: IColorSet; constructor() { diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index faed9bfe3..008b5662f 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -135,7 +135,7 @@ export class CursorRenderLayer extends BaseRenderLayer { if (!terminal.isFocused) { this._clearCursor(); this._ctx.save(); - this._ctx.fillStyle = this.colors.cursor; + this._ctx.fillStyle = this._colors.cursor; this._renderBlurCursor(terminal, terminal.buffer.x, viewportRelativeCursorY, charData); this._ctx.restore(); this._state.x = terminal.buffer.x; @@ -190,30 +190,30 @@ 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._ctx.fillStyle = this._colors.cursor; this.fillLeftLineAtCell(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._ctx.fillStyle = this._colors.cursor; this.fillCells(x, y, charData[CHAR_DATA_WIDTH_INDEX], 1); - this._ctx.fillStyle = this.colors.background; + this._ctx.fillStyle = this._colors.background; this.fillCharTrueColor(terminal, charData, x, y); this._ctx.restore(); } private _renderUnderlineCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void { this._ctx.save(); - this._ctx.fillStyle = this.colors.cursor; + this._ctx.fillStyle = this._colors.cursor; this.fillBottomLineAtCells(x, y); this._ctx.restore(); } private _renderBlurCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void { this._ctx.save(); - this._ctx.strokeStyle = 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/Interfaces.ts b/src/renderer/Interfaces.ts index 890b01f5a..719ca8cf8 100644 --- a/src/renderer/Interfaces.ts +++ b/src/renderer/Interfaces.ts @@ -7,6 +7,7 @@ import { ITerminal, ITerminalOptions, ITheme, IEventEmitter } from '../Interface export interface IRenderer extends IEventEmitter { dimensions: IRenderDimensions; + colorManager: IColorManager; setTheme(theme: ITheme): IColorSet; onWindowResize(devicePixelRatio: number): void; @@ -69,6 +70,9 @@ export interface IRenderLayer { reset(terminal: ITerminal): void; } +export interface IColorManager { + colors: IColorSet; +} export interface IColorSet { foreground: string; diff --git a/src/renderer/LinkRenderLayer.ts b/src/renderer/LinkRenderLayer.ts index 980840349..8f3b63a54 100644 --- a/src/renderer/LinkRenderLayer.ts +++ b/src/renderer/LinkRenderLayer.ts @@ -38,7 +38,7 @@ export class LinkRenderLayer extends BaseRenderLayer { } private _onLinkHover(e: LinkHoverEvent): void { - this._ctx.fillStyle = this.colors.foreground; + this._ctx.fillStyle = this._colors.foreground; this.fillBottomLineAtCells(e.x, e.y, e.length); this._state = e; } diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 8e8934d68..1bdd3b4a0 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -22,17 +22,20 @@ export class Renderer extends EventEmitter implements IRenderer { private _renderLayers: IRenderLayer[]; private _devicePixelRatio: number; - private _colorManager: ColorManager; + public colorManager: ColorManager; public dimensions: IRenderDimensions; - constructor(private _terminal: ITerminal) { + constructor(private _terminal: ITerminal, theme: ITheme) { super(); - this._colorManager = new ColorManager(); + this.colorManager = new ColorManager(); + if (theme) { + this.colorManager.setTheme(theme); + } this._renderLayers = [ - new TextRenderLayer(this._terminal.element, 0, this._colorManager.colors), - new SelectionRenderLayer(this._terminal.element, 1, this._colorManager.colors), - new LinkRenderLayer(this._terminal.element, 2, this._colorManager.colors, this._terminal), - new CursorRenderLayer(this._terminal.element, 3, this._colorManager.colors) + new TextRenderLayer(this._terminal.element, 0, this.colorManager.colors), + new SelectionRenderLayer(this._terminal.element, 1, this.colorManager.colors), + new LinkRenderLayer(this._terminal.element, 2, this.colorManager.colors, this._terminal), + new CursorRenderLayer(this._terminal.element, 3, this.colorManager.colors) ]; this.dimensions = { scaledCharWidth: null, @@ -57,17 +60,17 @@ export class Renderer extends EventEmitter implements IRenderer { } public setTheme(theme: ITheme): IColorSet { - this._colorManager.setTheme(theme); + this.colorManager.setTheme(theme); // Clear layers and force a full render this._renderLayers.forEach(l => { - l.onThemeChanged(this._terminal, this._colorManager.colors); + l.onThemeChanged(this._terminal, this.colorManager.colors); l.reset(this._terminal); }); this._terminal.refresh(0, this._terminal.rows - 1); - return this._colorManager.colors; + return this.colorManager.colors; } public onResize(cols: number, rows: number, didCharSizeChange: boolean): void { diff --git a/src/renderer/SelectionRenderLayer.ts b/src/renderer/SelectionRenderLayer.ts index 7a0aff839..54eb7fc4d 100644 --- a/src/renderer/SelectionRenderLayer.ts +++ b/src/renderer/SelectionRenderLayer.ts @@ -68,7 +68,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 = this.colors.selection; + this._ctx.fillStyle = this._colors.selection; this.fillCells(startCol, viewportCappedStartRow, startRowEndCol - startCol, 1); // Draw middle rows diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index 41fcdeee6..ced4ceaaf 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -136,7 +136,7 @@ export class TextRenderLayer extends BaseRenderLayer { // Draw background if (bg < 256) { this._ctx.save(); - this._ctx.fillStyle = (bg === INVERTED_DEFAULT_COLOR ? this.colors.foreground : this.colors.ansi[bg]); + this._ctx.fillStyle = (bg === INVERTED_DEFAULT_COLOR ? this._colors.foreground : this._colors.ansi[bg]); this.fillCells(x, y, width, 1); this._ctx.restore(); } @@ -152,12 +152,12 @@ export class TextRenderLayer extends BaseRenderLayer { if (flags & FLAGS.UNDERLINE) { if (fg === INVERTED_DEFAULT_COLOR) { - this._ctx.fillStyle = this.colors.background; + this._ctx.fillStyle = this._colors.background; } else if (fg < 256) { // 256 color support - this._ctx.fillStyle = this.colors.ansi[fg]; + this._ctx.fillStyle = this._colors.ansi[fg]; } else { - this._ctx.fillStyle = this.colors.foreground; + this._ctx.fillStyle = this._colors.foreground; } this.fillBottomLineAtCells(x, y); } From 8bdc8fb7a22b0d16a8cfe5e54d9eee42d5216e0f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 14 Sep 2017 00:29:38 -0700 Subject: [PATCH 143/156] Remove log --- src/renderer/BaseRenderLayer.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index b55b5aaa3..8f655de58 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -35,7 +35,6 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); // Draw the background if this is an opaque layer if (!_alpha) { - console.log('clearAll!'); this.clearAll(); } container.appendChild(this._canvas); @@ -167,7 +166,6 @@ export abstract class BaseRenderLayer implements IRenderLayer { if (this._alpha) { this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); } else { - console.log('fill with', this._colors.background); this._ctx.fillStyle = this._colors.background; this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height); } From bc824ad6a50d200009d02a882fbcc6876405a104 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 14 Sep 2017 09:03:41 -0700 Subject: [PATCH 144/156] Fix tests --- src/utils/TestUtils.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index 45cf169ac..beed0aaa0 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -7,7 +7,7 @@ import { ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManag import { LineData } from '../Types'; import { Buffer } from '../Buffer'; import * as Browser from './Browser'; -import { IColorSet, IRenderer, IRenderDimensions } from '../renderer/Interfaces'; +import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Interfaces'; export class MockTerminal implements ITerminal { renderer: IRenderer; @@ -216,6 +216,7 @@ export class MockBuffer implements IBuffer { } export class MockRenderer implements IRenderer { + colorManager: IColorManager; on(type: string, listener: IListenerType): void { throw new Error('Method not implemented.'); } From 0cf9f1d5364d93b3bca8ed1eb5d9942b902a6de7 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 14 Sep 2017 09:05:50 -0700 Subject: [PATCH 145/156] Fix tests --- src/utils/Mouse.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/Mouse.test.ts b/src/utils/Mouse.test.ts index 4163263be..b96dd68ac 100644 --- a/src/utils/Mouse.test.ts +++ b/src/utils/Mouse.test.ts @@ -58,6 +58,6 @@ describe('getCoords', () => { 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'); + assert.deepEqual(coords, [10, 10], 'coordinates should never come back as larger than the terminal'); }); }); From 9b6c4e046f61c39470d9d7ed295df03d55dc68df Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 16 Sep 2017 10:57:49 -0700 Subject: [PATCH 146/156] Only refresh char atlas when char dimensions are valid See Microsoft/vscode#34493 --- src/renderer/BaseRenderLayer.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 8f655de58..4cc074722 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -57,6 +57,9 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param colorSet The color set to use for the char atlas. */ private _refreshCharAtlas(terminal: ITerminal, colorSet: IColorSet): void { + if (this._scaledCharWidth > 0 && this._scaledCharHeight > 0) { + return; + } this._charAtlas = null; const result = acquireCharAtlas(terminal, this._colors, this._scaledCharWidth, this._scaledCharHeight); if (result instanceof HTMLCanvasElement) { From c8153104184622a9a243a24110acf27d9be7665b Mon Sep 17 00:00:00 2001 From: Thomas Zilz Date: Sat, 16 Sep 2017 20:36:55 +0200 Subject: [PATCH 147/156] Fix inverted cells, make sure inverted cells are cleared correctly (#996) * Corretly inverse foreground and background. Be more conservative when clearing * add missing space * Re-enable optimisation with better checks * Fix insversed check --- src/renderer/TextRenderLayer.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index ced4ceaaf..749978667 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -83,7 +83,8 @@ export class TextRenderLayer extends BaseRenderLayer { } // Clear the old character was not a space with the default background - if (state && !(state[CHAR_DATA_CODE_INDEX] === 32 /*' '*/ && (state[CHAR_DATA_ATTR_INDEX] & 0x1ff) >= 256)) { + const wasInverted = !!(state && state[CHAR_DATA_ATTR_INDEX] && state[CHAR_DATA_ATTR_INDEX] >> 18 & FLAGS.INVERSE); + if (state && !(state[CHAR_DATA_CODE_INDEX] === 32 /*' '*/ && (state[CHAR_DATA_ATTR_INDEX] & 0x1ff) >= 256 && !wasInverted)) { this._clearChar(x, y); } this._state.cache[x][y] = charData; @@ -94,7 +95,8 @@ export class TextRenderLayer extends BaseRenderLayer { // Skip rendering if the character is invisible const isDefaultBackground = bg >= 256; const isInvisible = flags & FLAGS.INVISIBLE; - if (!code || (code === 32 /*' '*/ && isDefaultBackground) || isInvisible) { + const isInverted = flags & FLAGS.INVERSE; + if (!code || (code === 32 /*' '*/ && isDefaultBackground && !isInverted) || isInvisible) { continue; } @@ -121,10 +123,10 @@ export class TextRenderLayer extends BaseRenderLayer { let fg = (attr >> 9) & 0x1ff; // If inverse flag is on, the foreground should become the background. - if (flags & FLAGS.INVERSE) { + if (isInverted) { const temp = bg; bg = fg; - fg = bg; + fg = temp; if (fg === 256) { fg = INVERTED_DEFAULT_COLOR; } From c4da8325ace6e4bde77822377bf994d389a7c75a Mon Sep 17 00:00:00 2001 From: Thomas Zilz Date: Sun, 17 Sep 2017 10:39:09 +0200 Subject: [PATCH 148/156] Allow all overflowing characters to extend to the next cell if followed by a space (Emojis etc) (#997) * Measure characters and thread them if they are overlapping * Fix whitespace * Use charData and ctx.save/restore, code cleanup * Fix trailing whitespace --- src/renderer/TextRenderLayer.ts | 79 +++++++++++++++++++++++---------- 1 file changed, 55 insertions(+), 24 deletions(-) diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index 749978667..e319efe7d 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -16,10 +16,13 @@ import { BaseRenderLayer, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer'; * 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]; +const OVERLAP_OWNED_CHAR_DATA: CharData = [null, '', 0, -1]; export class TextRenderLayer extends BaseRenderLayer { private _state: GridCache; + private _characterWidth: number; + private _characterFont: string; + private _characterOverlapCache: { [key: string]: boolean } = {}; constructor(container: HTMLElement, zIndex: number, colors: IColorSet) { super(container, 'text', zIndex, false, colors); @@ -28,6 +31,14 @@ export class TextRenderLayer extends BaseRenderLayer { public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void { super.resize(terminal, dim, charSizeChanged); + + // Clear the character width cache if the font or width has changed + const terminalFont = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`; + if (this._characterWidth !== dim.scaledCharWidth || this._characterFont !== terminalFont) { + this._characterWidth = dim.scaledCharWidth; + this._characterFont = terminalFont; + this._characterOverlapCache = {}; + } // Resizing the canvas discards the contents of the canvas so clear state this._state.clear(); this._state.resize(terminal.cols, terminal.rows); @@ -63,12 +74,12 @@ export class TextRenderLayer extends BaseRenderLayer { } // 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. + // overlapping character, skip the character and allow the overlapping + // 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])) { + if (this._isOverlapping(previousChar)) { continue; } } @@ -100,23 +111,23 @@ export class TextRenderLayer extends BaseRenderLayer { continue; } - // If the character is an emoji and the character to the right is a + // If the character is an overlapping char 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 + if (width !== 0 && this._isOverlapping(charData)) { + // If the character is overlapping, 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 + // overlaping chars `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; + this._state.cache[x][y] = OVERLAP_OWNED_CHAR_DATA; 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; + // The overlapping char's char data will force a clear and render when the + // overlapping char is no longer to the left of the character and also when + // the space changes to another character. + this._state.cache[x + 1][y] = OVERLAP_OWNED_CHAR_DATA; } } @@ -171,18 +182,38 @@ export class TextRenderLayer extends BaseRenderLayer { } } - /** - * Whether the character is an emoji. - * @param char The character to search. - */ - private _isEmoji(char: string): boolean { - // TODO: We need a generic solution for handling characters like this - // Check special ambiguous width characters - if (char === '➜') { - return true; + /** + * Whether a character is overlapping to the + * next cell. + */ + private _isOverlapping(charData: CharData): boolean { + // We assume that any ascii character will not overlap + const code = charData[CHAR_DATA_CODE_INDEX]; + if (code < 256) { + return false; } - // Check emoji unicode range - return char.search(/([\uD800-\uDBFF][\uDC00-\uDFFF])/g) >= 0; + + // Deliver from cache if available + const char = charData[CHAR_DATA_CHAR_INDEX]; + if (this._characterOverlapCache.hasOwnProperty(char)) { + return this._characterOverlapCache[char]; + } + + // Setup the font + this._ctx.save(); + this._ctx.font = this._characterFont; + + // Measure the width of the character, but Math.floor it + // because that is what the renderer does when it calculates + // the character dimensions we are comparing against + const overlaps = Math.floor(this._ctx.measureText(char).width) > this._characterWidth; + + // Restore the original context + this._ctx.restore(); + + // Cache and return + this._characterOverlapCache[char] = overlaps; + return overlaps; } /** From 40fdd00b9e1334c94618a69e60bee6538ff7eb36 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 20 Sep 2017 09:02:21 +0900 Subject: [PATCH 149/156] Correct if statement, ensure charAtlas exists --- src/renderer/BaseRenderLayer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 4cc074722..2bf49e681 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -57,7 +57,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param colorSet The color set to use for the char atlas. */ private _refreshCharAtlas(terminal: ITerminal, colorSet: IColorSet): void { - if (this._scaledCharWidth > 0 && this._scaledCharHeight > 0) { + if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) { return; } this._charAtlas = null; @@ -256,7 +256,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { const isBasicColor = (colorIndex > 1 && fg < 16); const isDefaultColor = fg >= 256; const isDefaultBackground = bg >= 256; - if (isAscii && (isBasicColor || isDefaultColor) && isDefaultBackground) { + if (this._charAtlas && isAscii && (isBasicColor || isDefaultColor) && isDefaultBackground) { // 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; From 79c07d436e5f5d6a5cb1f875dbafb7b51137b1f9 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 20 Sep 2017 11:53:19 +0900 Subject: [PATCH 150/156] Fix selection horizontal offset Fixes #1000 --- src/utils/Mouse.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utils/Mouse.ts b/src/utils/Mouse.ts index 7bc39b7fc..66e7f190a 100644 --- a/src/utils/Mouse.ts +++ b/src/utils/Mouse.ts @@ -49,7 +49,8 @@ export function getCoords(event: {pageX: number, pageY: number}, element: HTMLEl } // Convert to cols/rows. - coords[0] = Math.ceil((coords[0] + (isSelection ? charMeasure.width / 2 : 0)) / charMeasure.width); + const flooredCharWidth = Math.floor(charMeasure.width); + coords[0] = Math.ceil((coords[0] + (isSelection ? flooredCharWidth / 2 : 0)) / flooredCharWidth); coords[1] = Math.ceil(coords[1] / Math.ceil(charMeasure.height * lineHeight)); // Ensure coordinates are within the terminal viewport. From 0b54df14e2cda3f1c160e8031e8238fe3a81d1c7 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 20 Sep 2017 12:16:27 +0900 Subject: [PATCH 151/156] Remove unnecessary rounding --- src/renderer/BaseRenderLayer.ts | 40 +++++++++++---------------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 8f655de58..16de826ed 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -88,16 +88,6 @@ export abstract class BaseRenderLayer implements IRenderLayer { public abstract reset(terminal: ITerminal): void; - /** - * Gets the left position of a cell. Since character width is stored as a - * float in order to prevent bad letter spacing, drawing shapes in the cell - * need to be rounded. - * @param x The column of the cell. - */ - private _getCellLeft(x: number): number { - return Math.round(x * this._scaledCharWidth); - } - /** * Fills 1+ cells completely. This uses the existing fillStyle on the context. * @param x The column to start at. @@ -106,12 +96,11 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param height The number of rows to fill. */ protected fillCells(x: number, y: number, width: number, height: number): void { - const cellLeft = this._getCellLeft(x); this._ctx.fillRect( - cellLeft, - y * this._scaledLineHeight, - this._getCellLeft(x + width) - cellLeft, - height * this._scaledLineHeight); + x * this._scaledCharWidth, + y * this._scaledLineHeight, + width * this._scaledCharWidth, + height * this._scaledLineHeight); } /** @@ -121,11 +110,10 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param y The row to fill. */ protected fillBottomLineAtCells(x: number, y: number, width: number = 1): void { - const cellLeft = this._getCellLeft(x); this._ctx.fillRect( - cellLeft, + x * this._scaledCharWidth, (y + 1) * this._scaledLineHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */, - this._getCellLeft(x + width) - cellLeft, + width * this._scaledCharWidth, window.devicePixelRatio); } @@ -137,7 +125,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { */ protected fillLeftLineAtCell(x: number, y: number): void { this._ctx.fillRect( - this._getCellLeft(x), + x * this._scaledCharWidth, y * this._scaledLineHeight, window.devicePixelRatio, this._scaledLineHeight); @@ -150,12 +138,11 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param y The row to fill. */ protected strokeRectAtCell(x: number, y: number, width: number, height: number): void { - const cellLeft = this._getCellLeft(x); this._ctx.lineWidth = window.devicePixelRatio; this._ctx.strokeRect( - cellLeft + window.devicePixelRatio / 2, + x * this._scaledCharWidth + window.devicePixelRatio / 2, y * this._scaledLineHeight + (window.devicePixelRatio / 2), - this._getCellLeft(x + width) - cellLeft - window.devicePixelRatio, + width * this._scaledCharWidth - window.devicePixelRatio, (height * this._scaledLineHeight) - window.devicePixelRatio); } @@ -179,19 +166,18 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param height The number of rows to clear. */ protected clearCells(x: number, y: number, width: number, height: number): void { - const cellLeft = this._getCellLeft(x); if (this._alpha) { this._ctx.clearRect( - cellLeft, + x * this._scaledCharWidth, y * this._scaledLineHeight, - this._getCellLeft(x + width) - cellLeft, + width * this._scaledCharWidth, height * this._scaledLineHeight); } else { this._ctx.fillStyle = this._colors.background; this._ctx.fillRect( - cellLeft, + x * this._scaledCharWidth, y * this._scaledLineHeight, - this._getCellLeft(x + width) - cellLeft, + width * this._scaledCharWidth, height * this._scaledLineHeight); } } From 8243eef67ca6bc81a77d76b95cc4fd78d3ba8e6d Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 20 Sep 2017 12:54:20 +0900 Subject: [PATCH 152/156] Properly clear links MouzeZoneManager rows are not 0-based Fixes #990 --- src/input/MouseZoneManager.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/input/MouseZoneManager.ts b/src/input/MouseZoneManager.ts index c7feed2a3..026e79c25 100644 --- a/src/input/MouseZoneManager.ts +++ b/src/input/MouseZoneManager.ts @@ -50,10 +50,16 @@ export class MouseZoneManager implements IMouseZoneManager { return; } + // Clear all if start/end weren't set + if (!end) { + start = 0; + end = this._terminal.rows - 1; + } + // Iterate through zones and clear them out if they're within the range for (let i = 0; i < this._zones.length; i++) { const zone = this._zones[i]; - if (zone.y >= start && zone.y <= end) { + if (zone.y > start && zone.y <= end + 1) { if (this._currentZone && this._currentZone === zone) { this._currentZone.leaveCallback(); this._currentZone = null; From a2389d5c5b46d14797345ae75ba0bcc5b884f521 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 24 Sep 2017 10:59:38 -0700 Subject: [PATCH 153/156] Fix validation callback to use the uri, not whole row --- src/Linkifier.test.ts | 11 +++++++++++ src/Linkifier.ts | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index df9e459cd..0878aafc2 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -156,6 +156,17 @@ describe('Linkifier', () => { linkifier.linkifyRows(); }); + it('should validate the uri, not the row', done => { + addRow('abc test abc'); + linkifier.registerLinkMatcher(/test/, () => done(), { + validationCallback: (uri, cb) => { + assert.equal(uri, 'test'); + done(); + } + }); + linkifier.linkifyRows(); + }); + it('should disable link if false', done => { addRow('test'); linkifier.registerLinkMatcher(/test/, () => assert.fail(), { diff --git a/src/Linkifier.ts b/src/Linkifier.ts index fa47271c2..42d20f9c1 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -236,7 +236,7 @@ export class Linkifier extends EventEmitter implements ILinkifier { // Ensure the link is valid before registering if (matcher.validationCallback) { - matcher.validationCallback(text, isValid => { + matcher.validationCallback(uri, isValid => { // Discard link if the line has already changed if (this._rowsTimeoutId) { return; From 13d46476c896c7794deacd89f4d012c5ae25ddce Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 28 Sep 2017 14:52:56 -0400 Subject: [PATCH 154/156] Add null check when fetching line string See Microsoft/vscode#34466 --- src/Buffer.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Buffer.ts b/src/Buffer.ts index 2d23980dc..548fba712 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -211,6 +211,9 @@ export class Buffer implements IBuffer { let widthAdjustedStartCol = startCol; let widthAdjustedEndCol = endCol; const line = this.lines.get(lineIndex); + if (!line) { + return ''; + } for (let i = 0; i < line.length; i++) { const char = line[i]; lineString += char[CHAR_DATA_CHAR_INDEX]; From a8174398f593d7cc3360f203c140e724ec9e6a21 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 28 Sep 2017 17:23:45 -0400 Subject: [PATCH 155/156] Calculate mouse coordinates using correct char dimensions Previously the Mouse class was using raw CSS pixels, we need CSS pixels based on the amount of space the cells take up on the canvas See Microsoft/vscode#35114 --- src/Interfaces.ts | 6 +++ src/SelectionManager.ts | 6 +-- src/Terminal.ts | 10 ++-- src/input/MouseZoneManager.ts | 3 +- src/renderer/Interfaces.ts | 2 + src/renderer/Renderer.ts | 13 +++++- src/utils/Mouse.test.ts | 63 ------------------------- src/utils/Mouse.ts | 83 --------------------------------- src/utils/MouseHelper.test.ts | 70 ++++++++++++++++++++++++++++ src/utils/MouseHelper.ts | 86 +++++++++++++++++++++++++++++++++++ src/utils/TestUtils.test.ts | 3 +- 11 files changed, 188 insertions(+), 157 deletions(-) delete mode 100644 src/utils/Mouse.test.ts delete mode 100644 src/utils/Mouse.ts create mode 100644 src/utils/MouseHelper.test.ts create mode 100644 src/utils/MouseHelper.ts diff --git a/src/Interfaces.ts b/src/Interfaces.ts index 513deede1..2c77e8d3f 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -47,6 +47,7 @@ export interface ITerminal extends ILinkifierAccessor, IBufferAccessor, IElement options: ITerminalOptions; buffers: IBufferSet; isFocused: boolean; + mouseHelper: IMouseHelper; /** * Emit the 'data' event and populate the given data. @@ -174,6 +175,11 @@ export interface IBufferSet { activateAltBuffer(): void; } +export interface IMouseHelper { + getCoords(event: {pageX: number, pageY: number}, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number]; + getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number): { x: number, y: number }; +} + export interface IViewport { syncScrollArea(): void; onWheel(ev: WheelEvent): void; diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 28ed15227..23fe729d2 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -3,7 +3,7 @@ * @license MIT */ -import * as Mouse from './utils/Mouse'; +import { MouseHelper } from './utils/MouseHelper'; import * as Browser from './utils/Browser'; import { CharMeasure } from './utils/CharMeasure'; import { CircularList } from './utils/CircularList'; @@ -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._terminal.element, this._charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows, true); + const coords = this._terminal.mouseHelper.getCoords(event, this._terminal.element, this._charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows, true); if (!coords) { return null; } @@ -292,7 +292,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager * @param event The mouse event. */ private _getMouseEventScrollAmount(event: MouseEvent): number { - let offset = Mouse.getCoordsRelativeToElement(event, this._terminal.element)[1]; + let offset = MouseHelper.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; diff --git a/src/Terminal.ts b/src/Terminal.ts index 52a9142a5..8486f2650 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -36,9 +36,8 @@ import { Linkifier } from './Linkifier'; import { SelectionManager } from './SelectionManager'; import { CharMeasure } from './utils/CharMeasure'; import * as Browser from './utils/Browser'; -import * as Mouse from './utils/Mouse'; +import { MouseHelper } from './utils/MouseHelper'; 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, ILinkifier } from './Interfaces'; import { BellSound } from './utils/Sounds'; @@ -200,6 +199,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT private compositionHelper: ICompositionHelper; public charMeasure: CharMeasure; private _mouseZoneManager: IMouseZoneManager; + public mouseHelper: MouseHelper; public cols: number; public rows: number; @@ -652,6 +652,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT }); this.viewportElement.addEventListener('scroll', () => this.selectionManager.refresh()); + this.mouseHelper = new MouseHelper(this.renderer); + // Measure the character size this.charMeasure.measure(this.options); @@ -722,7 +724,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT button = getButton(ev); // get mouse coordinates - pos = getRawByteCoords(ev, self.element, self.charMeasure, self.options.lineHeight, self.cols, self.rows); + pos = self.mouseHelper.getRawByteCoords(ev, self.element, self.charMeasure, self.options.lineHeight, self.cols, self.rows); if (!pos) return; sendEvent(button, pos); @@ -748,7 +750,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.element, self.charMeasure, self.options.lineHeight, self.cols, self.rows); + let pos = self.mouseHelper.getRawByteCoords(ev, self.element, self.charMeasure, self.options.lineHeight, self.cols, self.rows); if (!pos) return; // buttons marked as motions diff --git a/src/input/MouseZoneManager.ts b/src/input/MouseZoneManager.ts index c7feed2a3..f7a1ff61f 100644 --- a/src/input/MouseZoneManager.ts +++ b/src/input/MouseZoneManager.ts @@ -5,7 +5,6 @@ import { IMouseZoneManager, IMouseZone } from './Interfaces'; import { ITerminal } from '../Interfaces'; -import { getCoords } from '../utils/Mouse'; const HOVER_DURATION = 500; @@ -144,7 +143,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 = this._terminal.mouseHelper.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]) { diff --git a/src/renderer/Interfaces.ts b/src/renderer/Interfaces.ts index bb9e36be1..74400b8cc 100644 --- a/src/renderer/Interfaces.ts +++ b/src/renderer/Interfaces.ts @@ -92,4 +92,6 @@ export interface IRenderDimensions { scaledCanvasHeight: number; canvasWidth: number; canvasHeight: number; + actualCellWidth: number; + actualCellHeight: number; } diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 1bdd3b4a0..168d6dc80 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -45,7 +45,9 @@ export class Renderer extends EventEmitter implements IRenderer { scaledCanvasWidth: null, scaledCanvasHeight: null, canvasWidth: null, - canvasHeight: null + canvasHeight: null, + actualCellWidth: null, + actualCellHeight: null }; this._devicePixelRatio = window.devicePixelRatio; } @@ -112,6 +114,15 @@ export class Renderer extends EventEmitter implements IRenderer { this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / window.devicePixelRatio); this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / window.devicePixelRatio); + // Get the _actual_ dimensions of an individual cell. This needs to be + // derived from the canvasWidth/Height calculated above which takes into + // account window.devicePixelRatio. CharMeasure.width/height by itself is + // insufficient when the page is not at 100% zoom level as CharMeasure is + // measured in CSS pixels, but the actual char size on the canvas can + // differ. + this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows; + this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols; + // Resize all render layers this._renderLayers.forEach(l => l.resize(this._terminal, this.dimensions, didCharSizeChange)); diff --git a/src/utils/Mouse.test.ts b/src/utils/Mouse.test.ts deleted file mode 100644 index b96dd68ac..000000000 --- a/src/utils/Mouse.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @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, [10, 10], 'coordinates should never come back as larger than the terminal'); - }); -}); diff --git a/src/utils/Mouse.ts b/src/utils/Mouse.ts deleted file mode 100644 index 66e7f190a..000000000 --- a/src/utils/Mouse.ts +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { ICharMeasure } from '../Interfaces'; - -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; - } - - let x = event.pageX; - let y = event.pageY; - - // Converts the coordinates from being relative to the document to being - // relative to the terminal. - while (element) { - x -= element.offsetLeft; - y -= element.offsetTop; - element = 'offsetParent' in element ? element.offsetParent : element.parentElement; - } - return [x, y]; -} - -/** - * Gets coordinates within the terminal for a particular mouse event. The result - * 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 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. - * @param isSelection Whether the request is for the selection or not. This will - * 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: {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; - } - - const coords = getCoordsRelativeToElement(event, element); - if (!coords) { - return null; - } - - // Convert to cols/rows. - const flooredCharWidth = Math.floor(charMeasure.width); - coords[0] = Math.ceil((coords[0] + (isSelection ? flooredCharWidth / 2 : 0)) / flooredCharWidth); - 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); - coords[1] = Math.min(Math.max(coords[1], 1), rowCount); - - return coords; -} - -/** - * Gets coordinates within the terminal for a particular mouse event, wrapping - * 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 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, 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]; - - // xterm sends raw bytes and starts at 32 (SP) for each. - x += 32; - y += 32; - - return { x, y }; -} diff --git a/src/utils/MouseHelper.test.ts b/src/utils/MouseHelper.test.ts new file mode 100644 index 000000000..ac3137f40 --- /dev/null +++ b/src/utils/MouseHelper.test.ts @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import jsdom = require('jsdom'); +import { assert } from 'chai'; +import { MouseHelper } from './MouseHelper'; +import { MockCharMeasure, MockRenderer } from './TestUtils.test'; + +const CHAR_WIDTH = 10; +const CHAR_HEIGHT = 20; + +describe('MouseHelper.getCoords', () => { + let dom: jsdom.JSDOM; + let window: Window; + let document: Document; + let mouseHelper: MouseHelper; + + 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; + const renderer = new MockRenderer(); + renderer.dimensions = { + actualCellWidth: CHAR_WIDTH, + actualCellHeight: CHAR_HEIGHT + }; + mouseHelper = new MouseHelper(renderer); + }); + + describe('when charMeasure is not initialized', () => { + it('should return null', () => { + charMeasure = new MockCharMeasure(); + assert.equal(mouseHelper.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(mouseHelper.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 = mouseHelper.getCoords({ pageX: CHAR_WIDTH / 2, pageY: CHAR_HEIGHT / 2 }, document.createElement('div'), charMeasure, 1, 10, 10); + assert.deepEqual(coords, [1, 1]); + coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH, pageY: CHAR_HEIGHT }, document.createElement('div'), charMeasure, 1, 10, 10); + assert.deepEqual(coords, [1, 1]); + coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH, pageY: CHAR_HEIGHT + 1 }, document.createElement('div'), charMeasure, 1, 10, 10); + assert.deepEqual(coords, [1, 2]); + coords = mouseHelper.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 = mouseHelper.getCoords({ pageX: -1, pageY: -1 }, document.createElement('div'), charMeasure, 1, 10, 10); + assert.deepEqual(coords, [1, 1]); + // Event are double the cols/rows + coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH * 20, pageY: CHAR_HEIGHT * 20 }, document.createElement('div'), charMeasure, 1, 10, 10); + assert.deepEqual(coords, [10, 10], 'coordinates should never come back as larger than the terminal'); + }); +}); diff --git a/src/utils/MouseHelper.ts b/src/utils/MouseHelper.ts new file mode 100644 index 000000000..75ea675cc --- /dev/null +++ b/src/utils/MouseHelper.ts @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ICharMeasure } from '../Interfaces'; +import { IRenderer } from '../renderer/Interfaces'; + +export class MouseHelper { + constructor(private _renderer: IRenderer) {} + + public static getCoordsRelativeToElement(event: {pageX: number, pageY: number}, element: HTMLElement): [number, number] { + // Ignore browsers that don't support MouseEvent.pageX + if (event.pageX == null) { + return null; + } + + let x = event.pageX; + let y = event.pageY; + + // Converts the coordinates from being relative to the document to being + // relative to the terminal. + while (element) { + x -= element.offsetLeft; + y -= element.offsetTop; + element = 'offsetParent' in element ? element.offsetParent : element.parentElement; + } + return [x, y]; + } + + /** + * Gets coordinates within the terminal for a particular mouse event. The result + * 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 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. + * @param isSelection Whether the request is for the selection or not. This will + * 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. + */ + public 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; + } + + const coords = MouseHelper.getCoordsRelativeToElement(event, element); + if (!coords) { + return null; + } + + coords[0] = Math.ceil((coords[0] + (isSelection ? this._renderer.dimensions.actualCellWidth / 2 : 0)) / this._renderer.dimensions.actualCellWidth); + coords[1] = Math.ceil(coords[1] / this._renderer.dimensions.actualCellHeight); + + // Ensure coordinates are within the terminal viewport. + coords[0] = Math.min(Math.max(coords[0], 1), colCount); + coords[1] = Math.min(Math.max(coords[1], 1), rowCount); + + return coords; + } + + /** + * Gets coordinates within the terminal for a particular mouse event, wrapping + * 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 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. + */ + public getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number): { x: number, y: number } { + const coords = this.getCoords(event, element, charMeasure, lineHeight, colCount, rowCount); + let x = coords[0]; + let y = coords[1]; + + // xterm sends raw bytes and starts at 32 (SP) for each. + x += 32; + y += 32; + + return { x, y }; + } +} diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index beed0aaa0..265733191 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -3,13 +3,14 @@ * @license MIT */ -import { ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, IListenerType, IInputHandlingTerminal, IViewport, ICircularList, ICompositionHelper, ITheme, ILinkifier } from '../Interfaces'; +import { ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, IListenerType, IInputHandlingTerminal, IViewport, ICircularList, ICompositionHelper, ITheme, ILinkifier, IMouseHelper } from '../Interfaces'; import { LineData } from '../Types'; import { Buffer } from '../Buffer'; import * as Browser from './Browser'; import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Interfaces'; export class MockTerminal implements ITerminal { + mouseHelper: IMouseHelper; renderer: IRenderer; linkifier: ILinkifier; isFocused: boolean; From 499c8288ebac90ef656359c62dbdbd70eb685ee1 Mon Sep 17 00:00:00 2001 From: Rick Baker Date: Fri, 29 Sep 2017 14:54:33 -0500 Subject: [PATCH 156/156] Update README.md Submitting an app for your list. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e9fafe142..1551c864d 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,7 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages. - [**Theia**](https://github.com/theia-ide/theia): Theia is a cloud & desktop IDE framework implemented in TypeScript. - +- [**DevOps Helper**](https://github.com/ricktbaker/devops_helper) DevOps Helper tool to make life easier working with AWS instances across multiple organizations. Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it in our list.