Skip to content
2 changes: 1 addition & 1 deletion addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function generateConfig(scaledCharWidth: number, scaledCharHeight: number
background: colors.background,
cursor: NULL_COLOR,
cursorAccent: NULL_COLOR,
selection: NULL_COLOR,
selectionTransparent: 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.
Expand Down
53 changes: 52 additions & 1 deletion src/browser/Color.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,43 @@ describe('Color', () => {
assert.deepEqual(color.opaque({ css: '#ffffffff', rgba: 0xffffffff }), { css: '#ffffff', rgba: 0xffffffFF });
});
});

describe('isOpaque', () => {
it('should return true for opaque colors', () => {
assert.ok(color.isOpaque(css.toColor('#000000')));
assert.ok(color.isOpaque(css.toColor('#000000ff')));
assert.ok(color.isOpaque(css.toColor('#808080')));
assert.ok(color.isOpaque(css.toColor('#808080ff')));
assert.ok(color.isOpaque(css.toColor('#ffffff')));
assert.ok(color.isOpaque(css.toColor('#ffffffff')));
});
it('should return false for transparent colors', () => {
assert.ok(!color.isOpaque(css.toColor('#00000000')));
assert.ok(!color.isOpaque(css.toColor('#00000080')));
assert.ok(!color.isOpaque(css.toColor('#000000fe')));
assert.ok(!color.isOpaque(css.toColor('#80808000')));
assert.ok(!color.isOpaque(css.toColor('#80808080')));
assert.ok(!color.isOpaque(css.toColor('#808080fe')));
assert.ok(!color.isOpaque(css.toColor('#ffffff00')));
assert.ok(!color.isOpaque(css.toColor('#ffffff80')));
assert.ok(!color.isOpaque(css.toColor('#fffffffe')));
});
});

describe('opacity', () => {
it('should make the color transparent', () => {
assert.deepEqual(color.opacity(css.toColor('#000000'), 0), { css: '#00000000', rgba: 0x00000000 });
assert.deepEqual(color.opacity(css.toColor('#000000'), 0.25), { css: '#00000040', rgba: 0x00000040 });
assert.deepEqual(color.opacity(css.toColor('#000000'), 0.5), { css: '#00000080', rgba: 0x00000080 });
assert.deepEqual(color.opacity(css.toColor('#000000'), 0.75), { css: '#000000bf', rgba: 0x000000bf });
assert.deepEqual(color.opacity(css.toColor('#000000'), 1), { css: '#000000ff', rgba: 0x000000ff });
});
});
});

