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