Skip to content

Fontique: 2-tier font fallback - #689

Open
nicoburns wants to merge 2 commits into
linebender:mainfrom
nicoburns:rigorous-font-fallback
Open

nicoburns wants to merge 2 commits into
linebender:mainfrom
nicoburns:rigorous-font-fallback

Conversation

@nicoburns

@nicoburns nicoburns commented Jul 14, 2026 •

Copy link
Copy Markdown
Collaborator

Generated with Fable High.

Motivation

Font fallback completely fails to find a matching font for some glyphs.

This is because font fallback was keyed on (script, locale), and resolved by asking the OS for a single font family which matches a per-script sample string. If that one font lacked the actual character (for example, CJK extensions, symbols, punctuation shared between scripts, scripts without samples) the Fontique gave up, even if there was actually an installed font that had the required glyph.

Changes made

Implement 3-tier 2-tier fallback:

Tier A (all backends) - Ordered list of fonts per script (script, locale)

This is the existing fallback mechanism, but modified to work with a list of fonts rather a single font per (script, locale).

  • CoreText uses CTFontCopyDefaultCascadeListForLanguages
  • fontconfig uses font_sort with coverage trimming instead of font_match
  • Android uses all matching locale/script chain entries instead of the first

Tier B (skipped on Android) - per-character system fallback

  • Added Query::set_fallback_chars() where Parley passes the specific characters we are falling back for
  • When script/locale lists (Tier A) lack coverage, the backend is asked for a font covering the actual text (CTFontCreateForString / IDWriteFontFallback::MapCharacters / FC_CHARSET match)
  • This is cached per (character-set, locale) in the collection.

Tier C (all backends) - exhaustive coverage scan

As a last resort, all families are checked for real cmap coverage (guaranteeing a glyph is found if any font has one)

  • Fonts are sorted by name, with hidden/.-fonts last, and LastResort excluded)
  • Positive and negative results are cached, so the scan happens once per unique character set.
  • This scan is expensive, so there is an opt-out via a new exhaustive_fallback: bool runtime setting on Collection

(Tier C removed per Chad's feedback)

This PR also:

  • Removes the query.rs "Hani fallback hack" (macOS: CJK punctuation marks not rendered correctly #597), which is now covered by the general per-character fallback mechanism (PingFang/Heiti hvgl hack remains)
  • Fixes a latent parley bug where empty layouts shaped their cursor-metrics run with a zero font size.

Verification Performed

The AI has run temporary integration tests (not included in this PR) on macOS and Ubuntu.

Tested in Blitz on macOS, and it definitely seems to work and find glyphs where main doesn't.

@nicoburns nicoburns added the C-fontique Relates to the fontique crate label Jul 14, 2026

#[derive(Clone, PartialEq, Eq, Hash)]
struct TextFallbackKey {
chars: SmallVec<[char; 8]>,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is 8 chars a sensible size here?

Comment thread fontique/src/collection/query.rs Outdated
Comment on lines +129 to +130
let chars: SmallVec<[char; 8]> = chars.into_iter().collect();
if self.state.fallback_chars != chars {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can eliminate this allocation.

@nicoburns nicoburns changed the title Fontique: rigorous 3-tier font fallback Fontique: 3-tier font fallback Jul 14, 2026
@nicoburns
nicoburns force-pushed the rigorous-font-fallback branch from 927b895 to 05bdd60 Compare July 14, 2026 12:52
@dfrg

dfrg commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

I don’t have time for a deep dive on this right now but, at a glance, I’m generally supportive of this with a couple caveats:

  1. The char lookup and cache is iffy to me. I’m not opposed in principle but I think we might be able to get by with appending a list of known large coverage fonts to the fallback chain for every query.
  2. I’d like to avoid adding an exhaustive search, even as an option. This is a perf killer and pretty much always produces the wrong result.

Big +1 to returning multiple fallback fonts from the system. We should probably also extend the script sample array to support multiple samples per script. This would help, for example, with selecting the -ExtB CJK fonts on Windows.

@nicoburns nicoburns changed the title Fontique: 3-tier font fallback Fontique: 2-tier font fallback Sep 15, 2026
Script/locale fallback now yields an ordered list of families instead of
a single family: the CoreText cascade list, fontconfig's sorted coverage
list (FcFontSort + trim), all matching Android fallback chain entries,
and DirectWrite's script-sample match.

Also fix empty layouts shaping their cursor-metrics run with a zero font
size by resolving the default style instead of using ResolvedStyle::default().
Add Query::set_fallback_chars so parley can tell the query which
characters a cluster needs covered. When none of the requested or
script/locale fallback families match, the query asks the platform for a
family covering those exact characters (CTFontCreateForString,
IDWriteFontFallback::MapCharacters, FcFontMatch with FC_CHARSET).
Results are cached in the collection per (chars, locale), capped at 1024
entries and cleared on font registration / fallback changes.

This supersedes the hack that appended a Han family to every fallback
list to catch CJK punctuation in Latin text (linebender#597), so remove it.
lucasmerlin added a commit to emilk/egui that referenced this pull request Oct 13, 2026
* Related: #5233, upstream
linebender/parley#644
* [ ] I have followed the instructions in the PR template

On Windows with a non-Chinese locale, fontique's Han fallback is a
single Japanese font (Yu Gothic UI), which lacks many Simplified Chinese
characters like 这 and 还. These rendered as tofu even with Microsoft
YaHei installed.

The example text from the added demo without the fix fails to show some
glyphs:

<img width="582" height="146" alt="image"
src="https://github.com/user-attachments/assets/1f37a778-6d67-431f-9aad-c7bd23e0e360"
/>


`SystemFontProvider` now tries the other Han locales
(Simplified/Traditional Chinese, Japanese, Korean) when the font for the
preferred locale can't render a char.

Simplified Chinese is looked up as `zh-SG` instead of `zh-CN`: fontique
treats `zh-CN` as the default Han locale and caches the result of the
locale-less Han lookup (which every `Query::set_fallbacks` makes) as
that default. So `zh-CN` lookups often returned the Japanese font,
depending on which scripts were looked up first. A TODO points at
linebender/parley#689, which should make this workaround unnecessary.

Also adds a collapsed "System font fallback" section to the Misc Demos
window to check the fallback by eye.

## Testing
Verified on Windows 11 (de-DE): all samples in Misc Demos → System font
fallback render, and `cargo test -p egui_system_fonts --
--include-ignored` passes.

**Still needs testing on macOS and Linux**: open Misc Demos → System
font fallback in `egui_demo_app` and run the ignored tests above. Both
platforms also pick a single fallback font per script in fontique, so
they could have the same problem in milder form, e.g. a Japanese-locale
Mac, or a Linux system with only a Japanese CJK font.

The Misc Demos snapshot was updated on Windows; if CI disagrees,
`./scripts/update_snapshots_from_ci.sh` should fix it.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

C-fontique Relates to the fontique crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fontique fails to find font for ❯ char (U+276F) on macOS Font Fallback not working for at least 汉 on Windows

2 participants