Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
bgw committed Feb 21, 2018
1 parent 93e5996 commit 2f3f393
Show file tree
Hide file tree
Showing 11 changed files with 537 additions and 54 deletions.
10 changes: 10 additions & 0 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ <h2>Options</h2>
<p>
<label>tabStopWidth <input type="number" id="option-tabstopwidth" value="8" /></label>
</p>
<p>
<label>
experimentalCharAtlas
<select id="option-experimental-char-atlas">
<option value="static">static</option>
<option value="dynamic">dynamic</option>
<option value="none">none</option>
</select>
</label>
</p>
<div>
<h3>Size</h3>
<div>
Expand Down
4 changes: 4 additions & 0 deletions demo/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ var terminalContainer = document.getElementById('terminal-container'),
macOptionIsMeta: document.querySelector('#option-mac-option-is-meta'),
scrollback: document.querySelector('#option-scrollback'),
tabstopwidth: document.querySelector('#option-tabstopwidth'),
experimentalCharAtlas: document.querySelector('#option-experimental-char-atlas'),
bellStyle: document.querySelector('#option-bell-style'),
screenReaderMode: document.querySelector('#option-screen-reader-mode')
},
Expand Down Expand Up @@ -87,6 +88,9 @@ optionElements.scrollback.addEventListener('change', function () {
optionElements.tabstopwidth.addEventListener('change', function () {
term.setOption('tabStopWidth', parseInt(optionElements.tabstopwidth.value, 10));
});
optionElements.experimentalCharAtlas.addEventListener('change', function () {
term.setOption('experimentalCharAtlas', optionElements.experimentalCharAtlas.value);
});
optionElements.screenReaderMode.addEventListener('change', function () {
term.setOption('screenReaderMode', optionElements.screenReaderMode.checked);
});
Expand Down
2 changes: 2 additions & 0 deletions src/Terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ const DEFAULT_OPTIONS: ITerminalOptions = {
bellSound: BELL_SOUND,
bellStyle: 'none',
enableBold: true,
experimentalCharAtlas: 'static',
fontFamily: 'courier-new, courier, monospace',
fontSize: 15,
fontWeight: 'normal',
Expand Down Expand Up @@ -437,6 +438,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.renderer.clear();
this.charMeasure.measure(this.options);
break;
case 'experimentalCharAtlas':
case 'enableBold':
case 'letterSpacing':
case 'lineHeight':
Expand Down
70 changes: 17 additions & 53 deletions src/renderer/BaseRenderLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@

import { IRenderLayer, IColorSet, IRenderDimensions } from './Types';
import { CharData, ITerminal, ITerminalOptions } from '../Types';
import { acquireCharAtlas, CHAR_ATLAS_CELL_SPACING } from './CharAtlas';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
import { ICharAtlas, INVERTED_DEFAULT_COLOR, DIM_OPACITY } from './atlas/CharAtlasUtils';
import { acquireCharAtlas } from './atlas/CharAtlasCache';

export const INVERTED_DEFAULT_COLOR = -1;
const DIM_OPACITY = 0.5;
export {INVERTED_DEFAULT_COLOR, DIM_OPACITY};

export abstract class BaseRenderLayer implements IRenderLayer {
private _canvas: HTMLCanvasElement;
Expand All @@ -21,7 +21,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
private _scaledCharLeft: number = 0;
private _scaledCharTop: number = 0;

private _charAtlas: HTMLCanvasElement | ImageBitmap;
private _charAtlas: ICharAtlas;

constructor(
private _container: HTMLElement,
Expand Down Expand Up @@ -84,16 +84,11 @@ export abstract class BaseRenderLayer implements IRenderLayer {
if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) {
return;
}
this._charAtlas = null;
const result = acquireCharAtlas(terminal, colorSet, this._scaledCharWidth, this._scaledCharHeight);
if (result instanceof HTMLCanvasElement) {
this._charAtlas = result;
} else {
result.then(bitmap => this._charAtlas = bitmap);
}
this._charAtlas = acquireCharAtlas(terminal, colorSet, this._scaledCharWidth, this._scaledCharHeight);
this._charAtlas.warmUp();
}

public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
public resize(terminal: ITerminal, dim: IRenderDimensions): void {
this._scaledCellWidth = dim.scaledCellWidth;
this._scaledCellHeight = dim.scaledCellHeight;
this._scaledCharWidth = dim.scaledCharWidth;
Expand All @@ -110,9 +105,8 @@ export abstract class BaseRenderLayer implements IRenderLayer {
this.clearAll();
}

if (charSizeChanged) {
this._refreshCharAtlas(terminal, this._colors);
}
// Call this unconditionally. We cache char atlases, so it should be fairly cheap.
this._refreshCharAtlas(terminal, this._colors);
}

public abstract reset(terminal: ITerminal): void;
Expand Down Expand Up @@ -255,46 +249,16 @@ export abstract class BaseRenderLayer implements IRenderLayer {
colorIndex = 1;
}
}
const isAscii = code < 256;
// A color is basic if it is one of the standard normal or bold weight
// colors of the characters held in the char atlas. Note that this excludes
// the normal weight _light_ color characters.
const isBasicColor = (colorIndex > 1 && fg < 16) && (fg < 8 || bold);
const isDefaultColor = fg >= 256;
const isDefaultBackground = bg >= 256;
if (this._charAtlas && isAscii && (isBasicColor || isDefaultColor) && isDefaultBackground) {
// ImageBitmap's draw about twice as fast as from a canvas
const charAtlasCellWidth = this._scaledCharWidth + CHAR_ATLAS_CELL_SPACING;
const charAtlasCellHeight = this._scaledCharHeight + CHAR_ATLAS_CELL_SPACING;

// Apply alpha to dim the character
if (dim) {
this._ctx.globalAlpha = DIM_OPACITY;
}

// Draw the non-bold version of the same color if bold is not enabled
if (bold && !terminal.options.enableBold) {
// Ignore default color as it's not touched above
if (colorIndex > 1) {
colorIndex -= 8;
}
}

this._ctx.drawImage(this._charAtlas,
code * charAtlasCellWidth,
colorIndex * charAtlasCellHeight,
charAtlasCellWidth,
this._scaledCharHeight,
x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop,
charAtlasCellWidth,
this._scaledCharHeight);
} else {
const atlasDidDraw = this._charAtlas && this._charAtlas.draw(
this._ctx,
{char, bg, fg, bold: bold && terminal.options.enableBold, dim},
x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop
);

if (!atlasDidDraw) {
this._drawUncachedChar(terminal, char, width, fg, x, y, bold && terminal.options.enableBold, dim);
}
// This draws the atlas (for debugging purposes)
// this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
// this._ctx.drawImage(this._charAtlas, 0, 0);
}

/**
Expand Down
88 changes: 88 additions & 0 deletions src/renderer/atlas/CharAtlasCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/

import { ITerminal } from '../../Types';
import {
ICharAtlas,
ICharAtlasConfig,
IGlyphIdentifier,
generateConfig,
configEquals,
} from './CharAtlasUtils';
import NoneCharAtlas from './NoneCharAtlas';
import DynamicCharAtlas from './DynamicCharAtlas';
import { IColorSet } from '../Types';

const charAtlasImplementations = {
'none': NoneCharAtlas,
'static': NoneCharAtlas, // TODO: implement
'dynamic': DynamicCharAtlas,
};

interface ICharAtlasCacheEntry {
atlas: ICharAtlas;
config: ICharAtlasConfig;
// N.B. This implementation potentially holds onto copies of the terminal forever, so
// this may cause memory leaks.
ownedBy: ITerminal[];
}

const charAtlasCache: ICharAtlasCacheEntry[] = [];

/**
* Acquires a char atlas, either generating a new one or returning an existing
* one that is in use by another terminal.
*
* @param terminal The terminal.
* @param colors The colors to use.
*/
export function acquireCharAtlas(
terminal: ITerminal,
colors: IColorSet,
scaledCharWidth: number,
scaledCharHeight: number,
): ICharAtlas {
const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, terminal, colors);

// Check to see if the terminal already owns this config
for (let i = 0; i < charAtlasCache.length; i++) {
const entry = charAtlasCache[i];
const ownedByIndex = entry.ownedBy.indexOf(terminal);
if (ownedByIndex >= 0) {
if (configEquals(entry.config, newConfig)) {
return entry.atlas;
} else {
// The configs differ, release the terminal from the entry
if (entry.ownedBy.length === 1) {
charAtlasCache.splice(i, 1);
} else {
entry.ownedBy.splice(ownedByIndex, 1);
}
break;
}
}
}

// Try match a char atlas from the cache
for (let i = 0; i < charAtlasCache.length; i++) {
const entry = charAtlasCache[i];
if (configEquals(entry.config, newConfig)) {
// Add the terminal to the cache entry and return
entry.ownedBy.push(terminal);
return entry.atlas;
}
}

const newEntry: ICharAtlasCacheEntry = {
atlas: new charAtlasImplementations[terminal.options.experimentalCharAtlas](
document,
newConfig,
),
config: newConfig,
ownedBy: [terminal],
};
charAtlasCache.push(newEntry);
return newEntry.atlas;
}
99 changes: 99 additions & 0 deletions src/renderer/atlas/CharAtlasUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/

import { ITerminal } from '../../Types';
import { ITheme } from 'xterm';
import { IColorSet } from '../Types';

export interface IGlyphIdentifier {
char: string;
bg: number;
fg: number;
bold: boolean;
dim: boolean;
}

export const CHAR_ATLAS_CELL_SPACING = 1;
export const INVERTED_DEFAULT_COLOR = -1;
export const DIM_OPACITY = 0.5;

export interface ICharAtlasConfig {
type: 'none' | 'static' | 'dynamic';
devicePixelRatio: number;
fontSize: number;
fontFamily: string;
fontWeight: string;
fontWeightBold: string;
scaledCharWidth: number;
scaledCharHeight: number;
allowTransparency: boolean;
colors: IColorSet;
}

export interface ICharAtlas {
/**
* Perform any work needed to warm the cache before it can be used.
*/
warmUp(): Promise<any>;

/**
* May be called before warmUp finishes, however it is okay for the implementation to
* do nothing and return false in that case.
*
* @param ctx Where to draw the character onto.
* @param glyph Information about what to draw
* @param x The position on the context to start drawing at
* @param y The position on the context to start drawing at
* @returns The success state. True if we drew the character.
*/
draw(
ctx: CanvasRenderingContext2D,
glyph: IGlyphIdentifier,
x: number,
y: number
): boolean;
}

export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: ITerminal, colors: IColorSet): ICharAtlasConfig {
const clonedColors = {
foreground: colors.foreground,
background: colors.background,
cursor: null,
cursorAccent: null,
selection: null,
ansi: colors.ansi.slice(0, 16)
};
return {
type: terminal.options.experimentalCharAtlas,
devicePixelRatio: window.devicePixelRatio,
scaledCharWidth,
scaledCharHeight,
fontFamily: terminal.options.fontFamily,
fontSize: terminal.options.fontSize,
fontWeight: terminal.options.fontWeight,
fontWeightBold: terminal.options.fontWeightBold,
allowTransparency: terminal.options.allowTransparency,
colors: clonedColors
};
}

export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean {
for (let i = 0; i < a.colors.ansi.length; i++) {
if (a.colors.ansi[i] !== b.colors.ansi[i]) {
return false;
}
}
return a.type === b.type &&
a.devicePixelRatio === b.devicePixelRatio &&
a.fontFamily === b.fontFamily &&
a.fontSize === b.fontSize &&
a.fontWeight === b.fontWeight &&
a.fontWeightBold === b.fontWeightBold &&
a.allowTransparency === b.allowTransparency &&
a.scaledCharWidth === b.scaledCharWidth &&
a.scaledCharHeight === b.scaledCharHeight &&
a.colors.foreground === b.colors.foreground &&
a.colors.background === b.colors.background;
}
Loading

0 comments on commit 2f3f393

Please sign in to comment.