Skip to content

DOM Renderer: Render background separately #4818

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions css/xterm.css
Original file line number Diff line number Diff line change
@@ -160,6 +160,20 @@
overflow: hidden;
}

.xterm .xterm-rows div {
position: relative;
}

.xterm .xterm-rows span {
position: relative;
}

.xterm .xterm-rows span.xterm-bg {
position: absolute;
top: 0;
bottom: 0;
}

.xterm-dim {
/* Dim should not apply to background, so the opacity of the foreground color is applied
* explicitly in the generated class and reset to 1 here */
76 changes: 0 additions & 76 deletions src/browser/renderer/dom/DomRenderer.ts
Original file line number Diff line number Diff line change
@@ -39,7 +39,6 @@ export class DomRenderer extends Disposable implements IRenderer {
private _dimensionsStyleElement!: HTMLStyleElement;
private _rowContainer: HTMLElement;
private _rowElements: HTMLElement[] = [];
private _selectionContainer: HTMLElement;
private _widthCache: WidthCache;

public dimensions: IRenderDimensions;
@@ -66,9 +65,6 @@ export class DomRenderer extends Disposable implements IRenderer {
this._rowContainer.style.lineHeight = 'normal';
this._rowContainer.setAttribute('aria-hidden', 'true');
this._refreshRowElements(this._bufferService.cols, this._bufferService.rows);
this._selectionContainer = this._document.createElement('div');
this._selectionContainer.classList.add(SELECTION_CLASS);
this._selectionContainer.setAttribute('aria-hidden', 'true');

this.dimensions = createRenderDimensions();
this._updateDimensions();
@@ -81,7 +77,6 @@ export class DomRenderer extends Disposable implements IRenderer {

this._element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass);
this._screenElement.appendChild(this._rowContainer);
this._screenElement.appendChild(this._selectionContainer);

this.register(this._linkifier2.onShowLinkUnderline(e => this._handleLinkHover(e)));
this.register(this._linkifier2.onHideLinkUnderline(e => this._handleLinkLeave(e)));
@@ -92,7 +87,6 @@ export class DomRenderer extends Disposable implements IRenderer {
// Outside influences such as React unmounts may manipulate the DOM before our disposal.
// https://github.com/xtermjs/xterm.js/issues/2960
this._rowContainer.remove();
this._selectionContainer.remove();
this._widthCache.dispose();
this._themeStyleElement.remove();
this._dimensionsStyleElement.remove();
@@ -127,25 +121,13 @@ export class DomRenderer extends Disposable implements IRenderer {
element.style.width = `${this.dimensions.css.canvas.width}px`;
element.style.height = `${this.dimensions.css.cell.height}px`;
element.style.lineHeight = `${this.dimensions.css.cell.height}px`;
// Make sure rows don't overflow onto following row
element.style.overflow = 'hidden';
}

if (!this._dimensionsStyleElement) {
this._dimensionsStyleElement = this._document.createElement('style');
this._screenElement.appendChild(this._dimensionsStyleElement);
}

const styles =
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} span {` +
` display: inline-block;` + // TODO: find workaround for inline-block (creates ~20% render penalty)
` height: 100%;` +
` vertical-align: top;` +
`}`;

this._dimensionsStyleElement.textContent = styles;

this._selectionContainer.style.height = this._viewportElement.style.height;
this._screenElement.style.width = `${this.dimensions.css.canvas.width}px`;
this._screenElement.style.height = `${this.dimensions.css.canvas.height}px`;
}
@@ -310,66 +292,8 @@ export class DomRenderer extends Disposable implements IRenderer {
}

public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
// Remove all selections
this._selectionContainer.replaceChildren();
this._rowFactory.handleSelectionChanged(start, end, columnSelectMode);
this.renderRows(0, this._bufferService.rows - 1);

// Selection does not exist
if (!start || !end) {
return;
}

// Translate from buffer position to viewport position
const viewportStartRow = start[1] - this._bufferService.buffer.ydisp;
const viewportEndRow = end[1] - this._bufferService.buffer.ydisp;
const viewportCappedStartRow = Math.max(viewportStartRow, 0);
const viewportCappedEndRow = Math.min(viewportEndRow, this._bufferService.rows - 1);

// No need to draw the selection
if (viewportCappedStartRow >= this._bufferService.rows || viewportCappedEndRow < 0) {
return;
}

// Create the selections
const documentFragment = this._document.createDocumentFragment();

if (columnSelectMode) {
const isXFlipped = start[0] > end[0];
documentFragment.appendChild(
this._createSelectionElement(viewportCappedStartRow, isXFlipped ? end[0] : start[0], isXFlipped ? start[0] : end[0], viewportCappedEndRow - viewportCappedStartRow + 1)
);
} else {
// Draw first row
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
const endCol = viewportCappedStartRow === viewportEndRow ? end[0] : this._bufferService.cols;
documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow, startCol, endCol));
// Draw middle rows
const middleRowsCount = viewportCappedEndRow - viewportCappedStartRow - 1;
documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow + 1, 0, this._bufferService.cols, middleRowsCount));
// Draw final row
if (viewportCappedStartRow !== viewportCappedEndRow) {
// Only draw viewportEndRow if it's not the same as viewporttartRow
const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._bufferService.cols;
documentFragment.appendChild(this._createSelectionElement(viewportCappedEndRow, 0, endCol));
}
}
this._selectionContainer.appendChild(documentFragment);
}

/**
* Creates a selection element at the specified position.
* @param row The row of the selection.
* @param colStart The start column.
* @param colEnd The end columns.
*/
private _createSelectionElement(row: number, colStart: number, colEnd: number, rowCount: number = 1): HTMLElement {
const element = this._document.createElement('div');
element.style.height = `${rowCount * this.dimensions.css.cell.height}px`;
element.style.top = `${row * this.dimensions.css.cell.height}px`;
element.style.left = `${colStart * this.dimensions.css.cell.width}px`;
element.style.width = `${this.dimensions.css.cell.width * (colEnd - colStart)}px`;
return element;
}

public handleCursorMove(): void {
632 changes: 352 additions & 280 deletions src/browser/renderer/dom/DomRendererRowFactory.ts
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ import { IBufferLine, ICellData, IColor } from 'common/Types';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
import { WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants';
import { CellData } from 'common/buffer/CellData';
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { ICoreService, IDecorationService, IInternalDecoration, IOptionsService } from 'common/services/Services';
import { color, rgba } from 'common/Color';
import { ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
@@ -72,24 +72,28 @@ export class DomRendererRowFactory {
linkEnd: number
): HTMLSpanElement[] {

const elements: HTMLSpanElement[] = [];
const charElements: HTMLSpanElement[] = [];
const backgroundElements: HTMLSpanElement[] = [];
const joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
const colors = this._themeService.colors;

let lineLength = lineData.getNoBgTrimmedLength();
let lineLength = this._isRowInSelection(row) ? lineData.length : lineData.getNoBgTrimmedLength();
if (isCursorRow && lineLength < cursorX + 1) {
lineLength = cursorX + 1;
}

let charElement: HTMLSpanElement | undefined;
let cellAmount = 0;
let text = '';
let charCellAmount = 0;
let charText = '';
let backgroundElement: HTMLSpanElement | undefined;
let backgroundCellAmount = 0;
let oldBg = 0;
let oldFg = 0;
let oldExt = 0;
let oldLinkHover: number | boolean = false;
let oldSpacing = 0;
let oldIsInSelection: boolean = false;
let oldDecorations: IInternalDecoration[] = [];
let spacing = 0;
const classes: string[] = [];

@@ -135,10 +139,11 @@ export class DomRendererRowFactory {
const isCursorCell = isCursorRow && x === cursorX;
const isLinkHover = hasHover && x >= linkStart && x <= linkEnd;

let isDecorated = false;
let decorations: IInternalDecoration[] = [];
this._decorationService.forEachDecorationAtCell(x, row, undefined, d => {
isDecorated = true;
decorations.push(d);
});
const isSameDecorations = this._isSameDecorations(decorations, oldDecorations);

// get chars to render for this cell
let chars = cell.getChars() || WHITESPACE_CELL_CHAR;
@@ -149,312 +154,356 @@ export class DomRendererRowFactory {
// lookup char render width and calc spacing
spacing = width * cellWidth - widthCache.get(chars, cell.isBold(), cell.isItalic());

if (!charElement) {
charElement = this._document.createElement('span');
} else {
/**
* chars can only be merged on existing span if:
* - existing span only contains mergeable chars (cellAmount != 0)
* - bg did not change (or both are in selection)
* - fg did not change (or both are in selection and selection fg is set)
* - ext did not change
* - underline from hover state did not change
* - cell content renders to same letter-spacing
* - cell is not cursor
*/
if (
cellAmount
&& (
(isInSelection && oldIsInSelection)
|| (!isInSelection && !oldIsInSelection && cell.bg === oldBg)
)
&& (
(isInSelection && oldIsInSelection && colors.selectionForeground)
|| cell.fg === oldFg
)
&& cell.extended.ext === oldExt
&& isLinkHover === oldLinkHover
&& spacing === oldSpacing
&& !isCursorCell
&& !isJoined
&& !isDecorated
) {
// no span alterations, thus only account chars skipping all code below
if (cell.isInvisible()) {
text += WHITESPACE_CELL_CHAR;
} else {
text += chars;
}
cellAmount++;
continue;
} else {
/**
* cannot merge:
* - apply left-over text to old span
* - create new span, reset state holders cellAmount & text
*/
if (cellAmount) {
charElement.textContent = text;
}
charElement = this._document.createElement('span');
cellAmount = 0;
text = '';
}
}
// preserve conditions for next merger eval round
oldBg = cell.bg;
oldFg = cell.fg;
oldExt = cell.extended.ext;
oldLinkHover = isLinkHover;
oldSpacing = spacing;
oldIsInSelection = isInSelection;

if (isJoined) {
// The DOM renderer colors the background of the cursor but for ligatures all cells are
// joined. The workaround here is to show a cursor around the whole ligature so it shows up,
// the cursor looks the same when on any character of the ligature though
if (cursorX >= x && cursorX <= lastCharX) {
cursorX = x;
}
}

if (!this._coreService.isCursorHidden && isCursorCell && this._coreService.isCursorInitialized) {
classes.push(RowCss.CURSOR_CLASS);
if (this._coreBrowserService.isFocused) {
if (cursorBlink) {
classes.push(RowCss.CURSOR_BLINK_CLASS);
}
classes.push(
cursorStyle === 'bar'
? RowCss.CURSOR_STYLE_BAR_CLASS
: cursorStyle === 'underline'
? RowCss.CURSOR_STYLE_UNDERLINE_CLASS
: RowCss.CURSOR_STYLE_BLOCK_CLASS
);
} else {
if (cursorInactiveStyle) {
switch (cursorInactiveStyle) {
case 'outline':
classes.push(RowCss.CURSOR_STYLE_OUTLINE_CLASS);
break;
case 'block':
classes.push(RowCss.CURSOR_STYLE_BLOCK_CLASS);
break;
case 'bar':
classes.push(RowCss.CURSOR_STYLE_BAR_CLASS);
break;
case 'underline':
classes.push(RowCss.CURSOR_STYLE_UNDERLINE_CLASS);
break;
default:
break;
}
}
}
}

if (cell.isBold()) {
classes.push(RowCss.BOLD_CLASS);
}
/**
* chars can only be merged on existing span if:
* - existing span only contains mergeable chars (cellAmount != 0)
* - bg did not change (or both are in selection)
* - fg did not change (or both are in selection and selection fg is set)
* - ext did not change
* - underline from hover state did not change
* - cell content renders to same letter-spacing
* - cell is not cursor
* - cell characters are not joined
* - cell is not decorated
*/
const canMergeCharacters = charElement
&& charCellAmount
&& (
(isInSelection && oldIsInSelection && colors.selectionForeground)
|| cell.fg === oldFg
)
&& cell.extended.ext === oldExt
&& isLinkHover === oldLinkHover
&& isSameDecorations
&& spacing === oldSpacing
&& !isCursorCell
&& !isJoined

/**
* background can only be merged on existing span if:
* - existing span only contains mergeable chars (cellAmount != 0)
* - bg did not change (or both are in selection)
* - cell is not decorated
*/
const canMergeBackground = backgroundElement
&& backgroundCellAmount
&& isSameDecorations
&& (
(isInSelection && oldIsInSelection)
|| (!isInSelection && !oldIsInSelection && cell.bg === oldBg)
);

if (cell.isItalic()) {
classes.push(RowCss.ITALIC_CLASS);
// if the background related cell attributes have not changed
// from the previous cell then we simply extend the existing span
if (canMergeBackground) {
backgroundCellAmount += width;
}

if (cell.isDim()) {
classes.push(RowCss.DIM_CLASS);
// if the character related cell attributes have not changed
// from the previous cell then we simply extend the existing span
if (canMergeCharacters) {
charText += chars;
charCellAmount++;
}

if (cell.isInvisible()) {
text = WHITESPACE_CELL_CHAR;
} else {
text = cell.getChars() || WHITESPACE_CELL_CHAR;
}
// character / background attributes have changed - we need to do more work
if (!canMergeCharacters || !canMergeBackground) {

// preserve conditions for next merger eval round
oldBg = cell.bg;
oldFg = cell.fg;
oldExt = cell.extended.ext;
oldLinkHover = isLinkHover;
oldSpacing = spacing;
oldIsInSelection = isInSelection;
oldDecorations = decorations;

let fg = cell.getFgColor();
let fgColorMode = cell.getFgColorMode();
let bg = cell.getBgColor();
let bgColorMode = cell.getBgColorMode();
const isInverse = !!cell.isInverse();
if (isInverse) {
const temp = fg;
fg = bg;
bg = temp;
const temp2 = fgColorMode;
fgColorMode = bgColorMode;
bgColorMode = temp2;
}

if (cell.isUnderline()) {
classes.push(`${RowCss.UNDERLINE_CLASS}-${cell.extended.underlineStyle}`);
if (text === ' ') {
text = '\xa0'; // = &nbsp;
// 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;
this._decorationService.forEachDecorationAtCell(x, row, undefined, d => {
if (d.options.layer !== 'top' && isTop) {
return;
}
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';
});

// Apply selection
if (!isTop && isInSelection) {
// If in the selection, force the element to be above the selection to improve contrast and
// support opaque selections.
bgOverride = this._coreBrowserService.isFocused ? colors.selectionBackgroundOpaque : colors.selectionInactiveBackgroundOpaque;
bg = bgOverride.rgba >> 8 & 0xFFFFFF;
bgColorMode = Attributes.CM_RGB;
// Since an opaque selection is being rendered, the selection pretends to be a decoration to
// ensure text is drawn above the selection.
isTop = true;
// Apply selection foreground if applicable
if (colors.selectionForeground) {
fgColorMode = Attributes.CM_RGB;
fg = colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
fgOverride = colors.selectionForeground;
}
}
if (!cell.isUnderlineColorDefault()) {
if (cell.isUnderlineColorRGB()) {
charElement.style.textDecorationColor = `rgb(${AttributeData.toColorRGB(cell.getUnderlineColor()).join(',')})`;
} else {
let fg = cell.getUnderlineColor();
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
fg += 8;

// Background
let resolvedBg: IColor;
switch (bgColorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
resolvedBg = colors.ansi[bg];
break;
case Attributes.CM_RGB:
resolvedBg = rgba.toColor(bg >> 16, bg >> 8 & 0xFF, bg & 0xFF);
break;
case Attributes.CM_DEFAULT:
default:
if (isInverse) {
resolvedBg = colors.foreground;
} else {
resolvedBg = colors.background;
}
charElement.style.textDecorationColor = colors.ansi[fg].css;
}

// If there is no background override by now it's the original color, so apply dim if needed
if (!bgOverride) {
if (cell.isDim()) {
bgOverride = color.multiplyOpacity(resolvedBg, 0.5);
}
}
}

if (cell.isOverline()) {
classes.push(RowCss.OVERLINE_CLASS);
if (text === ' ') {
text = '\xa0'; // = &nbsp;
// create a new background span
if (!canMergeBackground) {
if (backgroundCellAmount) {
backgroundElement!.style.width = `${backgroundCellAmount * cellWidth}px`;
}
backgroundElement = this._document.createElement('span');
backgroundElement.classList.add('xterm-bg');
backgroundElement.style.left = `${cellWidth * x}px`;
backgroundCellAmount = 0;

switch (bgColorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
backgroundElement.classList.add(`xterm-bg-${bg}`);
break;
case Attributes.CM_RGB:
backgroundElement.style.backgroundColor = `#${padStart((bg >>> 0).toString(16), '0', 6)}`;
break;
case Attributes.CM_DEFAULT:
default:
if (isInverse) {
backgroundElement.classList.add(`xterm-bg-${INVERTED_DEFAULT_COLOR}`);
}
}
// exclude conditions for cell merging - never merge these
backgroundCellAmount += width;
backgroundElements.push(backgroundElement);
}
}

