diff --git a/addons/xterm-addon-webgl/src/RectangleRenderer.ts b/addons/xterm-addon-webgl/src/RectangleRenderer.ts index 8dcc5c083..356e17472 100644 --- a/addons/xterm-addon-webgl/src/RectangleRenderer.ts +++ b/addons/xterm-addon-webgl/src/RectangleRenderer.ts @@ -248,23 +248,26 @@ export class RectangleRenderer { let currentStartX = -1; let currentBg = 0; let currentFg = 0; + let currentInverse = false; for (let x = 0; x < terminal.cols; x++) { const modelIndex = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL; const bg = model.cells[modelIndex + RENDER_MODEL_BG_OFFSET]; const fg = model.cells[modelIndex + RENDER_MODEL_FG_OFFSET]; - if (bg !== currentBg) { + const inverse = !!(fg & FgFlags.INVERSE); + if (bg !== currentBg || ((inverse || currentInverse) && fg !== currentFg)) { // A rectangle needs to be drawn if going from non-default to another color - if (currentBg !== 0) { + if (currentBg !== 0 || (currentInverse && currentFg !== 0)) { const offset = rectangleCount++ * INDICES_PER_RECTANGLE; this._updateRectangle(vertices, offset, currentFg, currentBg, currentStartX, x, y); } currentStartX = x; currentBg = bg; currentFg = fg; + currentInverse = inverse; } } // Finish rectangle if it's still going - if (currentBg !== 0) { + if (currentBg !== 0 || (currentInverse && currentFg !== 0)) { const offset = rectangleCount++ * INDICES_PER_RECTANGLE; this._updateRectangle(vertices, offset, currentFg, currentBg, currentStartX, terminal.cols, y); } @@ -274,12 +277,21 @@ export class RectangleRenderer { private _updateRectangle(vertices: IVertices, offset: number, fg: number, bg: number, startX: number, endX: number, y: number): void { let rgba: number | undefined; - const colorMode = bg & Attributes.CM_MASK; if (fg & FgFlags.INVERSE) { - // Inverted color - rgba = this._colors.foreground.rgba; + switch (fg & Attributes.CM_MASK) { + case Attributes.CM_P16: + case Attributes.CM_P256: + rgba = this._colors.ansi[fg & Attributes.PCOLOR_MASK].rgba; + break; + case Attributes.CM_RGB: + rgba = (fg & Attributes.RGB_MASK) << 8; + break; + case Attributes.CM_DEFAULT: + default: + rgba = this._colors.foreground.rgba; + } } else { - switch (colorMode) { + switch (bg & Attributes.CM_MASK) { case Attributes.CM_P16: case Attributes.CM_P256: rgba = this._colors.ansi[bg & Attributes.PCOLOR_MASK].rgba; diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts index 304973fef..556061e76 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts @@ -16,7 +16,7 @@ let page: puppeteer.Page; const width = 800; const height = 600; -describe('WebGL Renderer Integration Tests', function(): void { +describe.only('WebGL Renderer Integration Tests', function(): void { it('dispose removes renderer canvases', async () => { await setupBrowser(); assert.equal(await page.evaluate(`document.querySelectorAll('.xterm canvas').length`), 3); @@ -76,6 +76,52 @@ describe('WebGL Renderer Integration Tests', function(): void { await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]); }); + it('foreground 0-15 inverse', async () => { + const theme: ITheme = { + black: '#010203', + red: '#040506', + green: '#070809', + yellow: '#0a0b0c', + blue: '#0d0e0f', + magenta: '#101112', + cyan: '#131415', + white: '#161718' + }; + await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); + await writeSync(`\\x1b[7;30m \\x1b[7;31m \\x1b[7;32m \\x1b[7;33m \\x1b[7;34m \\x1b[7;35m \\x1b[7;36m \\x1b[7;37m `); + await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]); + await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]); + await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]); + await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]); + await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]); + await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]); + await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]); + await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]); + }); + + it('background 0-15 inverse', async () => { + const theme: ITheme = { + black: '#010203', + red: '#040506', + green: '#070809', + yellow: '#0a0b0c', + blue: '#0d0e0f', + magenta: '#101112', + cyan: '#131415', + white: '#161718' + }; + await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); + await writeSync(`\\x1b[7;40m█\\x1b[7;41m█\\x1b[7;42m█\\x1b[7;43m█\\x1b[7;44m█\\x1b[7;45m█\\x1b[7;46m█\\x1b[7;47m█`); + await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]); + await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]); + await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]); + await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]); + await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]); + await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]); + await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]); + await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]); + }); + it('foreground 0-15 bright', async () => { const theme: ITheme = { brightBlack: '#010203', @@ -162,6 +208,46 @@ describe('WebGL Renderer Integration Tests', function(): void { } }); + it('foreground 16-255 inverse', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\\x1b[7;38;5;${16 + y * 16 + x}m \x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + const cssColor = COLORS_16_TO_255[y * 16 + x]; + const r = parseInt(cssColor.substr(1, 2), 16); + const g = parseInt(cssColor.substr(3, 2), 16); + const b = parseInt(cssColor.substr(5, 2), 16); + await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); + } + } + }); + + it('background 16-255 inverse', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\\x1b[7;48;5;${16 + y * 16 + x}m█\x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + const cssColor = COLORS_16_TO_255[y * 16 + x]; + const r = parseInt(cssColor.substr(1, 2), 16); + const g = parseInt(cssColor.substr(3, 2), 16); + const b = parseInt(cssColor.substr(5, 2), 16); + await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); + } + } + }); + it('foreground true color red', async () => { let data = ''; for (let y = 0; y < 16; y++) { @@ -305,6 +391,151 @@ describe('WebGL Renderer Integration Tests', function(): void { } } }); + + it('foreground true color red inverse', async function(): Promise { + this.timeout(60000); + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[7;38;2;${i};0;0m \x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]); + } + } + }); + + it('background true color red inverse', async function(): Promise { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[7;48;2;${i};0;0m█\\x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]); + } + } + }); + + it('foreground true color green inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[7;38;2;0;${i};0m \x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]); + } + } + }); + + it('background true color green inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[7;48;2;0;${i};0m█\x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]); + } + } + }); + + it('foreground true color blue inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[7;38;2;0;0;${i}m \x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]); + } + } + }); + + it('background true color blue inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[7;48;2;0;0;${i}m█\x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]); + } + } + }); + + it('foreground true color grey inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[7;38;2;${i};${i};${i}m \x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]); + } + } + }); + + it('background true color grey inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[7;48;2;${i};${i};${i}m█\x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]); + } + } + }); }); }); diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 5eae61db6..3a71dcd8e 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -252,35 +252,13 @@ export class WebglRenderer extends Disposable implements IRenderer { this._model.lineLengths[y] = x + 1; } - // Resolve bg and fg - let bg = this._workCell.bg; - let fg = this._workCell.fg; - // Nothing has changed, no updates needed if (this._model.cells[i] === code && - this._model.cells[i + RENDER_MODEL_BG_OFFSET] === bg && - this._model.cells[i + RENDER_MODEL_FG_OFFSET] === fg) { + this._model.cells[i + RENDER_MODEL_BG_OFFSET] === this._workCell.bg && + this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._workCell.fg) { continue; } - // If inverse flag is on, the foreground should become the background. - if (this._workCell.isInverse()) { - const temp = bg; - bg = fg; - fg = temp; - if (fg === DEFAULT_COLOR) { - fg = INVERTED_DEFAULT_COLOR; - } - if (bg === DEFAULT_COLOR) { - bg = INVERTED_DEFAULT_COLOR; - } - } - - // Apply drawBoldTextInBrightColors - if (terminal.options.drawBoldTextInBrightColors && this._workCell.isBold() && fg & FgFlags.BOLD && this._workCell.getFgColor() < 8) { - fg += 8; - } - // Flag combined chars with a bit mask so they're easily identifiable if (chars.length > 1) { code = code | COMBINED_CHAR_BIT_MASK; @@ -288,10 +266,10 @@ export class WebglRenderer extends Disposable implements IRenderer { // Cache the results in the model this._model.cells[i] = code; - this._model.cells[i + RENDER_MODEL_BG_OFFSET] = bg; - this._model.cells[i + RENDER_MODEL_FG_OFFSET] = fg; + this._model.cells[i + RENDER_MODEL_BG_OFFSET] = this._workCell.bg; + this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._workCell.fg; - this._glyphRenderer.updateCell(x, y, code, bg, fg, chars); + this._glyphRenderer.updateCell(x, y, code, this._workCell.bg, this._workCell.fg, chars); } } this._rectangleRenderer.updateBackgrounds(this._model); diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 16b7ba07b..b4d3f822f 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -176,51 +176,47 @@ export class WebglCharAtlas implements IDisposable { return this._config.colors.ansi[idx]; } - private _getBackgroundColor(bg: number, fg: number): IColor { + private _getBackgroundColor(bgColorMode: number, bgColor: number, inverse: boolean): IColor { if (this._config.allowTransparency) { // The background color might have some transparency, so we need to render it as fully // transparent in the atlas. Otherwise we'd end up drawing the transparent background twice // around the anti-aliased edges of the glyph, and it would look too dark. return TRANSPARENT_COLOR; - } else if (fg & FgFlags.INVERSE) { - return this._config.colors.foreground; } - const colorMode = bg & Attributes.CM_MASK; - switch (colorMode) { + switch (bgColorMode) { case Attributes.CM_P16: case Attributes.CM_P256: - return this._getColorFromAnsiIndex(bg & Attributes.PCOLOR_MASK); + return this._getColorFromAnsiIndex(bgColor); case Attributes.CM_RGB: - const rgb = bg & Attributes.RGB_MASK; - const arr = AttributeData.toColorRGB(rgb); + const arr = AttributeData.toColorRGB(bgColor); // TODO: This object creation is slow return { - rgba: rgb << 8, + rgba: bgColor << 8, css: `#${toPaddedHex(arr[0])}${toPaddedHex(arr[1])}${toPaddedHex(arr[2])}` }; case Attributes.CM_DEFAULT: default: + if (inverse) { + return this._config.colors.foreground; + } return this._config.colors.background; } } - private _getForegroundCss(fg: number): string { - if (fg & FgFlags.INVERSE) { - return this._config.colors.background.css; - } - - const colorMode = fg & Attributes.CM_MASK; - switch (colorMode) { + private _getForegroundCss(fgColorMode: number, fgColor: number, inverse: boolean): string { + switch (fgColorMode) { case Attributes.CM_P16: case Attributes.CM_P256: - return this._getColorFromAnsiIndex(fg & Attributes.PCOLOR_MASK).css; + return this._getColorFromAnsiIndex(fgColor).css; case Attributes.CM_RGB: - const rgb = fg & Attributes.RGB_MASK; - const arr = AttributeData.toColorRGB(rgb); + const arr = AttributeData.toColorRGB(fgColor); return `#${toPaddedHex(arr[0])}${toPaddedHex(arr[1])}${toPaddedHex(arr[2])}`; case Attributes.CM_DEFAULT: default: + if (inverse) { + return this._config.colors.background.css; + } return this._config.colors.foreground.css; } } @@ -233,13 +229,33 @@ export class WebglCharAtlas implements IDisposable { this.hasCanvasChanged = true; const bold = !!(fg & FgFlags.BOLD); + const inverse = !!(fg & FgFlags.INVERSE); const dim = !!(bg & BgFlags.DIM); const italic = !!(bg & BgFlags.ITALIC); this._tmpCtx.save(); + let fgColor = getFgColor(fg); + let fgColorMode = fg & Attributes.CM_MASK; + let bgColor = getBgColor(bg); + let bgColorMode = bg & Attributes.CM_MASK; + if (inverse) { + const temp = fgColor; + fgColor = bgColor; + bgColor = temp; + const temp2 = fgColorMode; + fgColorMode = bgColorMode; + bgColorMode = temp2; + } + + // TODO: Pass drawBoldTextInBrightColors through + // Apply drawBoldTextInBrightColors + // if (terminal.options.drawBoldTextInBrightColors && this._workCell.isBold() && fg & FgFlags.BOLD && this._workCell.getFgColor() < 8) { + // fg += 8; + // } + // draw the background - const backgroundColor = this._getBackgroundColor(bg, fg); + const backgroundColor = this._getBackgroundColor(bgColorMode, bgColor, inverse); // Use a 'copy' composite operation to clear any existing glyph out of _tmpCtxWithAlpha, regardless of // transparency in backgroundColor this._tmpCtx.globalCompositeOperation = 'copy'; @@ -254,7 +270,7 @@ export class WebglCharAtlas implements IDisposable { `${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`; this._tmpCtx.textBaseline = 'top'; - this._tmpCtx.fillStyle = this._getForegroundCss(fg); + this._tmpCtx.fillStyle = this._getForegroundCss(fgColorMode, fgColor, inverse); // Apply alpha to dim the character if (dim) { @@ -441,3 +457,19 @@ function toPaddedHex(c: number): string { return s.length < 2 ? '0' + s : s; } +function getFgColor(fg: number): number { + switch (fg & Attributes.CM_MASK) { + case Attributes.CM_P16: + case Attributes.CM_P256: return fg & Attributes.PCOLOR_MASK; + case Attributes.CM_RGB: return fg & Attributes.RGB_MASK; + default: return -1; // CM_DEFAULT defaults to -1 + } +} +function getBgColor(bg: number): number { + switch (bg & Attributes.CM_MASK) { + case Attributes.CM_P16: + case Attributes.CM_P256: return bg & Attributes.PCOLOR_MASK; + case Attributes.CM_RGB: return bg & Attributes.RGB_MASK; + default: return -1; // CM_DEFAULT defaults to -1 + } +}