diff --git a/src/vs/base/common/color.ts b/src/vs/base/common/color.ts index caa1d4419f007..8b1a68294a507 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; @@ -630,6 +643,198 @@ 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 as needed + 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; + } + } + /** * 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..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)); @@ -204,6 +278,295 @@ 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')!.rgba, 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)); + 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/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..9c09181751a7a 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,8 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla if (this._logService.getLevel() === LogLevel.Trace) { this._logService.trace('New glyph', { chars, - metadata, + tokenMetadata, + charMetadata, rasterizedGlyph, glyph }); diff --git a/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts b/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts new file mode 100644 index 0000000000000..9490d2e1e9aeb --- /dev/null +++ b/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts @@ -0,0 +1,75 @@ +/*--------------------------------------------------------------------------------------------- + * 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 './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-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; + } + + // Set up DOM + this._dummyElement.className = decorationClassName; + canvas.appendChild(this._container); + + // Get rules + const rules = this._getStyleRules(decorationClassName); + this._ruleCache.set(decorationClassName, rules); + + // Tear down DOM + canvas.removeChild(this._container); + + return rules; + } + + private _getStyleRules(className: string) { + // 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) { + // 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); + } + } + } + } + + return rules; + } +} diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index 1a5919fb4ecec..7ebc66d68e006 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'; @@ -22,7 +22,7 @@ import { GPULifecycle } from './gpuDisposable.js'; import { quadVertices } from './gpuUtils.js'; import { GlyphRasterizer } from './raster/glyphRasterizer.js'; import { ViewGpuContext } from './viewGpuContext.js'; - +import { Color } from '../../../base/common/color.js'; const enum Constants { IndicesPerCell = 6, @@ -138,6 +138,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend } public override onDecorationsChanged(e: ViewDecorationsChangedEvent): boolean { + // TODO: Don't clear all cells if we can avoid it this._invalidateAllLines(); return true; } @@ -221,19 +222,22 @@ 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 { - // 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; @@ -247,7 +251,10 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend let tokenEndIndex = 0; let tokenMetadata = 0; + let charMetadata = 0; + let lineData: ViewLineRenderingData; + let decoration: InlineDecoration; let content: string = ''; let fillStartIndex = 0; let fillEndIndex = 0; @@ -273,7 +280,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 @@ -316,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(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); @@ -327,6 +333,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend if (upToDateLines.has(y)) { continue; } + dirtyLineStart = Math.min(dirtyLineStart, y); dirtyLineEnd = Math.max(dirtyLineEnd, y); @@ -352,6 +359,41 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend break; } chars = content.charAt(x); + charMetadata = 0; + + // 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; + } + + const rules = ViewGpuContext.decorationCssRuleExtractor.getStyleRules(this._viewGpuContext.canvas.domNode, decoration.inlineClassName); + for (const rule of rules) { + for (const r of rule.style) { + 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'); + } + } + } + } + if (chars === ' ' || chars === '\t') { // Zero out glyph to ensure it doesn't get rendered cellIndex = ((y - 1) * this._viewGpuContext.maxGpuCols + x) * Constants.IndicesPerCell; @@ -363,7 +405,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend continue; } - glyph = this._viewGpuContext.atlas.getGlyph(this._glyphRasterizer.value, chars, tokenMetadata); + glyph = this._viewGpuContext.atlas.getGlyph(this._glyphRasterizer.value, 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/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; +} diff --git a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts index 5032b44b1762c..5df78bc465e49 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 { 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, @@ -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,19 @@ 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.tokenMetadata === tokenMetadata && this._workGlyphConfig.charMetadata === charMetadata) { return this._workGlyph; } this._workGlyphConfig.chars = chars; - this._workGlyphConfig.metadata = metadata; - return this._rasterizeGlyph(chars, metadata, colorMap); + this._workGlyphConfig.tokenMetadata = tokenMetadata; + this._workGlyphConfig.charMetadata = charMetadata; + 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 +117,11 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { const originX = devicePixelFontSize; const originY = devicePixelFontSize; - this._ctx.fillStyle = colorMap[TokenMetadata.getForeground(metadata)]; + if (charMetadata) { + this._ctx.fillStyle = `#${charMetadata.toString(16).padStart(8, '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..c86b1649e34b7 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; } diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index 611d6a6c4a180..b7c2bb9cbe82d 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 { DecorationCssRuleExtractor } from './decorationCssRuleExtractor.js'; import { Event } from '../../../base/common/event.js'; import type { IEditorOptions } from '../../common/config/editorOptions.js'; @@ -47,8 +48,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. @@ -136,24 +141,50 @@ 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 canRender(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 || - data.inlineDecorations.length > 0 || 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(this.canvas.domNode, decoration.inlineClassName); + supported &&= styleRules.every(rule => { + // Pseudo classes aren't supported currently + if (rule.selectorText.includes(':')) { + return false; + } + for (const r of rule.style) { + if (!gpuSupportedDecorationCssRules.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. + * Like {@link canRender} but returns detailed information about why the line cannot be rendered. */ - public static canRenderDetailed(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) { @@ -166,7 +197,35 @@ export class ViewGpuContext extends Disposable { reasons.push('continuesWithWrappedLine'); } if (data.inlineDecorations.length > 0) { - reasons.push('inlineDecorations > 0'); + let supported = true; + const problemSelectors: string[] = []; + const problemRules: string[] = []; + for (const decoration of data.inlineDecorations) { + const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(this.canvas.domNode, 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 (!gpuSupportedDecorationCssRules.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 (problemSelectors.length > 0) { + reasons.push(`inlineDecorations with unsupported CSS selectors (\`${problemSelectors.join(', ')}\`)`); + } } if (lineNumber >= GpuRenderLimits.maxGpuLines) { reasons.push('lineNumber >= maxGpuLines'); @@ -174,3 +233,10 @@ export class ViewGpuContext extends Disposable { return reasons; } } + +/** + * A list of fully supported decoration CSS rules that can be used in the GPU renderer. + */ +const gpuSupportedDecorationCssRules = [ + 'color', +]; 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 7019eff5e007e..733cd8e681a0c 100644 --- a/src/vs/editor/browser/viewParts/gpuMark/gpuMark.ts +++ b/src/vs/editor/browser/viewParts/gpuMark/gpuMark.ts @@ -22,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; @@ -77,7 +77,7 @@ 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); + 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 5e555246cea86..16229cd928a9a 100644 --- a/src/vs/editor/browser/viewParts/viewLines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/viewLines/viewLine.ts @@ -54,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; @@ -98,7 +98,7 @@ 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)) { + 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 8d2adeadeec3d..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._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._lastViewLineOptions, this._lastViewportData, lineNumber)) { + if (!this._viewGpuContext.canRender(this._lastViewLineOptions, this._lastViewportData, lineNumber)) { return undefined; } const lineData = this._lastViewportData.getViewLineRenderingData(lineNumber); 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/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 85% 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 5a4353a1d19b4..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,25 +4,26 @@ *--------------------------------------------------------------------------------------------*/ 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; 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) { 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/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; }`, + ]); + }); +}); 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();