@@ -50,6 +50,7 @@ interface SvgInfo {
50
50
svg : string ;
51
51
svgHeightPx : number ;
52
52
svgWidthPx : number ;
53
+ strokeWidth : number ;
53
54
}
54
55
55
56
/**
@@ -230,6 +231,7 @@ export default class VscodeHatRenderer {
230
231
this . fontMeasurements ,
231
232
shape ,
232
233
scaleFactor ,
234
+ defaultShapeAdjustments [ shape ] . strokeFactor ?? 1 ,
233
235
finalVerticalOffsetEm ,
234
236
) ,
235
237
] ;
@@ -248,7 +250,7 @@ export default class VscodeHatRenderer {
248
250
] ;
249
251
}
250
252
251
- const { svg , svgWidthPx, svgHeightPx } = svgInfo ;
253
+ const { svgWidthPx, svgHeightPx } = svgInfo ;
252
254
253
255
const { light, dark } = getHatThemeColors ( color ) ;
254
256
@@ -258,12 +260,18 @@ export default class VscodeHatRenderer {
258
260
rangeBehavior : vscode . DecorationRangeBehavior . ClosedClosed ,
259
261
light : {
260
262
before : {
261
- contentIconPath : this . constructColoredSvgDataUri ( svg , light ) ,
263
+ contentIconPath : this . constructColoredSvgDataUri (
264
+ svgInfo ,
265
+ light ,
266
+ ) ,
262
267
} ,
263
268
} ,
264
269
dark : {
265
270
before : {
266
- contentIconPath : this . constructColoredSvgDataUri ( svg , dark ) ,
271
+ contentIconPath : this . constructColoredSvgDataUri (
272
+ svgInfo ,
273
+ dark ,
274
+ ) ,
267
275
} ,
268
276
} ,
269
277
before : {
@@ -307,17 +315,52 @@ export default class VscodeHatRenderer {
307
315
return isOk ;
308
316
}
309
317
310
- private constructColoredSvgDataUri ( originalSvg : string , color : string ) {
311
- const svg = originalSvg
312
- . replace ( / f i l l = " (? ! n o n e ) [ ^ " ] + " / g, `fill="${ color } "` )
313
- . replace ( / f i l l : (? ! n o n e ) [ ^ ; ] + ; / 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 ( / f i l l = " (? ! n o n e ) [ ^ " ] + " / g, `fill="${ fill } "` )
327
+ . replace ( / f i l l : (? ! n o n e ) [ ^ ; ] + ; / g, `fill:${ fill } ;` )
314
328
. replace ( / \r ? \n / g, " " ) ;
329
+ if ( stroke !== undefined ) {
330
+ svg = this . addInnerStrokeToSvg ( svgInfo , svg , stroke ) ;
331
+ }
315
332
316
333
const encoded = encodeURIComponent ( svg ) ;
317
334
318
335
return vscode . Uri . parse ( `data:image/svg+xml;utf8,${ encoded } ` ) ;
319
336
}
320
337
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 = / < p a t h [ ^ > ] * 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
+
321
364
/**
322
365
* Creates an SVG from the hat SVG that pads, offsets and scales it to end up
323
366
* in the right size / place relative to the character it will be placed over.
@@ -326,13 +369,15 @@ export default class VscodeHatRenderer {
326
369
* @param fontMeasurements Info about the user's font
327
370
* @param shape The hat shape to process
328
371
* @param scaleFactor How much to scale the hat
372
+ * @param strokeFactor How much to scale the width of the stroke
329
373
* @param hatVerticalOffsetEm How far off top of characters should hats be
330
374
* @returns An object with the new SVG and its measurements
331
375
*/
332
376
private processSvg (
333
377
fontMeasurements : FontMeasurements ,
334
378
shape : HatShape ,
335
379
scaleFactor : number ,
380
+ strokeFactor : number ,
336
381
hatVerticalOffsetEm : number ,
337
382
) : SvgInfo | null {
338
383
const iconPath =
@@ -394,10 +439,14 @@ export default class VscodeHatRenderer {
394
439
`height="${ svgHeightPx } px">` +
395
440
`<g transform="scale(${ widthFactor } , 1)">${ innerSvg } </g></svg>` ;
396
441
442
+ const strokeWidth =
443
+ ( 1.4 * strokeFactor * originalViewBoxWidth ) / svgWidthPx ;
444
+
397
445
return {
398
446
svg,
399
447
svgHeightPx,
400
448
svgWidthPx,
449
+ strokeWidth,
401
450
} ;
402
451
}
403
452
0 commit comments