diff --git a/addons/xterm-addon-fit/src/FitAddon.ts b/addons/xterm-addon-fit/src/FitAddon.ts index 360397ec..7b9c228f 100644 --- a/addons/xterm-addon-fit/src/FitAddon.ts +++ b/addons/xterm-addon-fit/src/FitAddon.ts @@ -63,6 +63,9 @@ export class FitAddon implements ITerminalAddon { return undefined; } + const scrollbarWidth = this._terminal.options.scrollback === 0 ? + 0 : core.viewport.scrollBarWidth; + const parentElementStyle = window.getComputedStyle(this._terminal.element.parentElement); const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')); const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width'))); @@ -76,7 +79,7 @@ export class FitAddon implements ITerminalAddon { const elementPaddingVer = elementPadding.top + elementPadding.bottom; const elementPaddingHor = elementPadding.right + elementPadding.left; const availableHeight = parentElementHeight - elementPaddingVer; - const availableWidth = parentElementWidth - elementPaddingHor - core.viewport.scrollBarWidth; + const availableWidth = parentElementWidth - elementPaddingHor - scrollbarWidth; const geometry = { cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / core._renderService.dimensions.actualCellWidth)), rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / core._renderService.dimensions.actualCellHeight)) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index c92c6c8a..e7ece483 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -115,6 +115,11 @@ export class SearchAddon implements ITerminalAddon { } } + public clearActiveDecoration(): void { + this._selectedDecoration?.dispose(); + this._selectedDecoration = undefined; + } + /** * Find the next instance of the term, then scroll to and select it. If it * doesn't exist, do nothing. @@ -653,26 +658,28 @@ 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(); + this.clearActiveDecoration(); if (!result) { terminal.clearSelection(); 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, + layer: 'top', 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 +702,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 +723,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..300e5063 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; @@ -111,6 +111,13 @@ declare module 'xterm-addon-search' { */ public clearDecorations(): void; + /** + * Clears the active result decoration, this decoration is applied on top of the selection so + * removing it will reveal the selection underneath. This is intended to be called on the search + * textarea's `blur` event. + */ + public clearActiveDecoration(): void; + /** * When decorations are enabled, fires when * the search results change. diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index e2c37be2..e9055a17 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -6,14 +6,11 @@ import { createProgram, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils'; import { WebglCharAtlas } from './atlas/WebglCharAtlas'; import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel, IRasterizedGlyph } from './Types'; -import { COMBINED_CHAR_BIT_MASK, RENDER_MODEL_INDICIES_PER_CELL, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_BG_OFFSET } from './RenderModel'; import { fill } from 'common/TypedArrayUtils'; -import { slice } from './TypedArray'; -import { NULL_CELL_CODE, WHITESPACE_CELL_CODE, Attributes, FgFlags } from 'common/buffer/Constants'; +import { NULL_CELL_CODE } from 'common/buffer/Constants'; import { Terminal, IBufferLine } from 'xterm'; -import { IColorSet, IColor } from 'browser/Types'; +import { IColorSet } from 'browser/Types'; import { IRenderDimensions } from 'browser/renderer/Types'; -import { AttributeData } from 'common/buffer/AttributeData'; interface IVertices { attributes: Float32Array; @@ -24,7 +21,6 @@ interface IVertices { * working on the next frame. */ attributesBuffers: Float32Array[]; - selectionAttributes: Float32Array; count: number; } @@ -91,8 +87,7 @@ export class GlyphRenderer { attributesBuffers: [ new Float32Array(0), new Float32Array(0) - ], - selectionAttributes: new Float32Array(0) + ] }; constructor( @@ -187,6 +182,8 @@ export class GlyphRenderer { if (!this._atlas) { return; } + + // Get the glyph if (chars && chars.length > 1) { rasterizedGlyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg); } else { @@ -214,91 +211,6 @@ export class GlyphRenderer { // a_cellpos only changes on resize } - public updateSelection(model: IRenderModel): void { - const terminal = this._terminal; - - this._vertices.selectionAttributes = slice(this._vertices.attributes, 0); - - const bg = (this._colors.selectionOpaque.rgba >>> 8) | Attributes.CM_RGB; - - if (model.selection.columnSelectMode) { - const startCol = model.selection.startCol; - const width = model.selection.endCol - startCol; - const height = model.selection.viewportCappedEndRow - model.selection.viewportCappedStartRow + 1; - for (let y = model.selection.viewportCappedStartRow; y < model.selection.viewportCappedStartRow + height; y++) { - this._updateSelectionRange(startCol, startCol + width, y, model, bg); - } - } else { - // Draw first row - const startCol = model.selection.viewportStartRow === model.selection.viewportCappedStartRow ? model.selection.startCol : 0; - const startRowEndCol = model.selection.viewportCappedStartRow === model.selection.viewportCappedEndRow ? model.selection.endCol : terminal.cols; - this._updateSelectionRange(startCol, startRowEndCol, model.selection.viewportCappedStartRow, model, bg); - - // Draw middle rows - const middleRowsCount = Math.max(model.selection.viewportCappedEndRow - model.selection.viewportCappedStartRow - 1, 0); - for (let y = model.selection.viewportCappedStartRow + 1; y <= model.selection.viewportCappedStartRow + middleRowsCount; y++) { - this._updateSelectionRange(0, startRowEndCol, y, model, bg); - } - - // Draw final row - if (model.selection.viewportCappedStartRow !== model.selection.viewportCappedEndRow) { - // Only draw viewportEndRow if it's not the same as viewportStartRow - const endCol = model.selection.viewportEndRow === model.selection.viewportCappedEndRow ? model.selection.endCol : terminal.cols; - this._updateSelectionRange(0, endCol, model.selection.viewportCappedEndRow, model, bg); - } - } - } - - private _updateSelectionRange(startCol: number, endCol: number, y: number, model: IRenderModel, bg: number): void { - const terminal = this._terminal; - const row = y + terminal.buffer.active.viewportY; - let line: IBufferLine | undefined; - for (let x = startCol; x < endCol; x++) { - const offset = (y * this._terminal.cols + x) * RENDER_MODEL_INDICIES_PER_CELL; - const code = model.cells[offset]; - let fg = model.cells[offset + RENDER_MODEL_FG_OFFSET]; - if (fg & FgFlags.INVERSE) { - const workCell = new AttributeData(); - workCell.fg = fg; - workCell.bg = model.cells[offset + RENDER_MODEL_BG_OFFSET]; - // Get attributes from fg (excluding inverse) and resolve inverse by pullibng rgb colors - // from bg. This is needed since the inverse fg color should be based on the original bg - // color, not on the selection color - fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK | FgFlags.INVERSE); - switch (workCell.getBgColorMode()) { - case Attributes.CM_P16: - case Attributes.CM_P256: - const c = this._getColorFromAnsiIndex(workCell.getBgColor()).rgba; - fg |= (c >> 8) & Attributes.RED_MASK | (c >> 8) & Attributes.GREEN_MASK | (c >> 8) & Attributes.BLUE_MASK; - case Attributes.CM_RGB: - const arr = AttributeData.toColorRGB(workCell.getBgColor()); - fg |= arr[0] << Attributes.RED_SHIFT | arr[1] << Attributes.GREEN_SHIFT | arr[2] << Attributes.BLUE_SHIFT; - case Attributes.CM_DEFAULT: - default: - const c2 = this._colors.background.rgba; - fg |= (c2 >> 8) & Attributes.RED_MASK | (c2 >> 8) & Attributes.GREEN_MASK | (c2 >> 8) & Attributes.BLUE_MASK; - } - fg |= Attributes.CM_RGB; - } - if (code & COMBINED_CHAR_BIT_MASK) { - if (!line) { - line = terminal.buffer.active.getLine(row); - } - const chars = line!.getCell(x)!.getChars(); - this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, fg, chars); - } else { - this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, fg); - } - } - } - - private _getColorFromAnsiIndex(idx: number): IColor { - if (idx >= this._colors.ansi.length) { - throw new Error('No color found for idx ' + idx); - } - return this._colors.ansi[idx]; - } - public clear(force?: boolean): void { const terminal = this._terminal; const newCount = terminal.cols * terminal.rows * INDICES_PER_CELL; @@ -333,7 +245,7 @@ export class GlyphRenderer { public setColors(): void { } - public render(renderModel: IRenderModel, isSelectionVisible: boolean): void { + public render(renderModel: IRenderModel): void { if (!this._atlas) { return; } @@ -357,7 +269,7 @@ export class GlyphRenderer { let bufferLength = 0; for (let y = 0; y < renderModel.lineLengths.length; y++) { const si = y * this._terminal.cols * INDICES_PER_CELL; - const sub = (isSelectionVisible ? this._vertices.selectionAttributes : this._vertices.attributes).subarray(si, si + renderModel.lineLengths[y] * INDICES_PER_CELL); + const sub = this._vertices.attributes.subarray(si, si + renderModel.lineLengths[y] * INDICES_PER_CELL); activeBuffer.set(sub, bufferLength); bufferLength += sub.length; } diff --git a/addons/xterm-addon-webgl/src/RectangleRenderer.ts b/addons/xterm-addon-webgl/src/RectangleRenderer.ts index c96cc6bc..420e58d4 100644 --- a/addons/xterm-addon-webgl/src/RectangleRenderer.ts +++ b/addons/xterm-addon-webgl/src/RectangleRenderer.ts @@ -4,11 +4,11 @@ */ import { createProgram, expandFloat32Array, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils'; -import { IRenderModel, IWebGLVertexArrayObject, IWebGL2RenderingContext, ISelectionRenderModel } from './Types'; -import { fill } from 'common/TypedArrayUtils'; +import { IRenderModel, IWebGLVertexArrayObject, IWebGL2RenderingContext } from './Types'; import { Attributes, FgFlags } from 'common/buffer/Constants'; import { Terminal } from 'xterm'; -import { 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'; @@ -49,7 +49,6 @@ void main() { interface IVertices { attributes: Float32Array; - selection: Float32Array; count: number; } @@ -66,12 +65,10 @@ export class RectangleRenderer { private _attributesBuffer: WebGLBuffer; private _projectionLocation: WebGLUniformLocation; private _bgFloat!: Float32Array; - private _selectionFloat!: Float32Array; private _vertices: IVertices = { count: 0, - attributes: new Float32Array(INITIAL_BUFFER_RECTANGLE_CAPACITY), - selection: new Float32Array(3 * INDICES_PER_RECTANGLE) + attributes: new Float32Array(INITIAL_BUFFER_RECTANGLE_CAPACITY) }; constructor( @@ -137,11 +134,6 @@ export class RectangleRenderer { gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer); gl.bufferData(gl.ARRAY_BUFFER, this._vertices.attributes, gl.DYNAMIC_DRAW); gl.drawElementsInstanced(this._gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, this._vertices.count); - - // Bind selection buffer and draw - gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer); - gl.bufferData(gl.ARRAY_BUFFER, this._vertices.selection, gl.DYNAMIC_DRAW); - gl.drawElementsInstanced(this._gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, 3); } public onResize(): void { @@ -155,7 +147,6 @@ export class RectangleRenderer { private _updateCachedColors(): void { this._bgFloat = this._colorToFloat32Array(this._colors.background); - this._selectionFloat = this._colorToFloat32Array(this._colors.selectionOpaque); } private _updateViewportRectangle(): void { @@ -171,73 +162,6 @@ export class RectangleRenderer { ); } - public updateSelection(model: ISelectionRenderModel): void { - const terminal = this._terminal; - - if (!model.hasSelection) { - fill(this._vertices.selection, 0, 0); - return; - } - - if (model.columnSelectMode) { - const startCol = model.startCol; - const width = model.endCol - startCol; - const height = model.viewportCappedEndRow - model.viewportCappedStartRow + 1; - this._addRectangleFloat( - this._vertices.selection, - 0, - startCol * this._dimensions.scaledCellWidth, - model.viewportCappedStartRow * this._dimensions.scaledCellHeight, - width * this._dimensions.scaledCellWidth, - height * this._dimensions.scaledCellHeight, - this._selectionFloat - ); - fill(this._vertices.selection, 0, INDICES_PER_RECTANGLE); - } else { - // Draw first row - const startCol = model.viewportStartRow === model.viewportCappedStartRow ? model.startCol : 0; - const startRowEndCol = model.viewportCappedStartRow === model.viewportEndRow ? model.endCol : terminal.cols; - this._addRectangleFloat( - this._vertices.selection, - 0, - startCol * this._dimensions.scaledCellWidth, - model.viewportCappedStartRow * this._dimensions.scaledCellHeight, - (startRowEndCol - startCol) * this._dimensions.scaledCellWidth, - this._dimensions.scaledCellHeight, - this._selectionFloat - ); - - // Draw middle rows - const middleRowsCount = Math.max(model.viewportCappedEndRow - model.viewportCappedStartRow - 1, 0); - this._addRectangleFloat( - this._vertices.selection, - INDICES_PER_RECTANGLE, - 0, - (model.viewportCappedStartRow + 1) * this._dimensions.scaledCellHeight, - terminal.cols * this._dimensions.scaledCellWidth, - middleRowsCount * this._dimensions.scaledCellHeight, - this._selectionFloat - ); - - // Draw final row - if (model.viewportCappedStartRow !== model.viewportCappedEndRow) { - // Only draw viewportEndRow if it's not the same as viewportStartRow - const endCol = model.viewportEndRow === model.viewportCappedEndRow ? model.endCol : terminal.cols; - this._addRectangleFloat( - this._vertices.selection, - INDICES_PER_RECTANGLE * 2, - 0, - model.viewportCappedEndRow * this._dimensions.scaledCellHeight, - endCol * this._dimensions.scaledCellWidth, - this._dimensions.scaledCellHeight, - this._selectionFloat - ); - } else { - fill(this._vertices.selection, 0, INDICES_PER_RECTANGLE * 2); - } - } - } - public updateBackgrounds(model: IRenderModel): void { const terminal = this._terminal; const vertices = this._vertices; diff --git a/addons/xterm-addon-webgl/src/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..d060c4d1 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(); @@ -164,10 +167,6 @@ export class WebglRenderer extends Disposable implements IRenderer { this._core.screenElement!.style.height = `${this.dimensions.canvasHeight}px`; this._rectangleRenderer.onResize(); - if (this._model.selection.hasSelection) { - // Update selection as dimensions have changed - this._rectangleRenderer.updateSelection(this._model.selection); - } this._glyphRenderer.setDimensions(this.dimensions); this._glyphRenderer.onResize(); @@ -198,10 +197,8 @@ export class WebglRenderer extends Disposable implements IRenderer { for (const l of this._renderLayers) { l.onSelectionChanged(this._terminal, start, end, columnSelectMode); } - this._updateSelectionModel(start, end, columnSelectMode); - - this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 }); + this._requestRedrawViewport(); } public onCursorMove(): void { @@ -243,7 +240,7 @@ export class WebglRenderer extends Disposable implements IRenderer { this._charAtlas?.clearTexture(); this._model.clear(); this._updateModel(0, this._terminal.rows - 1); - this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 }); + this._requestRedrawViewport(); } public clear(): void { @@ -289,7 +286,7 @@ export class WebglRenderer extends Disposable implements IRenderer { // Render this._rectangleRenderer.render(); - this._glyphRenderer.render(this._model, this._model.selection.hasSelection); + this._glyphRenderer.render(this._model); } private _updateModel(start: number, end: number): void { @@ -331,14 +328,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 +349,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,17 +363,103 @@ 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; } } } } this._rectangleRenderer.updateBackgrounds(this._model); - if (this._model.selection.hasSelection) { - // Model could be updated but the selection is unchanged - this._glyphRenderer.updateSelection(this._model); + } + + /** + * 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 foreground/background overrides, this happens on the model to avoid spreading + // override logic throughout the different sub-renderers + let bgOverride: number | undefined; + let fgOverride: number | undefined; + + // Apply decorations on the bottom layer + for (const d of this._decorationService.getDecorationsAtCell(x, y, 'bottom')) { + if (d.backgroundColorRGB) { + bgOverride = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF; + } + if (d.foregroundColorRGB) { + fgOverride = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF; + } } + + // Apply the selection color if needed + if (this._isCellSelected(x, y)) { + bgOverride = this._colors.selectionOpaque.rgba >> 8 & 0xFFFFFF; + } + + // Apply decorations on the top layer + for (const d of this._decorationService.getDecorationsAtCell(x, y, 'top')) { + if (d.backgroundColorRGB) { + bgOverride = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF; + } + if (d.foregroundColorRGB) { + fgOverride = d.foregroundColorRGB.rgba >> 8 & 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 _isCellSelected(x: number, y: number): boolean { + if (!this._model.selection.hasSelection) { + return false; + } + y -= this._terminal.buffer.active.viewportY; + if (this._model.selection.columnSelectMode) { + return x >= this._model.selection.startCol && y >= this._model.selection.viewportCappedStartRow && + x < this._model.selection.endCol && y < this._model.selection.viewportCappedEndRow; + } + return (y > this._model.selection.viewportStartRow && y < this._model.selection.viewportEndRow) || + (this._model.selection.viewportStartRow === this._model.selection.viewportEndRow && y === this._model.selection.viewportStartRow && x >= this._model.selection.startCol && x < this._model.selection.endCol) || + (this._model.selection.viewportStartRow < this._model.selection.viewportEndRow && y === this._model.selection.viewportEndRow && x < this._model.selection.endCol) || + (this._model.selection.viewportStartRow < this._model.selection.viewportEndRow && y === this._model.selection.viewportStartRow && x >= this._model.selection.startCol); } private _updateSelectionModel(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void { @@ -382,7 +468,6 @@ export class WebglRenderer extends Disposable implements IRenderer { // Selection does not exist if (!start || !end || (start[0] === end[0] && start[1] === end[1])) { this._model.clearSelection(); - this._rectangleRenderer.updateSelection(this._model.selection); return; } @@ -395,7 +480,6 @@ export class WebglRenderer extends Disposable implements IRenderer { // No need to draw the selection if (viewportCappedStartRow >= terminal.rows || viewportCappedEndRow < 0) { this._model.clearSelection(); - this._rectangleRenderer.updateSelection(this._model.selection); return; } @@ -407,8 +491,6 @@ export class WebglRenderer extends Disposable implements IRenderer { this._model.selection.viewportCappedEndRow = viewportCappedEndRow; this._model.selection.startCol = start[0]; this._model.selection.endCol = end[0]; - - this._rectangleRenderer.updateSelection(this._model.selection); } /** @@ -482,6 +564,10 @@ export class WebglRenderer extends Disposable implements IRenderer { this.dimensions.actualCellHeight = this.dimensions.scaledCellHeight / this._devicePixelRatio; this.dimensions.actualCellWidth = this.dimensions.scaledCellWidth / this._devicePixelRatio; } + + private _requestRedrawViewport(): void { + this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 }); + } } // TODO: Share impl with core 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/css/xterm.css b/css/xterm.css index 7432fbb1..2f84c859 100644 --- a/css/xterm.css +++ b/css/xterm.css @@ -36,6 +36,7 @@ */ .xterm { + cursor: text; position: relative; user-select: none; -ms-user-select: none; @@ -124,10 +125,6 @@ line-height: normal; } -.xterm { - cursor: text; -} - .xterm.enable-mouse-events { /* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */ cursor: default; @@ -184,4 +181,5 @@ position: absolute; top: 0; right: 0; + pointer-events: none; } diff --git a/demo/client.ts b/demo/client.ts index 7c21956a..b9e52d7b 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 }; @@ -212,10 +212,15 @@ function createTerminal(): void { addDomListener(actionElements.findNext, 'keyup', (e) => { addons.search.instance.findNext(actionElements.findNext.value, getSearchOptions(e)); }); - addDomListener(actionElements.findPrevious, 'keyup', (e) => { addons.search.instance.findPrevious(actionElements.findPrevious.value, getSearchOptions(e)); }); + addDomListener(actionElements.findNext, 'blur', (e) => { + addons.search.instance.clearActiveDecoration(); + }); + addDomListener(actionElements.findPrevious, 'blur', (e) => { + addons.search.instance.clearActiveDecoration(); + }); // fit is called within a setTimeout, cols and rows need this. setTimeout(() => { @@ -556,8 +561,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/demo/index.html b/demo/index.html index 6bbcb4a2..f36fc629 100644 --- a/demo/index.html +++ b/demo/index.html @@ -43,7 +43,7 @@ - +

SerializeAddon

diff --git a/demo/server.js b/demo/server.js index c0d5e1f6..71a9d36a 100644 --- a/demo/server.js +++ b/demo/server.js @@ -114,6 +114,9 @@ function startServer() { } const send = USE_BINARY ? bufferUtf8(ws, 5) : buffer(ws, 5); + // WARNING: This is a naive implementation that will not throttle the flow of data. This means + // it could flood the communication channel and make the terminal unresponsive. Learn more about + // the problem and how to implement flow control at https://xtermjs.org/docs/guides/flowcontrol/ term.on('data', function(data) { try { send(data); 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..de3fff90 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -52,11 +52,11 @@ 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'; -import { OverviewRulerRenderer } from 'browser/Decorations/OverviewRulerRenderer'; +import { BufferDecorationRenderer } from 'browser/decorations/BufferDecorationRenderer'; +import { OverviewRulerRenderer } from 'browser/decorations/OverviewRulerRenderer'; import { DecorationService } from 'common/services/DecorationService'; import { IDecorationService } from 'common/services/Services'; @@ -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/Viewport.ts b/src/browser/Viewport.ts index 14fab897..1eb9dc4e 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -26,7 +26,6 @@ export class Viewport extends Disposable implements IViewport { private _lastRecordedBufferHeight: number = 0; private _lastTouchY: number = 0; private _lastScrollTop: number = 0; - private _lastHadScrollBar: boolean = false; private _activeBuffer: IBuffer; private _renderDimensions: IRenderDimensions; @@ -54,7 +53,6 @@ export class Viewport extends Disposable implements IViewport { // Unfortunately the overlay scrollbar would be hidden underneath the screen element in that case, // therefore we account for a standard amount to make it visible this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH; - this._lastHadScrollBar = true; this.register(addDisposableDomListener(this._viewportElement, 'scroll', this._onScroll.bind(this))); // Track properties used in performance critical code manually to avoid using slow getters @@ -109,17 +107,6 @@ export class Viewport extends Disposable implements IViewport { this._viewportElement.scrollTop = scrollTop; } - // Update scroll bar width - if (this._optionsService.rawOptions.scrollback === 0) { - this.scrollBarWidth = 0; - } else { - this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH; - } - this._lastHadScrollBar = this.scrollBarWidth > 0; - - const elementStyle = window.getComputedStyle(this._element); - const elementPadding = parseInt(elementStyle.paddingLeft) + parseInt(elementStyle.paddingRight); - this._viewportElement.style.width = (this._renderService.dimensions.actualCellWidth * (this._bufferService.cols) + this.scrollBarWidth + (this._lastHadScrollBar ? elementPadding : 0)).toString() + 'px'; this._refreshAnimationFrame = null; } @@ -151,11 +138,6 @@ export class Viewport extends Disposable implements IViewport { this._refresh(immediate); return; } - - // If the scroll bar visibility changed - if (this._lastHadScrollBar !== (this._optionsService.rawOptions.scrollback > 0)) { - this._refresh(immediate); - } } /** diff --git a/src/browser/Decorations/BufferDecorationRenderer.ts b/src/browser/decorations/BufferDecorationRenderer.ts similarity index 97% rename from src/browser/Decorations/BufferDecorationRenderer.ts rename to src/browser/decorations/BufferDecorationRenderer.ts index a063f9bd..00b2b00b 100644 --- a/src/browser/Decorations/BufferDecorationRenderer.ts +++ b/src/browser/decorations/BufferDecorationRenderer.ts @@ -74,7 +74,7 @@ export class BufferDecorationRenderer extends Disposable { private _createElement(decoration: IInternalDecoration): HTMLElement { const element = document.createElement('div'); element.classList.add('xterm-decoration'); - element.style.width = `${(decoration.options.width || 1) * this._renderService.dimensions.actualCellWidth}px`; + element.style.width = `${Math.round((decoration.options.width || 1) * this._renderService.dimensions.actualCellWidth)}px`; element.style.height = `${(decoration.options.height || 1) * this._renderService.dimensions.actualCellHeight}px`; element.style.top = `${(decoration.marker.line - this._bufferService.buffers.active.ydisp) * this._renderService.dimensions.actualCellHeight}px`; element.style.lineHeight = `${this._renderService.dimensions.actualCellHeight}px`; diff --git a/src/browser/Decorations/ColorZoneStore.test.ts b/src/browser/decorations/ColorZoneStore.test.ts similarity index 96% rename from src/browser/Decorations/ColorZoneStore.test.ts rename to src/browser/decorations/ColorZoneStore.test.ts index 73e3402f..719ef45b 100644 --- a/src/browser/Decorations/ColorZoneStore.test.ts +++ b/src/browser/decorations/ColorZoneStore.test.ts @@ -4,7 +4,7 @@ */ import { assert } from 'chai'; -import { ColorZoneStore } from 'browser/Decorations/ColorZoneStore'; +import { ColorZoneStore } from 'browser/decorations/ColorZoneStore'; const optionsRedFull = { overviewRulerOptions: { diff --git a/src/browser/Decorations/ColorZoneStore.ts b/src/browser/decorations/ColorZoneStore.ts similarity index 100% rename from src/browser/Decorations/ColorZoneStore.ts rename to src/browser/decorations/ColorZoneStore.ts diff --git a/src/browser/Decorations/OverviewRulerRenderer.ts b/src/browser/decorations/OverviewRulerRenderer.ts similarity index 99% rename from src/browser/Decorations/OverviewRulerRenderer.ts rename to src/browser/decorations/OverviewRulerRenderer.ts index dc35b901..39480ca2 100644 --- a/src/browser/Decorations/OverviewRulerRenderer.ts +++ b/src/browser/decorations/OverviewRulerRenderer.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ColorZoneStore, IColorZone, IColorZoneStore } from 'browser/Decorations/ColorZoneStore'; +import { ColorZoneStore, IColorZone, IColorZoneStore } from 'browser/decorations/ColorZoneStore'; import { addDisposableDomListener } from 'browser/Lifecycle'; import { IRenderService } from 'browser/services/Services'; import { Disposable } from 'common/Lifecycle'; diff --git a/src/browser/renderer/BaseRenderLayer.ts b/src/browser/renderer/BaseRenderLayer.ts index 90d4f82f..696b793f 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,35 @@ export abstract class BaseRenderLayer implements IRenderLayer { return `${fontStyle} ${fontWeight} ${this._optionsService.rawOptions.fontSize * window.devicePixelRatio}px ${this._optionsService.rawOptions.fontFamily}`; } - private _getContrastColor(cell: CellData): IColor | undefined { - if (this._optionsService.rawOptions.minimumContrastRatio === 1 || isPowerlineGlyph(cell.getCode())) { + private _getContrastColor(cell: CellData, x: number, y: number): IColor | undefined { + // Get any decoration foreground/background overrides, this must be fetched before the early + // exist but applied after inverse + let bgOverride: number | undefined; + let fgOverride: number | undefined; + let isTop = false; + for (const d of this._decorationService.getDecorationsAtCell(x, y)) { + if (d.options.layer !== 'top' && isTop) { + continue; + } + if (d.backgroundColorRGB) { + bgOverride = d.backgroundColorRGB.rgba; + } + if (d.foregroundColorRGB) { + fgOverride = d.foregroundColorRGB.rgba; + } + isTop = d.options.layer === 'top'; + } + + if (!bgOverride && !fgOverride && (this._optionsService.rawOptions.minimumContrastRatio === 1 || isPowerlineGlyph(cell.getCode()))) { 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 +484,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 +505,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..ef5a9b62 100644 --- a/src/browser/renderer/TextRenderLayer.ts +++ b/src/browser/renderer/TextRenderLayer.ts @@ -11,7 +11,7 @@ import { AttributeData } from 'common/buffer/AttributeData'; import { NULL_CELL_CODE, Content } from 'common/buffer/Constants'; import { IColorSet } from 'browser/Types'; import { CellData } from 'common/buffer/CellData'; -import { IOptionsService, IBufferService } from 'common/services/Services'; +import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services'; import { ICharacterJoinerService } from 'browser/services/Services'; import { JoinedCellData } from 'browser/services/CharacterJoinerService'; @@ -37,9 +37,10 @@ export class TextRenderLayer extends BaseRenderLayer { rendererId: number, @IBufferService bufferService: IBufferService, @IOptionsService optionsService: IOptionsService, - @ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService + @ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService, + @IDecorationService decorationService: IDecorationService ) { - super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService); + super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService, decorationService); this._state = new GridCache(); } @@ -176,6 +177,19 @@ export class TextRenderLayer extends BaseRenderLayer { nextFillStyle = this._colors.ansi[cell.getBgColor()].css; } + // Get any decoration foreground/background overrides, this must be fetched before the early + // exist but applied after inverse + let isTop = false; + for (const d of this._decorationService.getDecorationsAtCell(x, this._bufferService.buffer.ydisp + y)) { + if (d.options.layer !== 'top' && isTop) { + continue; + } + if (d.backgroundColorRGB) { + nextFillStyle = d.backgroundColorRGB.css; + } + isTop = d.options.layer === 'top'; + } + if (prevFillStyle === null) { // 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..bf3939e8 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,28 @@ 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; + let isTop = false; + for (const d of this._decorationService.getDecorationsAtCell(x, row)) { + if (d.options.layer !== 'top' && isTop) { + continue; + } + if (d.backgroundColorRGB) { + bgColorMode = Attributes.CM_RGB; + bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF; + bgOverride = d.backgroundColorRGB; + } + if (d.foregroundColorRGB) { + fgColorMode = Attributes.CM_RGB; + fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF; + fgOverride = d.foregroundColorRGB; + } + isTop = d.options.layer === 'top'; + } + // Foreground switch (fgColorMode) { case Attributes.CM_P16: @@ -179,7 +202,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 +212,13 @@ export class DomRendererRowFactory { (fg >> 8) & 0xFF, (fg ) & 0xFF ); - if (!this._applyMinimumContrast(charElement, this._colors.background, color, cell)) { + if (!this._applyMinimumContrast(charElement, this._colors.background, color, cell, bgOverride, fgOverride)) { this._addStyle(charElement, `color:#${padStart(fg.toString(16), '0', 6)}`); } break; case Attributes.CM_DEFAULT: default: - if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.foreground, cell)) { + if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.foreground, cell, undefined, undefined)) { if (isInverse) { charElement.classList.add(`xterm-fg-${INVERTED_DEFAULT_COLOR}`); } @@ -209,7 +232,7 @@ export class DomRendererRowFactory { charElement.classList.add(`xterm-bg-${bg}`); break; case Attributes.CM_RGB: - this._addStyle(charElement, `background-color:#${padStart(bg.toString(16), '0', 6)}`); + this._addStyle(charElement, `background-color:#${padStart((bg >>> 0).toString(16), '0', 6)}`); break; case Attributes.CM_DEFAULT: default: @@ -225,18 +248,23 @@ export class DomRendererRowFactory { return fragment; } - private _applyMinimumContrast(element: HTMLElement, bg: IColor, fg: IColor, cell: ICellData): boolean { + private _applyMinimumContrast(element: HTMLElement, bg: IColor, fg: IColor, cell: ICellData, bgOverride: IColor | undefined, fgOverride: IColor | undefined): boolean { if (this._optionsService.rawOptions.minimumContrastRatio === 1 || isPowerlineGlyph(cell.getCode())) { return false; } - // Try get from cache first - let adjustedColor = this._colors.contrastCache.getColor(this._workCell.bg, this._workCell.fg); + // Try get from cache first, only use the cache when there are no decoration overrides + let adjustedColor: IColor | undefined | null = undefined; + if (!bgOverride || !fgOverride) { + adjustedColor = this._colors.contrastCache.getColor(this._workCell.bg, this._workCell.fg); + } // Calculate and store in cache if (adjustedColor === undefined) { - adjustedColor = color.ensureContrastRatio(bg, fg, this._optionsService.rawOptions.minimumContrastRatio); - this._colors.contrastCache.setColor(this._workCell.bg, this._workCell.fg, adjustedColor ?? null); + adjustedColor = color.ensureContrastRatio(bgOverride || bg, fgOverride || fg, this._optionsService.rawOptions.minimumContrastRatio); + if (!bgOverride || !fgOverride) { + this._colors.contrastCache.setColor(this._workCell.bg, this._workCell.fg, adjustedColor ?? null); + } } if (adjustedColor) { diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index b575858a..75c2d3c8 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._handleOptionsChanged())); 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..755f13b3 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, layer?: 'bottom' | 'top'): IterableIterator { + let xmin = 0; + let xmax = 0; + for (const d of this._decorations.getKeyIterator(line)) { + xmin = d.options.x ?? 0; + xmax = xmin + (d.options.width ?? 1); + if (x >= xmin && x < xmax && (!layer || (d.options.layer ?? 'bottom') === layer)) { + 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..c3190210 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, layer?: 'bottom' | 'top'): 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..15ee4650 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,28 @@ 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; + + /** + * What layer to render the decoration at when {@link backgroundColor} or + * {@link foregroundColor} are used. `'bottom'` will render under the selection, `'top`' will + * render above the selection\*. + * + * *\* The selection will render on top regardless of layer on the canvas renderer due to how + * it renders selection separately.* + */ + readonly layer?: 'bottom' | 'top'; + /** * When defined, renders the decoration in the overview ruler to the right * of the terminal. {@link ITerminalOptions.overviewRulerWidth} must be set