From 6c06d83525908d12d0c0ba245cfbfba0115f1868 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 10 Sep 2017 08:01:16 -0700 Subject: [PATCH 01/19] 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 d022dc3e..f6c20018 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 bf080cce..45562a25 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 b9afd0e1..afd5c243 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 b136a13a..bc6fbcb1 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 05f98300..4b0028b8 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 7a4d6940..de2a261b 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 2114bc4837b6cf7dd8b362e5791fc4c0ed762008 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 11 Sep 2017 11:12:51 -0700 Subject: [PATCH 02/19] 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 d1bc8a75..d84b7218 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 03/19] 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 e19887cb..f4d10dc4 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 04/19] 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 38457e13..3bc0a917 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 b610fd69..a2b8a7fe 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 e8fcff86..e5b03f87 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 05/19] 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 8e9e23fc..9334f109 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 06/19] 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 29f12169..28ed1522 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 07/19] Remove log --- src/CompositionHelper.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/CompositionHelper.ts b/src/CompositionHelper.ts index cec8fb37..ca5b4f4b 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 08/19] 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 9334f109..19625466 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 5538481833fe7c0add2ded86eec757267f2bf8de Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 13 Sep 2017 15:00:36 -0700 Subject: [PATCH 09/19] 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 a2b8a7fe..63782afd 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 10/19] 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 3bc0a917..22d42407 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 bf080cce..45835a27 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 8e25d7c1..faed9bfe 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 ad178933..98084034 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 63782afd..8e8934d6 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 5339c7a4..7a0aff83 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 19625466..4b06ef42 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 11/19] 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 22d42407..3f5b2bb6 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 12/19] 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 279aee6d..00000000 --- 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 3f5b2bb6..38e7b9d4 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 4b06ef42..41fcdeee 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 13/19] 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 38e7b9d4..88dfc26c 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 14/19] 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 88dfc26c..d06e0ee7 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 15/19] 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 45835a27..34404e12 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 16/19] 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 d06e0ee7..3f996545 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 17/19] 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 ef4b21eb..44320357 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 3f996545..b55b5aaa 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 b9afd0e1..746970bb 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 faed9bfe..008b5662 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 890b01f5..719ca8cf 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 98084034..8f3b63a5 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 8e8934d6..1bdd3b4a 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 7a0aff83..54eb7fc4 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 41fcdeee..ced4ceaa 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 18/19] 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 b55b5aaa..8f655de5 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 19/19] 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 45cf169a..beed0aaa 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.'); }