describe('css', () => {
describe('toColor', () => {
it('should covert a CSS string to an IColor', () => {
it('should convert the #rrggbb format to an IColor', () => {
assert.deepEqual(css.toColor('#000000'), { css: '#000000', rgba: 0x000000FF });
assert.deepEqual(css.toColor('#101010'), { css: '#101010', rgba: 0x101010FF });
assert.deepEqual(css.toColor('#202020'), { css: '#202020', rgba: 0x202020FF });
Expand All @@ -140,6 +172,25 @@ describe('Color', () => {
assert.deepEqual(css.toColor('#f0f0f0'), { css: '#f0f0f0', rgba: 0xf0f0f0FF });
assert.deepEqual(css.toColor('#ffffff'), { css: '#ffffff', rgba: 0xffffffFF });
});
it('should convert the #rrggbbaa format to an IColor', () => {
assert.deepEqual(css.toColor('#00000000'), { css: '#00000000', rgba: 0x00000000 });
assert.deepEqual(css.toColor('#10101010'), { css: '#10101010', rgba: 0x10101010 });
assert.deepEqual(css.toColor('#20202020'), { css: '#20202020', rgba: 0x20202020 });
assert.deepEqual(css.toColor('#30303030'), { css: '#30303030', rgba: 0x30303030 });
assert.deepEqual(css.toColor('#40404040'), { css: '#40404040', rgba: 0x40404040 });
assert.deepEqual(css.toColor('#50505050'), { css: '#50505050', rgba: 0x50505050 });
assert.deepEqual(css.toColor('#60606060'), { css: '#60606060', rgba: 0x60606060 });
assert.deepEqual(css.toColor('#70707070'), { css: '#70707070', rgba: 0x70707070 });
assert.deepEqual(css.toColor('#80808080'), { css: '#80808080', rgba: 0x80808080 });
assert.deepEqual(css.toColor('#90909090'), { css: '#90909090', rgba: 0x90909090 });
assert.deepEqual(css.toColor('#a0a0a0a0'), { css: '#a0a0a0a0', rgba: 0xa0a0a0a0 });
assert.deepEqual(css.toColor('#b0b0b0b0'), { css: '#b0b0b0b0', rgba: 0xb0b0b0b0 });
assert.deepEqual(css.toColor('#c0c0c0c0'), { css: '#c0c0c0c0', rgba: 0xc0c0c0c0 });
assert.deepEqual(css.toColor('#d0d0d0d0'), { css: '#d0d0d0d0', rgba: 0xd0d0d0d0 });
assert.deepEqual(css.toColor('#e0e0e0e0'), { css: '#e0e0e0e0', rgba: 0xe0e0e0e0 });
assert.deepEqual(css.toColor('#f0f0f0f0'), { css: '#f0f0f0f0', rgba: 0xf0f0f0f0 });
assert.deepEqual(css.toColor('#ffffffff'), { css: '#ffffffff', rgba: 0xffffffff });
});
});
});

Expand Down
30 changes: 26 additions & 4 deletions src/browser/Color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ export namespace color {
return { css, rgba };
}

export function isOpaque(color: IColor): boolean {
return (color.rgba & 0xFF) === 0xFF;
}

export function ensureContrastRatio(bg: IColor, fg: IColor, ratio: number): IColor | undefined {
const result = rgba.ensureContrastRatio(bg.rgba, fg.rgba, ratio);
if (!result) {
Expand All @@ -68,17 +72,35 @@ export namespace color {
rgba: rgbaColor
};
}

export function opacity(color: IColor, opacity: number): IColor {
const a = Math.round(opacity * 0xFF);
const [r, g, b] = rgba.toChannels(color.rgba);
return {
css: channels.toCss(r, g, b, a),
rgba: channels.toRgba(r, g, b, a)
};
}
}

/**
* Helper functions where the source type is "css" (string: '#rgb', '#rgba', '#rrggbb', '#rrggbbaa').
*/
export namespace css {
export function toColor(css: string): IColor {
return {
css,
rgba: (parseInt(css.slice(1), 16) << 8 | 0xFF) >>> 0
};
switch (css.length) {
case 7: // #rrggbb
return {
css,
rgba: (parseInt(css.slice(1), 16) << 8 | 0xFF) >>> 0
};
case 9: // #rrggbbaa
return {
css,
rgba: parseInt(css.slice(1), 16) >>> 0
};
}
throw new Error('css.toColor: Unsupported css format');
}
}

Expand Down
14 changes: 11 additions & 3 deletions src/browser/ColorManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export class ColorManager implements IColorManager {
background: DEFAULT_BACKGROUND,
cursor: DEFAULT_CURSOR,
cursorAccent: DEFAULT_CURSOR_ACCENT,
selection: DEFAULT_SELECTION,
selectionTransparent: DEFAULT_SELECTION,
selectionOpaque: color.blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION),
ansi: DEFAULT_ANSI_COLORS.slice(),
contrastCache: this._contrastCache
Expand All @@ -115,8 +115,16 @@ export class ColorManager implements IColorManager {
this.colors.background = this._parseColor(theme.background, DEFAULT_BACKGROUND);
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 = color.blend(this.colors.background, this.colors.selection);
this.colors.selectionTransparent = this._parseColor(theme.selection, DEFAULT_SELECTION, true);
this.colors.selectionOpaque = color.blend(this.colors.background, this.colors.selectionTransparent);
/**
* If selection color is opaque, blend it with background with 0.3 opacity
* Issue #2737
*/
if (color.isOpaque(this.colors.selectionTransparent)) {
const opacity = 0.3;
this.colors.selectionTransparent = color.opacity(this.colors.selectionTransparent, opacity);
}
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]);
Expand Down
2 changes: 1 addition & 1 deletion src/browser/Types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export interface IColorSet {
background: IColor;
cursor: IColor;
cursorAccent: IColor;
selection: IColor;
selectionTransparent: IColor;
/** The selection blended on top of background. */
selectionOpaque: IColor;
ansi: IColor[];
Expand Down
2 changes: 0 additions & 2 deletions src/browser/renderer/Renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ export class Renderer extends Disposable implements IRenderer {
super();
const allowTransparency = this._optionsService.options.allowTransparency;
this._characterJoinerRegistry = new CharacterJoinerRegistry(this._bufferService);

this._renderLayers = [
new TextRenderLayer(this._screenElement, 0, this._colors, this._characterJoinerRegistry, allowTransparency, this._id, this._bufferService, _optionsService),
new SelectionRenderLayer(this._screenElement, 1, this._colors, this._id, this._bufferService, _optionsService),
Expand Down Expand Up @@ -89,7 +88,6 @@ export class Renderer extends Disposable implements IRenderer {

public setColors(colors: IColorSet): void {
this._colors = colors;

// Clear layers and force a full render
for (const l of this._renderLayers) {
l.setColors(this._colors);
Expand Down
2 changes: 1 addition & 1 deletion src/browser/renderer/SelectionRenderLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export class SelectionRenderLayer extends BaseRenderLayer {
return;
}

this._ctx.fillStyle = this._colors.selection.css;
this._ctx.fillStyle = this._colors.selectionTransparent.css;

if (columnSelectMode) {
const startCol = start[0];
Expand Down
3 changes: 1 addition & 2 deletions src/browser/renderer/dom/DomRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ export class DomRenderer extends Disposable implements IRenderer {
@IBufferService private readonly _bufferService: IBufferService
) {
super();

this._rowContainer = document.createElement('div');
this._rowContainer.classList.add(ROW_CONTAINER_CLASS);
this._rowContainer.style.lineHeight = 'normal';
Expand Down Expand Up @@ -226,7 +225,7 @@ export class DomRenderer extends Disposable implements IRenderer {
`}` +
`${this._terminalSelector} .${SELECTION_CLASS} div {` +
` position: absolute;` +
` background-color: ${this._colors.selection.css};` +
` background-color: ${this._colors.selectionTransparent.css};` +
`}`;
// Colors
this._colors.ansi.forEach((c, i) => {
Expand Down