From 873e3745ec043cea97f219d00980477435785983 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 6 May 2022 07:36:22 -0700 Subject: [PATCH 01/39] Send ctrl modifier in page up/down sequence Part of microsoft/vscode#148685 --- src/common/input/Keyboard.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/common/input/Keyboard.ts b/src/common/input/Keyboard.ts index b4b3dce4..6d916a38 100644 --- a/src/common/input/Keyboard.ts +++ b/src/common/input/Keyboard.ts @@ -230,6 +230,8 @@ export function evaluateKeyboardEvent( // page up if (ev.shiftKey) { result.type = KeyboardResultType.PAGE_UP; + } else if (ev.ctrlKey) { + result.key = C0.ESC + '[5;' + (modifiers + 1) + '~'; } else { result.key = C0.ESC + '[5~'; } @@ -238,6 +240,8 @@ export function evaluateKeyboardEvent( // page down if (ev.shiftKey) { result.type = KeyboardResultType.PAGE_DOWN; + } else if (ev.ctrlKey) { + result.key = C0.ESC + '[6;' + (modifiers + 1) + '~'; } else { result.key = C0.ESC + '[6~'; } From 95979c4a7da47a100133398a03d9ad082a393351 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 9 May 2022 10:34:15 -0700 Subject: [PATCH 02/39] Correct param names/jsdoc in SearchAddon --- addons/xterm-addon-search/src/SearchAddon.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 345b9153..a56b3ab3 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -693,7 +693,6 @@ export class SearchAddon implements ITerminalAddon { * @param element the decoration's element * @param backgroundColor the background color to apply * @param borderColor the border color to apply - * @param result the search result associated with the decoration * @returns */ private _applyStyles(element: HTMLElement, backgroundColor: string | undefined, borderColor: string | undefined): void { @@ -714,13 +713,13 @@ export class SearchAddon implements ITerminalAddon { /** * Creates a decoration for the result and applies styles * @param result the search result for which to create the decoration - * @param color the color to use for the decoration + * @param options the options for the decoration * @returns the {@link IDecoration} or undefined if the marker has already been disposed of */ - private _createResultDecoration(result: ISearchResult, decorations: ISearchDecorationOptions): IDecoration | undefined { + private _createResultDecoration(result: ISearchResult, options: ISearchDecorationOptions): IDecoration | undefined { const terminal = this._terminal!; const marker = terminal.registerMarker(-terminal.buffer.active.baseY - terminal.buffer.active.cursorY + result.row); - if (!marker || !decorations?.matchOverviewRuler) { + if (!marker || !options?.matchOverviewRuler) { return undefined; } const findResultDecoration = terminal.registerDecoration({ @@ -728,10 +727,10 @@ export class SearchAddon implements ITerminalAddon { x: result.col, width: result.size, overviewRulerOptions: this._resultDecorations?.get(marker.line) ? undefined : { - color: decorations.matchOverviewRuler, position: 'center' + color: options.matchOverviewRuler, position: 'center' } }); - findResultDecoration?.onRender((e) => this._applyStyles(e, decorations.matchBackground, decorations.matchBorder)); + findResultDecoration?.onRender((e) => this._applyStyles(e, options.matchBackground, options.matchBorder)); findResultDecoration?.onDispose(() => marker.dispose()); return findResultDecoration; } From b7c0332626f4667aba7705f8d7e867820f4195a0 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 9 May 2022 10:49:58 -0700 Subject: [PATCH 03/39] Only create decoration elements when they are actually rendered Part of microsoft/vscode#145751 --- .../Decorations/BufferDecorationRenderer.ts | 28 ++++++++++--------- src/common/services/DecorationService.ts | 2 ++ 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/browser/Decorations/BufferDecorationRenderer.ts b/src/browser/Decorations/BufferDecorationRenderer.ts index 22dc73e9..dbe30187 100644 --- a/src/browser/Decorations/BufferDecorationRenderer.ts +++ b/src/browser/Decorations/BufferDecorationRenderer.ts @@ -60,17 +60,7 @@ export class BufferDecorationRenderer extends Disposable { } private _renderDecoration(decoration: IInternalDecoration): void { - let element = this._decorationElements.get(decoration); - if (!element) { - element = this._createElement(decoration); - decoration.onDispose(() => this._removeDecoration(decoration)); - decoration.marker.onDispose(() => decoration.dispose()); - decoration.element = element; - this._decorationElements.set(decoration, element); - this._container.appendChild(element); - } - this._refreshStyle(decoration, element); - decoration.onRenderEmitter.fire(element); + this._refreshStyle(decoration); } private _createElement(decoration: IInternalDecoration): HTMLElement { @@ -95,14 +85,26 @@ export class BufferDecorationRenderer extends Disposable { return element; } - private _refreshStyle(decoration: IInternalDecoration, element: HTMLElement): void { + private _refreshStyle(decoration: IInternalDecoration): void { const line = decoration.marker.line - this._bufferService.buffers.active.ydisp; if (line < 0 || line >= this._bufferService.rows) { // outside of viewport - element.style.display = 'none'; + if (decoration.element) { + decoration.element.style.display = 'none'; + decoration.onRenderEmitter.fire(decoration.element); + } } else { + let element = this._decorationElements.get(decoration); + if (!element) { + decoration.onDispose(() => this._removeDecoration(decoration)); + element = this._createElement(decoration); + decoration.element = element; + this._decorationElements.set(decoration, element); + this._container.appendChild(element); + } element.style.top = `${line * this._renderService.dimensions.actualCellHeight}px`; element.style.display = this._altBufferIsActive ? 'none' : 'block'; + decoration.onRenderEmitter.fire(element); } } diff --git a/src/common/services/DecorationService.ts b/src/common/services/DecorationService.ts index 03cfab4d..61936e15 100644 --- a/src/common/services/DecorationService.ts +++ b/src/common/services/DecorationService.ts @@ -30,6 +30,7 @@ export class DecorationService extends Disposable implements IDecorationService } const decoration = new Decoration(options); if (decoration) { + const markerDispose = decoration.marker.onDispose(() => decoration.dispose()); decoration.onDispose(() => { if (decoration) { const index = this._decorations.indexOf(decoration); @@ -37,6 +38,7 @@ export class DecorationService extends Disposable implements IDecorationService this._decorations.splice(this._decorations.indexOf(decoration), 1); this._onDecorationRemoved.fire(decoration); } + markerDispose.dispose(); } }); this._decorations.push(decoration); From 63eb9a45555d728bd320b6dd371c190e1c8fdef1 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 9 May 2022 12:44:51 -0700 Subject: [PATCH 04/39] Initial bg/fg renderer decoration proof of concept Part of #3770 --- demo/client.ts | 12 ++++- src/browser/renderer/dom/DomRenderer.ts | 11 +++-- .../dom/DomRendererRowFactory.test.ts | 5 ++- .../renderer/dom/DomRendererRowFactory.ts | 23 +++++++++- src/common/TestUtils.test.ts | 13 +++++- src/common/services/DecorationService.ts | 45 +++++++++++++++++++ src/common/services/Services.ts | 6 ++- typings/xterm.d.ts | 15 ++++++- 8 files changed, 115 insertions(+), 15 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index 7c21956a..2590d4b0 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -556,8 +556,16 @@ function loadTest() { function addDecoration() { term.options['overviewRulerWidth'] = 15; const marker = term.addMarker(1); - const decoration = term.registerDecoration({ marker, overviewRulerOptions: { color: '#ef292980', position: 'left' } }); - decoration.onRender((e) => e.style.backgroundColor = '#ef292980'); + const decoration = term.registerDecoration({ + marker, + backgroundColor: '#00FF00', + foregroundColor: '#000000', + overviewRulerOptions: { color: '#ef292980', position: 'left' } + }); + decoration.onRender((e: HTMLElement) => { + e.style.right = '100%'; + e.style.backgroundColor = '#ef292980'; + }); } function addOverviewRuler() { diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index ee283399..540da240 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -9,7 +9,7 @@ import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; import { Disposable } from 'common/Lifecycle'; import { IColorSet, ILinkifierEvent, ILinkifier, ILinkifier2 } from 'browser/Types'; import { ICharSizeService } from 'browser/services/Services'; -import { IOptionsService, IBufferService, IInstantiationService } from 'common/services/Services'; +import { IOptionsService, IBufferService, IInstantiationService, IDecorationService } from 'common/services/Services'; import { EventEmitter, IEvent } from 'common/EventEmitter'; import { color } from 'browser/Color'; import { removeElementFromParent } from 'browser/Dom'; @@ -87,11 +87,11 @@ export class DomRenderer extends Disposable implements IRenderer { this._screenElement.appendChild(this._rowContainer); this._screenElement.appendChild(this._selectionContainer); - this._linkifier.onShowLinkUnderline(e => this._onLinkHover(e)); - this._linkifier.onHideLinkUnderline(e => this._onLinkLeave(e)); + this.register(this._linkifier.onShowLinkUnderline(e => this._onLinkHover(e))); + this.register(this._linkifier.onHideLinkUnderline(e => this._onLinkLeave(e))); - this._linkifier2.onShowLinkUnderline(e => this._onLinkHover(e)); - this._linkifier2.onHideLinkUnderline(e => this._onLinkLeave(e)); + this.register(this._linkifier2.onShowLinkUnderline(e => this._onLinkHover(e))); + this.register(this._linkifier2.onHideLinkUnderline(e => this._onLinkLeave(e))); } public dispose(): void { @@ -361,7 +361,6 @@ export class DomRenderer extends Disposable implements IRenderer { for (let y = start; y <= end; y++) { const rowElement = this._rowElements[y]; rowElement.innerText = ''; - const row = y + this._bufferService.buffer.ydisp; const lineData = this._bufferService.buffer.lines.get(row); const cursorStyle = this._optionsService.rawOptions.cursorStyle; diff --git a/src/browser/renderer/dom/DomRendererRowFactory.test.ts b/src/browser/renderer/dom/DomRendererRowFactory.test.ts index f41e5d44..61cd6f40 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 { MockCoreService, MockOptionsService } from 'common/TestUtils.test'; +import { MockCoreService, MockDecorationService, MockOptionsService } from 'common/TestUtils.test'; import { css } from 'browser/Color'; import { MockCharacterJoinerService } from 'browser/TestUtils.test'; @@ -49,7 +49,8 @@ describe('DomRendererRowFactory', () => { } as any, new MockCharacterJoinerService(), new MockOptionsService({ drawBoldTextInBrightColors: true }), - new MockCoreService() + new MockCoreService(), + new MockDecorationService() ); lineData = createEmptyLineData(2); }); diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index 9822ec36..025cfd33 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -7,7 +7,7 @@ import { IBufferLine, ICellData } 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, IOptionsService } from 'common/services/Services'; +import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; import { color, rgba } from 'browser/Color'; import { IColorSet, IColor } from 'browser/Types'; import { ICharacterJoinerService } from 'browser/services/Services'; @@ -33,7 +33,8 @@ export class DomRendererRowFactory { private _colors: IColorSet, @ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService, @IOptionsService private readonly _optionsService: IOptionsService, - @ICoreService private readonly _coreService: ICoreService + @ICoreService private readonly _coreService: ICoreService, + @IDecorationService private readonly _decorationService: IDecorationService ) { } @@ -172,6 +173,23 @@ export class DomRendererRowFactory { bgColorMode = temp2; } + // Apply any decoration foreground/background overrides + const decorations = this._decorationService.getDecorationsOnLine(row); + for (const d of decorations) { + const xmin = d.options.x ?? 0; + const xmax = xmin + (d.options.width ?? 1); + if (x >= xmin && x < xmax) { + if (d.backgroundColorRGB) { + bgColorMode = Attributes.CM_RGB; + bg = (d.backgroundColorRGB[0] << 16) | (d.backgroundColorRGB[1]) << 8 | d.backgroundColorRGB[2]; + } + if (d.foregroundColorRGB) { + fgColorMode = Attributes.CM_RGB; + fg = (d.foregroundColorRGB[0] << 16) | (d.foregroundColorRGB[1]) << 8 | d.foregroundColorRGB[2]; + } + } + } + // Foreground switch (fgColorMode) { case Attributes.CM_P16: @@ -179,6 +197,7 @@ export class DomRendererRowFactory { if (cell.isBold() && fg < 8 && this._optionsService.rawOptions.drawBoldTextInBrightColors) { fg += 8; } + // TODO: Pass in bg override if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.ansi[fg], cell)) { charElement.classList.add(`xterm-fg-${fg}`); } diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index 58f6c709..c937f70c 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum } from 'common/services/Services'; +import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum, IDecorationService, IInternalDecoration } from 'common/services/Services'; import { IEvent, EventEmitter } from 'common/EventEmitter'; import { clone } from 'common/Clone'; import { DEFAULT_OPTIONS } from 'common/services/OptionsService'; @@ -11,6 +11,7 @@ import { IBufferSet, IBuffer } from 'common/buffer/Types'; import { BufferSet } from 'common/buffer/BufferSet'; import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharset, IModes, IAttributeData } from 'common/Types'; import { UnicodeV6 } from 'common/input/UnicodeV6'; +import { IDecorationOptions, IDecoration } from 'xterm'; export class MockBufferService implements IBufferService { public serviceBrand: any; @@ -158,3 +159,13 @@ export class MockUnicodeService implements IUnicodeService { throw new Error('Method not implemented.'); } } + +export class MockDecorationService implements IDecorationService { + public serviceBrand: any; + public get decorations(): IterableIterator { return [].values(); }; + public onDecorationRegistered = new EventEmitter().event; + public onDecorationRemoved = new EventEmitter().event; + public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined { return undefined; } + public *getDecorationsOnLine(line: number): IterableIterator { } + public dispose(): void { } +} diff --git a/src/common/services/DecorationService.ts b/src/common/services/DecorationService.ts index 03cfab4d..16cd2421 100644 --- a/src/common/services/DecorationService.ts +++ b/src/common/services/DecorationService.ts @@ -6,6 +6,7 @@ import { EventEmitter } from 'common/EventEmitter'; import { Disposable } from 'common/Lifecycle'; import { IDecorationService, IInternalDecoration } from 'common/services/Services'; +import { IColorRGB } from 'common/Types'; import { IDecorationOptions, IDecoration, IMarker, IEvent } from 'xterm'; export class DecorationService extends Disposable implements IDecorationService { @@ -45,6 +46,15 @@ export class DecorationService extends Disposable implements IDecorationService return decoration; } + public *getDecorationsOnLine(line: number): IterableIterator { + // TODO: This could be made much faster if _decorations was sorted by line (and col?) + for (const d of this.decorations) { + if (d.marker.line === line) { + yield d; + } + } + } + public dispose(): void { for (const decoration of this._decorations) { this._onDecorationRemoved.fire(decoration); @@ -64,6 +74,32 @@ class Decoration extends Disposable implements IInternalDecoration { private _onDispose = this.register(new EventEmitter()); public readonly onDispose = this._onDispose.event; + // TODO: React to changes on options + private _cachedBg: IColorRGB | undefined | null = null; + public get backgroundColorRGB(): IColorRGB | undefined { + if (this._cachedBg === null) { + if (this.options.backgroundColor) { + this._cachedBg = toColorRGB(this.options.backgroundColor); + } else { + this._cachedBg = undefined; + } + } + return this._cachedBg; + } + + // TODO: React to changes on options + private _cachedFg: IColorRGB | undefined | null = null; + public get foregroundColorRGB(): IColorRGB | undefined { + if (this._cachedFg === null) { + if (this.options.foregroundColor) { + this._cachedFg = toColorRGB(this.options.foregroundColor); + } else { + this._cachedFg = undefined; + } + } + return this._cachedFg; + } + constructor( public readonly options: IDecorationOptions ) { @@ -73,6 +109,7 @@ class Decoration extends Disposable implements IInternalDecoration { this.options.overviewRulerOptions.position = 'full'; } } + public override dispose(): void { if (this._isDisposed) { return; @@ -82,3 +119,11 @@ class Decoration extends Disposable implements IInternalDecoration { super.dispose(); } } + +function toColorRGB(css: string): IColorRGB { + // #rrggbb + if (css.length === 7) { + return [parseInt(css.slice(1, 3), 16), parseInt(css.slice(3, 5), 16), parseInt(css.slice(5, 7), 16)]; + } + throw new Error('css.toColor: Unsupported css format'); +} diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 876d90bc..4e306e63 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -5,7 +5,7 @@ import { IEvent, IEventEmitter } from 'common/EventEmitter'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; -import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes, IAttributeData, ScrollSource, IDisposable } from 'common/Types'; +import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes, IAttributeData, ScrollSource, IDisposable, IColorRGB } from 'common/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; import { IDecorationOptions, IDecoration } from 'xterm'; @@ -308,8 +308,12 @@ export interface IDecorationService extends IDisposable { readonly onDecorationRegistered: IEvent; readonly onDecorationRemoved: IEvent; registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined; + /** Iterates over the decorations on a line (in no particular order). */ + getDecorationsOnLine(line: number): IterableIterator; } export interface IInternalDecoration extends IDecoration { readonly options: IDecorationOptions; + readonly backgroundColorRGB: IColorRGB | undefined; + readonly foregroundColorRGB: IColorRGB | undefined; readonly onRenderEmitter: IEventEmitter; } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 76c228b2..fe1bb979 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -444,7 +444,8 @@ declare module 'xterm' { * This will only take effect when {@link IDecorationOptions.overviewRulerOptions} * were provided initially. */ - options: Pick; + options: Pick; + // options: Pick; } @@ -488,6 +489,18 @@ declare module 'xterm' { */ readonly height?: number; + /** + * The background color of the cell(s). When 2 decorations both set the foreground color the + * last registered decoration will be used. Only the `#RRGGBB` format is supported. + */ + backgroundColor?: string; + + /** + * The foreground color of the cell(s). When 2 decorations both set the foreground color the + * last registered decoration will be used. Only the `#RRGGBB` format is supported. + */ + foregroundColor?: string; + /** * When defined, renders the decoration in the overview ruler to the right * of the terminal. {@link ITerminalOptions.overviewRulerWidth} must be set From 0c6877f46f3bcb4584dd5fadc72bcda671bbe85e Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 9 May 2022 14:00:39 -0700 Subject: [PATCH 05/39] Move color to common, work with IColor IColor is needed for the minimum contrast ratio function currently. --- addons/xterm-addon-webgl/src/GlyphRenderer.ts | 3 ++- .../src/RectangleRenderer.ts | 3 ++- .../src/atlas/CharAtlasUtils.ts | 3 ++- .../src/atlas/WebglCharAtlas.ts | 4 ++-- src/browser/ColorContrastCache.ts | 3 ++- src/browser/ColorManager.ts | 6 ++--- src/browser/Terminal.ts | 2 +- src/browser/Types.d.ts | 8 +------ src/browser/renderer/BaseRenderLayer.ts | 6 ++--- .../renderer/atlas/DynamicCharAtlas.ts | 4 ++-- src/browser/renderer/dom/DomRenderer.ts | 2 +- .../dom/DomRendererRowFactory.test.ts | 2 +- .../renderer/dom/DomRendererRowFactory.ts | 10 ++++---- src/{browser => common}/Color.test.ts | 2 +- src/{browser => common}/Color.ts | 3 +-- src/common/TestUtils.test.ts | 2 +- src/common/Types.d.ts | 5 ++++ src/common/services/DecorationService.ts | 23 +++++++------------ src/common/services/Services.ts | 6 ++--- 19 files changed, 46 insertions(+), 51 deletions(-) rename src/{browser => common}/Color.test.ts (99%) rename src/{browser => common}/Color.ts (98%) diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index e2c37be2..6b9faf88 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -11,7 +11,8 @@ import { fill } from 'common/TypedArrayUtils'; import { slice } from './TypedArray'; import { NULL_CELL_CODE, WHITESPACE_CELL_CODE, Attributes, FgFlags } from 'common/buffer/Constants'; import { Terminal, IBufferLine } from 'xterm'; -import { IColorSet, IColor } from 'browser/Types'; +import { IColor } from 'common/Types'; +import { IColorSet } from 'browser/Types'; import { IRenderDimensions } from 'browser/renderer/Types'; import { AttributeData } from 'common/buffer/AttributeData'; diff --git a/addons/xterm-addon-webgl/src/RectangleRenderer.ts b/addons/xterm-addon-webgl/src/RectangleRenderer.ts index c96cc6bc..ab0b34e9 100644 --- a/addons/xterm-addon-webgl/src/RectangleRenderer.ts +++ b/addons/xterm-addon-webgl/src/RectangleRenderer.ts @@ -8,7 +8,8 @@ import { IRenderModel, IWebGLVertexArrayObject, IWebGL2RenderingContext, ISelect import { fill } from 'common/TypedArrayUtils'; import { Attributes, FgFlags } from 'common/buffer/Constants'; import { Terminal } from 'xterm'; -import { IColorSet, IColor } from 'browser/Types'; +import { IColor } from 'common/Types'; +import { IColorSet } from 'browser/Types'; import { IRenderDimensions } from 'browser/renderer/Types'; import { RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel'; diff --git a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts index 4705796a..0ce893df 100644 --- a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts +++ b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts @@ -6,7 +6,8 @@ import { ICharAtlasConfig } from './Types'; import { Attributes } from 'common/buffer/Constants'; import { Terminal, FontWeight } from 'xterm'; -import { IColorSet, IColor } from 'browser/Types'; +import { IColorSet } from 'browser/Types'; +import { IColor } from 'common/Types'; const NULL_COLOR: IColor = { css: '', diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 3194d397..34107fc5 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -8,10 +8,10 @@ import { DIM_OPACITY, TEXT_BASELINE } from 'browser/renderer/atlas/Constants'; import { IRasterizedGlyph, IBoundingBox, IRasterizedGlyphSet } from '../Types'; import { DEFAULT_COLOR, Attributes } from 'common/buffer/Constants'; import { throwIfFalsy } from '../WebglUtils'; -import { IColor } from 'browser/Types'; +import { IColor } from 'common/Types'; import { IDisposable } from 'xterm'; import { AttributeData } from 'common/buffer/AttributeData'; -import { channels, rgba } from 'browser/Color'; +import { channels, rgba } from 'common/Color'; import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs'; import { isPowerlineGlyph } from 'browser/renderer/RendererUtils'; diff --git a/src/browser/ColorContrastCache.ts b/src/browser/ColorContrastCache.ts index b96b66cc..73b7a0b7 100644 --- a/src/browser/ColorContrastCache.ts +++ b/src/browser/ColorContrastCache.ts @@ -3,7 +3,8 @@ * @license MIT */ -import { IColor, IColorContrastCache } from 'browser/Types'; +import { IColorContrastCache } from 'browser/Types'; +import { IColor } from 'common/Types'; export class ColorContrastCache implements IColorContrastCache { private _color: { [bg: number]: { [fg: number]: IColor | null | undefined } | undefined } = {}; diff --git a/src/browser/ColorManager.ts b/src/browser/ColorManager.ts index e7ac10ba..2d6e4ea5 100644 --- a/src/browser/ColorManager.ts +++ b/src/browser/ColorManager.ts @@ -3,11 +3,11 @@ * @license MIT */ -import { IColorManager, IColor, IColorSet, IColorContrastCache } from 'browser/Types'; +import { IColorManager, IColorSet, IColorContrastCache } from 'browser/Types'; import { ITheme } from 'common/services/Services'; -import { channels, color, css } from 'browser/Color'; +import { channels, color, css } from 'common/Color'; import { ColorContrastCache } from 'browser/ColorContrastCache'; -import { ColorIndex } from 'common/Types'; +import { ColorIndex, IColor } from 'common/Types'; interface IRestoreColorSet { diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index f08d8581..491a209e 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -52,7 +52,7 @@ import { MouseService } from 'browser/services/MouseService'; import { Linkifier2 } from 'browser/Linkifier2'; import { CoreBrowserService } from 'browser/services/CoreBrowserService'; import { CoreTerminal } from 'common/CoreTerminal'; -import { color, rgba } from 'browser/Color'; +import { color, rgba } from 'common/Color'; import { CharacterJoinerService } from 'browser/services/CharacterJoinerService'; import { toRgbString } from 'common/input/XParseColor'; import { BufferDecorationRenderer } from 'browser/Decorations/BufferDecorationRenderer'; diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index 8860bb41..0e83c213 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -5,11 +5,10 @@ import { IDecorationOptions, IDecoration, IDisposable, IMarker, ISelectionPosition } from 'xterm'; import { IEvent } from 'common/EventEmitter'; -import { ICoreTerminal, CharData, ITerminalOptions } from 'common/Types'; +import { ICoreTerminal, CharData, ITerminalOptions, IColor } from 'common/Types'; import { IMouseService, IRenderService } from './services/Services'; import { IBuffer } from 'common/buffer/Types'; import { IFunctionIdentifier, IParams } from 'common/parser/Types'; -import { createDecorator } from 'common/services/ServiceRegistry'; export interface ITerminal extends IPublicTerminal, ICoreTerminal { element: HTMLElement | undefined; @@ -113,11 +112,6 @@ export interface IColorManager { onOptionsChange(key: string): void; } -export interface IColor { - css: string; - rgba: number; // 32-bit int with rgba in each byte -} - export interface IColorSet { foreground: IColor; background: IColor; diff --git a/src/browser/renderer/BaseRenderLayer.ts b/src/browser/renderer/BaseRenderLayer.ts index 90d4f82f..67edcb02 100644 --- a/src/browser/renderer/BaseRenderLayer.ts +++ b/src/browser/renderer/BaseRenderLayer.ts @@ -4,18 +4,18 @@ */ import { IRenderDimensions, IRenderLayer } from 'browser/renderer/Types'; -import { ICellData } from 'common/Types'; +import { ICellData, IColor } from 'common/Types'; import { DEFAULT_COLOR, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE, Attributes } from 'common/buffer/Constants'; import { IGlyphIdentifier } from 'browser/renderer/atlas/Types'; import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, TEXT_BASELINE } from 'browser/renderer/atlas/Constants'; import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas'; import { acquireCharAtlas } from 'browser/renderer/atlas/CharAtlasCache'; import { AttributeData } from 'common/buffer/AttributeData'; -import { IColorSet, IColor } from 'browser/Types'; +import { IColorSet } from 'browser/Types'; import { CellData } from 'common/buffer/CellData'; import { IBufferService, IOptionsService } from 'common/services/Services'; import { isPowerlineGlyph, throwIfFalsy } from 'browser/renderer/RendererUtils'; -import { channels, color, rgba } from 'browser/Color'; +import { channels, color, rgba } from 'common/Color'; import { removeElementFromParent } from 'browser/Dom'; import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs'; diff --git a/src/browser/renderer/atlas/DynamicCharAtlas.ts b/src/browser/renderer/atlas/DynamicCharAtlas.ts index 118dbcd2..88194615 100644 --- a/src/browser/renderer/atlas/DynamicCharAtlas.ts +++ b/src/browser/renderer/atlas/DynamicCharAtlas.ts @@ -9,9 +9,9 @@ import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas'; import { DEFAULT_ANSI_COLORS } from 'browser/ColorManager'; import { LRUMap } from 'browser/renderer/atlas/LRUMap'; import { isFirefox, isSafari } from 'common/Platform'; -import { IColor } from 'browser/Types'; +import { IColor } from 'common/Types'; import { throwIfFalsy } from 'browser/renderer/RendererUtils'; -import { color } from 'browser/Color'; +import { color } from 'common/Color'; // In practice we're probably never going to exhaust a texture this large. For debugging purposes, // however, it can be useful to set this to a really tiny value, to verify that LRU eviction works. diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index 540da240..d15d7eac 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -11,7 +11,7 @@ import { IColorSet, ILinkifierEvent, ILinkifier, ILinkifier2 } from 'browser/Typ import { ICharSizeService } from 'browser/services/Services'; import { IOptionsService, IBufferService, IInstantiationService, IDecorationService } from 'common/services/Services'; import { EventEmitter, IEvent } from 'common/EventEmitter'; -import { color } from 'browser/Color'; +import { color } from 'common/Color'; import { removeElementFromParent } from 'browser/Dom'; const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-'; diff --git a/src/browser/renderer/dom/DomRendererRowFactory.test.ts b/src/browser/renderer/dom/DomRendererRowFactory.test.ts index 61cd6f40..bb511a47 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.test.ts @@ -11,7 +11,7 @@ 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 { css } from 'browser/Color'; +import { css } from 'common/Color'; import { MockCharacterJoinerService } from 'browser/TestUtils.test'; describe('DomRendererRowFactory', () => { diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index 025cfd33..300a8fb9 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -3,13 +3,13 @@ * @license MIT */ -import { IBufferLine, ICellData } from 'common/Types'; +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 { color, rgba } from 'browser/Color'; -import { IColorSet, IColor } from 'browser/Types'; +import { color, rgba } from 'common/Color'; +import { IColorSet } from 'browser/Types'; import { ICharacterJoinerService } from 'browser/services/Services'; import { JoinedCellData } from 'browser/services/CharacterJoinerService'; import { isPowerlineGlyph } from 'browser/renderer/RendererUtils'; @@ -181,11 +181,11 @@ export class DomRendererRowFactory { if (x >= xmin && x < xmax) { if (d.backgroundColorRGB) { bgColorMode = Attributes.CM_RGB; - bg = (d.backgroundColorRGB[0] << 16) | (d.backgroundColorRGB[1]) << 8 | d.backgroundColorRGB[2]; + bg = d.backgroundColorRGB.rgba >> 8; } if (d.foregroundColorRGB) { fgColorMode = Attributes.CM_RGB; - fg = (d.foregroundColorRGB[0] << 16) | (d.foregroundColorRGB[1]) << 8 | d.foregroundColorRGB[2]; + fg = d.foregroundColorRGB.rgba >> 8; } } } diff --git a/src/browser/Color.test.ts b/src/common/Color.test.ts similarity index 99% rename from src/browser/Color.test.ts rename to src/common/Color.test.ts index 0d410930..f16e6ffb 100644 --- a/src/browser/Color.test.ts +++ b/src/common/Color.test.ts @@ -4,7 +4,7 @@ */ import { assert } from 'chai'; -import { channels, color, css, rgb, rgba, toPaddedHex, contrastRatio } from 'browser/Color'; +import { channels, color, css, rgb, rgba, toPaddedHex, contrastRatio } from 'common/Color'; describe('Color', () => { diff --git a/src/browser/Color.ts b/src/common/Color.ts similarity index 98% rename from src/browser/Color.ts rename to src/common/Color.ts index 32e311db..1d00b730 100644 --- a/src/browser/Color.ts +++ b/src/common/Color.ts @@ -3,8 +3,7 @@ * @license MIT */ -import { IColor } from 'browser/Types'; -import { IColorRGB } from 'common/Types'; +import { IColor, IColorRGB } from 'common/Types'; /** * Helper functions where the source type is "channels" (individual color channels as numbers). diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index c937f70c..1ec5a05a 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -162,7 +162,7 @@ export class MockUnicodeService implements IUnicodeService { export class MockDecorationService implements IDecorationService { public serviceBrand: any; - public get decorations(): IterableIterator { return [].values(); }; + public get decorations(): IterableIterator { return [].values(); } public onDecorationRegistered = new EventEmitter().event; public onDecorationRemoved = new EventEmitter().event; public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined { return undefined; } diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index fee426e1..c48b23ea 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -102,6 +102,11 @@ export interface ICharset { } export type CharData = [number, string, number, number]; + +export interface IColor { + css: string; + rgba: number; // 32-bit int with rgba in each byte +} export type IColorRGB = [number, number, number]; export interface IExtendedAttrs { diff --git a/src/common/services/DecorationService.ts b/src/common/services/DecorationService.ts index 16cd2421..d251dc35 100644 --- a/src/common/services/DecorationService.ts +++ b/src/common/services/DecorationService.ts @@ -3,10 +3,11 @@ * @license MIT */ +import { css } from 'common/Color'; import { EventEmitter } from 'common/EventEmitter'; import { Disposable } from 'common/Lifecycle'; import { IDecorationService, IInternalDecoration } from 'common/services/Services'; -import { IColorRGB } from 'common/Types'; +import { IColor } from 'common/Types'; import { IDecorationOptions, IDecoration, IMarker, IEvent } from 'xterm'; export class DecorationService extends Disposable implements IDecorationService { @@ -75,11 +76,11 @@ class Decoration extends Disposable implements IInternalDecoration { public readonly onDispose = this._onDispose.event; // TODO: React to changes on options - private _cachedBg: IColorRGB | undefined | null = null; - public get backgroundColorRGB(): IColorRGB | undefined { + private _cachedBg: IColor | undefined | null = null; + public get backgroundColorRGB(): IColor | undefined { if (this._cachedBg === null) { if (this.options.backgroundColor) { - this._cachedBg = toColorRGB(this.options.backgroundColor); + this._cachedBg = css.toColor(this.options.backgroundColor); } else { this._cachedBg = undefined; } @@ -88,11 +89,11 @@ class Decoration extends Disposable implements IInternalDecoration { } // TODO: React to changes on options - private _cachedFg: IColorRGB | undefined | null = null; - public get foregroundColorRGB(): IColorRGB | undefined { + private _cachedFg: IColor | undefined | null = null; + public get foregroundColorRGB(): IColor | undefined { if (this._cachedFg === null) { if (this.options.foregroundColor) { - this._cachedFg = toColorRGB(this.options.foregroundColor); + this._cachedFg = css.toColor(this.options.foregroundColor); } else { this._cachedFg = undefined; } @@ -119,11 +120,3 @@ class Decoration extends Disposable implements IInternalDecoration { super.dispose(); } } - -function toColorRGB(css: string): IColorRGB { - // #rrggbb - if (css.length === 7) { - return [parseInt(css.slice(1, 3), 16), parseInt(css.slice(3, 5), 16), parseInt(css.slice(5, 7), 16)]; - } - throw new Error('css.toColor: Unsupported css format'); -} diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 4e306e63..e086ff56 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -5,7 +5,7 @@ import { IEvent, IEventEmitter } from 'common/EventEmitter'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; -import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes, IAttributeData, ScrollSource, IDisposable, IColorRGB } from 'common/Types'; +import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes, IAttributeData, ScrollSource, IDisposable, IColorRGB, IColor } from 'common/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; import { IDecorationOptions, IDecoration } from 'xterm'; @@ -313,7 +313,7 @@ export interface IDecorationService extends IDisposable { } export interface IInternalDecoration extends IDecoration { readonly options: IDecorationOptions; - readonly backgroundColorRGB: IColorRGB | undefined; - readonly foregroundColorRGB: IColorRGB | undefined; + readonly backgroundColorRGB: IColor | undefined; + readonly foregroundColorRGB: IColor | undefined; readonly onRenderEmitter: IEventEmitter; } From 10598462d00e36d7b5e7121ad86ca334b5e839ea Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 9 May 2022 15:36:54 -0700 Subject: [PATCH 06/39] Fix color channel used by luminance functions --- src/common/Color.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/common/Color.ts b/src/common/Color.ts index 1d00b730..b197cd66 100644 --- a/src/common/Color.ts +++ b/src/common/Color.ts @@ -172,13 +172,13 @@ export namespace rgba { let fgR = (fgRgba >> 24) & 0xFF; let fgG = (fgRgba >> 16) & 0xFF; let fgB = (fgRgba >> 8) & 0xFF; - let cr = contrastRatio(rgb.relativeLuminance2(fgR, fgB, fgG), rgb.relativeLuminance2(bgR, bgG, bgB)); + let cr = contrastRatio(rgb.relativeLuminance2(fgR, fgG, fgB), rgb.relativeLuminance2(bgR, bgG, bgB)); while (cr < ratio && (fgR > 0 || fgG > 0 || fgB > 0)) { // Reduce by 10% until the ratio is hit fgR -= Math.max(0, Math.ceil(fgR * 0.1)); fgG -= Math.max(0, Math.ceil(fgG * 0.1)); fgB -= Math.max(0, Math.ceil(fgB * 0.1)); - cr = contrastRatio(rgb.relativeLuminance2(fgR, fgB, fgG), rgb.relativeLuminance2(bgR, bgG, bgB)); + cr = contrastRatio(rgb.relativeLuminance2(fgR, fgG, fgB), rgb.relativeLuminance2(bgR, bgG, bgB)); } return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0; } @@ -192,13 +192,13 @@ export namespace rgba { let fgR = (fgRgba >> 24) & 0xFF; let fgG = (fgRgba >> 16) & 0xFF; let fgB = (fgRgba >> 8) & 0xFF; - let cr = contrastRatio(rgb.relativeLuminance2(fgR, fgB, fgG), rgb.relativeLuminance2(bgR, bgG, bgB)); + let cr = contrastRatio(rgb.relativeLuminance2(fgR, fgG, fgB), rgb.relativeLuminance2(bgR, bgG, bgB)); while (cr < ratio && (fgR < 0xFF || fgG < 0xFF || fgB < 0xFF)) { // Increase by 10% until the ratio is hit fgR = Math.min(0xFF, fgR + Math.ceil((255 - fgR) * 0.1)); fgG = Math.min(0xFF, fgG + Math.ceil((255 - fgG) * 0.1)); fgB = Math.min(0xFF, fgB + Math.ceil((255 - fgB) * 0.1)); - cr = contrastRatio(rgb.relativeLuminance2(fgR, fgB, fgG), rgb.relativeLuminance2(bgR, bgG, bgB)); + cr = contrastRatio(rgb.relativeLuminance2(fgR, fgG, fgB), rgb.relativeLuminance2(bgR, bgG, bgB)); } return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0; } From 13abd2aa96169dbcfcbda0450372ffa4b85d5703 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 9 May 2022 15:39:02 -0700 Subject: [PATCH 07/39] Support min contrast ratio in decoration fg/bg --- .../renderer/dom/DomRendererRowFactory.ts | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index 300a8fb9..7a20db5a 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -175,6 +175,8 @@ export class DomRendererRowFactory { // Apply any decoration foreground/background overrides const decorations = this._decorationService.getDecorationsOnLine(row); + let bgOverride: IColor | undefined; + let fgOverride: IColor | undefined; for (const d of decorations) { const xmin = d.options.x ?? 0; const xmax = xmin + (d.options.width ?? 1); @@ -182,10 +184,12 @@ export class DomRendererRowFactory { if (d.backgroundColorRGB) { bgColorMode = Attributes.CM_RGB; bg = d.backgroundColorRGB.rgba >> 8; + bgOverride = d.backgroundColorRGB; } if (d.foregroundColorRGB) { fgColorMode = Attributes.CM_RGB; fg = d.foregroundColorRGB.rgba >> 8; + fgOverride = d.foregroundColorRGB; } } } @@ -198,7 +202,7 @@ export class DomRendererRowFactory { fg += 8; } // TODO: Pass in bg override - if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.ansi[fg], cell)) { + if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.ansi[fg], cell, undefined, undefined)) { charElement.classList.add(`xterm-fg-${fg}`); } break; @@ -208,13 +212,13 @@ export class DomRendererRowFactory { (fg >> 8) & 0xFF, (fg ) & 0xFF ); - if (!this._applyMinimumContrast(charElement, this._colors.background, color, cell)) { + if (!this._applyMinimumContrast(charElement, this._colors.background, color, cell, bgOverride, fgOverride)) { this._addStyle(charElement, `color:#${padStart(fg.toString(16), '0', 6)}`); } break; case Attributes.CM_DEFAULT: default: - if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.foreground, cell)) { + if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.foreground, cell, undefined, undefined)) { if (isInverse) { charElement.classList.add(`xterm-fg-${INVERTED_DEFAULT_COLOR}`); } @@ -228,7 +232,7 @@ export class DomRendererRowFactory { charElement.classList.add(`xterm-bg-${bg}`); break; case Attributes.CM_RGB: - this._addStyle(charElement, `background-color:#${padStart(bg.toString(16), '0', 6)}`); + this._addStyle(charElement, `background-color:#${padStart((bg >>> 0).toString(16), '0', 6)}`); break; case Attributes.CM_DEFAULT: default: @@ -244,18 +248,23 @@ export class DomRendererRowFactory { return fragment; } - private _applyMinimumContrast(element: HTMLElement, bg: IColor, fg: IColor, cell: ICellData): boolean { + private _applyMinimumContrast(element: HTMLElement, bg: IColor, fg: IColor, cell: ICellData, bgOverride: IColor | undefined, fgOverride: IColor | undefined): boolean { if (this._optionsService.rawOptions.minimumContrastRatio === 1 || isPowerlineGlyph(cell.getCode())) { return false; } - // Try get from cache first - let adjustedColor = this._colors.contrastCache.getColor(this._workCell.bg, this._workCell.fg); + // Try get from cache first, only use the cache when there are no decoration overrides + let adjustedColor: IColor | undefined | null = undefined; + if (!bgOverride || !fgOverride) { + adjustedColor = this._colors.contrastCache.getColor(this._workCell.bg, this._workCell.fg); + } // Calculate and store in cache if (adjustedColor === undefined) { - adjustedColor = color.ensureContrastRatio(bg, fg, this._optionsService.rawOptions.minimumContrastRatio); - this._colors.contrastCache.setColor(this._workCell.bg, this._workCell.fg, adjustedColor ?? null); + adjustedColor = color.ensureContrastRatio(bgOverride || bg, fgOverride || fg, this._optionsService.rawOptions.minimumContrastRatio); + if (!bgOverride || !fgOverride) { + this._colors.contrastCache.setColor(this._workCell.bg, this._workCell.fg, adjustedColor ?? null); + } } if (adjustedColor) { From 56c7c5c4eda4ef55005b0370d49087b69c8bb2c6 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 9 May 2022 15:55:33 -0700 Subject: [PATCH 08/39] check if undefined instead of index directly to fix search addon issue (#3767) --- addons/xterm-addon-search/src/SearchAddon.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index a56b3ab3..c92c6c8a 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -128,7 +128,7 @@ export class SearchAddon implements ITerminalAddon { } this._lastSearchOptions = searchOptions; if (searchOptions?.decorations) { - if (this._resultIndex || this._cachedSearchTerm && term !== this._cachedSearchTerm) { + if (this._resultIndex !== undefined || this._cachedSearchTerm && term !== this._cachedSearchTerm) { this._highlightAllMatches(term, searchOptions); } } @@ -306,7 +306,7 @@ export class SearchAddon implements ITerminalAddon { throw new Error('Cannot use addon until it has been loaded'); } this._lastSearchOptions = searchOptions; - if (searchOptions?.decorations && (this._resultIndex || term !== this._cachedSearchTerm)) { + if (searchOptions?.decorations && (this._resultIndex !== undefined || term !== this._cachedSearchTerm)) { this._highlightAllMatches(term, searchOptions); } return this._fireResults(term, this._findPreviousAndSelect(term, searchOptions), searchOptions); From 9f1db12b1bd6f066812671434c2ac6005f7bdea1 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 9 May 2022 16:19:58 -0700 Subject: [PATCH 09/39] bg/fg decorations mostly working in canvas renderer --- demo/client.ts | 2 +- src/browser/renderer/BaseRenderLayer.ts | 56 ++++++++++++++----- src/browser/renderer/CursorRenderLayer.ts | 7 ++- src/browser/renderer/LinkRenderLayer.ts | 7 ++- src/browser/renderer/SelectionRenderLayer.ts | 7 ++- src/browser/renderer/TextRenderLayer.ts | 20 ++++++- .../renderer/atlas/DynamicCharAtlas.ts | 2 +- .../renderer/dom/DomRendererRowFactory.ts | 3 +- 8 files changed, 75 insertions(+), 29 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index 2590d4b0..09359905 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -559,7 +559,7 @@ function addDecoration() { const decoration = term.registerDecoration({ marker, backgroundColor: '#00FF00', - foregroundColor: '#000000', + foregroundColor: '#00FE00', overviewRulerOptions: { color: '#ef292980', position: 'left' } }); decoration.onRender((e: HTMLElement) => { diff --git a/src/browser/renderer/BaseRenderLayer.ts b/src/browser/renderer/BaseRenderLayer.ts index 67edcb02..b815c672 100644 --- a/src/browser/renderer/BaseRenderLayer.ts +++ b/src/browser/renderer/BaseRenderLayer.ts @@ -13,7 +13,7 @@ import { acquireCharAtlas } from 'browser/renderer/atlas/CharAtlasCache'; import { AttributeData } from 'common/buffer/AttributeData'; import { IColorSet } from 'browser/Types'; import { CellData } from 'common/buffer/CellData'; -import { IBufferService, IOptionsService } from 'common/services/Services'; +import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; import { isPowerlineGlyph, throwIfFalsy } from 'browser/renderer/RendererUtils'; import { channels, color, rgba } from 'common/Color'; import { removeElementFromParent } from 'browser/Dom'; @@ -52,7 +52,8 @@ export abstract class BaseRenderLayer implements IRenderLayer { protected _colors: IColorSet, private _rendererId: number, protected readonly _bufferService: IBufferService, - protected readonly _optionsService: IOptionsService + protected readonly _optionsService: IOptionsService, + protected readonly _decorationService: IDecorationService ) { this._canvas = document.createElement('canvas'); this._canvas.classList.add(`xterm-${id}-layer`); @@ -294,7 +295,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param bold Whether the text is bold. */ protected _drawChars(cell: ICellData, x: number, y: number): void { - const contrastColor = this._getContrastColor(cell); + const contrastColor = this._getContrastColor(cell, x, y); // skip cache right away if we draw in RGB // Note: to avoid bad runtime JoinedCellData will be skipped @@ -427,15 +428,35 @@ export abstract class BaseRenderLayer implements IRenderLayer { return `${fontStyle} ${fontWeight} ${this._optionsService.rawOptions.fontSize * window.devicePixelRatio}px ${this._optionsService.rawOptions.fontFamily}`; } - private _getContrastColor(cell: CellData): IColor | undefined { - if (this._optionsService.rawOptions.minimumContrastRatio === 1 || isPowerlineGlyph(cell.getCode())) { + private _getContrastColor(cell: CellData, x: number, y: number): IColor | undefined { + // Get any decoration foreground/background overrides, this must be fetched before the early + // exist but applied after inverse + const decorations = this._decorationService.getDecorationsOnLine(y); + let bgOverride: number | undefined; + let fgOverride: number | undefined; + for (const d of decorations) { + const xmin = d.options.x ?? 0; + const xmax = xmin + (d.options.width ?? 1); + if (x >= xmin && x < xmax) { + if (d.backgroundColorRGB) { + bgOverride = d.backgroundColorRGB.rgba; + } + if (d.foregroundColorRGB) { + fgOverride = d.foregroundColorRGB.rgba; + } + } + } + + if (!bgOverride && !fgOverride && (this._optionsService.rawOptions.minimumContrastRatio === 1 || isPowerlineGlyph(cell.getCode()))) { return undefined; } - // Try get from cache first - const adjustedColor = this._colors.contrastCache.getColor(cell.bg, cell.fg); - if (adjustedColor !== undefined) { - return adjustedColor || undefined; + if (!bgOverride && !fgOverride) { + // Try get from cache + const adjustedColor = this._colors.contrastCache.getColor(cell.bg, cell.fg); + if (adjustedColor !== undefined) { + return adjustedColor || undefined; + } } let fgColor = cell.getFgColor(); @@ -453,13 +474,18 @@ export abstract class BaseRenderLayer implements IRenderLayer { bgColorMode = temp2; } - const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, isInverse); + const bgRgba = this._resolveBackgroundRgba(bgOverride !== undefined ? Attributes.CM_RGB : bgColorMode, bgOverride ?? bgColor, isInverse); const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, isInverse, isBold); - const result = rgba.ensureContrastRatio(bgRgba, fgRgba, this._optionsService.rawOptions.minimumContrastRatio); + let result = rgba.ensureContrastRatio(bgOverride ?? bgRgba, fgOverride ?? fgRgba, this._optionsService.rawOptions.minimumContrastRatio); if (!result) { - this._colors.contrastCache.setColor(cell.bg, cell.fg, null); - return undefined; + if (!bgOverride && !fgOverride) { + this._colors.contrastCache.setColor(cell.bg, cell.fg, null); + return undefined; + } + // If it was an override and there was no contrast change, set as the result + // TODO: This is white when it should be green + result = fgRgba; } const color: IColor = { @@ -470,7 +496,9 @@ export abstract class BaseRenderLayer implements IRenderLayer { ), rgba: result }; - this._colors.contrastCache.setColor(cell.bg, cell.fg, color); + if (!bgOverride && !fgOverride) { + this._colors.contrastCache.setColor(cell.bg, cell.fg, color); + } return color; } diff --git a/src/browser/renderer/CursorRenderLayer.ts b/src/browser/renderer/CursorRenderLayer.ts index ea419cb2..3fa576a9 100644 --- a/src/browser/renderer/CursorRenderLayer.ts +++ b/src/browser/renderer/CursorRenderLayer.ts @@ -8,7 +8,7 @@ import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer'; import { ICellData } from 'common/Types'; import { CellData } from 'common/buffer/CellData'; import { IColorSet } from 'browser/Types'; -import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services'; +import { IBufferService, IOptionsService, ICoreService, IDecorationService } from 'common/services/Services'; import { IEventEmitter } from 'common/EventEmitter'; import { ICoreBrowserService } from 'browser/services/Services'; @@ -40,9 +40,10 @@ export class CursorRenderLayer extends BaseRenderLayer { @IBufferService bufferService: IBufferService, @IOptionsService optionsService: IOptionsService, @ICoreService private readonly _coreService: ICoreService, - @ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService + @ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService, + @IDecorationService decorationService: IDecorationService ) { - super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService); + 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 2492f921..15086d9a 100644 --- a/src/browser/renderer/LinkRenderLayer.ts +++ b/src/browser/renderer/LinkRenderLayer.ts @@ -8,7 +8,7 @@ import { BaseRenderLayer } from './BaseRenderLayer'; 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, IOptionsService } from 'common/services/Services'; +import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; export class LinkRenderLayer extends BaseRenderLayer { private _state: ILinkifierEvent | undefined; @@ -21,9 +21,10 @@ export class LinkRenderLayer extends BaseRenderLayer { linkifier: ILinkifier, linkifier2: ILinkifier2, @IBufferService bufferService: IBufferService, - @IOptionsService optionsService: IOptionsService + @IOptionsService optionsService: IOptionsService, + @IDecorationService decorationService: IDecorationService ) { - super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService); + 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/SelectionRenderLayer.ts b/src/browser/renderer/SelectionRenderLayer.ts index 9054e3ca..be911eb9 100644 --- a/src/browser/renderer/SelectionRenderLayer.ts +++ b/src/browser/renderer/SelectionRenderLayer.ts @@ -6,7 +6,7 @@ import { IRenderDimensions } from 'browser/renderer/Types'; import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer'; import { IColorSet } from 'browser/Types'; -import { IBufferService, IOptionsService } from 'common/services/Services'; +import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; interface ISelectionState { start?: [number, number]; @@ -24,9 +24,10 @@ export class SelectionRenderLayer extends BaseRenderLayer { colors: IColorSet, rendererId: number, @IBufferService bufferService: IBufferService, - @IOptionsService optionsService: IOptionsService + @IOptionsService optionsService: IOptionsService, + @IDecorationService decorationService: IDecorationService ) { - super(container, 'selection', zIndex, true, colors, rendererId, bufferService, optionsService); + super(container, 'selection', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService); this._clearState(); } diff --git a/src/browser/renderer/TextRenderLayer.ts b/src/browser/renderer/TextRenderLayer.ts index 33d942ff..e0f6d831 100644 --- a/src/browser/renderer/TextRenderLayer.ts +++ b/src/browser/renderer/TextRenderLayer.ts @@ -11,7 +11,7 @@ import { AttributeData } from 'common/buffer/AttributeData'; import { NULL_CELL_CODE, Content } from 'common/buffer/Constants'; import { IColorSet } from 'browser/Types'; import { CellData } from 'common/buffer/CellData'; -import { IOptionsService, IBufferService } from 'common/services/Services'; +import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services'; import { ICharacterJoinerService } from 'browser/services/Services'; import { JoinedCellData } from 'browser/services/CharacterJoinerService'; @@ -37,9 +37,10 @@ export class TextRenderLayer extends BaseRenderLayer { rendererId: number, @IBufferService bufferService: IBufferService, @IOptionsService optionsService: IOptionsService, - @ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService + @ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService, + @IDecorationService decorationService: IDecorationService ) { - super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService); + super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService, decorationService); this._state = new GridCache(); } @@ -176,6 +177,19 @@ export class TextRenderLayer extends BaseRenderLayer { nextFillStyle = this._colors.ansi[cell.getBgColor()].css; } + // Get any decoration foreground/background overrides, this must be fetched before the early + // exist but applied after inverse + const decorations = this._decorationService.getDecorationsOnLine(y); + for (const d of decorations) { + const xmin = d.options.x ?? 0; + const xmax = xmin + (d.options.width ?? 1); + if (x >= xmin && x < xmax) { + if (d.backgroundColorRGB) { + nextFillStyle = d.backgroundColorRGB.css; + } + } + } + if (prevFillStyle === null) { // This is either the first iteration, or the default background was set. Either way, we // don't need to draw anything. diff --git a/src/browser/renderer/atlas/DynamicCharAtlas.ts b/src/browser/renderer/atlas/DynamicCharAtlas.ts index 88194615..678f8b70 100644 --- a/src/browser/renderer/atlas/DynamicCharAtlas.ts +++ b/src/browser/renderer/atlas/DynamicCharAtlas.ts @@ -105,7 +105,7 @@ export class DynamicCharAtlas extends BaseCharAtlas { this._cacheMap.prealloc(capacity); // This is useful for debugging - // document.body.appendChild(this._cacheCanvas); + document.body.appendChild(this._cacheCanvas); } public dispose(): void { diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index 7a20db5a..3a8bf87c 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -173,7 +173,8 @@ export class DomRendererRowFactory { bgColorMode = temp2; } - // Apply any decoration foreground/background overrides + // Apply any decoration foreground/background overrides, this must happen after inverse has + // been applied const decorations = this._decorationService.getDecorationsOnLine(row); let bgOverride: IColor | undefined; let fgOverride: IColor | undefined; From 714d4b1cd625e3e21e6fd2e10d145b378c80d7c9 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 10 May 2022 04:05:35 -0700 Subject: [PATCH 10/39] Fix fg color when there is a decoration override in canvas renderer --- src/browser/renderer/BaseRenderLayer.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/browser/renderer/BaseRenderLayer.ts b/src/browser/renderer/BaseRenderLayer.ts index b815c672..a4f1e233 100644 --- a/src/browser/renderer/BaseRenderLayer.ts +++ b/src/browser/renderer/BaseRenderLayer.ts @@ -479,13 +479,12 @@ export abstract class BaseRenderLayer implements IRenderLayer { let result = rgba.ensureContrastRatio(bgOverride ?? bgRgba, fgOverride ?? fgRgba, this._optionsService.rawOptions.minimumContrastRatio); if (!result) { - if (!bgOverride && !fgOverride) { + if (!fgOverride) { this._colors.contrastCache.setColor(cell.bg, cell.fg, null); return undefined; } // If it was an override and there was no contrast change, set as the result - // TODO: This is white when it should be green - result = fgRgba; + result = fgOverride; } const color: IColor = { From d29388394c60cef40dc0aa4aece9aad94ac46945 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 10 May 2022 04:53:31 -0700 Subject: [PATCH 11/39] Get Webgl rendering fg/bg overrides --- addons/xterm-addon-webgl/src/GlyphRenderer.ts | 44 ++++++++++++++++++- .../src/RectangleRenderer.ts | 37 ++++++++++++++-- addons/xterm-addon-webgl/src/WebglAddon.ts | 4 +- addons/xterm-addon-webgl/src/WebglRenderer.ts | 6 ++- .../src/atlas/WebglCharAtlas.ts | 3 ++ addons/xterm-addon-webgl/src/tsconfig.json | 1 + .../renderer/atlas/DynamicCharAtlas.ts | 2 +- 7 files changed, 88 insertions(+), 9 deletions(-) diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index 6b9faf88..8977f0fa 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -15,6 +15,7 @@ import { IColor } from 'common/Types'; import { IColorSet } from 'browser/Types'; import { IRenderDimensions } from 'browser/renderer/Types'; import { AttributeData } from 'common/buffer/AttributeData'; +import { IDecorationService } from 'common/services/Services'; interface IVertices { attributes: Float32Array; @@ -100,7 +101,8 @@ export class GlyphRenderer { private _terminal: Terminal, private _colors: IColorSet, private _gl: IWebGL2RenderingContext, - private _dimensions: IRenderDimensions + private _dimensions: IRenderDimensions, + private readonly _decorationService: IDecorationService ) { const gl = this._gl; const program = throwIfFalsy(createProgram(gl, vertexShaderSource, fragmentShaderSource)); @@ -188,10 +190,48 @@ export class GlyphRenderer { if (!this._atlas) { return; } + + // Get any decoration foreground/background overrides + const decorations = this._decorationService.getDecorationsOnLine(y); + let bgOverride: number | undefined; + let fgOverride: number | undefined; + for (const d of decorations) { + const xmin = d.options.x ?? 0; + const xmax = xmin + (d.options.width ?? 1); + if (x >= xmin && x < xmax) { + if (d.backgroundColorRGB) { + bgOverride = d.backgroundColorRGB.rgba; + } + if (d.foregroundColorRGB) { + fgOverride = d.foregroundColorRGB.rgba; + } + } + } + + // Convert any overrides from rgba to the fg/bg packed format. This resolves the inverse flag + // ahead of time in order to use the correct cache key + if (bgOverride !== undefined) { + // Non-RGB attributes from model + override + force RGB color mode + if (fg & FgFlags.INVERSE) { + bgOverride = (bg & ~Attributes.RGB_MASK) | (fgOverride !== undefined ? fgOverride >> 8 : fg) | Attributes.CM_RGB; + } else { + bgOverride = (bg & ~Attributes.RGB_MASK) | bgOverride >> 8 | Attributes.CM_RGB; + } + } + if (fgOverride !== undefined) { + // Non-RGB attributes from model + force disable inverse + override + force RGB color mode + if (fg & FgFlags.INVERSE) { + fgOverride = (fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | (bgOverride !== undefined ? bgOverride >> 8 : bg) | Attributes.CM_RGB; + } else { + fgOverride = (fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | fgOverride >> 8 | Attributes.CM_RGB; + } + } + + // Get the glyph if (chars && chars.length > 1) { rasterizedGlyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg); } else { - rasterizedGlyph = this._atlas.getRasterizedGlyph(code, bg, fg); + rasterizedGlyph = this._atlas.getRasterizedGlyph(code, bgOverride ?? bg, fgOverride ?? fg); } // Fill empty if no glyph was found diff --git a/addons/xterm-addon-webgl/src/RectangleRenderer.ts b/addons/xterm-addon-webgl/src/RectangleRenderer.ts index ab0b34e9..f25cd4c4 100644 --- a/addons/xterm-addon-webgl/src/RectangleRenderer.ts +++ b/addons/xterm-addon-webgl/src/RectangleRenderer.ts @@ -12,6 +12,7 @@ import { IColor } from 'common/Types'; import { IColorSet } from 'browser/Types'; import { IRenderDimensions } from 'browser/renderer/Types'; import { RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel'; +import { IDecorationService } from 'common/services/Services'; const enum VertexAttribLocations { POSITION = 0, @@ -79,7 +80,8 @@ export class RectangleRenderer { private _terminal: Terminal, private _colors: IColorSet, private _gl: IWebGL2RenderingContext, - private _dimensions: IRenderDimensions + private _dimensions: IRenderDimensions, + private readonly _decorationService: IDecorationService ) { const gl = this._gl; @@ -252,8 +254,37 @@ export class RectangleRenderer { let currentInverse = false; for (let x = 0; x < terminal.cols; x++) { const modelIndex = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL; - const bg = model.cells[modelIndex + RENDER_MODEL_BG_OFFSET]; - const fg = model.cells[modelIndex + RENDER_MODEL_FG_OFFSET]; + + // Get any decoration foreground/background overrides + const decorations = this._decorationService.getDecorationsOnLine(y); + let bgOverride: number | undefined; + let fgOverride: number | undefined; + for (const d of decorations) { + const xmin = d.options.x ?? 0; + const xmax = xmin + (d.options.width ?? 1); + if (x >= xmin && x < xmax) { + if (d.backgroundColorRGB) { + bgOverride = d.backgroundColorRGB.rgba; + } + if (d.foregroundColorRGB) { + fgOverride = d.foregroundColorRGB.rgba; + } + } + } + + // Convert any overrides from rgba to the fg/bg packed format: + // Non RGB attributes from model + RGB from override + force RGB color mode + if (bgOverride !== undefined) { + bgOverride = (model.cells[modelIndex + RENDER_MODEL_BG_OFFSET] & ~Attributes.RGB_MASK) | bgOverride >> 8 | Attributes.CM_RGB; + } + if (fgOverride !== undefined) { + fgOverride = (model.cells[modelIndex + RENDER_MODEL_FG_OFFSET] & ~Attributes.RGB_MASK) | fgOverride >> 8 | Attributes.CM_RGB; + } + + // TODO: This isn't handling invert correctly + const bg = bgOverride ?? model.cells[modelIndex + RENDER_MODEL_BG_OFFSET]; + const fg = fgOverride ?? model.cells[modelIndex + RENDER_MODEL_FG_OFFSET]; + const inverse = !!(fg & FgFlags.INVERSE); if (bg !== currentBg || (fg !== currentFg && (currentInverse || inverse))) { // A rectangle needs to be drawn if going from non-default to another color diff --git a/addons/xterm-addon-webgl/src/WebglAddon.ts b/addons/xterm-addon-webgl/src/WebglAddon.ts index b8bcf5b1..4db072e8 100644 --- a/addons/xterm-addon-webgl/src/WebglAddon.ts +++ b/addons/xterm-addon-webgl/src/WebglAddon.ts @@ -9,6 +9,7 @@ import { ICharacterJoinerService, IRenderService } from 'browser/services/Servic import { IColorSet } from 'browser/Types'; import { EventEmitter } from 'common/EventEmitter'; import { isSafari } from 'common/Platform'; +import { IDecorationService } from 'common/services/Services'; export class WebglAddon implements ITerminalAddon { private _terminal?: Terminal; @@ -30,8 +31,9 @@ export class WebglAddon implements ITerminalAddon { this._terminal = terminal; const renderService: IRenderService = (terminal as any)._core._renderService; const characterJoinerService: ICharacterJoinerService = (terminal as any)._core._characterJoinerService; + const decorationService: IDecorationService = (terminal as any)._core._decorationService; const colors: IColorSet = (terminal as any)._core._colorManager.colors; - this._renderer = new WebglRenderer(terminal, colors, characterJoinerService, this._preserveDrawingBuffer); + this._renderer = new WebglRenderer(terminal, colors, characterJoinerService, decorationService, this._preserveDrawingBuffer); this._renderer.onContextLoss(() => this._onContextLoss.fire()); renderService.setRenderer(this._renderer); } diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index a256b9da..faaa6d2c 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -23,6 +23,7 @@ import { addDisposableDomListener } from 'browser/Lifecycle'; import { ICharacterJoinerService } from 'browser/services/Services'; import { CharData, ICellData } from 'common/Types'; import { AttributeData } from 'common/buffer/AttributeData'; +import { IDecorationService } from 'common/services/Services'; export class WebglRenderer extends Disposable implements IRenderer { private _renderLayers: IRenderLayer[]; @@ -52,6 +53,7 @@ export class WebglRenderer extends Disposable implements IRenderer { private _terminal: Terminal, private _colors: IColorSet, private readonly _characterJoinerService: ICharacterJoinerService, + decorationService: IDecorationService, preserveDrawingBuffer?: boolean ) { super(); @@ -95,8 +97,8 @@ export class WebglRenderer extends Disposable implements IRenderer { this._core.screenElement!.appendChild(this._canvas); - this._rectangleRenderer = new RectangleRenderer(this._terminal, this._colors, this._gl, this.dimensions); - this._glyphRenderer = new GlyphRenderer(this._terminal, this._colors, this._gl, this.dimensions); + this._rectangleRenderer = new RectangleRenderer(this._terminal, this._colors, this._gl, this.dimensions, decorationService); + this._glyphRenderer = new GlyphRenderer(this._terminal, this._colors, this._gl, this.dimensions, decorationService); // Update dimensions and acquire char atlas this.onCharSizeChanged(); diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 34107fc5..9e14d5b8 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -88,6 +88,9 @@ export class WebglCharAtlas implements IDisposable { this._tmpCanvas.width = this._config.scaledCellWidth * 4 + TMP_CANVAS_GLYPH_PADDING * 2; this._tmpCanvas.height = this._config.scaledCellHeight + TMP_CANVAS_GLYPH_PADDING * 2; this._tmpCtx = throwIfFalsy(this._tmpCanvas.getContext('2d', { alpha: this._config.allowTransparency })); + + // This is useful for debugging + document.body.appendChild(this.cacheCanvas); } public dispose(): void { diff --git a/addons/xterm-addon-webgl/src/tsconfig.json b/addons/xterm-addon-webgl/src/tsconfig.json index 0b95491f..b0c9f6be 100644 --- a/addons/xterm-addon-webgl/src/tsconfig.json +++ b/addons/xterm-addon-webgl/src/tsconfig.json @@ -20,6 +20,7 @@ ] }, "strict": true, + "downlevelIteration": true, "types": [ "../../../node_modules/@types/mocha" ] diff --git a/src/browser/renderer/atlas/DynamicCharAtlas.ts b/src/browser/renderer/atlas/DynamicCharAtlas.ts index 678f8b70..88194615 100644 --- a/src/browser/renderer/atlas/DynamicCharAtlas.ts +++ b/src/browser/renderer/atlas/DynamicCharAtlas.ts @@ -105,7 +105,7 @@ export class DynamicCharAtlas extends BaseCharAtlas { this._cacheMap.prealloc(capacity); // This is useful for debugging - document.body.appendChild(this._cacheCanvas); + // document.body.appendChild(this._cacheCanvas); } public dispose(): void { From 40716b8ca5f5ca2119342c3bbd79fd3a5a41b87f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 10 May 2022 05:00:52 -0700 Subject: [PATCH 12/39] Consolidate override logic into model/WebglRenderer --- addons/xterm-addon-webgl/src/GlyphRenderer.ts | 38 +---------- .../src/RectangleRenderer.ts | 31 +-------- addons/xterm-addon-webgl/src/WebglRenderer.ts | 66 +++++++++++++++---- 3 files changed, 58 insertions(+), 77 deletions(-) diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index 8977f0fa..7d35479a 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -191,47 +191,11 @@ export class GlyphRenderer { return; } - // Get any decoration foreground/background overrides - const decorations = this._decorationService.getDecorationsOnLine(y); - let bgOverride: number | undefined; - let fgOverride: number | undefined; - for (const d of decorations) { - const xmin = d.options.x ?? 0; - const xmax = xmin + (d.options.width ?? 1); - if (x >= xmin && x < xmax) { - if (d.backgroundColorRGB) { - bgOverride = d.backgroundColorRGB.rgba; - } - if (d.foregroundColorRGB) { - fgOverride = d.foregroundColorRGB.rgba; - } - } - } - - // Convert any overrides from rgba to the fg/bg packed format. This resolves the inverse flag - // ahead of time in order to use the correct cache key - if (bgOverride !== undefined) { - // Non-RGB attributes from model + override + force RGB color mode - if (fg & FgFlags.INVERSE) { - bgOverride = (bg & ~Attributes.RGB_MASK) | (fgOverride !== undefined ? fgOverride >> 8 : fg) | Attributes.CM_RGB; - } else { - bgOverride = (bg & ~Attributes.RGB_MASK) | bgOverride >> 8 | Attributes.CM_RGB; - } - } - if (fgOverride !== undefined) { - // Non-RGB attributes from model + force disable inverse + override + force RGB color mode - if (fg & FgFlags.INVERSE) { - fgOverride = (fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | (bgOverride !== undefined ? bgOverride >> 8 : bg) | Attributes.CM_RGB; - } else { - fgOverride = (fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | fgOverride >> 8 | Attributes.CM_RGB; - } - } - // Get the glyph if (chars && chars.length > 1) { rasterizedGlyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg); } else { - rasterizedGlyph = this._atlas.getRasterizedGlyph(code, bgOverride ?? bg, fgOverride ?? fg); + rasterizedGlyph = this._atlas.getRasterizedGlyph(code, bg, fg); } // Fill empty if no glyph was found diff --git a/addons/xterm-addon-webgl/src/RectangleRenderer.ts b/addons/xterm-addon-webgl/src/RectangleRenderer.ts index f25cd4c4..ae6258f0 100644 --- a/addons/xterm-addon-webgl/src/RectangleRenderer.ts +++ b/addons/xterm-addon-webgl/src/RectangleRenderer.ts @@ -255,35 +255,8 @@ export class RectangleRenderer { for (let x = 0; x < terminal.cols; x++) { const modelIndex = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL; - // Get any decoration foreground/background overrides - const decorations = this._decorationService.getDecorationsOnLine(y); - let bgOverride: number | undefined; - let fgOverride: number | undefined; - for (const d of decorations) { - const xmin = d.options.x ?? 0; - const xmax = xmin + (d.options.width ?? 1); - if (x >= xmin && x < xmax) { - if (d.backgroundColorRGB) { - bgOverride = d.backgroundColorRGB.rgba; - } - if (d.foregroundColorRGB) { - fgOverride = d.foregroundColorRGB.rgba; - } - } - } - - // Convert any overrides from rgba to the fg/bg packed format: - // Non RGB attributes from model + RGB from override + force RGB color mode - if (bgOverride !== undefined) { - bgOverride = (model.cells[modelIndex + RENDER_MODEL_BG_OFFSET] & ~Attributes.RGB_MASK) | bgOverride >> 8 | Attributes.CM_RGB; - } - if (fgOverride !== undefined) { - fgOverride = (model.cells[modelIndex + RENDER_MODEL_FG_OFFSET] & ~Attributes.RGB_MASK) | fgOverride >> 8 | Attributes.CM_RGB; - } - - // TODO: This isn't handling invert correctly - const bg = bgOverride ?? model.cells[modelIndex + RENDER_MODEL_BG_OFFSET]; - const fg = fgOverride ?? model.cells[modelIndex + RENDER_MODEL_FG_OFFSET]; + const bg = model.cells[modelIndex + RENDER_MODEL_BG_OFFSET]; + const fg = model.cells[modelIndex + RENDER_MODEL_FG_OFFSET]; const inverse = !!(fg & FgFlags.INVERSE); if (bg !== currentBg || (fg !== currentFg && (currentInverse || inverse))) { diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index faaa6d2c..20597401 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -12,7 +12,7 @@ import { RectangleRenderer } from './RectangleRenderer'; import { IWebGL2RenderingContext } from './Types'; import { RenderModel, COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel'; import { Disposable } from 'common/Lifecycle'; -import { Content, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants'; +import { Attributes, Content, FgFlags, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants'; import { Terminal, IEvent } from 'xterm'; import { IRenderLayer } from './renderLayer/Types'; import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types'; @@ -53,7 +53,7 @@ export class WebglRenderer extends Disposable implements IRenderer { private _terminal: Terminal, private _colors: IColorSet, private readonly _characterJoinerService: ICharacterJoinerService, - decorationService: IDecorationService, + private readonly _decorationService: IDecorationService, preserveDrawingBuffer?: boolean ) { super(); @@ -97,8 +97,8 @@ export class WebglRenderer extends Disposable implements IRenderer { this._core.screenElement!.appendChild(this._canvas); - this._rectangleRenderer = new RectangleRenderer(this._terminal, this._colors, this._gl, this.dimensions, decorationService); - this._glyphRenderer = new GlyphRenderer(this._terminal, this._colors, this._gl, this.dimensions, decorationService); + this._rectangleRenderer = new RectangleRenderer(this._terminal, this._colors, this._gl, this.dimensions, _decorationService); + this._glyphRenderer = new GlyphRenderer(this._terminal, this._colors, this._gl, this.dimensions, _decorationService); // Update dimensions and acquire char atlas this.onCharSizeChanged(); @@ -333,14 +333,58 @@ export class WebglRenderer extends Disposable implements IRenderer { let code = cell.getCode(); const i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL; + let bg = cell.bg; + let fg = cell.fg; + + // Get any decoration foreground/background overrides, this happens on the model to avoid + // spreading decoration override logic throughout the different sub-renderers + const decorations = this._decorationService.getDecorationsOnLine(y); + let bgOverride: number | undefined; + let fgOverride: number | undefined; + for (const d of decorations) { + const xmin = d.options.x ?? 0; + const xmax = xmin + (d.options.width ?? 1); + if (x >= xmin && x < xmax) { + if (d.backgroundColorRGB) { + bgOverride = d.backgroundColorRGB.rgba; + } + if (d.foregroundColorRGB) { + fgOverride = d.foregroundColorRGB.rgba; + } + } + } + + // Convert any overrides from rgba to the fg/bg packed format. This resolves the inverse flag + // ahead of time in order to use the correct cache key + if (bgOverride !== undefined) { + // Non-RGB attributes from model + override + force RGB color mode + if (fg & FgFlags.INVERSE) { + bgOverride = (bg & ~Attributes.RGB_MASK) | (fgOverride !== undefined ? fgOverride >> 8 : fg) | Attributes.CM_RGB; + } else { + bgOverride = (bg & ~Attributes.RGB_MASK) | bgOverride >> 8 | Attributes.CM_RGB; + } + } + if (fgOverride !== undefined) { + // Non-RGB attributes from model + force disable inverse + override + force RGB color mode + if (fg & FgFlags.INVERSE) { + fgOverride = (fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | (bgOverride !== undefined ? bgOverride >> 8 : bg) | Attributes.CM_RGB; + } else { + fgOverride = (fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | fgOverride >> 8 | Attributes.CM_RGB; + } + } + + // Use the override if it exists + bg = bgOverride ?? bg; + fg = fgOverride ?? fg; + if (code !== NULL_CELL_CODE) { this._model.lineLengths[y] = x + 1; } // Nothing has changed, no updates needed if (this._model.cells[i] === code && - this._model.cells[i + RENDER_MODEL_BG_OFFSET] === cell.bg && - this._model.cells[i + RENDER_MODEL_FG_OFFSET] === cell.fg) { + this._model.cells[i + RENDER_MODEL_BG_OFFSET] === bg && + this._model.cells[i + RENDER_MODEL_FG_OFFSET] === fg) { continue; } @@ -351,10 +395,10 @@ export class WebglRenderer extends Disposable implements IRenderer { // Cache the results in the model this._model.cells[i] = code; - this._model.cells[i + RENDER_MODEL_BG_OFFSET] = cell.bg; - this._model.cells[i + RENDER_MODEL_FG_OFFSET] = cell.fg; + this._model.cells[i + RENDER_MODEL_BG_OFFSET] = bg; + this._model.cells[i + RENDER_MODEL_FG_OFFSET] = fg; - this._glyphRenderer.updateCell(x, y, code, cell.bg, cell.fg, chars); + this._glyphRenderer.updateCell(x, y, code, bg, fg, chars); if (isJoined) { // Restore work cell @@ -365,8 +409,8 @@ export class WebglRenderer extends Disposable implements IRenderer { const j = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL; this._glyphRenderer.updateCell(x, y, NULL_CELL_CODE, 0, 0, NULL_CELL_CHAR); this._model.cells[j] = NULL_CELL_CODE; - this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._workCell.bg; - this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._workCell.fg; + this._model.cells[j + RENDER_MODEL_BG_OFFSET] = bg; + this._model.cells[j + RENDER_MODEL_FG_OFFSET] = fg; } } } From 0a26cee08bff6c973bfaf18a9e7e4dc878127acc Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 10 May 2022 05:06:20 -0700 Subject: [PATCH 13/39] Pull override color logic into a function --- addons/xterm-addon-webgl/src/WebglRenderer.ts | 110 ++++++++++-------- 1 file changed, 60 insertions(+), 50 deletions(-) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 20597401..9cdcf9cd 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -32,6 +32,7 @@ export class WebglRenderer extends Disposable implements IRenderer { private _model: RenderModel = new RenderModel(); private _workCell: CellData = new CellData(); + private _workColors: { fg: number, bg: number } = { fg: 0, bg: 0 }; private _canvas: HTMLCanvasElement; private _gl: IWebGL2RenderingContext; @@ -333,49 +334,8 @@ export class WebglRenderer extends Disposable implements IRenderer { let code = cell.getCode(); const i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL; - let bg = cell.bg; - let fg = cell.fg; - - // Get any decoration foreground/background overrides, this happens on the model to avoid - // spreading decoration override logic throughout the different sub-renderers - const decorations = this._decorationService.getDecorationsOnLine(y); - let bgOverride: number | undefined; - let fgOverride: number | undefined; - for (const d of decorations) { - const xmin = d.options.x ?? 0; - const xmax = xmin + (d.options.width ?? 1); - if (x >= xmin && x < xmax) { - if (d.backgroundColorRGB) { - bgOverride = d.backgroundColorRGB.rgba; - } - if (d.foregroundColorRGB) { - fgOverride = d.foregroundColorRGB.rgba; - } - } - } - - // Convert any overrides from rgba to the fg/bg packed format. This resolves the inverse flag - // ahead of time in order to use the correct cache key - if (bgOverride !== undefined) { - // Non-RGB attributes from model + override + force RGB color mode - if (fg & FgFlags.INVERSE) { - bgOverride = (bg & ~Attributes.RGB_MASK) | (fgOverride !== undefined ? fgOverride >> 8 : fg) | Attributes.CM_RGB; - } else { - bgOverride = (bg & ~Attributes.RGB_MASK) | bgOverride >> 8 | Attributes.CM_RGB; - } - } - if (fgOverride !== undefined) { - // Non-RGB attributes from model + force disable inverse + override + force RGB color mode - if (fg & FgFlags.INVERSE) { - fgOverride = (fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | (bgOverride !== undefined ? bgOverride >> 8 : bg) | Attributes.CM_RGB; - } else { - fgOverride = (fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | fgOverride >> 8 | Attributes.CM_RGB; - } - } - - // Use the override if it exists - bg = bgOverride ?? bg; - fg = fgOverride ?? fg; + // Load colors/resolve overrides into work colors + this._loadColorsForCell(x, y); if (code !== NULL_CELL_CODE) { this._model.lineLengths[y] = x + 1; @@ -383,8 +343,8 @@ export class WebglRenderer extends Disposable implements IRenderer { // Nothing has changed, no updates needed if (this._model.cells[i] === code && - this._model.cells[i + RENDER_MODEL_BG_OFFSET] === bg && - this._model.cells[i + RENDER_MODEL_FG_OFFSET] === fg) { + this._model.cells[i + RENDER_MODEL_BG_OFFSET] === this._workColors.bg && + this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._workColors.fg) { continue; } @@ -395,10 +355,10 @@ export class WebglRenderer extends Disposable implements IRenderer { // Cache the results in the model this._model.cells[i] = code; - this._model.cells[i + RENDER_MODEL_BG_OFFSET] = bg; - this._model.cells[i + RENDER_MODEL_FG_OFFSET] = fg; + this._model.cells[i + RENDER_MODEL_BG_OFFSET] = this._workColors.bg; + this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._workColors.fg; - this._glyphRenderer.updateCell(x, y, code, bg, fg, chars); + this._glyphRenderer.updateCell(x, y, code, this._workColors.bg, this._workColors.fg, chars); if (isJoined) { // Restore work cell @@ -409,8 +369,8 @@ export class WebglRenderer extends Disposable implements IRenderer { const j = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL; this._glyphRenderer.updateCell(x, y, NULL_CELL_CODE, 0, 0, NULL_CELL_CHAR); this._model.cells[j] = NULL_CELL_CODE; - this._model.cells[j + RENDER_MODEL_BG_OFFSET] = bg; - this._model.cells[j + RENDER_MODEL_FG_OFFSET] = fg; + this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._workColors.bg; + this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._workColors.fg; } } } @@ -422,6 +382,56 @@ export class WebglRenderer extends Disposable implements IRenderer { } } + /** + * Loads colors for the cell into the work colors object. This resolves overrides/inverse if + * necessary which is why the work cell object is not used. + */ + private _loadColorsForCell(x: number, y: number): void { + this._workColors.bg = this._workCell.bg; + this._workColors.fg = this._workCell.fg; + + // Get any decoration foreground/background overrides, this happens on the model to avoid + // spreading decoration override logic throughout the different sub-renderers + const decorations = this._decorationService.getDecorationsOnLine(y); + let bgOverride: number | undefined; + let fgOverride: number | undefined; + for (const d of decorations) { + const xmin = d.options.x ?? 0; + const xmax = xmin + (d.options.width ?? 1); + if (x >= xmin && x < xmax) { + if (d.backgroundColorRGB) { + bgOverride = d.backgroundColorRGB.rgba; + } + if (d.foregroundColorRGB) { + fgOverride = d.foregroundColorRGB.rgba; + } + } + } + + // Convert any overrides from rgba to the fg/bg packed format. This resolves the inverse flag + // ahead of time in order to use the correct cache key + if (bgOverride !== undefined) { + // Non-RGB attributes from model + override + force RGB color mode + if (this._workColors.fg & FgFlags.INVERSE) { + bgOverride = (this._workColors.bg & ~Attributes.RGB_MASK) | (fgOverride !== undefined ? fgOverride >> 8 : this._workColors.fg) | Attributes.CM_RGB; + } else { + bgOverride = (this._workColors.bg & ~Attributes.RGB_MASK) | bgOverride >> 8 | Attributes.CM_RGB; + } + } + if (fgOverride !== undefined) { + // Non-RGB attributes from model + force disable inverse + override + force RGB color mode + if (this._workColors.fg & FgFlags.INVERSE) { + fgOverride = (this._workColors.fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | (bgOverride !== undefined ? bgOverride >> 8 : this._workColors.bg) | Attributes.CM_RGB; + } else { + fgOverride = (this._workColors.fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | fgOverride >> 8 | Attributes.CM_RGB; + } + } + + // Use the override if it exists + this._workColors.bg = bgOverride ?? this._workColors.bg; + this._workColors.fg = fgOverride ?? this._workColors.fg; + } + private _updateSelectionModel(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void { const terminal = this._terminal; From 7c87f5b47a3589f1ba962ca522ebc49c46c6b99f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 10 May 2022 05:31:49 -0700 Subject: [PATCH 14/39] Start to use new API in search addon --- addons/xterm-addon-search/src/SearchAddon.ts | 24 +++++++++---------- .../typings/xterm-addon-search.d.ts | 6 ++--- .../src/atlas/WebglCharAtlas.ts | 3 --- demo/client.ts | 4 ++-- .../renderer/dom/DomRendererRowFactory.ts | 4 ++-- 5 files changed, 19 insertions(+), 22 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 345b9153..0fb08056 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -653,7 +653,7 @@ export class SearchAddon implements ITerminalAddon { * @param result The result to select. * @return Whether a result was selected. */ - private _selectResult(result: ISearchResult | undefined, decorations?: ISearchDecorationOptions, noScroll?: boolean): boolean { + private _selectResult(result: ISearchResult | undefined, options?: ISearchDecorationOptions, noScroll?: boolean): boolean { const terminal = this._terminal!; this._selectedDecoration?.dispose(); if (!result) { @@ -661,18 +661,19 @@ export class SearchAddon implements ITerminalAddon { return false; } terminal.select(result.col, result.row, result.size); - if (decorations?.activeMatchColorOverviewRuler) { + if (options) { const marker = terminal.registerMarker(-terminal.buffer.active.baseY - terminal.buffer.active.cursorY + result.row); if (marker) { this._selectedDecoration = terminal.registerDecoration({ marker, x: result.col, width: result.size, + backgroundColor: options.activeMatchBackground, overviewRulerOptions: { - color: decorations.activeMatchColorOverviewRuler + color: options.activeMatchColorOverviewRuler } }); - this._selectedDecoration?.onRender((e) => this._applyStyles(e, decorations.activeMatchBackground, decorations.activeMatchBorder)); + this._selectedDecoration?.onRender((e) => this._applyStyles(e, options.activeMatchBorder)); this._selectedDecoration?.onDispose(() => marker.dispose()); } } @@ -696,15 +697,12 @@ export class SearchAddon implements ITerminalAddon { * @param result the search result associated with the decoration * @returns */ - private _applyStyles(element: HTMLElement, backgroundColor: string | undefined, borderColor: string | undefined): void { + private _applyStyles(element: HTMLElement, borderColor: string | undefined): void { if (element.clientWidth <= 0) { return; } if (!element.classList.contains('xterm-find-result-decoration')) { element.classList.add('xterm-find-result-decoration'); - if (backgroundColor) { - element.style.backgroundColor = backgroundColor; - } if (borderColor) { element.style.outline = `1px solid ${borderColor}`; } @@ -717,21 +715,23 @@ export class SearchAddon implements ITerminalAddon { * @param color the color to use for the decoration * @returns the {@link IDecoration} or undefined if the marker has already been disposed of */ - private _createResultDecoration(result: ISearchResult, decorations: ISearchDecorationOptions): IDecoration | undefined { + private _createResultDecoration(result: ISearchResult, options: ISearchDecorationOptions): IDecoration | undefined { const terminal = this._terminal!; const marker = terminal.registerMarker(-terminal.buffer.active.baseY - terminal.buffer.active.cursorY + result.row); - if (!marker || !decorations?.matchOverviewRuler) { + if (!marker) { return undefined; } const findResultDecoration = terminal.registerDecoration({ marker, x: result.col, width: result.size, + backgroundColor: options.matchBackground, overviewRulerOptions: this._resultDecorations?.get(marker.line) ? undefined : { - color: decorations.matchOverviewRuler, position: 'center' + color: options.matchOverviewRuler, + position: 'center' } }); - findResultDecoration?.onRender((e) => this._applyStyles(e, decorations.matchBackground, decorations.matchBorder)); + findResultDecoration?.onRender((e) => this._applyStyles(e, options.matchBorder)); findResultDecoration?.onDispose(() => marker.dispose()); return findResultDecoration; } diff --git a/addons/xterm-addon-search/typings/xterm-addon-search.d.ts b/addons/xterm-addon-search/typings/xterm-addon-search.d.ts index 4d683db0..9ed1da62 100644 --- a/addons/xterm-addon-search/typings/xterm-addon-search.d.ts +++ b/addons/xterm-addon-search/typings/xterm-addon-search.d.ts @@ -45,12 +45,12 @@ declare module 'xterm-addon-search' { */ interface ISearchDecorationOptions { /** - * The background color of a match. + * The background color of a match, this must use #RRGGBB format. */ matchBackground?: string; /** - * The border color of a match + * The border color of a match. */ matchBorder?: string; @@ -60,7 +60,7 @@ declare module 'xterm-addon-search' { matchOverviewRuler: string; /** - * The background color for the currently active match. + * The background color for the currently active match, this must use #RRGGBB format. */ activeMatchBackground?: string; diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 9e14d5b8..34107fc5 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -88,9 +88,6 @@ export class WebglCharAtlas implements IDisposable { this._tmpCanvas.width = this._config.scaledCellWidth * 4 + TMP_CANVAS_GLYPH_PADDING * 2; this._tmpCanvas.height = this._config.scaledCellHeight + TMP_CANVAS_GLYPH_PADDING * 2; this._tmpCtx = throwIfFalsy(this._tmpCanvas.getContext('2d', { alpha: this._config.allowTransparency })); - - // This is useful for debugging - document.body.appendChild(this.cacheCanvas); } public dispose(): void { diff --git a/demo/client.ts b/demo/client.ts index 09359905..3996652a 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -110,10 +110,10 @@ function getSearchOptions(e: KeyboardEvent): ISearchOptions { caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked, incremental: e.key !== `Enter`, decorations: (document.getElementById('highlight-all-matches') as HTMLInputElement).checked ? { - matchBackground: '#55575380', + matchBackground: '#232422', matchBorder: '#555753', matchOverviewRuler: '#555753', - activeMatchBackground: '#ef292980', + activeMatchBackground: '#ef2929', activeMatchBorder: '#ef2929', activeMatchColorOverviewRuler: '#ef2929' } : undefined diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index 3a8bf87c..24a7b2c6 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -184,12 +184,12 @@ export class DomRendererRowFactory { if (x >= xmin && x < xmax) { if (d.backgroundColorRGB) { bgColorMode = Attributes.CM_RGB; - bg = d.backgroundColorRGB.rgba >> 8; + bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF; bgOverride = d.backgroundColorRGB; } if (d.foregroundColorRGB) { fgColorMode = Attributes.CM_RGB; - fg = d.foregroundColorRGB.rgba >> 8; + fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF; fgOverride = d.foregroundColorRGB; } } From de574a27fc71ec5544e15d767789d2a1e6dc0bc9 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 10 May 2022 05:44:00 -0700 Subject: [PATCH 15/39] Full re-render when there are decoration changes --- demo/client.ts | 2 +- src/browser/services/RenderService.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index 3996652a..a63864a2 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -114,7 +114,7 @@ function getSearchOptions(e: KeyboardEvent): ISearchOptions { matchBorder: '#555753', matchOverviewRuler: '#555753', activeMatchBackground: '#ef2929', - activeMatchBorder: '#ef2929', + activeMatchBorder: '#ffffff', activeMatchColorOverviewRuler: '#ef2929' } : undefined }; diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index 91b510a3..a789e025 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -10,7 +10,7 @@ import { Disposable } from 'common/Lifecycle'; import { ScreenDprMonitor } from 'browser/ScreenDprMonitor'; import { addDisposableDomListener } from 'browser/Lifecycle'; import { IColorSet, IRenderDebouncer } from 'browser/Types'; -import { IOptionsService, IBufferService } from 'common/services/Services'; +import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services'; import { ICharSizeService, IRenderService } from 'browser/services/Services'; interface ISelectionState { @@ -54,6 +54,7 @@ export class RenderService extends Disposable implements IRenderService { screenElement: HTMLElement, @IOptionsService optionsService: IOptionsService, @ICharSizeService private readonly _charSizeService: ICharSizeService, + @IDecorationService decorationService: IDecorationService, @IBufferService bufferService: IBufferService ) { super(); @@ -67,6 +68,9 @@ export class RenderService extends Disposable implements IRenderService { this._screenDprMonitor.setListener(() => this.onDevicePixelRatioChange()); this.register(this._screenDprMonitor); + // TODO: This will slow things down + this.register(decorationService.onDecorationRegistered(() => this._fullRefresh())); + this.register(decorationService.onDecorationRemoved(() => this._fullRefresh())); this.register(bufferService.onResize(() => this._fullRefresh())); this.register(bufferService.buffers.onBufferActivate(() => this._renderer?.clear())); this.register(optionsService.onOptionChange(() => this._renderer.onOptionsChanged())); From b85f5aacd62c753ca59d4a7bf2b105ae084acfd4 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 10 May 2022 06:14:53 -0700 Subject: [PATCH 16/39] Use correct row for override in canvas renderer --- src/browser/renderer/Renderer.ts | 1 - src/browser/renderer/TextRenderLayer.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/browser/renderer/Renderer.ts b/src/browser/renderer/Renderer.ts index a58893b4..8dfe09c9 100644 --- a/src/browser/renderer/Renderer.ts +++ b/src/browser/renderer/Renderer.ts @@ -14,7 +14,6 @@ import { ICharSizeService } from 'browser/services/Services'; import { IBufferService, IOptionsService, IInstantiationService } from 'common/services/Services'; import { removeTerminalFromCache } from 'browser/renderer/atlas/CharAtlasCache'; import { EventEmitter, IEvent } from 'common/EventEmitter'; -import { IDecorationOptions, IDecoration } from 'xterm'; let nextRendererId = 1; diff --git a/src/browser/renderer/TextRenderLayer.ts b/src/browser/renderer/TextRenderLayer.ts index e0f6d831..e94b53e0 100644 --- a/src/browser/renderer/TextRenderLayer.ts +++ b/src/browser/renderer/TextRenderLayer.ts @@ -179,7 +179,7 @@ export class TextRenderLayer extends BaseRenderLayer { // Get any decoration foreground/background overrides, this must be fetched before the early // exist but applied after inverse - const decorations = this._decorationService.getDecorationsOnLine(y); + const decorations = this._decorationService.getDecorationsOnLine(this._bufferService.buffer.ydisp + y); for (const d of decorations) { const xmin = d.options.x ?? 0; const xmax = xmin + (d.options.width ?? 1); From a67e7286d3547aad8ff37a2e24ff47e4c32cada2 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 10 May 2022 06:16:32 -0700 Subject: [PATCH 17/39] Use correct row for override in webgl renderer --- 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 9cdcf9cd..2a267ff8 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -335,7 +335,7 @@ export class WebglRenderer extends Disposable implements IRenderer { const i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL; // Load colors/resolve overrides into work colors - this._loadColorsForCell(x, y); + this._loadColorsForCell(x, row); if (code !== NULL_CELL_CODE) { this._model.lineLengths[y] = x + 1; From ba61509fdd3aacd16995190e37b5729ed516d397 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 10 May 2022 06:20:48 -0700 Subject: [PATCH 18/39] Remove react to changes idea --- src/common/services/DecorationService.ts | 2 -- typings/xterm.d.ts | 5 ++--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/common/services/DecorationService.ts b/src/common/services/DecorationService.ts index d251dc35..fb7373bf 100644 --- a/src/common/services/DecorationService.ts +++ b/src/common/services/DecorationService.ts @@ -75,7 +75,6 @@ class Decoration extends Disposable implements IInternalDecoration { private _onDispose = this.register(new EventEmitter()); public readonly onDispose = this._onDispose.event; - // TODO: React to changes on options private _cachedBg: IColor | undefined | null = null; public get backgroundColorRGB(): IColor | undefined { if (this._cachedBg === null) { @@ -88,7 +87,6 @@ class Decoration extends Disposable implements IInternalDecoration { return this._cachedBg; } - // TODO: React to changes on options private _cachedFg: IColor | undefined | null = null; public get foregroundColorRGB(): IColor | undefined { if (this._cachedFg === null) { diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index fe1bb979..2de77989 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -445,7 +445,6 @@ declare module 'xterm' { * were provided initially. */ options: Pick; - // options: Pick; } @@ -493,13 +492,13 @@ declare module 'xterm' { * The background color of the cell(s). When 2 decorations both set the foreground color the * last registered decoration will be used. Only the `#RRGGBB` format is supported. */ - backgroundColor?: string; + readonly backgroundColor?: string; /** * The foreground color of the cell(s). When 2 decorations both set the foreground color the * last registered decoration will be used. Only the `#RRGGBB` format is supported. */ - foregroundColor?: string; + readonly foregroundColor?: string; /** * When defined, renders the decoration in the overview ruler to the right From e9643f593669211c8fa8277f23d0af9b3f6bbed6 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 10 May 2022 09:24:09 -0700 Subject: [PATCH 19/39] Ensure texture atlas isn't for overrides used in canvas renderer --- src/browser/renderer/BaseRenderLayer.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/browser/renderer/BaseRenderLayer.ts b/src/browser/renderer/BaseRenderLayer.ts index a4f1e233..f30d95a7 100644 --- a/src/browser/renderer/BaseRenderLayer.ts +++ b/src/browser/renderer/BaseRenderLayer.ts @@ -326,7 +326,22 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._currentGlyphIdentifier.bold = !!cell.isBold(); this._currentGlyphIdentifier.dim = !!cell.isDim(); this._currentGlyphIdentifier.italic = !!cell.isItalic(); - const atlasDidDraw = this._charAtlas?.draw(this._ctx, this._currentGlyphIdentifier, x * this._scaledCellWidth + this._scaledCharLeft, y * this._scaledCellHeight + this._scaledCharTop); + + // Don't try cache the glyph if it uses any decoration foreground/background override. + let hasOverrides = false; + const decorations = this._decorationService.getDecorationsOnLine(y); + for (const d of decorations) { + const xmin = d.options.x ?? 0; + const xmax = xmin + (d.options.width ?? 1); + if (x >= xmin && x < xmax) { + if (d.backgroundColorRGB || d.foregroundColorRGB) { + hasOverrides = true; + break; + } + } + } + + const atlasDidDraw = hasOverrides ? false : this._charAtlas?.draw(this._ctx, this._currentGlyphIdentifier, x * this._scaledCellWidth + this._scaledCharLeft, y * this._scaledCellHeight + this._scaledCharTop); if (!atlasDidDraw) { this._drawUncachedChars(cell, x, y); From 162879555751bb7dcb91d3708e409ba7398c9e04 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 10 May 2022 09:42:12 -0700 Subject: [PATCH 20/39] Fix fg/bg flags being set by negative ints --- addons/xterm-addon-webgl/src/WebglRenderer.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 2a267ff8..495a9740 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -400,10 +400,10 @@ export class WebglRenderer extends Disposable implements IRenderer { const xmax = xmin + (d.options.width ?? 1); if (x >= xmin && x < xmax) { if (d.backgroundColorRGB) { - bgOverride = d.backgroundColorRGB.rgba; + bgOverride = (d.backgroundColorRGB.rgba >> 8) >>> 0 & 0xFFFFFF; } if (d.foregroundColorRGB) { - fgOverride = d.foregroundColorRGB.rgba; + fgOverride = (d.foregroundColorRGB.rgba >> 8) >>> 0 & 0xFFFFFF; } } } @@ -413,17 +413,17 @@ export class WebglRenderer extends Disposable implements IRenderer { if (bgOverride !== undefined) { // Non-RGB attributes from model + override + force RGB color mode if (this._workColors.fg & FgFlags.INVERSE) { - bgOverride = (this._workColors.bg & ~Attributes.RGB_MASK) | (fgOverride !== undefined ? fgOverride >> 8 : this._workColors.fg) | Attributes.CM_RGB; + bgOverride = (this._workColors.bg & ~Attributes.RGB_MASK) | (fgOverride !== undefined ? fgOverride : this._workColors.fg) | Attributes.CM_RGB; } else { - bgOverride = (this._workColors.bg & ~Attributes.RGB_MASK) | bgOverride >> 8 | Attributes.CM_RGB; + bgOverride = (this._workColors.bg & ~Attributes.RGB_MASK) | bgOverride | Attributes.CM_RGB; } } if (fgOverride !== undefined) { // Non-RGB attributes from model + force disable inverse + override + force RGB color mode if (this._workColors.fg & FgFlags.INVERSE) { - fgOverride = (this._workColors.fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | (bgOverride !== undefined ? bgOverride >> 8 : this._workColors.bg) | Attributes.CM_RGB; + fgOverride = (this._workColors.fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | (bgOverride !== undefined ? bgOverride : this._workColors.bg) | Attributes.CM_RGB; } else { - fgOverride = (this._workColors.fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | fgOverride >> 8 | Attributes.CM_RGB; + fgOverride = (this._workColors.fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | fgOverride | Attributes.CM_RGB; } } From afe1d7ed6fa733f897e100452c863be58ea1242d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 10 May 2022 11:48:37 -0700 Subject: [PATCH 21/39] Fix webgl contrast tests since a bug in luminance was fixed --- .../test/WebglRenderer.api.ts | 38 +++++++++++-------- typings/xterm.d.ts | 2 +- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts index 0b86b14b..0b3480ab 100644 --- a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts @@ -6,7 +6,7 @@ import { assert } from 'chai'; import { Browser, Page } from 'playwright'; import { ITheme } from 'xterm'; -import { getBrowserType, launchBrowser, openTerminal, pollFor, writeSync } from '../../../out-test/api/TestUtils'; +import { getBrowserType, launchBrowser, openTerminal, pollFor, timeout, writeSync } from '../../../out-test/api/TestUtils'; import { ITerminalOptions } from '../../../src/common/Types'; const APP = 'http://127.0.0.1:3001/test'; @@ -745,18 +745,18 @@ describe('WebGL Renderer Integration Tests', async () => { await page.evaluate(`window.term.options.minimumContrastRatio = 10;`); await pollFor(page, () => getCellColor(1, 1), [176, 180, 180, 255]); await pollFor(page, () => getCellColor(2, 1), [238, 158, 158, 255]); - await pollFor(page, () => getCellColor(3, 1), [197, 223, 171, 255]); - await pollFor(page, () => getCellColor(4, 1), [235, 221, 158, 255]); - await pollFor(page, () => getCellColor(5, 1), [124, 156, 198, 255]); - await pollFor(page, () => getCellColor(6, 1), [183, 165, 187, 255]); + await pollFor(page, () => getCellColor(3, 1), [152, 198, 110, 255]); + await pollFor(page, () => getCellColor(4, 1), [208, 179, 49, 255]); + await pollFor(page, () => getCellColor(5, 1), [161, 183, 215, 255]); + await pollFor(page, () => getCellColor(6, 1), [191, 174, 194, 255]); await pollFor(page, () => getCellColor(7, 1), [110, 197, 198, 255]); await pollFor(page, () => getCellColor(8, 1), [211, 215, 207, 255]); await pollFor(page, () => getCellColor(1, 2), [183, 185, 183, 255]); await pollFor(page, () => getCellColor(2, 2), [249, 156, 156, 255]); await pollFor(page, () => getCellColor(3, 2), [138, 226, 52, 255]); await pollFor(page, () => getCellColor(4, 2), [252, 233, 79, 255]); - await pollFor(page, () => getCellColor(5, 2), [114, 159, 207, 255]); - await pollFor(page, () => getCellColor(6, 2), [190, 152, 185, 255]); + await pollFor(page, () => getCellColor(5, 2), [154, 186, 221, 255]); + await pollFor(page, () => getCellColor(6, 2), [203, 173, 199, 255]); // Unchanged await pollFor(page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]); await pollFor(page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]); @@ -813,18 +813,18 @@ describe('WebGL Renderer Integration Tests', async () => { await page.evaluate(`window.term.options.minimumContrastRatio = 10;`); await pollFor(page, () => getCellColor(1, 1), [46, 52, 54, 255]); await pollFor(page, () => getCellColor(2, 1), [132, 0, 0, 255]); - await pollFor(page, () => getCellColor(3, 1), [78, 154, 6, 255]); - await pollFor(page, () => getCellColor(4, 1), [114, 93, 0, 255]); - await pollFor(page, () => getCellColor(5, 1), [19, 40, 68, 255]); - await pollFor(page, () => getCellColor(6, 1), [60, 40, 64, 255]); + await pollFor(page, () => getCellColor(3, 1), [36, 72, 0, 255]); + await pollFor(page, () => getCellColor(4, 1), [72, 59, 0, 255]); + await pollFor(page, () => getCellColor(5, 1), [32, 64, 106, 255]); + await pollFor(page, () => getCellColor(6, 1), [75, 51, 80, 255]); await pollFor(page, () => getCellColor(7, 1), [0, 71, 72, 255]); await pollFor(page, () => getCellColor(8, 1), [64, 64, 63, 255]); await pollFor(page, () => getCellColor(1, 2), [61, 63, 59, 255]); await pollFor(page, () => getCellColor(2, 2), [125, 19, 19, 255]); - await pollFor(page, () => getCellColor(3, 2), [89, 146, 32, 255]); - await pollFor(page, () => getCellColor(4, 2), [105, 98, 32, 255]); - await pollFor(page, () => getCellColor(5, 2), [36, 52, 70, 255]); - await pollFor(page, () => getCellColor(6, 2), [64, 45, 63, 255]); + await pollFor(page, () => getCellColor(3, 2), [40, 67, 13, 255]); + await pollFor(page, () => getCellColor(4, 2), [67, 63, 19, 255]); + await pollFor(page, () => getCellColor(5, 2), [45, 65, 87, 255]); + await pollFor(page, () => getCellColor(6, 2), [81, 57, 78, 255]); await pollFor(page, () => getCellColor(7, 2), [13, 67, 67, 255]); await pollFor(page, () => getCellColor(8, 2), [64, 64, 64, 255]); }); @@ -874,6 +874,14 @@ describe('WebGL Renderer Integration Tests', async () => { await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]); }); }); + + describe('decoration color overrides', async () => { + await page.evaluate(` + window.term.registerDecoration({ + x: + }); + `); + }); }); async function getCellColor(col: number, row: number): Promise { diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 2de77989..0c421dc0 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -498,7 +498,7 @@ declare module 'xterm' { * The foreground color of the cell(s). When 2 decorations both set the foreground color the * last registered decoration will be used. Only the `#RRGGBB` format is supported. */ - readonly foregroundColor?: string; + readonly foregroundColor?: string; /** * When defined, renders the decoration in the overview ruler to the right From 6b4df216d30b039dd9ff69381c39891292364ea3 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 10 May 2022 12:08:42 -0700 Subject: [PATCH 22/39] Remove unneeded code --- addons/xterm-addon-webgl/src/GlyphRenderer.ts | 6 +- .../src/RectangleRenderer.ts | 6 +- addons/xterm-addon-webgl/src/WebglRenderer.ts | 4 +- .../test/WebglRenderer.api.ts | 65 +++++++++++++++++-- 4 files changed, 64 insertions(+), 17 deletions(-) diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index 7d35479a..f3fd53a6 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -9,13 +9,12 @@ import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel, IRaster import { COMBINED_CHAR_BIT_MASK, RENDER_MODEL_INDICIES_PER_CELL, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_BG_OFFSET } from './RenderModel'; import { fill } from 'common/TypedArrayUtils'; import { slice } from './TypedArray'; -import { NULL_CELL_CODE, WHITESPACE_CELL_CODE, Attributes, FgFlags } from 'common/buffer/Constants'; +import { NULL_CELL_CODE, Attributes, FgFlags } from 'common/buffer/Constants'; import { Terminal, IBufferLine } from 'xterm'; import { IColor } from 'common/Types'; import { IColorSet } from 'browser/Types'; import { IRenderDimensions } from 'browser/renderer/Types'; import { AttributeData } from 'common/buffer/AttributeData'; -import { IDecorationService } from 'common/services/Services'; interface IVertices { attributes: Float32Array; @@ -101,8 +100,7 @@ export class GlyphRenderer { private _terminal: Terminal, private _colors: IColorSet, private _gl: IWebGL2RenderingContext, - private _dimensions: IRenderDimensions, - private readonly _decorationService: IDecorationService + private _dimensions: IRenderDimensions ) { const gl = this._gl; const program = throwIfFalsy(createProgram(gl, vertexShaderSource, fragmentShaderSource)); diff --git a/addons/xterm-addon-webgl/src/RectangleRenderer.ts b/addons/xterm-addon-webgl/src/RectangleRenderer.ts index ae6258f0..ab0b34e9 100644 --- a/addons/xterm-addon-webgl/src/RectangleRenderer.ts +++ b/addons/xterm-addon-webgl/src/RectangleRenderer.ts @@ -12,7 +12,6 @@ import { IColor } from 'common/Types'; import { IColorSet } from 'browser/Types'; import { IRenderDimensions } from 'browser/renderer/Types'; import { RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel'; -import { IDecorationService } from 'common/services/Services'; const enum VertexAttribLocations { POSITION = 0, @@ -80,8 +79,7 @@ export class RectangleRenderer { private _terminal: Terminal, private _colors: IColorSet, private _gl: IWebGL2RenderingContext, - private _dimensions: IRenderDimensions, - private readonly _decorationService: IDecorationService + private _dimensions: IRenderDimensions ) { const gl = this._gl; @@ -254,10 +252,8 @@ export class RectangleRenderer { let currentInverse = false; for (let x = 0; x < terminal.cols; x++) { const modelIndex = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL; - const bg = model.cells[modelIndex + RENDER_MODEL_BG_OFFSET]; const fg = model.cells[modelIndex + RENDER_MODEL_FG_OFFSET]; - const inverse = !!(fg & FgFlags.INVERSE); if (bg !== currentBg || (fg !== currentFg && (currentInverse || inverse))) { // A rectangle needs to be drawn if going from non-default to another color diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 495a9740..6d84bd26 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -98,8 +98,8 @@ export class WebglRenderer extends Disposable implements IRenderer { this._core.screenElement!.appendChild(this._canvas); - this._rectangleRenderer = new RectangleRenderer(this._terminal, this._colors, this._gl, this.dimensions, _decorationService); - this._glyphRenderer = new GlyphRenderer(this._terminal, this._colors, this._gl, this.dimensions, _decorationService); + this._rectangleRenderer = new RectangleRenderer(this._terminal, this._colors, this._gl, this.dimensions); + this._glyphRenderer = new GlyphRenderer(this._terminal, this._colors, this._gl, this.dimensions); // Update dimensions and acquire char atlas this.onCharSizeChanged(); diff --git a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts index 0b3480ab..15359409 100644 --- a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts @@ -875,12 +875,65 @@ describe('WebGL Renderer Integration Tests', async () => { }); }); - describe('decoration color overrides', async () => { - await page.evaluate(` - window.term.registerDecoration({ - x: - }); - `); + describe.only('decoration color overrides', async () => { + if (areTestsEnabled) { + before(async () => setupBrowser({ rendererType: 'dom', allowTransparency: true })); + after(async () => browser.close()); + beforeEach(async () => page.evaluate(`window.term.reset()`)); + } + + itWebgl('foregroundColor', async () => { + await page.evaluate(` + const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); + window.term.registerDecoration({ + marker, + foregroundColor: '#ff0000', + backgroundColor: '#0000ff' + }); + `); + const data = `â–ˆ`; + await writeSync(page, data); + await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]); + }); + itWebgl('foregroundColor should ignore inverse', async () => { + await page.evaluate(` + const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); + window.term.registerDecoration({ + marker, + foregroundColor: '#ff0000', + backgroundColor: '#0000ff' + }); + `); + const data = `\\x1b[7mâ–ˆ\\x1b0m`; + await writeSync(page, data); + await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]); + }); + itWebgl('backgroundColor', async () => { + await page.evaluate(` + const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); + window.term.registerDecoration({ + marker, + foregroundColor: '#ff0000', + backgroundColor: '#0000ff' + }); + `); + const data = ` `; + await writeSync(page, data); + await pollFor(page, () => getCellColor(1, 1), [0, 0, 255, 255]); + }); + itWebgl('backgroundColor should ignore inverse', async () => { + await page.evaluate(` + const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); + window.term.registerDecoration({ + marker, + foregroundColor: '#ff0000', + backgroundColor: '#0000ff' + }); + `); + const data = `\\x1b[7m \\x1b0m`; + await writeSync(page, data); + await pollFor(page, () => getCellColor(1, 1), [0, 0, 255, 255]); + }); }); }); From edba006045ea2a9acf459385bae442ed0d5ad5d7 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 10 May 2022 12:32:06 -0700 Subject: [PATCH 23/39] Clear all decorations on reset --- src/browser/Terminal.ts | 1 + src/common/TestUtils.test.ts | 1 + src/common/services/DecorationService.ts | 7 +++++++ src/common/services/Services.ts | 1 + 4 files changed, 10 insertions(+) diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index 491a209e..a8accd78 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -1358,6 +1358,7 @@ export class Terminal extends CoreTerminal implements ITerminal { this._setup(); super.reset(); this._selectionService?.reset(); + this._decorationService.reset(); // reattach this._customKeyEventHandler = customKeyEventHandler; diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index 1ec5a05a..10b6b5ef 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -166,6 +166,7 @@ export class MockDecorationService implements IDecorationService { public onDecorationRegistered = new EventEmitter().event; public onDecorationRemoved = new EventEmitter().event; public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined { return undefined; } + public reset(): void { } public *getDecorationsOnLine(line: number): IterableIterator { } public dispose(): void { } } diff --git a/src/common/services/DecorationService.ts b/src/common/services/DecorationService.ts index 00ea3236..3c646501 100644 --- a/src/common/services/DecorationService.ts +++ b/src/common/services/DecorationService.ts @@ -49,6 +49,13 @@ export class DecorationService extends Disposable implements IDecorationService return decoration; } + public reset(): void { + for (let i = 0; i < this._decorations.length; i++) { + this._decorations[0].dispose(); + } + this._decorations.length = 0; + } + public *getDecorationsOnLine(line: number): IterableIterator { // TODO: This could be made much faster if _decorations was sorted by line (and col?) for (const d of this.decorations) { diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index e086ff56..c6d47816 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -308,6 +308,7 @@ export interface IDecorationService extends IDisposable { readonly onDecorationRegistered: IEvent; readonly onDecorationRemoved: IEvent; registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined; + reset(): void; /** Iterates over the decorations on a line (in no particular order). */ getDecorationsOnLine(line: number): IterableIterator; } From abdce874dba252238572a002ffb6d93bf3289874 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 11 May 2022 08:31:09 -0700 Subject: [PATCH 24/39] Fix cases where only one override was with tests --- addons/xterm-addon-webgl/src/WebglRenderer.ts | 32 ++++++++++++----- .../test/WebglRenderer.api.ts | 36 ++++++++++++++++--- 2 files changed, 55 insertions(+), 13 deletions(-) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 6d84bd26..c42c0067 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -412,18 +412,32 @@ export class WebglRenderer extends Disposable implements IRenderer { // ahead of time in order to use the correct cache key if (bgOverride !== undefined) { // Non-RGB attributes from model + override + force RGB color mode - if (this._workColors.fg & FgFlags.INVERSE) { - bgOverride = (this._workColors.bg & ~Attributes.RGB_MASK) | (fgOverride !== undefined ? fgOverride : this._workColors.fg) | Attributes.CM_RGB; - } else { - bgOverride = (this._workColors.bg & ~Attributes.RGB_MASK) | bgOverride | Attributes.CM_RGB; - } + bgOverride = (this._workCell.bg & ~Attributes.RGB_MASK) | bgOverride | Attributes.CM_RGB; } if (fgOverride !== undefined) { // Non-RGB attributes from model + force disable inverse + override + force RGB color mode - if (this._workColors.fg & FgFlags.INVERSE) { - fgOverride = (this._workColors.fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | (bgOverride !== undefined ? bgOverride : this._workColors.bg) | Attributes.CM_RGB; - } else { - fgOverride = (this._workColors.fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | fgOverride | Attributes.CM_RGB; + fgOverride = (this._workCell.fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | fgOverride | Attributes.CM_RGB; + } + + // Handle case where inverse was specified by only one of bgOverride or fgOverride was set, + // resolving the other inverse color and setting the inverse flag if needed. + if (this._workColors.fg & FgFlags.INVERSE) { + if (bgOverride !== undefined && fgOverride === undefined) { + // Resolve bg color type (default color has a different meaning in fg vs bg) + debugger; + if ((this._workColors.bg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) { + fgOverride = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | ((this._colors.background.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB; + } else { + fgOverride = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | this._workColors.bg & (Attributes.RGB_MASK | Attributes.CM_MASK); + } + } + if (bgOverride === undefined && fgOverride !== undefined) { + // Resolve bg color type (default color has a different meaning in fg vs bg) + if ((this._workColors.fg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) { + bgOverride = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | ((this._colors.foreground.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB; + } else { + bgOverride = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | this._workColors.fg & (Attributes.RGB_MASK | Attributes.CM_MASK); + } } } diff --git a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts index 15359409..cec1e3b1 100644 --- a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts @@ -875,9 +875,9 @@ describe('WebGL Renderer Integration Tests', async () => { }); }); - describe.only('decoration color overrides', async () => { + describe('decoration color overrides', async () => { if (areTestsEnabled) { - before(async () => setupBrowser({ rendererType: 'dom', allowTransparency: true })); + before(async () => setupBrowser({ rendererType: 'dom' })); after(async () => browser.close()); beforeEach(async () => page.evaluate(`window.term.reset()`)); } @@ -904,10 +904,24 @@ describe('WebGL Renderer Integration Tests', async () => { backgroundColor: '#0000ff' }); `); - const data = `\\x1b[7mâ–ˆ\\x1b0m`; + const data = `\\x1b[7mâ–ˆ\\x1b[0m`; await writeSync(page, data); await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]); }); + itWebgl('foregroundColor should ignore inverse (only fg on decoration)', async () => { + await page.evaluate(` + const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); + window.term.registerDecoration({ + marker, + width: 2, + foregroundColor: '#ff0000' + }); + `); + const data = `\\x1b[7mâ–ˆ \\x1b[0m`; + await writeSync(page, data); + await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]); // inverse foreground of 'â–ˆ' should be decoration fg override + await pollFor(page, () => getCellColor(2, 1), [255, 255, 255, 255]); // inverse background of ' ' should be default foreground + }); itWebgl('backgroundColor', async () => { await page.evaluate(` const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); @@ -930,10 +944,24 @@ describe('WebGL Renderer Integration Tests', async () => { backgroundColor: '#0000ff' }); `); - const data = `\\x1b[7m \\x1b0m`; + const data = `\\x1b[7m \\x1b[0m`; await writeSync(page, data); await pollFor(page, () => getCellColor(1, 1), [0, 0, 255, 255]); }); + itWebgl('backgroundColor should ignore inverse (only bg on decoration)', async () => { + const data = `\\x1b[7mâ–ˆ \\x1b[0m`; + await writeSync(page, data); + await page.evaluate(` + const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); + window.term.registerDecoration({ + marker, + width: 2, + backgroundColor: '#0000ff' + }); + `); + await pollFor(page, () => getCellColor(1, 1), [0, 0, 0, 255]); // inverse foreground of 'â–ˆ' should be default + await pollFor(page, () => getCellColor(2, 1), [0, 0, 255, 255]); // inverse background of ' ' should be decoration bg override + }); }); }); From c0b4edaf31c2eb1cd44d204757e6e9559a80a100 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 11 May 2022 08:37:42 -0700 Subject: [PATCH 25/39] Reduce duplicate with getDecorationsAtCell method --- addons/xterm-addon-webgl/src/WebglRenderer.ts | 17 ++++------- src/browser/renderer/BaseRenderLayer.ts | 30 +++++++------------ src/browser/renderer/TextRenderLayer.ts | 11 ++----- .../renderer/dom/DomRendererRowFactory.ts | 25 +++++++--------- src/common/TestUtils.test.ts | 3 +- src/common/services/DecorationService.ts | 16 +++++++++- src/common/services/Services.ts | 6 ++-- 7 files changed, 50 insertions(+), 58 deletions(-) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index c42c0067..5d853f0f 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -392,19 +392,14 @@ export class WebglRenderer extends Disposable implements IRenderer { // Get any decoration foreground/background overrides, this happens on the model to avoid // spreading decoration override logic throughout the different sub-renderers - const decorations = this._decorationService.getDecorationsOnLine(y); let bgOverride: number | undefined; let fgOverride: number | undefined; - for (const d of decorations) { - const xmin = d.options.x ?? 0; - const xmax = xmin + (d.options.width ?? 1); - if (x >= xmin && x < xmax) { - if (d.backgroundColorRGB) { - bgOverride = (d.backgroundColorRGB.rgba >> 8) >>> 0 & 0xFFFFFF; - } - if (d.foregroundColorRGB) { - fgOverride = (d.foregroundColorRGB.rgba >> 8) >>> 0 & 0xFFFFFF; - } + for (const d of this._decorationService.getDecorationsAtCell(x, y)) { + if (d.backgroundColorRGB) { + bgOverride = (d.backgroundColorRGB.rgba >> 8) >>> 0 & 0xFFFFFF; + } + if (d.foregroundColorRGB) { + fgOverride = (d.foregroundColorRGB.rgba >> 8) >>> 0 & 0xFFFFFF; } } diff --git a/src/browser/renderer/BaseRenderLayer.ts b/src/browser/renderer/BaseRenderLayer.ts index f30d95a7..e0f3566c 100644 --- a/src/browser/renderer/BaseRenderLayer.ts +++ b/src/browser/renderer/BaseRenderLayer.ts @@ -329,15 +329,10 @@ export abstract class BaseRenderLayer implements IRenderLayer { // Don't try cache the glyph if it uses any decoration foreground/background override. let hasOverrides = false; - const decorations = this._decorationService.getDecorationsOnLine(y); - for (const d of decorations) { - const xmin = d.options.x ?? 0; - const xmax = xmin + (d.options.width ?? 1); - if (x >= xmin && x < xmax) { - if (d.backgroundColorRGB || d.foregroundColorRGB) { - hasOverrides = true; - break; - } + for (const d of this._decorationService.getDecorationsAtCell(x, y)) { + if (d.backgroundColorRGB || d.foregroundColorRGB) { + hasOverrides = true; + break; } } @@ -446,19 +441,14 @@ export abstract class BaseRenderLayer implements IRenderLayer { private _getContrastColor(cell: CellData, x: number, y: number): IColor | undefined { // Get any decoration foreground/background overrides, this must be fetched before the early // exist but applied after inverse - const decorations = this._decorationService.getDecorationsOnLine(y); let bgOverride: number | undefined; let fgOverride: number | undefined; - for (const d of decorations) { - const xmin = d.options.x ?? 0; - const xmax = xmin + (d.options.width ?? 1); - if (x >= xmin && x < xmax) { - if (d.backgroundColorRGB) { - bgOverride = d.backgroundColorRGB.rgba; - } - if (d.foregroundColorRGB) { - fgOverride = d.foregroundColorRGB.rgba; - } + for (const d of this._decorationService.getDecorationsAtCell(x, y)) { + if (d.backgroundColorRGB) { + bgOverride = d.backgroundColorRGB.rgba; + } + if (d.foregroundColorRGB) { + fgOverride = d.foregroundColorRGB.rgba; } } diff --git a/src/browser/renderer/TextRenderLayer.ts b/src/browser/renderer/TextRenderLayer.ts index e94b53e0..193d891d 100644 --- a/src/browser/renderer/TextRenderLayer.ts +++ b/src/browser/renderer/TextRenderLayer.ts @@ -179,14 +179,9 @@ export class TextRenderLayer extends BaseRenderLayer { // Get any decoration foreground/background overrides, this must be fetched before the early // exist but applied after inverse - const decorations = this._decorationService.getDecorationsOnLine(this._bufferService.buffer.ydisp + y); - for (const d of decorations) { - const xmin = d.options.x ?? 0; - const xmax = xmin + (d.options.width ?? 1); - if (x >= xmin && x < xmax) { - if (d.backgroundColorRGB) { - nextFillStyle = d.backgroundColorRGB.css; - } + for (const d of this._decorationService.getDecorationsAtCell(x, this._bufferService.buffer.ydisp + y)) { + if (d.backgroundColorRGB) { + nextFillStyle = d.backgroundColorRGB.css; } } diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index 24a7b2c6..c92f82c6 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -175,23 +175,18 @@ export class DomRendererRowFactory { // Apply any decoration foreground/background overrides, this must happen after inverse has // been applied - const decorations = this._decorationService.getDecorationsOnLine(row); let bgOverride: IColor | undefined; let fgOverride: IColor | undefined; - for (const d of decorations) { - const xmin = d.options.x ?? 0; - const xmax = xmin + (d.options.width ?? 1); - if (x >= xmin && x < xmax) { - if (d.backgroundColorRGB) { - bgColorMode = Attributes.CM_RGB; - bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF; - bgOverride = d.backgroundColorRGB; - } - if (d.foregroundColorRGB) { - fgColorMode = Attributes.CM_RGB; - fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF; - fgOverride = d.foregroundColorRGB; - } + for (const d of this._decorationService.getDecorationsAtCell(x, row)) { + if (d.backgroundColorRGB) { + bgColorMode = Attributes.CM_RGB; + bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF; + bgOverride = d.backgroundColorRGB; + } + if (d.foregroundColorRGB) { + fgColorMode = Attributes.CM_RGB; + fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF; + fgOverride = d.foregroundColorRGB; } } diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index 10b6b5ef..11d9a8c5 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -167,6 +167,7 @@ export class MockDecorationService implements IDecorationService { public onDecorationRemoved = new EventEmitter().event; public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined { return undefined; } public reset(): void { } - public *getDecorationsOnLine(line: number): IterableIterator { } + public *getDecorationsAtLine(line: number): IterableIterator { } + public *getDecorationsAtCell(x: number, line: number): IterableIterator { } public dispose(): void { } } diff --git a/src/common/services/DecorationService.ts b/src/common/services/DecorationService.ts index 3c646501..ed58c813 100644 --- a/src/common/services/DecorationService.ts +++ b/src/common/services/DecorationService.ts @@ -56,7 +56,7 @@ export class DecorationService extends Disposable implements IDecorationService this._decorations.length = 0; } - public *getDecorationsOnLine(line: number): IterableIterator { + public *getDecorationsAtLine(line: number): IterableIterator { // TODO: This could be made much faster if _decorations was sorted by line (and col?) for (const d of this.decorations) { if (d.marker.line === line) { @@ -65,6 +65,20 @@ export class DecorationService extends Disposable implements IDecorationService } } + public *getDecorationsAtCell(x: number, line: number): IterableIterator { + let xmin = 0; + let xmax = 0; + for (const d of this.decorations) { + if (d.marker.line === line) { + xmin = d.options.x ?? 0; + xmax = xmin + (d.options.width ?? 1); + if (x >= xmin && x < xmax) { + yield d; + } + } + } + } + public dispose(): void { for (const decoration of this._decorations) { this._onDecorationRemoved.fire(decoration); diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index c6d47816..82492eb4 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -309,8 +309,10 @@ export interface IDecorationService extends IDisposable { readonly onDecorationRemoved: IEvent; registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined; reset(): void; - /** Iterates over the decorations on a line (in no particular order). */ - getDecorationsOnLine(line: number): IterableIterator; + /** Iterates over the decorations at a line (in no particular order). */ + getDecorationsAtLine(line: number): IterableIterator; + /** Iterates over the decorations at a cell (in no particular order). */ + getDecorationsAtCell(x: number, line: number): IterableIterator; } export interface IInternalDecoration extends IDecoration { readonly options: IDecorationOptions; From 931ee9e89ef776a10b9659ec2d20695ae1220dfc Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 11 May 2022 09:34:25 -0700 Subject: [PATCH 26/39] Maintain decorations sorted by line --- src/common/SortedList.test.ts | 105 +++++++++++++++++++++++ src/common/SortedList.ts | 80 +++++++++++++++++ src/common/services/DecorationService.ts | 47 +++++----- 3 files changed, 207 insertions(+), 25 deletions(-) create mode 100644 src/common/SortedList.test.ts create mode 100644 src/common/SortedList.ts diff --git a/src/common/SortedList.test.ts b/src/common/SortedList.test.ts new file mode 100644 index 00000000..5ecdbb77 --- /dev/null +++ b/src/common/SortedList.test.ts @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert } from 'chai'; +import { SortedList } from 'common/SortedList'; + +const deepStrictEqual = assert.deepStrictEqual; + +describe('SortedList', () => { + let list: SortedList; + function assertList(expected: number[]): void { + deepStrictEqual(Array.from(list.values()), expected); + } + + beforeEach(() => { + list = new SortedList(e => e); + }); + + describe('insert', () => { + it('should maintain sorted values', () => { + list.insert(10); + assertList([10]); + list.insert(8); + assertList([8, 10]); + list.insert(15); + assertList([8, 10, 15]); + list.insert(2); + assertList([2, 8, 10, 15]); + list.insert(1); + assertList([1, 2, 8, 10, 15]); + list.insert(6); + assertList([1, 2, 6, 8, 10, 15]); + }); + it('should allow duplicates of the same key', () => { + list.insert(5); + assertList([5]); + list.insert(5); + assertList([5, 5]); + list.insert(8); + assertList([5, 5, 8]); + list.insert(5); + assertList([5, 5, 5, 8]); + list.insert(8); + assertList([5, 5, 5, 8, 8]); + list.insert(6); + assertList([5, 5, 5, 6, 8, 8]); + }); + }); + it('delete', () => { + list.insert(1); + list.insert(2); + list.insert(4); + list.insert(3); + list.insert(5); + assertList([1, 2, 3, 4, 5]); + list.delete(1); + assertList([2, 3, 4, 5]); + list.delete(3); + assertList([2, 4, 5]); + list.delete(4); + assertList([2, 5]); + list.delete(5); + assertList([2]); + list.delete(2); + assertList([]); + }); + it('getKeyIterator', () => { + list.insert(5); + list.insert(5); + list.insert(8); + list.insert(5); + list.insert(8); + list.insert(6); + assertList([5, 5, 5, 6, 8, 8]); + deepStrictEqual(Array.from(list.getKeyIterator(5)), [5, 5, 5]); + deepStrictEqual(Array.from(list.getKeyIterator(6)), [6]); + deepStrictEqual(Array.from(list.getKeyIterator(8)), [8, 8]); + }); + it('clear', () => { + list.insert(1); + list.insert(2); + list.insert(4); + list.insert(3); + list.insert(5); + list.clear(); + assertList([]); + }); + it('custom key', () => { + const customList = new SortedList<{ key: number }>(e => e.key); + customList.insert({ key: 5 }); + customList.insert({ key: 2 }); + customList.insert({ key: 10 }); + customList.insert({ key: 5 }); + customList.insert({ key: 6 }); + deepStrictEqual(Array.from(customList.values()), [ + { key: 2 }, + { key: 5 }, + { key: 5 }, + { key: 6 }, + { key: 10 } + ]); + }); +}); diff --git a/src/common/SortedList.ts b/src/common/SortedList.ts new file mode 100644 index 00000000..2112a73f --- /dev/null +++ b/src/common/SortedList.ts @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2022 The xterm.js authors. All rights reserved. + * @license MIT + */ + +export class SortedList { + private readonly _array: T[] = []; + + constructor( + private readonly _getKey: (value: T) => number + ) { + } + + public clear(): void { + this._array.length = 0; + } + + public insert(value: T): void { + if (this._array.length === 0) { + this._array.push(value); + return; + } + const i = this._search(this._getKey(value), 0, this._array.length - 1); + this._array.splice(i, 0, value); + } + + public delete(value: T): boolean { + if (this._array.length === 0) { + return false; + } + const key = this._getKey(value); + let i = this._search(key, 0, this._array.length - 1); + if (this._getKey(this._array[i]) !== key) { + return false; + } + do { + if (this._array[i] === value) { + this._array.splice(i, 1); + return true; + } + } while (++i < this._array.length && this._getKey(this._array[i]) === key); + return false; + } + + public *getKeyIterator(key: number): IterableIterator { + if (this._array.length === 0) { + return; + } + let i = this._search(key, 0, this._array.length - 1); + if (this._getKey(this._array[i]) !== key) { + return; + } + do { + yield this._array[i]; + } while (++i < this._array.length && this._getKey(this._array[i]) === key); + } + + public values(): IterableIterator { + return this._array.values(); + } + + private _search(key: number, min: number, max: number): number { + if (max < min) { + return min; + } + let mid = Math.floor((min + max) / 2); + if (this._getKey(this._array[mid]) > key) { + return this._search(key, min, mid - 1); + } + if (this._getKey(this._array[mid]) < key) { + return this._search(key, mid + 1, max); + } + // Value found! Since keys can be duplicates, move the result index back to the lowest index + // that matches the key. + while (mid > 0 && this._getKey(this._array[mid - 1]) === key) { + mid--; + } + return mid; + } +} diff --git a/src/common/services/DecorationService.ts b/src/common/services/DecorationService.ts index ed58c813..58718333 100644 --- a/src/common/services/DecorationService.ts +++ b/src/common/services/DecorationService.ts @@ -7,13 +7,19 @@ import { css } from 'common/Color'; import { EventEmitter } from 'common/EventEmitter'; import { Disposable } from 'common/Lifecycle'; import { IDecorationService, IInternalDecoration } from 'common/services/Services'; +import { SortedList } from 'common/SortedList'; import { IColor } from 'common/Types'; import { IDecorationOptions, IDecoration, IMarker, IEvent } from 'xterm'; export class DecorationService extends Disposable implements IDecorationService { public serviceBrand: any; - private readonly _decorations: IInternalDecoration[] = []; + /** + * A list of all decorations, sorted by the marker's line value. This relies on the fact that + * while marker line values do change, they should all change by the same amount so this should + * never become out of order. + */ + private readonly _decorations: SortedList = new SortedList(e => e.marker.line); private _onDecorationRegistered = this.register(new EventEmitter()); public get onDecorationRegistered(): IEvent { return this._onDecorationRegistered.event; } @@ -35,56 +41,47 @@ export class DecorationService extends Disposable implements IDecorationService const markerDispose = decoration.marker.onDispose(() => decoration.dispose()); decoration.onDispose(() => { if (decoration) { - const index = this._decorations.indexOf(decoration); - if (index >= 0) { - this._decorations.splice(this._decorations.indexOf(decoration), 1); + if (this._decorations.delete(decoration)) { this._onDecorationRemoved.fire(decoration); } markerDispose.dispose(); } }); - this._decorations.push(decoration); + this._decorations.insert(decoration); this._onDecorationRegistered.fire(decoration); } return decoration; } public reset(): void { - for (let i = 0; i < this._decorations.length; i++) { - this._decorations[0].dispose(); + for (const d of this._decorations.values()) { + d.dispose(); } - this._decorations.length = 0; + this._decorations.clear(); } public *getDecorationsAtLine(line: number): IterableIterator { - // TODO: This could be made much faster if _decorations was sorted by line (and col?) - for (const d of this.decorations) { - if (d.marker.line === line) { - yield d; - } - } + return this._decorations.getKeyIterator(line); } public *getDecorationsAtCell(x: number, line: number): IterableIterator { let xmin = 0; let xmax = 0; - for (const d of this.decorations) { - if (d.marker.line === line) { - xmin = d.options.x ?? 0; - xmax = xmin + (d.options.width ?? 1); - if (x >= xmin && x < xmax) { - yield d; - } + for (const d of this._decorations.getKeyIterator(line)) { + console.log('d', d); + xmin = d.options.x ?? 0; + xmax = xmin + (d.options.width ?? 1); + if (x >= xmin && x < xmax) { + yield d; } } } public dispose(): void { - for (const decoration of this._decorations) { - this._onDecorationRemoved.fire(decoration); - decoration.dispose(); + for (const d of this._decorations.values()) { + this._onDecorationRemoved.fire(d); } - this._decorations.length = 0; + this.reset(); } } From 999f2558933cf5df9598615eab2ac588256867b8 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 11 May 2022 09:49:23 -0700 Subject: [PATCH 27/39] Fix sorted list edge case --- addons/xterm-addon-webgl/src/WebglRenderer.ts | 1 - src/common/SortedList.test.ts | 2 ++ src/common/SortedList.ts | 8 ++++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 5d853f0f..e80464da 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -419,7 +419,6 @@ export class WebglRenderer extends Disposable implements IRenderer { if (this._workColors.fg & FgFlags.INVERSE) { if (bgOverride !== undefined && fgOverride === undefined) { // Resolve bg color type (default color has a different meaning in fg vs bg) - debugger; if ((this._workColors.bg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) { fgOverride = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | ((this._colors.background.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB; } else { diff --git a/src/common/SortedList.test.ts b/src/common/SortedList.test.ts index 5ecdbb77..ecafdb8f 100644 --- a/src/common/SortedList.test.ts +++ b/src/common/SortedList.test.ts @@ -74,9 +74,11 @@ describe('SortedList', () => { list.insert(8); list.insert(6); assertList([5, 5, 5, 6, 8, 8]); + deepStrictEqual(Array.from(list.getKeyIterator(1)), []); deepStrictEqual(Array.from(list.getKeyIterator(5)), [5, 5, 5]); deepStrictEqual(Array.from(list.getKeyIterator(6)), [6]); deepStrictEqual(Array.from(list.getKeyIterator(8)), [8, 8]); + deepStrictEqual(Array.from(list.getKeyIterator(9)), []); }); it('clear', () => { list.insert(1); diff --git a/src/common/SortedList.ts b/src/common/SortedList.ts index 2112a73f..051c6702 100644 --- a/src/common/SortedList.ts +++ b/src/common/SortedList.ts @@ -3,6 +3,11 @@ * @license MIT */ +/** + * A generic list that is maintained in sorted order and allows values with duplicate keys. This + * list is based on binary search and as such locating a key will take O(log n) amortized, this + * includes the by key iterator. + */ export class SortedList { private readonly _array: T[] = []; @@ -47,6 +52,9 @@ export class SortedList { return; } let i = this._search(key, 0, this._array.length - 1); + if (i < 0 || i >= this._array.length) { + return; + } if (this._getKey(this._array[i]) !== key) { return; } From 324ea2ecb7adf6ef145de73310e85c3cc692c0d2 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 11 May 2022 09:54:40 -0700 Subject: [PATCH 28/39] Clean up/resolve todos --- src/browser/renderer/dom/DomRendererRowFactory.ts | 1 - src/browser/services/RenderService.ts | 9 ++++++--- src/common/services/DecorationService.ts | 1 - 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index c92f82c6..71dc782a 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -197,7 +197,6 @@ export class DomRendererRowFactory { if (cell.isBold() && fg < 8 && this._optionsService.rawOptions.drawBoldTextInBrightColors) { fg += 8; } - // TODO: Pass in bg override if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.ansi[fg], cell, undefined, undefined)) { charElement.classList.add(`xterm-fg-${fg}`); } diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index a789e025..b2e619fe 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -68,14 +68,17 @@ export class RenderService extends Disposable implements IRenderService { this._screenDprMonitor.setListener(() => this.onDevicePixelRatioChange()); this.register(this._screenDprMonitor); - // TODO: This will slow things down - this.register(decorationService.onDecorationRegistered(() => this._fullRefresh())); - this.register(decorationService.onDecorationRemoved(() => this._fullRefresh())); this.register(bufferService.onResize(() => this._fullRefresh())); this.register(bufferService.buffers.onBufferActivate(() => this._renderer?.clear())); this.register(optionsService.onOptionChange(() => this._renderer.onOptionsChanged())); this.register(this._charSizeService.onCharSizeChange(() => this.onCharSizeChanged())); + // Do a full refresh whenever any decoration is added or removed. This may not actually result + // in changes but since decorations should be used sparingly or added/removed all in the same + // frame this should have minimal performance impact. + this.register(decorationService.onDecorationRegistered(() => this._fullRefresh())); + this.register(decorationService.onDecorationRemoved(() => this._fullRefresh())); + // No need to register this as renderer is explicitly disposed in RenderService.dispose this._renderer.onRequestRedraw(e => this.refreshRows(e.start, e.end, true)); diff --git a/src/common/services/DecorationService.ts b/src/common/services/DecorationService.ts index 58718333..e32abdce 100644 --- a/src/common/services/DecorationService.ts +++ b/src/common/services/DecorationService.ts @@ -68,7 +68,6 @@ export class DecorationService extends Disposable implements IDecorationService let xmin = 0; let xmax = 0; for (const d of this._decorations.getKeyIterator(line)) { - console.log('d', d); xmin = d.options.x ?? 0; xmax = xmin + (d.options.width ?? 1); if (x >= xmin && x < xmax) { From 09aed4d339a89c2fef45f8c8d78fbdd9173ceaec Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 11 May 2022 10:27:35 -0700 Subject: [PATCH 29/39] Correct positioning of decoration elements Fixes #3774 --- src/browser/Decorations/BufferDecorationRenderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/Decorations/BufferDecorationRenderer.ts b/src/browser/Decorations/BufferDecorationRenderer.ts index 22dc73e9..61df134a 100644 --- a/src/browser/Decorations/BufferDecorationRenderer.ts +++ b/src/browser/Decorations/BufferDecorationRenderer.ts @@ -76,7 +76,7 @@ export class BufferDecorationRenderer extends Disposable { private _createElement(decoration: IInternalDecoration): HTMLElement { const element = document.createElement('div'); element.classList.add('xterm-decoration'); - element.style.width = `${(decoration.options.width || 1) * this._renderService.dimensions.actualCellWidth}px`; + element.style.width = `${Math.round((decoration.options.width || 1) * this._renderService.dimensions.actualCellWidth)}px`; element.style.height = `${(decoration.options.height || 1) * this._renderService.dimensions.actualCellHeight}px`; element.style.top = `${(decoration.marker.line - this._bufferService.buffers.active.ydisp) * this._renderService.dimensions.actualCellHeight}px`; element.style.lineHeight = `${this._renderService.dimensions.actualCellHeight}px`; From 181fb1cfe5dcfe81b409906fcc82c29fd61a2385 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 11 May 2022 11:46:44 -0700 Subject: [PATCH 30/39] Remove selection-specific render pass, use bg overrides instead Part of #3778 --- addons/xterm-addon-webgl/src/GlyphRenderer.ts | 99 +------------------ .../src/RectangleRenderer.ts | 81 +-------------- addons/xterm-addon-webgl/src/WebglRenderer.ts | 53 ++++++---- 3 files changed, 38 insertions(+), 195 deletions(-) diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index f3fd53a6..e9055a17 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -6,15 +6,11 @@ import { createProgram, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils'; import { WebglCharAtlas } from './atlas/WebglCharAtlas'; import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel, IRasterizedGlyph } from './Types'; -import { COMBINED_CHAR_BIT_MASK, RENDER_MODEL_INDICIES_PER_CELL, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_BG_OFFSET } from './RenderModel'; import { fill } from 'common/TypedArrayUtils'; -import { slice } from './TypedArray'; -import { NULL_CELL_CODE, Attributes, FgFlags } from 'common/buffer/Constants'; +import { NULL_CELL_CODE } from 'common/buffer/Constants'; import { Terminal, IBufferLine } from 'xterm'; -import { IColor } from 'common/Types'; import { IColorSet } from 'browser/Types'; import { IRenderDimensions } from 'browser/renderer/Types'; -import { AttributeData } from 'common/buffer/AttributeData'; interface IVertices { attributes: Float32Array; @@ -25,7 +21,6 @@ interface IVertices { * working on the next frame. */ attributesBuffers: Float32Array[]; - selectionAttributes: Float32Array; count: number; } @@ -92,8 +87,7 @@ export class GlyphRenderer { attributesBuffers: [ new Float32Array(0), new Float32Array(0) - ], - selectionAttributes: new Float32Array(0) + ] }; constructor( @@ -217,91 +211,6 @@ export class GlyphRenderer { // a_cellpos only changes on resize } - public updateSelection(model: IRenderModel): void { - const terminal = this._terminal; - - this._vertices.selectionAttributes = slice(this._vertices.attributes, 0); - - const bg = (this._colors.selectionOpaque.rgba >>> 8) | Attributes.CM_RGB; - - if (model.selection.columnSelectMode) { - const startCol = model.selection.startCol; - const width = model.selection.endCol - startCol; - const height = model.selection.viewportCappedEndRow - model.selection.viewportCappedStartRow + 1; - for (let y = model.selection.viewportCappedStartRow; y < model.selection.viewportCappedStartRow + height; y++) { - this._updateSelectionRange(startCol, startCol + width, y, model, bg); - } - } else { - // Draw first row - const startCol = model.selection.viewportStartRow === model.selection.viewportCappedStartRow ? model.selection.startCol : 0; - const startRowEndCol = model.selection.viewportCappedStartRow === model.selection.viewportCappedEndRow ? model.selection.endCol : terminal.cols; - this._updateSelectionRange(startCol, startRowEndCol, model.selection.viewportCappedStartRow, model, bg); - - // Draw middle rows - const middleRowsCount = Math.max(model.selection.viewportCappedEndRow - model.selection.viewportCappedStartRow - 1, 0); - for (let y = model.selection.viewportCappedStartRow + 1; y <= model.selection.viewportCappedStartRow + middleRowsCount; y++) { - this._updateSelectionRange(0, startRowEndCol, y, model, bg); - } - - // Draw final row - if (model.selection.viewportCappedStartRow !== model.selection.viewportCappedEndRow) { - // Only draw viewportEndRow if it's not the same as viewportStartRow - const endCol = model.selection.viewportEndRow === model.selection.viewportCappedEndRow ? model.selection.endCol : terminal.cols; - this._updateSelectionRange(0, endCol, model.selection.viewportCappedEndRow, model, bg); - } - } - } - - private _updateSelectionRange(startCol: number, endCol: number, y: number, model: IRenderModel, bg: number): void { - const terminal = this._terminal; - const row = y + terminal.buffer.active.viewportY; - let line: IBufferLine | undefined; - for (let x = startCol; x < endCol; x++) { - const offset = (y * this._terminal.cols + x) * RENDER_MODEL_INDICIES_PER_CELL; - const code = model.cells[offset]; - let fg = model.cells[offset + RENDER_MODEL_FG_OFFSET]; - if (fg & FgFlags.INVERSE) { - const workCell = new AttributeData(); - workCell.fg = fg; - workCell.bg = model.cells[offset + RENDER_MODEL_BG_OFFSET]; - // Get attributes from fg (excluding inverse) and resolve inverse by pullibng rgb colors - // from bg. This is needed since the inverse fg color should be based on the original bg - // color, not on the selection color - fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK | FgFlags.INVERSE); - switch (workCell.getBgColorMode()) { - case Attributes.CM_P16: - case Attributes.CM_P256: - const c = this._getColorFromAnsiIndex(workCell.getBgColor()).rgba; - fg |= (c >> 8) & Attributes.RED_MASK | (c >> 8) & Attributes.GREEN_MASK | (c >> 8) & Attributes.BLUE_MASK; - case Attributes.CM_RGB: - const arr = AttributeData.toColorRGB(workCell.getBgColor()); - fg |= arr[0] << Attributes.RED_SHIFT | arr[1] << Attributes.GREEN_SHIFT | arr[2] << Attributes.BLUE_SHIFT; - case Attributes.CM_DEFAULT: - default: - const c2 = this._colors.background.rgba; - fg |= (c2 >> 8) & Attributes.RED_MASK | (c2 >> 8) & Attributes.GREEN_MASK | (c2 >> 8) & Attributes.BLUE_MASK; - } - fg |= Attributes.CM_RGB; - } - if (code & COMBINED_CHAR_BIT_MASK) { - if (!line) { - line = terminal.buffer.active.getLine(row); - } - const chars = line!.getCell(x)!.getChars(); - this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, fg, chars); - } else { - this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, fg); - } - } - } - - private _getColorFromAnsiIndex(idx: number): IColor { - if (idx >= this._colors.ansi.length) { - throw new Error('No color found for idx ' + idx); - } - return this._colors.ansi[idx]; - } - public clear(force?: boolean): void { const terminal = this._terminal; const newCount = terminal.cols * terminal.rows * INDICES_PER_CELL; @@ -336,7 +245,7 @@ export class GlyphRenderer { public setColors(): void { } - public render(renderModel: IRenderModel, isSelectionVisible: boolean): void { + public render(renderModel: IRenderModel): void { if (!this._atlas) { return; } @@ -360,7 +269,7 @@ export class GlyphRenderer { let bufferLength = 0; for (let y = 0; y < renderModel.lineLengths.length; y++) { const si = y * this._terminal.cols * INDICES_PER_CELL; - const sub = (isSelectionVisible ? this._vertices.selectionAttributes : this._vertices.attributes).subarray(si, si + renderModel.lineLengths[y] * INDICES_PER_CELL); + const sub = this._vertices.attributes.subarray(si, si + renderModel.lineLengths[y] * INDICES_PER_CELL); activeBuffer.set(sub, bufferLength); bufferLength += sub.length; } diff --git a/addons/xterm-addon-webgl/src/RectangleRenderer.ts b/addons/xterm-addon-webgl/src/RectangleRenderer.ts index ab0b34e9..420e58d4 100644 --- a/addons/xterm-addon-webgl/src/RectangleRenderer.ts +++ b/addons/xterm-addon-webgl/src/RectangleRenderer.ts @@ -4,8 +4,7 @@ */ import { createProgram, expandFloat32Array, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils'; -import { IRenderModel, IWebGLVertexArrayObject, IWebGL2RenderingContext, ISelectionRenderModel } from './Types'; -import { fill } from 'common/TypedArrayUtils'; +import { IRenderModel, IWebGLVertexArrayObject, IWebGL2RenderingContext } from './Types'; import { Attributes, FgFlags } from 'common/buffer/Constants'; import { Terminal } from 'xterm'; import { IColor } from 'common/Types'; @@ -50,7 +49,6 @@ void main() { interface IVertices { attributes: Float32Array; - selection: Float32Array; count: number; } @@ -67,12 +65,10 @@ export class RectangleRenderer { private _attributesBuffer: WebGLBuffer; private _projectionLocation: WebGLUniformLocation; private _bgFloat!: Float32Array; - private _selectionFloat!: Float32Array; private _vertices: IVertices = { count: 0, - attributes: new Float32Array(INITIAL_BUFFER_RECTANGLE_CAPACITY), - selection: new Float32Array(3 * INDICES_PER_RECTANGLE) + attributes: new Float32Array(INITIAL_BUFFER_RECTANGLE_CAPACITY) }; constructor( @@ -138,11 +134,6 @@ export class RectangleRenderer { gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer); gl.bufferData(gl.ARRAY_BUFFER, this._vertices.attributes, gl.DYNAMIC_DRAW); gl.drawElementsInstanced(this._gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, this._vertices.count); - - // Bind selection buffer and draw - gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer); - gl.bufferData(gl.ARRAY_BUFFER, this._vertices.selection, gl.DYNAMIC_DRAW); - gl.drawElementsInstanced(this._gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, 3); } public onResize(): void { @@ -156,7 +147,6 @@ export class RectangleRenderer { private _updateCachedColors(): void { this._bgFloat = this._colorToFloat32Array(this._colors.background); - this._selectionFloat = this._colorToFloat32Array(this._colors.selectionOpaque); } private _updateViewportRectangle(): void { @@ -172,73 +162,6 @@ export class RectangleRenderer { ); } - public updateSelection(model: ISelectionRenderModel): void { - const terminal = this._terminal; - - if (!model.hasSelection) { - fill(this._vertices.selection, 0, 0); - return; - } - - if (model.columnSelectMode) { - const startCol = model.startCol; - const width = model.endCol - startCol; - const height = model.viewportCappedEndRow - model.viewportCappedStartRow + 1; - this._addRectangleFloat( - this._vertices.selection, - 0, - startCol * this._dimensions.scaledCellWidth, - model.viewportCappedStartRow * this._dimensions.scaledCellHeight, - width * this._dimensions.scaledCellWidth, - height * this._dimensions.scaledCellHeight, - this._selectionFloat - ); - fill(this._vertices.selection, 0, INDICES_PER_RECTANGLE); - } else { - // Draw first row - const startCol = model.viewportStartRow === model.viewportCappedStartRow ? model.startCol : 0; - const startRowEndCol = model.viewportCappedStartRow === model.viewportEndRow ? model.endCol : terminal.cols; - this._addRectangleFloat( - this._vertices.selection, - 0, - startCol * this._dimensions.scaledCellWidth, - model.viewportCappedStartRow * this._dimensions.scaledCellHeight, - (startRowEndCol - startCol) * this._dimensions.scaledCellWidth, - this._dimensions.scaledCellHeight, - this._selectionFloat - ); - - // Draw middle rows - const middleRowsCount = Math.max(model.viewportCappedEndRow - model.viewportCappedStartRow - 1, 0); - this._addRectangleFloat( - this._vertices.selection, - INDICES_PER_RECTANGLE, - 0, - (model.viewportCappedStartRow + 1) * this._dimensions.scaledCellHeight, - terminal.cols * this._dimensions.scaledCellWidth, - middleRowsCount * this._dimensions.scaledCellHeight, - this._selectionFloat - ); - - // Draw final row - if (model.viewportCappedStartRow !== model.viewportCappedEndRow) { - // Only draw viewportEndRow if it's not the same as viewportStartRow - const endCol = model.viewportEndRow === model.viewportCappedEndRow ? model.endCol : terminal.cols; - this._addRectangleFloat( - this._vertices.selection, - INDICES_PER_RECTANGLE * 2, - 0, - model.viewportCappedEndRow * this._dimensions.scaledCellHeight, - endCol * this._dimensions.scaledCellWidth, - this._dimensions.scaledCellHeight, - this._selectionFloat - ); - } else { - fill(this._vertices.selection, 0, INDICES_PER_RECTANGLE * 2); - } - } - } - public updateBackgrounds(model: IRenderModel): void { const terminal = this._terminal; const vertices = this._vertices; diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index e80464da..1b45ae3a 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -167,10 +167,6 @@ export class WebglRenderer extends Disposable implements IRenderer { this._core.screenElement!.style.height = `${this.dimensions.canvasHeight}px`; this._rectangleRenderer.onResize(); - if (this._model.selection.hasSelection) { - // Update selection as dimensions have changed - this._rectangleRenderer.updateSelection(this._model.selection); - } this._glyphRenderer.setDimensions(this.dimensions); this._glyphRenderer.onResize(); @@ -201,10 +197,8 @@ export class WebglRenderer extends Disposable implements IRenderer { for (const l of this._renderLayers) { l.onSelectionChanged(this._terminal, start, end, columnSelectMode); } - this._updateSelectionModel(start, end, columnSelectMode); - - this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 }); + this._requestRedrawViewport(); } public onCursorMove(): void { @@ -246,7 +240,7 @@ export class WebglRenderer extends Disposable implements IRenderer { this._charAtlas?.clearTexture(); this._model.clear(); this._updateModel(0, this._terminal.rows - 1); - this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 }); + this._requestRedrawViewport(); } public clear(): void { @@ -292,7 +286,7 @@ export class WebglRenderer extends Disposable implements IRenderer { // Render this._rectangleRenderer.render(); - this._glyphRenderer.render(this._model, this._model.selection.hasSelection); + this._glyphRenderer.render(this._model); } private _updateModel(start: number, end: number): void { @@ -376,10 +370,6 @@ export class WebglRenderer extends Disposable implements IRenderer { } } this._rectangleRenderer.updateBackgrounds(this._model); - if (this._model.selection.hasSelection) { - // Model could be updated but the selection is unchanged - this._glyphRenderer.updateSelection(this._model); - } } /** @@ -390,16 +380,22 @@ export class WebglRenderer extends Disposable implements IRenderer { this._workColors.bg = this._workCell.bg; this._workColors.fg = this._workCell.fg; - // Get any decoration foreground/background overrides, this happens on the model to avoid - // spreading decoration override logic throughout the different sub-renderers let bgOverride: number | undefined; let fgOverride: number | undefined; + + // Apply the selection color if needed + if (this._isCellSelected(x, y)) { + bgOverride = this._colors.selectionOpaque.rgba >> 8 & 0xFFFFFF; + } + + // Get any decoration foreground/background overrides, this happens on the model to avoid + // spreading decoration override logic throughout the different sub-renderers for (const d of this._decorationService.getDecorationsAtCell(x, y)) { if (d.backgroundColorRGB) { - bgOverride = (d.backgroundColorRGB.rgba >> 8) >>> 0 & 0xFFFFFF; + bgOverride = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF; } if (d.foregroundColorRGB) { - fgOverride = (d.foregroundColorRGB.rgba >> 8) >>> 0 & 0xFFFFFF; + fgOverride = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF; } } @@ -440,13 +436,27 @@ export class WebglRenderer extends Disposable implements IRenderer { this._workColors.fg = fgOverride ?? this._workColors.fg; } + private _isCellSelected(x: number, y: number): boolean { + if (!this._model.selection.hasSelection) { + return false; + } + y -= this._terminal.buffer.active.viewportY; + if (this._model.selection.columnSelectMode) { + return x >= this._model.selection.startCol && y >= this._model.selection.viewportCappedStartRow && + x < this._model.selection.endCol && y < this._model.selection.viewportCappedEndRow; + } + return (y > this._model.selection.viewportStartRow && y < this._model.selection.viewportEndRow) || + (this._model.selection.viewportStartRow === this._model.selection.viewportEndRow && y === this._model.selection.viewportStartRow && x >= this._model.selection.startCol && x < this._model.selection.endCol) || + (this._model.selection.viewportStartRow < this._model.selection.viewportEndRow && y === this._model.selection.viewportEndRow && x < this._model.selection.endCol) || + (this._model.selection.viewportStartRow < this._model.selection.viewportEndRow && y === this._model.selection.viewportStartRow && x >= this._model.selection.startCol); + } + private _updateSelectionModel(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void { const terminal = this._terminal; // Selection does not exist if (!start || !end || (start[0] === end[0] && start[1] === end[1])) { this._model.clearSelection(); - this._rectangleRenderer.updateSelection(this._model.selection); return; } @@ -459,7 +469,6 @@ export class WebglRenderer extends Disposable implements IRenderer { // No need to draw the selection if (viewportCappedStartRow >= terminal.rows || viewportCappedEndRow < 0) { this._model.clearSelection(); - this._rectangleRenderer.updateSelection(this._model.selection); return; } @@ -471,8 +480,6 @@ export class WebglRenderer extends Disposable implements IRenderer { this._model.selection.viewportCappedEndRow = viewportCappedEndRow; this._model.selection.startCol = start[0]; this._model.selection.endCol = end[0]; - - this._rectangleRenderer.updateSelection(this._model.selection); } /** @@ -546,6 +553,10 @@ export class WebglRenderer extends Disposable implements IRenderer { this.dimensions.actualCellHeight = this.dimensions.scaledCellHeight / this._devicePixelRatio; this.dimensions.actualCellWidth = this.dimensions.scaledCellWidth / this._devicePixelRatio; } + + private _requestRedrawViewport(): void { + this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 }); + } } // TODO: Share impl with core From 7b4423819ee4f5d87118b40445b185af4fc573f4 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 12 May 2022 10:51:11 -0700 Subject: [PATCH 31/39] Support setting the layer of a decoration Fixes #3778 --- addons/xterm-addon-search/src/SearchAddon.ts | 8 +++++++- .../typings/xterm-addon-search.d.ts | 7 +++++++ addons/xterm-addon-webgl/src/WebglRenderer.ts | 17 ++++++++++++++--- demo/client.ts | 7 ++++++- src/browser/renderer/BaseRenderLayer.ts | 5 +++++ src/browser/renderer/TextRenderLayer.ts | 5 +++++ .../renderer/dom/DomRendererRowFactory.ts | 5 +++++ src/common/services/DecorationService.ts | 4 ++-- src/common/services/Services.ts | 2 +- typings/xterm.d.ts | 10 ++++++++++ 10 files changed, 62 insertions(+), 8 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 7d1b145c..e7ece483 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -115,6 +115,11 @@ export class SearchAddon implements ITerminalAddon { } } + public clearActiveDecoration(): void { + this._selectedDecoration?.dispose(); + this._selectedDecoration = undefined; + } + /** * Find the next instance of the term, then scroll to and select it. If it * doesn't exist, do nothing. @@ -655,7 +660,7 @@ export class SearchAddon implements ITerminalAddon { */ private _selectResult(result: ISearchResult | undefined, options?: ISearchDecorationOptions, noScroll?: boolean): boolean { const terminal = this._terminal!; - this._selectedDecoration?.dispose(); + this.clearActiveDecoration(); if (!result) { terminal.clearSelection(); return false; @@ -669,6 +674,7 @@ export class SearchAddon implements ITerminalAddon { x: result.col, width: result.size, backgroundColor: options.activeMatchBackground, + layer: 'top', overviewRulerOptions: { color: options.activeMatchColorOverviewRuler } diff --git a/addons/xterm-addon-search/typings/xterm-addon-search.d.ts b/addons/xterm-addon-search/typings/xterm-addon-search.d.ts index 9ed1da62..e5a47190 100644 --- a/addons/xterm-addon-search/typings/xterm-addon-search.d.ts +++ b/addons/xterm-addon-search/typings/xterm-addon-search.d.ts @@ -111,6 +111,13 @@ declare module 'xterm-addon-search' { */ public clearDecorations(): void; + /** + * Clears the active result decoration, this decoration is applied on top of the selection so + * removing it will reveal the selection underneath. This is intended to be call on the search + * textarea's `blur` event. + */ + public clearActiveDecoration(): void; + /** * When decorations are enabled, fires when * the search results change. diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 1b45ae3a..d060c4d1 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -380,17 +380,28 @@ export class WebglRenderer extends Disposable implements IRenderer { this._workColors.bg = this._workCell.bg; this._workColors.fg = this._workCell.fg; + // Get any foreground/background overrides, this happens on the model to avoid spreading + // override logic throughout the different sub-renderers let bgOverride: number | undefined; let fgOverride: number | undefined; + // Apply decorations on the bottom layer + for (const d of this._decorationService.getDecorationsAtCell(x, y, 'bottom')) { + if (d.backgroundColorRGB) { + bgOverride = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF; + } + if (d.foregroundColorRGB) { + fgOverride = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF; + } + } + // Apply the selection color if needed if (this._isCellSelected(x, y)) { bgOverride = this._colors.selectionOpaque.rgba >> 8 & 0xFFFFFF; } - // Get any decoration foreground/background overrides, this happens on the model to avoid - // spreading decoration override logic throughout the different sub-renderers - for (const d of this._decorationService.getDecorationsAtCell(x, y)) { + // Apply decorations on the top layer + for (const d of this._decorationService.getDecorationsAtCell(x, y, 'top')) { if (d.backgroundColorRGB) { bgOverride = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF; } diff --git a/demo/client.ts b/demo/client.ts index a63864a2..b9e52d7b 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -212,10 +212,15 @@ function createTerminal(): void { addDomListener(actionElements.findNext, 'keyup', (e) => { addons.search.instance.findNext(actionElements.findNext.value, getSearchOptions(e)); }); - addDomListener(actionElements.findPrevious, 'keyup', (e) => { addons.search.instance.findPrevious(actionElements.findPrevious.value, getSearchOptions(e)); }); + addDomListener(actionElements.findNext, 'blur', (e) => { + addons.search.instance.clearActiveDecoration(); + }); + addDomListener(actionElements.findPrevious, 'blur', (e) => { + addons.search.instance.clearActiveDecoration(); + }); // fit is called within a setTimeout, cols and rows need this. setTimeout(() => { diff --git a/src/browser/renderer/BaseRenderLayer.ts b/src/browser/renderer/BaseRenderLayer.ts index e0f3566c..696b793f 100644 --- a/src/browser/renderer/BaseRenderLayer.ts +++ b/src/browser/renderer/BaseRenderLayer.ts @@ -443,13 +443,18 @@ export abstract class BaseRenderLayer implements IRenderLayer { // exist but applied after inverse let bgOverride: number | undefined; let fgOverride: number | undefined; + let isTop = false; for (const d of this._decorationService.getDecorationsAtCell(x, y)) { + if (d.options.layer !== 'top' && isTop) { + continue; + } if (d.backgroundColorRGB) { bgOverride = d.backgroundColorRGB.rgba; } if (d.foregroundColorRGB) { fgOverride = d.foregroundColorRGB.rgba; } + isTop = d.options.layer === 'top'; } if (!bgOverride && !fgOverride && (this._optionsService.rawOptions.minimumContrastRatio === 1 || isPowerlineGlyph(cell.getCode()))) { diff --git a/src/browser/renderer/TextRenderLayer.ts b/src/browser/renderer/TextRenderLayer.ts index 193d891d..ef5a9b62 100644 --- a/src/browser/renderer/TextRenderLayer.ts +++ b/src/browser/renderer/TextRenderLayer.ts @@ -179,10 +179,15 @@ export class TextRenderLayer extends BaseRenderLayer { // Get any decoration foreground/background overrides, this must be fetched before the early // exist but applied after inverse + let isTop = false; for (const d of this._decorationService.getDecorationsAtCell(x, this._bufferService.buffer.ydisp + y)) { + if (d.options.layer !== 'top' && isTop) { + continue; + } if (d.backgroundColorRGB) { nextFillStyle = d.backgroundColorRGB.css; } + isTop = d.options.layer === 'top'; } if (prevFillStyle === null) { diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index 71dc782a..bf3939e8 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -177,7 +177,11 @@ export class DomRendererRowFactory { // been applied let bgOverride: IColor | undefined; let fgOverride: IColor | undefined; + let isTop = false; for (const d of this._decorationService.getDecorationsAtCell(x, row)) { + if (d.options.layer !== 'top' && isTop) { + continue; + } if (d.backgroundColorRGB) { bgColorMode = Attributes.CM_RGB; bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF; @@ -188,6 +192,7 @@ export class DomRendererRowFactory { fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF; fgOverride = d.foregroundColorRGB; } + isTop = d.options.layer === 'top'; } // Foreground diff --git a/src/common/services/DecorationService.ts b/src/common/services/DecorationService.ts index e32abdce..755f13b3 100644 --- a/src/common/services/DecorationService.ts +++ b/src/common/services/DecorationService.ts @@ -64,13 +64,13 @@ export class DecorationService extends Disposable implements IDecorationService return this._decorations.getKeyIterator(line); } - public *getDecorationsAtCell(x: number, line: number): IterableIterator { + public *getDecorationsAtCell(x: number, line: number, layer?: 'bottom' | 'top'): IterableIterator { let xmin = 0; let xmax = 0; for (const d of this._decorations.getKeyIterator(line)) { xmin = d.options.x ?? 0; xmax = xmin + (d.options.width ?? 1); - if (x >= xmin && x < xmax) { + if (x >= xmin && x < xmax && (!layer || (d.options.layer ?? 'bottom') === layer)) { yield d; } } diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 82492eb4..c3190210 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -312,7 +312,7 @@ export interface IDecorationService extends IDisposable { /** Iterates over the decorations at a line (in no particular order). */ getDecorationsAtLine(line: number): IterableIterator; /** Iterates over the decorations at a cell (in no particular order). */ - getDecorationsAtCell(x: number, line: number): IterableIterator; + getDecorationsAtCell(x: number, line: number, layer?: 'bottom' | 'top'): IterableIterator; } export interface IInternalDecoration extends IDecoration { readonly options: IDecorationOptions; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 0c421dc0..15ee4650 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -500,6 +500,16 @@ declare module 'xterm' { */ readonly foregroundColor?: string; + /** + * What layer to render the decoration at when {@link backgroundColor} or + * {@link foregroundColor} are used. `'bottom'` will render under the selection, `'top`' will + * render above the selection\*. + * + * *\* The selection will render on top regardless of layer on the canvas renderer due to how + * it renders selection separately.* + */ + readonly layer?: 'bottom' | 'top'; + /** * When defined, renders the decoration in the overview ruler to the right * of the terminal. {@link ITerminalOptions.overviewRulerWidth} must be set From f5223d30b85220c1c8e50022a692e33ab7dc05cc Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 12 May 2022 10:52:16 -0700 Subject: [PATCH 32/39] Fix typo --- addons/xterm-addon-search/typings/xterm-addon-search.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/xterm-addon-search/typings/xterm-addon-search.d.ts b/addons/xterm-addon-search/typings/xterm-addon-search.d.ts index e5a47190..300e5063 100644 --- a/addons/xterm-addon-search/typings/xterm-addon-search.d.ts +++ b/addons/xterm-addon-search/typings/xterm-addon-search.d.ts @@ -113,7 +113,7 @@ declare module 'xterm-addon-search' { /** * Clears the active result decoration, this decoration is applied on top of the selection so - * removing it will reveal the selection underneath. This is intended to be call on the search + * removing it will reveal the selection underneath. This is intended to be called on the search * textarea's `blur` event. */ public clearActiveDecoration(): void; From 24919e36f09de3da0cb57b2e3f997726aaf98bb2 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 12 May 2022 10:54:36 -0700 Subject: [PATCH 33/39] Highlight all matches in demo by default --- demo/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/index.html b/demo/index.html index 6bbcb4a2..f36fc629 100644 --- a/demo/index.html +++ b/demo/index.html @@ -43,7 +43,7 @@ - +

