Skip to content

Commit

Permalink
Initial bg/fg renderer decoration proof of concept
Browse files Browse the repository at this point in the history
Part of xtermjs#3770
  • Loading branch information
Tyriar committed May 9, 2022
1 parent 85e0625 commit 63eb9a4
Show file tree
Hide file tree
Showing 8 changed files with 115 additions and 15 deletions.
12 changes: 10 additions & 2 deletions demo/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
11 changes: 5 additions & 6 deletions src/browser/renderer/dom/DomRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down
5 changes: 3 additions & 2 deletions src/browser/renderer/dom/DomRendererRowFactory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -49,7 +49,8 @@ describe('DomRendererRowFactory', () => {
} as any,
new MockCharacterJoinerService(),
new MockOptionsService({ drawBoldTextInBrightColors: true }),
new MockCoreService()
new MockCoreService(),
new MockDecorationService()
);
lineData = createEmptyLineData(2);
});
Expand Down
23 changes: 21 additions & 2 deletions src/browser/renderer/dom/DomRendererRowFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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
) {
}

Expand Down Expand Up @@ -172,13 +173,31 @@ 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:
case Attributes.CM_P256:
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}`);
}
Expand Down
13 changes: 12 additions & 1 deletion src/common/TestUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
* @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';
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;
Expand Down Expand Up @@ -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<IInternalDecoration> { return [].values(); };
public onDecorationRegistered = new EventEmitter<IInternalDecoration>().event;
public onDecorationRemoved = new EventEmitter<IInternalDecoration>().event;
public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined { return undefined; }
public *getDecorationsOnLine(line: number): IterableIterator<IInternalDecoration> { }
public dispose(): void { }
}
45 changes: 45 additions & 0 deletions src/common/services/DecorationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -45,6 +46,15 @@ export class DecorationService extends Disposable implements IDecorationService
return decoration;
}

public *getDecorationsOnLine(line: number): IterableIterator<IInternalDecoration> {
// 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);
Expand All @@ -64,6 +74,32 @@ class Decoration extends Disposable implements IInternalDecoration {
private _onDispose = this.register(new EventEmitter<void>());
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
) {
Expand All @@ -73,6 +109,7 @@ class Decoration extends Disposable implements IInternalDecoration {
this.options.overviewRulerOptions.position = 'full';
}
}

public override dispose(): void {
if (this._isDisposed) {
return;
Expand All @@ -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');
}
6 changes: 5 additions & 1 deletion src/common/services/Services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -308,8 +308,12 @@ export interface IDecorationService extends IDisposable {
readonly onDecorationRegistered: IEvent<IInternalDecoration>;
readonly onDecorationRemoved: IEvent<IInternalDecoration>;
registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined;
/** Iterates over the decorations on a line (in no particular order). */
getDecorationsOnLine(line: number): IterableIterator<IInternalDecoration>;
}
export interface IInternalDecoration extends IDecoration {
readonly options: IDecorationOptions;
readonly backgroundColorRGB: IColorRGB | undefined;
readonly foregroundColorRGB: IColorRGB | undefined;
readonly onRenderEmitter: IEventEmitter<HTMLElement>;
}
15 changes: 14 additions & 1 deletion typings/xterm.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,8 @@ declare module 'xterm' {
* This will only take effect when {@link IDecorationOptions.overviewRulerOptions}
* were provided initially.
*/
options: Pick<IDecorationOptions, 'overviewRulerOptions'>;
options: Pick<IDecorationOptions, 'overviewRulerOptions'>;
// options: Pick<IDecorationOptions, 'overviewRulerOptions' | 'backgroundColor' | 'foregroundColor'>;
}


Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 63eb9a4

Please sign in to comment.