Merge pull request #4920 from Tyriar/tyriar/bg_blend
Add bg+powerline background blend behavior
This commit is contained in:
@@ -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';
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -65,11 +67,11 @@ 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;
|
||||
}
|
||||
});
|
||||
@@ -77,10 +79,94 @@ export class CellColorResolver {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -88,11 +174,11 @@ export class CellColorResolver {
|
||||
// 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;
|
||||
}
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user