diff --git a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts index 1fd4c4f6b..03867b410 100644 --- a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts +++ b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts @@ -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() diff --git a/src/browser/Color.test.ts b/src/browser/Color.test.ts new file mode 100644 index 000000000..fc75d4b90 --- /dev/null +++ b/src/browser/Color.test.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert } from 'chai'; +import { blend } 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 }); + }); + }); +}); diff --git a/src/browser/Color.ts b/src/browser/Color.ts new file mode 100644 index 000000000..31087116b --- /dev/null +++ b/src/browser/Color.ts @@ -0,0 +1,39 @@ +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 = ((r << 24) | (g << 16) | (b << 8) | 0xFF) >>> 0; + return { css, rgba }; +} + +export function fromCss(css: string): IColor { + return { + css, + rgba: parseInt(css.slice(1), 16) << 8 | 0xFF + }; +} + +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)}`; +} diff --git a/src/browser/ColorManager.ts b/src/browser/ColorManager.ts index 70d21a7a4..a1bfc105e 100644 --- a/src/browser/ColorManager.ts +++ b/src/browser/ColorManager.ts @@ -5,11 +5,12 @@ import { IColorManager, IColor, IColorSet } from 'browser/Types'; import { ITheme } from 'common/services/Services'; +import { fromCss, toPaddedHex, toCss, blend } 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 @@ -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,7 +48,7 @@ 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)}`, + css: toCss(r, g, b), // Use >>> 0 to force a conversion to an unsigned int rgba: ((r << 24) | (g << 16) | (b << 8) | 0xFF) >>> 0 }); @@ -66,18 +67,6 @@ export const DEFAULT_ANSI_COLORS = (() => { 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 +92,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() }; } diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index aa7a4c86e..ca852b6aa 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -21,6 +21,8 @@ export interface IColorSet { cursor: IColor; cursorAccent: IColor; selection: IColor; + /** The selection blended on top of background. */ + selectionOpaque: IColor; ansi: IColor[]; }