Skip to content

Commit

Permalink
support hat strokes as a hidden feature (#1810)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
josharian authored Jan 19, 2024
1 parent c1b8440 commit 2b596eb
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ interface SvgInfo {
svg: string;
svgHeightPx: number;
svgWidthPx: number;
strokeWidth: number;
}

/**
Expand Down Expand Up @@ -230,6 +231,7 @@ export default class VscodeHatRenderer {
this.fontMeasurements,
shape,
scaleFactor,
defaultShapeAdjustments[shape].strokeFactor ?? 1,
finalVerticalOffsetEm,
),
];
Expand All @@ -248,7 +250,7 @@ export default class VscodeHatRenderer {
];
}

const { svg, svgWidthPx, svgHeightPx } = svgInfo;
const { svgWidthPx, svgHeightPx } = svgInfo;

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

Expand All @@ -258,12 +260,18 @@ export default class VscodeHatRenderer {
rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed,
light: {
before: {
contentIconPath: this.constructColoredSvgDataUri(svg, light),
contentIconPath: this.constructColoredSvgDataUri(
svgInfo,
light,
),
},
},
dark: {
before: {
contentIconPath: this.constructColoredSvgDataUri(svg, dark),
contentIconPath: this.constructColoredSvgDataUri(
svgInfo,
dark,
),
},
},
before: {
Expand Down Expand Up @@ -307,17 +315,52 @@ export default class VscodeHatRenderer {
return isOk;
}

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

const encoded = encodeURIComponent(svg);

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

private addInnerStrokeToSvg(
svgInfo: SvgInfo,
svg: string,
stroke: string,
): string {
// All hat svgs have exactly one path element. Extract it.
const pathRegex = /<path[^>]*d="([^"]+)"[^>]*\/>/;
const pathMatch = pathRegex.exec(svg);
if (!pathMatch) {
console.error(`Could not find path in svg: ${svg}`);
return svg;
}
const pathData = pathMatch[1];
const pathEnd = pathMatch.index! + pathMatch[0].length;

// Construct the stroke path and clipPath elements
const clipPathElem = `<clipPath id="clipPath"><path d="${pathData}" /></clipPath>`;
const strokePathElem = `<path d="${pathData}" stroke="${stroke}" stroke-width="${svgInfo.strokeWidth}" fill="none" clip-path="url(#clipPath)" />`;

// Insert the elements into the SVG after the original path.

return (
svg.slice(0, pathEnd) + clipPathElem + strokePathElem + svg.slice(pathEnd)
);
}

/**
* Creates an SVG from the hat SVG that pads, offsets and scales it to end up
* in the right size / place relative to the character it will be placed over.
Expand All @@ -326,13 +369,15 @@ export default class VscodeHatRenderer {
* @param fontMeasurements Info about the user's font
* @param shape The hat shape to process
* @param scaleFactor How much to scale the hat
* @param strokeFactor How much to scale the width of the stroke
* @param hatVerticalOffsetEm How far off top of characters should hats be
* @returns An object with the new SVG and its measurements
*/
private processSvg(
fontMeasurements: FontMeasurements,
shape: HatShape,
scaleFactor: number,
strokeFactor: number,
hatVerticalOffsetEm: number,
): SvgInfo | null {
const iconPath =
Expand Down Expand Up @@ -394,10 +439,14 @@ export default class VscodeHatRenderer {
`height="${svgHeightPx}px">` +
`<g transform="scale(${widthFactor}, 1)">${innerSvg}</g></svg>`;

const strokeWidth =
(1.4 * strokeFactor * originalViewBoxWidth) / svgWidthPx;

return {
svg,
svgHeightPx,
svgWidthPx,
strokeWidth,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { HatShape } from "../hatStyles.types";

export interface HatAdjustments {
sizeAdjustment?: number;
strokeFactor?: number;
verticalOffset?: number;
}

Expand All @@ -23,7 +24,9 @@ export const defaultShapeAdjustments: IndividualHatAdjustmentMap = {
wing: {
sizeAdjustment: -2.5,
},
hole: {},
hole: {
strokeFactor: 0.7,
},
frame: {
sizeAdjustment: -20,
},
Expand Down

0 comments on commit 2b596eb

Please sign in to comment.