On any host without a platform text layer, every text weight inks with the regular face. Medium and bold are pixel-identical to regular, and an app cannot supply the missing faces itself.
All references are v0.10.1. I also checked main (5 commits ahead, none touching the canvas text code).
The mechanism
Weight mapping is correct up front. textSpanFontId maps a span's weight onto the reserved sans variant ids:
// src/primitives/canvas/text_spans.zig:143-147
return switch (span.weight) {
.regular => ...default_sans_font_id,
.medium => ...default_sans_medium_font_id,
.bold => ...default_sans_bold_font_id,
};
The reference renderer then has nothing to resolve them to:
// src/primitives/canvas/reference.zig:1152-1157
fn referenceFaceForFontId(fonts: []const ReferenceFont, font_id: canvas.FontId) *const font_ttf.Face {
for (fonts) |font| {
if (font.id == font_id) return font.face;
}
if (font_id == canvas.default_mono_font_id) return &font_ttf.geist_mono;
return &font_ttf.geist_regular;
}
src/primitives/canvas/fonts/ contains exactly two files, Geist-Regular.ttf and GeistMono-Regular.ttf, so every sans id that is not mono falls to the last line and inks regular.
An app cannot fill the gap. Registration below canvas.min_registered_font_id fails with error.ReservedFontId (canvas_fonts.zig:147), so the reserved weight ids are unreachable. Pointing typography.font_id at a registered family instead makes it worse: text_spans.zig:142 returns that single id for every span, so a custom font loses weight mapping entirely.
Why it matters
On Linux there is no host text layer at all, so this is the whole UI. Every heading, every bold span in body text, and every button label (buttonFontId returns the medium id) renders at regular weight. Layout stays correct because measurement uses the same face, so nothing breaks. The app just has no typographic hierarchy, which reads as flat and unfinished.
It also reaches macOS in one place: native automate screenshot renders through the same surface, so screenshots of a correctly weighted macOS window come back flat.
Suggested fix, cheapest first
1. Synthesise the weights in the rasteriser. Emboldening the outline for the medium and bold ids needs no new assets and no binary growth, and it is what most CPU rasterisers do when a real face is unavailable. Least faithful, but it restores hierarchy for every existing app with a change confined to the renderer.
2. Let an app register into the reserved sans variant ids. Relaxing the min_registered_font_id check (canvas_fonts.zig:147) for those specific ids lets an app that already ships its own family fill medium and bold. This also fixes a second problem: today there is no way to tell the runtime "this registered face is the bold of that one", which is why a custom family collapses to a single weight.
3. Bundle Geist-Medium and Geist-Bold alongside the regular face and map them in referenceFaceForFontId. Simplest and best-looking, at the cost of two more embedded faces.
1 alone restores hierarchy everywhere. 2 is the one I would want as an app author, because it also solves the custom-family case.
I ship a Zig canvas app on macOS and am preparing a Linux build, so I can test a fix on both.
Related, and the reason I hit this: #421, where a glyph missing from the resolved face paints as a solid block. Same subsystem, separate defect.
On any host without a platform text layer, every text weight inks with the regular face. Medium and bold are pixel-identical to regular, and an app cannot supply the missing faces itself.
All references are v0.10.1. I also checked
main(5 commits ahead, none touching the canvas text code).The mechanism
Weight mapping is correct up front.
textSpanFontIdmaps a span's weight onto the reserved sans variant ids:The reference renderer then has nothing to resolve them to:
src/primitives/canvas/fonts/contains exactly two files,Geist-Regular.ttfandGeistMono-Regular.ttf, so every sans id that is not mono falls to the last line and inks regular.An app cannot fill the gap. Registration below
canvas.min_registered_font_idfails witherror.ReservedFontId(canvas_fonts.zig:147), so the reserved weight ids are unreachable. Pointingtypography.font_idat a registered family instead makes it worse: text_spans.zig:142 returns that single id for every span, so a custom font loses weight mapping entirely.Why it matters
On Linux there is no host text layer at all, so this is the whole UI. Every heading, every bold span in body text, and every button label (
buttonFontIdreturns the medium id) renders at regular weight. Layout stays correct because measurement uses the same face, so nothing breaks. The app just has no typographic hierarchy, which reads as flat and unfinished.It also reaches macOS in one place:
native automate screenshotrenders through the same surface, so screenshots of a correctly weighted macOS window come back flat.Suggested fix, cheapest first
1. Synthesise the weights in the rasteriser. Emboldening the outline for the medium and bold ids needs no new assets and no binary growth, and it is what most CPU rasterisers do when a real face is unavailable. Least faithful, but it restores hierarchy for every existing app with a change confined to the renderer.
2. Let an app register into the reserved sans variant ids. Relaxing the
min_registered_font_idcheck (canvas_fonts.zig:147) for those specific ids lets an app that already ships its own family fill medium and bold. This also fixes a second problem: today there is no way to tell the runtime "this registered face is the bold of that one", which is why a custom family collapses to a single weight.3. Bundle
Geist-MediumandGeist-Boldalongside the regular face and map them inreferenceFaceForFontId. Simplest and best-looking, at the cost of two more embedded faces.1 alone restores hierarchy everywhere. 2 is the one I would want as an app author, because it also solves the custom-family case.
I ship a Zig canvas app on macOS and am preparing a Linux build, so I can test a fix on both.
Related, and the reason I hit this: #421, where a glyph missing from the resolved face paints as a solid block. Same subsystem, separate defect.