Conversation
Replace the character-by-character glyph lookup in `layout_section()` with a proper text shaping pipeline using harfrust (a Rust port of HarfBuzz). This enables GPOS kerning, ligatures (fi, fl), and correct positioning of combining diacritical marks. The shaping pipeline works as follows: 1. Split text into font-fallback runs (grapheme-cluster-aware) 2. Shape each run with harfrust (GSUB + GPOS) 3. Allocate and position glyphs from the shaping output Key changes: - Add harfrust, unicode-segmentation, unicode-general-category deps - Cache ShaperData on FontFace (parsed GSUB/GPOS tables) - Add shape_text() with buffer flags and variable font support - Add allocate_glyph_by_id() for shaper-produced glyph IDs - Recycle harfrust UnicodeBuffer across layout calls - Handle NOTDEF fallback (combining marks via unicode-general-category) Addresses emilk#2517.
Verify that letter pairs like AV, VA, AT are measurably tighter when laid out together than the sum of their individual widths. This test fails without harfrust text shaping (kern adjustment ≈ 0) and passes with it (kern adjustment > 0.5px).
Remove copy-pasted docstring on segment_into_runs, and set BEGINNING_OF_TEXT on the first run of every paragraph segment (not just the first segment) to match END_OF_TEXT behavior.
…between runs Two bugs in layout_shaped_run: 1. When the shaper returned NOTDEF, glyph_info was resolved via font fallback but the returned FontFaceKey was discarded. The glyph was then allocated with run.font_key (the face that couldn't render it), causing the glyph to silently render as invisible. Now uses the correct fallback font face and its metrics for both allocation and Glyph font_face_height/font_face_ascent. 2. prev_cluster was not reset between runs. Since harfrust cluster values are byte offsets within each run's text, comparing clusters across runs is semantically wrong and could skip extra_letter_spacing at run boundaries (e.g. when both runs start at cluster 0).
…o_runs - Update GlyphAllocation.id docstring to reflect its actual usage (overflow character kerning via legacy kern table) - Replace planning artifact comment on shaper y_offset caching - Document script-mixing limitation in segment_into_runs
|
Preview available at https://egui-pr-preview.github.io/pr/8031-featharfrust-text-shaping View snapshot changes at kitdiff |
Text shaping changes glyph positioning, which affects rendered output. Snapshots regenerated from macOS CI run #23601039294.
Regenerated from macOS CI run #23602213577. 56 snapshots updated to reflect both text shaping changes and v0.34.0 visual updates (scroll fade, etc.).
These were incorrectly overwritten by commit 50d820c. Restored to the correct CI-generated versions.
These were never updated on our branch. Text shaping changes the rendered output for text-heavy side containers. Local UPDATE_SNAPSHOTS=1 confirms only sides/ and rotated/ snapshots needed updating — layout/, visuals/ etc. pass as-is.
Text shaping changes widget widths by ~1px on macOS, causing image size mismatches (e.g. 850->849px). 24 snapshots updated from CI run artifacts.
These will differ from macOS CI — the next CI run will produce .new.png for ALL of them at once, allowing a single bulk fix.
|
Wow, very cool! I haven't looked at the code yet but:
|
The previous code had a custom override for U+2009 (thin space) that clamped the advance width to As for \t, I agree with you : tab stops are a layout-level concept that no shaper handles. I'll add a post-shaping override to restore the
About adding new snapshot tests for the shaping demos (ligatures, kerning), there's a difficulty : the bundled Ubuntu font doesn't have visible ligatures. Would it be acceptable to embed a test-only font in the test fixtures to demonstrate these features ?
I saw that @valadaptive put a lot of work into the Parley integration and concluded it wasn't viable. I hope she finds this approach correct. Moreover, I'm a little embarrassed to be using AI without fully understanding what I'm doing, even though I have good intentions and take many precautions to do things correctly. I hope I'm not wasting your time. |
The text shaper doesn't handle tab stops — override the advance width to TAB_SIZE × space width in layout_shaped_run, matching the previous character-by-character behavior.
Replace 5 loose parameters with a ShapedGlyph struct, removing the need for #[expect(clippy::too_many_arguments)].
|
Can you possibly consider updating skrifa and vello_cpu as well? These will change some of the test images and it may be just as well to do all in one go... |
Update vello_cpu to 0.0.7, which produces slightly different rasterization output. All snapshot tests have been regenerated. Note: skrifa cannot be bumped to 0.41.0 in this PR because it pulls in read-fonts 0.38, while harfrust 0.5.2 depends on read-fonts 0.37. The two FontRef types are incompatible.
|
Thanks for the suggestion! I've bumped However, Note: harfrust's |
Summary
This PR integrates harfrust (a pure-Rust port of HarfBuzz) into epaint's text layout pipeline, replacing the character-by-character glyph positioning with proper OpenType text shaping.
What this enables
kerntable). Pairs like "AV", "VA", "AT" are now properly tightened.Before/After
Kerning, etc.
Ligatures
Architecture
The shaping integrates into the existing pipeline without changing the public API:
Font::segment_into_runs— segments text into contiguous runs by font face (grapheme-cluster aware, never splits combining sequences)FontFace::shape_text— calls harfrust to shape each run, returning glyph IDs + positioned advances/offsetslayout_shaped_run— emitsGlyphstructs from the shaping output, with NOTDEF fallback to other font faces for missing glyphsFontsImplpools aharfrust::UnicodeBufferto avoid per-layout allocationsDisclaimer
I'm far from being a good Rust programmer. Claude Code did most of the heavy lifting here. I did my best and used my limited knowledge to avoid making too many mistakes. If this PR isn't up to quality standards, please don't hesitate to close it.
Test plan
cargo test -p epaint— all 18 text tests pass, including 6 new onescargo clippy -p epaint --all-features— cleancargo fmt— cleantest_gpos_kerning— verifies GPOS kerning tightens "AV", "VA", "AT" pairstest_combining_diacritics— combining tilde doesn't add extra widthtest_shaping_basic_latin— sanity check for Latin texttest_shaping_empty_string— empty input doesn't panictest_shaping_multiple_newlines— newline splitting works correctlytest_shaping_mixed_font_fallback— Latin + emoji in same string