From cb4076e563719dc2b29761f8239cbcf4de585d49 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 18 Nov 2024 10:37:26 -0800 Subject: [PATCH 01/20] Decorations wip Part of #227095 --- .../browser/gpu/fullFileRenderStrategy.ts | 49 ++++++++++++++++++- .../browser/gpu/raster/glyphRasterizer.ts | 6 +++ src/vs/editor/browser/gpu/viewGpuContext.ts | 9 +++- 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index a6df706d2b89e..a42148b222561 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -7,9 +7,12 @@ import { getActiveWindow } from '../../../base/browser/dom.js'; import { BugIndicatingError } from '../../../base/common/errors.js'; import { EditorOption } from '../../common/config/editorOptions.js'; import { CursorColumns } from '../../common/core/cursorColumns.js'; +import { Range } from '../../common/core/range.js'; +import { MetadataConsts } from '../../common/encodedTokenAttributes.js'; +import { ClassName } from '../../common/model/intervalTree.js'; import type { IViewLineTokens } from '../../common/tokens/lineTokens.js'; import { ViewEventHandler } from '../../common/viewEventHandler.js'; -import { ViewEventType, type ViewConfigurationChangedEvent, type ViewLinesChangedEvent, type ViewLinesDeletedEvent, type ViewLinesInsertedEvent, type ViewScrollChangedEvent, type ViewTokensChangedEvent, type ViewZonesChangedEvent } from '../../common/viewEvents.js'; +import { ViewEventType, type ViewConfigurationChangedEvent, type ViewDecorationsChangedEvent, type ViewLinesChangedEvent, type ViewLinesDeletedEvent, type ViewLinesInsertedEvent, type ViewScrollChangedEvent, type ViewTokensChangedEvent, type ViewZonesChangedEvent } from '../../common/viewEvents.js'; import type { ViewportData } from '../../common/viewLayout/viewLinesViewportData.js'; import type { ViewLineRenderingData } from '../../common/viewModel.js'; import type { ViewContext } from '../../common/viewModel/viewContext.js'; @@ -115,6 +118,13 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend return true; } + public override onDecorationsChanged(e: ViewDecorationsChangedEvent): boolean { + // TODO: Don't clear all lines + this._upToDateLines[0].clear(); + this._upToDateLines[1].clear(); + return true; + } + public override onTokensChanged(e: ViewTokensChangedEvent): boolean { // TODO: This currently fires for the entire viewport whenever scrolling stops // https://github.com/microsoft/vscode/issues/233942 @@ -274,6 +284,8 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend } } + const decorations = viewportData.getDecorationsInViewport(); + for (y = viewportData.startLineNumber; y <= viewportData.endLineNumber; y++) { // Only attempt to render lines that the GPU renderer can handle @@ -291,6 +303,14 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend dirtyLineStart = Math.min(dirtyLineStart, y); dirtyLineEnd = Math.max(dirtyLineEnd, y); + const inlineDecorations = decorations.filter(e => ( + e.range.startLineNumber <= y && e.range.endLineNumber >= y && + e.options.inlineClassName + )); + if (inlineDecorations.length > 0) { + console.log('decoration!', inlineDecorations); + } + lineData = viewportData.getViewLineRenderingData(y); content = lineData.content; xOffset = 0; @@ -313,6 +333,33 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend break; } chars = content.charAt(x); + + // TODO: We'd want to optimize pulling the decorations in order + // HACK: Temporary replace char to demonstrate inline decorations + const cellDecorations = decorations.filter(decoration => { + // TODO: Why does Range.containsPosition and Range.strictContainsPosition not work here? + if (y < decoration.range.startLineNumber || y > decoration.range.endLineNumber) { + return false; + } + if (y === decoration.range.startLineNumber && x < decoration.range.startColumn - 1) { + return false; + } + if (y === decoration.range.endLineNumber && x >= decoration.range.endColumn - 1) { + return false; + } + return true; + }); + for (const decoration of cellDecorations) { + switch (decoration.options.inlineClassName) { + case (ClassName.EditorDeprecatedInlineDecoration): { + // HACK: We probably shouldn't override tokenMetadata + tokenMetadata |= MetadataConsts.STRIKETHROUGH_MASK; + // chars = '-'; + break; + } + } + } + if (chars === ' ' || chars === '\t') { // Zero out glyph to ensure it doesn't get rendered cellIndex = ((y - 1) * this._viewGpuContext.maxGpuCols + x) * Constants.IndicesPerCell; diff --git a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts index 26010b7e38690..07d343bb677c4 100644 --- a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts @@ -120,6 +120,12 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { this._ctx.textBaseline = 'top'; this._ctx.fillText(chars, originX, originY); + // TODO: Don't draw beyond glyph - how to handle monospace, wide and proportional? + // TODO: Support strikethrough color + if (fontStyle & FontStyle.Strikethrough) { + this._ctx.fillRect(originX, originY + Math.round(devicePixelFontSize / 2), devicePixelFontSize, Math.max(Math.floor(getActiveWindow().devicePixelRatio), 1)); + } + const imageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); this._findGlyphBoundingBox(imageData, this._workGlyph.boundingBox); // const offset = { diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index 307636b37e1fe..310a6f97f5d1e 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -19,6 +19,7 @@ import { GPULifecycle } from './gpuDisposable.js'; import { ensureNonNullable, observeDevicePixelDimensions } from './gpuUtils.js'; import { RectangleRenderer } from './rectangleRenderer.js'; import type { ViewContext } from '../../common/viewModel/viewContext.js'; +import { ClassName } from '../../common/model/intervalTree.js'; const enum GpuRenderLimits { maxGpuLines = 3000, @@ -131,7 +132,8 @@ export class ViewGpuContext extends Disposable { data.containsRTL || data.maxColumn > GpuRenderLimits.maxGpuCols || data.continuesWithWrappedLine || - data.inlineDecorations.length > 0 || + // HACK: ... + data.inlineDecorations.length > 0 && data.inlineDecorations[0].inlineClassName !== ClassName.EditorDeprecatedInlineDecoration || lineNumber >= GpuRenderLimits.maxGpuLines ) { return false; @@ -155,7 +157,10 @@ export class ViewGpuContext extends Disposable { reasons.push('continuesWithWrappedLine'); } if (data.inlineDecorations.length > 0) { - reasons.push('inlineDecorations > 0'); + // HACK: ... + if (data.inlineDecorations[0].inlineClassName !== ClassName.EditorDeprecatedInlineDecoration) { + reasons.push('inlineDecorations > 0'); + } } if (lineNumber >= GpuRenderLimits.maxGpuLines) { reasons.push('lineNumber >= maxGpuLines'); From 8354641acf0adb64e6b881bbdc91345bd9dc05d5 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 19 Nov 2024 07:58:57 -0800 Subject: [PATCH 02/20] Prototype for extracting inline decoration styles --- src/vs/editor/browser/gpu/cssRuleExtractor.ts | 77 +++++++++++++++++++ .../browser/gpu/fullFileRenderStrategy.ts | 27 ++++++- 2 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 src/vs/editor/browser/gpu/cssRuleExtractor.ts diff --git a/src/vs/editor/browser/gpu/cssRuleExtractor.ts b/src/vs/editor/browser/gpu/cssRuleExtractor.ts new file mode 100644 index 0000000000000..d8bd41ac385d7 --- /dev/null +++ b/src/vs/editor/browser/gpu/cssRuleExtractor.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { $, getActiveDocument } from '../../../base/browser/dom.js'; +import { Disposable, toDisposable } from '../../../base/common/lifecycle.js'; +import type { ViewGpuContext } from './viewGpuContext.js'; + +export class CssRuleExtractor extends Disposable { + private _container: HTMLElement; + + private _ruleCache: Map = new Map(); + + constructor( + private readonly _viewGpuContext: ViewGpuContext, + ) { + super(); + + this._container = $('div.monaco-css-rule-extractor'); + this._container.style.visibility = 'hidden'; + const parentElement = this._viewGpuContext.canvas.domNode.parentElement; + if (!parentElement) { + throw new Error('No parent element found for the canvas'); + } + parentElement.appendChild(this._container); + this._register(toDisposable(() => this._container.remove())); + } + + getStyleRules(className: string): CSSStyleRule[] { + const existing = this._ruleCache.get(className); + if (existing) { + return existing; + } + const dummyElement = $(`span.${className}`); + this._container.appendChild(dummyElement); + const rules = this._getStyleRules(dummyElement, className); + this._ruleCache.set(className, rules); + return rules; + } + + private _getStyleRules(element: HTMLElement, className: string) { + const matchedRules = []; + + // Iterate through all stylesheets + const doc = getActiveDocument(); + for (const stylesheet of doc.styleSheets) { + try { + // Iterate through all CSS rules in the stylesheet + for (const rule of stylesheet.cssRules) { + if (rule instanceof CSSImportRule) { + // Recursively process the import rule + if (rule.styleSheet?.cssRules) { + for (const innerRule of rule.styleSheet.cssRules) { + if (innerRule instanceof CSSStyleRule) { + if (element.matches(innerRule.selectorText) && innerRule.selectorText.includes(className)) { + matchedRules.push(innerRule); + } + } + } + } + } else if (rule instanceof CSSStyleRule) { + // Check if the element matches the selector + if (element.matches(rule.selectorText) && rule.selectorText.includes(className)) { + matchedRules.push(rule); + } + } + } + } catch (e) { + // Some stylesheets may not be accessible due to CORS restrictions + console.warn('Could not access stylesheet:', stylesheet.href); + } + } + + return matchedRules; + } +} diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index a42148b222561..a57f6c9f085fe 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -3,11 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getActiveWindow } from '../../../base/browser/dom.js'; +import { getActiveDocument, getActiveWindow } from '../../../base/browser/dom.js'; import { BugIndicatingError } from '../../../base/common/errors.js'; import { EditorOption } from '../../common/config/editorOptions.js'; import { CursorColumns } from '../../common/core/cursorColumns.js'; -import { Range } from '../../common/core/range.js'; import { MetadataConsts } from '../../common/encodedTokenAttributes.js'; import { ClassName } from '../../common/model/intervalTree.js'; import type { IViewLineTokens } from '../../common/tokens/lineTokens.js'; @@ -18,6 +17,7 @@ import type { ViewLineRenderingData } from '../../common/viewModel.js'; import type { ViewContext } from '../../common/viewModel/viewContext.js'; import type { ViewLineOptions } from '../viewParts/viewLines/viewLineOptions.js'; import type { ITextureAtlasPageGlyph } from './atlas/atlas.js'; +import { CssRuleExtractor } from './cssRuleExtractor.js'; import { fullFileRenderStrategyWgsl } from './fullFileRenderStrategy.wgsl.js'; import { BindingId, type IGpuRenderStrategy } from './gpu.js'; import { GPULifecycle } from './gpuDisposable.js'; @@ -48,6 +48,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend readonly wgsl: string = fullFileRenderStrategyWgsl; private readonly _glyphRasterizer: GlyphRasterizer; + private readonly _cssRuleExtractor: CssRuleExtractor; private _cellBindBuffer!: GPUBuffer; @@ -89,6 +90,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend const fontSize = this._context.configuration.options.get(EditorOption.fontSize); this._glyphRasterizer = this._register(new GlyphRasterizer(fontSize, fontFamily)); + this._cssRuleExtractor = this._register(new CssRuleExtractor(this._viewGpuContext)); const bufferSize = this._viewGpuContext.maxGpuLines * this._viewGpuContext.maxGpuCols * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT; this._cellBindBuffer = this._register(GPULifecycle.createBuffer(this._device, { @@ -336,7 +338,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend // TODO: We'd want to optimize pulling the decorations in order // HACK: Temporary replace char to demonstrate inline decorations - const cellDecorations = decorations.filter(decoration => { + const cellDecorations = inlineDecorations.filter(decoration => { // TODO: Why does Range.containsPosition and Range.strictContainsPosition not work here? if (y < decoration.range.startLineNumber || y > decoration.range.endLineNumber) { return false; @@ -350,6 +352,25 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend return true; }); for (const decoration of cellDecorations) { + if (!decoration.options.inlineClassName) { + throw new BugIndicatingError('Expected inlineClassName on decoration'); + } + const rules = this._cssRuleExtractor.getStyleRules(decoration.options.inlineClassName); + const supportedCssRules = [ + 'text-decoration-line', + 'text-decoration-thickness', + 'text-decoration-style', + 'text-decoration-color', + ]; + const supported = rules.every(rule => { + for (const r of rule.style) { + if (!supportedCssRules.includes(r)) { + return false; + } + } + return true; + }); + console.log('rules supported?', supported, rules); switch (decoration.options.inlineClassName) { case (ClassName.EditorDeprecatedInlineDecoration): { // HACK: We probably shouldn't override tokenMetadata From a42e4e9a5f923e10a4af8506502ad8041905e464 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 19 Nov 2024 12:13:49 -0800 Subject: [PATCH 03/20] Improve rule pulling, report unsupported rules --- src/vs/editor/browser/gpu/cssRuleExtractor.ts | 77 ------------------- .../browser/gpu/decorationCssRuleExtractor.ts | 57 ++++++++++++++ .../browser/gpu/fullFileRenderStrategy.ts | 29 ++----- src/vs/editor/browser/gpu/viewGpuContext.ts | 66 ++++++++++++++-- .../browser/viewParts/gpuMark/gpuMark.ts | 4 +- .../browser/viewParts/viewLines/viewLine.ts | 4 +- .../viewParts/viewLinesGpu/viewLinesGpu.ts | 4 +- 7 files changed, 128 insertions(+), 113 deletions(-) delete mode 100644 src/vs/editor/browser/gpu/cssRuleExtractor.ts create mode 100644 src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts diff --git a/src/vs/editor/browser/gpu/cssRuleExtractor.ts b/src/vs/editor/browser/gpu/cssRuleExtractor.ts deleted file mode 100644 index d8bd41ac385d7..0000000000000 --- a/src/vs/editor/browser/gpu/cssRuleExtractor.ts +++ /dev/null @@ -1,77 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { $, getActiveDocument } from '../../../base/browser/dom.js'; -import { Disposable, toDisposable } from '../../../base/common/lifecycle.js'; -import type { ViewGpuContext } from './viewGpuContext.js'; - -export class CssRuleExtractor extends Disposable { - private _container: HTMLElement; - - private _ruleCache: Map = new Map(); - - constructor( - private readonly _viewGpuContext: ViewGpuContext, - ) { - super(); - - this._container = $('div.monaco-css-rule-extractor'); - this._container.style.visibility = 'hidden'; - const parentElement = this._viewGpuContext.canvas.domNode.parentElement; - if (!parentElement) { - throw new Error('No parent element found for the canvas'); - } - parentElement.appendChild(this._container); - this._register(toDisposable(() => this._container.remove())); - } - - getStyleRules(className: string): CSSStyleRule[] { - const existing = this._ruleCache.get(className); - if (existing) { - return existing; - } - const dummyElement = $(`span.${className}`); - this._container.appendChild(dummyElement); - const rules = this._getStyleRules(dummyElement, className); - this._ruleCache.set(className, rules); - return rules; - } - - private _getStyleRules(element: HTMLElement, className: string) { - const matchedRules = []; - - // Iterate through all stylesheets - const doc = getActiveDocument(); - for (const stylesheet of doc.styleSheets) { - try { - // Iterate through all CSS rules in the stylesheet - for (const rule of stylesheet.cssRules) { - if (rule instanceof CSSImportRule) { - // Recursively process the import rule - if (rule.styleSheet?.cssRules) { - for (const innerRule of rule.styleSheet.cssRules) { - if (innerRule instanceof CSSStyleRule) { - if (element.matches(innerRule.selectorText) && innerRule.selectorText.includes(className)) { - matchedRules.push(innerRule); - } - } - } - } - } else if (rule instanceof CSSStyleRule) { - // Check if the element matches the selector - if (element.matches(rule.selectorText) && rule.selectorText.includes(className)) { - matchedRules.push(rule); - } - } - } - } catch (e) { - // Some stylesheets may not be accessible due to CORS restrictions - console.warn('Could not access stylesheet:', stylesheet.href); - } - } - - return matchedRules; - } -} diff --git a/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts b/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts new file mode 100644 index 0000000000000..5959bd36cd3e6 --- /dev/null +++ b/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { $, getActiveDocument } from '../../../base/browser/dom.js'; +import { Disposable, toDisposable } from '../../../base/common/lifecycle.js'; + +export class DecorationCssRuleExtractor extends Disposable { + private _container: HTMLElement; + + private _ruleCache: Map = new Map(); + + constructor() { + super(); + this._container = $('div.monaco-css-rule-extractor'); + this._container.style.visibility = 'hidden'; + this._register(toDisposable(() => this._container.remove())); + } + + getStyleRules(canvas: HTMLElement, decorationClassName: string): CSSStyleRule[] { + const existing = this._ruleCache.get(decorationClassName); + if (existing) { + return existing; + } + const dummyElement = $(`span.${decorationClassName}`); + this._container.appendChild(dummyElement); + const rules = this._getStyleRules(canvas, dummyElement, decorationClassName); + this._ruleCache.set(decorationClassName, rules); + return rules; + } + + private _getStyleRules(canvas: HTMLElement, element: HTMLElement, className: string) { + canvas.appendChild(this._container); + + // Iterate through all stylesheets and imported stylesheets to find matching rules + const rules = []; + const doc = getActiveDocument(); + const stylesheets = [...doc.styleSheets]; + for (let i = 0; i < stylesheets.length; i++) { + const stylesheet = stylesheets[i]; + for (const rule of stylesheet.cssRules) { + if (rule instanceof CSSImportRule) { + if (rule.styleSheet) { + stylesheets.push(rule.styleSheet); + } + } else if (rule instanceof CSSStyleRule) { + if (element.matches(rule.selectorText) && rule.selectorText.includes(`.${className}`)) { + rules.push(rule); + } + } + } + } + + return rules; + } +} diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index a57f6c9f085fe..473e662a37b64 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getActiveDocument, getActiveWindow } from '../../../base/browser/dom.js'; +import { getActiveWindow } from '../../../base/browser/dom.js'; import { BugIndicatingError } from '../../../base/common/errors.js'; import { EditorOption } from '../../common/config/editorOptions.js'; import { CursorColumns } from '../../common/core/cursorColumns.js'; @@ -17,7 +17,6 @@ import type { ViewLineRenderingData } from '../../common/viewModel.js'; import type { ViewContext } from '../../common/viewModel/viewContext.js'; import type { ViewLineOptions } from '../viewParts/viewLines/viewLineOptions.js'; import type { ITextureAtlasPageGlyph } from './atlas/atlas.js'; -import { CssRuleExtractor } from './cssRuleExtractor.js'; import { fullFileRenderStrategyWgsl } from './fullFileRenderStrategy.wgsl.js'; import { BindingId, type IGpuRenderStrategy } from './gpu.js'; import { GPULifecycle } from './gpuDisposable.js'; @@ -48,7 +47,6 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend readonly wgsl: string = fullFileRenderStrategyWgsl; private readonly _glyphRasterizer: GlyphRasterizer; - private readonly _cssRuleExtractor: CssRuleExtractor; private _cellBindBuffer!: GPUBuffer; @@ -90,7 +88,6 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend const fontSize = this._context.configuration.options.get(EditorOption.fontSize); this._glyphRasterizer = this._register(new GlyphRasterizer(fontSize, fontFamily)); - this._cssRuleExtractor = this._register(new CssRuleExtractor(this._viewGpuContext)); const bufferSize = this._viewGpuContext.maxGpuLines * this._viewGpuContext.maxGpuCols * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT; this._cellBindBuffer = this._register(GPULifecycle.createBuffer(this._device, { @@ -291,7 +288,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend for (y = viewportData.startLineNumber; y <= viewportData.endLineNumber; y++) { // Only attempt to render lines that the GPU renderer can handle - if (!ViewGpuContext.canRender(viewLineOptions, viewportData, y)) { + if (!ViewGpuContext.canRender(this._viewGpuContext.canvas.domNode, viewLineOptions, viewportData, y)) { fillStartIndex = ((y - 1) * this._viewGpuContext.maxGpuCols) * Constants.IndicesPerCell; fillEndIndex = (y * this._viewGpuContext.maxGpuCols) * Constants.IndicesPerCell; cellBuffer.fill(0, fillStartIndex, fillEndIndex); @@ -351,26 +348,10 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend } return true; }); + + // Only lines containing fully supported inline decorations should have made it + // this far. for (const decoration of cellDecorations) { - if (!decoration.options.inlineClassName) { - throw new BugIndicatingError('Expected inlineClassName on decoration'); - } - const rules = this._cssRuleExtractor.getStyleRules(decoration.options.inlineClassName); - const supportedCssRules = [ - 'text-decoration-line', - 'text-decoration-thickness', - 'text-decoration-style', - 'text-decoration-color', - ]; - const supported = rules.every(rule => { - for (const r of rule.style) { - if (!supportedCssRules.includes(r)) { - return false; - } - } - return true; - }); - console.log('rules supported?', supported, rules); switch (decoration.options.inlineClassName) { case (ClassName.EditorDeprecatedInlineDecoration): { // HACK: We probably shouldn't override tokenMetadata diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index 310a6f97f5d1e..70aff96eaaa33 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -19,7 +19,7 @@ import { GPULifecycle } from './gpuDisposable.js'; import { ensureNonNullable, observeDevicePixelDimensions } from './gpuUtils.js'; import { RectangleRenderer } from './rectangleRenderer.js'; import type { ViewContext } from '../../common/viewModel/viewContext.js'; -import { ClassName } from '../../common/model/intervalTree.js'; +import { DecorationCssRuleExtractor } from './decorationCssRuleExtractor.js'; const enum GpuRenderLimits { maxGpuLines = 3000, @@ -48,6 +48,7 @@ export class ViewGpuContext extends Disposable { private static _atlas: TextureAtlas | undefined; + private static readonly _decorationCssRuleExtractor = new DecorationCssRuleExtractor(); /** * The shared texture atlas to use across all views. @@ -126,25 +127,52 @@ export class ViewGpuContext extends Disposable { * renderer. Eventually this should trend all lines, except maybe exceptional cases like * decorations that use class names. */ - public static canRender(options: ViewLineOptions, viewportData: ViewportData, lineNumber: number): boolean { + public static canRender(container: HTMLElement, options: ViewLineOptions, viewportData: ViewportData, lineNumber: number): boolean { const data = viewportData.getViewLineRenderingData(lineNumber); + + // Check if the line has simple attributes that aren't supported if ( data.containsRTL || data.maxColumn > GpuRenderLimits.maxGpuCols || data.continuesWithWrappedLine || - // HACK: ... - data.inlineDecorations.length > 0 && data.inlineDecorations[0].inlineClassName !== ClassName.EditorDeprecatedInlineDecoration || lineNumber >= GpuRenderLimits.maxGpuLines ) { return false; } + + // Check if all inline decorations are supported + if (data.inlineDecorations.length > 0) { + let supported = true; + for (const decoration of data.inlineDecorations) { + const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(container, decoration.inlineClassName); + const supportedCssRules = [ + 'text-decoration-line', + 'text-decoration-thickness', + 'text-decoration-style', + 'text-decoration-color', + ]; + supported &&= styleRules.every(rule => { + for (const r of rule.style) { + if (!supportedCssRules.includes(r)) { + return false; + } + } + return true; + }); + if (!supported) { + break; + } + } + return supported; + } + return true; } /** * Like {@link canRender} but returned detailed information about why the line cannot be rendered. */ - public static canRenderDetailed(options: ViewLineOptions, viewportData: ViewportData, lineNumber: number): string[] { + public static canRenderDetailed(container: HTMLElement, options: ViewLineOptions, viewportData: ViewportData, lineNumber: number): string[] { const data = viewportData.getViewLineRenderingData(lineNumber); const reasons: string[] = []; if (data.containsRTL) { @@ -157,9 +185,31 @@ export class ViewGpuContext extends Disposable { reasons.push('continuesWithWrappedLine'); } if (data.inlineDecorations.length > 0) { - // HACK: ... - if (data.inlineDecorations[0].inlineClassName !== ClassName.EditorDeprecatedInlineDecoration) { - reasons.push('inlineDecorations > 0'); + let supported = true; + const problemRules: string[] = []; + for (const decoration of data.inlineDecorations) { + const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(container, decoration.inlineClassName); + const supportedCssRules = [ + 'text-decoration-line', + 'text-decoration-thickness', + 'text-decoration-style', + 'text-decoration-color', + ]; + supported &&= styleRules.every(rule => { + for (const r of rule.style) { + if (!supportedCssRules.includes(r)) { + problemRules.push(r); + return false; + } + } + return true; + }); + if (!supported) { + break; + } + } + if (problemRules.length > 0) { + reasons.push(`inlineDecorations with unsupported CSS rules (\`${problemRules.join(', ')}\`)`); } } if (lineNumber >= GpuRenderLimits.maxGpuLines) { diff --git a/src/vs/editor/browser/viewParts/gpuMark/gpuMark.ts b/src/vs/editor/browser/viewParts/gpuMark/gpuMark.ts index 7019eff5e007e..861ff529cdc47 100644 --- a/src/vs/editor/browser/viewParts/gpuMark/gpuMark.ts +++ b/src/vs/editor/browser/viewParts/gpuMark/gpuMark.ts @@ -3,6 +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 * as viewEvents from '../../../common/viewEvents.js'; import { ViewContext } from '../../../common/viewModel/viewContext.js'; import { ViewGpuContext } from '../../gpu/viewGpuContext.js'; @@ -77,7 +78,8 @@ export class GpuMarkOverlay extends DynamicViewOverlay { const output: string[] = []; for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) { const lineIndex = lineNumber - visibleStartLineNumber; - const cannotRenderReasons = ViewGpuContext.canRenderDetailed(options, viewportData, lineNumber); + // TODO: How to get the container? + const cannotRenderReasons = ViewGpuContext.canRenderDetailed(getActiveDocument().querySelector('.view-lines')!, options, viewportData, lineNumber); output[lineIndex] = cannotRenderReasons.length ? `
` : ''; } diff --git a/src/vs/editor/browser/viewParts/viewLines/viewLine.ts b/src/vs/editor/browser/viewParts/viewLines/viewLine.ts index 5e555246cea86..d25238bb4cf54 100644 --- a/src/vs/editor/browser/viewParts/viewLines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/viewLines/viewLine.ts @@ -19,6 +19,7 @@ import { EditorFontLigatures } from '../../../common/config/editorOptions.js'; import { DomReadingContext } from './domReadingContext.js'; import type { ViewLineOptions } from './viewLineOptions.js'; import { ViewGpuContext } from '../../gpu/viewGpuContext.js'; +import { getActiveDocument } from '../../../../base/browser/dom.js'; const canUseFastRenderedViewLine = (function () { if (platform.isNative) { @@ -98,7 +99,8 @@ export class ViewLine implements IVisibleLine { } public renderLine(lineNumber: number, deltaTop: number, lineHeight: number, viewportData: ViewportData, sb: StringBuilder): boolean { - if (this._options.useGpu && ViewGpuContext.canRender(this._options, viewportData, lineNumber)) { + // TODO: How to get the container? + if (this._options.useGpu && ViewGpuContext.canRender(getActiveDocument().querySelector('.view-lines')!, this._options, viewportData, lineNumber)) { this._renderedViewLine?.domNode?.domNode.remove(); this._renderedViewLine = null; return false; diff --git a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts index c4d8c0400d214..cfe4a974bd78f 100644 --- a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts +++ b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts @@ -551,7 +551,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { if (!this._lastViewportData || !this._lastViewLineOptions) { return undefined; } - if (!ViewGpuContext.canRender(this._lastViewLineOptions, this._lastViewportData, lineNumber)) { + if (!ViewGpuContext.canRender(this._viewGpuContext.canvas.domNode, this._lastViewLineOptions, this._lastViewportData, lineNumber)) { return undefined; } @@ -569,7 +569,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { if (!this._lastViewportData || !this._lastViewLineOptions) { return undefined; } - if (!ViewGpuContext.canRender(this._lastViewLineOptions, this._lastViewportData, lineNumber)) { + if (!ViewGpuContext.canRender(this._viewGpuContext.canvas.domNode, this._lastViewLineOptions, this._lastViewportData, lineNumber)) { return undefined; } const lineData = this._lastViewportData.getViewLineRenderingData(lineNumber); From f536f763b2ac08658fd0c7473325e42710083776 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 19 Nov 2024 12:33:24 -0800 Subject: [PATCH 04/20] Apply CSS styles in gpu lines --- .../browser/gpu/fullFileRenderStrategy.ts | 32 ++++++++++++++++--- src/vs/editor/browser/gpu/viewGpuContext.ts | 31 +++++++++--------- 2 files changed, 42 insertions(+), 21 deletions(-) diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index 473e662a37b64..2969cbe23ea53 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -8,7 +8,6 @@ import { BugIndicatingError } from '../../../base/common/errors.js'; import { EditorOption } from '../../common/config/editorOptions.js'; import { CursorColumns } from '../../common/core/cursorColumns.js'; import { MetadataConsts } from '../../common/encodedTokenAttributes.js'; -import { ClassName } from '../../common/model/intervalTree.js'; import type { IViewLineTokens } from '../../common/tokens/lineTokens.js'; import { ViewEventHandler } from '../../common/viewEventHandler.js'; import { ViewEventType, type ViewConfigurationChangedEvent, type ViewDecorationsChangedEvent, type ViewLinesChangedEvent, type ViewLinesDeletedEvent, type ViewLinesInsertedEvent, type ViewScrollChangedEvent, type ViewTokensChangedEvent, type ViewZonesChangedEvent } from '../../common/viewEvents.js'; @@ -351,14 +350,37 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend // Only lines containing fully supported inline decorations should have made it // this far. + const inlineStyles: Map = new Map(); for (const decoration of cellDecorations) { - switch (decoration.options.inlineClassName) { - case (ClassName.EditorDeprecatedInlineDecoration): { - // HACK: We probably shouldn't override tokenMetadata + if (!decoration.options.inlineClassName) { + throw new BugIndicatingError('Unexpected inline decoration without class name'); + } + const rules = ViewGpuContext.decorationCssRuleExtractor.getStyleRules(this._viewGpuContext.canvas.domNode, decoration.options.inlineClassName); + for (const rule of rules) { + for (const r of rule.style) { + inlineStyles.set(r, rule.styleMap.get(r)?.toString() ?? ''); + } + } + } + + for (const [k, v] of inlineStyles.entries()) { + switch (k) { + case 'text-decoration-line': { + // TODO: Don't set tokenMetadata as it applies to more than just this token tokenMetadata |= MetadataConsts.STRIKETHROUGH_MASK; - // chars = '-'; break; } + case 'text-decoration-thickness': + case 'text-decoration-style': + case 'text-decoration-color': { + // HACK: Ignore for now to avoid throwing + break; + } + // case 'color': { + // tokenMetadata |= ... + // break; + // } + default: throw new BugIndicatingError('Unexpected inline decoration style'); } } diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index 70aff96eaaa33..ae4264cb24438 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -46,9 +46,12 @@ export class ViewGpuContext extends Disposable { readonly rectangleRenderer: RectangleRenderer; - private static _atlas: TextureAtlas | undefined; - private static readonly _decorationCssRuleExtractor = new DecorationCssRuleExtractor(); + static get decorationCssRuleExtractor(): DecorationCssRuleExtractor { + return ViewGpuContext._decorationCssRuleExtractor; + } + + private static _atlas: TextureAtlas | undefined; /** * The shared texture atlas to use across all views. @@ -145,15 +148,9 @@ export class ViewGpuContext extends Disposable { let supported = true; for (const decoration of data.inlineDecorations) { const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(container, decoration.inlineClassName); - const supportedCssRules = [ - 'text-decoration-line', - 'text-decoration-thickness', - 'text-decoration-style', - 'text-decoration-color', - ]; supported &&= styleRules.every(rule => { for (const r of rule.style) { - if (!supportedCssRules.includes(r)) { + if (!gpuSupportedCssRules.includes(r)) { return false; } } @@ -189,15 +186,9 @@ export class ViewGpuContext extends Disposable { const problemRules: string[] = []; for (const decoration of data.inlineDecorations) { const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(container, decoration.inlineClassName); - const supportedCssRules = [ - 'text-decoration-line', - 'text-decoration-thickness', - 'text-decoration-style', - 'text-decoration-color', - ]; supported &&= styleRules.every(rule => { for (const r of rule.style) { - if (!supportedCssRules.includes(r)) { + if (!gpuSupportedCssRules.includes(r)) { problemRules.push(r); return false; } @@ -218,3 +209,11 @@ export class ViewGpuContext extends Disposable { return reasons; } } + +const gpuSupportedCssRules = [ + // 'color', + 'text-decoration-line', + 'text-decoration-thickness', + 'text-decoration-style', + 'text-decoration-color', +]; From ee5b89187745d866cf849ff5d2e78e1bb39b5b72 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 19 Nov 2024 13:56:50 -0800 Subject: [PATCH 05/20] Apply style to char metadata, not token --- .../browser/gpu/fullFileRenderStrategy.ts | 22 +++++++++++-------- src/vs/editor/browser/gpu/viewGpuContext.ts | 10 ++++----- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index 2969cbe23ea53..d9f053c3a1a44 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -225,6 +225,8 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend let tokenEndIndex = 0; let tokenMetadata = 0; + let charMetadata = 0; + let lineData: ViewLineRenderingData; let content: string = ''; let fillStartIndex = 0; @@ -331,6 +333,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend break; } chars = content.charAt(x); + charMetadata = tokenMetadata; // TODO: We'd want to optimize pulling the decorations in order // HACK: Temporary replace char to demonstrate inline decorations @@ -363,11 +366,10 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend } } - for (const [k, v] of inlineStyles.entries()) { - switch (k) { + for (const [key, _value] of inlineStyles.entries()) { + switch (key) { case 'text-decoration-line': { - // TODO: Don't set tokenMetadata as it applies to more than just this token - tokenMetadata |= MetadataConsts.STRIKETHROUGH_MASK; + charMetadata |= MetadataConsts.STRIKETHROUGH_MASK; break; } case 'text-decoration-thickness': @@ -376,10 +378,12 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend // HACK: Ignore for now to avoid throwing break; } - // case 'color': { - // tokenMetadata |= ... - // break; - // } + case 'color': { + // HACK: Set color requests to the first token's fg color + charMetadata &= ~MetadataConsts.FOREGROUND_MASK; + charMetadata |= 0b1 << MetadataConsts.FOREGROUND_OFFSET; + break; + } default: throw new BugIndicatingError('Unexpected inline decoration style'); } } @@ -395,7 +399,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend continue; } - glyph = this._viewGpuContext.atlas.getGlyph(this._glyphRasterizer, chars, tokenMetadata); + glyph = this._viewGpuContext.atlas.getGlyph(this._glyphRasterizer, chars, charMetadata); // TODO: Support non-standard character widths absoluteOffsetX = Math.round((x + xOffset) * viewLineOptions.spaceWidth * dpr); diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index ae4264cb24438..162139b3cb82b 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -211,9 +211,9 @@ export class ViewGpuContext extends Disposable { } const gpuSupportedCssRules = [ - // 'color', - 'text-decoration-line', - 'text-decoration-thickness', - 'text-decoration-style', - 'text-decoration-color', + 'color', + // 'text-decoration-line', + // 'text-decoration-thickness', + // 'text-decoration-style', + // 'text-decoration-color', ]; From adaa9f2c51f44bc71372d0d258f1268b20b1d01f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 20 Nov 2024 07:16:16 -0800 Subject: [PATCH 06/20] Start of gpu character metadata concept --- src/vs/editor/browser/gpu/atlas/atlas.ts | 4 +-- .../editor/browser/gpu/atlas/textureAtlas.ts | 34 +++++++++---------- .../browser/gpu/atlas/textureAtlasPage.ts | 16 ++++----- .../browser/gpu/fullFileRenderStrategy.ts | 22 ++++++++---- .../browser/gpu/raster/glyphRasterizer.ts | 19 +++++++---- src/vs/editor/browser/gpu/raster/raster.ts | 16 +++++++-- src/vs/editor/browser/gpu/viewGpuContext.ts | 1 + 7 files changed, 70 insertions(+), 42 deletions(-) diff --git a/src/vs/editor/browser/gpu/atlas/atlas.ts b/src/vs/editor/browser/gpu/atlas/atlas.ts index e971904270525..a8a2fcee9aac1 100644 --- a/src/vs/editor/browser/gpu/atlas/atlas.ts +++ b/src/vs/editor/browser/gpu/atlas/atlas.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { ThreeKeyMap } from '../../../../base/common/map.js'; +import type { FourKeyMap } from '../../../../base/common/map.js'; import type { IBoundingBox, IRasterizedGlyph } from '../raster/raster.js'; /** @@ -92,4 +92,4 @@ export const enum UsagePreviewColors { Restricted = '#FF000088', } -export type GlyphMap = ThreeKeyMap; +export type GlyphMap = FourKeyMap; diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlas.ts b/src/vs/editor/browser/gpu/atlas/textureAtlas.ts index fb9ac44f4a8ec..2d4365da93bbb 100644 --- a/src/vs/editor/browser/gpu/atlas/textureAtlas.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlas.ts @@ -8,7 +8,7 @@ import { CharCode } from '../../../../base/common/charCode.js'; import { BugIndicatingError } from '../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable, dispose, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { ThreeKeyMap } from '../../../../base/common/map.js'; +import { FourKeyMap } from '../../../../base/common/map.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { MetadataConsts } from '../../../common/encodedTokenAttributes.js'; @@ -44,7 +44,7 @@ export class TextureAtlas extends Disposable { * so it is not guaranteed to be the actual page the glyph is on. But it is guaranteed that all * pages with a lower index do not contain the glyph. */ - private readonly _glyphPageIndex: GlyphMap = new ThreeKeyMap(); + private readonly _glyphPageIndex: GlyphMap = new FourKeyMap(); private readonly _onDidDeleteGlyphs = this._register(new Emitter()); readonly onDidDeleteGlyphs = this._onDidDeleteGlyphs.event; @@ -83,7 +83,7 @@ export class TextureAtlas extends Disposable { // cells end up rendering nothing // TODO: This currently means the first slab is for 0x0 glyphs and is wasted const nullRasterizer = new GlyphRasterizer(1, ''); - firstPage.getGlyph(nullRasterizer, '', 0); + firstPage.getGlyph(nullRasterizer, '', 0, 0); nullRasterizer.dispose(); } @@ -104,10 +104,10 @@ export class TextureAtlas extends Disposable { this._onDidDeleteGlyphs.fire(); } - getGlyph(rasterizer: IGlyphRasterizer, chars: string, metadata: number): Readonly { + getGlyph(rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, charMetadata: number): Readonly { // TODO: Encode font size and family into key // Ignore metadata that doesn't affect the glyph - metadata &= ~(MetadataConsts.LANGUAGEID_MASK | MetadataConsts.TOKEN_TYPE_MASK | MetadataConsts.BALANCED_BRACKETS_MASK); + tokenMetadata &= ~(MetadataConsts.LANGUAGEID_MASK | MetadataConsts.TOKEN_TYPE_MASK | MetadataConsts.BALANCED_BRACKETS_MASK); // Warm up common glyphs if (!this._warmedUpRasterizers.has(rasterizer.id)) { @@ -116,25 +116,25 @@ export class TextureAtlas extends Disposable { } // Try get the glyph, overflowing to a new page if necessary - return this._tryGetGlyph(this._glyphPageIndex.get(chars, metadata, rasterizer.cacheKey) ?? 0, rasterizer, chars, metadata); + return this._tryGetGlyph(this._glyphPageIndex.get(chars, tokenMetadata, charMetadata, rasterizer.cacheKey) ?? 0, rasterizer, chars, tokenMetadata, charMetadata); } - private _tryGetGlyph(pageIndex: number, rasterizer: IGlyphRasterizer, chars: string, metadata: number): Readonly { - this._glyphPageIndex.set(chars, metadata, rasterizer.cacheKey, pageIndex); + private _tryGetGlyph(pageIndex: number, rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, charMetadata: number): Readonly { + this._glyphPageIndex.set(chars, tokenMetadata, charMetadata, rasterizer.cacheKey, pageIndex); return ( - this._pages[pageIndex].getGlyph(rasterizer, chars, metadata) + this._pages[pageIndex].getGlyph(rasterizer, chars, tokenMetadata, charMetadata) ?? (pageIndex + 1 < this._pages.length - ? this._tryGetGlyph(pageIndex + 1, rasterizer, chars, metadata) + ? this._tryGetGlyph(pageIndex + 1, rasterizer, chars, tokenMetadata, charMetadata) : undefined) - ?? this._getGlyphFromNewPage(rasterizer, chars, metadata) + ?? this._getGlyphFromNewPage(rasterizer, chars, tokenMetadata, charMetadata) ); } - private _getGlyphFromNewPage(rasterizer: IGlyphRasterizer, chars: string, metadata: number): Readonly { + private _getGlyphFromNewPage(rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, charMetadata: number): Readonly { // TODO: Support more than 2 pages and the GPU texture layer limit this._pages.push(this._instantiationService.createInstance(TextureAtlasPage, this._pages.length, this.pageSize, this._allocatorType)); - this._glyphPageIndex.set(chars, metadata, rasterizer.cacheKey, this._pages.length - 1); - return this._pages[this._pages.length - 1].getGlyph(rasterizer, chars, metadata)!; + this._glyphPageIndex.set(chars, tokenMetadata, charMetadata, rasterizer.cacheKey, this._pages.length - 1); + return this._pages[this._pages.length - 1].getGlyph(rasterizer, chars, tokenMetadata, charMetadata)!; } public getUsagePreview(): Promise { @@ -161,7 +161,7 @@ export class TextureAtlas extends Disposable { for (let code = CharCode.A; code <= CharCode.Z; code++) { taskQueue.enqueue(() => { for (const fgColor of colorMap.keys()) { - this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK); + this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK, 0); } }); } @@ -169,7 +169,7 @@ export class TextureAtlas extends Disposable { for (let code = CharCode.a; code <= CharCode.z; code++) { taskQueue.enqueue(() => { for (const fgColor of colorMap.keys()) { - this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK); + this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK, 0); } }); } @@ -177,7 +177,7 @@ export class TextureAtlas extends Disposable { for (let code = CharCode.ExclamationMark; code <= CharCode.Tilde; code++) { taskQueue.enqueue(() => { for (const fgColor of colorMap.keys()) { - this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK); + this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK, 0); } }); } diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts b/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts index 01edf66913051..cbda9352a3bc8 100644 --- a/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { ThreeKeyMap } from '../../../../base/common/map.js'; +import { FourKeyMap } from '../../../../base/common/map.js'; import { ILogService, LogLevel } from '../../../../platform/log/common/log.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import type { IBoundingBox, IGlyphRasterizer } from '../raster/raster.js'; @@ -31,7 +31,7 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla private readonly _canvas: OffscreenCanvas; get source(): OffscreenCanvas { return this._canvas; } - private readonly _glyphMap: GlyphMap = new ThreeKeyMap(); + private readonly _glyphMap: GlyphMap = new FourKeyMap(); private readonly _glyphInOrderSet: Set = new Set(); get glyphs(): IterableIterator { return this._glyphInOrderSet.values(); @@ -65,20 +65,20 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla })); } - public getGlyph(rasterizer: IGlyphRasterizer, chars: string, metadata: number): Readonly | undefined { + public getGlyph(rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, charMetadata: number): Readonly | undefined { // IMPORTANT: There are intentionally no intermediate variables here to aid in runtime // optimization as it's a very hot function - return this._glyphMap.get(chars, metadata, rasterizer.cacheKey) ?? this._createGlyph(rasterizer, chars, metadata); + return this._glyphMap.get(chars, tokenMetadata, charMetadata, rasterizer.cacheKey) ?? this._createGlyph(rasterizer, chars, tokenMetadata, charMetadata); } - private _createGlyph(rasterizer: IGlyphRasterizer, chars: string, metadata: number): Readonly | undefined { + private _createGlyph(rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, charMetadata: number): Readonly | undefined { // Ensure the glyph can fit on the page if (this._glyphInOrderSet.size >= TextureAtlasPage.maximumGlyphCount) { return undefined; } // Rasterize and allocate the glyph - const rasterizedGlyph = rasterizer.rasterizeGlyph(chars, metadata, this._colorMap); + const rasterizedGlyph = rasterizer.rasterizeGlyph(chars, tokenMetadata, charMetadata, this._colorMap); const glyph = this._allocator.allocate(rasterizedGlyph); // Ensure the glyph was allocated @@ -89,7 +89,7 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla } // Save the glyph - this._glyphMap.set(chars, metadata, rasterizer.cacheKey, glyph); + this._glyphMap.set(chars, tokenMetadata, charMetadata, rasterizer.cacheKey, glyph); this._glyphInOrderSet.add(glyph); // Update page version and it's tracked used area @@ -100,7 +100,7 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla if (this._logService.getLevel() === LogLevel.Trace) { this._logService.trace('New glyph', { chars, - metadata, + metadata: tokenMetadata, rasterizedGlyph, glyph }); diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index d9f053c3a1a44..80bfb679b23a7 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -22,6 +22,7 @@ import { GPULifecycle } from './gpuDisposable.js'; import { quadVertices } from './gpuUtils.js'; import { GlyphRasterizer } from './raster/glyphRasterizer.js'; import { ViewGpuContext } from './viewGpuContext.js'; +import { GpuCharMetadata } from './raster/raster.js'; const enum Constants { @@ -333,7 +334,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend break; } chars = content.charAt(x); - charMetadata = tokenMetadata; + charMetadata = 0; // TODO: We'd want to optimize pulling the decorations in order // HACK: Temporary replace char to demonstrate inline decorations @@ -366,7 +367,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend } } - for (const [key, _value] of inlineStyles.entries()) { + for (const [key, value] of inlineStyles.entries()) { switch (key) { case 'text-decoration-line': { charMetadata |= MetadataConsts.STRIKETHROUGH_MASK; @@ -379,9 +380,18 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend break; } case 'color': { - // HACK: Set color requests to the first token's fg color - charMetadata &= ~MetadataConsts.FOREGROUND_MASK; - charMetadata |= 0b1 << MetadataConsts.FOREGROUND_OFFSET; + // TODO: Move to color.ts and make more generic + function parseRgb(text: string): number { + const color = text.match(/rgb\((\d+), (\d+), (\d+)\)/); + if (!color) { + throw new Error('Invalid color format'); + } + const r = parseInt(color[1], 10); + const g = parseInt(color[2], 10); + const b = parseInt(color[3], 10); + return r << 16 | g << 8 | b; + } + charMetadata = ((parseRgb(value) << GpuCharMetadata.FOREGROUND_OFFSET) & GpuCharMetadata.FOREGROUND_MASK) >>> 0; break; } default: throw new BugIndicatingError('Unexpected inline decoration style'); @@ -399,7 +409,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend continue; } - glyph = this._viewGpuContext.atlas.getGlyph(this._glyphRasterizer, chars, charMetadata); + glyph = this._viewGpuContext.atlas.getGlyph(this._glyphRasterizer, chars, tokenMetadata, charMetadata); // TODO: Support non-standard character widths absoluteOffsetX = Math.round((x + xOffset) * viewLineOptions.spaceWidth * dpr); diff --git a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts index 07d343bb677c4..0c1c5db102e2c 100644 --- a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts @@ -9,7 +9,7 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { StringBuilder } from '../../../common/core/stringBuilder.js'; import { FontStyle, TokenMetadata } from '../../../common/encodedTokenAttributes.js'; import { ensureNonNullable } from '../gpuUtils.js'; -import type { IBoundingBox, IGlyphRasterizer, IRasterizedGlyph } from './raster.js'; +import { GpuCharMetadata, type IBoundingBox, type IGlyphRasterizer, type IRasterizedGlyph } from './raster.js'; let nextId = 0; @@ -61,7 +61,8 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { */ public rasterizeGlyph( chars: string, - metadata: number, + tokenMetadata: number, + charMetadata: number, colorMap: string[], ): Readonly { if (chars === '') { @@ -74,17 +75,18 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { // Check if the last glyph matches the config, reuse if so. This helps avoid unnecessary // work when the rasterizer is called multiple times like when the glyph doesn't fit into a // page. - if (this._workGlyphConfig.chars === chars && this._workGlyphConfig.metadata === metadata) { + if (this._workGlyphConfig.chars === chars && this._workGlyphConfig.metadata === tokenMetadata) { return this._workGlyph; } this._workGlyphConfig.chars = chars; - this._workGlyphConfig.metadata = metadata; - return this._rasterizeGlyph(chars, metadata, colorMap); + this._workGlyphConfig.metadata = tokenMetadata; + return this._rasterizeGlyph(chars, tokenMetadata, charMetadata, colorMap); } public _rasterizeGlyph( chars: string, metadata: number, + charMetadata: number, colorMap: string[], ): Readonly { const devicePixelFontSize = Math.ceil(this._fontSize * getActiveWindow().devicePixelRatio); @@ -114,7 +116,12 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { const originX = devicePixelFontSize; const originY = devicePixelFontSize; - this._ctx.fillStyle = colorMap[TokenMetadata.getForeground(metadata)]; + if (charMetadata) { + const fg = (charMetadata & GpuCharMetadata.FOREGROUND_MASK) >> GpuCharMetadata.FOREGROUND_OFFSET; + this._ctx.fillStyle = `#${fg.toString(16).padStart(6, '0')}`; + } else { + this._ctx.fillStyle = colorMap[TokenMetadata.getForeground(metadata)]; + } // TODO: This might actually be slower // const textMetrics = this._ctx.measureText(chars); this._ctx.textBaseline = 'top'; diff --git a/src/vs/editor/browser/gpu/raster/raster.ts b/src/vs/editor/browser/gpu/raster/raster.ts index 6eb41e680b299..d83b55665dbf5 100644 --- a/src/vs/editor/browser/gpu/raster/raster.ts +++ b/src/vs/editor/browser/gpu/raster/raster.ts @@ -21,13 +21,15 @@ export interface IGlyphRasterizer { * Rasterizes a glyph. * @param chars The character(s) to rasterize. This can be a single character, a ligature, an * emoji, etc. - * @param metadata The metadata of the glyph to rasterize. See {@link MetadataConsts} for how - * this works. + * @param tokenMetadata The token metadata of the glyph to rasterize. See {@link MetadataConsts} + * for how this works. + * @param charMetadata The chracter metadata of the glyph to rasterize. * @param colorMap A theme's color map. */ rasterizeGlyph( chars: string, - metadata: number, + tokenMetadata: number, + charMetadata: number, colorMap: string[], ): Readonly; } @@ -63,3 +65,11 @@ export interface IRasterizedGlyph { */ originOffset: { x: number; y: number }; } + +export const enum GpuCharMetadata { + FOREGROUND_MASK /* */ = 0b00000000_11111111_11111111_11111111, + OPACITY_MASK /* */ = 0b11111111_00000000_00000000_00000000, + + FOREGROUND_OFFSET = 0, + OPACITY_OFFSET = 24, +} diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index 162139b3cb82b..fdd4aa9f5efcf 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -150,6 +150,7 @@ export class ViewGpuContext extends Disposable { const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(container, decoration.inlineClassName); supported &&= styleRules.every(rule => { for (const r of rule.style) { + // TODO: Consider pseudo classes when checking for support if (!gpuSupportedCssRules.includes(r)) { return false; } From d3c1d2e831afd9ab4636bb0ee2cebe9fd60bf990 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 20 Nov 2024 09:55:57 -0800 Subject: [PATCH 07/20] Basic handling of alpha channel, ignore for now --- src/vs/editor/browser/gpu/fullFileRenderStrategy.ts | 7 ++----- src/vs/editor/contrib/gpu/browser/gpuActions.ts | 7 ++++--- .../test/browser/view/gpu/atlas/textureAtlas.test.ts | 7 ++++--- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index 80bfb679b23a7..e3de4735d31fc 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -308,9 +308,6 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend e.range.startLineNumber <= y && e.range.endLineNumber >= y && e.options.inlineClassName )); - if (inlineDecorations.length > 0) { - console.log('decoration!', inlineDecorations); - } lineData = viewportData.getViewLineRenderingData(y); content = lineData.content; @@ -382,9 +379,9 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend case 'color': { // TODO: Move to color.ts and make more generic function parseRgb(text: string): number { - const color = text.match(/rgb\((\d+), (\d+), (\d+)\)/); + const color = text.match(/rgba?\((\d+), ?(\d+), ?(\d+)(?:, ?\d+(?:.\d+)?)?\)/); if (!color) { - throw new Error('Invalid color format'); + throw new Error('Invalid color format ' + text); } const r = parseInt(color[1], 10); const g = parseInt(color[2], 10); diff --git a/src/vs/editor/contrib/gpu/browser/gpuActions.ts b/src/vs/editor/contrib/gpu/browser/gpuActions.ts index 29de263652703..f64ac1c738047 100644 --- a/src/vs/editor/contrib/gpu/browser/gpuActions.ts +++ b/src/vs/editor/contrib/gpu/browser/gpuActions.ts @@ -115,8 +115,9 @@ class DebugEditorGpuRendererAction extends EditorAction { if (codePoint !== undefined) { chars = String.fromCodePoint(parseInt(codePoint, 16)); } - const metadata = 0; - const rasterizedGlyph = atlas.getGlyph(rasterizer, chars, metadata); + const tokenMetadata = 0; + const charMetadata = 0; + const rasterizedGlyph = atlas.getGlyph(rasterizer, chars, tokenMetadata, charMetadata); if (!rasterizedGlyph) { return; } @@ -133,7 +134,7 @@ class DebugEditorGpuRendererAction extends EditorAction { const ctx = ensureNonNullable(canvas.getContext('2d')); ctx.putImageData(imageData, 0, 0); const blob = await canvas.convertToBlob({ type: 'image/png' }); - const resource = URI.joinPath(folders[0].uri, `glyph_${chars}_${metadata}_${fontSize}px_${fontFamily.replaceAll(/[,\\\/\.'\s]/g, '_')}.png`); + const resource = URI.joinPath(folders[0].uri, `glyph_${chars}_${tokenMetadata}_${fontSize}px_${fontFamily.replaceAll(/[,\\\/\.'\s]/g, '_')}.png`); await fileService.writeFile(resource, VSBuffer.wrap(new Uint8Array(await blob.arrayBuffer()))); }); break; diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts index 5a4353a1d19b4..3445beee97794 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts +++ b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts @@ -14,15 +14,16 @@ import { assertIsValidGlyph } from './testUtil.js'; import { TextureAtlasSlabAllocator } from '../../../../../browser/gpu/atlas/textureAtlasSlabAllocator.js'; const blackInt = 0x000000FF; +const nullCharMetadata = 0x0; let lastUniqueGlyph: string | undefined; -function getUniqueGlyphId(): [chars: string, tokenFg: number] { +function getUniqueGlyphId(): [chars: string, tokenMetadata: number, charMetadata: number] { if (!lastUniqueGlyph) { lastUniqueGlyph = 'a'; } else { lastUniqueGlyph = String.fromCharCode(lastUniqueGlyph.charCodeAt(0) + 1); } - return [lastUniqueGlyph, blackInt]; + return [lastUniqueGlyph, blackInt, nullCharMetadata]; } class TestGlyphRasterizer implements IGlyphRasterizer { @@ -30,7 +31,7 @@ class TestGlyphRasterizer implements IGlyphRasterizer { readonly cacheKey = ''; nextGlyphColor: [number, number, number, number] = [0, 0, 0, 0]; nextGlyphDimensions: [number, number] = [0, 0]; - rasterizeGlyph(chars: string, metadata: number, colorMap: string[]): Readonly { + rasterizeGlyph(chars: string, tokenMetadata: number, charMetadata: number, colorMap: string[]): Readonly { const w = this.nextGlyphDimensions[0]; const h = this.nextGlyphDimensions[1]; if (w === 0 || h === 0) { From 37e6cb3300061f0b7481011e60a459e917b6dd5a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 20 Nov 2024 10:09:52 -0800 Subject: [PATCH 08/20] Clean up DecorationCssRulerExtractor DOM nodes This was causing mouse event request paths to get messed up --- src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts b/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts index 5959bd36cd3e6..d76761e4c1ec5 100644 --- a/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts +++ b/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts @@ -25,14 +25,18 @@ export class DecorationCssRuleExtractor extends Disposable { } const dummyElement = $(`span.${decorationClassName}`); this._container.appendChild(dummyElement); + canvas.appendChild(this._container); + const rules = this._getStyleRules(canvas, dummyElement, decorationClassName); this._ruleCache.set(decorationClassName, rules); + + canvas.removeChild(this._container); + this._container.removeChild(dummyElement); + return rules; } private _getStyleRules(canvas: HTMLElement, element: HTMLElement, className: string) { - canvas.appendChild(this._container); - // Iterate through all stylesheets and imported stylesheets to find matching rules const rules = []; const doc = getActiveDocument(); From 1f900684f6304d1b1e50d8f7f6d2113a29aa152d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 20 Nov 2024 10:22:48 -0800 Subject: [PATCH 09/20] Move CSS out and improve lifecycle of DecorationCssRuleExtractor --- .../browser/gpu/decorationCssRuleExtractor.ts | 28 +++++++++++++------ .../gpu/media/decorationCssRuleExtractor.css | 9 ++++++ 2 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 src/vs/editor/browser/gpu/media/decorationCssRuleExtractor.css diff --git a/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts b/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts index d76761e4c1ec5..27b52a3e54381 100644 --- a/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts +++ b/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts @@ -5,38 +5,50 @@ import { $, getActiveDocument } from '../../../base/browser/dom.js'; import { Disposable, toDisposable } from '../../../base/common/lifecycle.js'; +import './media/decorationCssRuleExtractor.css'; +/** + * Extracts CSS rules that would be applied to certain decoration classes. + */ export class DecorationCssRuleExtractor extends Disposable { private _container: HTMLElement; + private _dummyElement: HTMLSpanElement; private _ruleCache: Map = new Map(); constructor() { super(); - this._container = $('div.monaco-css-rule-extractor'); - this._container.style.visibility = 'hidden'; + + this._container = $('div.monaco-decoration-css-rule-extractor'); + this._dummyElement = $('span'); + this._container.appendChild(this._dummyElement); + this._register(toDisposable(() => this._container.remove())); } getStyleRules(canvas: HTMLElement, decorationClassName: string): CSSStyleRule[] { + // Check cache const existing = this._ruleCache.get(decorationClassName); if (existing) { return existing; } - const dummyElement = $(`span.${decorationClassName}`); - this._container.appendChild(dummyElement); + + // Set up DOM + this._dummyElement.classList.add(decorationClassName); canvas.appendChild(this._container); - const rules = this._getStyleRules(canvas, dummyElement, decorationClassName); + // Get rules + const rules = this._getStyleRules(decorationClassName); this._ruleCache.set(decorationClassName, rules); + // Tear down DOM canvas.removeChild(this._container); - this._container.removeChild(dummyElement); + this._dummyElement.classList.remove(decorationClassName); return rules; } - private _getStyleRules(canvas: HTMLElement, element: HTMLElement, className: string) { + private _getStyleRules(className: string) { // Iterate through all stylesheets and imported stylesheets to find matching rules const rules = []; const doc = getActiveDocument(); @@ -49,7 +61,7 @@ export class DecorationCssRuleExtractor extends Disposable { stylesheets.push(rule.styleSheet); } } else if (rule instanceof CSSStyleRule) { - if (element.matches(rule.selectorText) && rule.selectorText.includes(`.${className}`)) { + if (this._dummyElement.matches(rule.selectorText) && rule.selectorText.includes(`.${className}`)) { rules.push(rule); } } diff --git a/src/vs/editor/browser/gpu/media/decorationCssRuleExtractor.css b/src/vs/editor/browser/gpu/media/decorationCssRuleExtractor.css new file mode 100644 index 0000000000000..900154c64fdf8 --- /dev/null +++ b/src/vs/editor/browser/gpu/media/decorationCssRuleExtractor.css @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-editor .monaco-decoration-css-rule-extractor { + visibility: hidden; + pointer-events: none; +} From 3489890a810a8aee40ac77c31dc913b30a64e2ed Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 20 Nov 2024 10:24:57 -0800 Subject: [PATCH 10/20] Move gpu/ test folder into correct new place --- .../test/browser/{view => }/gpu/atlas/testUtil.ts | 8 ++++---- .../{view => }/gpu/atlas/textureAtlas.test.ts | 14 +++++++------- .../gpu/atlas/textureAtlasAllocator.test.ts | 14 +++++++------- .../{view => }/gpu/bufferDirtyTracker.test.ts | 4 ++-- .../{view => }/gpu/objectCollectionBuffer.test.ts | 4 ++-- 5 files changed, 22 insertions(+), 22 deletions(-) rename src/vs/editor/test/browser/{view => }/gpu/atlas/testUtil.ts (86%) rename src/vs/editor/test/browser/{view => }/gpu/atlas/textureAtlas.test.ts (90%) rename src/vs/editor/test/browser/{view => }/gpu/atlas/textureAtlasAllocator.test.ts (92%) rename src/vs/editor/test/browser/{view => }/gpu/bufferDirtyTracker.test.ts (91%) rename src/vs/editor/test/browser/{view => }/gpu/objectCollectionBuffer.test.ts (97%) diff --git a/src/vs/editor/test/browser/view/gpu/atlas/testUtil.ts b/src/vs/editor/test/browser/gpu/atlas/testUtil.ts similarity index 86% rename from src/vs/editor/test/browser/view/gpu/atlas/testUtil.ts rename to src/vs/editor/test/browser/gpu/atlas/testUtil.ts index 73d1e167f1e15..ddc3f1583a064 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/testUtil.ts +++ b/src/vs/editor/test/browser/gpu/atlas/testUtil.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { fail, ok } from 'assert'; -import type { ITextureAtlasPageGlyph } from '../../../../../browser/gpu/atlas/atlas.js'; -import { TextureAtlas } from '../../../../../browser/gpu/atlas/textureAtlas.js'; -import { isNumber } from '../../../../../../base/common/types.js'; -import { ensureNonNullable } from '../../../../../browser/gpu/gpuUtils.js'; +import type { ITextureAtlasPageGlyph } from '../../../../browser/gpu/atlas/atlas.js'; +import { TextureAtlas } from '../../../../browser/gpu/atlas/textureAtlas.js'; +import { isNumber } from '../../../../../base/common/types.js'; +import { ensureNonNullable } from '../../../../browser/gpu/gpuUtils.js'; export function assertIsValidGlyph(glyph: Readonly | undefined, atlasOrSource: TextureAtlas | OffscreenCanvas) { if (glyph === undefined) { diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts b/src/vs/editor/test/browser/gpu/atlas/textureAtlas.test.ts similarity index 90% rename from src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts rename to src/vs/editor/test/browser/gpu/atlas/textureAtlas.test.ts index 3445beee97794..0610f84eb3e5e 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts +++ b/src/vs/editor/test/browser/gpu/atlas/textureAtlas.test.ts @@ -4,14 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { strictEqual, throws } from 'assert'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; -import type { IGlyphRasterizer, IRasterizedGlyph } from '../../../../../browser/gpu/raster/raster.js'; -import { ensureNonNullable } from '../../../../../browser/gpu/gpuUtils.js'; -import type { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; -import { TextureAtlas } from '../../../../../browser/gpu/atlas/textureAtlas.js'; -import { createCodeEditorServices } from '../../../testCodeEditor.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import type { IGlyphRasterizer, IRasterizedGlyph } from '../../../../browser/gpu/raster/raster.js'; +import { ensureNonNullable } from '../../../../browser/gpu/gpuUtils.js'; +import type { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { TextureAtlas } from '../../../../browser/gpu/atlas/textureAtlas.js'; +import { createCodeEditorServices } from '../../testCodeEditor.js'; import { assertIsValidGlyph } from './testUtil.js'; -import { TextureAtlasSlabAllocator } from '../../../../../browser/gpu/atlas/textureAtlasSlabAllocator.js'; +import { TextureAtlasSlabAllocator } from '../../../../browser/gpu/atlas/textureAtlasSlabAllocator.js'; const blackInt = 0x000000FF; const nullCharMetadata = 0x0; diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts b/src/vs/editor/test/browser/gpu/atlas/textureAtlasAllocator.test.ts similarity index 92% rename from src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts rename to src/vs/editor/test/browser/gpu/atlas/textureAtlasAllocator.test.ts index 92d354a5f789d..377bb752df8a0 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts +++ b/src/vs/editor/test/browser/gpu/atlas/textureAtlasAllocator.test.ts @@ -4,14 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { deepStrictEqual, strictEqual, throws } from 'assert'; -import type { IRasterizedGlyph } from '../../../../../browser/gpu/raster/raster.js'; -import { ensureNonNullable } from '../../../../../browser/gpu/gpuUtils.js'; -import type { ITextureAtlasAllocator } from '../../../../../browser/gpu/atlas/atlas.js'; -import { TextureAtlasShelfAllocator } from '../../../../../browser/gpu/atlas/textureAtlasShelfAllocator.js'; -import { TextureAtlasSlabAllocator, type TextureAtlasSlabAllocatorOptions } from '../../../../../browser/gpu/atlas/textureAtlasSlabAllocator.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import type { IRasterizedGlyph } from '../../../../browser/gpu/raster/raster.js'; +import { ensureNonNullable } from '../../../../browser/gpu/gpuUtils.js'; +import type { ITextureAtlasAllocator } from '../../../../browser/gpu/atlas/atlas.js'; +import { TextureAtlasShelfAllocator } from '../../../../browser/gpu/atlas/textureAtlasShelfAllocator.js'; +import { TextureAtlasSlabAllocator, type TextureAtlasSlabAllocatorOptions } from '../../../../browser/gpu/atlas/textureAtlasSlabAllocator.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { assertIsValidGlyph } from './testUtil.js'; -import { BugIndicatingError } from '../../../../../../base/common/errors.js'; +import { BugIndicatingError } from '../../../../../base/common/errors.js'; const blackArr = [0x00, 0x00, 0x00, 0xFF]; diff --git a/src/vs/editor/test/browser/view/gpu/bufferDirtyTracker.test.ts b/src/vs/editor/test/browser/gpu/bufferDirtyTracker.test.ts similarity index 91% rename from src/vs/editor/test/browser/view/gpu/bufferDirtyTracker.test.ts rename to src/vs/editor/test/browser/gpu/bufferDirtyTracker.test.ts index 0ddc7a5befeff..0794961644e0a 100644 --- a/src/vs/editor/test/browser/view/gpu/bufferDirtyTracker.test.ts +++ b/src/vs/editor/test/browser/gpu/bufferDirtyTracker.test.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { strictEqual } from 'assert'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { BufferDirtyTracker } from '../../../../browser/gpu/bufferDirtyTracker.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { BufferDirtyTracker } from '../../../browser/gpu/bufferDirtyTracker.js'; suite('BufferDirtyTracker', () => { ensureNoDisposablesAreLeakedInTestSuite(); diff --git a/src/vs/editor/test/browser/view/gpu/objectCollectionBuffer.test.ts b/src/vs/editor/test/browser/gpu/objectCollectionBuffer.test.ts similarity index 97% rename from src/vs/editor/test/browser/view/gpu/objectCollectionBuffer.test.ts rename to src/vs/editor/test/browser/gpu/objectCollectionBuffer.test.ts index ae8c0670d2b8b..52ca0b7619f39 100644 --- a/src/vs/editor/test/browser/view/gpu/objectCollectionBuffer.test.ts +++ b/src/vs/editor/test/browser/gpu/objectCollectionBuffer.test.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { deepStrictEqual, strictEqual } from 'assert'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { createObjectCollectionBuffer, type IObjectCollectionBuffer } from '../../../../browser/gpu/objectCollectionBuffer.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { createObjectCollectionBuffer, type IObjectCollectionBuffer } from '../../../browser/gpu/objectCollectionBuffer.js'; suite('ObjectCollectionBuffer', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); From 01e1c7c01fb267d5b0a95688dd6b092afe839fcf Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 20 Nov 2024 10:46:08 -0800 Subject: [PATCH 11/20] Add unit tests for DecorationCssRuleExtractor --- .../browser/gpu/decorationCssRuleExtractor.ts | 5 +- .../gpu/decorationCssRulerExtractor.test.ts | 83 +++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 src/vs/editor/test/browser/gpu/decorationCssRulerExtractor.test.ts diff --git a/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts b/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts index 27b52a3e54381..c05f2d63418d4 100644 --- a/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts +++ b/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts @@ -61,7 +61,10 @@ export class DecorationCssRuleExtractor extends Disposable { stylesheets.push(rule.styleSheet); } } else if (rule instanceof CSSStyleRule) { - if (this._dummyElement.matches(rule.selectorText) && rule.selectorText.includes(`.${className}`)) { + // Note that originally `.matches(rule.selectorText)` was used but this would + // not pick up pseudo-classes which are important to determine support of the + // returned styles. + if (rule.selectorText.includes(`.${className}`)) { rules.push(rule); } } diff --git a/src/vs/editor/test/browser/gpu/decorationCssRulerExtractor.test.ts b/src/vs/editor/test/browser/gpu/decorationCssRulerExtractor.test.ts new file mode 100644 index 0000000000000..ddeffcdb82909 --- /dev/null +++ b/src/vs/editor/test/browser/gpu/decorationCssRulerExtractor.test.ts @@ -0,0 +1,83 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { deepStrictEqual } from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { DecorationCssRuleExtractor } from '../../../browser/gpu/decorationCssRuleExtractor.js'; +import { generateUuid } from '../../../../base/common/uuid.js'; +import { $, getActiveDocument } from '../../../../base/browser/dom.js'; + +function randomClass(): string { + return 'test-class-' + generateUuid(); +} + +suite('DecorationCssRulerExtractor', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + let doc: Document; + let container: HTMLElement; + let extractor: DecorationCssRuleExtractor; + let testClassName: string; + + function addStyleElement(content: string): void { + const styleElement = $('style'); + styleElement.textContent = content; + container.append(styleElement); + } + + function assertStyles(className: string, expectedCssText: string[]): void { + deepStrictEqual(extractor.getStyleRules(container, className).map(e => e.cssText), expectedCssText); + } + + setup(() => { + doc = getActiveDocument(); + extractor = store.add(new DecorationCssRuleExtractor()); + testClassName = randomClass(); + container = $('div'); + doc.body.append(container); + }); + + teardown(() => { + container.remove(); + }); + + test('unknown class should give no styles', () => { + assertStyles(randomClass(), []); + }); + + test('single style should be picked up', () => { + addStyleElement(`.${testClassName} { color: red; }`); + assertStyles(testClassName, [ + `.${testClassName} { color: red; }` + ]); + }); + + test('multiple styles from the same selector should be picked up', () => { + addStyleElement(`.${testClassName} { color: red; opacity: 0.5; }`); + assertStyles(testClassName, [ + `.${testClassName} { color: red; opacity: 0.5; }` + ]); + }); + + test('multiple styles from different selectors should be picked up', () => { + addStyleElement([ + `.${testClassName} { color: red; opacity: 0.5; }`, + `.${testClassName}:hover { opacity: 1; }`, + ].join('\n')); + assertStyles(testClassName, [ + `.${testClassName} { color: red; opacity: 0.5; }`, + `.${testClassName}:hover { opacity: 1; }`, + ]); + }); + + test('multiple styles from the different stylesheets should be picked up', () => { + addStyleElement(`.${testClassName} { color: red; opacity: 0.5; }`); + addStyleElement(`.${testClassName}:hover { opacity: 1; }`); + assertStyles(testClassName, [ + `.${testClassName} { color: red; opacity: 0.5; }`, + `.${testClassName}:hover { opacity: 1; }`, + ]); + }); +}); From 46abc8b541003676268bf477e07576a50d0ad271 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 20 Nov 2024 10:54:40 -0800 Subject: [PATCH 12/20] Don't support lines with pseudo classes --- src/vs/editor/browser/gpu/viewGpuContext.ts | 29 ++++++++++++++------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index 2da2c770ffca6..883c75d75bdd6 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -160,9 +160,12 @@ export class ViewGpuContext extends Disposable { for (const decoration of data.inlineDecorations) { const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(container, decoration.inlineClassName); supported &&= styleRules.every(rule => { + // Pseudo classes aren't supported currently + if (rule.selectorText.includes(':')) { + return false; + } for (const r of rule.style) { - // TODO: Consider pseudo classes when checking for support - if (!gpuSupportedCssRules.includes(r)) { + if (!gpuSupportedDecorationCssRules.includes(r)) { return false; } } @@ -179,7 +182,7 @@ export class ViewGpuContext extends Disposable { } /** - * Like {@link canRender} but returned detailed information about why the line cannot be rendered. + * Like {@link canRender} but returns detailed information about why the line cannot be rendered. */ public static canRenderDetailed(container: HTMLElement, options: ViewLineOptions, viewportData: ViewportData, lineNumber: number): string[] { const data = viewportData.getViewLineRenderingData(lineNumber); @@ -195,12 +198,18 @@ export class ViewGpuContext extends Disposable { } if (data.inlineDecorations.length > 0) { let supported = true; + const problemSelectors: string[] = []; const problemRules: string[] = []; for (const decoration of data.inlineDecorations) { const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(container, decoration.inlineClassName); supported &&= styleRules.every(rule => { + // Pseudo classes aren't supported currently + if (rule.selectorText.includes(':')) { + problemSelectors.push(rule.selectorText); + return false; + } for (const r of rule.style) { - if (!gpuSupportedCssRules.includes(r)) { + if (!gpuSupportedDecorationCssRules.includes(r)) { problemRules.push(r); return false; } @@ -214,6 +223,9 @@ export class ViewGpuContext extends Disposable { if (problemRules.length > 0) { reasons.push(`inlineDecorations with unsupported CSS rules (\`${problemRules.join(', ')}\`)`); } + if (problemSelectors.length > 0) { + reasons.push(`inlineDecorations with unsupported CSS selectors (\`${problemSelectors.join(', ')}\`)`); + } } if (lineNumber >= GpuRenderLimits.maxGpuLines) { reasons.push('lineNumber >= maxGpuLines'); @@ -222,10 +234,9 @@ export class ViewGpuContext extends Disposable { } } -const gpuSupportedCssRules = [ +/** + * A list of fully supported decoration CSS rules that can be used in the GPU renderer. + */ +const gpuSupportedDecorationCssRules = [ 'color', - // 'text-decoration-line', - // 'text-decoration-thickness', - // 'text-decoration-style', - // 'text-decoration-color', ]; From f87274f213dec5bd4aaf5872b2302017ae6ca7bd Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 20 Nov 2024 11:48:55 -0800 Subject: [PATCH 13/20] Move general parse function to color.ts and add some tests --- src/vs/base/common/color.ts | 37 +++++ src/vs/base/test/common/color.test.ts | 136 ++++++++++++++++++ .../browser/gpu/fullFileRenderStrategy.ts | 22 +-- 3 files changed, 184 insertions(+), 11 deletions(-) diff --git a/src/vs/base/common/color.ts b/src/vs/base/common/color.ts index caa1d4419f007..a4faedd349956 100644 --- a/src/vs/base/common/color.ts +++ b/src/vs/base/common/color.ts @@ -630,6 +630,43 @@ export namespace Color { return Color.Format.CSS.formatRGBA(color); } + /** + * Parse a CSS color and return a {@link Color}. + * @param css The CSS color to parse. + * @see https://drafts.csswg.org/css-color/#typedef-color + */ + export function parse(css: string): Color | null { + if (css === 'transparent') { + return Color.transparent; + } + if (css.startsWith('#')) { + return parseHex(css); + } + if (css.startsWith('rgba(')) { + const color = css.match(/rgba\((?(?:\+|-)?\d+), *(?(?:\+|-)?\d+), *(?(?:\+|-)?\d+), *(?(?:\+|-)?\d+(\.\d+)?)\)/); + if (!color) { + throw new Error('Invalid color format ' + css); + } + const r = parseInt(color.groups?.r ?? '0'); + const g = parseInt(color.groups?.g ?? '0'); + const b = parseInt(color.groups?.b ?? '0'); + const a = parseFloat(color.groups?.a ?? '0'); + return new Color(new RGBA(r, g, b, a)); + } + if (css.startsWith('rgb(')) { + const color = css.match(/rgb\((?(?:\+|-)?\d+), *(?(?:\+|-)?\d+), *(?(?:\+|-)?\d+)\)/); + if (!color) { + throw new Error('Invalid color format ' + css); + } + const r = parseInt(color.groups?.r ?? '0'); + const g = parseInt(color.groups?.g ?? '0'); + const b = parseInt(color.groups?.b ?? '0'); + return new Color(new RGBA(r, g, b)); + } + // TODO: Support more formats + return null; + } + /** * Converts an Hex color value to a Color. * returns r, g, and b are contained in the set [0, 255] diff --git a/src/vs/base/test/common/color.test.ts b/src/vs/base/test/common/color.test.ts index 0f6c1a689cf84..3ae8353bcdff3 100644 --- a/src/vs/base/test/common/color.test.ts +++ b/src/vs/base/test/common/color.test.ts @@ -204,6 +204,142 @@ suite('Color', () => { suite('Format', () => { suite('CSS', () => { + suite('parse', () => { + test('invalid', () => { + assert.deepStrictEqual(Color.Format.CSS.parse(''), null); + assert.deepStrictEqual(Color.Format.CSS.parse('#'), null); + assert.deepStrictEqual(Color.Format.CSS.parse('#0102030'), null); + }); + test('transparent', () => { + assert.deepStrictEqual(Color.Format.CSS.parse('transparent'), new Color(new RGBA(0, 0, 0, 0))); + }); + test('hex-color', () => { + // somewhat valid + assert.deepStrictEqual(Color.Format.CSS.parse('#FFFFG0')!.rgba, new RGBA(255, 255, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#FFFFg0')!.rgba, new RGBA(255, 255, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#-FFF00')!.rgba, new RGBA(15, 255, 0, 1)); + + // valid + assert.deepStrictEqual(Color.Format.CSS.parse('#000000')!.rgba, new RGBA(0, 0, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#FFFFFF')!.rgba, new RGBA(255, 255, 255, 1)); + + assert.deepStrictEqual(Color.Format.CSS.parse('#FF0000')!.rgba, new RGBA(255, 0, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#00FF00')!.rgba, new RGBA(0, 255, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#0000FF')!.rgba, new RGBA(0, 0, 255, 1)); + + assert.deepStrictEqual(Color.Format.CSS.parse('#FFFF00')!.rgba, new RGBA(255, 255, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#00FFFF')!.rgba, new RGBA(0, 255, 255, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#FF00FF')!.rgba, new RGBA(255, 0, 255, 1)); + + assert.deepStrictEqual(Color.Format.CSS.parse('#C0C0C0')!.rgba, new RGBA(192, 192, 192, 1)); + + assert.deepStrictEqual(Color.Format.CSS.parse('#808080')!.rgba, new RGBA(128, 128, 128, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#800000')!.rgba, new RGBA(128, 0, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#808000')!.rgba, new RGBA(128, 128, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#008000')!.rgba, new RGBA(0, 128, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#800080')!.rgba, new RGBA(128, 0, 128, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#008080')!.rgba, new RGBA(0, 128, 128, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#000080')!.rgba, new RGBA(0, 0, 128, 1)); + + assert.deepStrictEqual(Color.Format.CSS.parse('#010203')!.rgba, new RGBA(1, 2, 3, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#040506')!.rgba, new RGBA(4, 5, 6, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#070809')!.rgba, new RGBA(7, 8, 9, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#0a0A0a')!.rgba, new RGBA(10, 10, 10, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#0b0B0b')!.rgba, new RGBA(11, 11, 11, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#0c0C0c')!.rgba, new RGBA(12, 12, 12, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#0d0D0d')!.rgba, new RGBA(13, 13, 13, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#0e0E0e')!.rgba, new RGBA(14, 14, 14, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#0f0F0f')!.rgba, new RGBA(15, 15, 15, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#a0A0a0')!.rgba, new RGBA(160, 160, 160, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#CFA')!.rgba, new RGBA(204, 255, 170, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#CFA8')!.rgba, new RGBA(204, 255, 170, 0.533)); + }); + + test('rgb()', () => { + // somewhat valid / unusual + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(-255, 0, 0)')!.rgba, new RGBA(0, 0, 0)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(+255, 0, 0)')!.rgba, new RGBA(255, 0, 0)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(800, 0, 0)')!.rgba, new RGBA(255, 0, 0)); + + // valid + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(0, 0, 0)')!.rgba, new RGBA(0, 0, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(255, 255, 255)')!.rgba, new RGBA(255, 255, 255, 1)); + + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(255, 0, 0)')!.rgba, new RGBA(255, 0, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(0, 255, 0)')!.rgba, new RGBA(0, 255, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(0, 0, 255)')!.rgba, new RGBA(0, 0, 255, 1)); + + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(255, 255, 0)')!.rgba, new RGBA(255, 255, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(0, 255, 255)')!.rgba, new RGBA(0, 255, 255, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(255, 0, 255)')!.rgba, new RGBA(255, 0, 255, 1)); + + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(192, 192, 192)')!.rgba, new RGBA(192, 192, 192, 1)); + + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(128, 128, 128)')!.rgba, new RGBA(128, 128, 128, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(128, 0, 0)')!.rgba, new RGBA(128, 0, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(128, 128, 0)')!.rgba, new RGBA(128, 128, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(0, 128, 0)')!.rgba, new RGBA(0, 128, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(128, 0, 128)')!.rgba, new RGBA(128, 0, 128, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(0, 128, 128)')!.rgba, new RGBA(0, 128, 128, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(0, 0, 128)')!.rgba, new RGBA(0, 0, 128, 1)); + + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(1, 2, 3)')!.rgba, new RGBA(1, 2, 3, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(4, 5, 6)')!.rgba, new RGBA(4, 5, 6, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(7, 8, 9)')!.rgba, new RGBA(7, 8, 9, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(10, 10, 10)')!.rgba, new RGBA(10, 10, 10, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(11, 11, 11)')!.rgba, new RGBA(11, 11, 11, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(12, 12, 12)')!.rgba, new RGBA(12, 12, 12, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(13, 13, 13)')!.rgba, new RGBA(13, 13, 13, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(14, 14, 14)')!.rgba, new RGBA(14, 14, 14, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(15, 15, 15)')!.rgba, new RGBA(15, 15, 15, 1)); + }); + + test('rgba()', () => { + // somewhat valid / unusual + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(0, 0, 0, 255)')!.rgba, new RGBA(0, 0, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(-255, 0, 0, 1)')!.rgba, new RGBA(0, 0, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(+255, 0, 0, 1)')!.rgba, new RGBA(255, 0, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(800, 0, 0, 1)')!.rgba, new RGBA(255, 0, 0, 1)); + + // alpha values + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(255, 0, 0, 0.2)')!.rgba, new RGBA(255, 0, 0, 0.2)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(255, 0, 0, 0.5)')!.rgba, new RGBA(255, 0, 0, 0.5)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(255, 0, 0, 0.75)')!.rgba, new RGBA(255, 0, 0, 0.75)); + + // valid + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(0, 0, 0, 1)')!.rgba, new RGBA(0, 0, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(255, 255, 255, 1)')!.rgba, new RGBA(255, 255, 255, 1)); + + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(255, 0, 0, 1)')!.rgba, new RGBA(255, 0, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(0, 255, 0, 1)')!.rgba, new RGBA(0, 255, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(0, 0, 255, 1)')!.rgba, new RGBA(0, 0, 255, 1)); + + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(255, 255, 0, 1)')!.rgba, new RGBA(255, 255, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(0, 255, 255, 1)')!.rgba, new RGBA(0, 255, 255, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(255, 0, 255, 1)')!.rgba, new RGBA(255, 0, 255, 1)); + + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(192, 192, 192, 1)')!.rgba, new RGBA(192, 192, 192, 1)); + + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(128, 128, 128, 1)')!.rgba, new RGBA(128, 128, 128, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(128, 0, 0, 1)')!.rgba, new RGBA(128, 0, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(128, 128, 0, 1)')!.rgba, new RGBA(128, 128, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(0, 128, 0, 1)')!.rgba, new RGBA(0, 128, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(128, 0, 128, 1)')!.rgba, new RGBA(128, 0, 128, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(0, 128, 128, 1)')!.rgba, new RGBA(0, 128, 128, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(0, 0, 128, 1)')!.rgba, new RGBA(0, 0, 128, 1)); + + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(1, 2, 3, 1)')!.rgba, new RGBA(1, 2, 3, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(4, 5, 6, 1)')!.rgba, new RGBA(4, 5, 6, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(7, 8, 9, 1)')!.rgba, new RGBA(7, 8, 9, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(10, 10, 10, 1)')!.rgba, new RGBA(10, 10, 10, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(11, 11, 11, 1)')!.rgba, new RGBA(11, 11, 11, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(12, 12, 12, 1)')!.rgba, new RGBA(12, 12, 12, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(13, 13, 13, 1)')!.rgba, new RGBA(13, 13, 13, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(14, 14, 14, 1)')!.rgba, new RGBA(14, 14, 14, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(15, 15, 15, 1)')!.rgba, new RGBA(15, 15, 15, 1)); + }); + }); + test('parseHex', () => { // invalid diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index 941599a1143c8..b1c99760b28f5 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -24,6 +24,7 @@ import { quadVertices } from './gpuUtils.js'; import { GlyphRasterizer } from './raster/glyphRasterizer.js'; import { ViewGpuContext } from './viewGpuContext.js'; import { GpuCharMetadata } from './raster/raster.js'; +import { Color } from '../../../base/common/color.js'; const enum Constants { @@ -410,18 +411,17 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend break; } case 'color': { - // TODO: Move to color.ts and make more generic - function parseRgb(text: string): number { - const color = text.match(/rgba?\((\d+), ?(\d+), ?(\d+)(?:, ?\d+(?:.\d+)?)?\)/); - if (!color) { - throw new Error('Invalid color format ' + text); - } - const r = parseInt(color[1], 10); - const g = parseInt(color[2], 10); - const b = parseInt(color[3], 10); - return r << 16 | g << 8 | b; + // TODO: This parsing/error handling should move into canRender so fallback to DOM works + const parsedColor = Color.Format.CSS.parse(value); + if (!parsedColor) { + throw new Error('Invalid color format ' + value); + } + const rgb = parsedColor.rgba.r << 16 | parsedColor.rgba.g << 8 | parsedColor.rgba.b; + charMetadata |= ((rgb << GpuCharMetadata.FOREGROUND_OFFSET) & GpuCharMetadata.FOREGROUND_MASK) >>> 0; + // TODO: _foreground_ opacity should not be applied to regular opacity + if (parsedColor.rgba.a < 1) { + charMetadata |= ((parsedColor.rgba.a * 0xFF << GpuCharMetadata.OPACITY_OFFSET) & GpuCharMetadata.OPACITY_MASK) >>> 0; } - charMetadata = ((parseRgb(value) << GpuCharMetadata.FOREGROUND_OFFSET) & GpuCharMetadata.FOREGROUND_MASK) >>> 0; break; } default: throw new BugIndicatingError('Unexpected inline decoration style'); From 07d84aae4a81d32cf66954a32564c680f7584d46 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 21 Nov 2024 07:03:04 -0800 Subject: [PATCH 14/20] Add CSS named color support to parse function --- src/vs/base/common/color.ts | 157 +++++++++++++++++++++++++- src/vs/base/test/common/color.test.ts | 153 +++++++++++++++++++++++++ 2 files changed, 309 insertions(+), 1 deletion(-) diff --git a/src/vs/base/common/color.ts b/src/vs/base/common/color.ts index a4faedd349956..02d98667fb71d 100644 --- a/src/vs/base/common/color.ts +++ b/src/vs/base/common/color.ts @@ -664,7 +664,162 @@ export namespace Color { return new Color(new RGBA(r, g, b)); } // TODO: Support more formats - return null; + return parseNamedKeyword(css); + } + + function parseNamedKeyword(css: string): Color | null { + // https://drafts.csswg.org/css-color/#named-colors + switch (css) { + case 'aliceblue': return new Color(new RGBA(240, 248, 255, 1)); + case 'antiquewhite': return new Color(new RGBA(250, 235, 215, 1)); + case 'aqua': return new Color(new RGBA(0, 255, 255, 1)); + case 'aquamarine': return new Color(new RGBA(127, 255, 212, 1)); + case 'azure': return new Color(new RGBA(240, 255, 255, 1)); + case 'beige': return new Color(new RGBA(245, 245, 220, 1)); + case 'bisque': return new Color(new RGBA(255, 228, 196, 1)); + case 'black': return new Color(new RGBA(0, 0, 0, 1)); + case 'blanchedalmond': return new Color(new RGBA(255, 235, 205, 1)); + case 'blue': return new Color(new RGBA(0, 0, 255, 1)); + case 'blueviolet': return new Color(new RGBA(138, 43, 226, 1)); + case 'brown': return new Color(new RGBA(165, 42, 42, 1)); + case 'burlywood': return new Color(new RGBA(222, 184, 135, 1)); + case 'cadetblue': return new Color(new RGBA(95, 158, 160, 1)); + case 'chartreuse': return new Color(new RGBA(127, 255, 0, 1)); + case 'chocolate': return new Color(new RGBA(210, 105, 30, 1)); + case 'coral': return new Color(new RGBA(255, 127, 80, 1)); + case 'cornflowerblue': return new Color(new RGBA(100, 149, 237, 1)); + case 'cornsilk': return new Color(new RGBA(255, 248, 220, 1)); + case 'crimson': return new Color(new RGBA(220, 20, 60, 1)); + case 'cyan': return new Color(new RGBA(0, 255, 255, 1)); + case 'darkblue': return new Color(new RGBA(0, 0, 139, 1)); + case 'darkcyan': return new Color(new RGBA(0, 139, 139, 1)); + case 'darkgoldenrod': return new Color(new RGBA(184, 134, 11, 1)); + case 'darkgray': return new Color(new RGBA(169, 169, 169, 1)); + case 'darkgreen': return new Color(new RGBA(0, 100, 0, 1)); + case 'darkgrey': return new Color(new RGBA(169, 169, 169, 1)); + case 'darkkhaki': return new Color(new RGBA(189, 183, 107, 1)); + case 'darkmagenta': return new Color(new RGBA(139, 0, 139, 1)); + case 'darkolivegreen': return new Color(new RGBA(85, 107, 47, 1)); + case 'darkorange': return new Color(new RGBA(255, 140, 0, 1)); + case 'darkorchid': return new Color(new RGBA(153, 50, 204, 1)); + case 'darkred': return new Color(new RGBA(139, 0, 0, 1)); + case 'darksalmon': return new Color(new RGBA(233, 150, 122, 1)); + case 'darkseagreen': return new Color(new RGBA(143, 188, 143, 1)); + case 'darkslateblue': return new Color(new RGBA(72, 61, 139, 1)); + case 'darkslategray': return new Color(new RGBA(47, 79, 79, 1)); + case 'darkslategrey': return new Color(new RGBA(47, 79, 79, 1)); + case 'darkturquoise': return new Color(new RGBA(0, 206, 209, 1)); + case 'darkviolet': return new Color(new RGBA(148, 0, 211, 1)); + case 'deeppink': return new Color(new RGBA(255, 20, 147, 1)); + case 'deepskyblue': return new Color(new RGBA(0, 191, 255, 1)); + case 'dimgray': return new Color(new RGBA(105, 105, 105, 1)); + case 'dimgrey': return new Color(new RGBA(105, 105, 105, 1)); + case 'dodgerblue': return new Color(new RGBA(30, 144, 255, 1)); + case 'firebrick': return new Color(new RGBA(178, 34, 34, 1)); + case 'floralwhite': return new Color(new RGBA(255, 250, 240, 1)); + case 'forestgreen': return new Color(new RGBA(34, 139, 34, 1)); + case 'fuchsia': return new Color(new RGBA(255, 0, 255, 1)); + case 'gainsboro': return new Color(new RGBA(220, 220, 220, 1)); + case 'ghostwhite': return new Color(new RGBA(248, 248, 255, 1)); + case 'gold': return new Color(new RGBA(255, 215, 0, 1)); + case 'goldenrod': return new Color(new RGBA(218, 165, 32, 1)); + case 'gray': return new Color(new RGBA(128, 128, 128, 1)); + case 'green': return new Color(new RGBA(0, 128, 0, 1)); + case 'greenyellow': return new Color(new RGBA(173, 255, 47, 1)); + case 'grey': return new Color(new RGBA(128, 128, 128, 1)); + case 'honeydew': return new Color(new RGBA(240, 255, 240, 1)); + case 'hotpink': return new Color(new RGBA(255, 105, 180, 1)); + case 'indianred': return new Color(new RGBA(205, 92, 92, 1)); + case 'indigo': return new Color(new RGBA(75, 0, 130, 1)); + case 'ivory': return new Color(new RGBA(255, 255, 240, 1)); + case 'khaki': return new Color(new RGBA(240, 230, 140, 1)); + case 'lavender': return new Color(new RGBA(230, 230, 250, 1)); + case 'lavenderblush': return new Color(new RGBA(255, 240, 245, 1)); + case 'lawngreen': return new Color(new RGBA(124, 252, 0, 1)); + case 'lemonchiffon': return new Color(new RGBA(255, 250, 205, 1)); + case 'lightblue': return new Color(new RGBA(173, 216, 230, 1)); + case 'lightcoral': return new Color(new RGBA(240, 128, 128, 1)); + case 'lightcyan': return new Color(new RGBA(224, 255, 255, 1)); + case 'lightgoldenrodyellow': return new Color(new RGBA(250, 250, 210, 1)); + case 'lightgray': return new Color(new RGBA(211, 211, 211, 1)); + case 'lightgreen': return new Color(new RGBA(144, 238, 144, 1)); + case 'lightgrey': return new Color(new RGBA(211, 211, 211, 1)); + case 'lightpink': return new Color(new RGBA(255, 182, 193, 1)); + case 'lightsalmon': return new Color(new RGBA(255, 160, 122, 1)); + case 'lightseagreen': return new Color(new RGBA(32, 178, 170, 1)); + case 'lightskyblue': return new Color(new RGBA(135, 206, 250, 1)); + case 'lightslategray': return new Color(new RGBA(119, 136, 153, 1)); + case 'lightslategrey': return new Color(new RGBA(119, 136, 153, 1)); + case 'lightsteelblue': return new Color(new RGBA(176, 196, 222, 1)); + case 'lightyellow': return new Color(new RGBA(255, 255, 224, 1)); + case 'lime': return new Color(new RGBA(0, 255, 0, 1)); + case 'limegreen': return new Color(new RGBA(50, 205, 50, 1)); + case 'linen': return new Color(new RGBA(250, 240, 230, 1)); + case 'magenta': return new Color(new RGBA(255, 0, 255, 1)); + case 'maroon': return new Color(new RGBA(128, 0, 0, 1)); + case 'mediumaquamarine': return new Color(new RGBA(102, 205, 170, 1)); + case 'mediumblue': return new Color(new RGBA(0, 0, 205, 1)); + case 'mediumorchid': return new Color(new RGBA(186, 85, 211, 1)); + case 'mediumpurple': return new Color(new RGBA(147, 112, 219, 1)); + case 'mediumseagreen': return new Color(new RGBA(60, 179, 113, 1)); + case 'mediumslateblue': return new Color(new RGBA(123, 104, 238, 1)); + case 'mediumspringgreen': return new Color(new RGBA(0, 250, 154, 1)); + case 'mediumturquoise': return new Color(new RGBA(72, 209, 204, 1)); + case 'mediumvioletred': return new Color(new RGBA(199, 21, 133, 1)); + case 'midnightblue': return new Color(new RGBA(25, 25, 112, 1)); + case 'mintcream': return new Color(new RGBA(245, 255, 250, 1)); + case 'mistyrose': return new Color(new RGBA(255, 228, 225, 1)); + case 'moccasin': return new Color(new RGBA(255, 228, 181, 1)); + case 'navajowhite': return new Color(new RGBA(255, 222, 173, 1)); + case 'navy': return new Color(new RGBA(0, 0, 128, 1)); + case 'oldlace': return new Color(new RGBA(253, 245, 230, 1)); + case 'olive': return new Color(new RGBA(128, 128, 0, 1)); + case 'olivedrab': return new Color(new RGBA(107, 142, 35, 1)); + case 'orange': return new Color(new RGBA(255, 165, 0, 1)); + case 'orangered': return new Color(new RGBA(255, 69, 0, 1)); + case 'orchid': return new Color(new RGBA(218, 112, 214, 1)); + case 'palegoldenrod': return new Color(new RGBA(238, 232, 170, 1)); + case 'palegreen': return new Color(new RGBA(152, 251, 152, 1)); + case 'paleturquoise': return new Color(new RGBA(175, 238, 238, 1)); + case 'palevioletred': return new Color(new RGBA(219, 112, 147, 1)); + case 'papayawhip': return new Color(new RGBA(255, 239, 213, 1)); + case 'peachpuff': return new Color(new RGBA(255, 218, 185, 1)); + case 'peru': return new Color(new RGBA(205, 133, 63, 1)); + case 'pink': return new Color(new RGBA(255, 192, 203, 1)); + case 'plum': return new Color(new RGBA(221, 160, 221, 1)); + case 'powderblue': return new Color(new RGBA(176, 224, 230, 1)); + case 'purple': return new Color(new RGBA(128, 0, 128, 1)); + case 'rebeccapurple': return new Color(new RGBA(102, 51, 153, 1)); + case 'red': return new Color(new RGBA(255, 0, 0, 1)); + case 'rosybrown': return new Color(new RGBA(188, 143, 143, 1)); + case 'royalblue': return new Color(new RGBA(65, 105, 225, 1)); + case 'saddlebrown': return new Color(new RGBA(139, 69, 19, 1)); + case 'salmon': return new Color(new RGBA(250, 128, 114, 1)); + case 'sandybrown': return new Color(new RGBA(244, 164, 96, 1)); + case 'seagreen': return new Color(new RGBA(46, 139, 87, 1)); + case 'seashell': return new Color(new RGBA(255, 245, 238, 1)); + case 'sienna': return new Color(new RGBA(160, 82, 45, 1)); + case 'silver': return new Color(new RGBA(192, 192, 192, 1)); + case 'skyblue': return new Color(new RGBA(135, 206, 235, 1)); + case 'slateblue': return new Color(new RGBA(106, 90, 205, 1)); + case 'slategray': return new Color(new RGBA(112, 128, 144, 1)); + case 'slategrey': return new Color(new RGBA(112, 128, 144, 1)); + case 'snow': return new Color(new RGBA(255, 250, 250, 1)); + case 'springgreen': return new Color(new RGBA(0, 255, 127, 1)); + case 'steelblue': return new Color(new RGBA(70, 130, 180, 1)); + case 'tan': return new Color(new RGBA(210, 180, 140, 1)); + case 'teal': return new Color(new RGBA(0, 128, 128, 1)); + case 'thistle': return new Color(new RGBA(216, 191, 216, 1)); + case 'tomato': return new Color(new RGBA(255, 99, 71, 1)); + case 'turquoise': return new Color(new RGBA(64, 224, 208, 1)); + case 'violet': return new Color(new RGBA(238, 130, 238, 1)); + case 'wheat': return new Color(new RGBA(245, 222, 179, 1)); + case 'white': return new Color(new RGBA(255, 255, 255, 1)); + case 'whitesmoke': return new Color(new RGBA(245, 245, 245, 1)); + case 'yellow': return new Color(new RGBA(255, 255, 0, 1)); + case 'yellowgreen': return new Color(new RGBA(154, 205, 50, 1)); + default: return null; + } } /** diff --git a/src/vs/base/test/common/color.test.ts b/src/vs/base/test/common/color.test.ts index 3ae8353bcdff3..ca5db568ca67f 100644 --- a/src/vs/base/test/common/color.test.ts +++ b/src/vs/base/test/common/color.test.ts @@ -210,9 +210,162 @@ suite('Color', () => { assert.deepStrictEqual(Color.Format.CSS.parse('#'), null); assert.deepStrictEqual(Color.Format.CSS.parse('#0102030'), null); }); + test('transparent', () => { assert.deepStrictEqual(Color.Format.CSS.parse('transparent'), new Color(new RGBA(0, 0, 0, 0))); }); + + test('named keyword', () => { + assert.deepStrictEqual(Color.Format.CSS.parse('aliceblue')!.rgba, new RGBA(240, 248, 255, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('antiquewhite')!.rgba, new RGBA(250, 235, 215, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('aqua')!.rgba, new RGBA(0, 255, 255, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('aquamarine')!.rgba, new RGBA(127, 255, 212, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('azure')!.rgba, new RGBA(240, 255, 255, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('beige')!.rgba, new RGBA(245, 245, 220, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('bisque')!.rgba, new RGBA(255, 228, 196, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('black')!.rgba, new RGBA(0, 0, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('blanchedalmond')!.rgba, new RGBA(255, 235, 205, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('blue')!.rgba, new RGBA(0, 0, 255, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('blueviolet')!.rgba, new RGBA(138, 43, 226, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('brown')!.rgba, new RGBA(165, 42, 42, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('burlywood')!.rgba, new RGBA(222, 184, 135, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('cadetblue')!.rgba, new RGBA(95, 158, 160, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('chartreuse')!.rgba, new RGBA(127, 255, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('chocolate')!.rgba, new RGBA(210, 105, 30, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('coral')!.rgba, new RGBA(255, 127, 80, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('cornflowerblue')!.rgba, new RGBA(100, 149, 237, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('cornsilk')!.rgba, new RGBA(255, 248, 220, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('crimson')!.rgba, new RGBA(220, 20, 60, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('cyan')!.rgba, new RGBA(0, 255, 255, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darkblue')!.rgba, new RGBA(0, 0, 139, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darkcyan')!.rgba, new RGBA(0, 139, 139, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darkgoldenrod')!.rgba, new RGBA(184, 134, 11, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darkgray')!.rgba, new RGBA(169, 169, 169, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darkgreen')!.rgba, new RGBA(0, 100, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darkgrey')!.rgba, new RGBA(169, 169, 169, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darkkhaki')!.rgba, new RGBA(189, 183, 107, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darkmagenta')!.rgba, new RGBA(139, 0, 139, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darkolivegreen')!.rgba, new RGBA(85, 107, 47, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darkorange')!.rgba, new RGBA(255, 140, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darkorchid')!.rgba, new RGBA(153, 50, 204, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darkred')!.rgba, new RGBA(139, 0, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darksalmon')!.rgba, new RGBA(233, 150, 122, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darkseagreen')!.rgba, new RGBA(143, 188, 143, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darkslateblue')!.rgba, new RGBA(72, 61, 139, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darkslategray')!.rgba, new RGBA(47, 79, 79, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darkslategrey')!.rgba, new RGBA(47, 79, 79, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darkturquoise')!.rgba, new RGBA(0, 206, 209, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darkviolet')!.rgba, new RGBA(148, 0, 211, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('deeppink')!.rgba, new RGBA(255, 20, 147, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('deepskyblue')!.rgba, new RGBA(0, 191, 255, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('dimgray')!.rgba, new RGBA(105, 105, 105, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('dimgrey')!.rgba, new RGBA(105, 105, 105, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('dodgerblue')!.rgba, new RGBA(30, 144, 255, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('firebrick')!.rgba, new RGBA(178, 34, 34, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('floralwhite')!.rgba, new RGBA(255, 250, 240, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('forestgreen')!.rgba, new RGBA(34, 139, 34, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('fuchsia')!.rgba, new RGBA(255, 0, 255, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('gainsboro')!.rgba, new RGBA(220, 220, 220, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('ghostwhite')!.rgba, new RGBA(248, 248, 255, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('gold')!.rgba, new RGBA(255, 215, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('goldenrod')!.rgba, new RGBA(218, 165, 32, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('gray')!.rgba, new RGBA(128, 128, 128, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('green')!.rgba, new RGBA(0, 128, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('greenyellow')!.rgba, new RGBA(173, 255, 47, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('grey')!.rgba, new RGBA(128, 128, 128, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('honeydew')!.rgba, new RGBA(240, 255, 240, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('hotpink')!.rgba, new RGBA(255, 105, 180, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('indianred')!.rgba, new RGBA(205, 92, 92, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('indigo')!.rgba, new RGBA(75, 0, 130, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('ivory')!.rgba, new RGBA(255, 255, 240, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('khaki')!.rgba, new RGBA(240, 230, 140, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lavender')!.rgba, new RGBA(230, 230, 250, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lavenderblush')!.rgba, new RGBA(255, 240, 245, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lawngreen')!.rgba, new RGBA(124, 252, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lemonchiffon')!.rgba, new RGBA(255, 250, 205, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lightblue')!.rgba, new RGBA(173, 216, 230, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lightcoral')!.rgba, new RGBA(240, 128, 128, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lightcyan')!.rgba, new RGBA(224, 255, 255, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lightgoldenrodyellow')!.rgba, new RGBA(250, 250, 210, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lightgray')!.rgba, new RGBA(211, 211, 211, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lightgreen')!.rgba, new RGBA(144, 238, 144, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lightgrey')!.rgba, new RGBA(211, 211, 211, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lightpink')!.rgba, new RGBA(255, 182, 193, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lightsalmon')!.rgba, new RGBA(255, 160, 122, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lightseagreen')!.rgba, new RGBA(32, 178, 170, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lightskyblue')!.rgba, new RGBA(135, 206, 250, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lightslategray')!.rgba, new RGBA(119, 136, 153, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lightslategrey')!.rgba, new RGBA(119, 136, 153, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lightsteelblue')!.rgba, new RGBA(176, 196, 222, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lightyellow')!.rgba, new RGBA(255, 255, 224, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lime')!.rgba, new RGBA(0, 255, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('limegreen')!.rgba, new RGBA(50, 205, 50, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('linen')!.rgba, new RGBA(250, 240, 230, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('magenta')!.rgba, new RGBA(255, 0, 255, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('maroon')!.rgba, new RGBA(128, 0, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('mediumaquamarine')!.rgba, new RGBA(102, 205, 170, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('mediumblue')!.rgba, new RGBA(0, 0, 205, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('mediumorchid')!.rgba, new RGBA(186, 85, 211, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('mediumpurple')!.rgba, new RGBA(147, 112, 219, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('mediumseagreen')!.rgba, new RGBA(60, 179, 113, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('mediumslateblue')!.rgba, new RGBA(123, 104, 238, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('mediumspringgreen')!.rgba, new RGBA(0, 250, 154, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('mediumturquoise')!.rgba, new RGBA(72, 209, 204, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('mediumvioletred')!.rgba, new RGBA(199, 21, 133, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('midnightblue')!.rgba, new RGBA(25, 25, 112, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('mintcream')!.rgba, new RGBA(245, 255, 250, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('mistyrose')!.rgba, new RGBA(255, 228, 225, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('moccasin')!.rgba, new RGBA(255, 228, 181, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('navajowhite')!.rgba, new RGBA(255, 222, 173, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('navy')!.rgba, new RGBA(0, 0, 128, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('oldlace')!.rgba, new RGBA(253, 245, 230, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('olive')!.rgba, new RGBA(128, 128, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('olivedrab')!.rgba, new RGBA(107, 142, 35, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('orange')!.rgba, new RGBA(255, 165, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('orangered')!.rgba, new RGBA(255, 69, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('orchid')!.rgba, new RGBA(218, 112, 214, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('palegoldenrod')!.rgba, new RGBA(238, 232, 170, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('palegreen')!.rgba, new RGBA(152, 251, 152, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('paleturquoise')!.rgba, new RGBA(175, 238, 238, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('palevioletred')!.rgba, new RGBA(219, 112, 147, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('papayawhip')!.rgba, new RGBA(255, 239, 213, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('peachpuff')!.rgba, new RGBA(255, 218, 185, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('peru')!.rgba, new RGBA(205, 133, 63, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('pink')!.rgba, new RGBA(255, 192, 203, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('plum')!.rgba, new RGBA(221, 160, 221, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('powderblue')!.rgba, new RGBA(176, 224, 230, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('purple')!.rgba, new RGBA(128, 0, 128, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rebeccapurple')!.rgba, new RGBA(102, 51, 153, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('red')!.rgba, new RGBA(255, 0, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rosybrown')!.rgba, new RGBA(188, 143, 143, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('royalblue')!.rgba, new RGBA(65, 105, 225, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('saddlebrown')!.rgba, new RGBA(139, 69, 19, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('salmon')!.rgba, new RGBA(250, 128, 114, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('sandybrown')!.rgba, new RGBA(244, 164, 96, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('seagreen')!.rgba, new RGBA(46, 139, 87, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('seashell')!.rgba, new RGBA(255, 245, 238, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('sienna')!.rgba, new RGBA(160, 82, 45, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('silver')!.rgba, new RGBA(192, 192, 192, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('skyblue')!.rgba, new RGBA(135, 206, 235, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('slateblue')!.rgba, new RGBA(106, 90, 205, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('slategray')!.rgba, new RGBA(112, 128, 144, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('slategrey')!.rgba, new RGBA(112, 128, 144, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('snow')!.rgba, new RGBA(255, 250, 250, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('springgreen')!.rgba, new RGBA(0, 255, 127, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('steelblue')!.rgba, new RGBA(70, 130, 180, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('tan')!.rgba, new RGBA(210, 180, 140, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('teal')!.rgba, new RGBA(0, 128, 128, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('thistle')!.rgba, new RGBA(216, 191, 216, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('tomato')!.rgba, new RGBA(255, 99, 71, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('turquoise')!.rgba, new RGBA(64, 224, 208, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('violet')!.rgba, new RGBA(238, 130, 238, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('wheat')!.rgba, new RGBA(245, 222, 179, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('white')!.rgba, new RGBA(255, 255, 255, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('whitesmoke')!.rgba, new RGBA(245, 245, 245, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('yellow')!.rgba, new RGBA(255, 255, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('yellowgreen')!.rgba, new RGBA(154, 205, 50, 1)); + }); + test('hex-color', () => { // somewhat valid assert.deepStrictEqual(Color.Format.CSS.parse('#FFFFG0')!.rgba, new RGBA(255, 255, 0, 1)); From b5c3a7f91362dba398912e4ac5dc1750864e0c89 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 21 Nov 2024 11:40:45 -0800 Subject: [PATCH 15/20] Fix transparent color test --- src/vs/base/test/common/color.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/base/test/common/color.test.ts b/src/vs/base/test/common/color.test.ts index ca5db568ca67f..5410575cc3304 100644 --- a/src/vs/base/test/common/color.test.ts +++ b/src/vs/base/test/common/color.test.ts @@ -212,7 +212,7 @@ suite('Color', () => { }); test('transparent', () => { - assert.deepStrictEqual(Color.Format.CSS.parse('transparent'), new Color(new RGBA(0, 0, 0, 0))); + assert.deepStrictEqual(Color.Format.CSS.parse('transparent')!.rgba, new RGBA(0, 0, 0, 0)); }); test('named keyword', () => { From 8f65102dbf76b8cf35d1143e21350481e13395ca Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 22 Nov 2024 06:55:35 -0800 Subject: [PATCH 16/20] Fix issue with caching ignoring charMetadata --- src/vs/base/common/color.ts | 13 ++++++++ .../browser/gpu/atlas/textureAtlasPage.ts | 3 +- .../browser/gpu/fullFileRenderStrategy.ts | 32 ++++--------------- .../browser/gpu/raster/glyphRasterizer.ts | 12 +++---- src/vs/editor/browser/gpu/raster/raster.ts | 8 ----- 5 files changed, 27 insertions(+), 41 deletions(-) diff --git a/src/vs/base/common/color.ts b/src/vs/base/common/color.ts index 02d98667fb71d..67eeeec757a3e 100644 --- a/src/vs/base/common/color.ts +++ b/src/vs/base/common/color.ts @@ -535,6 +535,19 @@ export class Color { return this._toString; } + private _toNumber24Bit?: number; + toNumber24Bit(): number { + if (!this._toNumber24Bit) { + this._toNumber24Bit = ( + this.rgba.r /* */ << 24 | + this.rgba.g /* */ << 16 | + this.rgba.b /* */ << 8 | + this.rgba.a * 0xFF << 0 + ) >>> 0; + } + return this._toNumber24Bit; + } + static getLighterColor(of: Color, relative: Color, factor?: number): Color { if (of.isLighterThan(relative)) { return of; diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts b/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts index cbda9352a3bc8..9c09181751a7a 100644 --- a/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts @@ -100,7 +100,8 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla if (this._logService.getLevel() === LogLevel.Trace) { this._logService.trace('New glyph', { chars, - metadata: tokenMetadata, + tokenMetadata, + charMetadata, rasterizedGlyph, glyph }); diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index b1c99760b28f5..bfb27a81d332b 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -8,7 +8,6 @@ import { BugIndicatingError } from '../../../base/common/errors.js'; import { MandatoryMutableDisposable } from '../../../base/common/lifecycle.js'; import { EditorOption } from '../../common/config/editorOptions.js'; import { CursorColumns } from '../../common/core/cursorColumns.js'; -import { MetadataConsts } from '../../common/encodedTokenAttributes.js'; import type { IViewLineTokens } from '../../common/tokens/lineTokens.js'; import { ViewEventHandler } from '../../common/viewEventHandler.js'; import { ViewEventType, type ViewConfigurationChangedEvent, type ViewDecorationsChangedEvent, type ViewLinesChangedEvent, type ViewLinesDeletedEvent, type ViewLinesInsertedEvent, type ViewScrollChangedEvent, type ViewTokensChangedEvent, type ViewZonesChangedEvent } from '../../common/viewEvents.js'; @@ -23,10 +22,8 @@ import { GPULifecycle } from './gpuDisposable.js'; import { quadVertices } from './gpuUtils.js'; import { GlyphRasterizer } from './raster/glyphRasterizer.js'; import { ViewGpuContext } from './viewGpuContext.js'; -import { GpuCharMetadata } from './raster/raster.js'; import { Color } from '../../../base/common/color.js'; - const enum Constants { IndicesPerCell = 6, } @@ -141,7 +138,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend } public override onDecorationsChanged(e: ViewDecorationsChangedEvent): boolean { - // TODO: Don't clear all lines + // TODO: Don't clear all cells if we can avoid it this._invalidateAllLines(); return true; } @@ -225,14 +222,14 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend } reset() { + this._invalidateAllLines(); for (const bufferIndex of [0, 1]) { // Zero out buffer and upload to GPU to prevent stale rows from rendering const buffer = new Float32Array(this._cellValueBuffers[bufferIndex]); buffer.fill(0, 0, buffer.length); this._device.queue.writeBuffer(this._cellBindBuffer, 0, buffer.buffer, 0, buffer.byteLength); - this._upToDateLines[bufferIndex].clear(); } - this._visibleObjectCount = 0; + this._finalRenderedLine = 0; } update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): number { @@ -279,7 +276,6 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend const queuedBufferUpdates = this._queuedBufferUpdates[this._activeDoubleBufferIndex]; while (queuedBufferUpdates.length) { const e = queuedBufferUpdates.shift()!; - switch (e.type) { case ViewEventType.ViewConfigurationChanged: { // TODO: Refine the cases for when we throw away all the data @@ -368,9 +364,8 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend charMetadata = 0; // TODO: We'd want to optimize pulling the decorations in order - // HACK: Temporary replace char to demonstrate inline decorations const cellDecorations = inlineDecorations.filter(decoration => { - // TODO: Why does Range.containsPosition and Range.strictContainsPosition not work here? + // This is Range.strictContainsPosition except it's working at the cell level. if (y < decoration.range.startLineNumber || y > decoration.range.endLineNumber) { return false; } @@ -400,28 +395,13 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend for (const [key, value] of inlineStyles.entries()) { switch (key) { - case 'text-decoration-line': { - charMetadata |= MetadataConsts.STRIKETHROUGH_MASK; - break; - } - case 'text-decoration-thickness': - case 'text-decoration-style': - case 'text-decoration-color': { - // HACK: Ignore for now to avoid throwing - break; - } case 'color': { // TODO: This parsing/error handling should move into canRender so fallback to DOM works const parsedColor = Color.Format.CSS.parse(value); if (!parsedColor) { - throw new Error('Invalid color format ' + value); - } - const rgb = parsedColor.rgba.r << 16 | parsedColor.rgba.g << 8 | parsedColor.rgba.b; - charMetadata |= ((rgb << GpuCharMetadata.FOREGROUND_OFFSET) & GpuCharMetadata.FOREGROUND_MASK) >>> 0; - // TODO: _foreground_ opacity should not be applied to regular opacity - if (parsedColor.rgba.a < 1) { - charMetadata |= ((parsedColor.rgba.a * 0xFF << GpuCharMetadata.OPACITY_OFFSET) & GpuCharMetadata.OPACITY_MASK) >>> 0; + throw new BugIndicatingError('Invalid color format ' + value); } + charMetadata = parsedColor.toNumber24Bit(); break; } default: throw new BugIndicatingError('Unexpected inline decoration style'); diff --git a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts index efec3211c9a7c..a0ae8927b832d 100644 --- a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts @@ -9,7 +9,7 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { StringBuilder } from '../../../common/core/stringBuilder.js'; import { FontStyle, TokenMetadata } from '../../../common/encodedTokenAttributes.js'; import { ensureNonNullable } from '../gpuUtils.js'; -import { GpuCharMetadata, type IBoundingBox, type IGlyphRasterizer, type IRasterizedGlyph } from './raster.js'; +import { type IBoundingBox, type IGlyphRasterizer, type IRasterizedGlyph } from './raster.js'; let nextId = 0; @@ -37,7 +37,7 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { y: 0, } }; - private _workGlyphConfig: { chars: string | undefined; metadata: number } = { chars: undefined, metadata: 0 }; + private _workGlyphConfig: { chars: string | undefined; tokenMetadata: number; charMetadata: number } = { chars: undefined, tokenMetadata: 0, charMetadata: 0 }; constructor( readonly fontSize: number, @@ -75,11 +75,12 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { // Check if the last glyph matches the config, reuse if so. This helps avoid unnecessary // work when the rasterizer is called multiple times like when the glyph doesn't fit into a // page. - if (this._workGlyphConfig.chars === chars && this._workGlyphConfig.metadata === tokenMetadata) { + if (this._workGlyphConfig.chars === chars && this._workGlyphConfig.tokenMetadata === tokenMetadata && this._workGlyphConfig.charMetadata === charMetadata) { return this._workGlyph; } this._workGlyphConfig.chars = chars; - this._workGlyphConfig.metadata = tokenMetadata; + this._workGlyphConfig.tokenMetadata = tokenMetadata; + this._workGlyphConfig.charMetadata = charMetadata; return this._rasterizeGlyph(chars, tokenMetadata, charMetadata, colorMap); } @@ -117,8 +118,7 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { const originX = devicePixelFontSize; const originY = devicePixelFontSize; if (charMetadata) { - const fg = (charMetadata & GpuCharMetadata.FOREGROUND_MASK) >> GpuCharMetadata.FOREGROUND_OFFSET; - this._ctx.fillStyle = `#${fg.toString(16).padStart(6, '0')}`; + this._ctx.fillStyle = `#${charMetadata.toString(16).padStart(8, '0')}`; } else { this._ctx.fillStyle = colorMap[TokenMetadata.getForeground(metadata)]; } diff --git a/src/vs/editor/browser/gpu/raster/raster.ts b/src/vs/editor/browser/gpu/raster/raster.ts index d83b55665dbf5..c86b1649e34b7 100644 --- a/src/vs/editor/browser/gpu/raster/raster.ts +++ b/src/vs/editor/browser/gpu/raster/raster.ts @@ -65,11 +65,3 @@ export interface IRasterizedGlyph { */ originOffset: { x: number; y: number }; } - -export const enum GpuCharMetadata { - FOREGROUND_MASK /* */ = 0b00000000_11111111_11111111_11111111, - OPACITY_MASK /* */ = 0b11111111_00000000_00000000_00000000, - - FOREGROUND_OFFSET = 0, - OPACITY_OFFSET = 24, -} From 9b4c43bf852c0af2accfe60257862cded58b064a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 22 Nov 2024 06:56:49 -0800 Subject: [PATCH 17/20] Add tests for toString and toNumber24Bit --- src/vs/base/test/common/color.test.ts | 74 +++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/vs/base/test/common/color.test.ts b/src/vs/base/test/common/color.test.ts index 5410575cc3304..c0f439d744ed4 100644 --- a/src/vs/base/test/common/color.test.ts +++ b/src/vs/base/test/common/color.test.ts @@ -81,6 +81,80 @@ suite('Color', () => { assert.deepStrictEqual(new Color(new RGBA(0, 0, 0, 0.58)).blend(new Color(new RGBA(255, 255, 255, 0.33))), new Color(new RGBA(49, 49, 49, 0.719))); }); + suite('toString', () => { + test('alpha channel', () => { + assert.deepStrictEqual(Color.fromHex('#00000000').toString(), 'rgba(0, 0, 0, 0)'); + assert.deepStrictEqual(Color.fromHex('#00000080').toString(), 'rgba(0, 0, 0, 0.5)'); + assert.deepStrictEqual(Color.fromHex('#000000FF').toString(), '#000000'); + }); + + test('opaque', () => { + assert.deepStrictEqual(Color.fromHex('#000000').toString().toUpperCase(), '#000000'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#FFFFFF').toString().toUpperCase(), '#FFFFFF'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#FF0000').toString().toUpperCase(), '#FF0000'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#00FF00').toString().toUpperCase(), '#00FF00'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#0000FF').toString().toUpperCase(), '#0000FF'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#FFFF00').toString().toUpperCase(), '#FFFF00'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#00FFFF').toString().toUpperCase(), '#00FFFF'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#FF00FF').toString().toUpperCase(), '#FF00FF'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#C0C0C0').toString().toUpperCase(), '#C0C0C0'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#808080').toString().toUpperCase(), '#808080'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#800000').toString().toUpperCase(), '#800000'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#808000').toString().toUpperCase(), '#808000'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#008000').toString().toUpperCase(), '#008000'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#800080').toString().toUpperCase(), '#800080'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#008080').toString().toUpperCase(), '#008080'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#000080').toString().toUpperCase(), '#000080'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#010203').toString().toUpperCase(), '#010203'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#040506').toString().toUpperCase(), '#040506'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#070809').toString().toUpperCase(), '#070809'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#0a0A0a').toString().toUpperCase(), '#0a0A0a'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#0b0B0b').toString().toUpperCase(), '#0b0B0b'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#0c0C0c').toString().toUpperCase(), '#0c0C0c'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#0d0D0d').toString().toUpperCase(), '#0d0D0d'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#0e0E0e').toString().toUpperCase(), '#0e0E0e'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#0f0F0f').toString().toUpperCase(), '#0f0F0f'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#a0A0a0').toString().toUpperCase(), '#a0A0a0'.toUpperCase()); + }); + }); + + suite('toNumber24Bit', () => { + test('alpha channel', () => { + assert.deepStrictEqual(Color.fromHex('#00000000').toNumber24Bit(), 0x00000000); + assert.deepStrictEqual(Color.fromHex('#00000080').toNumber24Bit(), 0x00000080); + assert.deepStrictEqual(Color.fromHex('#000000FF').toNumber24Bit(), 0x000000FF); + }); + + test('opaque', () => { + assert.deepStrictEqual(Color.fromHex('#000000').toNumber24Bit(), 0x000000FF); + assert.deepStrictEqual(Color.fromHex('#FFFFFF').toNumber24Bit(), 0xFFFFFFFF); + assert.deepStrictEqual(Color.fromHex('#FF0000').toNumber24Bit(), 0xFF0000FF); + assert.deepStrictEqual(Color.fromHex('#00FF00').toNumber24Bit(), 0x00FF00FF); + assert.deepStrictEqual(Color.fromHex('#0000FF').toNumber24Bit(), 0x0000FFFF); + assert.deepStrictEqual(Color.fromHex('#FFFF00').toNumber24Bit(), 0xFFFF00FF); + assert.deepStrictEqual(Color.fromHex('#00FFFF').toNumber24Bit(), 0x00FFFFFF); + assert.deepStrictEqual(Color.fromHex('#FF00FF').toNumber24Bit(), 0xFF00FFFF); + assert.deepStrictEqual(Color.fromHex('#C0C0C0').toNumber24Bit(), 0xC0C0C0FF); + assert.deepStrictEqual(Color.fromHex('#808080').toNumber24Bit(), 0x808080FF); + assert.deepStrictEqual(Color.fromHex('#800000').toNumber24Bit(), 0x800000FF); + assert.deepStrictEqual(Color.fromHex('#808000').toNumber24Bit(), 0x808000FF); + assert.deepStrictEqual(Color.fromHex('#008000').toNumber24Bit(), 0x008000FF); + assert.deepStrictEqual(Color.fromHex('#800080').toNumber24Bit(), 0x800080FF); + assert.deepStrictEqual(Color.fromHex('#008080').toNumber24Bit(), 0x008080FF); + assert.deepStrictEqual(Color.fromHex('#000080').toNumber24Bit(), 0x000080FF); + assert.deepStrictEqual(Color.fromHex('#010203').toNumber24Bit(), 0x010203FF); + assert.deepStrictEqual(Color.fromHex('#040506').toNumber24Bit(), 0x040506FF); + assert.deepStrictEqual(Color.fromHex('#070809').toNumber24Bit(), 0x070809FF); + assert.deepStrictEqual(Color.fromHex('#0a0A0a').toNumber24Bit(), 0x0a0A0aFF); + assert.deepStrictEqual(Color.fromHex('#0b0B0b').toNumber24Bit(), 0x0b0B0bFF); + assert.deepStrictEqual(Color.fromHex('#0c0C0c').toNumber24Bit(), 0x0c0C0cFF); + assert.deepStrictEqual(Color.fromHex('#0d0D0d').toNumber24Bit(), 0x0d0D0dFF); + assert.deepStrictEqual(Color.fromHex('#0e0E0e').toNumber24Bit(), 0x0e0E0eFF); + assert.deepStrictEqual(Color.fromHex('#0f0F0f').toNumber24Bit(), 0x0f0F0fFF); + assert.deepStrictEqual(Color.fromHex('#a0A0a0').toNumber24Bit(), 0xa0A0a0FF); + }); + }); + suite('HSLA', () => { test('HSLA.toRGBA', () => { assert.deepStrictEqual(HSLA.toRGBA(new HSLA(0, 0, 0, 0)), new RGBA(0, 0, 0, 0)); From 6de7763887dffc0e87e153842bc038b54ae0dbeb Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 22 Nov 2024 07:23:11 -0800 Subject: [PATCH 18/20] Speed up and simplify handling of inline decorations --- src/vs/base/common/color.ts | 2 +- .../browser/gpu/fullFileRenderStrategy.ts | 80 ++++++++----------- 2 files changed, 34 insertions(+), 48 deletions(-) diff --git a/src/vs/base/common/color.ts b/src/vs/base/common/color.ts index 67eeeec757a3e..8b1a68294a507 100644 --- a/src/vs/base/common/color.ts +++ b/src/vs/base/common/color.ts @@ -676,7 +676,7 @@ export namespace Color { const b = parseInt(color.groups?.b ?? '0'); return new Color(new RGBA(r, g, b)); } - // TODO: Support more formats + // TODO: Support more formats as needed return parseNamedKeyword(css); } diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index bfb27a81d332b..4b3b3c26c0901 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -12,7 +12,7 @@ import type { IViewLineTokens } from '../../common/tokens/lineTokens.js'; import { ViewEventHandler } from '../../common/viewEventHandler.js'; import { ViewEventType, type ViewConfigurationChangedEvent, type ViewDecorationsChangedEvent, type ViewLinesChangedEvent, type ViewLinesDeletedEvent, type ViewLinesInsertedEvent, type ViewScrollChangedEvent, type ViewTokensChangedEvent, type ViewZonesChangedEvent } from '../../common/viewEvents.js'; import type { ViewportData } from '../../common/viewLayout/viewLinesViewportData.js'; -import type { ViewLineRenderingData } from '../../common/viewModel.js'; +import type { InlineDecoration, ViewLineRenderingData } from '../../common/viewModel.js'; import type { ViewContext } from '../../common/viewModel/viewContext.js'; import type { ViewLineOptions } from '../viewParts/viewLines/viewLineOptions.js'; import type { ITextureAtlasPageGlyph } from './atlas/atlas.js'; @@ -233,8 +233,11 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend } update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): number { - // Pre-allocate variables to be shared within the loop - don't trust the JIT compiler to do - // this optimization to avoid additional blocking time in garbage collector + // IMPORTANT: This is a hot function. Variables are pre-allocated and shared within the + // loop. This is done so we don't need to trust the JIT compiler to do this optimization to + // avoid potential additional blocking time in garbage collector which is a common cause of + // dropped frames. + let chars = ''; let y = 0; let x = 0; @@ -251,6 +254,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend let charMetadata = 0; let lineData: ViewLineRenderingData; + let decoration: InlineDecoration; let content: string = ''; let fillStartIndex = 0; let fillEndIndex = 0; @@ -315,8 +319,6 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend } } - const decorations = viewportData.getDecorationsInViewport(); - for (y = viewportData.startLineNumber; y <= viewportData.endLineNumber; y++) { // Only attempt to render lines that the GPU renderer can handle @@ -331,14 +333,10 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend if (upToDateLines.has(y)) { continue; } + dirtyLineStart = Math.min(dirtyLineStart, y); dirtyLineEnd = Math.max(dirtyLineEnd, y); - const inlineDecorations = decorations.filter(e => ( - e.range.startLineNumber <= y && e.range.endLineNumber >= y && - e.options.inlineClassName - )); - lineData = viewportData.getViewLineRenderingData(y); content = lineData.content; xOffset = 0; @@ -363,48 +361,36 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend chars = content.charAt(x); charMetadata = 0; - // TODO: We'd want to optimize pulling the decorations in order - const cellDecorations = inlineDecorations.filter(decoration => { - // This is Range.strictContainsPosition except it's working at the cell level. - if (y < decoration.range.startLineNumber || y > decoration.range.endLineNumber) { - return false; - } - if (y === decoration.range.startLineNumber && x < decoration.range.startColumn - 1) { - return false; - } - if (y === decoration.range.endLineNumber && x >= decoration.range.endColumn - 1) { - return false; + // Apply supported inline decoration styles to the cell metadata + for (decoration of lineData.inlineDecorations) { + // This is Range.strictContainsPosition except it works at the cell level, + // it's also inlined to avoid overhead. + if ( + (y < decoration.range.startLineNumber || y > decoration.range.endLineNumber) || + (y === decoration.range.startLineNumber && x < decoration.range.startColumn - 1) || + (y === decoration.range.endLineNumber && x >= decoration.range.endColumn - 1) + ) { + continue; } - return true; - }); - - // Only lines containing fully supported inline decorations should have made it - // this far. - const inlineStyles: Map = new Map(); - for (const decoration of cellDecorations) { - if (!decoration.options.inlineClassName) { - throw new BugIndicatingError('Unexpected inline decoration without class name'); - } - const rules = ViewGpuContext.decorationCssRuleExtractor.getStyleRules(this._viewGpuContext.canvas.domNode, decoration.options.inlineClassName); + + const rules = ViewGpuContext.decorationCssRuleExtractor.getStyleRules(this._viewGpuContext.canvas.domNode, decoration.inlineClassName); for (const rule of rules) { for (const r of rule.style) { - inlineStyles.set(r, rule.styleMap.get(r)?.toString() ?? ''); - } - } - } - - for (const [key, value] of inlineStyles.entries()) { - switch (key) { - case 'color': { - // TODO: This parsing/error handling should move into canRender so fallback to DOM works - const parsedColor = Color.Format.CSS.parse(value); - if (!parsedColor) { - throw new BugIndicatingError('Invalid color format ' + value); + const value = rule.styleMap.get(r)?.toString() ?? ''; + switch (r) { + case 'color': { + // TODO: This parsing and error handling should move into canRender so fallback + // to DOM works + const parsedColor = Color.Format.CSS.parse(value); + if (!parsedColor) { + throw new BugIndicatingError('Invalid color format ' + value); + } + charMetadata = parsedColor.toNumber24Bit(); + break; + } + default: throw new BugIndicatingError('Unexpected inline decoration style'); } - charMetadata = parsedColor.toNumber24Bit(); - break; } - default: throw new BugIndicatingError('Unexpected inline decoration style'); } } From 27687b2229467b1409b51a30f9bd024758feec7e Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 22 Nov 2024 07:33:44 -0800 Subject: [PATCH 19/20] Make canRender call non-static Since this now depends on the DOM, it's too difficult and inconsistent to pass in a DOM node to do the inline decoration test on. It's simplest to pass it in to whatever view parts need it. --- src/vs/editor/browser/gpu/fullFileRenderStrategy.ts | 2 +- src/vs/editor/browser/gpu/raster/glyphRasterizer.ts | 6 ------ src/vs/editor/browser/gpu/viewGpuContext.ts | 8 ++++---- src/vs/editor/browser/view.ts | 4 ++-- src/vs/editor/browser/viewParts/gpuMark/gpuMark.ts | 6 ++---- src/vs/editor/browser/viewParts/viewLines/viewLine.ts | 6 ++---- src/vs/editor/browser/viewParts/viewLines/viewLines.ts | 5 +++-- .../editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts | 4 ++-- 8 files changed, 16 insertions(+), 25 deletions(-) diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index 4b3b3c26c0901..7ebc66d68e006 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -322,7 +322,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend for (y = viewportData.startLineNumber; y <= viewportData.endLineNumber; y++) { // Only attempt to render lines that the GPU renderer can handle - if (!ViewGpuContext.canRender(this._viewGpuContext.canvas.domNode, viewLineOptions, viewportData, y)) { + if (!this._viewGpuContext.canRender(viewLineOptions, viewportData, y)) { fillStartIndex = ((y - 1) * this._viewGpuContext.maxGpuCols) * Constants.IndicesPerCell; fillEndIndex = (y * this._viewGpuContext.maxGpuCols) * Constants.IndicesPerCell; cellBuffer.fill(0, fillStartIndex, fillEndIndex); diff --git a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts index a0ae8927b832d..5df78bc465e49 100644 --- a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts @@ -127,12 +127,6 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { this._ctx.textBaseline = 'top'; this._ctx.fillText(chars, originX, originY); - // TODO: Don't draw beyond glyph - how to handle monospace, wide and proportional? - // TODO: Support strikethrough color - if (fontStyle & FontStyle.Strikethrough) { - this._ctx.fillRect(originX, originY + Math.round(devicePixelFontSize / 2), devicePixelFontSize, Math.max(Math.floor(getActiveWindow().devicePixelRatio), 1)); - } - const imageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); this._findGlyphBoundingBox(imageData, this._workGlyph.boundingBox); // const offset = { diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index 883c75d75bdd6..b7c2bb9cbe82d 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -141,7 +141,7 @@ export class ViewGpuContext extends Disposable { * renderer. Eventually this should trend all lines, except maybe exceptional cases like * decorations that use class names. */ - public static canRender(container: HTMLElement, options: ViewLineOptions, viewportData: ViewportData, lineNumber: number): boolean { + public canRender(options: ViewLineOptions, viewportData: ViewportData, lineNumber: number): boolean { const data = viewportData.getViewLineRenderingData(lineNumber); // Check if the line has simple attributes that aren't supported @@ -158,7 +158,7 @@ export class ViewGpuContext extends Disposable { if (data.inlineDecorations.length > 0) { let supported = true; for (const decoration of data.inlineDecorations) { - const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(container, decoration.inlineClassName); + const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(this.canvas.domNode, decoration.inlineClassName); supported &&= styleRules.every(rule => { // Pseudo classes aren't supported currently if (rule.selectorText.includes(':')) { @@ -184,7 +184,7 @@ export class ViewGpuContext extends Disposable { /** * Like {@link canRender} but returns detailed information about why the line cannot be rendered. */ - public static canRenderDetailed(container: HTMLElement, options: ViewLineOptions, viewportData: ViewportData, lineNumber: number): string[] { + public canRenderDetailed(options: ViewLineOptions, viewportData: ViewportData, lineNumber: number): string[] { const data = viewportData.getViewLineRenderingData(lineNumber); const reasons: string[] = []; if (data.containsRTL) { @@ -201,7 +201,7 @@ export class ViewGpuContext extends Disposable { const problemSelectors: string[] = []; const problemRules: string[] = []; for (const decoration of data.inlineDecorations) { - const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(container, decoration.inlineClassName); + const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(this.canvas.domNode, decoration.inlineClassName); supported &&= styleRules.every(rule => { // Pseudo classes aren't supported currently if (rule.selectorText.includes(':')) { diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index 0b90dab63ed16..10ecd41bb1663 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -167,7 +167,7 @@ export class View extends ViewEventHandler { this._viewParts.push(this._scrollbar); // View Lines - this._viewLines = new ViewLines(this._context, this._linesContent); + this._viewLines = new ViewLines(this._context, this._viewGpuContext, this._linesContent); if (this._viewGpuContext) { this._viewLinesGpu = this._instantiationService.createInstance(ViewLinesGpu, this._context, this._viewGpuContext); } @@ -199,7 +199,7 @@ export class View extends ViewEventHandler { marginViewOverlays.addDynamicOverlay(new LinesDecorationsOverlay(this._context)); marginViewOverlays.addDynamicOverlay(new LineNumbersOverlay(this._context)); if (this._viewGpuContext) { - marginViewOverlays.addDynamicOverlay(new GpuMarkOverlay(this._context)); + marginViewOverlays.addDynamicOverlay(new GpuMarkOverlay(this._context, this._viewGpuContext)); } // Glyph margin widgets diff --git a/src/vs/editor/browser/viewParts/gpuMark/gpuMark.ts b/src/vs/editor/browser/viewParts/gpuMark/gpuMark.ts index 861ff529cdc47..733cd8e681a0c 100644 --- a/src/vs/editor/browser/viewParts/gpuMark/gpuMark.ts +++ b/src/vs/editor/browser/viewParts/gpuMark/gpuMark.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getActiveDocument } from '../../../../base/browser/dom.js'; import * as viewEvents from '../../../common/viewEvents.js'; import { ViewContext } from '../../../common/viewModel/viewContext.js'; import { ViewGpuContext } from '../../gpu/viewGpuContext.js'; @@ -23,7 +22,7 @@ export class GpuMarkOverlay extends DynamicViewOverlay { private _renderResult: string[] | null; - constructor(context: ViewContext) { + constructor(context: ViewContext, private readonly _viewGpuContext: ViewGpuContext) { super(); this._context = context; this._renderResult = null; @@ -78,8 +77,7 @@ export class GpuMarkOverlay extends DynamicViewOverlay { const output: string[] = []; for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) { const lineIndex = lineNumber - visibleStartLineNumber; - // TODO: How to get the container? - const cannotRenderReasons = ViewGpuContext.canRenderDetailed(getActiveDocument().querySelector('.view-lines')!, options, viewportData, lineNumber); + const cannotRenderReasons = this._viewGpuContext.canRenderDetailed(options, viewportData, lineNumber); output[lineIndex] = cannotRenderReasons.length ? `
` : ''; } diff --git a/src/vs/editor/browser/viewParts/viewLines/viewLine.ts b/src/vs/editor/browser/viewParts/viewLines/viewLine.ts index d25238bb4cf54..16229cd928a9a 100644 --- a/src/vs/editor/browser/viewParts/viewLines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/viewLines/viewLine.ts @@ -19,7 +19,6 @@ import { EditorFontLigatures } from '../../../common/config/editorOptions.js'; import { DomReadingContext } from './domReadingContext.js'; import type { ViewLineOptions } from './viewLineOptions.js'; import { ViewGpuContext } from '../../gpu/viewGpuContext.js'; -import { getActiveDocument } from '../../../../base/browser/dom.js'; const canUseFastRenderedViewLine = (function () { if (platform.isNative) { @@ -55,7 +54,7 @@ export class ViewLine implements IVisibleLine { private _isMaybeInvalid: boolean; private _renderedViewLine: IRenderedViewLine | null; - constructor(options: ViewLineOptions) { + constructor(private readonly _viewGpuContext: ViewGpuContext | undefined, options: ViewLineOptions) { this._options = options; this._isMaybeInvalid = true; this._renderedViewLine = null; @@ -99,8 +98,7 @@ export class ViewLine implements IVisibleLine { } public renderLine(lineNumber: number, deltaTop: number, lineHeight: number, viewportData: ViewportData, sb: StringBuilder): boolean { - // TODO: How to get the container? - if (this._options.useGpu && ViewGpuContext.canRender(getActiveDocument().querySelector('.view-lines')!, this._options, viewportData, lineNumber)) { + if (this._options.useGpu && this._viewGpuContext?.canRender(this._options, viewportData, lineNumber)) { this._renderedViewLine?.domNode?.domNode.remove(); this._renderedViewLine = null; return false; diff --git a/src/vs/editor/browser/viewParts/viewLines/viewLines.ts b/src/vs/editor/browser/viewParts/viewLines/viewLines.ts index b93a0028b0bbf..6de14aedb6aeb 100644 --- a/src/vs/editor/browser/viewParts/viewLines/viewLines.ts +++ b/src/vs/editor/browser/viewParts/viewLines/viewLines.ts @@ -25,6 +25,7 @@ import { ViewportData } from '../../../common/viewLayout/viewLinesViewportData.j import { Viewport } from '../../../common/viewModel.js'; import { ViewContext } from '../../../common/viewModel/viewContext.js'; import { ViewLineOptions } from './viewLineOptions.js'; +import type { ViewGpuContext } from '../../gpu/viewGpuContext.js'; class LastRenderedData { @@ -125,7 +126,7 @@ export class ViewLines extends ViewPart implements IViewLines { private _stickyScrollEnabled: boolean; private _maxNumberStickyLines: number; - constructor(context: ViewContext, linesContent: FastDomNode) { + constructor(context: ViewContext, viewGpuContext: ViewGpuContext | undefined, linesContent: FastDomNode) { super(context); const conf = this._context.configuration; @@ -145,7 +146,7 @@ export class ViewLines extends ViewPart implements IViewLines { this._linesContent = linesContent; this._textRangeRestingSpot = document.createElement('div'); this._visibleLines = new VisibleLinesCollection({ - createLine: () => new ViewLine(this._viewLineOptions), + createLine: () => new ViewLine(viewGpuContext, this._viewLineOptions), }); this.domNode = this._visibleLines.domNode; diff --git a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts index cdaa217d5e5df..f483e0eda95e6 100644 --- a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts +++ b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts @@ -555,7 +555,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { if (!this._lastViewportData || !this._lastViewLineOptions) { return undefined; } - if (!ViewGpuContext.canRender(this._viewGpuContext.canvas.domNode, this._lastViewLineOptions, this._lastViewportData, lineNumber)) { + if (!this._viewGpuContext.canRender(this._lastViewLineOptions, this._lastViewportData, lineNumber)) { return undefined; } @@ -573,7 +573,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { if (!this._lastViewportData || !this._lastViewLineOptions) { return undefined; } - if (!ViewGpuContext.canRender(this._viewGpuContext.canvas.domNode, this._lastViewLineOptions, this._lastViewportData, lineNumber)) { + if (!this._viewGpuContext.canRender(this._lastViewLineOptions, this._lastViewportData, lineNumber)) { return undefined; } const lineData = this._lastViewportData.getViewLineRenderingData(lineNumber); From e6bc2ee20f657fbc8e2ed295da0c8f52bca311f7 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 22 Nov 2024 07:48:05 -0800 Subject: [PATCH 20/20] Get style extraction working for decorations with multiple class names --- src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts b/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts index c05f2d63418d4..9490d2e1e9aeb 100644 --- a/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts +++ b/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts @@ -34,7 +34,7 @@ export class DecorationCssRuleExtractor extends Disposable { } // Set up DOM - this._dummyElement.classList.add(decorationClassName); + this._dummyElement.className = decorationClassName; canvas.appendChild(this._container); // Get rules @@ -43,7 +43,6 @@ export class DecorationCssRuleExtractor extends Disposable { // Tear down DOM canvas.removeChild(this._container); - this._dummyElement.classList.remove(decorationClassName); return rules; }