SerializeAddon

From ddde738326b7514c08c896d421403bf35c3a391f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 12 May 2022 10:57:12 -0700 Subject: [PATCH 34/39] Merge .xterm css rules Fixes #3780 --- css/xterm.css | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/css/xterm.css b/css/xterm.css index 7432fbb1..3d7cbb9b 100644 --- a/css/xterm.css +++ b/css/xterm.css @@ -36,6 +36,7 @@ */ .xterm { + cursor: text; position: relative; user-select: none; -ms-user-select: none; @@ -124,10 +125,6 @@ line-height: normal; } -.xterm { - cursor: text; -} - .xterm.enable-mouse-events { /* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */ cursor: default; From d84b71d4c6f119c7498526b99c68f705fa71468a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 12 May 2022 10:59:57 -0700 Subject: [PATCH 35/39] Disable pointer events on the overview ruler Fixes #3762 --- css/xterm.css | 1 + 1 file changed, 1 insertion(+) diff --git a/css/xterm.css b/css/xterm.css index 7432fbb1..0fd5af4e 100644 --- a/css/xterm.css +++ b/css/xterm.css @@ -184,4 +184,5 @@ position: absolute; top: 0; right: 0; + pointer-events: none; } From 29c1264b42b69162a17b9f00a457e1c14a5d331d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 12 May 2022 11:04:48 -0700 Subject: [PATCH 36/39] Add warning in demo code about flow control Fixes #3760 --- demo/server.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/demo/server.js b/demo/server.js index c0d5e1f6..71a9d36a 100644 --- a/demo/server.js +++ b/demo/server.js @@ -114,6 +114,9 @@ function startServer() { } const send = USE_BINARY ? bufferUtf8(ws, 5) : buffer(ws, 5); + // WARNING: This is a naive implementation that will not throttle the flow of data. This means + // it could flood the communication channel and make the terminal unresponsive. Learn more about + // the problem and how to implement flow control at https://xtermjs.org/docs/guides/flowcontrol/ term.on('data', function(data) { try { send(data); From 4cc4b2cd427a298bea702c9efe1d4a9808a1c78f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 12 May 2022 11:18:49 -0700 Subject: [PATCH 37/39] Rename Decorations -> decoration2 to fix casing --- .../{Decorations => decorations2}/BufferDecorationRenderer.ts | 0 src/browser/{Decorations => decorations2}/ColorZoneStore.test.ts | 0 src/browser/{Decorations => decorations2}/ColorZoneStore.ts | 0 .../{Decorations => decorations2}/OverviewRulerRenderer.ts | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename src/browser/{Decorations => decorations2}/BufferDecorationRenderer.ts (100%) rename src/browser/{Decorations => decorations2}/ColorZoneStore.test.ts (100%) rename src/browser/{Decorations => decorations2}/ColorZoneStore.ts (100%) rename src/browser/{Decorations => decorations2}/OverviewRulerRenderer.ts (100%) diff --git a/src/browser/Decorations/BufferDecorationRenderer.ts b/src/browser/decorations2/BufferDecorationRenderer.ts similarity index 100% rename from src/browser/Decorations/BufferDecorationRenderer.ts rename to src/browser/decorations2/BufferDecorationRenderer.ts diff --git a/src/browser/Decorations/ColorZoneStore.test.ts b/src/browser/decorations2/ColorZoneStore.test.ts similarity index 100% rename from src/browser/Decorations/ColorZoneStore.test.ts rename to src/browser/decorations2/ColorZoneStore.test.ts diff --git a/src/browser/Decorations/ColorZoneStore.ts b/src/browser/decorations2/ColorZoneStore.ts similarity index 100% rename from src/browser/Decorations/ColorZoneStore.ts rename to src/browser/decorations2/ColorZoneStore.ts diff --git a/src/browser/Decorations/OverviewRulerRenderer.ts b/src/browser/decorations2/OverviewRulerRenderer.ts similarity index 100% rename from src/browser/Decorations/OverviewRulerRenderer.ts rename to src/browser/decorations2/OverviewRulerRenderer.ts From cd01157e55883cceb8af26e060eef7f4e830935c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 12 May 2022 11:19:07 -0700 Subject: [PATCH 38/39] Rename decorations2 -> decorations to fix casing --- .../{decorations2 => decorations}/BufferDecorationRenderer.ts | 0 src/browser/{decorations2 => decorations}/ColorZoneStore.test.ts | 0 src/browser/{decorations2 => decorations}/ColorZoneStore.ts | 0 .../{decorations2 => decorations}/OverviewRulerRenderer.ts | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename src/browser/{decorations2 => decorations}/BufferDecorationRenderer.ts (100%) rename src/browser/{decorations2 => decorations}/ColorZoneStore.test.ts (100%) rename src/browser/{decorations2 => decorations}/ColorZoneStore.ts (100%) rename src/browser/{decorations2 => decorations}/OverviewRulerRenderer.ts (100%) diff --git a/src/browser/decorations2/BufferDecorationRenderer.ts b/src/browser/decorations/BufferDecorationRenderer.ts similarity index 100% rename from src/browser/decorations2/BufferDecorationRenderer.ts rename to src/browser/decorations/BufferDecorationRenderer.ts diff --git a/src/browser/decorations2/ColorZoneStore.test.ts b/src/browser/decorations/ColorZoneStore.test.ts similarity index 100% rename from src/browser/decorations2/ColorZoneStore.test.ts rename to src/browser/decorations/ColorZoneStore.test.ts diff --git a/src/browser/decorations2/ColorZoneStore.ts b/src/browser/decorations/ColorZoneStore.ts similarity index 100% rename from src/browser/decorations2/ColorZoneStore.ts rename to src/browser/decorations/ColorZoneStore.ts diff --git a/src/browser/decorations2/OverviewRulerRenderer.ts b/src/browser/decorations/OverviewRulerRenderer.ts similarity index 100% rename from src/browser/decorations2/OverviewRulerRenderer.ts rename to src/browser/decorations/OverviewRulerRenderer.ts From 748d0cc74f0b0a6ce9903856b0db039b64c0af45 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 12 May 2022 11:49:57 -0700 Subject: [PATCH 39/39] Fix missed decorations casing --- src/browser/Terminal.ts | 4 ++-- src/browser/decorations/ColorZoneStore.test.ts | 2 +- src/browser/decorations/OverviewRulerRenderer.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index a8accd78..de3fff90 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -55,8 +55,8 @@ import { CoreTerminal } from 'common/CoreTerminal'; import { color, rgba } from 'common/Color'; import { CharacterJoinerService } from 'browser/services/CharacterJoinerService'; import { toRgbString } from 'common/input/XParseColor'; -import { BufferDecorationRenderer } from 'browser/Decorations/BufferDecorationRenderer'; -import { OverviewRulerRenderer } from 'browser/Decorations/OverviewRulerRenderer'; +import { BufferDecorationRenderer } from 'browser/decorations/BufferDecorationRenderer'; +import { OverviewRulerRenderer } from 'browser/decorations/OverviewRulerRenderer'; import { DecorationService } from 'common/services/DecorationService'; import { IDecorationService } from 'common/services/Services'; diff --git a/src/browser/decorations/ColorZoneStore.test.ts b/src/browser/decorations/ColorZoneStore.test.ts index 73e3402f..719ef45b 100644 --- a/src/browser/decorations/ColorZoneStore.test.ts +++ b/src/browser/decorations/ColorZoneStore.test.ts @@ -4,7 +4,7 @@ */ import { assert } from 'chai'; -import { ColorZoneStore } from 'browser/Decorations/ColorZoneStore'; +import { ColorZoneStore } from 'browser/decorations/ColorZoneStore'; const optionsRedFull = { overviewRulerOptions: { diff --git a/src/browser/decorations/OverviewRulerRenderer.ts b/src/browser/decorations/OverviewRulerRenderer.ts index dc35b901..39480ca2 100644 --- a/src/browser/decorations/OverviewRulerRenderer.ts +++ b/src/browser/decorations/OverviewRulerRenderer.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ColorZoneStore, IColorZone, IColorZoneStore } from 'browser/Decorations/ColorZoneStore'; +import { ColorZoneStore, IColorZone, IColorZoneStore } from 'browser/decorations/ColorZoneStore'; import { addDisposableDomListener } from 'browser/Lifecycle'; import { IRenderService } from 'browser/services/Services'; import { Disposable } from 'common/Lifecycle';