Skip to content

Commit 2b596eb

Browse files
authored
support hat strokes as a hidden feature (#1810)
To use, add e.g. `-#aa10ff` after any color in settings to add `#aa10ff` as the stroke color. For example, my `userColor2` is `#000000-#ffffff`, and my green is `#14ff07-#00a336`. This is intentionally undocumented: It is a hidden feature for folks to play around with, but only if they are paying a lot of attention, at which point they will know that is not officially supported. :) If we decide we like it, we can eventually see about documenting and testing and generally "unhiding" it. ## Checklist - [-] I have added [tests](https://www.cursorless.org/docs/contributing/test-case-recorder/) - [-] I have updated the [docs](https://github.com/cursorless-dev/cursorless/tree/main/docs) and [cheatsheet](https://github.com/cursorless-dev/cursorless/tree/main/cursorless-talon/src/cheatsheet) - [-] I have not broken the cheatsheet
1 parent c1b8440 commit 2b596eb

File tree

2 files changed

+60
-8
lines changed

2 files changed

+60
-8
lines changed

packages/cursorless-vscode/src/ide/vscode/hats/VscodeHatRenderer.ts

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ interface SvgInfo {
5050
svg: string;
5151
svgHeightPx: number;
5252
svgWidthPx: number;
53+
strokeWidth: number;
5354
}
5455

5556
/**
@@ -230,6 +231,7 @@ export default class VscodeHatRenderer {
230231
this.fontMeasurements,
231232
shape,
232233
scaleFactor,
234+
defaultShapeAdjustments[shape].strokeFactor ?? 1,
233235
finalVerticalOffsetEm,
234236
),
235237
];
@@ -248,7 +250,7 @@ export default class VscodeHatRenderer {
248250
];
249251
}
250252

251-
const { svg, svgWidthPx, svgHeightPx } = svgInfo;
253+
const { svgWidthPx, svgHeightPx } = svgInfo;
252254

253255
const { light, dark } = getHatThemeColors(color);
254256

@@ -258,12 +260,18 @@ export default class VscodeHatRenderer {
258260
rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed,
259261
light: {
260262
before: {
261-
contentIconPath: this.constructColoredSvgDataUri(svg, light),
263+
contentIconPath: this.constructColoredSvgDataUri(
264+
svgInfo,
265+
light,
266+
),
262267
},
263268
},
264269
dark: {
265270
before: {
266-
contentIconPath: this.constructColoredSvgDataUri(svg, dark),
271+
contentIconPath: this.constructColoredSvgDataUri(
272+
svgInfo,
273+
dark,
274+
),
267275
},
268276
},
269277
before: {
@@ -307,17 +315,52 @@ export default class VscodeHatRenderer {
307315
return isOk;
308316
}
309317

310-
private constructColoredSvgDataUri(originalSvg: string, color: string) {
311-
const svg = originalSvg
312-
.replace(/fill="(?!none)[^"]+"/g, `fill="${color}"`)
313-
.replace(/fill:(?!none)[^;]+;/g, `fill:${color};`)
318+
private constructColoredSvgDataUri(svgInfo: SvgInfo, color: string) {
319+
const { svg: originalSvg } = svgInfo;
320+
// If color contains a dash, the second part is a stroke.
321+
// If you are code spelunking and have found this undocumented (and thus potentially transient) feature,
322+
// please subscribe to https://github.com/cursorless-dev/cursorless/pull/1810
323+
// so that you can be notified if/when it changes or is removed.
324+
const [fill, stroke] = color.split("-");
325+
let svg = originalSvg
326+
.replace(/fill="(?!none)[^"]+"/g, `fill="${fill}"`)
327+
.replace(/fill:(?!none)[^;]+;/g, `fill:${fill};`)
314328
.replace(/\r?\n/g, " ");
329+
if (stroke !== undefined) {
330+
svg = this.addInnerStrokeToSvg(svgInfo, svg, stroke);
331+
}
315332

316333
const encoded = encodeURIComponent(svg);
317334

318335
return vscode.Uri.parse(`data:image/svg+xml;utf8,${encoded}`);
319336
}
320337

338+
private addInnerStrokeToSvg(
339+
svgInfo: SvgInfo,
340+
svg: string,
341+
stroke: string,
342+
): string {
343+
// All hat svgs have exactly one path element. Extract it.
344+
const pathRegex = /<path[^>]*d="([^"]+)"[^>]*\/>/;
345+
const pathMatch = pathRegex.exec(svg);
346+
if (!pathMatch) {
347+
console.error(`Could not find path in svg: ${svg}`);
348+
return svg;
349+
}
350+
const pathData = pathMatch[1];
351+
const pathEnd = pathMatch.index! + pathMatch[0].length;
352+
353+
// Construct the stroke path and clipPath elements
354+
const clipPathElem = `<clipPath id="clipPath"><path d="${pathData}" /></clipPath>`;
355+
const strokePathElem = `<path d="${pathData}" stroke="${stroke}" stroke-width="${svgInfo.strokeWidth}" fill="none" clip-path="url(#clipPath)" />`;
356+
357+
// Insert the elements into the SVG after the original path.
358+
359+
return (
360+
svg.slice(0, pathEnd) + clipPathElem + strokePathElem + svg.slice(pathEnd)
361+
);
362+
}
363+
321364
/**
322365
* Creates an SVG from the hat SVG that pads, offsets and scales it to end up
323366
* in the right size / place relative to the character it will be placed over.
@@ -326,13 +369,15 @@ export default class VscodeHatRenderer {
326369
* @param fontMeasurements Info about the user's font
327370
* @param shape The hat shape to process
328371
* @param scaleFactor How much to scale the hat
372+
* @param strokeFactor How much to scale the width of the stroke
329373
* @param hatVerticalOffsetEm How far off top of characters should hats be
330374
* @returns An object with the new SVG and its measurements
331375
*/
332376
private processSvg(
333377
fontMeasurements: FontMeasurements,
334378
shape: HatShape,
335379
scaleFactor: number,
380+
strokeFactor: number,
336381
hatVerticalOffsetEm: number,
337382
): SvgInfo | null {
338383
const iconPath =
@@ -394,10 +439,14 @@ export default class VscodeHatRenderer {
394439
`height="${svgHeightPx}px">` +
395440
`<g transform="scale(${widthFactor}, 1)">${innerSvg}</g></svg>`;
396441

442+
const strokeWidth =
443+
(1.4 * strokeFactor * originalViewBoxWidth) / svgWidthPx;
444+
397445
return {
398446
svg,
399447
svgHeightPx,
400448
svgWidthPx,
449+
strokeWidth,
401450
};
402451
}
403452

packages/cursorless-vscode/src/ide/vscode/hats/shapeAdjustments.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { HatShape } from "../hatStyles.types";
22

33
export interface HatAdjustments {
44
sizeAdjustment?: number;
5+
strokeFactor?: number;
56
verticalOffset?: number;
67
}
78

@@ -23,7 +24,9 @@ export const defaultShapeAdjustments: IndividualHatAdjustmentMap = {
2324
wing: {
2425
sizeAdjustment: -2.5,
2526
},
26-
hole: {},
27+
hole: {
28+
strokeFactor: 0.7,
29+
},
2730
frame: {
2831
sizeAdjustment: -20,
2932
},

0 commit comments

Comments
 (0)