diff --git a/css/xterm.css b/css/xterm.css index 74acc26708..79a72a9e09 100644 --- a/css/xterm.css +++ b/css/xterm.css @@ -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 */ diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index 2e9f3701e9..6e3bbe39d5 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -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,8 +121,6 @@ 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) { @@ -136,16 +128,6 @@ export class DomRenderer extends Disposable implements IRenderer { 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 { diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index 6ab68e7d6e..05c71b8c3a 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -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'; // =   + // 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'; // =   + // 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'; // =   + } + 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'; // =   + } } - 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 {