Repository navigation
[Text] Do not shape the space after fallback text with the fallback font - #22067
Merged
Merged
Conversation
Gillibald
force-pushed
the
fix/fallback-run-absorbs-whitespace
branch
from
August 26, 2026 04:36
bcd4869 to
c075338
Compare
Gillibald
force-pushed
the
fix/fallback-run-absorbs-whitespace
branch
from
August 26, 2026 04:51
c075338 to
b0b6073
Compare
Gillibald
force-pushed
the
fix/fallback-run-absorbs-whitespace
branch
from
August 26, 2026 04:54
b0b6073 to
a85ca0c
Compare
Gillibald
marked this pull request as ready for review
August 26, 2026 04:55
MrJul
force-pushed
the
fix/fallback-run-absorbs-whitespace
branch
from
August 26, 2026 09:51
a85ca0c to
a47255e
Compare
Gillibald
force-pushed
the
fix/fallback-run-absorbs-whitespace
branch
from
August 26, 2026 10:36
a47255e to
5ea3968
Compare
jsuarezruiz
reviewed
Aug 26, 2026
Gillibald
force-pushed
the
fix/fallback-run-absorbs-whitespace
branch
3 times, most recently
from
August 26, 2026 13:26
fdd332c to
717682c
Compare
A fallback run is extended for as long as the fallback font has glyphs instead of ending where the primary font regains coverage. Practically every font maps U+0020, so the space that follows fallback text is pulled into the fallback run and shaped with its space glyph. - itemization: a Hebrew letter followed by " b" must produce a 1-char fallback run, not a 2-char one that swallows the space - measurement: the space after an emoji must have the same advance as the same space elsewhere in the line
The two edges being compared are reached by summing glyph advances along different paths, so abutting bounds can land an ULP apart and a single directional span gets reported as two rectangles. Compare them the way the rest of layout compares coordinates.
Whitespace was exempt from the return-to-primary check so that a space wouldn't split a fallback run. Practically every font maps U+0020, so the exemption let a fallback run reach past the text the default typeface couldn't render and shape the following space with the fallback's own advance - a full em in most emoji fonts, which is the long space reported after an emoji. - the check now applies to spacing whitespace (Zs), so a fallback run ends at the first cluster the default typeface can render; control and format codepoints stay exempt - many fonts map the default-ignorable bidi controls, and a default typeface whose cmap merely has such a mark must not pull it out of the fallback run: the mark renders nothing either way, and the split cuts the run for no reason - a default typeface that cannot shape the run's script is still not a return target for text, but it does reclaim the spacing whitespace between the words, which carries no shaping - a run of pure whitespace no longer becomes the anti-thrashing bias for the next run, so the words on either side of a space keep resolving to the same fallback font Test expectations that encoded the old run structure move with it: a line of emoji separated by spaces is no longer a single run, glyph clusters are relative to each run's own text (the affected helpers now add the run's start), and the spaces of a right-to-left line measure with the primary font, which widens those lines.
MrJul
force-pushed
the
fix/fallback-run-absorbs-whitespace
branch
from
August 26, 2026 15:17
717682c to
ef226ca
Compare
MrJul
approved these changes
Aug 26, 2026
|
You can test this PR using the following package version. |
MrJul
pushed a commit
to MrJul/Avalonia
that referenced
this pull request
Sep 2, 2026
…ont (AvaloniaUI#22067) * Add failing tests for whitespace absorbed into a fallback run A fallback run is extended for as long as the fallback font has glyphs instead of ending where the primary font regains coverage. Practically every font maps U+0020, so the space that follows fallback text is pulled into the fallback run and shaped with its space glyph. - itemization: a Hebrew letter followed by " b" must produce a 1-char fallback run, not a 2-char one that swallows the space - measurement: the space after an emoji must have the same advance as the same space elsewhere in the line * Merge abutting text bounds within the usual float tolerance The two edges being compared are reached by summing glyph advances along different paths, so abutting bounds can land an ULP apart and a single directional span gets reported as two rectangles. Compare them the way the rest of layout compares coordinates. * End a fallback run where the default typeface regains coverage Whitespace was exempt from the return-to-primary check so that a space wouldn't split a fallback run. Practically every font maps U+0020, so the exemption let a fallback run reach past the text the default typeface couldn't render and shape the following space with the fallback's own advance - a full em in most emoji fonts, which is the long space reported after an emoji. - the check now applies to spacing whitespace (Zs), so a fallback run ends at the first cluster the default typeface can render; control and format codepoints stay exempt - many fonts map the default-ignorable bidi controls, and a default typeface whose cmap merely has such a mark must not pull it out of the fallback run: the mark renders nothing either way, and the split cuts the run for no reason - a default typeface that cannot shape the run's script is still not a return target for text, but it does reclaim the spacing whitespace between the words, which carries no shaping - a run of pure whitespace no longer becomes the anti-thrashing bias for the next run, so the words on either side of a space keep resolving to the same fallback font Test expectations that encoded the old run structure move with it: a line of emoji separated by spaces is no longer a single run, glyph clusters are relative to each run's own text (the affected helpers now add the run's start), and the spaces of a right-to-left line measure with the primary font, which widens those lines.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does the pull request do?
Fixes the long space after an emoji reported in #14011: a fallback run swallowed the space that follows it and shaped that space with the fallback font.
Top layer of a stack; based on #22066.
What is the current behavior?
TextCharacters.CreateShapeableRunextends a fallback run for as long as the fallback font has glyphs, rather than ending it where the default typeface regains coverage. Whitespace was explicitly exempt from the return-to-primary check so a space would not split a fallback run - but practically every font maps U+0020, so the space after fallback text is pulled into the fallback run and rendered with that font's space glyph. In most emoji fonts that is a full em.Measured with
FormattedTextatFontSize=16, isolating the space by differencing:Nothing about it is emoji-specific: any fallback font that maps the space leaks the same way. Characters emoji fonts do not map (U+00A0, U+2060, U+200B) already end the run correctly, which is the same rule seen from the other side.
What is the updated/expected behavior with this PR?
A space next to fallback text measures with the default typeface, the same as anywhere else on the line:
Or type an emoji followed by a space into a
TextBox. This matches browsers, where the primary font wins the space in every position - after an emoji, between two emoji, and between two CJK words (measured in Chrome via canvasmeasureTextfor both Arial and Times New Roman).How was the solution implemented (if it's not obvious)?
TryGetShapeableLengthapplies the return-to-primary check to spacing whitespace (Zs) as well, so a fallback run ends at the first cluster the default typeface can render. Only Zs qualifies: control and format codepoints keep their cluster with the fallback run - many fonts map the default-ignorable bidi controls, and a default typeface whose cmap merely has such a mark must not pull it out of the run (TryGetShapeableLength_Does_Not_Reclaim_A_Bidi_Control_As_Whitespacepins this).defaultCanShapeScriptargument. A default typeface that cannot shape the run's script was passed asnulland so was no return target at all, which is why Arabic and Hebrew lines never reached the check. It is still not a return target for text - handing clusters back would block a shaping-capable fallback that merely shares its cmap - but it does reclaim the spacing whitespace between the words, which carries no shaping.GetShapeableCharacters_Keeps_The_Previous_Fallback_Across_A_Spacepins this with two regional subset fonts.TextLineImpl.TryMergeWithLastBoundscompares the two coordinates withMathUtilities.AreCloseinstead of==. The edges are reached by summing glyph advances along different paths, so abutting bounds can land an ULP apart (43.20703125 vs 43.20703124999999) and one directional span gets reported as two rectangles.The first commit adds the failing tests.
Test expectations that encoded the old run structure moved with it. Two are worth a reviewer's eye: glyph clusters are relative to each run's own text (the affected helpers now add the run's start via
TextTestHelper.GetStartCharIndex, and one helper also indexed its rect list with a per-run glyph index), and the right-to-left trimming rows concatenate run texts in visual run order, so the same characters regroup -"…שתיים one"becomes"… שתיים one".Gates: Skia unit 445/445, Base unit 3136/3136, Skia render tests 370/370 with no golden image changed.
Checklist
Breaking changes
No API changes, but the layout of existing text changes where fallback text and whitespace meet:
Obsoletions / Deprecations
None.
Fixed issues
Fixes #14011