if (cell.isStrikethrough()) {
classes.push(RowCss.STRIKETHROUGH_CLASS);
}
// character span
if (!canMergeCharacters) {
if (charCellAmount) {
charElement!.textContent = charText;
}
charElement = this._document.createElement('span');
charCellAmount = 0;
charText = '';

if (isJoined) {
// The DOM renderer colors the background of the cursor but for ligatures all cells are
// joined. The workaround here is to show a cursor around the whole ligature so it shows up,
// the cursor looks the same when on any character of the ligature though
if (cursorX >= x && cursorX <= lastCharX) {
cursorX = x;
}
}

// apply link hover underline late, effectively overrides any previous text-decoration
// settings
if (isLinkHover) {
charElement.style.textDecoration = 'underline';
}
// If it's a top decoration, render above the selection
if (isTop) {
classes.push('xterm-decoration-top');
}

let fg = cell.getFgColor();
let fgColorMode = cell.getFgColorMode();
let bg = cell.getBgColor();
let bgColorMode = cell.getBgColorMode();
const isInverse = !!cell.isInverse();
if (isInverse) {
const temp = fg;
fg = bg;
bg = temp;
const temp2 = fgColorMode;
fgColorMode = bgColorMode;
bgColorMode = temp2;
}
if (!this._coreService.isCursorHidden && isCursorCell && this._coreService.isCursorInitialized) {
classes.push(RowCss.CURSOR_CLASS);
if (this._coreBrowserService.isFocused) {
if (cursorBlink) {
classes.push(RowCss.CURSOR_BLINK_CLASS);
}
classes.push(
cursorStyle === 'bar'
? RowCss.CURSOR_STYLE_BAR_CLASS
: cursorStyle === 'underline'
? RowCss.CURSOR_STYLE_UNDERLINE_CLASS
: RowCss.CURSOR_STYLE_BLOCK_CLASS
);
} else {
if (cursorInactiveStyle) {
switch (cursorInactiveStyle) {
case 'outline':
classes.push(RowCss.CURSOR_STYLE_OUTLINE_CLASS);
break;
case 'block':
classes.push(RowCss.CURSOR_STYLE_BLOCK_CLASS);
break;
case 'bar':
classes.push(RowCss.CURSOR_STYLE_BAR_CLASS);
break;
case 'underline':
classes.push(RowCss.CURSOR_STYLE_UNDERLINE_CLASS);
break;
default:
break;
}
}
}
}

// 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;
this._decorationService.forEachDecorationAtCell(x, row, undefined, d => {
if (d.options.layer !== 'top' && isTop) {
return;
}
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';
});
if (cell.isBold()) {
classes.push(RowCss.BOLD_CLASS);
}

// Apply selection
if (!isTop && isInSelection) {
// If in the selection, force the element to be above the selection to improve contrast and
// support opaque selections. The applies background is not actually needed here as
// selection is drawn in a seperate container, the main purpose of this to ensuring minimum
// contrast ratio
bgOverride = this._coreBrowserService.isFocused ? colors.selectionBackgroundOpaque : colors.selectionInactiveBackgroundOpaque;
bg = bgOverride.rgba >> 8 & 0xFFFFFF;
bgColorMode = Attributes.CM_RGB;
// Since an opaque selection is being rendered, the selection pretends to be a decoration to
// ensure text is drawn above the selection.
isTop = true;
// Apply selection foreground if applicable
if (colors.selectionForeground) {
fgColorMode = Attributes.CM_RGB;
fg = colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
fgOverride = colors.selectionForeground;
}
}
if (cell.isItalic()) {
classes.push(RowCss.ITALIC_CLASS);
}

// If it's a top decoration, render above the selection
if (isTop) {
classes.push('xterm-decoration-top');
}
if (cell.isDim()) {
classes.push(RowCss.DIM_CLASS);
}

// Background
let resolvedBg: IColor;
switch (bgColorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
resolvedBg = colors.ansi[bg];
classes.push(`xterm-bg-${bg}`);
break;
case Attributes.CM_RGB:
resolvedBg = rgba.toColor(bg >> 16, bg >> 8 & 0xFF, bg & 0xFF);
this._addStyle(charElement, `background-color:#${padStart((bg >>> 0).toString(16), '0', 6)}`);
break;
case Attributes.CM_DEFAULT:
default:
if (isInverse) {
resolvedBg = colors.foreground;
classes.push(`xterm-bg-${INVERTED_DEFAULT_COLOR}`);
if (cell.isInvisible()) {
charText = WHITESPACE_CELL_CHAR;
} else {
resolvedBg = colors.background;
charText = cell.getChars() || WHITESPACE_CELL_CHAR;
}
}

// If there is no background override by now it's the original color, so apply dim if needed
if (!bgOverride) {
if (cell.isDim()) {
bgOverride = color.multiplyOpacity(resolvedBg, 0.5);
}
}
if (cell.isUnderline()) {
classes.push(`${RowCss.UNDERLINE_CLASS}-${cell.extended.underlineStyle}`);
if (charText === ' ') {
charText = '\xa0'; // = &nbsp;
}
if (!cell.isUnderlineColorDefault()) {
if (cell.isUnderlineColorRGB()) {
charElement.style.textDecorationColor = `rgb(${AttributeData.toColorRGB(cell.getUnderlineColor()).join(',')})`;
} else {
let fg = cell.getUnderlineColor();
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
fg += 8;
}
charElement.style.textDecorationColor = colors.ansi[fg].css;
}
}
}

// Foreground
switch (fgColorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
if (cell.isBold() && fg < 8 && this._optionsService.rawOptions.drawBoldTextInBrightColors) {
fg += 8;
if (cell.isOverline()) {
classes.push(RowCss.OVERLINE_CLASS);
if (charText === ' ') {
charText = '\xa0'; // = &nbsp;
}
}
if (!this._applyMinimumContrast(charElement, resolvedBg, colors.ansi[fg], cell, bgOverride, undefined)) {
classes.push(`xterm-fg-${fg}`);

if (cell.isStrikethrough()) {
classes.push(RowCss.STRIKETHROUGH_CLASS);
}
break;
case Attributes.CM_RGB:
const color = rgba.toColor(
(fg >> 16) & 0xFF,
(fg >> 8) & 0xFF,
(fg ) & 0xFF
);
if (!this._applyMinimumContrast(charElement, resolvedBg, color, cell, bgOverride, fgOverride)) {
this._addStyle(charElement, `color:#${padStart(fg.toString(16), '0', 6)}`);

// apply link hover underline late, effectively overrides any previous text-decoration
// settings
if (isLinkHover) {
charElement.style.textDecoration = 'underline';
}
break;
case Attributes.CM_DEFAULT:
default:
if (!this._applyMinimumContrast(charElement, resolvedBg, colors.foreground, cell, bgOverride, fgOverride)) {
if (isInverse) {
classes.push(`xterm-fg-${INVERTED_DEFAULT_COLOR}`);
}

// Foreground
switch (fgColorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
if (cell.isBold() && fg < 8 && this._optionsService.rawOptions.drawBoldTextInBrightColors) {
fg += 8;
}
if (!this._applyMinimumContrast(charElement, resolvedBg, colors.ansi[fg], cell, bgOverride, undefined)) {
classes.push(`xterm-fg-${fg}`);
}
break;
case Attributes.CM_RGB:
const color = rgba.toColor(
(fg >> 16) & 0xFF,
(fg >> 8) & 0xFF,
(fg) & 0xFF
);
if (!this._applyMinimumContrast(charElement, resolvedBg, color, cell, bgOverride, fgOverride)) {
charElement.style.color = `#${padStart(fg.toString(16), '0', 6)}`;
}
break;
case Attributes.CM_DEFAULT:
default:
if (!this._applyMinimumContrast(charElement, resolvedBg, colors.foreground, cell, bgOverride, fgOverride)) {
if (isInverse) {
classes.push(`xterm-fg-${INVERTED_DEFAULT_COLOR}`);
}
}
}
}

// apply CSS classes
// slightly faster than using classList by omitting
// checks for doubled entries (code above should not have doublets)
if (classes.length) {
charElement.className = classes.join(' ');
classes.length = 0;
}
// apply CSS classes
// slightly faster than using classList by omitting
// checks for doubled entries (code above should not have doublets)
if (classes.length) {
charElement.className = classes.join(' ');
classes.length = 0;
}

// exclude conditions for cell merging - never merge these
if (!isCursorCell && !isJoined) {
charCellAmount++;
} else {
charElement.textContent = charText;
}
// apply letter-spacing rule
if (spacing !== this.defaultSpacing) {
charElement.style.letterSpacing = `${spacing}px`;
}

charElements.push(charElement);
x = lastCharX;
}

// exclude conditions for cell merging - never merge these
if (!isCursorCell && !isJoined && !isDecorated) {
cellAmount++;
} else {
charElement.textContent = text;
}
// apply letter-spacing rule
if (spacing !== this.defaultSpacing) {
charElement.style.letterSpacing = `${spacing}px`;
}

elements.push(charElement);
x = lastCharX;
}

// postfix text of last merged span
if (charElement && cellAmount) {
charElement.textContent = text;
if (charElement && charCellAmount) {
charElement.textContent = charText;
}
if (backgroundElement && backgroundCellAmount) {
backgroundElement.style.width = `${backgroundCellAmount * cellWidth}px`;
}

return elements;
return [...backgroundElements, ...charElements];
}

private _applyMinimumContrast(element: HTMLElement, bg: IColor, fg: IColor, cell: ICellData, bgOverride: IColor | undefined, fgOverride: IColor | undefined): boolean {
@@ -479,7 +528,7 @@ export class DomRendererRowFactory {
}

if (adjustedColor) {
this._addStyle(element, `color:${adjustedColor.css}`);
element.style.color = adjustedColor.css;
return true;
}

@@ -493,8 +542,13 @@ export class DomRendererRowFactory {
return this._themeService.colors.contrastCache;
}

private _addStyle(element: HTMLElement, style: string): void {
element.setAttribute('style', `${element.getAttribute('style') || ''}${style};`);
private _isRowInSelection(y: number): boolean {
const start = this._selectionStart;
const end = this._selectionEnd;
if (!start || !end) {
return false;
}
return y >= start[1] && y <= end[1];
}

private _isCellInSelection(x: number, y: number): boolean {
@@ -516,6 +570,24 @@ export class DomRendererRowFactory {
(start[1] < end[1] && y === end[1] && x < end[0]) ||
(start[1] < end[1] && y === start[1] && x >= start[0]);
}

private _isSameDecorations(decos1: IInternalDecoration[], decos2: IInternalDecoration[]): boolean {
const decos1Length = decos1.length;
const decos2Length = decos2.length;
if (!decos1Length && !decos2Length) {
return true;
}
if (decos1Length !== decos2Length) {
return false;
}
for (let i = 0; i < decos1Length; i++) {
if (decos1[i] !== decos2[i]) {
return false;
}
}
return true;
}

}

function padStart(text: string, padChar: string, length: number): string {