Merge pull request #3813 from Tyriar/3810

Support selectionForeground theme color
This commit is contained in:
Daniel Imms
2022-05-18 04:14:34 -07:00
committed by GitHub
17 changed files with 180 additions and 5 deletions
@@ -397,6 +397,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
// Apply the selection color if needed
if (this._isCellSelected(x, y)) {
bgOverride = this._colors.selectionOpaque.rgba >> 8 & 0xFFFFFF;
if (this._colors.selectionForeground) {
fgOverride = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
}
}
// Apply decorations on the top layer
@@ -23,6 +23,7 @@ export function generateConfig(scaledCellWidth: number, scaledCellHeight: number
cursorAccent: NULL_COLOR,
selectionTransparent: NULL_COLOR,
selectionOpaque: NULL_COLOR,
selectionForeground: 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(),
@@ -875,6 +875,25 @@ describe('WebGL Renderer Integration Tests', async () => {
});
});
describe('selectionForeground', () => {
if (areTestsEnabled) {
before(async () => setupBrowser({ rendererType: 'dom' }));
after(async () => browser.close());
beforeEach(async () => page.evaluate(`window.term.reset()`));
}
itWebgl('transparent background inverse', async () => {
const theme: ITheme = {
selectionForeground: '#ff0000'
};
await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
const data = `\\x1b[7m█\x1b[0m`;
await writeSync(page, data);
await page.evaluate(`window.term.selectAll()`);
await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]);
});
});
describe('decoration color overrides', async () => {
if (areTestsEnabled) {
before(async () => setupBrowser({ rendererType: 'dom' }));
+1 -1
View File
@@ -34,7 +34,7 @@ describe('ColorManager', () => {
describe('constructor', () => {
it('should fill all colors with values', () => {
for (const key of Object.keys(cm.colors)) {
if (key !== 'ansi' && key !== 'contrastCache') {
if (key !== 'ansi' && key !== 'contrastCache' && key !== 'selectionForeground') {
// A #rrggbb or rgba(...)
assert.ok((cm.colors as any)[key].css.length >= 7);
}
+10
View File
@@ -104,6 +104,7 @@ export class ColorManager implements IColorManager {
cursorAccent: DEFAULT_CURSOR_ACCENT,
selectionTransparent: DEFAULT_SELECTION,
selectionOpaque: color.blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION),
selectionForeground: undefined,
ansi: DEFAULT_ANSI_COLORS.slice(),
contrastCache: this._contrastCache
};
@@ -128,6 +129,15 @@ export class ColorManager implements IColorManager {
this.colors.cursorAccent = this._parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT, true);
this.colors.selectionTransparent = this._parseColor(theme.selection, DEFAULT_SELECTION, true);
this.colors.selectionOpaque = color.blend(this.colors.background, this.colors.selectionTransparent);
const nullColor: IColor = {
css: '',
rgba: 0
};
this.colors.selectionForeground = theme.selectionForeground ? this._parseColor(theme.selectionForeground, nullColor) : undefined;
if (this.colors.selectionForeground === nullColor) {
this.colors.selectionForeground = undefined;
}
/**
* If selection color is opaque, blend it with background with 0.3 opacity
* Issue #2737
+52
View File
@@ -16,6 +16,7 @@ import { Terminal } from 'browser/Terminal';
import { IUnicodeService, IOptionsService, ICoreService, ICoreMouseService } from 'common/services/Services';
import { IFunctionIdentifier, IParams } from 'common/parser/Types';
import { AttributeData } from 'common/buffer/AttributeData';
import { ISelectionRedrawRequestEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
export class TestTerminal extends Terminal {
public get curAttrData(): IAttributeData { return (this as any)._inputHandler._curAttrData; }
@@ -450,3 +451,54 @@ export class MockCharacterJoinerService implements ICharacterJoinerService {
return [];
}
}
export class MockSelectionService implements ISelectionService {
public serviceBrand: undefined;
public selectionText: string = '';
public hasSelection: boolean = false;
public selectionStart: [number, number] | undefined;
public selectionEnd: [number, number] | undefined;
public onLinuxMouseSelection = new EventEmitter<string>().event;
public onRequestRedraw = new EventEmitter<ISelectionRedrawRequestEvent>().event;
public onRequestScrollLines = new EventEmitter<ISelectionRequestScrollLinesEvent>().event;
public onSelectionChange = new EventEmitter<void>().event;
public disable(): void {
throw new Error('Method not implemented.');
}
public enable(): void {
throw new Error('Method not implemented.');
}
public reset(): void {
throw new Error('Method not implemented.');
}
public setSelection(row: number, col: number, length: number): void {
throw new Error('Method not implemented.');
}
public selectAll(): void {
throw new Error('Method not implemented.');
}
public selectLines(start: number, end: number): void {
throw new Error('Method not implemented.');
}
public clearSelection(): void {
throw new Error('Method not implemented.');
}
public rightClickSelect(event: MouseEvent): void {
throw new Error('Method not implemented.');
}
public shouldColumnSelect(event: MouseEvent | KeyboardEvent): boolean {
throw new Error('Method not implemented.');
}
public shouldForceSelection(event: MouseEvent): boolean {
throw new Error('Method not implemented.');
}
public refresh(isLinuxMouseSelection?: boolean): void {
throw new Error('Method not implemented.');
}
public onMouseDown(event: MouseEvent): void {
throw new Error('Method not implemented.');
}
public isCellInSelection(x: number, y: number): boolean {
return false;
}
}
+1
View File
@@ -121,6 +121,7 @@ export interface IColorSet {
selectionTransparent: IColor;
/** The selection blended on top of background. */
selectionOpaque: IColor;
selectionForeground: IColor | undefined;
ansi: IColor[];
contrastCache: IColorContrastCache;
}
+33 -1
View File
@@ -29,6 +29,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
private _scaledCharLeft: number = 0;
private _scaledCharTop: number = 0;
private _selectionStart: [number, number] | undefined;
private _selectionEnd: [number, number] | undefined;
private _columnSelectMode: boolean = false;
protected _charAtlas: BaseCharAtlas | undefined;
/**
@@ -80,7 +84,12 @@ export abstract class BaseRenderLayer implements IRenderLayer {
public onFocus(): void {}
public onCursorMove(): void {}
public onGridChanged(startRow: number, endRow: number): void {}
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {}
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
this._selectionStart = start;
this._selectionEnd = end;
this._columnSelectMode = columnSelectMode;
}
public setColors(colorSet: IColorSet): void {
this._refreshCharAtlas(colorSet);
@@ -457,6 +466,13 @@ export abstract class BaseRenderLayer implements IRenderLayer {
isTop = d.options.layer === 'top';
}
// Apply selection foreground if applicable
if (!isTop) {
if (this._colors.selectionForeground && this._isCellInSelection(x, y)) {
fgOverride = this._colors.selectionForeground.rgba;
}
}
if (!bgOverride && !fgOverride && (this._optionsService.rawOptions.minimumContrastRatio === 1 || isPowerlineGlyph(cell.getCode()))) {
return undefined;
}
@@ -546,5 +562,21 @@ export abstract class BaseRenderLayer implements IRenderLayer {
return this._colors.foreground.rgba;
}
}
private _isCellInSelection(x: number, y: number): boolean {
const start = this._selectionStart;
const end = this._selectionEnd;
if (!start || !end) {
return false;
}
if (this._columnSelectMode) {
return x >= start[0] && y >= start[1] &&
x < end[0] && y < end[1];
}
return (y > start[1] && y < end[1]) ||
(start[1] === end[1] && y === start[1] && x >= start[0] && x < end[0]) ||
(start[1] < end[1] && y === end[1] && x < end[0]) ||
(start[1] < end[1] && y === start[1] && x >= start[0]);
}
}
+4
View File
@@ -119,6 +119,10 @@ export class Renderer extends Disposable implements IRenderer {
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
this._runOperation(l => l.onSelectionChanged(start, end, columnSelectMode));
// Selection foreground requires a full re-render
if (this._colors.selectionForeground) {
this._onRequestRedraw.fire({ start: 0, end: this._bufferService.rows - 1 });
}
}
public onCursorMove(): void {
@@ -54,6 +54,8 @@ export class SelectionRenderLayer extends BaseRenderLayer {
}
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
super.onSelectionChanged(start, end, columnSelectMode);
// Selection has not changed
if (!this._didStateChange(start, end, columnSelectMode, this._bufferService.buffer.ydisp)) {
return;
+3
View File
@@ -281,6 +281,9 @@ export class DomRenderer extends Disposable implements IRenderer {
this._selectionContainer.removeChild(this._selectionContainer.children[0]);
}
this._rowFactory.onSelectionChanged(start, end, columnSelectMode);
this.renderRows(0, this._bufferService.rows - 1);
// Selection does not exist
if (!start || !end) {
return;
@@ -12,7 +12,7 @@ import { IBufferLine } from 'common/Types';
import { CellData } from 'common/buffer/CellData';
import { MockCoreService, MockDecorationService, MockOptionsService } from 'common/TestUtils.test';
import { css } from 'common/Color';
import { MockCharacterJoinerService } from 'browser/TestUtils.test';
import { MockCharacterJoinerService, MockSelectionService } from 'browser/TestUtils.test';
describe('DomRendererRowFactory', () => {
let dom: jsdom.JSDOM;
@@ -7,10 +7,10 @@ import { IBufferLine, ICellData, IColor } from 'common/Types';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants';
import { CellData } from 'common/buffer/CellData';
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { color, rgba } from 'common/Color';
import { IColorSet } from 'browser/Types';
import { ICharacterJoinerService } from 'browser/services/Services';
import { ICharacterJoinerService, ISelectionService } from 'browser/services/Services';
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
import { isPowerlineGlyph } from 'browser/renderer/RendererUtils';
@@ -28,6 +28,10 @@ export const CURSOR_STYLE_UNDERLINE_CLASS = 'xterm-cursor-underline';
export class DomRendererRowFactory {
private _workCell: CellData = new CellData();
private _selectionStart: [number, number] | undefined;
private _selectionEnd: [number, number] | undefined;
private _columnSelectMode: boolean = false;
constructor(
private readonly _document: Document,
private _colors: IColorSet,
@@ -42,6 +46,12 @@ export class DomRendererRowFactory {
this._colors = colors;
}
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
this._selectionStart = start;
this._selectionEnd = end;
this._columnSelectMode = columnSelectMode;
}
public createRow(lineData: IBufferLine, row: number, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cursorBlink: boolean, cellWidth: number, cols: number): DocumentFragment {
const fragment = this._document.createDocumentFragment();
@@ -195,6 +205,15 @@ export class DomRendererRowFactory {
isTop = d.options.layer === 'top';
}
// Apply selection foreground if applicable
if (!isTop) {
if (this._colors.selectionForeground && this._isCellInSelection(x, row)) {
fgColorMode = Attributes.CM_RGB;
fg = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
fgOverride = this._colors.selectionForeground;
}
}
// If it's a top decoration, render above the selection
if (isTop) {
charElement.classList.add(`xterm-decoration-top`);
@@ -283,6 +302,22 @@ export class DomRendererRowFactory {
private _addStyle(element: HTMLElement, style: string): void {
element.setAttribute('style', `${element.getAttribute('style') || ''}${style};`);
}
private _isCellInSelection(x: number, y: number): boolean {
const start = this._selectionStart;
const end = this._selectionEnd;
if (!start || !end) {
return false;
}
if (this._columnSelectMode) {
return x >= start[0] && y >= start[1] &&
x < end[0] && y < end[1];
}
return (y > start[1] && y < end[1]) ||
(start[1] === end[1] && y === start[1] && x >= start[0] && x < end[0]) ||
(start[1] < end[1] && y === end[1] && x < end[0]) ||
(start[1] < end[1] && y === start[1] && x >= start[0]);
}
}
function padStart(text: string, padChar: string, length: number): string {
+9
View File
@@ -308,6 +308,15 @@ export class SelectionService extends Disposable implements ISelectionService {
return this._areCoordsInSelection(coords, start, end);
}
public isCellInSelection(x: number, y: number): boolean {
const start = this._model.finalSelectionStart;
const end = this._model.finalSelectionEnd;
if (!start || !end) {
return false;
}
return this._areCoordsInSelection([x, y], start, end);
}
protected _areCoordsInSelection(coords: [number, number], start: [number, number], end: [number, number]): boolean {
return (coords[1] > start[1] && coords[1] < end[1]) ||
(start[1] === end[1] && coords[1] === start[1] && coords[0] >= start[0] && coords[0] < end[0]) ||
+1
View File
@@ -102,6 +102,7 @@ export interface ISelectionService {
shouldForceSelection(event: MouseEvent): boolean;
refresh(isLinuxMouseSelection?: boolean): void;
onMouseDown(event: MouseEvent): void;
isCellInSelection(x: number, y: number): boolean;
}
export const ISoundService = createDecorator<ISoundService>('SoundService');
+1
View File
@@ -259,6 +259,7 @@ export interface ITheme {
cursor?: string;
cursorAccent?: string;
selection?: string;
selectionForeground?: string;
black?: string;
red?: string;
green?: string;
+2
View File
@@ -288,6 +288,8 @@ declare module 'xterm' {
cursorAccent?: string;
/** The selection background color (can be transparent) */
selection?: string;
/** The selection foreground color */
selectionForeground?: string;
/** ANSI black (eg. `\x1b[30m`) */
black?: string;
/** ANSI red (eg. `\x1b[31m`) */