Repository navigation
Fix paragraph end handling in bidirectional text lines - #22553
Merged
Merged
Conversation
…a paragraph end A TextEndOfParagraph run must not change bounds, run indices or caret distances of the text before it. Lines whose last text run is right-to-left and sits visually next to the paragraph end report shifted bounds today.
TextLineImpl gave runs without glyphs whatever direction the caller asked about, while BidiReorderer places TextEndOfLine at level 0. When text ends in RTL, the paragraph end sits visually after the trailing RTL run, so it was pulled into that directional span. The right-to-left walk consumes the span visually from the right, so it consumed the paragraph end first and every glyph after it was measured one character off: empty bounds for the first RTL character and a full-range query one character short. Only text sources that end with TextEndOfParagraph (TextBlock, SelectableTextBlock) are affected. - Store the level each run was reordered with on IndexedTextRun. - GetTextBounds and GetDistanceFromCharacterHit read direction from that level; the duplicated content-based direction helper is gone. - A range that includes the paragraph end of an LTR paragraph ending in RTL text now reports the paragraph end as its own empty LTR bounds at the line end instead of merging it into the RTL span.
…graphs The paragraph end belongs at the logical end of the paragraph: the right edge of a left-to-right line and the left edge of a right-to-left line. Caret distance and bounds put it at the right edge in both directions today. - A wrapped TextLayout must get the same lines with and without a TextEndOfParagraph, so moving the paragraph end cannot bring back the extra line RTL wrapped text used to measure.
BidiReorderer forced TextEndOfLine to level 0 so it would always be the last visual run. That hid TextLineImpl reading the visual run array as if its last run were the logical end of the line: in a right-to-left paragraph the end run is visually first, FinalizeLine missed the line break and wrapped text measured an extra line. Level 0 broke the Unicode bidi rule that a paragraph separator takes the paragraph level, and put the paragraph end of a right-to-left line at its right edge, where the line starts. - TextEndOfLine takes the paragraph level again. - FinalizeLine and GetCharacterHitFromDistance find the end run through the logical run order, wherever it sits visually. - Caret distance and bounds at the paragraph end of a right-to-left line now land on the left edge.
1 of 3 tasks
NuGet packagesThe packages for this pull request have been published (workflow run):
|
MrJul
approved these changes
Oct 21, 2026
MrJul
pushed a commit
that referenced
this pull request
Nov 2, 2026
* Add failing test for hit testing a line that ends in RTL text before a paragraph end A TextEndOfParagraph run must not change bounds, run indices or caret distances of the text before it. Lines whose last text run is right-to-left and sits visually next to the paragraph end report shifted bounds today. * Derive a text line run's direction from its reordered bidi level TextLineImpl gave runs without glyphs whatever direction the caller asked about, while BidiReorderer places TextEndOfLine at level 0. When text ends in RTL, the paragraph end sits visually after the trailing RTL run, so it was pulled into that directional span. The right-to-left walk consumes the span visually from the right, so it consumed the paragraph end first and every glyph after it was measured one character off: empty bounds for the first RTL character and a full-range query one character short. Only text sources that end with TextEndOfParagraph (TextBlock, SelectableTextBlock) are affected. - Store the level each run was reordered with on IndexedTextRun. - GetTextBounds and GetDistanceFromCharacterHit read direction from that level; the duplicated content-based direction helper is gone. - A range that includes the paragraph end of an LTR paragraph ending in RTL text now reports the paragraph end as its own empty LTR bounds at the line end instead of merging it into the RTL span. * Add failing test for the paragraph end position in right-to-left paragraphs The paragraph end belongs at the logical end of the paragraph: the right edge of a left-to-right line and the left edge of a right-to-left line. Caret distance and bounds put it at the right edge in both directions today. - A wrapped TextLayout must get the same lines with and without a TextEndOfParagraph, so moving the paragraph end cannot bring back the extra line RTL wrapped text used to measure. * Give the paragraph end the paragraph embedding level BidiReorderer forced TextEndOfLine to level 0 so it would always be the last visual run. That hid TextLineImpl reading the visual run array as if its last run were the logical end of the line: in a right-to-left paragraph the end run is visually first, FinalizeLine missed the line break and wrapped text measured an extra line. Level 0 broke the Unicode bidi rule that a paragraph separator takes the paragraph level, and put the paragraph end of a right-to-left line at its right edge, where the line starts. - TextEndOfLine takes the paragraph level again. - FinalizeLine and GetCharacterHitFromDistance find the end run through the logical run order, wherever it sits visually. - Caret distance and bounds at the paragraph end of a right-to-left line now land on the left edge.
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 two defects in how
TextLineImpltreats theTextEndOfLinerun of bidirectional lines: its direction and its visual position in right-to-left paragraphs.What is the current behavior?
TextLineImpl.GetRunDirectionreturned the caller's direction for any run without glyphs. When a line ends in RTL text, the paragraph end sits visually right after the trailing RTL run, soGetLastDirectionalRunIndexpulled it into that span.GetTextRunBoundsRightToLeftwalks the span from the visual right, consumed the paragraph end first, and measured every glyph after it one character off.BidiReordererforcesTextEndOfLineto level 0 (Fixed an issue RTL wrapped text calculates an extra line for the control desired size #17209). That hidFinalizeLineandGetCharacterHitFromDistancereading the visual run array as if its last run were the logical end of the line. Level 0 contradicts the bidi rule that a paragraph separator takes the paragraph level (UAX # 9, L1), and puts the paragraph end of a right-to-left line at its right edge, where the line starts.What is the updated/expected behavior with this PR?
IndexedTextRuncarries the level each run was reordered with, andTextLineImpltakes a run's direction from it. The duplicated content-based direction helper inGetDistanceFromCharacterHitis gone.TextEndOfLinetakes the paragraph level again.FinalizeLineandGetCharacterHitFromDistancefind it through the logical run order.TextBlockandSelectableTextBlockhit testing matchesTextLayoutfor text ending in Hebrew or Arabic, in both flow directions.GetDistanceFromCharacterHitandGetTextBoundsat the paragraph end now return the left edge of the line instead of the right edge, and the paragraph end is the first entry ofTextLine.TextRuns.Checklist
Breaking changes
None to the API; all changed types are internal. The paragraph end position and
TextRunsorder listed above change for right-to-left paragraphs.Obsoletions / Deprecations
None.
Fixed issues
Fixes #22530
🤖 Generated with Claude Code