mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Add theme support
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
+8
-1
@@ -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.
|
||||
|
||||
@@ -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<number>;
|
||||
|
||||
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<number>();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<ImageBitmap> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 = <number><any>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;
|
||||
}
|
||||
|
||||
@@ -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<CharData>;
|
||||
|
||||
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<CharData>();
|
||||
}
|
||||
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,8 +8,3 @@ export enum FLAGS {
|
||||
INVERSE = 8,
|
||||
INVISIBLE = 16
|
||||
};
|
||||
|
||||
export type Point = {
|
||||
x: number,
|
||||
y: number
|
||||
};
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { ITerminal } from '../Interfaces';
|
||||
import { IColorSet } from '../renderer/Interfaces';
|
||||
|
||||
export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet): Promise<ImageBitmap> {
|
||||
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<ImageBitmap> {
|
||||
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();
|
||||
Reference in New Issue
Block a user