text/v2: `(*GoTextFace).Metrics` should reflect the face's font variations
Operating System
- Windows
- macOS
- Linux
- FreeBSD
- OpenBSD
- Android
- iOS
- Nintendo Switch
- PlayStation 5
- Xbox
- Web Browsers
What feature would you like to be added?
(*GoTextFace).Metrics should report the metrics of the face's own variation coordinates. Today the variations set on a face never reach the metrics computation, so a face with SetVariation applied reports the metrics of the font's default coordinates.
The variations are dropped at the call boundary, which passes only the size:
// Metrics implements Face.
func (g *GoTextFace) Metrics() Metrics {
return g.Source.metrics(g.Size)
}GoTextFaceSource.metrics scales a set of unscaled values read once from the shared go-text face, and g.variations is not among its inputs.
These values are not constants of the font. go-text threads the face's current coordinates through every one of them:
func (f *Face) FontHExtents() (FontExtents, bool) {
out.Ascender, ok1 = f.Font.getPositionCommon(metricsTagHorizontalAscender, f.coords)
...
}
case XHeight:
if f.os2.version < 2 {
return f.runeHeight('x') // glyph bbox, at the current coordinates
}
return float32(f.os2.sxHeigh) + f.mvar.getVar(tagXHeight, f.coords)Reading them from one *font.Face (RobotoFlex) with nothing changing but SetVariations:
| coordinates | HAscent | HDescent | LineGap | XHeight | CapHeight |
|---|---|---|---|---|---|
| default | 1900 | 500 | 0 | 905 | 1556 |
wght=100 |
1900 | 500 | 0 | 1052 | 1456 |
wght=900 |
1900 | 500 | 0 | 1056.9999 | 1456 |
| back to default | 1900 | 500 | 0 | 905 | 1556 |
RobotoFlex's x-height moves by about 16% across the weight axis while its ascender and descender stay put, because it supplies no MVAR deltas for those two. A font that does supply them would see the ascender and descender vary as well, since they go through getPositionCommon(..., f.coords).
A sketch of what the change involves:
- pass the face's variations (or the already-computed
variationsString) intoGoTextFaceSource.metrics - key the cached unscaled metrics by the coordinate set rather than storing a single set per source
- compute them under
shapeMu, since obtaining the values for a coordinate set means installing those coordinates on the shared*font.Facefirst
This changes the values Metrics returns for variable-font faces that have variations set, so it is a behavior change rather than a fix.
For context on the current behavior: a pending fix for a data race in GoTextFaceSource.metrics computes the unscaled metrics eagerly at construction, which makes the reported values deterministically those of the default coordinates. Before that fix they were whichever coordinates happened to be installed when Metrics was first called on the source. Neither reflects the variations of the face being asked.
Why is this needed?
Metrics drive layout, and callers use them to position text they have already asked to be rendered with a specific instance of a variable font. Metrics currently describes a different instance than the one that gets drawn.
The mismatch also reaches Measure, whose height comes straight from the face's metrics:
m := face.Metrics()
if face.direction().isHorizontal() {
secondary := float64(lineCount-1)*lineSpacingInPixels + m.HAscent + m.HDescent
return primary, secondary
}The advance direction of Measure is variation-aware, because it goes through shaping, so a caller measuring a varied face today gets a width for its own coordinates and a height for the default ones. For a font with MVAR deltas on the vertical metrics, those two disagree.
XHeight and CapHeight are the values most affected in practice, and they are exactly the ones reached for when aligning text to a baseline, centering on the x-height, or fitting a cap-height box.
Filed by Claude (Claude Code), on behalf of @hajimehoshi.
Source: hajimehoshi/ebiten