Skip to content

[Text] Snap a right-to-left buffer split to the cluster boundary - #22066

Merged
MrJul merged 2 commits into
mainfrom
fix/rtl-split-inside-cluster
Aug 26, 2026
Merged

MrJul merged 2 commits into
mainfrom
fix/rtl-split-inside-cluster

Conversation

@Gillibald

@Gillibald Gillibald commented Aug 25, 2026 •

Copy link
Copy Markdown
Contributor

What does the pull request do?

Fixes ShapedBuffer.Split producing two halves whose character and glyph coverage disagree when the split point falls inside a cluster of a right-to-left buffer. Downstream, this crashes the bidi reorderer with a NullReferenceException.

Second layer of a stack; based on #22065.

What is the current behavior?

A cluster straddling the split point keeps all of its glyphs in the leading half. This prevents breaking a cluster, which is correct; but the text is still cut at the requested offset. The leading half then claims fewer characters than its glyphs cover, and the trailing half claims characters whose glyphs it does not have. The left-to-right path (SplitAscending) already snaps the text boundary to the cluster; the right-to-left path did not.

// "أَبْجَدِيَّة": each Arabic letter carries its harakat in the same cluster,
// so the first cluster spans two characters.
var split = buffer.Split(1);
// split.First.Text.Length == 1, but split.First holds 2 glyphs

Both halves share the parent's cluster cache, which now describes neither of them, so everything built on it misleads in turn: FindTrailingCharCountWithinWidth returns a count that cuts a cluster, and splitting the half at that count returns a leading half holding every glyph and no trailing half at all. That null run reaches BidiReorderer.BidiReorder and throws.

What is the updated/expected behavior with this PR?

The text boundary follows the glyphs, so the halves stay consistent: no character and no glyph is lost or duplicated, and every glyph of the trailing half belongs to a character the trailing half owns. Should_Not_Split_RightToLeft_Cluster asserts exactly that. The Collapse crash disappears.

As with the left-to-right path, Split(n) may return a leading half slightly longer than n when n lands inside a cluster - that is the only way to keep a cluster whole.

How was the solution implemented (if it's not obvious)?

Checklist

  • Added unit tests (if possible)?
  • Added XML documentation to any related classes? (no new API; the existing SplitDescending remarks describe the boundary and were updated in place)
  • Consider submitting a PR to https://github.com/AvaloniaUI/avalonia-docs with user documentation

Breaking changes

ShapedBuffer.Split(n) on a right-to-left buffer can now return a leading half longer than n when n falls inside a cluster, where it previously returned exactly n characters with a glyph set that did not match them. This mirrors the existing left-to-right behaviour. No other public API changes.

Obsoletions / Deprecations

None.

Fixed issues

No tracked issues; found while fixing #14011.

@Gillibald Gillibald changed the title fix/rtl split inside cluster [Text] Snap a right-to-left buffer split to the cluster boundary Aug 25, 2026
@Gillibald
Gillibald force-pushed the fix/rtl-split-inside-cluster branch from e04083e to 3c1f729 Compare August 26, 2026 04:36
@Gillibald
Gillibald force-pushed the fix/rtl-split-inside-cluster branch from 3c1f729 to 5ff8b4c Compare August 26, 2026 04:51
@Gillibald
Gillibald force-pushed the fix/rtl-split-inside-cluster branch from 5ff8b4c to fd4012c Compare August 26, 2026 04:54
@Gillibald
Gillibald marked this pull request as ready for review August 26, 2026 04:55
@MrJul MrJul added area-textprocessing backport-candidate-12.1.x Consider this PR for backporting to 12.1 branch bug labels Aug 26, 2026
@MrJul MrJul self-assigned this Aug 26, 2026

@MrJul MrJul left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM!

MrJul
MrJul previously approved these changes Aug 26, 2026
@MrJul
MrJul added this pull request to the merge queue Aug 26, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to invalid changes in the merge commit Aug 26, 2026
@MrJul
MrJul added this pull request to the merge queue Aug 26, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to invalid changes in the merge commit Aug 26, 2026
@MrJul

MrJul commented Aug 26, 2026

Copy link
Copy Markdown
Member

Well, it seems like the merge queue isn't working correctly with stacked PRs, even though there's a nice UI showing that it depends on the previous one in the queue. Let's wait for #22065 to get through.

Base automatically changed from fix/justify-word-gap-at-run-boundary to main August 26, 2026 09:51
@MrJul
MrJul force-pushed the fix/rtl-split-inside-cluster branch from fd4012c to 8880599 Compare August 26, 2026 09:51
@MrJul

MrJul commented Aug 26, 2026

Copy link
Copy Markdown
Member

Note: the force-push above happened automatically with the stacked PRs. It also automatically dismissed my review.

Splitting a right-to-left shaped buffer keeps a straddling cluster's
glyphs together in the leading half, but cuts the text at the requested
offset instead of following them. The halves then disagree about which
characters their glyphs cover: the leading half claims one character
while holding the glyphs for two, and the trailing half claims a
character whose glyphs it doesn't have. The left-to-right path already
snaps the text boundary to the cluster.
A cluster straddling the split point keeps all its glyphs in the leading
half, so the text has to be cut where those glyphs end, not at the
requested offset. Cutting at the offset produced two halves whose glyph
and character coverage disagreed, and their shared cluster cache then
described neither: measuring the trailing half returned counts that cut
a cluster, and splitting it again could hand back a leading half holding
every glyph and no trailing half at all - a null run that crashed the
bidi reorderer.

The ascending path already snapped this way; this makes the descending
path match.
@Gillibald
Gillibald force-pushed the fix/rtl-split-inside-cluster branch from 8880599 to 8102a5e Compare August 26, 2026 10:36
@avaloniaui-bot

Copy link
Copy Markdown

You can test this PR using the following package version. 12.2.999-cibuild0068868-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@avaloniaui-bot

Copy link
Copy Markdown

You can test this PR using the following package version. 12.2.999-cibuild0068879-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@Gillibald
Gillibald added this pull request to the merge queue Aug 26, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 26, 2026
@MrJul
MrJul added this pull request to the merge queue Aug 26, 2026
Merged via the queue into main with commit 4459a57 Aug 26, 2026
10 checks passed
@MrJul
MrJul deleted the fix/rtl-split-inside-cluster branch August 26, 2026 15:17
MrJul pushed a commit to MrJul/Avalonia that referenced this pull request Sep 2, 2026
…loniaUI#22066)

* Add failing test for a right-to-left split inside a cluster

Splitting a right-to-left shaped buffer keeps a straddling cluster's
glyphs together in the leading half, but cuts the text at the requested
offset instead of following them. The halves then disagree about which
characters their glyphs cover: the leading half claims one character
while holding the glyphs for two, and the trailing half claims a
character whose glyphs it doesn't have. The left-to-right path already
snaps the text boundary to the cluster.

* Snap a right-to-left split to the cluster the glyphs stay with

A cluster straddling the split point keeps all its glyphs in the leading
half, so the text has to be cut where those glyphs end, not at the
requested offset. Cutting at the offset produced two halves whose glyph
and character coverage disagreed, and their shared cluster cache then
described neither: measuring the trailing half returned counts that cut
a cluster, and splitting it again could hand back a leading half holding
every glyph and no trailing half at all - a null run that crashed the
bidi reorderer.

The ascending path already snapped this way; this makes the descending
path match.
@MrJul MrJul added backported-12.1.x and removed backport-candidate-12.1.x Consider this PR for backporting to 12.1 branch labels Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants