Skip to content

Conversation

@kerams
Copy link
Contributor

@kerams kerams commented Feb 9, 2026

What does the pull request do?

Prevents tiny floating-point discrepancies from falsely triggering wrapping or overflow detection.

What is the current behavior?

This extremely hard to reproduce in a minimal sample, but in my project I see the following:

When using TextWrapping.Wrap on Android/iOS in a TextBlock of specific size and content, parts of text blocks with certain width get cut off and are not visible (even when wrapping isn't actually required).

For instance, into eternal life is rendered as into eternal. into eternal iife is rendered that way too, supposedly because i and l have the same width. However, into eternal lifa is rendered in full (e and a have different widths). If I increase or decrease the font size (or slightly change margins somewhere in the visual hierarchy), the issue disappears.

I can reliably reproduce this on Android ARM64 device, iOS device and simulator, but not in Android x86 emulator (it's possible it would reproduce with a different string).

What is the updated/expected behavior with this PR?

Text blocks are wrapped correctly and shown in their entirety when wrapping is not required.

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

Use MathUtilities.GreaterThan, which returns true only if the difference is greater than epsilon, instead of >.

Even though this fixes the issue in my project, I am still a bit apprehensive. How have other people not run into this problem? It's somewhat of an edge case, but still.

Opus' analysis 1:

Due to IEEE 754 floating-point associativity: (a+b) + (c+d) ≠ ((a+b) + c) + d. The flat sum can be slightly larger than the hierarchical sum. When the paragraph constraint was derived from the hierarchical sum (from a previous layout pass during MeasureOverride(Size)), the flat sum in MeasureLength(IReadOnlyList<TextRun>, double) can barely exceed it, falsely triggering wrapping.
This explains the symptoms:
• Character-width dependence: different advance values shift the floating-point accumulation pattern, sometimes crossing the threshold and sometimes not
• Font-size dependence: textScale = fontSize / unitsPerEm changes the mantissa bits of all advances, changing which accumulations trigger the error
• ARM vs x86: different native HarfBuzz/Skia builds or different system fonts for fallback produce different advance values, shifting the boundary
• "Completely cut off": the text wraps during ArrangeOverride(Size) (creating 2 lines) but was measured for 1 line during MeasureOverride(Size), so the second line falls outside the TextBlock's allocated height

Opus analysis 2:

The paragraphWidth constraint reaching MeasureLength(IReadOnlyList<TextRun>, double) during the arrange pass may not be the exact same value as the text's own computed width. The discrepancy doesn't require multiple runs — it comes from how the width constraint flows through the layout system.

During measure, the TextLayout uses a large MaxWidth (from the parent). The text fits on one line and reports WidthIncludingTrailingWhitespace = W.

During arrange, the TextLayout is recreated from scratch (line 769 in SelectableTextBlock.cs: _textLayout = null). The MaxWidth this time is finalSize.Width - padding, which came through the parent's arrange logic — involving Deflate(double), Inflate(Thickness), RoundLayoutSizeUp(Size, double, double), and DPI-scaled arithmetic. Each of these operations introduces potential floating-point rounding. By the time the value arrives at MeasureLength(IReadOnlyList<TextRun>, double) as paragraphWidth, it may differ from the freshly re-computed text width by a fraction of a ULP.

The raw > had zero tolerance for this, so a discrepancy as small as 2.2e-16 relative to the values involved could trigger wrapping. GreaterThan(double, double) treats values within machine-epsilon as equal, which is the correct behavior for a width comparison that went through different arithmetic paths.

Checklist

Breaking changes

No

@avaloniaui-bot
Copy link

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

@MrJul MrJul added bug area-textprocessing backport-candidate-11.3.x Consider this PR for backporting to 11.3 branch labels Feb 9, 2026
Copy link
Member

@MrJul MrJul left a comment

Choose a reason for hiding this comment

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

I understand that you're unable to provide a stable reproduction on desktop platforms.
Considering that the change is very simple for a common issue (floating-point precision), we're going to accept it.

LGTM, thank you!

@MrJul MrJul enabled auto-merge February 10, 2026 09:09
@avaloniaui-bot
Copy link

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

@MrJul MrJul added this pull request to the merge queue Feb 10, 2026
Merged via the queue into AvaloniaUI:master with commit cdb91f7 Feb 10, 2026
11 checks passed
@kerams kerams deleted the text branch February 10, 2026 11:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-textprocessing backport-candidate-11.3.x Consider this PR for backporting to 11.3 branch bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants