Skip to content

Fix TextLayout.HitTestPoint IsInside for multi-line and aligned text - #22433

Merged
MrJul merged 8 commits into
AvaloniaUI:mainfrom
sean-reed:fix/textlayout-hittestpoint-isinside
Oct 21, 2026
Merged

MrJul merged 8 commits into
AvaloniaUI:mainfrom
sean-reed:fix/textlayout-hittestpoint-isinside

Conversation

@sean-reed

@sean-reed sean-reed commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

What does the pull request do?

Fixes TextLayout.HitTestPoint so that IsInside is correct for points on lines below the first, and on centered or right-aligned lines.

What is the current behavior?

HitTestPoint returns IsInside = false for a point over a character:

  • on any line except the first, and
  • near the right edge of a line that doesn't start at x = 0 (centered or right-aligned text, in either flow direction).

What is the updated/expected behavior with this PR?

IsInside is true for a point over a character on any line, whatever the text alignment.

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

GetHitTestResult compared a point relative to the whole layout against the size of a single line. HitTestPoint now translates the point into the hit line's coordinates (relative to the line's top and its TextLine.Start) before passing it to GetHitTestResult.

The commits come in test/fix pairs: test and fix for lines after the first, a refactor to use line-relative points, then test and fix for aligned lines.

Checklist

Breaking changes

None

Obsoletions / Deprecations

None

Fixed issues

I didn't find any issues relating to this.

Add a test that hit-tests a point over the first character of each line
in a three-line TextLayout, once with hard line breaks and once with
wrapping. HitTestPoint incorrectly returns IsInside = false for a point
over a character on any line below the first. The CharacterHit is
correct.
Check IsInside against the point's offset from the top of the hit line
instead of from the top of the layout.
@Gillibald

Copy link
Copy Markdown
Contributor

Isn't the issue that we are using the same point for all lines? So, by translating the point to the line's local coordinates, we should get the same desired results. This would not require changing the signature of the helper function. Ideally, we make it clear that the specified point should be in local coordinates.

@Gillibald
Gillibald requested a balanced review from Copilot October 11, 2026 13:45
@cla-avalonia

cla-avalonia commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator
  • All contributors have signed the CLA.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The implementation correctly addresses the coordinate-space bug and includes focused regression coverage.

0 open findings

What changed in this PR

Fixes multiline TextLayout.HitTestPoint results by evaluating vertical containment relative to the selected line.

Changes:

  • Passes each line’s vertical offset into hit-test result calculation.
  • Adds explicit and wrapped multiline regression tests.
File Description
src/​Avalonia.Base/​Media/​TextFormatting/​TextLayout.cs Corrects per-line IsInside calculation.
tests/​Avalonia.Skia.UnitTests/​Media/​TextFormatting/​TextLayoutTests.cs Covers hit testing across multiple lines.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Translate the hit point into the hit line's coordinates in HitTestPoint
instead of passing the line's top to GetHitTestResult, so the helper
works in a single coordinate space. The check for a point below the
text becomes y > textLine.Height, which is equivalent because only the
last line can have a point below it.
@sean-reed

Copy link
Copy Markdown
Contributor Author

@cla-avalonia agree

Add a test that hit-tests a point just inside the right edge of a
centered or right-aligned line, in both flow directions. HitTestPoint
incorrectly returns IsInside = false because it ignores the line's
horizontal offset.
Also translate the hit point horizontally by the line's Start, so the
point passed to GetHitTestResult is relative to the line's top-left
corner as documented.
@sean-reed

sean-reed commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor Author

@Gillibald Thanks, that's a better approach. Done in 4220d5b: HitTestPoint now translates the point into the hit line's coordinates, and GetHitTestResult keeps its original signature, with the parameter documented as line-relative. Doing that showed the same problem horizontally. The point also needs to be relative to the line's Start, or IsInside is wrong for centered and right-aligned lines. I've added a test and fix for that in 6ab6474 and 02a0225. I added doc comments to the TextHitResult method, and renamed param point to linePoint, to make it clear it's local coords.

@sean-reed sean-reed changed the title Fix/textlayout hittestpoint isinside Fix TextLayout.HitTestPoint IsInside for multi-line and aligned text Oct 11, 2026
- Points left of, right of, above and below an aligned line must not report IsInside, guarding against the line-relative translation over-correcting.
- An aligned layout hit at Start + dx must give the same CharacterHit, TextPosition, IsInside and IsTrailing as a left-aligned layout hit at dx, which covers the newline trimming that now sees line-relative x.

@Gillibald Gillibald left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

Thank you

@github-actions

Copy link
Copy Markdown

NuGet packages

The packages for this pull request have been published (workflow run):

@MrJul
MrJul enabled auto-merge October 21, 2026 13:17
@MrJul
MrJul added this pull request to the merge queue Oct 21, 2026
Merged via the queue into AvaloniaUI:main with commit 0d6c2aa Oct 21, 2026
11 checks passed
MrJul added a commit that referenced this pull request Nov 2, 2026
…22433)

* Test HitTestPoint IsInside on lines after the first

Add a test that hit-tests a point over the first character of each line
in a three-line TextLayout, once with hard line breaks and once with
wrapping. HitTestPoint incorrectly returns IsInside = false for a point
over a character on any line below the first. The CharacterHit is
correct.

* Fix HitTestPoint IsInside on lines after the first

Check IsInside against the point's offset from the top of the hit line
instead of from the top of the layout.

* Pass a line-relative point to GetHitTestResult

Translate the hit point into the hit line's coordinates in HitTestPoint
instead of passing the line's top to GetHitTestResult, so the helper
works in a single coordinate space. The check for a point below the
text becomes y > textLine.Height, which is equivalent because only the
last line can have a point below it.

* Test HitTestPoint IsInside on aligned lines

Add a test that hit-tests a point just inside the right edge of a
centered or right-aligned line, in both flow directions. HitTestPoint
incorrectly returns IsInside = false because it ignores the line's
horizontal offset.

* Fix HitTestPoint IsInside on aligned lines

Also translate the hit point horizontally by the line's Start, so the
point passed to GetHitTestResult is relative to the line's top-left
corner as documented.

* Test HitTestPoint outside aligned lines and aligned/left parity

- Points left of, right of, above and below an aligned line must not report IsInside, guarding against the line-relative translation over-correcting.
- An aligned layout hit at Start + dx must give the same CharacterHit, TextPosition, IsInside and IsTrailing as a left-aligned layout hit at dx, which covers the newline trimming that now sees line-relative x.

---------

Co-authored-by: Benedikt Stebner <Gillibald@users.noreply.github.com>
Co-authored-by: Julien Lebosquain <julien@lebosquain.net>
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.

5 participants