From 62fea4d3edfb7f80f7d23f89253cacef26077fd2 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 1 Sep 2017 15:50:18 -0700 Subject: [PATCH] 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 afa2de58..3a0c9929 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 17fd0d97..a6633235 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 a37c4387..cf122cc2 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 378ac1a1..d131bee0 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 446d440b..00000000 --- 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 00000000..ca8015a2 --- /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 3ccf2ec2..bfec1d8e 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 680f1abc..fa4ef800 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 5da13109..4b8ce816 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 7c6f6d40..653e2cba 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 daaf160d..4ec96764 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 621db0bd..09e74439 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 00000000..a375d468 --- /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();