mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge pull request #3921 from Tyriar/underline
Underline style and color rendering
This commit is contained in:
@@ -191,14 +191,77 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
* @param x The column to fill.
|
||||
* @param y The row to fill.
|
||||
*/
|
||||
protected _fillBottomLineAtCells(x: number, y: number, width: number = 1): void {
|
||||
protected _fillBottomLineAtCells(x: number, y: number, width: number = 1, pixelOffset: number = 0): void {
|
||||
this._ctx.fillRect(
|
||||
x * this._scaledCellWidth,
|
||||
(y + 1) * this._scaledCellHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */,
|
||||
(y + 1) * this._scaledCellHeight + pixelOffset - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */,
|
||||
width * this._scaledCellWidth,
|
||||
window.devicePixelRatio);
|
||||
}
|
||||
|
||||
protected _curlyUnderlineAtCell(x: number, y: number, width: number = 1): void {
|
||||
this._ctx.save();
|
||||
this._ctx.beginPath();
|
||||
this._ctx.strokeStyle = this._ctx.fillStyle;
|
||||
this._ctx.lineWidth = window.devicePixelRatio;
|
||||
for (let xOffset = 0; xOffset < width; xOffset++) {
|
||||
const xLeft = (x + xOffset) * this._scaledCellWidth;
|
||||
const xMid = (x + xOffset + 0.5) * this._scaledCellWidth;
|
||||
const xRight = (x + xOffset + 1) * this._scaledCellWidth;
|
||||
const yMid = (y + 1) * this._scaledCellHeight - window.devicePixelRatio - 1;
|
||||
const yMidBot = yMid - window.devicePixelRatio;
|
||||
const yMidTop = yMid + window.devicePixelRatio;
|
||||
this._ctx.moveTo(xLeft, yMid);
|
||||
this._ctx.bezierCurveTo(
|
||||
xLeft, yMidBot,
|
||||
xMid, yMidBot,
|
||||
xMid, yMid
|
||||
);
|
||||
this._ctx.bezierCurveTo(
|
||||
xMid, yMidTop,
|
||||
xRight, yMidTop,
|
||||
xRight, yMid
|
||||
);
|
||||
}
|
||||
this._ctx.stroke();
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
protected _dottedUnderlineAtCell(x: number, y: number, width: number = 1): void {
|
||||
this._ctx.save();
|
||||
this._ctx.beginPath();
|
||||
this._ctx.strokeStyle = this._ctx.fillStyle;
|
||||
this._ctx.lineWidth = window.devicePixelRatio;
|
||||
this._ctx.setLineDash([window.devicePixelRatio * 2, window.devicePixelRatio]);
|
||||
const xLeft = x * this._scaledCellWidth;
|
||||
const yMid = (y + 1) * this._scaledCellHeight - window.devicePixelRatio - 1;
|
||||
this._ctx.moveTo(xLeft, yMid);
|
||||
for (let xOffset = 0; xOffset < width; xOffset++) {
|
||||
// const xLeft = x * this._scaledCellWidth;
|
||||
const xRight = (x + width + xOffset) * this._scaledCellWidth;
|
||||
this._ctx.lineTo(xRight, yMid);
|
||||
}
|
||||
this._ctx.stroke();
|
||||
this._ctx.closePath();
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
protected _dashedUnderlineAtCell(x: number, y: number, width: number = 1): void {
|
||||
this._ctx.save();
|
||||
this._ctx.beginPath();
|
||||
this._ctx.strokeStyle = this._ctx.fillStyle;
|
||||
this._ctx.lineWidth = window.devicePixelRatio;
|
||||
this._ctx.setLineDash([window.devicePixelRatio * 4, window.devicePixelRatio * 3]);
|
||||
const xLeft = x * this._scaledCellWidth;
|
||||
const xRight = (x + width) * this._scaledCellWidth;
|
||||
const yMid = (y + 1) * this._scaledCellHeight - window.devicePixelRatio - 1;
|
||||
this._ctx.moveTo(xLeft, yMid);
|
||||
this._ctx.lineTo(xRight, yMid);
|
||||
this._ctx.stroke();
|
||||
this._ctx.closePath();
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills a 1px line (2px on HDPI) at the left of the cell. This uses the
|
||||
* existing fillStyle on the context.
|
||||
|
||||
@@ -8,7 +8,7 @@ import { CharData, ICellData } from 'common/Types';
|
||||
import { GridCache } from './GridCache';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { NULL_CELL_CODE, Content } from 'common/buffer/Constants';
|
||||
import { NULL_CELL_CODE, Content, UnderlineStyle } from 'common/buffer/Constants';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services';
|
||||
@@ -269,7 +269,36 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
this._fillMiddleLineAtCells(x, y, cell.getWidth());
|
||||
}
|
||||
if (cell.isUnderline()) {
|
||||
this._fillBottomLineAtCells(x, y, cell.getWidth());
|
||||
if (!cell.isUnderlineColorDefault()) {
|
||||
if (cell.isUnderlineColorRGB()) {
|
||||
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getUnderlineColor()).join(',')})`;
|
||||
} else {
|
||||
let fg = cell.getUnderlineColor();
|
||||
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
|
||||
fg += 8;
|
||||
}
|
||||
this._ctx.fillStyle = this._colors.ansi[fg].css;
|
||||
}
|
||||
}
|
||||
switch (cell.extended.underlineStyle) {
|
||||
case UnderlineStyle.DOUBLE:
|
||||
this._fillBottomLineAtCells(x, y, cell.getWidth(), -window.devicePixelRatio);
|
||||
this._fillBottomLineAtCells(x, y, cell.getWidth(), window.devicePixelRatio);
|
||||
break;
|
||||
case UnderlineStyle.CURLY:
|
||||
this._curlyUnderlineAtCell(x, y, cell.getWidth());
|
||||
break;
|
||||
case UnderlineStyle.DOTTED:
|
||||
this._dottedUnderlineAtCell(x, y, cell.getWidth());
|
||||
break;
|
||||
case UnderlineStyle.DASHED:
|
||||
this._dashedUnderlineAtCell(x, y, cell.getWidth());
|
||||
break;
|
||||
case UnderlineStyle.SINGLE:
|
||||
default:
|
||||
this._fillBottomLineAtCells(x, y, cell.getWidth());
|
||||
break;
|
||||
}
|
||||
}
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
@@ -169,11 +169,11 @@ export class GlyphRenderer extends Disposable {
|
||||
return this._atlas ? this._atlas.beginFrame() : true;
|
||||
}
|
||||
|
||||
public updateCell(x: number, y: number, code: number, bg: number, fg: number, chars: string, lastBg: number): void {
|
||||
this._updateCell(this._vertices.attributes, x, y, code, bg, fg, chars, lastBg);
|
||||
public updateCell(x: number, y: number, code: number, bg: number, fg: number, ext: number, chars: string, lastBg: number): void {
|
||||
this._updateCell(this._vertices.attributes, x, y, code, bg, fg, ext, chars, lastBg);
|
||||
}
|
||||
|
||||
private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, bg: number, fg: number, chars: string, lastBg: number): void {
|
||||
private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, bg: number, fg: number, ext: number, chars: string, lastBg: number): void {
|
||||
const terminal = this._terminal;
|
||||
|
||||
const i = (y * terminal.cols + x) * INDICES_PER_CELL;
|
||||
@@ -185,16 +185,16 @@ export class GlyphRenderer extends Disposable {
|
||||
return;
|
||||
}
|
||||
|
||||
let rasterizedGlyph: IRasterizedGlyph;
|
||||
if (!this._atlas) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the glyph
|
||||
let rasterizedGlyph: IRasterizedGlyph;
|
||||
if (chars && chars.length > 1) {
|
||||
rasterizedGlyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg);
|
||||
rasterizedGlyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext);
|
||||
} else {
|
||||
rasterizedGlyph = this._atlas.getRasterizedGlyph(code, bg, fg);
|
||||
rasterizedGlyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext);
|
||||
}
|
||||
|
||||
// Fill empty if no glyph was found
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
import { IRenderModel, ISelectionRenderModel } from './Types';
|
||||
import { fill } from 'common/TypedArrayUtils';
|
||||
|
||||
export const RENDER_MODEL_INDICIES_PER_CELL = 3;
|
||||
export const RENDER_MODEL_INDICIES_PER_CELL = 4;
|
||||
export const RENDER_MODEL_BG_OFFSET = 1;
|
||||
export const RENDER_MODEL_FG_OFFSET = 2;
|
||||
export const RENDER_MODEL_EXT_OFFSET = 3;
|
||||
|
||||
export const COMBINED_CHAR_BIT_MASK = 0x80000000;
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
export interface IRasterizedGlyphSet {
|
||||
[bg: number]: { [fg: number]: IRasterizedGlyph } | undefined;
|
||||
[bg: number]: { [fg: number]: { [ext: number]: IRasterizedGlyph } } | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,7 +10,7 @@ import { acquireCharAtlas, removeTerminalFromCache } from './atlas/CharAtlasCach
|
||||
import { WebglCharAtlas } from './atlas/WebglCharAtlas';
|
||||
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 { RenderModel, COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_EXT_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { Attributes, BgFlags, Content, FgFlags, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
|
||||
import { Terminal, IEvent } from 'xterm';
|
||||
@@ -34,7 +34,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 _workColors: { fg: number, bg: number, ext: number } = { fg: 0, bg: 0, ext: 0 };
|
||||
|
||||
private _canvas: HTMLCanvasElement;
|
||||
private _gl: IWebGL2RenderingContext;
|
||||
@@ -353,7 +353,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
// Nothing has changed, no updates needed
|
||||
if (this._model.cells[i] === code &&
|
||||
this._model.cells[i + RENDER_MODEL_BG_OFFSET] === this._workColors.bg &&
|
||||
this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._workColors.fg) {
|
||||
this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._workColors.fg &&
|
||||
this._model.cells[i + RENDER_MODEL_EXT_OFFSET] === this._workColors.ext) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -366,8 +367,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
this._model.cells[i] = code;
|
||||
this._model.cells[i + RENDER_MODEL_BG_OFFSET] = this._workColors.bg;
|
||||
this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._workColors.fg;
|
||||
this._model.cells[i + RENDER_MODEL_EXT_OFFSET] = this._workColors.ext;
|
||||
|
||||
this._glyphRenderer.updateCell(x, y, code, this._workColors.bg, this._workColors.fg, chars, lastBg);
|
||||
this._glyphRenderer.updateCell(x, y, code, this._workColors.bg, this._workColors.fg, this._workColors.ext, chars, lastBg);
|
||||
|
||||
if (isJoined) {
|
||||
// Restore work cell
|
||||
@@ -376,10 +378,11 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
// Null out non-first cells
|
||||
for (x++; x < lastCharX; x++) {
|
||||
const j = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
|
||||
this._glyphRenderer.updateCell(x, y, NULL_CELL_CODE, 0, 0, NULL_CELL_CHAR, 0);
|
||||
this._glyphRenderer.updateCell(x, y, NULL_CELL_CODE, 0, 0, 0, NULL_CELL_CHAR, 0);
|
||||
this._model.cells[j] = NULL_CELL_CODE;
|
||||
this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._workColors.bg;
|
||||
this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._workColors.fg;
|
||||
this._model.cells[j + RENDER_MODEL_EXT_OFFSET] = this._workColors.ext;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -394,7 +397,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
private _loadColorsForCell(x: number, y: number): void {
|
||||
this._workColors.bg = this._workCell.bg;
|
||||
this._workColors.fg = this._workCell.fg;
|
||||
|
||||
this._workColors.ext = this._workCell.bg & BgFlags.HAS_EXTENDED ? this._workCell.extended.ext : 0;
|
||||
// Get any foreground/background overrides, this happens on the model to avoid spreading
|
||||
// override logic throughout the different sub-renderers
|
||||
let bgOverride: number | undefined;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { ICharAtlasConfig } from './Types';
|
||||
import { DIM_OPACITY, TEXT_BASELINE } from 'browser/renderer/Constants';
|
||||
import { IRasterizedGlyph, IBoundingBox, IRasterizedGlyphSet } from '../Types';
|
||||
import { DEFAULT_COLOR, Attributes } from 'common/buffer/Constants';
|
||||
import { DEFAULT_COLOR, Attributes, DEFAULT_EXT, UnderlineStyle } from 'common/buffer/Constants';
|
||||
import { throwIfFalsy } from '../WebglUtils';
|
||||
import { IColor } from 'common/Types';
|
||||
import { IDisposable } from 'xterm';
|
||||
@@ -106,10 +106,12 @@ export class WebglCharAtlas implements IDisposable {
|
||||
private _doWarmUp(): void {
|
||||
// Pre-fill with ASCII 33-126
|
||||
for (let i = 33; i < 126; i++) {
|
||||
const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR);
|
||||
const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT);
|
||||
this._cacheMap[i] = {
|
||||
[DEFAULT_COLOR]: {
|
||||
[DEFAULT_COLOR]: rasterizedGlyph
|
||||
[DEFAULT_COLOR]: {
|
||||
[DEFAULT_EXT]: rasterizedGlyph
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -137,48 +139,50 @@ export class WebglCharAtlas implements IDisposable {
|
||||
this._didWarmUp = false;
|
||||
}
|
||||
|
||||
public getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number): IRasterizedGlyph {
|
||||
let rasterizedGlyphSet = this._cacheMapCombined[chars];
|
||||
if (!rasterizedGlyphSet) {
|
||||
rasterizedGlyphSet = {};
|
||||
this._cacheMapCombined[chars] = rasterizedGlyphSet;
|
||||
}
|
||||
let rasterizedGlyph: IRasterizedGlyph | undefined;
|
||||
const rasterizedGlyphSetBg = rasterizedGlyphSet[bg];
|
||||
if (rasterizedGlyphSetBg) {
|
||||
rasterizedGlyph = rasterizedGlyphSetBg[fg];
|
||||
}
|
||||
if (!rasterizedGlyph) {
|
||||
rasterizedGlyph = this._drawToCache(chars, bg, fg);
|
||||
if (!rasterizedGlyphSet[bg]) {
|
||||
rasterizedGlyphSet[bg] = {};
|
||||
}
|
||||
rasterizedGlyphSet[bg]![fg] = rasterizedGlyph;
|
||||
}
|
||||
return rasterizedGlyph;
|
||||
public getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number): IRasterizedGlyph {
|
||||
return this._getFromCacheMap(this._cacheMapCombined, chars, bg, fg, ext);
|
||||
}
|
||||
|
||||
public getRasterizedGlyph(code: number, bg: number, fg: number, ext: number): IRasterizedGlyph {
|
||||
return this._getFromCacheMap(this._cacheMap, code, bg, fg, ext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the glyphs texture coords, drawing the texture if it's not already
|
||||
*/
|
||||
public getRasterizedGlyph(code: number, bg: number, fg: number): IRasterizedGlyph {
|
||||
let rasterizedGlyphSet = this._cacheMap[code];
|
||||
private _getFromCacheMap(
|
||||
cacheMap: { [key: string | number]: IRasterizedGlyphSet },
|
||||
key: string | number,
|
||||
bg: number,
|
||||
fg: number,
|
||||
ext: number
|
||||
): IRasterizedGlyph {
|
||||
let rasterizedGlyphSet = cacheMap[key];
|
||||
if (!rasterizedGlyphSet) {
|
||||
rasterizedGlyphSet = {};
|
||||
this._cacheMap[code] = rasterizedGlyphSet;
|
||||
cacheMap[key] = rasterizedGlyphSet;
|
||||
}
|
||||
|
||||
let rasterizedGlyphSetBg = rasterizedGlyphSet[bg];
|
||||
if (!rasterizedGlyphSetBg) {
|
||||
rasterizedGlyphSetBg = {};
|
||||
rasterizedGlyphSet[bg] = rasterizedGlyphSetBg;
|
||||
}
|
||||
|
||||
let rasterizedGlyph: IRasterizedGlyph | undefined;
|
||||
const rasterizedGlyphSetBg = rasterizedGlyphSet[bg];
|
||||
if (rasterizedGlyphSetBg) {
|
||||
rasterizedGlyph = rasterizedGlyphSetBg[fg];
|
||||
let rasterizedGlyphSetFg = rasterizedGlyphSetBg[fg];
|
||||
if (!rasterizedGlyphSetFg) {
|
||||
rasterizedGlyphSetFg = {};
|
||||
rasterizedGlyphSetBg[fg] = rasterizedGlyphSetFg;
|
||||
} else {
|
||||
rasterizedGlyph = rasterizedGlyphSetFg[ext];
|
||||
}
|
||||
|
||||
if (!rasterizedGlyph) {
|
||||
rasterizedGlyph = this._drawToCache(code, bg, fg);
|
||||
if (!rasterizedGlyphSet[bg]) {
|
||||
rasterizedGlyphSet[bg] = {};
|
||||
}
|
||||
rasterizedGlyphSet[bg]![fg] = rasterizedGlyph;
|
||||
rasterizedGlyph = this._drawToCache(key, bg, fg, ext);
|
||||
rasterizedGlyphSetFg[ext] = rasterizedGlyph;
|
||||
}
|
||||
|
||||
return rasterizedGlyph;
|
||||
}
|
||||
|
||||
@@ -334,9 +338,7 @@ export class WebglCharAtlas implements IDisposable {
|
||||
return color;
|
||||
}
|
||||
|
||||
private _drawToCache(code: number, bg: number, fg: number): IRasterizedGlyph;
|
||||
private _drawToCache(chars: string, bg: number, fg: number): IRasterizedGlyph;
|
||||
private _drawToCache(codeOrChars: number | string, bg: number, fg: number): IRasterizedGlyph {
|
||||
private _drawToCache(codeOrChars: number | string, bg: number, fg: number, ext: number): IRasterizedGlyph {
|
||||
const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars;
|
||||
|
||||
this.hasCanvasChanged = true;
|
||||
@@ -349,7 +351,7 @@ export class WebglCharAtlas implements IDisposable {
|
||||
this._tmpCanvas.width = allowedWidth;
|
||||
}
|
||||
// Include line height when drawing glyphs
|
||||
const allowedHeight = this._config.scaledCellHeight + TMP_CANVAS_GLYPH_PADDING * 2;
|
||||
const allowedHeight = this._config.scaledCellHeight + TMP_CANVAS_GLYPH_PADDING * 4;
|
||||
if (this._tmpCanvas.height < allowedHeight) {
|
||||
this._tmpCanvas.height = allowedHeight;
|
||||
}
|
||||
@@ -357,6 +359,7 @@ export class WebglCharAtlas implements IDisposable {
|
||||
|
||||
this._workAttributeData.fg = fg;
|
||||
this._workAttributeData.bg = bg;
|
||||
this._workAttributeData.extended.ext = ext;
|
||||
|
||||
const invisible = !!this._workAttributeData.isInvisible();
|
||||
if (invisible) {
|
||||
@@ -403,7 +406,7 @@ export class WebglCharAtlas implements IDisposable {
|
||||
this._tmpCtx.fillStyle = foregroundColor.css;
|
||||
|
||||
// For powerline glyphs left/top padding is excluded (https://github.com/microsoft/vscode/issues/120129)
|
||||
const padding = powerLineGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING;
|
||||
const padding = powerLineGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING * 2;
|
||||
|
||||
// Draw custom characters if applicable
|
||||
let drawSuccess = false;
|
||||
@@ -411,6 +414,115 @@ export class WebglCharAtlas implements IDisposable {
|
||||
drawSuccess = tryDrawCustomChar(this._tmpCtx, chars, padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight);
|
||||
}
|
||||
|
||||
// Whether to clear pixels based on a threshold difference between the glyph color and the
|
||||
// background color. This should be disabled when the glyph contains multiple colors such as
|
||||
// underline colors to prevent important colors could get cleared.
|
||||
let enableClearThresholdCheck = true;
|
||||
|
||||
// Draw underline
|
||||
if (underline) {
|
||||
this._tmpCtx.save();
|
||||
const lineWidth = Math.max(1, Math.floor(this._config.fontSize * window.devicePixelRatio / 10));
|
||||
const yOffset = this._tmpCtx.lineWidth % 2 === 1 ? 0.5 : 0; // When the width is odd, draw at 0.5 position
|
||||
this._tmpCtx.lineWidth = lineWidth;
|
||||
|
||||
// Underline color
|
||||
if (this._workAttributeData.isUnderlineColorDefault()) {
|
||||
this._tmpCtx.strokeStyle = this._tmpCtx.fillStyle;
|
||||
} else if (this._workAttributeData.isUnderlineColorRGB()) {
|
||||
enableClearThresholdCheck = false;
|
||||
this._tmpCtx.strokeStyle = `rgb(${AttributeData.toColorRGB(this._workAttributeData.getUnderlineColor()).join(',')})`;
|
||||
} else {
|
||||
enableClearThresholdCheck = false;
|
||||
let fg = this._workAttributeData.getUnderlineColor();
|
||||
if (this._config.drawBoldTextInBrightColors && this._workAttributeData.isBold() && fg < 8) {
|
||||
fg += 8;
|
||||
}
|
||||
this._tmpCtx.strokeStyle = this._getColorFromAnsiIndex(fg).css;
|
||||
}
|
||||
|
||||
// Underline style/stroke
|
||||
this._tmpCtx.beginPath();
|
||||
const xLeft = padding;
|
||||
const xRight = padding + this._config.scaledCellWidth;
|
||||
const yTop = Math.ceil(padding + this._config.scaledCharHeight - lineWidth) - yOffset;
|
||||
const yMid = padding + this._config.scaledCharHeight - yOffset;
|
||||
const yBot = Math.ceil(padding + this._config.scaledCharHeight + lineWidth) - yOffset;
|
||||
switch (this._workAttributeData.extended.underlineStyle) {
|
||||
case UnderlineStyle.DOUBLE:
|
||||
this._tmpCtx.moveTo(xLeft, yTop);
|
||||
this._tmpCtx.lineTo(xRight, yTop);
|
||||
this._tmpCtx.moveTo(xLeft, yBot);
|
||||
this._tmpCtx.lineTo(xRight, yBot);
|
||||
break;
|
||||
case UnderlineStyle.CURLY:
|
||||
const xMid = padding + this._config.scaledCellWidth / 2;
|
||||
// Choose the bezier top and bottom based on the device pixel ratio, the curly line is
|
||||
// made taller when the line width is as otherwise it's not very clear otherwise.
|
||||
const yCurlyBot = lineWidth <= 1 ? yBot : Math.ceil(padding + this._config.scaledCharHeight - lineWidth / 2) - yOffset;
|
||||
const yCurlyTop = lineWidth <= 1 ? yTop : Math.ceil(padding + this._config.scaledCharHeight + lineWidth / 2) - yOffset;
|
||||
// Clip the left and right edges of the underline such that it can be drawn just outside
|
||||
// the edge of the cell to ensure a continuous stroke when there are multiple underlined
|
||||
// glyphs adjacent to one another.
|
||||
const clipRegion = new Path2D();
|
||||
clipRegion.rect(xLeft, yTop, this._config.scaledCellWidth, yBot - yTop);
|
||||
this._tmpCtx.clip(clipRegion);
|
||||
// Start 1/2 cell before and end 1/2 cells after to ensure a smooth curve with other cells
|
||||
this._tmpCtx.moveTo(xLeft - this._config.scaledCellWidth / 2, yMid);
|
||||
this._tmpCtx.bezierCurveTo(
|
||||
xLeft - this._config.scaledCellWidth / 2, yCurlyTop,
|
||||
xLeft, yCurlyTop,
|
||||
xLeft, yMid
|
||||
);
|
||||
this._tmpCtx.bezierCurveTo(
|
||||
xLeft, yCurlyBot,
|
||||
xMid, yCurlyBot,
|
||||
xMid, yMid
|
||||
);
|
||||
this._tmpCtx.bezierCurveTo(
|
||||
xMid, yCurlyTop,
|
||||
xRight, yCurlyTop,
|
||||
xRight, yMid
|
||||
);
|
||||
this._tmpCtx.bezierCurveTo(
|
||||
xRight, yCurlyBot,
|
||||
xRight + this._config.scaledCellWidth / 2, yCurlyBot,
|
||||
xRight + this._config.scaledCellWidth / 2, yMid
|
||||
);
|
||||
break;
|
||||
case UnderlineStyle.DOTTED:
|
||||
this._tmpCtx.setLineDash([window.devicePixelRatio * 2, window.devicePixelRatio]);
|
||||
this._tmpCtx.moveTo(xLeft, yMid);
|
||||
this._tmpCtx.lineTo(xRight, yMid);
|
||||
break;
|
||||
case UnderlineStyle.DASHED:
|
||||
this._tmpCtx.setLineDash([window.devicePixelRatio * 4, window.devicePixelRatio * 3]);
|
||||
this._tmpCtx.moveTo(xLeft, yMid);
|
||||
this._tmpCtx.lineTo(xRight, yMid);
|
||||
break;
|
||||
case UnderlineStyle.SINGLE:
|
||||
default:
|
||||
this._tmpCtx.moveTo(xLeft, yMid);
|
||||
this._tmpCtx.lineTo(xRight, yMid);
|
||||
break;
|
||||
}
|
||||
this._tmpCtx.stroke();
|
||||
this._tmpCtx.restore();
|
||||
|
||||
// Draw stroke in the background color for non custom characters in order to give an outline
|
||||
// between the text and the underline
|
||||
if (!drawSuccess) {
|
||||
// This only works when transparency is disabled because it's not clear how to clear stroked
|
||||
// text
|
||||
if (!this._config.allowTransparency && chars !== ' ') {
|
||||
// This translates to 1/2 the line width in either direction
|
||||
this._tmpCtx.lineWidth = window.devicePixelRatio * 3;
|
||||
this._tmpCtx.strokeStyle = backgroundColor.css;
|
||||
this._tmpCtx.strokeText(chars, padding, padding + this._config.scaledCharHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the character
|
||||
if (!drawSuccess) {
|
||||
this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight);
|
||||
@@ -419,12 +531,12 @@ export class WebglCharAtlas implements IDisposable {
|
||||
// If this charcater is underscore and beyond the cell bounds, shift it up until it is visible
|
||||
// even on the bottom row, try for a maximum of 5 pixels.
|
||||
if (chars === '_' && !this._config.allowTransparency) {
|
||||
let isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor, foregroundColor);
|
||||
let isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor, foregroundColor, enableClearThresholdCheck);
|
||||
if (isBeyondCellBounds) {
|
||||
for (let offset = 1; offset <= 5; offset++) {
|
||||
this._tmpCtx.clearRect(0, 0, this._tmpCanvas.width, this._tmpCanvas.height);
|
||||
this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight - offset);
|
||||
isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor, foregroundColor);
|
||||
isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor, foregroundColor, enableClearThresholdCheck);
|
||||
if (!isBeyondCellBounds) {
|
||||
break;
|
||||
}
|
||||
@@ -432,23 +544,16 @@ export class WebglCharAtlas implements IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
// Draw underline and strikethrough
|
||||
if (underline || strikethrough) {
|
||||
const lineWidth = Math.max(1, Math.floor(this._config.fontSize / 10));
|
||||
// Draw strokethrough
|
||||
if (strikethrough) {
|
||||
const lineWidth = Math.max(1, Math.floor(this._config.fontSize * window.devicePixelRatio / 10));
|
||||
const yOffset = this._tmpCtx.lineWidth % 2 === 1 ? 0.5 : 0; // When the width is odd, draw at 0.5 position
|
||||
this._tmpCtx.lineWidth = lineWidth;
|
||||
this._tmpCtx.strokeStyle = this._tmpCtx.fillStyle;
|
||||
this._tmpCtx.beginPath();
|
||||
if (underline) {
|
||||
this._tmpCtx.moveTo(padding, padding + this._config.scaledCharHeight - yOffset);
|
||||
this._tmpCtx.lineTo(padding + this._config.scaledCharWidth, padding + this._config.scaledCharHeight - yOffset);
|
||||
}
|
||||
if (strikethrough) {
|
||||
this._tmpCtx.moveTo(padding, padding + Math.floor(this._config.scaledCharHeight / 2) - yOffset);
|
||||
this._tmpCtx.lineTo(padding + this._config.scaledCharWidth, padding + Math.floor(this._config.scaledCharHeight / 2) - yOffset);
|
||||
}
|
||||
this._tmpCtx.moveTo(padding, padding + Math.floor(this._config.scaledCharHeight / 2) - yOffset);
|
||||
this._tmpCtx.lineTo(padding + this._config.scaledCharWidth, padding + Math.floor(this._config.scaledCharHeight / 2) - yOffset);
|
||||
this._tmpCtx.stroke();
|
||||
this._tmpCtx.closePath();
|
||||
}
|
||||
|
||||
this._tmpCtx.restore();
|
||||
@@ -462,7 +567,7 @@ export class WebglCharAtlas implements IDisposable {
|
||||
// Clear out the background color and determine if the glyph is empty.
|
||||
let isEmpty: boolean;
|
||||
if (!this._config.allowTransparency) {
|
||||
isEmpty = clearColor(imageData, backgroundColor, foregroundColor);
|
||||
isEmpty = clearColor(imageData, backgroundColor, foregroundColor, enableClearThresholdCheck);
|
||||
} else {
|
||||
isEmpty = checkCompletelyTransparent(imageData);
|
||||
}
|
||||
@@ -609,7 +714,7 @@ export class WebglCharAtlas implements IDisposable {
|
||||
* transparent.
|
||||
* @returns True if the result is "empty", meaning all pixels are fully transparent.
|
||||
*/
|
||||
function clearColor(imageData: ImageData, bg: IColor, fg: IColor): boolean {
|
||||
function clearColor(imageData: ImageData, bg: IColor, fg: IColor, enableThresholdCheck: boolean): boolean {
|
||||
// Get color channels
|
||||
const r = bg.rgba >>> 24;
|
||||
const g = bg.rgba >>> 16 & 0xFF;
|
||||
@@ -636,7 +741,8 @@ function clearColor(imageData: ImageData, bg: IColor, fg: IColor): boolean {
|
||||
imageData.data[offset + 3] = 0;
|
||||
} else {
|
||||
// Check the threshold based difference
|
||||
if ((Math.abs(imageData.data[offset] - r) +
|
||||
if (enableThresholdCheck &&
|
||||
(Math.abs(imageData.data[offset] - r) +
|
||||
Math.abs(imageData.data[offset + 1] - g) +
|
||||
Math.abs(imageData.data[offset + 2] - b)) < threshold) {
|
||||
imageData.data[offset + 3] = 0;
|
||||
|
||||
+5
-3
@@ -163,9 +163,11 @@
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.xterm-underline {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.xterm-underline-1 { text-decoration: underline; }
|
||||
.xterm-underline-2 { text-decoration: double underline; }
|
||||
.xterm-underline-3 { text-decoration: wavy underline; }
|
||||
.xterm-underline-4 { text-decoration: dotted underline; }
|
||||
.xterm-underline-5 { text-decoration: dashed underline; }
|
||||
|
||||
.xterm-strikethrough {
|
||||
text-decoration: line-through;
|
||||
|
||||
+52
-1
@@ -187,6 +187,7 @@ if (document.location.pathname === '/test') {
|
||||
document.getElementById('custom-glyph').addEventListener('click', writeCustomGlyphHandler);
|
||||
document.getElementById('load-test').addEventListener('click', loadTest);
|
||||
document.getElementById('powerline-symbol-test').addEventListener('click', powerlineSymbolTest);
|
||||
document.getElementById('underline-test').addEventListener('click', underlineTest);
|
||||
document.getElementById('add-decoration').addEventListener('click', addDecoration);
|
||||
document.getElementById('add-overview-ruler').addEventListener('click', addOverviewRuler);
|
||||
}
|
||||
@@ -200,7 +201,6 @@ function createTerminal(): void {
|
||||
const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0;
|
||||
term = new Terminal({
|
||||
allowProposedApi: true,
|
||||
allowTransparency: true,
|
||||
windowsMode: isWindows,
|
||||
fontFamily: 'Fira Code, courier-new, courier, monospace',
|
||||
theme: xtermjsTheme
|
||||
@@ -212,6 +212,7 @@ function createTerminal(): void {
|
||||
addons.serialize.instance = new SerializeAddon();
|
||||
addons.fit.instance = new FitAddon();
|
||||
addons.unicode11.instance = new Unicode11Addon();
|
||||
addons.webgl.instance = new WebglAddon();
|
||||
// TODO: Remove arguments when link provider API is the default
|
||||
addons['web-links'].instance = new WebLinksAddon(undefined, undefined, true);
|
||||
typedTerm.loadAddon(addons.fit.instance);
|
||||
@@ -236,6 +237,10 @@ function createTerminal(): void {
|
||||
|
||||
term.open(terminalContainer);
|
||||
addons.fit.instance!.fit();
|
||||
typedTerm.loadAddon(addons.webgl.instance);
|
||||
setTimeout(() => {
|
||||
document.body.appendChild(addons.webgl.instance.textureAtlas);
|
||||
}, 0);
|
||||
term.focus();
|
||||
|
||||
addDomListener(paddingElement, 'change', setPadding);
|
||||
@@ -738,6 +743,52 @@ function powerlineSymbolTest() {
|
||||
term.writeln('nf-mdi-github_face (\\uFbd9) \ufbd9');
|
||||
}
|
||||
|
||||
function underlineTest() {
|
||||
function u(style: number): string {
|
||||
return `\x1b[4:${style}m`;
|
||||
}
|
||||
function c(color: string): string {
|
||||
return `\x1b[58:${color}m`;
|
||||
}
|
||||
term.write('\n\n\r');
|
||||
term.writeln('Underline styles:');
|
||||
term.writeln('');
|
||||
term.writeln(`${u(0)}4:0m - No underline`);
|
||||
term.writeln(`${u(1)}4:1m - Straight`);
|
||||
term.writeln(`${u(2)}4:2m - Double`);
|
||||
term.writeln(`${u(3)}4:3m - Curly`);
|
||||
term.writeln(`${u(4)}4:4m - Dotted`);
|
||||
term.writeln(`${u(5)}4:5m - Dashed\x1b[0m`);
|
||||
term.writeln('');
|
||||
term.writeln(`Underline colors (256 color mode):`);
|
||||
term.writeln('');
|
||||
for (let i = 0; i < 256; i++) {
|
||||
term.write((i !== 0 ? '\x1b[0m, ' : '') + u(1 + i % 5) + c('5:' + i) + i);
|
||||
}
|
||||
term.writeln(`\x1b[0m\n\n\rUnderline colors (true color mode):`);
|
||||
term.writeln('');
|
||||
for (let i = 0; i < 80; i++) {
|
||||
const v = Math.round(i / 79 * 255);
|
||||
term.write(u(1) + c(`2:0:${v}:${v}:${v}`) + (i < 4 ? 'grey'[i] : ' '));
|
||||
}
|
||||
term.write('\n\r');
|
||||
for (let i = 0; i < 80; i++) {
|
||||
const v = Math.round(i / 79 * 255);
|
||||
term.write(u(1) + c(`2:0:${v}:${0}:${0}`) + (i < 3 ? 'red'[i] : ' '));
|
||||
}
|
||||
term.write('\n\r');
|
||||
for (let i = 0; i < 80; i++) {
|
||||
const v = Math.round(i / 79 * 255);
|
||||
term.write(u(1) + c(`2:0:${0}:${v}:${0}`) + (i < 5 ? 'green'[i] : ' '));
|
||||
}
|
||||
term.write('\n\r');
|
||||
for (let i = 0; i < 80; i++) {
|
||||
const v = Math.round(i / 79 * 255);
|
||||
term.write(u(1) + c(`2:0:${0}:${0}:${v}`) + (i < 4 ? 'blue'[i] : ' '));
|
||||
}
|
||||
term.write('\x1b[0m\n\r');
|
||||
}
|
||||
|
||||
function addDecoration() {
|
||||
term.options['overviewRulerWidth'] = 15;
|
||||
const marker = term.registerMarker(1);
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
<dt>Styles</dt>
|
||||
<dd><button id="custom-glyph" title="Write custom box drawing and block element characters to the terminal">Test custom glyphs</button></dd>
|
||||
<dd><button id="powerline-symbol-test" title="Write powerline symbol characters to the terminal (\ue0a0+)">Powerline symbol test</button></dd>
|
||||
<dd><button id="underline-test" title="Write text with Kitty's extended underline sequences">Underline test</button></dd>
|
||||
|
||||
<dt>Decorations</dt>
|
||||
<dd><button id="add-decoration" title="Add a decoration to the terminal">Decoration</button></dd>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import jsdom = require('jsdom');
|
||||
import { assert } from 'chai';
|
||||
import { DomRendererRowFactory } from 'browser/renderer/dom/DomRendererRowFactory';
|
||||
import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, DEFAULT_ATTR, FgFlags, BgFlags, Attributes } from 'common/buffer/Constants';
|
||||
import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, DEFAULT_ATTR, FgFlags, BgFlags, Attributes, UnderlineStyle } from 'common/buffer/Constants';
|
||||
import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
|
||||
import { IBufferLine } from 'common/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
@@ -130,14 +130,62 @@ describe('DomRendererRowFactory', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should add class for underline', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.UNDERLINE;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-underline">a</span>'
|
||||
);
|
||||
describe('underline', () => {
|
||||
it('should add class for straight underline style', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.UNDERLINE;
|
||||
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
|
||||
cell.extended.underlineStyle = UnderlineStyle.SINGLE;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-underline-1" style="text-decoration-color: rgb(255,255,255);">a</span>'
|
||||
);
|
||||
});
|
||||
it('should add class for double underline style', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.UNDERLINE;
|
||||
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
|
||||
cell.extended.underlineStyle = UnderlineStyle.DOUBLE;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-underline-2" style="text-decoration-color: rgb(255,255,255);">a</span>'
|
||||
);
|
||||
});
|
||||
it('should add class for curly underline style', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.UNDERLINE;
|
||||
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
|
||||
cell.extended.underlineStyle = UnderlineStyle.CURLY;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-underline-3" style="text-decoration-color: rgb(255,255,255);">a</span>'
|
||||
);
|
||||
});
|
||||
it('should add class for double dotted style', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.UNDERLINE;
|
||||
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
|
||||
cell.extended.underlineStyle = UnderlineStyle.DOTTED;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-underline-4" style="text-decoration-color: rgb(255,255,255);">a</span>'
|
||||
);
|
||||
});
|
||||
it('should add class for dashed underline style', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.UNDERLINE;
|
||||
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
|
||||
cell.extended.underlineStyle = UnderlineStyle.DASHED;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-underline-5" style="text-decoration-color: rgb(255,255,255);">a</span>'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should add class for strikethrough', () => {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { IColorSet } from 'browser/Types';
|
||||
import { ICharacterJoinerService, ICoreBrowserService } from 'browser/services/Services';
|
||||
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
|
||||
import { excludeFromContrastRatioDemands } from 'browser/renderer/RendererUtils';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
|
||||
export const BOLD_CLASS = 'xterm-bold';
|
||||
export const DIM_CLASS = 'xterm-dim';
|
||||
@@ -156,16 +157,30 @@ export class DomRendererRowFactory {
|
||||
charElement.classList.add(DIM_CLASS);
|
||||
}
|
||||
|
||||
if (cell.isUnderline()) {
|
||||
charElement.classList.add(UNDERLINE_CLASS);
|
||||
}
|
||||
|
||||
if (cell.isInvisible()) {
|
||||
charElement.textContent = WHITESPACE_CELL_CHAR;
|
||||
} else {
|
||||
charElement.textContent = cell.getChars() || WHITESPACE_CELL_CHAR;
|
||||
}
|
||||
|
||||
if (cell.isUnderline()) {
|
||||
charElement.classList.add(`${UNDERLINE_CLASS}-${cell.extended.underlineStyle}`);
|
||||
if (charElement.textContent === ' ') {
|
||||
charElement.innerHTML = ' ';
|
||||
}
|
||||
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 = this._colors.ansi[fg].css;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cell.isStrikethrough()) {
|
||||
charElement.classList.add(STRIKETHROUGH_CLASS);
|
||||
}
|
||||
|
||||
Vendored
+1
@@ -113,6 +113,7 @@ export interface IColor {
|
||||
export type IColorRGB = [number, number, number];
|
||||
|
||||
export interface IExtendedAttrs {
|
||||
ext: number;
|
||||
underlineStyle: number;
|
||||
underlineColor: number;
|
||||
clone(): IExtendedAttrs;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { IAttributeData, IColorRGB, IExtendedAttrs } from 'common/Types';
|
||||
import { Attributes, FgFlags, BgFlags, UnderlineStyle } from 'common/buffer/Constants';
|
||||
import { Attributes, FgFlags, BgFlags, UnderlineStyle, ExtFlags } from 'common/buffer/Constants';
|
||||
|
||||
export class AttributeData implements IAttributeData {
|
||||
public static toColorRGB(value: number): IColorRGB {
|
||||
@@ -30,7 +30,7 @@ export class AttributeData implements IAttributeData {
|
||||
// data
|
||||
public fg = 0;
|
||||
public bg = 0;
|
||||
public extended = new ExtendedAttrs();
|
||||
public extended: IExtendedAttrs = new ExtendedAttrs();
|
||||
|
||||
// flags
|
||||
public isInverse(): number { return this.fg & FgFlags.INVERSE; }
|
||||
@@ -127,12 +127,33 @@ export class AttributeData implements IAttributeData {
|
||||
* Holds information about different underline styles and color.
|
||||
*/
|
||||
export class ExtendedAttrs implements IExtendedAttrs {
|
||||
private _ext: number = 0;
|
||||
public get ext(): number { return this._ext; }
|
||||
public set ext(value: number) { this._ext = value; }
|
||||
|
||||
public get underlineStyle(): UnderlineStyle {
|
||||
return (this._ext & ExtFlags.UNDERLINE_STYLE) >> 26;
|
||||
}
|
||||
public set underlineStyle(value: UnderlineStyle) {
|
||||
this._ext &= ~ExtFlags.UNDERLINE_STYLE;
|
||||
this._ext |= (value << 26) & ExtFlags.UNDERLINE_STYLE;
|
||||
}
|
||||
|
||||
public get underlineColor(): number {
|
||||
return this._ext & (Attributes.CM_MASK | Attributes.RGB_MASK);
|
||||
}
|
||||
public set underlineColor(value: number) {
|
||||
this._ext &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);
|
||||
this._ext |= value & (Attributes.CM_MASK | Attributes.RGB_MASK);
|
||||
}
|
||||
|
||||
constructor(
|
||||
// underline style, NONE is empty
|
||||
public underlineStyle: UnderlineStyle = UnderlineStyle.NONE,
|
||||
// underline color, -1 is empty (same as FG)
|
||||
public underlineColor: number = -1
|
||||
) {}
|
||||
underlineStyle: UnderlineStyle = UnderlineStyle.NONE,
|
||||
underlineColor: number = -1
|
||||
) {
|
||||
this.underlineStyle = underlineStyle;
|
||||
this.underlineColor = underlineColor;
|
||||
}
|
||||
|
||||
public clone(): IExtendedAttrs {
|
||||
return new ExtendedAttrs(this.underlineStyle, this.underlineColor);
|
||||
|
||||
@@ -45,7 +45,7 @@ describe('AttributeData', () => {
|
||||
assert.equal(attrs.getUnderlineColor(), 45);
|
||||
|
||||
// should use FG color if underlineColor holds no value
|
||||
attrs.extended.underlineColor = -1;
|
||||
attrs.extended.underlineColor = 0;
|
||||
attrs.fg |= Attributes.CM_P256 | 123;
|
||||
assert.equal(attrs.getUnderlineColor(), 123);
|
||||
});
|
||||
@@ -62,7 +62,7 @@ describe('AttributeData', () => {
|
||||
assert.equal(attrs.getUnderlineColor(), (1 << 16) | (2 << 8) | 3);
|
||||
|
||||
// should use FG color if underlineColor holds no value
|
||||
attrs.extended.underlineColor = -1;
|
||||
attrs.extended.underlineColor = 0;
|
||||
attrs.fg |= Attributes.CM_P256 | 123;
|
||||
assert.equal(attrs.getUnderlineColor(), 123);
|
||||
});
|
||||
|
||||
@@ -55,7 +55,7 @@ export const DEFAULT_ATTR_DATA = Object.freeze(new AttributeData());
|
||||
export class BufferLine implements IBufferLine {
|
||||
protected _data: Uint32Array;
|
||||
protected _combined: {[index: number]: string} = {};
|
||||
protected _extendedAttrs: {[index: number]: ExtendedAttrs} = {};
|
||||
protected _extendedAttrs: {[index: number]: IExtendedAttrs} = {};
|
||||
public length: number;
|
||||
|
||||
constructor(cols: number, fillCellData?: ICellData, public isWrapped: boolean = false) {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
export const DEFAULT_COLOR = 256;
|
||||
export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0);
|
||||
export const DEFAULT_EXT = 0;
|
||||
|
||||
export const CHAR_DATA_ATTR_INDEX = 0;
|
||||
export const CHAR_DATA_CHAR_INDEX = 1;
|
||||
@@ -129,6 +130,13 @@ export const enum BgFlags {
|
||||
HAS_EXTENDED = 0x10000000
|
||||
}
|
||||
|
||||
export const enum ExtFlags {
|
||||
/**
|
||||
* bit 27..32 (upper 3 unused)
|
||||
*/
|
||||
UNDERLINE_STYLE = 0x1C000000
|
||||
}
|
||||
|
||||
export const enum UnderlineStyle {
|
||||
NONE = 0,
|
||||
SINGLE = 1,
|
||||
|
||||
Reference in New Issue
Block a user