Merge remote-tracking branch 'upstream/master' into 3773

This commit is contained in:
Daniel Imms
2022-05-13 08:46:05 -07:00
44 changed files with 763 additions and 358 deletions
+4 -1
View File
@@ -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))
+18 -12
View File
@@ -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;
}
+10 -3
View File
@@ -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.
+7 -95
View File
@@ -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;
}
@@ -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;
+3 -1
View File
@@ -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);
}
+110 -24
View File
@@ -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
@@ -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: '',
@@ -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';
@@ -20,6 +20,7 @@
]
},
"strict": true,
"downlevelIteration": true,
"types": [
"../../../node_modules/@types/mocha"
]
@@ -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<number[]> {
+2 -4
View File
@@ -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;
}
+19 -6
View File
@@ -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() {
+1 -1
View File
@@ -43,7 +43,7 @@
<label><input type="checkbox" id="regex"/>Use regex</label>
<label><input type="checkbox" id="case-sensitive"/>Case sensitive</label>
<label><input type="checkbox" id="whole-word"/>Whole word</label>
<label><input type="checkbox" id="highlight-all-matches"/>Highlight All Matches</label>
<label><input type="checkbox" id="highlight-all-matches" checked/>Highlight All Matches</label>
</div>
<h4>SerializeAddon</h4>
<div>
+3
View File
@@ -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);
+2 -1
View File
@@ -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 } = {};
+3 -3
View File
@@ -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 {
+4 -3
View File
@@ -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;
+1 -7
View File
@@ -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;
-18
View File
@@ -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);
}
}
/**
@@ -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`;
@@ -4,7 +4,7 @@
*/
import { assert } from 'chai';
import { ColorZoneStore } from 'browser/Decorations/ColorZoneStore';
import { ColorZoneStore } from 'browser/decorations/ColorZoneStore';
const optionsRedFull = {
overviewRulerOptions: {
@@ -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';
+55 -18
View File
@@ -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;
}
+4 -3
View File
@@ -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,
+4 -3
View File
@@ -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));
-1
View File
@@ -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;
+4 -3
View File
@@ -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();
}
+17 -3
View File
@@ -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<CharData>();
}
@@ -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.
@@ -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.
+6 -7
View File
@@ -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;
@@ -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);
});
@@ -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) {
+8 -1
View File
@@ -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));
@@ -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', () => {
+5 -6
View File
@@ -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;
}
+107
View File
@@ -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<number>;
function assertList(expected: number[]): void {
deepStrictEqual(Array.from(list.values()), expected);
}
beforeEach(() => {
list = new SortedList<number>(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 }
]);
});
});
+88
View File
@@ -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<T> {
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<T> {
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<T> {
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;
}
}
+14 -1
View File
@@ -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<IInternalDecoration> { return [].values(); }
public onDecorationRegistered = new EventEmitter<IInternalDecoration>().event;
public onDecorationRemoved = new EventEmitter<IInternalDecoration>().event;
public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined { return undefined; }
public reset(): void { }
public *getDecorationsAtLine(line: number): IterableIterator<IInternalDecoration> { }
public *getDecorationsAtCell(x: number, line: number): IterableIterator<IInternalDecoration> { }
public dispose(): void { }
}
+5
View File
@@ -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 {
+63 -10
View File
@@ -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<IInternalDecoration> = new SortedList(e => e.marker.line);
private _onDecorationRegistered = this.register(new EventEmitter<IInternalDecoration>());
public get onDecorationRegistered(): IEvent<IInternalDecoration> { 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<IInternalDecoration> {
return this._decorations.getKeyIterator(line);
}
public *getDecorationsAtCell(x: number, line: number, layer?: 'bottom' | 'top'): IterableIterator<IInternalDecoration> {
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<void>());
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;
+8 -1
View File
@@ -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<IInternalDecoration>;
readonly onDecorationRemoved: IEvent<IInternalDecoration>;
registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined;
reset(): void;
/** Iterates over the decorations at a line (in no particular order). */
getDecorationsAtLine(line: number): IterableIterator<IInternalDecoration>;
/** Iterates over the decorations at a cell (in no particular order). */
getDecorationsAtCell(x: number, line: number, layer?: 'bottom' | 'top'): IterableIterator<IInternalDecoration>;
}
export interface IInternalDecoration extends IDecoration {
readonly options: IDecorationOptions;
readonly backgroundColorRGB: IColor | undefined;
readonly foregroundColorRGB: IColor | undefined;
readonly onRenderEmitter: IEventEmitter<HTMLElement>;
}
+23 -1
View File
@@ -444,7 +444,7 @@ declare module 'xterm' {
* This will only take effect when {@link IDecorationOptions.overviewRulerOptions}
* were provided initially.
*/
options: Pick<IDecorationOptions, 'overviewRulerOptions'>;
options: Pick<IDecorationOptions, 'overviewRulerOptions'>;
}
@@ -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