Merge pull request #2552 from Tyriar/webgl_compat_attr

True color support in WebGL renderer
This commit is contained in:
Daniel Imms
2019-11-10 11:37:26 -08:00
committed by GitHub
16 changed files with 614 additions and 273 deletions
@@ -1,28 +0,0 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { CellData } from 'common/buffer/CellData';
import { FLAGS } from './Constants';
import { IBufferLine } from 'common/Types';
export function getCompatAttr(bufferLine: IBufferLine, index: number): number {
// TODO: Need to move WebGL over to the new system and remove this block
const cell = new CellData();
bufferLine.loadCell(index, cell);
const oldBg = cell.getBgColor() === -1 ? 256 : cell.getBgColor();
const oldFg = cell.getFgColor() === -1 ? 256 : cell.getFgColor();
const oldAttr =
(cell.isBold() ? FLAGS.BOLD : 0) |
(cell.isUnderline() ? FLAGS.UNDERLINE : 0) |
(cell.isBlink() ? FLAGS.BLINK : 0) |
(cell.isInverse() ? FLAGS.INVERSE : 0) |
(cell.isDim() ? FLAGS.DIM : 0) |
(cell.isItalic() ? FLAGS.ITALIC : 0);
const attrCompat =
oldBg |
(oldFg << 9) |
(oldAttr << 18);
return attrCompat;
}
@@ -1,14 +0,0 @@
/**
* @license MIT
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
*/
import { IColor } from 'browser/Types';
export function getLuminance(color: IColor): number {
// Coefficients taken from: https://www.w3.org/TR/AERT/#color-contrast
const r = color.rgba >> 24 & 0xff;
const g = color.rgba >> 16 & 0xff;
const b = color.rgba >> 8 & 0xff;
return (0.299 * r + 0.587 * g + 0.114 * b) / 255;
}
-15
View File
@@ -1,15 +0,0 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
// TODO: Should be removed after chardata workaround is fixed
export const enum FLAGS {
BOLD = 1,
UNDERLINE = 2,
BLINK = 4,
INVERSE = 8,
INVISIBLE = 16,
DIM = 32,
ITALIC = 64
}
+16 -25
View File
@@ -6,12 +6,10 @@
import { createProgram, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils';
import { WebglCharAtlas } from './atlas/WebglCharAtlas';
import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel, IRasterizedGlyph } from './Types';
import { INDICIES_PER_CELL } from './WebglRenderer';
import { COMBINED_CHAR_BIT_MASK } from './RenderModel';
import { COMBINED_CHAR_BIT_MASK, RENDER_MODEL_INDICIES_PER_CELL, RENDER_MODEL_FG_OFFSET } from './RenderModel';
import { fill } from 'common/TypedArrayUtils';
import { slice } from './TypedArray';
import { NULL_CELL_CODE, WHITESPACE_CELL_CODE } from 'common/buffer/Constants';
import { getLuminance } from './ColorUtils';
import { NULL_CELL_CODE, WHITESPACE_CELL_CODE, Attributes } from 'common/buffer/Constants';
import { Terminal, IBufferLine } from 'xterm';
import { IColorSet } from 'browser/Types';
import { IRenderDimensions } from 'browser/renderer/Types';
@@ -169,11 +167,11 @@ export class GlyphRenderer {
return this._atlas ? this._atlas.beginFrame() : true;
}
public updateCell(x: number, y: number, code: number, attr: number, bg: number, fg: number, chars: string): void {
this._updateCell(this._vertices.attributes, x, y, code, attr, bg, fg, chars);
public updateCell(x: number, y: number, code: number, bg: number, fg: number, chars: string): void {
this._updateCell(this._vertices.attributes, x, y, code, bg, fg, chars);
}
private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, attr: number, bg: number, fg: number, chars?: string): void {
private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, bg: number, fg: number, chars?: string): void {
const terminal = this._terminal;
const i = (y * terminal.cols + x) * INDICES_PER_CELL;
@@ -189,9 +187,9 @@ export class GlyphRenderer {
throw new Error('atlas must be set before updating cell');
}
if (chars && chars.length > 1) {
rasterizedGlyph = this._atlas.getRasterizedGlyphCombinedChar(chars, attr, bg, fg);
rasterizedGlyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg);
} else {
rasterizedGlyph = this._atlas.getRasterizedGlyph(code, attr, bg, fg);
rasterizedGlyph = this._atlas.getRasterizedGlyph(code, bg, fg);
}
// Fill empty if no glyph was found
@@ -220,58 +218,51 @@ export class GlyphRenderer {
this._vertices.selectionAttributes = slice(this._vertices.attributes, 0);
// TODO: Make fg and bg configurable, currently since the buffer doesn't
// support truecolor the char atlas cannot store it.
const lumi = getLuminance(this._colors.background);
const fg = lumi > 0.5 ? 7 : 0;
const bg = lumi > 0.5 ? 0 : 7;
const bg = (this._colors.selectionOpaque.rgba >>> 8) | Attributes.CM_RGB;
if (columnSelectMode) {
const startCol = model.selection.startCol;
const width = model.selection.endCol - startCol;
const height = model.selection.viewportCappedEndRow - model.selection.viewportCappedStartRow + 1;
for (let y = model.selection.viewportCappedStartRow; y < model.selection.viewportCappedStartRow + height; y++) {
this._updateSelectionRange(startCol, startCol + width, y, model, bg, fg);
this._updateSelectionRange(startCol, startCol + width, y, model, bg);
}
} else {
// Draw first row
const startCol = model.selection.viewportStartRow === model.selection.viewportCappedStartRow ? model.selection.startCol : 0;
const startRowEndCol = model.selection.viewportCappedStartRow === model.selection.viewportCappedEndRow ? model.selection.endCol : terminal.cols;
this._updateSelectionRange(startCol, startRowEndCol, model.selection.viewportCappedStartRow, model, bg, fg);
this._updateSelectionRange(startCol, startRowEndCol, model.selection.viewportCappedStartRow, model, bg);
// Draw middle rows
const middleRowsCount = Math.max(model.selection.viewportCappedEndRow - model.selection.viewportCappedStartRow - 1, 0);
for (let y = model.selection.viewportCappedStartRow + 1; y <= model.selection.viewportCappedStartRow + middleRowsCount; y++) {
this._updateSelectionRange(0, startRowEndCol, y, model, bg, fg);
this._updateSelectionRange(0, startRowEndCol, y, model, bg);
}
// Draw final row
if (model.selection.viewportCappedStartRow !== model.selection.viewportCappedEndRow) {
// Only draw viewportEndRow if it's not the same as viewportStartRow
const endCol = model.selection.viewportEndRow === model.selection.viewportCappedEndRow ? model.selection.endCol : terminal.cols;
this._updateSelectionRange(0, endCol, model.selection.viewportCappedEndRow, model, bg, fg);
this._updateSelectionRange(0, endCol, model.selection.viewportCappedEndRow, model, bg);
}
}
}
private _updateSelectionRange(startCol: number, endCol: number, y: number, model: IRenderModel, bg: number, fg: number): void {
private _updateSelectionRange(startCol: number, endCol: number, y: number, model: IRenderModel, bg: number): void {
const terminal = this._terminal;
const row = y + terminal.buffer.viewportY;
let line: IBufferLine | undefined;
for (let x = startCol; x < endCol; x++) {
const offset = (y * this._terminal.cols + x) * INDICIES_PER_CELL;
// Because the cache uses attr as a lookup key it needs to contain the selection colors as well
let attr = model.cells[offset + 1];
attr = attr & ~0x3ffff | bg << 9 | fg;
const offset = (y * this._terminal.cols + x) * RENDER_MODEL_INDICIES_PER_CELL;
const code = model.cells[offset];
if (code & COMBINED_CHAR_BIT_MASK) {
if (!line) {
line = terminal.buffer.getLine(row);
}
const chars = line!.getCell(x)!.char;
this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], attr, bg, fg, chars);
this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, model.cells[offset + RENDER_MODEL_FG_OFFSET], chars);
} else {
this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], attr, bg, fg);
this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, model.cells[offset + RENDER_MODEL_FG_OFFSET]);
}
}
}
@@ -6,12 +6,11 @@
import { createProgram, expandFloat32Array, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils';
import { IRenderModel, IWebGLVertexArrayObject, IWebGL2RenderingContext, ISelectionRenderModel } from './Types';
import { fill } from 'common/TypedArrayUtils';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
import { is256Color } from './atlas/CharAtlasUtils';
import { DEFAULT_COLOR } from 'common/buffer/Constants';
import { Attributes, FgFlags } from 'common/buffer/Constants';
import { Terminal } from 'xterm';
import { IColorSet, IColor } from 'browser/Types';
import { IRenderDimensions } from 'browser/renderer/Types';
import { RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
const enum VertexAttribLocations {
POSITION = 0,
@@ -156,7 +155,7 @@ export class RectangleRenderer {
private _updateCachedColors(): void {
this._bgFloat = this._colorToFloat32Array(this._colors.background);
this._selectionFloat = this._colorToFloat32Array(this._colors.selection);
this._selectionFloat = this._colorToFloat32Array(this._colors.selectionOpaque);
}
private _updateViewportRectangle(): void {
@@ -247,47 +246,61 @@ export class RectangleRenderer {
for (let y = 0; y < terminal.rows; y++) {
let currentStartX = -1;
let currentBg = DEFAULT_COLOR;
let currentBg = 0;
let currentFg = 0;
for (let x = 0; x < terminal.cols; x++) {
const modelIndex = ((y * terminal.cols) + x) * 4;
const bg = model.cells[modelIndex + 2];
const modelIndex = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
const bg = model.cells[modelIndex + RENDER_MODEL_BG_OFFSET];
const fg = model.cells[modelIndex + RENDER_MODEL_FG_OFFSET];
if (bg !== currentBg) {
// A rectangle needs to be drawn if going from non-default to another color
if (currentBg !== DEFAULT_COLOR) {
if (currentBg !== 0) {
const offset = rectangleCount++ * INDICES_PER_RECTANGLE;
this._updateRectangle(vertices, offset, currentBg, currentStartX, x, y);
this._updateRectangle(vertices, offset, currentFg, currentBg, currentStartX, x, y);
}
currentStartX = x;
currentBg = bg;
currentFg = fg;
}
}
// Finish rectangle if it's still going
if (currentBg !== DEFAULT_COLOR) {
if (currentBg !== 0) {
const offset = rectangleCount++ * INDICES_PER_RECTANGLE;
this._updateRectangle(vertices, offset, currentBg, currentStartX, terminal.cols, y);
this._updateRectangle(vertices, offset, currentFg, currentBg, currentStartX, terminal.cols, y);
}
}
vertices.count = rectangleCount;
}
private _updateRectangle(vertices: IVertices, offset: number, bg: number, startX: number, endX: number, y: number): void {
let color: IColor | null = null;
if (bg === INVERTED_DEFAULT_COLOR) {
color = this._colors.foreground;
} else if (is256Color(bg)) {
color = this._colors.ansi[bg];
private _updateRectangle(vertices: IVertices, offset: number, fg: number, bg: number, startX: number, endX: number, y: number): void {
let rgba: number | undefined;
const colorMode = bg & Attributes.CM_MASK;
if (fg & FgFlags.INVERSE) {
// Inverted color
rgba = this._colors.foreground.rgba;
} else {
// TODO: Add support for true color
color = this._colors.foreground;
switch (colorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
rgba = this._colors.ansi[bg & Attributes.PCOLOR_MASK].rgba;
break;
case Attributes.CM_RGB:
rgba = (bg & Attributes.RGB_MASK) << 8;
break;
case Attributes.CM_DEFAULT:
default:
rgba = this._colors.background.rgba;
}
}
if (vertices.attributes.length < offset + 4) {
vertices.attributes = expandFloat32Array(vertices.attributes, this._terminal.rows * this._terminal.cols * INDICES_PER_RECTANGLE);
}
const x1 = startX * this._dimensions.scaledCellWidth;
const y1 = y * this._dimensions.scaledCellHeight;
const r = ((color.rgba >> 24) & 0xFF) / 255;
const g = ((color.rgba >> 16) & 0xFF) / 255;
const b = ((color.rgba >> 8 ) & 0xFF) / 255;
const r = ((rgba >> 24) & 0xFF) / 255;
const g = ((rgba >> 16) & 0xFF) / 255;
const b = ((rgba >> 8 ) & 0xFF) / 255;
this._addRectangle(vertices.attributes, offset, x1, y1, (endX - startX) * this._dimensions.scaledCellWidth, this._dimensions.scaledCellHeight, r, g, b, 1);
}
+3 -1
View File
@@ -6,7 +6,9 @@
import { IRenderModel, ISelectionRenderModel } from './Types';
import { fill } from 'common/TypedArrayUtils';
export const RENDER_MODEL_INDICIES_PER_CELL = 4;
export const RENDER_MODEL_INDICIES_PER_CELL = 3;
export const RENDER_MODEL_BG_OFFSET = 1;
export const RENDER_MODEL_FG_OFFSET = 2;
export const COMBINED_CHAR_BIT_MASK = 0x80000000;
+1 -1
View File
@@ -4,7 +4,7 @@
*/
export interface IRasterizedGlyphSet {
[flags: number]: IRasterizedGlyph;
[bg: number]: { [fg: number]: IRasterizedGlyph } | undefined;
}
/**
+232 -35
View File
@@ -7,6 +7,7 @@ import * as puppeteer from 'puppeteer';
import { ITerminalOptions } from '../../../src/Types';
import { ITheme } from 'xterm';
import { assert } from 'chai';
import deepEqual = require('deep-equal');
const APP = 'http://127.0.0.1:3000/test';
@@ -29,7 +30,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
after(async () => browser.close());
beforeEach(async () => page.evaluate(`window.term.reset()`));
it('foreground colors normal', async () => {
it('foreground 0-15', async () => {
const theme: ITheme = {
black: '#010203',
red: '#040506',
@@ -52,30 +53,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]);
});
it('foreground colors bright', async () => {
const theme: ITheme = {
brightBlack: '#010203',
brightRed: '#040506',
brightGreen: '#070809',
brightYellow: '#0a0b0c',
brightBlue: '#0d0e0f',
brightMagenta: '#101112',
brightCyan: '#131415',
brightWhite: '#161718'
};
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
await writeSync(`\\x1b[90m█\\x1b[91m█\\x1b[92m█\\x1b[93m█\\x1b[94m█\\x1b[95m█\\x1b[96m█\\x1b[97m█`);
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]);
await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]);
await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]);
await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]);
await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]);
await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]);
});
it('background colors normal', async () => {
it('background 0-15', async () => {
const theme: ITheme = {
black: '#010203',
red: '#040506',
@@ -98,7 +76,30 @@ describe('WebGL Renderer Integration Tests', function(): void {
await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]);
});
it('background colors bright', async () => {
it('foreground 0-15 bright', async () => {
const theme: ITheme = {
brightBlack: '#010203',
brightRed: '#040506',
brightGreen: '#070809',
brightYellow: '#0a0b0c',
brightBlue: '#0d0e0f',
brightMagenta: '#101112',
brightCyan: '#131415',
brightWhite: '#161718'
};
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
await writeSync(`\\x1b[90m█\\x1b[91m█\\x1b[92m█\\x1b[93m█\\x1b[94m█\\x1b[95m█\\x1b[96m█\\x1b[97m█`);
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]);
await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]);
await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]);
await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]);
await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]);
await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]);
});
it('background 0-15 bright', async () => {
const theme: ITheme = {
brightBlack: '#010203',
brightRed: '#040506',
@@ -120,6 +121,190 @@ describe('WebGL Renderer Integration Tests', function(): void {
await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]);
await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]);
});
it('foreground 16-255', async () => {
let data = '';
for (let y = 0; y < 240 / 16; y++) {
for (let x = 0; x < 16; x++) {
data += `\\x1b[38;5;${16 + y * 16 + x}m█\x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(data);
for (let y = 0; y < 240 / 16; y++) {
for (let x = 0; x < 16; x++) {
const cssColor = COLORS_16_TO_255[y * 16 + x];
const r = parseInt(cssColor.substr(1, 2), 16);
const g = parseInt(cssColor.substr(3, 2), 16);
const b = parseInt(cssColor.substr(5, 2), 16);
await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]);
}
}
});
it('background 16-255', async () => {
let data = '';
for (let y = 0; y < 240 / 16; y++) {
for (let x = 0; x < 16; x++) {
data += `\\x1b[48;5;${16 + y * 16 + x}m \x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(data);
for (let y = 0; y < 240 / 16; y++) {
for (let x = 0; x < 16; x++) {
const cssColor = COLORS_16_TO_255[y * 16 + x];
const r = parseInt(cssColor.substr(1, 2), 16);
const g = parseInt(cssColor.substr(3, 2), 16);
const b = parseInt(cssColor.substr(5, 2), 16);
await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]);
}
}
});
it('foreground true color red', async () => {
let data = '';
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[38;2;${i};0;0m█\x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(data);
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
await pollFor(page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]);
}
}
});
it('background true color red', async () => {
let data = '';
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[48;2;${i};0;0m \x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(data);
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
await pollFor(page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]);
}
}
});
it('foreground true color green', async () => {
let data = '';
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[38;2;0;${i};0m█\x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(data);
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
await pollFor(page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]);
}
}
});
it('background true color green', async () => {
let data = '';
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[48;2;0;${i};0m \x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(data);
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
await pollFor(page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]);
}
}
});
it('foreground true color blue', async () => {
let data = '';
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[38;2;0;0;${i}m█\x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(data);
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
await pollFor(page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]);
}
}
});
it('background true color blue', async () => {
let data = '';
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[48;2;0;0;${i}m \x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(data);
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
await pollFor(page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]);
}
}
});
it('foreground true color grey', async () => {
let data = '';
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[38;2;${i};${i};${i}m█\x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(data);
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
await pollFor(page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]);
}
}
});
it('background true color grey', async () => {
let data = '';
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[48;2;${i};${i};${i}m \x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(data);
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
await pollFor(page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]);
}
}
});
});
});
@@ -168,20 +353,32 @@ async function setupBrowser(): Promise<void> {
`);
}
async function pollFor<T>(page: puppeteer.Page, evalOrFn: string | (() => Promise<T>), val: T, preFn?: () => Promise<void>): Promise<void> {
export async function pollFor<T>(page: puppeteer.Page, evalOrFn: string | (() => Promise<T>), val: T, preFn?: () => Promise<void>): Promise<void> {
if (preFn) {
await preFn();
}
const result = typeof evalOrFn === 'string' ? await page.evaluate(evalOrFn) : await evalOrFn();
let equal = false;
if (typeof result === 'object') {
equal = Object.keys(result).every(e => result[e] === (val as any)[e]);
} else {
equal = result === val;
}
if (!equal) {
if (!deepEqual(result, val)) {
return new Promise<void>(r => {
setTimeout(() => r(pollFor(page, evalOrFn, val, preFn)), 10);
setTimeout(() => r(pollFor(page, evalOrFn, val, preFn)), 1);
});
}
}
const COLORS_16_TO_255 = [
'#000000', '#00005f', '#000087', '#0000af', '#0000d7', '#0000ff', '#005f00', '#005f5f', '#005f87', '#005faf', '#005fd7', '#005fff', '#008700', '#00875f', '#008787', '#0087af',
'#0087d7', '#0087ff', '#00af00', '#00af5f', '#00af87', '#00afaf', '#00afd7', '#00afff', '#00d700', '#00d75f', '#00d787', '#00d7af', '#00d7d7', '#00d7ff', '#00ff00', '#00ff5f',
'#00ff87', '#00ffaf', '#00ffd7', '#00ffff', '#5f0000', '#5f005f', '#5f0087', '#5f00af', '#5f00d7', '#5f00ff', '#5f5f00', '#5f5f5f', '#5f5f87', '#5f5faf', '#5f5fd7', '#5f5fff',
'#5f8700', '#5f875f', '#5f8787', '#5f87af', '#5f87d7', '#5f87ff', '#5faf00', '#5faf5f', '#5faf87', '#5fafaf', '#5fafd7', '#5fafff', '#5fd700', '#5fd75f', '#5fd787', '#5fd7af',
'#5fd7d7', '#5fd7ff', '#5fff00', '#5fff5f', '#5fff87', '#5fffaf', '#5fffd7', '#5fffff', '#870000', '#87005f', '#870087', '#8700af', '#8700d7', '#8700ff', '#875f00', '#875f5f',
'#875f87', '#875faf', '#875fd7', '#875fff', '#878700', '#87875f', '#878787', '#8787af', '#8787d7', '#8787ff', '#87af00', '#87af5f', '#87af87', '#87afaf', '#87afd7', '#87afff',
'#87d700', '#87d75f', '#87d787', '#87d7af', '#87d7d7', '#87d7ff', '#87ff00', '#87ff5f', '#87ff87', '#87ffaf', '#87ffd7', '#87ffff', '#af0000', '#af005f', '#af0087', '#af00af',
'#af00d7', '#af00ff', '#af5f00', '#af5f5f', '#af5f87', '#af5faf', '#af5fd7', '#af5fff', '#af8700', '#af875f', '#af8787', '#af87af', '#af87d7', '#af87ff', '#afaf00', '#afaf5f',
'#afaf87', '#afafaf', '#afafd7', '#afafff', '#afd700', '#afd75f', '#afd787', '#afd7af', '#afd7d7', '#afd7ff', '#afff00', '#afff5f', '#afff87', '#afffaf', '#afffd7', '#afffff',
'#d70000', '#d7005f', '#d70087', '#d700af', '#d700d7', '#d700ff', '#d75f00', '#d75f5f', '#d75f87', '#d75faf', '#d75fd7', '#d75fff', '#d78700', '#d7875f', '#d78787', '#d787af',
'#d787d7', '#d787ff', '#d7af00', '#d7af5f', '#d7af87', '#d7afaf', '#d7afd7', '#d7afff', '#d7d700', '#d7d75f', '#d7d787', '#d7d7af', '#d7d7d7', '#d7d7ff', '#d7ff00', '#d7ff5f',
'#d7ff87', '#d7ffaf', '#d7ffd7', '#d7ffff', '#ff0000', '#ff005f', '#ff0087', '#ff00af', '#ff00d7', '#ff00ff', '#ff5f00', '#ff5f5f', '#ff5f87', '#ff5faf', '#ff5fd7', '#ff5fff',
'#ff8700', '#ff875f', '#ff8787', '#ff87af', '#ff87d7', '#ff87ff', '#ffaf00', '#ffaf5f', '#ffaf87', '#ffafaf', '#ffafd7', '#ffafff', '#ffd700', '#ffd75f', '#ffd787', '#ffd7af',
'#ffd7d7', '#ffd7ff', '#ffff00', '#ffff5f', '#ffff87', '#ffffaf', '#ffffd7', '#ffffff', '#080808', '#121212', '#1c1c1c', '#262626', '#303030', '#3a3a3a', '#444444', '#4e4e4e',
'#585858', '#626262', '#6c6c6c', '#767676', '#808080', '#8a8a8a', '#949494', '#9e9e9e', '#a8a8a8', '#b2b2b2', '#bcbcbc', '#c6c6c6', '#d0d0d0', '#dadada', '#e4e4e4', '#eeeeee'
];
+27 -39
View File
@@ -12,19 +12,15 @@ import { WebglCharAtlas } from './atlas/WebglCharAtlas';
import { RectangleRenderer } from './RectangleRenderer';
import { IWebGL2RenderingContext } from './Types';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
import { RenderModel, COMBINED_CHAR_BIT_MASK } from './RenderModel';
import { RenderModel, COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
import { Disposable } from 'common/Lifecycle';
import { DEFAULT_COLOR, CHAR_DATA_CHAR_INDEX, CHAR_DATA_CODE_INDEX, NULL_CELL_CODE } from 'common/buffer/Constants';
import { DEFAULT_COLOR, NULL_CELL_CODE, FgFlags } from 'common/buffer/Constants';
import { Terminal, IEvent } from 'xterm';
import { getLuminance } from './ColorUtils';
import { IRenderLayer } from './renderLayer/Types';
import { IRenderDimensions, IRenderer, IRequestRefreshRowsEvent } from 'browser/renderer/Types';
import { IColorSet } from 'browser/Types';
import { FLAGS } from './Constants';
import { getCompatAttr } from './CharDataCompat';
import { EventEmitter } from 'common/EventEmitter';
export const INDICIES_PER_CELL = 4;
import { CellData } from 'common/buffer/CellData';
export class WebglRenderer extends Disposable implements IRenderer {
private _renderLayers: IRenderLayer[];
@@ -32,6 +28,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
private _devicePixelRatio: number;
private _model: RenderModel = new RenderModel();
private _workCell: CellData = new CellData();
private _canvas: HTMLCanvasElement;
private _gl: IWebGL2RenderingContext;
@@ -54,8 +51,6 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._core = (<any>this._terminal)._core;
this._applyBgLuminanceBasedSelection();
this._renderLayers = [
new LinkRenderLayer(this._core.screenElement, 2, this._colors, this._core),
new CursorRenderLayer(this._core.screenElement, 3, this._colors, this._onRequestRefreshRows)
@@ -103,20 +98,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
super.dispose();
}
private _applyBgLuminanceBasedSelection(): void {
// HACK: This is needed until webgl renderer adds support for selection colors
if (getLuminance(this._colors.background) > 0.5) {
this._colors.selection = { css: '#000', rgba: 255 };
} else {
this._colors.selection = { css: '#fff', rgba: 4294967295 };
}
}
public setColors(colors: IColorSet): void {
this._colors = colors;
this._applyBgLuminanceBasedSelection();
// Clear layers and force a full render
this._renderLayers.forEach(l => {
l.setColors(this._terminal, this._colors);
@@ -258,28 +242,29 @@ export class WebglRenderer extends Disposable implements IRenderer {
const line = terminal.buffer.lines.get(row)!;
this._model.lineLengths[y] = 0;
for (let x = 0; x < terminal.cols; x++) {
const charData = line.get(x);
const chars = charData[CHAR_DATA_CHAR_INDEX];
let code = charData[CHAR_DATA_CODE_INDEX];
const attr = getCompatAttr(line, x); // charData[CHAR_DATA_ATTR_INDEX];
const i = ((y * terminal.cols) + x) * INDICIES_PER_CELL;
line.loadCell(x, this._workCell);
const chars = this._workCell.getChars();
let code = this._workCell.getCode();
const i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
if (code !== NULL_CELL_CODE) {
this._model.lineLengths[y] = x + 1;
}
// Resolve bg and fg
let bg = this._workCell.bg;
let fg = this._workCell.fg;
// Nothing has changed, no updates needed
if (this._model.cells[i] === code && this._model.cells[i + 1] === attr) {
if (this._model.cells[i] === code &&
this._model.cells[i + RENDER_MODEL_BG_OFFSET] === bg &&
this._model.cells[i + RENDER_MODEL_FG_OFFSET] === fg) {
continue;
}
// Resolve bg and fg and cache in the model
const flags = attr >> 18;
let bg = attr & 0x1ff;
let fg = (attr >> 9) & 0x1ff;
// If inverse flag is on, the foreground should become the background.
if (flags & FLAGS.INVERSE) {
if (this._workCell.isInverse()) {
const temp = bg;
bg = fg;
fg = temp;
@@ -290,20 +275,23 @@ export class WebglRenderer extends Disposable implements IRenderer {
bg = INVERTED_DEFAULT_COLOR;
}
}
const drawInBrightColor = terminal.options.drawBoldTextInBrightColors && !!(flags & FLAGS.BOLD) && fg < 8 && fg !== INVERTED_DEFAULT_COLOR;
fg += drawInBrightColor ? 8 : 0;
// Apply drawBoldTextInBrightColors
if (terminal.options.drawBoldTextInBrightColors && this._workCell.isBold() && fg & FgFlags.BOLD && this._workCell.getFgColor() < 8) {
fg += 8;
}
// Flag combined chars with a bit mask so they're easily identifiable
if (chars.length > 1) {
code = code | COMBINED_CHAR_BIT_MASK;
}
this._model.cells[i ] = code;
this._model.cells[i + 1] = attr;
this._model.cells[i + 2] = bg;
this._model.cells[i + 3] = fg;
// Cache the results in the model
this._model.cells[i] = code;
this._model.cells[i + RENDER_MODEL_BG_OFFSET] = bg;
this._model.cells[i + RENDER_MODEL_FG_OFFSET] = fg;
this._glyphRenderer.updateCell(x, y, code, attr, bg, fg, chars);
this._glyphRenderer.updateCell(x, y, code, bg, fg, chars);
}
}
this._rectangleRenderer.updateBackgrounds(this._model);
@@ -4,7 +4,7 @@
*/
import { ICharAtlasConfig } from './Types';
import { DEFAULT_COLOR } from 'common/buffer/Constants';
import { Attributes } from 'common/buffer/Constants';
import { Terminal, FontWeight } from 'xterm';
import { IColorSet, IColor } from 'browser/Types';
@@ -21,6 +21,7 @@ export function generateConfig(scaledCharWidth: number, scaledCharHeight: number
cursor: NULL_COLOR,
cursorAccent: NULL_COLOR,
selection: NULL_COLOR,
selectionOpaque: NULL_COLOR,
// For the static char atlas, we only use the first 16 colors, but we need all 256 for the
// dynamic character atlas.
ansi: colors.ansi.slice()
@@ -57,5 +58,5 @@ export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean
}
export function is256Color(colorCode: number): boolean {
return colorCode < DEFAULT_COLOR;
return (colorCode & Attributes.CM_MASK) === Attributes.CM_P16 || (colorCode & Attributes.CM_MASK) === Attributes.CM_P256;
}
@@ -4,14 +4,13 @@
*/
import { ICharAtlasConfig } from './Types';
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
import { DIM_OPACITY } from 'browser/renderer/atlas/Constants';
import { IRasterizedGlyph, IBoundingBox, IRasterizedGlyphSet } from '../Types';
import { DEFAULT_COLOR, DEFAULT_ATTR } from 'common/buffer/Constants';
import { is256Color } from './CharAtlasUtils';
import { DEFAULT_COLOR, FgFlags, Attributes, BgFlags } from 'common/buffer/Constants';
import { throwIfFalsy } from '../WebglUtils';
import { IColor } from 'browser/Types';
import { FLAGS } from '../Constants';
import { IDisposable } from 'xterm';
import { AttributeData } from 'common/buffer/AttributeData';
// In practice we're probably never going to exhaust a texture this large. For debugging purposes,
// however, it can be useful to set this to a really tiny value, to verify that LRU eviction works.
@@ -103,9 +102,11 @@ export class WebglCharAtlas implements IDisposable {
protected _doWarmUp(): void {
// Pre-fill with ASCII 33-126
for (let i = 33; i < 126; i++) {
const rasterizedGlyph = this._drawToCache(i, DEFAULT_ATTR, DEFAULT_COLOR, DEFAULT_COLOR);
const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR);
this._cacheMap[i] = {
[DEFAULT_ATTR]: rasterizedGlyph
[DEFAULT_COLOR]: {
[DEFAULT_COLOR]: rasterizedGlyph
}
};
}
}
@@ -123,16 +124,23 @@ export class WebglCharAtlas implements IDisposable {
return false;
}
public getRasterizedGlyphCombinedChar(chars: string, attr: number, bg: number, fg: number): IRasterizedGlyph {
public getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number): IRasterizedGlyph {
let rasterizedGlyphSet = this._cacheMapCombined[chars];
if (!rasterizedGlyphSet) {
rasterizedGlyphSet = {};
this._cacheMapCombined[chars] = rasterizedGlyphSet;
}
let rasterizedGlyph = rasterizedGlyphSet[attr];
let rasterizedGlyph: IRasterizedGlyph | undefined;
const rasterizedGlyphSetBg = rasterizedGlyphSet[bg];
if (rasterizedGlyphSetBg) {
rasterizedGlyph = rasterizedGlyphSetBg[fg];
}
if (!rasterizedGlyph) {
rasterizedGlyph = this._drawToCache(chars, attr, bg, fg);
rasterizedGlyphSet[attr] = rasterizedGlyph;
rasterizedGlyph = this._drawToCache(chars, bg, fg);
if (!rasterizedGlyphSet[bg]) {
rasterizedGlyphSet[bg] = {};
}
rasterizedGlyphSet[bg]![fg] = rasterizedGlyph;
}
return rasterizedGlyph;
}
@@ -140,16 +148,23 @@ export class WebglCharAtlas implements IDisposable {
/**
* Gets the glyphs texture coords, drawing the texture if it's not already
*/
public getRasterizedGlyph(code: number, attr: number, bg: number, fg: number): IRasterizedGlyph {
public getRasterizedGlyph(code: number, bg: number, fg: number): IRasterizedGlyph {
let rasterizedGlyphSet = this._cacheMap[code];
if (!rasterizedGlyphSet) {
rasterizedGlyphSet = {};
this._cacheMap[code] = rasterizedGlyphSet;
}
let rasterizedGlyph = rasterizedGlyphSet[attr];
let rasterizedGlyph: IRasterizedGlyph | undefined;
const rasterizedGlyphSetBg = rasterizedGlyphSet[bg];
if (rasterizedGlyphSetBg) {
rasterizedGlyph = rasterizedGlyphSetBg[fg];
}
if (!rasterizedGlyph) {
rasterizedGlyph = this._drawToCache(code, attr, bg, fg);
rasterizedGlyphSet[attr] = rasterizedGlyph;
rasterizedGlyph = this._drawToCache(code, bg, fg);
if (!rasterizedGlyphSet[bg]) {
rasterizedGlyphSet[bg] = {};
}
rasterizedGlyphSet[bg]![fg] = rasterizedGlyph;
}
return rasterizedGlyph;
}
@@ -161,48 +176,70 @@ export class WebglCharAtlas implements IDisposable {
return this._config.colors.ansi[idx];
}
private _getBackgroundColor(bg: number): IColor {
private _getBackgroundColor(bg: number, fg: number): IColor {
if (this._config.allowTransparency) {
// The background color might have some transparency, so we need to render it as fully
// transparent in the atlas. Otherwise we'd end up drawing the transparent background twice
// around the anti-aliased edges of the glyph, and it would look too dark.
return TRANSPARENT_COLOR;
} else if (bg === INVERTED_DEFAULT_COLOR) {
} else if (fg & FgFlags.INVERSE) {
return this._config.colors.foreground;
} else if (is256Color(bg)) {
return this._getColorFromAnsiIndex(bg);
}
// TODO: Support true color
return this._config.colors.background;
const colorMode = bg & Attributes.CM_MASK;
switch (colorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
return this._getColorFromAnsiIndex(bg & Attributes.PCOLOR_MASK);
case Attributes.CM_RGB:
const rgb = bg & Attributes.RGB_MASK;
const arr = AttributeData.toColorRGB(rgb);
// TODO: This object creation is slow
return {
rgba: rgb << 8,
css: `#${toPaddedHex(arr[0])}${toPaddedHex(arr[1])}${toPaddedHex(arr[2])}`
};
case Attributes.CM_DEFAULT:
default:
return this._config.colors.background;
}
}
private _getForegroundColor(fg: number): IColor {
if (fg === INVERTED_DEFAULT_COLOR) {
return this._config.colors.background;
} else if (is256Color(fg)) {
return this._getColorFromAnsiIndex(fg);
private _getForegroundCss(fg: number): string {
if (fg & FgFlags.INVERSE) {
return this._config.colors.background.css;
}
const colorMode = fg & Attributes.CM_MASK;
switch (colorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
return this._getColorFromAnsiIndex(fg & Attributes.PCOLOR_MASK).css;
case Attributes.CM_RGB:
const rgb = fg & Attributes.RGB_MASK;
const arr = AttributeData.toColorRGB(rgb);
return `#${toPaddedHex(arr[0])}${toPaddedHex(arr[1])}${toPaddedHex(arr[2])}`;
case Attributes.CM_DEFAULT:
default:
return this._config.colors.foreground.css;
}
// TODO: Support true color
return this._config.colors.foreground;
}
private _drawToCache(code: number, attr: number, bg: number, fg: number): IRasterizedGlyph;
private _drawToCache(chars: string, attr: number, bg: number, fg: number): IRasterizedGlyph;
private _drawToCache(codeOrChars: number | string, attr: number, bg: number, fg: number): IRasterizedGlyph {
private _drawToCache(code: number, bg: number, fg: number): IRasterizedGlyph;
private _drawToCache(chars: string, bg: number, fg: number): IRasterizedGlyph;
private _drawToCache(codeOrChars: number | string, bg: number, fg: number): IRasterizedGlyph {
const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars;
this.hasCanvasChanged = true;
const flags = attr >> 18;
const bold = !!(flags & FLAGS.BOLD);
const dim = !!(flags & FLAGS.DIM);
const italic = !!(flags & FLAGS.ITALIC);
const bold = !!(fg & FgFlags.BOLD);
const dim = !!(bg & BgFlags.DIM);
const italic = !!(bg & BgFlags.ITALIC);
this._tmpCtx.save();
// draw the background
const backgroundColor = this._getBackgroundColor(bg);
const backgroundColor = this._getBackgroundColor(bg, fg);
// Use a 'copy' composite operation to clear any existing glyph out of _tmpCtxWithAlpha, regardless of
// transparency in backgroundColor
this._tmpCtx.globalCompositeOperation = 'copy';
@@ -217,7 +254,7 @@ export class WebglCharAtlas implements IDisposable {
`${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`;
this._tmpCtx.textBaseline = 'top';
this._tmpCtx.fillStyle = this._getForegroundColor(fg).css;
this._tmpCtx.fillStyle = this._getForegroundCss(fg);
// Apply alpha to dim the character
if (dim) {
@@ -398,3 +435,8 @@ function clearColor(imageData: ImageData, color: IColor): boolean {
}
return isEmpty;
}
function toPaddedHex(c: number): string {
const s = c.toString(16);
return s.length < 2 ? '0' + s : s;
}
@@ -4,7 +4,6 @@
*/
import { IRenderLayer } from './Types';
import { IGlyphIdentifier } from '../atlas/Types';
import { acquireCharAtlas } from '../atlas/CharAtlasCache';
import { Terminal } from 'xterm';
import { IColorSet } from 'browser/Types';
@@ -25,19 +24,6 @@ export abstract class BaseRenderLayer implements IRenderLayer {
protected _charAtlas: WebglCharAtlas | undefined;
/**
* An object that's reused when drawing glyphs in order to reduce GC.
*/
private _currentGlyphIdentifier: IGlyphIdentifier = {
chars: '',
code: 0,
bg: 0,
fg: 0,
bold: false,
dim: false,
italic: false
};
constructor(
private _container: HTMLElement,
id: string,
+138
View File
@@ -0,0 +1,138 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { blend, fromCss, toPaddedHex, toCss, toRgba } from 'browser/Color';
describe('Color', () => {
describe('blend', () => {
it('should blend colors based on the alpha channel', () => {
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF00', rgba: 0xFFFFFF00 }), { css: '#000000', rgba: 0x000000FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF10', rgba: 0xFFFFFF10 }), { css: '#101010', rgba: 0x101010FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF20', rgba: 0xFFFFFF20 }), { css: '#202020', rgba: 0x202020FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF30', rgba: 0xFFFFFF30 }), { css: '#303030', rgba: 0x303030FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF40', rgba: 0xFFFFFF40 }), { css: '#404040', rgba: 0x404040FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF50', rgba: 0xFFFFFF50 }), { css: '#505050', rgba: 0x505050FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF60', rgba: 0xFFFFFF60 }), { css: '#606060', rgba: 0x606060FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF70', rgba: 0xFFFFFF70 }), { css: '#707070', rgba: 0x707070FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF80', rgba: 0xFFFFFF80 }), { css: '#808080', rgba: 0x808080FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF90', rgba: 0xFFFFFF90 }), { css: '#909090', rgba: 0x909090FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFA0', rgba: 0xFFFFFFA0 }), { css: '#a0a0a0', rgba: 0xA0A0A0FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFB0', rgba: 0xFFFFFFB0 }), { css: '#b0b0b0', rgba: 0xB0B0B0FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFC0', rgba: 0xFFFFFFC0 }), { css: '#c0c0c0', rgba: 0xC0C0C0FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFD0', rgba: 0xFFFFFFD0 }), { css: '#d0d0d0', rgba: 0xD0D0D0FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFE0', rgba: 0xFFFFFFE0 }), { css: '#e0e0e0', rgba: 0xE0E0E0FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFF0', rgba: 0xFFFFFFF0 }), { css: '#f0f0f0', rgba: 0xF0F0F0FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFFF', rgba: 0xFFFFFFFF }), { css: '#FFFFFFFF', rgba: 0xFFFFFFFF });
});
});
describe('fromCss', () => {
it('should covert a CSS string to an IColor', () => {
assert.deepEqual(fromCss('#000000'), { css: '#000000', rgba: 0x000000FF });
assert.deepEqual(fromCss('#101010'), { css: '#101010', rgba: 0x101010FF });
assert.deepEqual(fromCss('#202020'), { css: '#202020', rgba: 0x202020FF });
assert.deepEqual(fromCss('#303030'), { css: '#303030', rgba: 0x303030FF });
assert.deepEqual(fromCss('#404040'), { css: '#404040', rgba: 0x404040FF });
assert.deepEqual(fromCss('#505050'), { css: '#505050', rgba: 0x505050FF });
assert.deepEqual(fromCss('#606060'), { css: '#606060', rgba: 0x606060FF });
assert.deepEqual(fromCss('#707070'), { css: '#707070', rgba: 0x707070FF });
assert.deepEqual(fromCss('#808080'), { css: '#808080', rgba: 0x808080FF });
assert.deepEqual(fromCss('#909090'), { css: '#909090', rgba: 0x909090FF });
assert.deepEqual(fromCss('#a0a0a0'), { css: '#a0a0a0', rgba: 0xa0a0a0FF });
assert.deepEqual(fromCss('#b0b0b0'), { css: '#b0b0b0', rgba: 0xb0b0b0FF });
assert.deepEqual(fromCss('#c0c0c0'), { css: '#c0c0c0', rgba: 0xc0c0c0FF });
assert.deepEqual(fromCss('#d0d0d0'), { css: '#d0d0d0', rgba: 0xd0d0d0FF });
assert.deepEqual(fromCss('#e0e0e0'), { css: '#e0e0e0', rgba: 0xe0e0e0FF });
assert.deepEqual(fromCss('#f0f0f0'), { css: '#f0f0f0', rgba: 0xf0f0f0FF });
assert.deepEqual(fromCss('#ffffff'), { css: '#ffffff', rgba: 0xffffffFF });
});
});
describe('toPaddedHex', () => {
it('should convert numbers to 2-digit hex values', () => {
assert.equal(toPaddedHex(0x00), '00');
assert.equal(toPaddedHex(0x10), '10');
assert.equal(toPaddedHex(0x20), '20');
assert.equal(toPaddedHex(0x30), '30');
assert.equal(toPaddedHex(0x40), '40');
assert.equal(toPaddedHex(0x50), '50');
assert.equal(toPaddedHex(0x60), '60');
assert.equal(toPaddedHex(0x70), '70');
assert.equal(toPaddedHex(0x80), '80');
assert.equal(toPaddedHex(0x90), '90');
assert.equal(toPaddedHex(0xa0), 'a0');
assert.equal(toPaddedHex(0xb0), 'b0');
assert.equal(toPaddedHex(0xc0), 'c0');
assert.equal(toPaddedHex(0xd0), 'd0');
assert.equal(toPaddedHex(0xe0), 'e0');
assert.equal(toPaddedHex(0xf0), 'f0');
assert.equal(toPaddedHex(0xff), 'ff');
});
});
describe('toCss', () => {
it('should convert an rgb array to css hex string', () => {
assert.equal(toCss(0x00, 0x00, 0x00), '#000000');
assert.equal(toCss(0x10, 0x10, 0x10), '#101010');
assert.equal(toCss(0x20, 0x20, 0x20), '#202020');
assert.equal(toCss(0x30, 0x30, 0x30), '#303030');
assert.equal(toCss(0x40, 0x40, 0x40), '#404040');
assert.equal(toCss(0x50, 0x50, 0x50), '#505050');
assert.equal(toCss(0x60, 0x60, 0x60), '#606060');
assert.equal(toCss(0x70, 0x70, 0x70), '#707070');
assert.equal(toCss(0x80, 0x80, 0x80), '#808080');
assert.equal(toCss(0x90, 0x90, 0x90), '#909090');
assert.equal(toCss(0xa0, 0xa0, 0xa0), '#a0a0a0');
assert.equal(toCss(0xb0, 0xb0, 0xb0), '#b0b0b0');
assert.equal(toCss(0xc0, 0xc0, 0xc0), '#c0c0c0');
assert.equal(toCss(0xd0, 0xd0, 0xd0), '#d0d0d0');
assert.equal(toCss(0xe0, 0xe0, 0xe0), '#e0e0e0');
assert.equal(toCss(0xf0, 0xf0, 0xf0), '#f0f0f0');
assert.equal(toCss(0xff, 0xff, 0xff), '#ffffff');
});
});
describe('toRgba', () => {
it('should convert an rgb array to an rgba number', () => {
assert.equal(toRgba(0x00, 0x00, 0x00), 0x000000FF);
assert.equal(toRgba(0x10, 0x10, 0x10), 0x101010FF);
assert.equal(toRgba(0x20, 0x20, 0x20), 0x202020FF);
assert.equal(toRgba(0x30, 0x30, 0x30), 0x303030FF);
assert.equal(toRgba(0x40, 0x40, 0x40), 0x404040FF);
assert.equal(toRgba(0x50, 0x50, 0x50), 0x505050FF);
assert.equal(toRgba(0x60, 0x60, 0x60), 0x606060FF);
assert.equal(toRgba(0x70, 0x70, 0x70), 0x707070FF);
assert.equal(toRgba(0x80, 0x80, 0x80), 0x808080FF);
assert.equal(toRgba(0x90, 0x90, 0x90), 0x909090FF);
assert.equal(toRgba(0xa0, 0xa0, 0xa0), 0xa0a0a0FF);
assert.equal(toRgba(0xb0, 0xb0, 0xb0), 0xb0b0b0FF);
assert.equal(toRgba(0xc0, 0xc0, 0xc0), 0xc0c0c0FF);
assert.equal(toRgba(0xd0, 0xd0, 0xd0), 0xd0d0d0FF);
assert.equal(toRgba(0xe0, 0xe0, 0xe0), 0xe0e0e0FF);
assert.equal(toRgba(0xf0, 0xf0, 0xf0), 0xf0f0f0FF);
assert.equal(toRgba(0xff, 0xff, 0xff), 0xffffffFF);
});
it('should convert an rgba array to an rgba number', () => {
assert.equal(toRgba(0x00, 0x00, 0x00, 0x00), 0x00000000);
assert.equal(toRgba(0x10, 0x10, 0x10, 0x10), 0x10101010);
assert.equal(toRgba(0x20, 0x20, 0x20, 0x20), 0x20202020);
assert.equal(toRgba(0x30, 0x30, 0x30, 0x30), 0x30303030);
assert.equal(toRgba(0x40, 0x40, 0x40, 0x40), 0x40404040);
assert.equal(toRgba(0x50, 0x50, 0x50, 0x50), 0x50505050);
assert.equal(toRgba(0x60, 0x60, 0x60, 0x60), 0x60606060);
assert.equal(toRgba(0x70, 0x70, 0x70, 0x70), 0x70707070);
assert.equal(toRgba(0x80, 0x80, 0x80, 0x80), 0x80808080);
assert.equal(toRgba(0x90, 0x90, 0x90, 0x90), 0x90909090);
assert.equal(toRgba(0xa0, 0xa0, 0xa0, 0xa0), 0xa0a0a0a0);
assert.equal(toRgba(0xb0, 0xb0, 0xb0, 0xb0), 0xb0b0b0b0);
assert.equal(toRgba(0xc0, 0xc0, 0xc0, 0xc0), 0xc0c0c0c0);
assert.equal(toRgba(0xd0, 0xd0, 0xd0, 0xd0), 0xd0d0d0d0);
assert.equal(toRgba(0xe0, 0xe0, 0xe0, 0xe0), 0xe0e0e0e0);
assert.equal(toRgba(0xf0, 0xf0, 0xf0, 0xf0), 0xf0f0f0f0);
assert.equal(toRgba(0xff, 0xff, 0xff, 0xff), 0xffffffff);
});
});
});
+49
View File
@@ -0,0 +1,49 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IColor } from './Types';
export function blend(bg: IColor, fg: IColor): IColor {
const a = (fg.rgba & 0xFF) / 255;
if (a === 1) {
return {
css: fg.css,
rgba: fg.rgba
};
}
const fgR = (fg.rgba >> 24) & 0xFF;
const fgG = (fg.rgba >> 16) & 0xFF;
const fgB = (fg.rgba >> 8) & 0xFF;
const bgR = (bg.rgba >> 24) & 0xFF;
const bgG = (bg.rgba >> 16) & 0xFF;
const bgB = (bg.rgba >> 8) & 0xFF;
const r = bgR + Math.round((fgR - bgR) * a);
const g = bgG + Math.round((fgG - bgG) * a);
const b = bgB + Math.round((fgB - bgB) * a);
const css = toCss(r, g, b);
const rgba = toRgba(r, g, b);
return { css, rgba };
}
export function fromCss(css: string): IColor {
return {
css,
rgba: (parseInt(css.slice(1), 16) << 8 | 0xFF) >>> 0
};
}
export function toPaddedHex(c: number): string {
const s = c.toString(16);
return s.length < 2 ? '0' + s : s;
}
export function toCss(r: number, g: number, b: number): string {
return `#${toPaddedHex(r)}${toPaddedHex(g)}${toPaddedHex(b)}`;
}
export function toRgba(r: number, g: number, b: number, a: number = 0xFF): number {
// >>> 0 forces an unsigned int
return (r << 24 | g << 16 | b << 8 | a) >>> 0;
}
+29 -40
View File
@@ -5,14 +5,15 @@
import { IColorManager, IColor, IColorSet } from 'browser/Types';
import { ITheme } from 'common/services/Services';
import { fromCss, toCss, blend, toRgba } from 'browser/Color';
const DEFAULT_FOREGROUND = fromHex('#ffffff');
const DEFAULT_BACKGROUND = fromHex('#000000');
const DEFAULT_CURSOR = fromHex('#ffffff');
const DEFAULT_CURSOR_ACCENT = fromHex('#000000');
const DEFAULT_FOREGROUND = fromCss('#ffffff');
const DEFAULT_BACKGROUND = fromCss('#000000');
const DEFAULT_CURSOR = fromCss('#ffffff');
const DEFAULT_CURSOR_ACCENT = fromCss('#000000');
const DEFAULT_SELECTION = {
css: 'rgba(255, 255, 255, 0.3)',
rgba: 0xFFFFFF77
rgba: 0xFFFFFF4D
};
// An IIFE to generate DEFAULT_ANSI_COLORS. Do not mutate DEFAULT_ANSI_COLORS, instead make a copy
@@ -20,23 +21,23 @@ const DEFAULT_SELECTION = {
export const DEFAULT_ANSI_COLORS = (() => {
const colors = [
// dark:
fromHex('#2e3436'),
fromHex('#cc0000'),
fromHex('#4e9a06'),
fromHex('#c4a000'),
fromHex('#3465a4'),
fromHex('#75507b'),
fromHex('#06989a'),
fromHex('#d3d7cf'),
fromCss('#2e3436'),
fromCss('#cc0000'),
fromCss('#4e9a06'),
fromCss('#c4a000'),
fromCss('#3465a4'),
fromCss('#75507b'),
fromCss('#06989a'),
fromCss('#d3d7cf'),
// bright:
fromHex('#555753'),
fromHex('#ef2929'),
fromHex('#8ae234'),
fromHex('#fce94f'),
fromHex('#729fcf'),
fromHex('#ad7fa8'),
fromHex('#34e2e2'),
fromHex('#eeeeec')
fromCss('#555753'),
fromCss('#ef2929'),
fromCss('#8ae234'),
fromCss('#fce94f'),
fromCss('#729fcf'),
fromCss('#ad7fa8'),
fromCss('#34e2e2'),
fromCss('#eeeeec')
];
// Fill in the remaining 240 ANSI colors.
@@ -47,37 +48,23 @@ export const DEFAULT_ANSI_COLORS = (() => {
const g = v[(i / 6) % 6 | 0];
const b = v[i % 6];
colors.push({
css: `#${toPaddedHex(r)}${toPaddedHex(g)}${toPaddedHex(b)}`,
// Use >>> 0 to force a conversion to an unsigned int
rgba: ((r << 24) | (g << 16) | (b << 8) | 0xFF) >>> 0
css: toCss(r, g, b),
rgba: toRgba(r, g, b)
});
}
// Generate greys (232-255)
for (let i = 0; i < 24; i++) {
const c = 8 + i * 10;
const ch = toPaddedHex(c);
colors.push({
css: `#${ch}${ch}${ch}`,
rgba: ((c << 24) | (c << 16) | (c << 8) | 0xFF) >>> 0
css: toCss(c, c, c),
rgba: toRgba(c, c, c)
});
}
return colors;
})();
function fromHex(css: string): IColor {
return {
css,
rgba: parseInt(css.slice(1), 16) << 8 | 0xFF
};
}
function toPaddedHex(c: number): string {
const s = c.toString(16);
return s.length < 2 ? '0' + s : s;
}
/**
* Manages the source of truth for a terminal's colors.
*/
@@ -103,6 +90,7 @@ export class ColorManager implements IColorManager {
cursor: DEFAULT_CURSOR,
cursorAccent: DEFAULT_CURSOR_ACCENT,
selection: DEFAULT_SELECTION,
selectionOpaque: blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION),
ansi: DEFAULT_ANSI_COLORS.slice()
};
}
@@ -118,6 +106,7 @@ export class ColorManager implements IColorManager {
this.colors.cursor = this._parseColor(theme.cursor, DEFAULT_CURSOR, true);
this.colors.cursorAccent = this._parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT, true);
this.colors.selection = this._parseColor(theme.selection, DEFAULT_SELECTION, true);
this.colors.selectionOpaque = blend(this.colors.background, this.colors.selection);
this.colors.ansi[0] = this._parseColor(theme.black, DEFAULT_ANSI_COLORS[0]);
this.colors.ansi[1] = this._parseColor(theme.red, DEFAULT_ANSI_COLORS[1]);
this.colors.ansi[2] = this._parseColor(theme.green, DEFAULT_ANSI_COLORS[2]);
@@ -184,7 +173,7 @@ export class ColorManager implements IColorManager {
return {
css,
rgba: (data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3]) >>> 0
rgba: toRgba(data[0], data[1], data[2], data[3])
};
}
}
+2
View File
@@ -21,6 +21,8 @@ export interface IColorSet {
cursor: IColor;
cursorAccent: IColor;
selection: IColor;
/** The selection blended on top of background. */
selectionOpaque: IColor;
ansi: IColor[];
}