From 40bae59f99b21e0fda2bd68f7ced7b477795cc4d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 1 Jan 2026 16:17:40 -0800 Subject: [PATCH 1/4] Editor GPU: Support rendering strikethrough Fixes #233992 --- .../browser/gpu/css/decorationStyleCache.ts | 22 +++++++++++---- .../browser/gpu/raster/glyphRasterizer.ts | 19 ++++++++++--- .../renderStrategy/fullFileRenderStrategy.ts | 25 ++++++++++++++++- .../renderStrategy/viewportRenderStrategy.ts | 25 ++++++++++++++++- src/vs/editor/browser/gpu/viewGpuContext.ts | 27 +++++++++++++++++++ 5 files changed, 108 insertions(+), 10 deletions(-) diff --git a/src/vs/editor/browser/gpu/css/decorationStyleCache.ts b/src/vs/editor/browser/gpu/css/decorationStyleCache.ts index 1b1c07df16303..5b191fab3dc33 100644 --- a/src/vs/editor/browser/gpu/css/decorationStyleCache.ts +++ b/src/vs/editor/browser/gpu/css/decorationStyleCache.ts @@ -18,6 +18,14 @@ export interface IDecorationStyleSet { * A number between 0 and 1 representing the opacity of the text. */ opacity: number | undefined; + /** + * Whether the text should be rendered with a strikethrough. + */ + strikethrough: boolean | undefined; + /** + * The thickness of the strikethrough line in pixels (CSS pixels, not device pixels). + */ + strikethroughThickness: number | undefined; } export interface IDecorationStyleCacheEntry extends IDecorationStyleSet { @@ -32,17 +40,19 @@ export class DecorationStyleCache { private _nextId = 1; private readonly _cacheById = new Map(); - private readonly _cacheByStyle = new NKeyMap(); + private readonly _cacheByStyle = new NKeyMap(); getOrCreateEntry( color: number | undefined, bold: boolean | undefined, - opacity: number | undefined + opacity: number | undefined, + strikethrough: boolean | undefined, + strikethroughThickness: number | undefined ): number { - if (color === undefined && bold === undefined && opacity === undefined) { + if (color === undefined && bold === undefined && opacity === undefined && strikethrough === undefined && strikethroughThickness === undefined) { return 0; } - const result = this._cacheByStyle.get(color ?? 0, bold ? 1 : 0, opacity === undefined ? '' : opacity.toFixed(2)); + const result = this._cacheByStyle.get(color ?? 0, bold ? 1 : 0, opacity === undefined ? '' : opacity.toFixed(2), strikethrough ? 1 : 0, strikethroughThickness === undefined ? '' : strikethroughThickness.toFixed(2)); if (result) { return result.id; } @@ -52,9 +62,11 @@ export class DecorationStyleCache { color, bold, opacity, + strikethrough, + strikethroughThickness, }; this._cacheById.set(id, entry); - this._cacheByStyle.set(entry, color ?? 0, bold ? 1 : 0, opacity === undefined ? '' : opacity.toFixed(2)); + this._cacheByStyle.set(entry, color ?? 0, bold ? 1 : 0, opacity === undefined ? '' : opacity.toFixed(2), strikethrough ? 1 : 0, strikethroughThickness === undefined ? '' : strikethroughThickness.toFixed(2)); return id; } diff --git a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts index cc41c8dcaa4b8..90daa9bcb1c6f 100644 --- a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts @@ -145,9 +145,8 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { fontSb.appendString(`${devicePixelFontSize}px ${this.fontFamily}`); this._ctx.font = fontSb.build(); - // TODO: Support FontStyle.Strikethrough and FontStyle.Underline text decorations, these - // need to be drawn manually to the canvas. See xterm.js for "dodging" the text for - // underlines. + // TODO: Support FontStyle.Underline text decorations, these need to be drawn manually to + // the canvas. See xterm.js for "dodging" the text for underlines. const originX = devicePixelFontSize; const originY = devicePixelFontSize; @@ -163,6 +162,20 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { } this._ctx.fillText(chars, originX + xSubPixelXOffset, originY); + + // Draw strikethrough if needed + if (decorationStyleSet?.strikethrough || (fontStyle & FontStyle.Strikethrough)) { + const textMetrics = this._ctx.measureText(chars); + // Position strikethrough at the vertical center of lowercase letters. + // With textBaseline='top', alphabeticBaseline is the distance from top to baseline. + // Strikethrough should be at roughly 65-70% down from top to baseline (x-height center). + const strikethroughY = Math.round(originY - textMetrics.alphabeticBaseline * 0.65); + const lineWidth = decorationStyleSet?.strikethroughThickness !== undefined + ? Math.round(decorationStyleSet.strikethroughThickness * this.devicePixelRatio) + : Math.max(1, Math.floor(devicePixelFontSize / 10)); + this._ctx.fillRect(originX + xSubPixelXOffset, strikethroughY - Math.floor(lineWidth / 2), textMetrics.width, lineWidth); + } + this._ctx.restore(); const imageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); diff --git a/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts index 9588c78c49c59..e4592079612c8 100644 --- a/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts @@ -257,6 +257,8 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { let decorationStyleSetBold: boolean | undefined; let decorationStyleSetColor: number | undefined; let decorationStyleSetOpacity: number | undefined; + let decorationStyleSetStrikethrough: boolean | undefined; + let decorationStyleSetStrikethroughThickness: number | undefined; let lineData: ViewLineRenderingData; let decoration: InlineDecoration; @@ -375,6 +377,8 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { decorationStyleSetColor = undefined; decorationStyleSetBold = undefined; decorationStyleSetOpacity = undefined; + decorationStyleSetStrikethrough = undefined; + decorationStyleSetStrikethroughThickness = undefined; // Apply supported inline decoration styles to the cell metadata for (decoration of lineData.inlineDecorations) { @@ -419,6 +423,25 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { decorationStyleSetOpacity = parsedValue; break; } + case 'text-decoration': + case 'text-decoration-line': { + if (value === 'line-through') { + decorationStyleSetStrikethrough = true; + } + break; + } + case 'text-decoration-thickness': { + const match = value.match(/^(\d+(?:\.\d+)?)px$/); + if (match) { + decorationStyleSetStrikethroughThickness = parseFloat(match[1]); + } + break; + } + case 'text-decoration-color': + case 'text-decoration-style': { + // These are validated in canRender and use default behavior + break; + } default: throw new BugIndicatingError('Unexpected inline decoration style'); } } @@ -443,7 +466,7 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { continue; } - const decorationStyleSetId = ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor, decorationStyleSetBold, decorationStyleSetOpacity); + const decorationStyleSetId = ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor, decorationStyleSetBold, decorationStyleSetOpacity, decorationStyleSetStrikethrough, decorationStyleSetStrikethroughThickness); glyph = this._viewGpuContext.atlas.getGlyph(this.glyphRasterizer, chars, tokenMetadata, decorationStyleSetId, absoluteOffsetX); absoluteOffsetY = Math.round( diff --git a/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts index 9dbfa48a53fdc..3212a52924094 100644 --- a/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts @@ -209,6 +209,8 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { let decorationStyleSetBold: boolean | undefined; let decorationStyleSetColor: number | undefined; let decorationStyleSetOpacity: number | undefined; + let decorationStyleSetStrikethrough: boolean | undefined; + let decorationStyleSetStrikethroughThickness: number | undefined; let lineData: ViewLineRenderingData; let decoration: InlineDecoration; @@ -278,6 +280,8 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { decorationStyleSetColor = undefined; decorationStyleSetBold = undefined; decorationStyleSetOpacity = undefined; + decorationStyleSetStrikethrough = undefined; + decorationStyleSetStrikethroughThickness = undefined; // Apply supported inline decoration styles to the cell metadata for (decoration of lineData.inlineDecorations) { @@ -322,6 +326,25 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { decorationStyleSetOpacity = parsedValue; break; } + case 'text-decoration': + case 'text-decoration-line': { + if (value === 'line-through') { + decorationStyleSetStrikethrough = true; + } + break; + } + case 'text-decoration-thickness': { + const match = value.match(/^(\d+(?:\.\d+)?)px$/); + if (match) { + decorationStyleSetStrikethroughThickness = parseFloat(match[1]); + } + break; + } + case 'text-decoration-color': + case 'text-decoration-style': { + // These are validated in canRender and use default behavior + break; + } default: throw new BugIndicatingError('Unexpected inline decoration style'); } } @@ -346,7 +369,7 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { continue; } - const decorationStyleSetId = ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor, decorationStyleSetBold, decorationStyleSetOpacity); + const decorationStyleSetId = ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor, decorationStyleSetBold, decorationStyleSetOpacity, decorationStyleSetStrikethrough, decorationStyleSetStrikethroughThickness); glyph = this._viewGpuContext.atlas.getGlyph(this.glyphRasterizer, chars, tokenMetadata, decorationStyleSetId, absoluteOffsetX); absoluteOffsetY = Math.round( diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index bab5b2f9408bb..bae6ef85bf991 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -255,6 +255,12 @@ const gpuSupportedDecorationCssRules = [ 'color', 'font-weight', 'opacity', + 'text-decoration', + // TODO: This isn't actually supported, it uses the text color currently + 'text-decoration-color', + 'text-decoration-line', + 'text-decoration-style', + 'text-decoration-thickness', ]; function supportsCssRule(rule: string, style: CSSStyleDeclaration) { @@ -263,6 +269,27 @@ function supportsCssRule(rule: string, style: CSSStyleDeclaration) { } // Check for values that aren't supported switch (rule) { + case 'text-decoration': + case 'text-decoration-line': { + const value = style.getPropertyValue(rule); + // Only line-through is supported currently + return value === 'line-through'; + } + case 'text-decoration-color': { + const value = style.getPropertyValue(rule); + // Support var(--something, initial/inherit) which falls back to currentcolor + return /^var\(--[^,]+,\s*(?:initial|inherit)\)$/.test(value); + } + case 'text-decoration-style': { + const value = style.getPropertyValue(rule); + // Only 'initial' (solid) is supported + return value === 'initial'; + } + case 'text-decoration-thickness': { + const value = style.getPropertyValue(rule); + // Only pixel values and 'initial' are supported + return value === 'initial' || /^\d+(\.\d+)?px$/.test(value); + } default: return true; } } From 0b29a318b72a1370d9867c24e24983549c6a20cd Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 2 Jan 2026 04:15:41 -0800 Subject: [PATCH 2/4] Improve comments and flow --- .../browser/gpu/raster/glyphRasterizer.ts | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts index 90daa9bcb1c6f..fc4f454ed3bbc 100644 --- a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts @@ -115,7 +115,7 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { // The sub-pixel x offset is the fractional part of the x pixel coordinate of the cell, this // is used to improve the spacing between rendered characters. - const xSubPixelXOffset = (tokenMetadata & 0b1111) / 10; + const subPixelXOffset = (tokenMetadata & 0b1111) / 10; const bgId = TokenMetadata.getBackground(tokenMetadata); const bg = colorMap[bgId]; @@ -150,34 +150,45 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { const originX = devicePixelFontSize; const originY = devicePixelFontSize; + + // Apply text color if (decorationStyleSet?.color !== undefined) { this._ctx.fillStyle = `#${decorationStyleSet.color.toString(16).padStart(8, '0')}`; } else { this._ctx.fillStyle = colorMap[TokenMetadata.getForeground(tokenMetadata)]; } - this._ctx.textBaseline = 'top'; + // Apply opacity if (decorationStyleSet?.opacity !== undefined) { this._ctx.globalAlpha = decorationStyleSet.opacity; } - this._ctx.fillText(chars, originX + xSubPixelXOffset, originY); + // The glyph baseline is top, meaning it's drawn at the top-left of the + // cell. Add `TextMetrics.alphabeticBaseline` to the drawn position to + // get the alphabetic baseline. + this._ctx.textBaseline = 'top'; + + // Draw the text + this._ctx.fillText(chars, originX + subPixelXOffset, originY); - // Draw strikethrough if needed - if (decorationStyleSet?.strikethrough || (fontStyle & FontStyle.Strikethrough)) { - const textMetrics = this._ctx.measureText(chars); - // Position strikethrough at the vertical center of lowercase letters. - // With textBaseline='top', alphabeticBaseline is the distance from top to baseline. - // Strikethrough should be at roughly 65-70% down from top to baseline (x-height center). - const strikethroughY = Math.round(originY - textMetrics.alphabeticBaseline * 0.65); + // Draw strikethrough + if (decorationStyleSet?.strikethrough) { + // TODO: This position could be refined further by checking + // TextMetrics of lowercase letters. + // Position strikethrough at approximately the vertical center of + // lowercase letters. + const strikethroughY = Math.round(originY - this._textMetrics.alphabeticBaseline * 0.65); const lineWidth = decorationStyleSet?.strikethroughThickness !== undefined ? Math.round(decorationStyleSet.strikethroughThickness * this.devicePixelRatio) : Math.max(1, Math.floor(devicePixelFontSize / 10)); - this._ctx.fillRect(originX + xSubPixelXOffset, strikethroughY - Math.floor(lineWidth / 2), textMetrics.width, lineWidth); + // Intentionally do not apply the sub pixel x offset to + // strikethrough to ensure successive glyphs form a contiguous line. + this._ctx.fillRect(originX, strikethroughY - Math.floor(lineWidth / 2), Math.ceil(this._textMetrics.width), lineWidth); } this._ctx.restore(); + // Extract the image data and clear the background color const imageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); if (this._antiAliasing === 'subpixel') { const bgR = parseInt(bg.substring(1, 3), 16); @@ -186,7 +197,10 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { this._clearColor(imageData, bgR, bgG, bgB); this._ctx.putImageData(imageData, 0, 0); } + + // Find the bounding box this._findGlyphBoundingBox(imageData, this._workGlyph.boundingBox); + // const offset = { // x: textMetrics.actualBoundingBoxLeft, // y: textMetrics.actualBoundingBoxAscent From 88cb10c40b11ee31c4c6d432f18a47f4e2779526 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 2 Jan 2026 04:47:43 -0800 Subject: [PATCH 3/4] Support text-decoration-color --- .../gpu/css/decorationCssRuleExtractor.ts | 12 ++++++- .../browser/gpu/css/decorationStyleCache.ts | 32 +++++++++++++++---- .../browser/gpu/raster/glyphRasterizer.ts | 4 +++ .../renderStrategy/fullFileRenderStrategy.ts | 18 +++++++++-- .../renderStrategy/viewportRenderStrategy.ts | 17 ++++++++-- src/vs/editor/browser/gpu/viewGpuContext.ts | 8 +++-- 6 files changed, 78 insertions(+), 13 deletions(-) diff --git a/src/vs/editor/browser/gpu/css/decorationCssRuleExtractor.ts b/src/vs/editor/browser/gpu/css/decorationCssRuleExtractor.ts index 43dbd85c87ac5..91afd694b1feb 100644 --- a/src/vs/editor/browser/gpu/css/decorationCssRuleExtractor.ts +++ b/src/vs/editor/browser/gpu/css/decorationCssRuleExtractor.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { $, getActiveDocument } from '../../../../base/browser/dom.js'; +import { $, getActiveDocument, getActiveWindow } from '../../../../base/browser/dom.js'; import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js'; import './media/decorationCssRuleExtractor.css'; @@ -81,4 +81,14 @@ export class DecorationCssRuleExtractor extends Disposable { return rules; } + + /** + * Resolves a CSS variable to its computed value using the container element. + */ + resolveCssVariable(canvas: HTMLCanvasElement, variableName: string): string { + canvas.appendChild(this._container); + const result = getActiveWindow().getComputedStyle(this._container).getPropertyValue(variableName).trim(); + canvas.removeChild(this._container); + return result; + } } diff --git a/src/vs/editor/browser/gpu/css/decorationStyleCache.ts b/src/vs/editor/browser/gpu/css/decorationStyleCache.ts index 5b191fab3dc33..1f8d6f0138032 100644 --- a/src/vs/editor/browser/gpu/css/decorationStyleCache.ts +++ b/src/vs/editor/browser/gpu/css/decorationStyleCache.ts @@ -26,6 +26,10 @@ export interface IDecorationStyleSet { * The thickness of the strikethrough line in pixels (CSS pixels, not device pixels). */ strikethroughThickness: number | undefined; + /** + * A 24-bit number representing the strikethrough color. + */ + strikethroughColor: number | undefined; } export interface IDecorationStyleCacheEntry extends IDecorationStyleSet { @@ -40,33 +44,49 @@ export class DecorationStyleCache { private _nextId = 1; private readonly _cacheById = new Map(); - private readonly _cacheByStyle = new NKeyMap(); + private readonly _cacheByStyle = new NKeyMap(); getOrCreateEntry( color: number | undefined, bold: boolean | undefined, opacity: number | undefined, strikethrough: boolean | undefined, - strikethroughThickness: number | undefined + strikethroughThickness: number | undefined, + strikethroughColor: number | undefined ): number { - if (color === undefined && bold === undefined && opacity === undefined && strikethrough === undefined && strikethroughThickness === undefined) { + if (color === undefined && bold === undefined && opacity === undefined && strikethrough === undefined && strikethroughThickness === undefined && strikethroughColor === undefined) { return 0; } - const result = this._cacheByStyle.get(color ?? 0, bold ? 1 : 0, opacity === undefined ? '' : opacity.toFixed(2), strikethrough ? 1 : 0, strikethroughThickness === undefined ? '' : strikethroughThickness.toFixed(2)); + const result = this._cacheByStyle.get( + color ?? 0, + bold ? 1 : 0, + opacity === undefined ? '' : opacity.toFixed(2), + strikethrough ? 1 : 0, + strikethroughThickness === undefined ? '' : strikethroughThickness.toFixed(2), + strikethroughColor ?? 0 + ); if (result) { return result.id; } const id = this._nextId++; - const entry = { + const entry: IDecorationStyleCacheEntry = { id, color, bold, opacity, strikethrough, strikethroughThickness, + strikethroughColor, }; this._cacheById.set(id, entry); - this._cacheByStyle.set(entry, color ?? 0, bold ? 1 : 0, opacity === undefined ? '' : opacity.toFixed(2), strikethrough ? 1 : 0, strikethroughThickness === undefined ? '' : strikethroughThickness.toFixed(2)); + this._cacheByStyle.set(entry, + color ?? 0, + bold ? 1 : 0, + opacity === undefined ? '' : opacity.toFixed(2), + strikethrough ? 1 : 0, + strikethroughThickness === undefined ? '' : strikethroughThickness.toFixed(2), + strikethroughColor ?? 0 + ); return id; } diff --git a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts index fc4f454ed3bbc..7b74ec8830ceb 100644 --- a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts @@ -181,6 +181,10 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { const lineWidth = decorationStyleSet?.strikethroughThickness !== undefined ? Math.round(decorationStyleSet.strikethroughThickness * this.devicePixelRatio) : Math.max(1, Math.floor(devicePixelFontSize / 10)); + // Apply strikethrough color if specified + if (decorationStyleSet?.strikethroughColor !== undefined) { + this._ctx.fillStyle = `#${decorationStyleSet.strikethroughColor.toString(16).padStart(8, '0')}`; + } // Intentionally do not apply the sub pixel x offset to // strikethrough to ensure successive glyphs form a contiguous line. this._ctx.fillRect(originX, strikethroughY - Math.floor(lineWidth / 2), Math.ceil(this._textMetrics.width), lineWidth); diff --git a/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts index e4592079612c8..7248941efa5d4 100644 --- a/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts @@ -259,6 +259,7 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { let decorationStyleSetOpacity: number | undefined; let decorationStyleSetStrikethrough: boolean | undefined; let decorationStyleSetStrikethroughThickness: number | undefined; + let decorationStyleSetStrikethroughColor: number | undefined; let lineData: ViewLineRenderingData; let decoration: InlineDecoration; @@ -379,6 +380,7 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { decorationStyleSetOpacity = undefined; decorationStyleSetStrikethrough = undefined; decorationStyleSetStrikethroughThickness = undefined; + decorationStyleSetStrikethroughColor = undefined; // Apply supported inline decoration styles to the cell metadata for (decoration of lineData.inlineDecorations) { @@ -437,7 +439,19 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { } break; } - case 'text-decoration-color': + case 'text-decoration-color': { + let colorValue = value; + // Resolve CSS variables that fall back to currentcolor + const varMatch = value.match(/^var\((--[^,]+),\s*(?:initial|inherit)\)$/); + if (varMatch) { + colorValue = ViewGpuContext.decorationCssRuleExtractor.resolveCssVariable(this._viewGpuContext.canvas.domNode, varMatch[1]); + } + const parsedColor = Color.Format.CSS.parse(colorValue); + if (parsedColor) { + decorationStyleSetStrikethroughColor = parsedColor.toNumber32Bit(); + } + break; + } case 'text-decoration-style': { // These are validated in canRender and use default behavior break; @@ -466,7 +480,7 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { continue; } - const decorationStyleSetId = ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor, decorationStyleSetBold, decorationStyleSetOpacity, decorationStyleSetStrikethrough, decorationStyleSetStrikethroughThickness); + const decorationStyleSetId = ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor, decorationStyleSetBold, decorationStyleSetOpacity, decorationStyleSetStrikethrough, decorationStyleSetStrikethroughThickness, decorationStyleSetStrikethroughColor); glyph = this._viewGpuContext.atlas.getGlyph(this.glyphRasterizer, chars, tokenMetadata, decorationStyleSetId, absoluteOffsetX); absoluteOffsetY = Math.round( diff --git a/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts index 3212a52924094..446fef8cbbcd7 100644 --- a/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts @@ -211,6 +211,7 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { let decorationStyleSetOpacity: number | undefined; let decorationStyleSetStrikethrough: boolean | undefined; let decorationStyleSetStrikethroughThickness: number | undefined; + let decorationStyleSetStrikethroughColor: number | undefined; let lineData: ViewLineRenderingData; let decoration: InlineDecoration; @@ -282,6 +283,7 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { decorationStyleSetOpacity = undefined; decorationStyleSetStrikethrough = undefined; decorationStyleSetStrikethroughThickness = undefined; + decorationStyleSetStrikethroughColor = undefined; // Apply supported inline decoration styles to the cell metadata for (decoration of lineData.inlineDecorations) { @@ -340,7 +342,18 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { } break; } - case 'text-decoration-color': + case 'text-decoration-color': { + let colorValue = value; + const varMatch = value.match(/^var\((--[^,]+),\s*(?:initial|inherit)\)$/); + if (varMatch) { + colorValue = ViewGpuContext.decorationCssRuleExtractor.resolveCssVariable(this._viewGpuContext.canvas.domNode, varMatch[1]); + } + const parsedColor = Color.Format.CSS.parse(colorValue); + if (parsedColor) { + decorationStyleSetStrikethroughColor = parsedColor.toNumber32Bit(); + } + break; + } case 'text-decoration-style': { // These are validated in canRender and use default behavior break; @@ -369,7 +382,7 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { continue; } - const decorationStyleSetId = ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor, decorationStyleSetBold, decorationStyleSetOpacity, decorationStyleSetStrikethrough, decorationStyleSetStrikethroughThickness); + const decorationStyleSetId = ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor, decorationStyleSetBold, decorationStyleSetOpacity, decorationStyleSetStrikethrough, decorationStyleSetStrikethroughThickness, decorationStyleSetStrikethroughColor); glyph = this._viewGpuContext.atlas.getGlyph(this.glyphRasterizer, chars, tokenMetadata, decorationStyleSetId, absoluteOffsetX); absoluteOffsetY = Math.round( diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index bae6ef85bf991..e3f24380846a6 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -6,6 +6,7 @@ import * as nls from '../../../nls.js'; import { addDisposableListener, getActiveWindow } from '../../../base/browser/dom.js'; import { createFastDomNode, type FastDomNode } from '../../../base/browser/fastDomNode.js'; +import { Color } from '../../../base/common/color.js'; import { BugIndicatingError } from '../../../base/common/errors.js'; import { Disposable } from '../../../base/common/lifecycle.js'; import type { ViewportData } from '../../common/viewLayout/viewLinesViewportData.js'; @@ -256,7 +257,6 @@ const gpuSupportedDecorationCssRules = [ 'font-weight', 'opacity', 'text-decoration', - // TODO: This isn't actually supported, it uses the text color currently 'text-decoration-color', 'text-decoration-line', 'text-decoration-style', @@ -278,7 +278,11 @@ function supportsCssRule(rule: string, style: CSSStyleDeclaration) { case 'text-decoration-color': { const value = style.getPropertyValue(rule); // Support var(--something, initial/inherit) which falls back to currentcolor - return /^var\(--[^,]+,\s*(?:initial|inherit)\)$/.test(value); + if (/^var\(--[^,]+,\s*(?:initial|inherit)\)$/.test(value)) { + return true; + } + // Support parsed color values + return Color.Format.CSS.parse(value) !== null; } case 'text-decoration-style': { const value = style.getPropertyValue(rule); From f8fd70a4873e220505b1b96f80d0f337cd4001a2 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 2 Jan 2026 05:40:57 -0800 Subject: [PATCH 4/4] Address comment feedback --- src/vs/editor/browser/gpu/css/decorationStyleCache.ts | 2 +- .../editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/editor/browser/gpu/css/decorationStyleCache.ts b/src/vs/editor/browser/gpu/css/decorationStyleCache.ts index 1f8d6f0138032..9a2eea56e1ab3 100644 --- a/src/vs/editor/browser/gpu/css/decorationStyleCache.ts +++ b/src/vs/editor/browser/gpu/css/decorationStyleCache.ts @@ -27,7 +27,7 @@ export interface IDecorationStyleSet { */ strikethroughThickness: number | undefined; /** - * A 24-bit number representing the strikethrough color. + * A 32-bit number representing the strikethrough color. */ strikethroughColor: number | undefined; } diff --git a/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts index 7248941efa5d4..f0ae8bba97f3c 100644 --- a/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts @@ -441,7 +441,6 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { } case 'text-decoration-color': { let colorValue = value; - // Resolve CSS variables that fall back to currentcolor const varMatch = value.match(/^var\((--[^,]+),\s*(?:initial|inherit)\)$/); if (varMatch) { colorValue = ViewGpuContext.decorationCssRuleExtractor.resolveCssVariable(this._viewGpuContext.canvas.domNode, varMatch[1]);