Skip to content

Commit

Permalink
Merge pull request xtermjs#4920 from Tyriar/tyriar/bg_blend
Browse files Browse the repository at this point in the history
Add bg+powerline background blend behavior
  • Loading branch information
Tyriar authored Dec 19, 2023
2 parents 805823d + a4505ed commit 341d9f0
Show file tree
Hide file tree
Showing 10 changed files with 226 additions and 46 deletions.
7 changes: 6 additions & 1 deletion addons/addon-canvas/test/CanvasRenderer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,10 @@ test.describe('Canvas Renderer Integration Tests', () => {
test.skip(({ browserName }) => browserName === 'webkit');

injectSharedRendererTests(ctxWrapper);
injectSharedRendererTestsStandalone(ctxWrapper);
injectSharedRendererTestsStandalone(ctxWrapper, async () => {
await ctx.page.evaluate(`
window.addon = new window.CanvasAddon(true);
window.term.loadAddon(window.addon);
`);
});
});
7 changes: 6 additions & 1 deletion addons/addon-webgl/test/WebglRenderer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,10 @@ test.describe('WebGL Renderer Integration Tests', async () => {
}

injectSharedRendererTests(ctxWrapper);
injectSharedRendererTestsStandalone(ctxWrapper);
injectSharedRendererTestsStandalone(ctxWrapper, async () => {
await ctx.page.evaluate(`
window.addon = new window.WebglAddon(true);
window.term.loadAddon(window.addon);
`);
});
});
4 changes: 2 additions & 2 deletions src/browser/renderer/dom/DomRendererRowFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { ICoreService, IDecorationService, IOptionsService } from 'common/servic
import { color, rgba } from 'common/Color';
import { ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
import { excludeFromContrastRatioDemands } from 'browser/renderer/shared/RendererUtils';
import { treatGlyphAsBackgroundColor } from 'browser/renderer/shared/RendererUtils';
import { AttributeData } from 'common/buffer/AttributeData';
import { WidthCache } from 'browser/renderer/dom/WidthCache';
import { IColorContrastCache } from 'browser/Types';
Expand Down Expand Up @@ -458,7 +458,7 @@ export class DomRendererRowFactory {
}

private _applyMinimumContrast(element: HTMLElement, bg: IColor, fg: IColor, cell: ICellData, bgOverride: IColor | undefined, fgOverride: IColor | undefined): boolean {
if (this._optionsService.rawOptions.minimumContrastRatio === 1 || excludeFromContrastRatioDemands(cell.getCode())) {
if (this._optionsService.rawOptions.minimumContrastRatio === 1 || treatGlyphAsBackgroundColor(cell.getCode())) {
return false;
}

Expand Down
102 changes: 94 additions & 8 deletions src/browser/renderer/shared/CellColorResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { Attributes, BgFlags, ExtFlags, FgFlags, NULL_CELL_CODE, UnderlineStyle
import { IDecorationService, IOptionsService } from 'common/services/Services';
import { ICellData } from 'common/Types';
import { Terminal } from '@xterm/xterm';
import { rgba } from 'common/Color';
import { treatGlyphAsBackgroundColor } from 'browser/renderer/shared/RendererUtils';

// Work variables to avoid garbage collection
let $fg = 0;
Expand Down Expand Up @@ -65,34 +67,118 @@ export class CellColorResolver {
// Apply decorations on the bottom layer
this._decorationService.forEachDecorationAtCell(x, y, 'bottom', d => {
if (d.backgroundColorRGB) {
$bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
$bg = d.backgroundColorRGB.rgba >> 8 & Attributes.RGB_MASK;
$hasBg = true;
}
if (d.foregroundColorRGB) {
$fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
$fg = d.foregroundColorRGB.rgba >> 8 & Attributes.RGB_MASK;
$hasFg = true;
}
});

// Apply the selection color if needed
$isSelected = this._selectionRenderModel.isCellSelected(this._terminal, x, y);
if ($isSelected) {
$bg = (this._coreBrowserService.isFocused ? $colors.selectionBackgroundOpaque : $colors.selectionInactiveBackgroundOpaque).rgba >> 8 & 0xFFFFFF;
// If the cell has a bg color, retain the color by blending it with the selection color
if (
(this.result.fg & FgFlags.INVERSE) ||
(this.result.bg & Attributes.CM_MASK) !== Attributes.CM_DEFAULT
) {
// Resolve the standard bg color
if (this.result.fg & FgFlags.INVERSE) {
switch (this.result.fg & Attributes.CM_MASK) {
case Attributes.CM_P16:
case Attributes.CM_P256:
$bg = this._themeService.colors.ansi[this.result.fg & Attributes.PCOLOR_MASK].rgba;
break;
case Attributes.CM_RGB:
$bg = (this.result.fg & Attributes.RGB_MASK) << 8 | 0xFF;
break;
case Attributes.CM_DEFAULT:
default:
$bg = this._themeService.colors.foreground.rgba;
}
} else {
switch (this.result.bg & Attributes.CM_MASK) {
case Attributes.CM_P16:
case Attributes.CM_P256:
$bg = this._themeService.colors.ansi[this.result.bg & Attributes.PCOLOR_MASK].rgba;
break;
case Attributes.CM_RGB:
$bg = this.result.bg & Attributes.RGB_MASK << 8 | 0xFF;
break;
// No need to consider default bg color here as it's not possible
}
}
// Blend with selection bg color
$bg = rgba.blend(
$bg,
((this._coreBrowserService.isFocused ? $colors.selectionBackgroundOpaque : $colors.selectionInactiveBackgroundOpaque).rgba & 0xFFFFFF00) | 0x80
) >> 8 & Attributes.RGB_MASK;
} else {
$bg = (this._coreBrowserService.isFocused ? $colors.selectionBackgroundOpaque : $colors.selectionInactiveBackgroundOpaque).rgba >> 8 & Attributes.RGB_MASK;
}
$hasBg = true;

// Apply explicit selection foreground if present
if ($colors.selectionForeground) {
$fg = $colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
$fg = $colors.selectionForeground.rgba >> 8 & Attributes.RGB_MASK;
$hasFg = true;
}

// Overwrite fg as bg if it's a special decorative glyph (eg. powerline)
if (treatGlyphAsBackgroundColor(cell.getCode())) {
// Inverse default background should be treated as transparent
if (
(this.result.fg & FgFlags.INVERSE) &&
(this.result.bg & Attributes.CM_MASK) === Attributes.CM_DEFAULT
) {
$fg = (this._coreBrowserService.isFocused ? $colors.selectionBackgroundOpaque : $colors.selectionInactiveBackgroundOpaque).rgba >> 8 & Attributes.RGB_MASK;
} else {

if (this.result.fg & FgFlags.INVERSE) {
switch (this.result.bg & Attributes.CM_MASK) {
case Attributes.CM_P16:
case Attributes.CM_P256:
$fg = this._themeService.colors.ansi[this.result.bg & Attributes.PCOLOR_MASK].rgba;
break;
case Attributes.CM_RGB:
$fg = this.result.bg & Attributes.RGB_MASK << 8 | 0xFF;
break;
// No need to consider default bg color here as it's not possible
}
} else {
switch (this.result.fg & Attributes.CM_MASK) {
case Attributes.CM_P16:
case Attributes.CM_P256:
$fg = this._themeService.colors.ansi[this.result.fg & Attributes.PCOLOR_MASK].rgba;
break;
case Attributes.CM_RGB:
$fg = (this.result.fg & Attributes.RGB_MASK) << 8 | 0xFF;
break;
case Attributes.CM_DEFAULT:
default:
$fg = this._themeService.colors.foreground.rgba;
}
}

$fg = rgba.blend(
$fg,
((this._coreBrowserService.isFocused ? $colors.selectionBackgroundOpaque : $colors.selectionInactiveBackgroundOpaque).rgba & 0xFFFFFF00) | 0x80
) >> 8 & Attributes.RGB_MASK;
}
$hasFg = true;
}
}

// Apply decorations on the top layer
this._decorationService.forEachDecorationAtCell(x, y, 'top', d => {
if (d.backgroundColorRGB) {
$bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
$bg = d.backgroundColorRGB.rgba >> 8 & Attributes.RGB_MASK;
$hasBg = true;
}
if (d.foregroundColorRGB) {
$fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
$fg = d.foregroundColorRGB.rgba >> 8 & Attributes.RGB_MASK;
$hasFg = true;
}
});
Expand All @@ -119,7 +205,7 @@ export class CellColorResolver {
if ($hasBg && !$hasFg) {
// Resolve bg color type (default color has a different meaning in fg vs bg)
if ((this.result.bg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
$fg = (this.result.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | (($colors.background.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
$fg = (this.result.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | (($colors.background.rgba >> 8 & Attributes.RGB_MASK) & Attributes.RGB_MASK) | Attributes.CM_RGB;
} else {
$fg = (this.result.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | this.result.bg & (Attributes.RGB_MASK | Attributes.CM_MASK);
}
Expand All @@ -128,7 +214,7 @@ export class CellColorResolver {
if (!$hasBg && $hasFg) {
// Resolve bg color type (default color has a different meaning in fg vs bg)
if ((this.result.fg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
$bg = (this.result.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | (($colors.foreground.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
$bg = (this.result.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | (($colors.foreground.rgba >> 8 & Attributes.RGB_MASK) & Attributes.RGB_MASK) | Attributes.CM_RGB;
} else {
$bg = (this.result.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | this.result.fg & (Attributes.RGB_MASK | Attributes.CM_MASK);
}
Expand Down
2 changes: 1 addition & 1 deletion src/browser/renderer/shared/RendererUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ function isBoxOrBlockGlyph(codepoint: number): boolean {
return 0x2500 <= codepoint && codepoint <= 0x259F;
}

export function excludeFromContrastRatioDemands(codepoint: number): boolean {
export function treatGlyphAsBackgroundColor(codepoint: number): boolean {
return isPowerlineGlyph(codepoint) || isBoxOrBlockGlyph(codepoint);
}

Expand Down
4 changes: 2 additions & 2 deletions src/browser/renderer/shared/TextureAtlas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { IColorContrastCache } from 'browser/Types';
import { DIM_OPACITY, TEXT_BASELINE } from 'browser/renderer/shared/Constants';
import { tryDrawCustomChar } from 'browser/renderer/shared/CustomGlyphs';
import { computeNextVariantOffset, excludeFromContrastRatioDemands, isPowerlineGlyph, isRestrictedPowerlineGlyph, throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
import { computeNextVariantOffset, treatGlyphAsBackgroundColor, isPowerlineGlyph, isRestrictedPowerlineGlyph, throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
import { IBoundingBox, ICharAtlasConfig, IRasterizedGlyph, ITextureAtlas } from 'browser/renderer/shared/Types';
import { NULL_COLOR, color, rgba } from 'common/Color';
import { EventEmitter } from 'common/EventEmitter';
Expand Down Expand Up @@ -490,7 +490,7 @@ export class TextureAtlas implements ITextureAtlas {

const powerlineGlyph = chars.length === 1 && isPowerlineGlyph(chars.charCodeAt(0));
const restrictedPowerlineGlyph = chars.length === 1 && isRestrictedPowerlineGlyph(chars.charCodeAt(0));
const foregroundColor = this._getForegroundColor(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, dim, bold, excludeFromContrastRatioDemands(chars.charCodeAt(0)));
const foregroundColor = this._getForegroundColor(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, dim, bold, treatGlyphAsBackgroundColor(chars.charCodeAt(0)));
this._tmpCtx.fillStyle = foregroundColor.css;

// For powerline glyphs left/top padding is excluded (https://github.com/microsoft/vscode/issues/120129)
Expand Down
21 changes: 21 additions & 0 deletions src/common/Color.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,27 @@ describe('Color', () => {
});

describe('rgba', () => {
describe('blend', () => {
it('should blend colors based on the alpha channel', () => {
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF00), 0x000000FF);
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF10), 0x101010FF);
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF20), 0x202020FF);
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF30), 0x303030FF);
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF40), 0x404040FF);
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF50), 0x505050FF);
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF60), 0x606060FF);
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF70), 0x707070FF);
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF80), 0x808080FF);
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF90), 0x909090FF);
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFFA0), 0xA0A0A0FF);
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFFB0), 0xB0B0B0FF);
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFFC0), 0xC0C0C0FF);
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFFD0), 0xD0D0D0FF);
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFFE0), 0xE0E0E0FF);
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFFF0), 0xF0F0F0FF);
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFFFF), 0xFFFFFFFF);
});
});
describe('ensureContrastRatio', () => {
it('should return undefined if the color already meets the contrast ratio (black bg)', () => {
assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 1), undefined);
Expand Down
17 changes: 17 additions & 0 deletions src/common/Color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,23 @@ export namespace rgb {
* Helper functions where the source type is "rgba" (number: 0xrrggbbaa).
*/
export namespace rgba {
export function blend(bg: number, fg: number): number {
$a = (fg & 0xFF) / 0xFF;
if ($a === 1) {
return fg;
}
const fgR = (fg >> 24) & 0xFF;
const fgG = (fg >> 16) & 0xFF;
const fgB = (fg >> 8) & 0xFF;
const bgR = (bg >> 24) & 0xFF;
const bgG = (bg >> 16) & 0xFF;
const bgB = (bg >> 8) & 0xFF;
$r = bgR + Math.round((fgR - bgR) * $a);
$g = bgG + Math.round((fgG - bgG) * $a);
$b = bgB + Math.round((fgB - bgB) * $a);
return channels.toRgba($r, $g, $b);
}

/**
* Given a foreground color and a background color, either increase or reduce the luminance of the
* foreground color until the specified contrast ratio is met. If pure white or black is hit
Expand Down
7 changes: 5 additions & 2 deletions test/playwright/Renderer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import { ITestContext, createTestContext, openTerminal } from './TestUtils';
import { ISharedRendererTestContext, injectSharedRendererTestsStandalone, injectSharedRendererTests } from './SharedRendererTests';

let ctx: ITestContext;
const ctxWrapper: ISharedRendererTestContext = { value: undefined } as any;
const ctxWrapper: ISharedRendererTestContext = {
value: undefined,
skipDomExceptions: true
} as any;
test.beforeAll(async ({ browser }) => {
ctx = await createTestContext(browser);
ctxWrapper.value = ctx;
Expand All @@ -18,5 +21,5 @@ test.afterAll(async () => await ctx.page.close());

test.describe('DOM Renderer Integration Tests', () => {
injectSharedRendererTests(ctxWrapper);
injectSharedRendererTestsStandalone(ctxWrapper);
injectSharedRendererTestsStandalone(ctxWrapper, () => {});
});
Loading

0 comments on commit 341d9f0

Please sign in to comment.