From ec9f50db3dd210a239e5b93abfe78953e613db96 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 8 Sep 2025 13:30:54 +0200 Subject: [PATCH] Fixes TextLineImpl.GetTextBounds with trailing zero width (#19616) * Fixes TextLineImpl.GetTextBounds with trailing zero width * Add assert --- .../Media/TextFormatting/TextLineImpl.cs | 18 ++++++--- .../Media/TextFormatting/TextLineTests.cs | 38 +++++++++++++++++++ 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 71cff3c483..a8898782e5 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -1118,23 +1118,29 @@ namespace Avalonia.Media.TextFormatting } // Find the start of the hit - var startHit = currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); - var startHitIndex = startHit.FirstCharacterIndex + startHit.TrailingLength; + var startHit = currentRun.GlyphRun.FindNearestCharacterHit(startIndex, out _); + var startHitIndex = startHit.FirstCharacterIndex; + + //If the requested text range starts at the trailing edge we need to move at the end of the hit + if(startHitIndex < startIndex) + { + startHitIndex += startHit.TrailingLength; + } //Find the next possible position that contains the endIndex - var nearestCharacterHit = currentRun.GlyphRun.FindNearestCharacterHit(endIndex, out _); + var nearestEndHit = currentRun.GlyphRun.FindNearestCharacterHit(endIndex, out _); int endHitIndex; - if (nearestCharacterHit.FirstCharacterIndex < endIndex) + if (nearestEndHit.FirstCharacterIndex < endIndex) { //The hit is inside or at the trailing edge - endHitIndex = nearestCharacterHit.FirstCharacterIndex + nearestCharacterHit.TrailingLength; + endHitIndex = nearestEndHit.FirstCharacterIndex + nearestEndHit.TrailingLength; } else { //The hit is at the leading edge - endHitIndex = nearestCharacterHit.FirstCharacterIndex; + endHitIndex = nearestEndHit.FirstCharacterIndex; } var coveredLength = Math.Max(0, Math.Abs(startHitIndex - endHitIndex) - clusterOffset); diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index d5f4b47f4d..b7b6390242 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -2135,6 +2135,44 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + [Fact] + public void Should_GetTextBounds_Trailing_ZeroWidth() + { + var text = "dasdsad\r\n"; + + using (Start()) + { + var typeface = Typeface.Default; + + var defaultProperties = new GenericTextRunProperties(typeface); + var shaperOption = new TextShaperOptions(typeface.GlyphTypeface); + + var textSource = new SingleBufferTextSource(text, defaultProperties); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + Assert.NotNull(textLine); + + var textBounds = textLine.GetTextBounds(7, 3); + + Assert.NotEmpty(textBounds); + + var firstBounds = textBounds[0]; + + Assert.NotEmpty(firstBounds.TextRunBounds); + + var firstRunBounds = firstBounds.TextRunBounds[0]; + + Assert.Equal(7, firstRunBounds.TextSourceCharacterIndex); + + Assert.Equal(2, firstRunBounds.Length); + } + } + private class FixedRunsTextSource : ITextSource { private readonly IReadOnlyList _textRuns;