From 32a3c9812d517684521db9a0f246e5e0b01c6e45 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 2 Sep 2017 10:12:30 -0700 Subject: [PATCH] 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 7e883598..2650af3a 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 96e4b8fb..cc443b49 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 f4bc1e77..182c9ead 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(); }