From c5022723731c503d7862758a8c846e397babaeb2 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 16 May 2022 09:40:07 -0700 Subject: [PATCH 01/10] Support selection foreground in webgl Part of #3810 --- addons/xterm-addon-webgl/src/WebglRenderer.ts | 3 +++ addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts | 1 + src/browser/ColorManager.ts | 10 ++++++++++ src/browser/Types.d.ts | 1 + src/common/services/Services.ts | 1 + typings/xterm.d.ts | 2 ++ 6 files changed, 18 insertions(+) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index d060c4d1..dbe9da91 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -398,6 +398,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 diff --git a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts index 0ce893df..92cfd4a9 100644 --- a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts +++ b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts @@ -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(), diff --git a/src/browser/ColorManager.ts b/src/browser/ColorManager.ts index 2d6e4ea5..f11ef8fd 100644 --- a/src/browser/ColorManager.ts +++ b/src/browser/ColorManager.ts @@ -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 diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index 0e83c213..129842d5 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -120,6 +120,7 @@ export interface IColorSet { selectionTransparent: IColor; /** The selection blended on top of background. */ selectionOpaque: IColor; + selectionForeground: IColor | undefined; ansi: IColor[]; contrastCache: IColorContrastCache; } diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index c3190210..017eb386 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -259,6 +259,7 @@ export interface ITheme { cursor?: string; cursorAccent?: string; selection?: string; + selectionForeground?: string; black?: string; red?: string; green?: string; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 15ee4650..ce6c8d3f 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -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`) */ From eb961878012d6ef3c411406e64987061a67d3ca7 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 16 May 2022 10:08:51 -0700 Subject: [PATCH 02/10] Support selectionForeground in dom Part of #3810 --- src/browser/TestUtils.test.ts | 52 +++++++++++++++++++ src/browser/renderer/dom/DomRenderer.ts | 2 + .../dom/DomRendererRowFactory.test.ts | 8 +-- .../renderer/dom/DomRendererRowFactory.ts | 16 ++++-- src/browser/services/SelectionService.ts | 9 ++++ src/browser/services/Services.ts | 1 + 6 files changed, 82 insertions(+), 6 deletions(-) diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index 85e2bb55..f7e6bcb4 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -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; } @@ -449,3 +450,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().event; + public onRequestRedraw = new EventEmitter().event; + public onRequestScrollLines = new EventEmitter().event; + public onSelectionChange = new EventEmitter().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; + } +} diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index d15d7eac..840ef40d 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -281,6 +281,8 @@ export class DomRenderer extends Disposable implements IRenderer { this._selectionContainer.removeChild(this._selectionContainer.children[0]); } + this.renderRows(0, this._bufferService.rows - 1); + // Selection does not exist if (!start || !end) { return; diff --git a/src/browser/renderer/dom/DomRendererRowFactory.test.ts b/src/browser/renderer/dom/DomRendererRowFactory.test.ts index bb511a47..ae7a1434 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.test.ts @@ -10,9 +10,9 @@ import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, DEFAULT_ATTR, FgFlags, import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { IBufferLine } from 'common/Types'; import { CellData } from 'common/buffer/CellData'; -import { MockCoreService, MockDecorationService, MockOptionsService } from 'common/TestUtils.test'; +import { MockBufferService, 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; @@ -50,7 +50,9 @@ describe('DomRendererRowFactory', () => { new MockCharacterJoinerService(), new MockOptionsService({ drawBoldTextInBrightColors: true }), new MockCoreService(), - new MockDecorationService() + new MockDecorationService(), + new MockBufferService(80, 30), + new MockSelectionService() ); lineData = createEmptyLineData(2); }); diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index 4bed48e5..f421859b 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -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'; @@ -34,7 +34,9 @@ export class DomRendererRowFactory { @ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService, @IOptionsService private readonly _optionsService: IOptionsService, @ICoreService private readonly _coreService: ICoreService, - @IDecorationService private readonly _decorationService: IDecorationService + @IDecorationService private readonly _decorationService: IDecorationService, + @IBufferService private readonly _bufferService: IBufferService, + @ISelectionService private readonly _selectionService: ISelectionService ) { } @@ -195,6 +197,14 @@ export class DomRendererRowFactory { isTop = d.options.layer === 'top'; } + // Apply selection foreground if applicable + if (!isTop) { + if (this._colors.selectionForeground && this._selectionService.isCellInSelection(x, row)) { + 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`); diff --git a/src/browser/services/SelectionService.ts b/src/browser/services/SelectionService.ts index 4b882e6d..c7b7707a 100644 --- a/src/browser/services/SelectionService.ts +++ b/src/browser/services/SelectionService.ts @@ -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]) || diff --git a/src/browser/services/Services.ts b/src/browser/services/Services.ts index 7191d0ed..c5328f76 100644 --- a/src/browser/services/Services.ts +++ b/src/browser/services/Services.ts @@ -101,6 +101,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('SoundService'); From f2b90d690f97ec72e5ca82d46787975ac9277901 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 16 May 2022 15:17:03 -0700 Subject: [PATCH 03/10] Selection foreground for canvas (circular dep issue) --- src/browser/renderer/BaseRenderLayer.ts | 11 ++++++++++- src/browser/renderer/CursorRenderLayer.ts | 7 ++++--- src/browser/renderer/LinkRenderLayer.ts | 6 ++++-- src/browser/renderer/SelectionRenderLayer.ts | 6 ++++-- src/browser/renderer/TextRenderLayer.ts | 7 ++++--- 5 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/browser/renderer/BaseRenderLayer.ts b/src/browser/renderer/BaseRenderLayer.ts index 696b793f..7bcc6881 100644 --- a/src/browser/renderer/BaseRenderLayer.ts +++ b/src/browser/renderer/BaseRenderLayer.ts @@ -18,6 +18,7 @@ import { isPowerlineGlyph, throwIfFalsy } from 'browser/renderer/RendererUtils'; import { channels, color, rgba } from 'common/Color'; import { removeElementFromParent } from 'browser/Dom'; import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs'; +import { ISelectionService } from 'browser/services/Services'; export abstract class BaseRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; @@ -53,7 +54,8 @@ export abstract class BaseRenderLayer implements IRenderLayer { private _rendererId: number, protected readonly _bufferService: IBufferService, protected readonly _optionsService: IOptionsService, - protected readonly _decorationService: IDecorationService + protected readonly _decorationService: IDecorationService, + protected readonly _selectionService: ISelectionService ) { this._canvas = document.createElement('canvas'); this._canvas.classList.add(`xterm-${id}-layer`); @@ -457,6 +459,13 @@ export abstract class BaseRenderLayer implements IRenderLayer { isTop = d.options.layer === 'top'; } + // Apply selection foreground if applicable + if (!isTop) { + if (this._colors.selectionForeground && this._selectionService.isCellInSelection(x, y)) { + fgOverride = this._colors.selectionForeground.rgba; + } + } + if (!bgOverride && !fgOverride && (this._optionsService.rawOptions.minimumContrastRatio === 1 || isPowerlineGlyph(cell.getCode()))) { return undefined; } diff --git a/src/browser/renderer/CursorRenderLayer.ts b/src/browser/renderer/CursorRenderLayer.ts index 3fa576a9..6405390e 100644 --- a/src/browser/renderer/CursorRenderLayer.ts +++ b/src/browser/renderer/CursorRenderLayer.ts @@ -10,7 +10,7 @@ import { CellData } from 'common/buffer/CellData'; import { IColorSet } from 'browser/Types'; import { IBufferService, IOptionsService, ICoreService, IDecorationService } from 'common/services/Services'; import { IEventEmitter } from 'common/EventEmitter'; -import { ICoreBrowserService } from 'browser/services/Services'; +import { ICoreBrowserService, ISelectionService } from 'browser/services/Services'; interface ICursorState { x: number; @@ -41,9 +41,10 @@ export class CursorRenderLayer extends BaseRenderLayer { @IOptionsService optionsService: IOptionsService, @ICoreService private readonly _coreService: ICoreService, @ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService, - @IDecorationService decorationService: IDecorationService + @IDecorationService decorationService: IDecorationService, + @ISelectionService selectionService: ISelectionService ) { - super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService); + super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService, selectionService); this._state = { x: 0, y: 0, diff --git a/src/browser/renderer/LinkRenderLayer.ts b/src/browser/renderer/LinkRenderLayer.ts index 15086d9a..4d350f4d 100644 --- a/src/browser/renderer/LinkRenderLayer.ts +++ b/src/browser/renderer/LinkRenderLayer.ts @@ -9,6 +9,7 @@ import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; import { is256Color } from 'browser/renderer/atlas/CharAtlasUtils'; import { IColorSet, ILinkifierEvent, ILinkifier, ILinkifier2 } from 'browser/Types'; import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; +import { ISelectionService } from 'browser/services/Services'; export class LinkRenderLayer extends BaseRenderLayer { private _state: ILinkifierEvent | undefined; @@ -22,9 +23,10 @@ export class LinkRenderLayer extends BaseRenderLayer { linkifier2: ILinkifier2, @IBufferService bufferService: IBufferService, @IOptionsService optionsService: IOptionsService, - @IDecorationService decorationService: IDecorationService + @IDecorationService decorationService: IDecorationService, + @ISelectionService selectionService: ISelectionService ) { - super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService); + super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService, selectionService); linkifier.onShowLinkUnderline(e => this._onShowLinkUnderline(e)); linkifier.onHideLinkUnderline(e => this._onHideLinkUnderline(e)); diff --git a/src/browser/renderer/SelectionRenderLayer.ts b/src/browser/renderer/SelectionRenderLayer.ts index be911eb9..60c53eaf 100644 --- a/src/browser/renderer/SelectionRenderLayer.ts +++ b/src/browser/renderer/SelectionRenderLayer.ts @@ -7,6 +7,7 @@ import { IRenderDimensions } from 'browser/renderer/Types'; import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer'; import { IColorSet } from 'browser/Types'; import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; +import { ISelectionService } from 'browser/services/Services'; interface ISelectionState { start?: [number, number]; @@ -25,9 +26,10 @@ export class SelectionRenderLayer extends BaseRenderLayer { rendererId: number, @IBufferService bufferService: IBufferService, @IOptionsService optionsService: IOptionsService, - @IDecorationService decorationService: IDecorationService + @IDecorationService decorationService: IDecorationService, + @ISelectionService selectionService: ISelectionService ) { - super(container, 'selection', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService); + super(container, 'selection', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService, selectionService); this._clearState(); } diff --git a/src/browser/renderer/TextRenderLayer.ts b/src/browser/renderer/TextRenderLayer.ts index ef5a9b62..97c7e6f6 100644 --- a/src/browser/renderer/TextRenderLayer.ts +++ b/src/browser/renderer/TextRenderLayer.ts @@ -12,7 +12,7 @@ import { NULL_CELL_CODE, Content } from 'common/buffer/Constants'; import { IColorSet } from 'browser/Types'; import { CellData } from 'common/buffer/CellData'; import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services'; -import { ICharacterJoinerService } from 'browser/services/Services'; +import { ICharacterJoinerService, ISelectionService } from 'browser/services/Services'; import { JoinedCellData } from 'browser/services/CharacterJoinerService'; /** @@ -38,9 +38,10 @@ export class TextRenderLayer extends BaseRenderLayer { @IBufferService bufferService: IBufferService, @IOptionsService optionsService: IOptionsService, @ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService, - @IDecorationService decorationService: IDecorationService + @IDecorationService decorationService: IDecorationService, + @ISelectionService selectionService: ISelectionService ) { - super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService, decorationService); + super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService, decorationService, selectionService); this._state = new GridCache(); } From d7fdf17eabbb60b4671038b9da8a5c072d764f45 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 17 May 2022 11:20:20 -0700 Subject: [PATCH 04/10] Ensure color mode is set for selection foreground overrides --- src/browser/renderer/dom/DomRendererRowFactory.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index f421859b..5ce61113 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -200,6 +200,7 @@ export class DomRendererRowFactory { // Apply selection foreground if applicable if (!isTop) { if (this._colors.selectionForeground && this._selectionService.isCellInSelection(x, row)) { + fgColorMode = Attributes.CM_RGB; fg = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF; fgOverride = this._colors.selectionForeground; } From ec26820ea6e543aa69a09af8285d91387e8820bb Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 17 May 2022 11:23:33 -0700 Subject: [PATCH 05/10] Fix circular dependency mouseservice <-> renderservice --- src/browser/renderer/BaseRenderLayer.ts | 29 ++++++++++++++++---- src/browser/renderer/CursorRenderLayer.ts | 7 ++--- src/browser/renderer/LinkRenderLayer.ts | 6 ++-- src/browser/renderer/Renderer.ts | 4 +++ src/browser/renderer/SelectionRenderLayer.ts | 8 +++--- src/browser/renderer/TextRenderLayer.ts | 7 ++--- 6 files changed, 40 insertions(+), 21 deletions(-) diff --git a/src/browser/renderer/BaseRenderLayer.ts b/src/browser/renderer/BaseRenderLayer.ts index 7bcc6881..447f5ade 100644 --- a/src/browser/renderer/BaseRenderLayer.ts +++ b/src/browser/renderer/BaseRenderLayer.ts @@ -18,7 +18,6 @@ import { isPowerlineGlyph, throwIfFalsy } from 'browser/renderer/RendererUtils'; import { channels, color, rgba } from 'common/Color'; import { removeElementFromParent } from 'browser/Dom'; import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs'; -import { ISelectionService } from 'browser/services/Services'; export abstract class BaseRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; @@ -30,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; /** @@ -54,8 +57,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { private _rendererId: number, protected readonly _bufferService: IBufferService, protected readonly _optionsService: IOptionsService, - protected readonly _decorationService: IDecorationService, - protected readonly _selectionService: ISelectionService + protected readonly _decorationService: IDecorationService ) { this._canvas = document.createElement('canvas'); this._canvas.classList.add(`xterm-${id}-layer`); @@ -82,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); @@ -461,7 +468,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { // Apply selection foreground if applicable if (!isTop) { - if (this._colors.selectionForeground && this._selectionService.isCellInSelection(x, y)) { + if (this._colors.selectionForeground && this._isCellInSelection(x, y)) { fgOverride = this._colors.selectionForeground.rgba; } } @@ -555,5 +562,17 @@ 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; + } + 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]); + } } diff --git a/src/browser/renderer/CursorRenderLayer.ts b/src/browser/renderer/CursorRenderLayer.ts index 6405390e..3fa576a9 100644 --- a/src/browser/renderer/CursorRenderLayer.ts +++ b/src/browser/renderer/CursorRenderLayer.ts @@ -10,7 +10,7 @@ import { CellData } from 'common/buffer/CellData'; import { IColorSet } from 'browser/Types'; import { IBufferService, IOptionsService, ICoreService, IDecorationService } from 'common/services/Services'; import { IEventEmitter } from 'common/EventEmitter'; -import { ICoreBrowserService, ISelectionService } from 'browser/services/Services'; +import { ICoreBrowserService } from 'browser/services/Services'; interface ICursorState { x: number; @@ -41,10 +41,9 @@ export class CursorRenderLayer extends BaseRenderLayer { @IOptionsService optionsService: IOptionsService, @ICoreService private readonly _coreService: ICoreService, @ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService, - @IDecorationService decorationService: IDecorationService, - @ISelectionService selectionService: ISelectionService + @IDecorationService decorationService: IDecorationService ) { - super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService, selectionService); + super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService); this._state = { x: 0, y: 0, diff --git a/src/browser/renderer/LinkRenderLayer.ts b/src/browser/renderer/LinkRenderLayer.ts index 4d350f4d..15086d9a 100644 --- a/src/browser/renderer/LinkRenderLayer.ts +++ b/src/browser/renderer/LinkRenderLayer.ts @@ -9,7 +9,6 @@ import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; import { is256Color } from 'browser/renderer/atlas/CharAtlasUtils'; import { IColorSet, ILinkifierEvent, ILinkifier, ILinkifier2 } from 'browser/Types'; import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; -import { ISelectionService } from 'browser/services/Services'; export class LinkRenderLayer extends BaseRenderLayer { private _state: ILinkifierEvent | undefined; @@ -23,10 +22,9 @@ export class LinkRenderLayer extends BaseRenderLayer { linkifier2: ILinkifier2, @IBufferService bufferService: IBufferService, @IOptionsService optionsService: IOptionsService, - @IDecorationService decorationService: IDecorationService, - @ISelectionService selectionService: ISelectionService + @IDecorationService decorationService: IDecorationService ) { - super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService, selectionService); + super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService); linkifier.onShowLinkUnderline(e => this._onShowLinkUnderline(e)); linkifier.onHideLinkUnderline(e => this._onHideLinkUnderline(e)); diff --git a/src/browser/renderer/Renderer.ts b/src/browser/renderer/Renderer.ts index 8dfe09c9..8bc32278 100644 --- a/src/browser/renderer/Renderer.ts +++ b/src/browser/renderer/Renderer.ts @@ -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 { diff --git a/src/browser/renderer/SelectionRenderLayer.ts b/src/browser/renderer/SelectionRenderLayer.ts index 60c53eaf..ce4fe071 100644 --- a/src/browser/renderer/SelectionRenderLayer.ts +++ b/src/browser/renderer/SelectionRenderLayer.ts @@ -7,7 +7,6 @@ import { IRenderDimensions } from 'browser/renderer/Types'; import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer'; import { IColorSet } from 'browser/Types'; import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; -import { ISelectionService } from 'browser/services/Services'; interface ISelectionState { start?: [number, number]; @@ -26,10 +25,9 @@ export class SelectionRenderLayer extends BaseRenderLayer { rendererId: number, @IBufferService bufferService: IBufferService, @IOptionsService optionsService: IOptionsService, - @IDecorationService decorationService: IDecorationService, - @ISelectionService selectionService: ISelectionService + @IDecorationService decorationService: IDecorationService ) { - super(container, 'selection', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService, selectionService); + super(container, 'selection', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService); this._clearState(); } @@ -56,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; diff --git a/src/browser/renderer/TextRenderLayer.ts b/src/browser/renderer/TextRenderLayer.ts index 97c7e6f6..ef5a9b62 100644 --- a/src/browser/renderer/TextRenderLayer.ts +++ b/src/browser/renderer/TextRenderLayer.ts @@ -12,7 +12,7 @@ import { NULL_CELL_CODE, Content } from 'common/buffer/Constants'; import { IColorSet } from 'browser/Types'; import { CellData } from 'common/buffer/CellData'; import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services'; -import { ICharacterJoinerService, ISelectionService } from 'browser/services/Services'; +import { ICharacterJoinerService } from 'browser/services/Services'; import { JoinedCellData } from 'browser/services/CharacterJoinerService'; /** @@ -38,10 +38,9 @@ export class TextRenderLayer extends BaseRenderLayer { @IBufferService bufferService: IBufferService, @IOptionsService optionsService: IOptionsService, @ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService, - @IDecorationService decorationService: IDecorationService, - @ISelectionService selectionService: ISelectionService + @IDecorationService decorationService: IDecorationService ) { - super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService, decorationService, selectionService); + super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService, decorationService); this._state = new GridCache(); } From f9bd0b8fed8046f06ba287d27e5bd0d587c5f28c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 17 May 2022 11:39:14 -0700 Subject: [PATCH 06/10] Fix setting selection foreground on webgl --- addons/xterm-addon-webgl/src/WebglRenderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index dbe9da91..e437ca90 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -399,7 +399,7 @@ export class WebglRenderer extends Disposable implements IRenderer { if (this._isCellSelected(x, y)) { bgOverride = this._colors.selectionOpaque.rgba >> 8 & 0xFFFFFF; if (this._colors.selectionForeground) { - fgOverride = this._colors.selectionForeground.rgba >> 8 && 0xFFFFFF; + fgOverride = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF; } } From d6ad4a4fc336225f0393a3b2557b997b3a79ecf5 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 17 May 2022 11:41:13 -0700 Subject: [PATCH 07/10] Remove unneeded dep --- src/browser/renderer/dom/DomRendererRowFactory.test.ts | 3 +-- src/browser/renderer/dom/DomRendererRowFactory.ts | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/browser/renderer/dom/DomRendererRowFactory.test.ts b/src/browser/renderer/dom/DomRendererRowFactory.test.ts index ae7a1434..d50765f0 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.test.ts @@ -10,7 +10,7 @@ import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, DEFAULT_ATTR, FgFlags, import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { IBufferLine } from 'common/Types'; import { CellData } from 'common/buffer/CellData'; -import { MockBufferService, MockCoreService, MockDecorationService, MockOptionsService } from 'common/TestUtils.test'; +import { MockCoreService, MockDecorationService, MockOptionsService } from 'common/TestUtils.test'; import { css } from 'common/Color'; import { MockCharacterJoinerService, MockSelectionService } from 'browser/TestUtils.test'; @@ -51,7 +51,6 @@ describe('DomRendererRowFactory', () => { new MockOptionsService({ drawBoldTextInBrightColors: true }), new MockCoreService(), new MockDecorationService(), - new MockBufferService(80, 30), new MockSelectionService() ); lineData = createEmptyLineData(2); diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index 5ce61113..fc21e638 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -35,7 +35,6 @@ export class DomRendererRowFactory { @IOptionsService private readonly _optionsService: IOptionsService, @ICoreService private readonly _coreService: ICoreService, @IDecorationService private readonly _decorationService: IDecorationService, - @IBufferService private readonly _bufferService: IBufferService, @ISelectionService private readonly _selectionService: ISelectionService ) { } From 18a275147174c5e0a9a2e378016e6ae5faf02b77 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 17 May 2022 11:55:03 -0700 Subject: [PATCH 08/10] Fix selectionForeground handling in test --- src/browser/ColorManager.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/ColorManager.test.ts b/src/browser/ColorManager.test.ts index 926a7df3..96bc82e4 100644 --- a/src/browser/ColorManager.test.ts +++ b/src/browser/ColorManager.test.ts @@ -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); } From d1a87facfd7c7142da4f77b623e876da105a3ff5 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 17 May 2022 12:10:52 -0700 Subject: [PATCH 09/10] Fix selectionForeground column select mode on dom/canvas --- src/browser/renderer/BaseRenderLayer.ts | 4 +++ src/browser/renderer/dom/DomRenderer.ts | 1 + .../dom/DomRendererRowFactory.test.ts | 3 +- .../renderer/dom/DomRendererRowFactory.ts | 31 +++++++++++++++++-- 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/browser/renderer/BaseRenderLayer.ts b/src/browser/renderer/BaseRenderLayer.ts index 447f5ade..ead0844f 100644 --- a/src/browser/renderer/BaseRenderLayer.ts +++ b/src/browser/renderer/BaseRenderLayer.ts @@ -569,6 +569,10 @@ export abstract class BaseRenderLayer implements IRenderLayer { 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]) || diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index 840ef40d..fdeef91c 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -281,6 +281,7 @@ 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 diff --git a/src/browser/renderer/dom/DomRendererRowFactory.test.ts b/src/browser/renderer/dom/DomRendererRowFactory.test.ts index d50765f0..5a374b14 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.test.ts @@ -50,8 +50,7 @@ describe('DomRendererRowFactory', () => { new MockCharacterJoinerService(), new MockOptionsService({ drawBoldTextInBrightColors: true }), new MockCoreService(), - new MockDecorationService(), - new MockSelectionService() + new MockDecorationService() ); lineData = createEmptyLineData(2); }); diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index fc21e638..266d7587 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -28,14 +28,17 @@ 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, @ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService, @IOptionsService private readonly _optionsService: IOptionsService, @ICoreService private readonly _coreService: ICoreService, - @IDecorationService private readonly _decorationService: IDecorationService, - @ISelectionService private readonly _selectionService: ISelectionService + @IDecorationService private readonly _decorationService: IDecorationService ) { } @@ -43,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(); @@ -198,7 +207,7 @@ export class DomRendererRowFactory { // Apply selection foreground if applicable if (!isTop) { - if (this._colors.selectionForeground && this._selectionService.isCellInSelection(x, row)) { + if (this._colors.selectionForeground && this._isCellInSelection(x, row)) { fgColorMode = Attributes.CM_RGB; fg = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF; fgOverride = this._colors.selectionForeground; @@ -293,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 { From 53f05de3e7f3644b5f378fe3414dcb519a7f4fd7 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 17 May 2022 12:11:51 -0700 Subject: [PATCH 10/10] Add selectionForeground webgl unit test --- .../test/WebglRenderer.api.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts index cec1e3b1..d51990a7 100644 --- a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts @@ -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' }));