Skip to content

desktop: list only fonts actually installed, using a local() lookup - #239

Open
raymondginger2018-sudo wants to merge 1 commit into
HKUDS:mainfrom
raymondginger2018-sudo:feat/font-availability-detection
Open

raymondginger2018-sudo wants to merge 1 commit into
HKUDS:mainfrom
raymondginger2018-sudo:feat/font-availability-detection

Conversation

@raymondginger2018-sudo

Copy link
Copy Markdown
Contributor

What this fixes

The Appearance → Fonts family picker is meant to list only the families the machine actually has. It does not: the presence test behind it is document.fonts.check(), which answers "can this text be rendered?", not "is this family installed?". A family the machine lacks still renders through the fallback, so check() says yes to everything and every candidate survives the filter.

Measured on Edge/WebView2 153.0.4234 — the engine Tauri uses on Windows, and the one this picker has to work in:

Probe Result
document.fonts.check(\12px "…"`)` over 39 families swept from the Windows font registry true for all 39, including the sentinel __Absent Font 12345__
new FontFace(name, \local("…")`).load()` over the 17 candidates resolves for the 6 the machine has, rejects (NetworkError) for the other 11
Shipped module run in that engine offers exactly those 6: Segoe UI Variable Text, Segoe UI, Cascadia Code, Consolas, Microsoft YaHei, Noto Sans SC

So the picker currently offers fonts the machine does not have, and the drop-down is identical on every machine.

The fix

local() goes through font matching and answers the real question: it resolves for an installed family and rejects for an absent one. It is the only practical option here — queryLocalFonts() enumerates the system list directly but needs a user gesture and a permission grant, so it cannot back a picker that is populated when the settings page opens.

  • isFontAvailable / availableFontCandidates become async, awaiting new FontFace(name, \local("…")`).load()`; a failed load is the negative answer.
  • The settings component holds the result in state (useState + useEffect) instead of a useMemo. The probe runs once per mount, as before.
  • A probe loads a throwaway face and never registers it, so document.fonts is left untouched.
  • An engine without FontFace, or one that refuses local lookups, reports nothing rather than claiming every family exists.
  • Segoe UI VariableSegoe UI Variable Text: the bare name is not a family the system resolves (see below), so that candidate was dead.

Reconciliation with the OS font registry

15 of the 17 candidates agree by exact name. The two remaining rows are the registry labelling font files rather than families, confirmed independently by reading the name table inside the font files:

  • Cascadia Code Regular is the registry label for CascadiaCode.ttf, whose family (name ID 1) is Cascadia Code — which local() resolves.
  • SegUIVar.ttf carries the family name Segoe UI Variable, but the system exposes only its optical sizes as families: local("Segoe UI Variable") fails, local("Segoe UI Variable Text") and local("Segoe UI Variable Display") resolve. Hence the candidate change.

No installed family is wrongly filtered out, and the false positive the canvas-measurement approach produced (Roboto, installed nowhere on this machine) does not occur.

Tests

The suite is rebuilt around a stub faithful to the measured engine behaviour — a local() lookup resolves for an installed family and rejects with NetworkError otherwise. The old stub encoded the assumption that check() reports absence, which is exactly how a picker that listed every family stayed green. Added a regression test that asserts document.fonts.check is never consulted, plus coverage for the no-FontFace and refused-lookup paths. 15 tests in the file; full suite 39 files / 267 tests.

Gates

tsc --noEmit, eslint ., vitest run, npm run build and npm run build:web all pass; cargo fmt --check passes. Not run: setup:sidecar / build:sidecar / audit:licenses (no sidecar or Python content in the diff; the sidecar env is absent and needs a network install) and cargo clippy / cargo test --all-targets (no Rust content in the diff). Not verified: behaviour inside a real Tauri window, and font detection on macOS/Linux — the evidence above is from the Windows WebView engine.

Refs #155.

…fonts.check()

The Appearance → Fonts picker is supposed to list only the families the machine
actually has, but the test behind it was `document.fonts.check()`, which answers
"can this text be rendered" rather than "is this family installed". An unmatched
family still renders through the fallback, so `check()` answered true for
everything and every candidate survived the filter.

Measured on Edge/WebView2 153.0.4234 — the engine Tauri uses on Windows, and the
one this picker has to work in — `check()` returned true for all 39 families
swept out of the Windows font registry, including the sentinel
`__Absent Font 12345__`. The picker therefore offered fonts the machine does not
have.

`local()` goes through font matching and answers the real question. In the same
engine it resolved the six families this machine has and rejected the rest
(Roboto, Helvetica, PingFang SC, …). Reconciliation against the Windows font
registry: 15/17 candidates agree by exact name, and the two remaining rows are
the registry labelling font *files* rather than families — `Cascadia Code
Regular` is the full name of the family `Cascadia Code`, and `Segoe UI Variable`
is the family name inside SegUIVar.ttf while only its optical sizes (`… Text`,
`… Display`) resolve as families. `local("Segoe UI Variable")` fails, so the
candidate list now carries `Segoe UI Variable Text`. No installed family is
wrongly filtered out.

The probe is asynchronous, so `isFontAvailable` and `availableFontCandidates`
now return promises, and the settings component holds the result in state rather
than a `useMemo`. A probe loads a throwaway face and never registers it, so it
leaves `document.fonts` untouched. An engine without `FontFace`, or one that
refuses local lookups, reports nothing rather than claiming every family exists.

The suite is rebuilt around a stub faithful to the measured engine behaviour
(resolve for an installed family, NetworkError otherwise), including a
regression test that asserts `document.fonts.check` is never consulted.

Gates: tsc --noEmit, eslint ., vitest run (39 files / 267 tests), npm run build,
npm run build:web and cargo fmt --check all pass.

Refs HKUDS#155
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant