diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index c92c6c8a..7d1b145c 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()); } } @@ -695,15 +696,12 @@ export class SearchAddon implements ITerminalAddon { * @param borderColor the border color to apply * @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}`; } @@ -719,18 +717,20 @@ export class SearchAddon implements ITerminalAddon { 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 || !options?.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: options.matchOverviewRuler, position: 'center' + color: options.matchOverviewRuler, + position: 'center' } }); - findResultDecoration?.onRender((e) => this._applyStyles(e, options.matchBackground, options.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/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index e2c37be2..f3fd53a6 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -9,9 +9,10 @@ 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 { 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'; @@ -187,6 +188,8 @@ export class GlyphRenderer { if (!this._atlas) { return; } + + // Get the glyph if (chars && chars.length > 1) { rasterizedGlyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg); } else { 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/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..e80464da 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'; @@ -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[]; @@ -31,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; @@ -52,6 +54,7 @@ export class WebglRenderer extends Disposable implements IRenderer { private _terminal: Terminal, private _colors: IColorSet, private readonly _characterJoinerService: ICharacterJoinerService, + private readonly _decorationService: IDecorationService, preserveDrawingBuffer?: boolean ) { super(); @@ -331,14 +334,17 @@ export class WebglRenderer extends Disposable implements IRenderer { let code = cell.getCode(); const i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL; + // Load colors/resolve overrides into work colors + this._loadColorsForCell(x, row); + 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] === this._workColors.bg && + this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._workColors.fg) { continue; } @@ -349,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] = cell.bg; - this._model.cells[i + RENDER_MODEL_FG_OFFSET] = cell.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, cell.bg, cell.fg, chars); + this._glyphRenderer.updateCell(x, y, code, this._workColors.bg, this._workColors.fg, chars); if (isJoined) { // Restore work cell @@ -363,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] = this._workCell.bg; - this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._workCell.fg; + this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._workColors.bg; + this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._workColors.fg; } } } @@ -376,6 +382,64 @@ 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 + let bgOverride: number | undefined; + let fgOverride: number | undefined; + 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; + } + } + + // 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 + 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 + 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) + 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); + } + } + } + + // 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; 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/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/addons/xterm-addon-webgl/test/WebglRenderer.api.ts b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts index 0b86b14b..cec1e3b1 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,95 @@ describe('WebGL Renderer Integration Tests', async () => { await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]); }); }); + + describe('decoration color overrides', async () => { + if (areTestsEnabled) { + before(async () => setupBrowser({ rendererType: 'dom' })); + 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█\\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); + 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 \\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 + }); + }); }); async function getCellColor(col: number, row: number): Promise { diff --git a/demo/client.ts b/demo/client.ts index 7c21956a..a63864a2 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -110,11 +110,11 @@ 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', - activeMatchBorder: '#ef2929', + activeMatchBackground: '#ef2929', + activeMatchBorder: '#ffffff', activeMatchColorOverviewRuler: '#ef2929' } : undefined }; @@ -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: '#00FE00', + overviewRulerOptions: { color: '#ef292980', position: 'left' } + }); + decoration.onRender((e: HTMLElement) => { + e.style.right = '100%'; + e.style.backgroundColor = '#ef292980'; + }); } function addOverviewRuler() { 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..a8accd78 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'; @@ -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/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..e0f3566c 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 { IBufferService, IDecorationService, 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'; @@ -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 @@ -325,7 +326,17 @@ 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; + for (const d of this._decorationService.getDecorationsAtCell(x, y)) { + 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); @@ -427,15 +438,30 @@ 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 + let bgOverride: number | undefined; + let fgOverride: number | undefined; + for (const d of this._decorationService.getDecorationsAtCell(x, y)) { + 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 +479,17 @@ 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 (!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 + result = fgOverride; } const color: IColor = { @@ -470,7 +500,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/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/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..193d891d 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,14 @@ 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 + for (const d of this._decorationService.getDecorationsAtCell(x, this._bufferService.buffer.ydisp + y)) { + 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 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 ee283399..d15d7eac 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -9,9 +9,9 @@ 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 { color } from 'common/Color'; import { removeElementFromParent } from 'browser/Dom'; const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-'; @@ -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..bb511a47 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.test.ts @@ -10,8 +10,8 @@ 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 { css } from 'browser/Color'; +import { MockCoreService, MockDecorationService, MockOptionsService } from 'common/TestUtils.test'; +import { css } from 'common/Color'; import { MockCharacterJoinerService } from 'browser/TestUtils.test'; describe('DomRendererRowFactory', () => { @@ -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..71dc782a 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, IOptionsService } from 'common/services/Services'; -import { color, rgba } from 'browser/Color'; -import { IColorSet, IColor } from 'browser/Types'; +import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; +import { color, rgba } from 'common/Color'; +import { IColorSet } from 'browser/Types'; import { ICharacterJoinerService } from 'browser/services/Services'; import { JoinedCellData } from 'browser/services/CharacterJoinerService'; import { isPowerlineGlyph } from 'browser/renderer/RendererUtils'; @@ -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, this must happen after inverse has + // been applied + let bgOverride: IColor | undefined; + let fgOverride: IColor | undefined; + 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; + } + } + // Foreground switch (fgColorMode) { case Attributes.CM_P16: @@ -179,7 +197,7 @@ export class DomRendererRowFactory { if (cell.isBold() && fg < 8 && this._optionsService.rawOptions.drawBoldTextInBrightColors) { fg += 8; } - 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; @@ -189,13 +207,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}`); } @@ -209,7 +227,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: @@ -225,18 +243,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) { diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index 91b510a3..b2e619fe 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(); @@ -72,6 +73,12 @@ export class RenderService extends Disposable implements IRenderService { 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/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 95% rename from src/browser/Color.ts rename to src/common/Color.ts index 32e311db..b197cd66 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). @@ -173,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; } @@ -193,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; } diff --git a/src/common/SortedList.test.ts b/src/common/SortedList.test.ts new file mode 100644 index 00000000..ecafdb8f --- /dev/null +++ b/src/common/SortedList.test.ts @@ -0,0 +1,107 @@ +/** + * 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(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); + 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..051c6702 --- /dev/null +++ b/src/common/SortedList.ts @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2022 The xterm.js authors. All rights reserved. + * @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[] = []; + + 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 (i < 0 || i >= this._array.length) { + return; + } + 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/TestUtils.test.ts b/src/common/TestUtils.test.ts index 58f6c709..11d9a8c5 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,15 @@ 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 reset(): void { } + public *getDecorationsAtLine(line: number): IterableIterator { } + public *getDecorationsAtCell(x: number, line: number): IterableIterator { } + public dispose(): void { } +} 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 61936e15..e32abdce 100644 --- a/src/common/services/DecorationService.ts +++ b/src/common/services/DecorationService.ts @@ -3,15 +3,23 @@ * @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 { 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; } @@ -33,26 +41,46 @@ 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 dispose(): void { - for (const decoration of this._decorations) { - this._onDecorationRemoved.fire(decoration); - decoration.dispose(); + public reset(): void { + for (const d of this._decorations.values()) { + d.dispose(); } - this._decorations.length = 0; + this._decorations.clear(); + } + + public *getDecorationsAtLine(line: number): IterableIterator { + return this._decorations.getKeyIterator(line); + } + + public *getDecorationsAtCell(x: number, line: number): 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) { + yield d; + } + } + } + + public dispose(): void { + for (const d of this._decorations.values()) { + this._onDecorationRemoved.fire(d); + } + this.reset(); } } @@ -66,6 +94,30 @@ class Decoration extends Disposable implements IInternalDecoration { private _onDispose = this.register(new EventEmitter()); public readonly onDispose = this._onDispose.event; + private _cachedBg: IColor | undefined | null = null; + public get backgroundColorRGB(): IColor | undefined { + if (this._cachedBg === null) { + if (this.options.backgroundColor) { + this._cachedBg = css.toColor(this.options.backgroundColor); + } else { + this._cachedBg = undefined; + } + } + return this._cachedBg; + } + + private _cachedFg: IColor | undefined | null = null; + public get foregroundColorRGB(): IColor | undefined { + if (this._cachedFg === null) { + if (this.options.foregroundColor) { + this._cachedFg = css.toColor(this.options.foregroundColor); + } else { + this._cachedFg = undefined; + } + } + return this._cachedFg; + } + constructor( public readonly options: IDecorationOptions ) { @@ -75,6 +127,7 @@ class Decoration extends Disposable implements IInternalDecoration { this.options.overviewRulerOptions.position = 'full'; } } + public override dispose(): void { if (this._isDisposed) { return; diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 876d90bc..82492eb4 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, IColor } from 'common/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; import { IDecorationOptions, IDecoration } from 'xterm'; @@ -308,8 +308,15 @@ export interface IDecorationService extends IDisposable { readonly onDecorationRegistered: IEvent; readonly onDecorationRemoved: IEvent; registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined; + reset(): void; + /** 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; + readonly backgroundColorRGB: IColor | undefined; + readonly foregroundColorRGB: IColor | undefined; readonly onRenderEmitter: IEventEmitter; } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 76c228b2..0c421dc0 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -444,7 +444,7 @@ declare module 'xterm' { * This will only take effect when {@link IDecorationOptions.overviewRulerOptions} * were provided initially. */ - options: Pick; + options: Pick; } @@ -488,6 +488,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. + */ + 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. + */ + readonly foregroundColor?: string; + /** * When defined, renders the decoration in the overview ruler to the right * of the terminal. {@link ITerminalOptions.overviewRulerWidth} must be set