From 4f3af7e0fbe89c08af63068bcd6b0a88e564dcd3 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 14 Jul 2025 10:32:31 +0200 Subject: [PATCH] Fix TextLineIImpl.GetTextBounds for clustered trailing zero width characters (#19208) (#19251) * Make sure we only apply the cluster offset if we are inside the current cluster * Better naming * Guard coveredLength against invalid values --------- Co-authored-by: Max Katz --- .../Media/TextFormatting/TextLineImpl.cs | 11 ++++-- .../TextFormatting/TextFormatterTests.cs | 2 +- .../Media/TextFormatting/TextLineTests.cs | 36 +++++++++++++++++++ 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index f13fd26f27..31cddd7bf4 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -706,6 +706,11 @@ namespace Avalonia.Media.TextFormatting lastBounds = currentBounds; + if(coveredLength <= 0) + { + throw new InvalidOperationException("Covered length must be greater than zero."); + } + remainingLength -= coveredLength; } @@ -1090,7 +1095,8 @@ namespace Avalonia.Media.TextFormatting var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); //Adjust characterLength by the cluster offset to only cover the remaining length of the cluster. - var characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength) - clusterOffset; + var characterLength = Math.Max(0, Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - + endHit.FirstCharacterIndex - endHit.TrailingLength) - clusterOffset); if (characterLength == 0 && currentRun.Text.Length > 0 && startIndex < currentRun.Text.Length) { @@ -1172,7 +1178,8 @@ namespace Avalonia.Media.TextFormatting startIndex -= clusterOffset; } - var characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength) - clusterOffset; + var characterLength = Math.Max(0, Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - + endHit.FirstCharacterIndex - endHit.TrailingLength) - clusterOffset); if (characterLength == 0 && currentRun.Text.Length > 0 && startIndex < currentRun.Text.Length) { diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index da00ce0672..39bf60a42c 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -1158,7 +1158,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } - private class ListTextSource : ITextSource + internal class ListTextSource : ITextSource { private readonly List _runs; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index dcf285263e..27fb7d754b 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -1715,6 +1715,42 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + [Fact] + public void Should_GetTextBounds_For_Clustered_Zero_Width_Characters() + { + const string text = "\r\n"; + + using (Start()) + { + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + + var textSource = new TextFormatterTests.ListTextSource(new TextHidden(1) ,new TextCharacters(text, defaultProperties)); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, + true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0)); + + Assert.NotNull(textLine); + + var textBounds = textLine.GetTextBounds(2, 1); + + Assert.NotEmpty(textBounds); + + var firstBounds = textBounds[0]; + + Assert.NotEmpty(firstBounds.TextRunBounds); + + var firstRunBounds = firstBounds.TextRunBounds[0]; + + Assert.Equal(2, firstRunBounds.TextSourceCharacterIndex); + + Assert.Equal(1, firstRunBounds.Length); + } + } + private class FixedRunsTextSource : ITextSource { private readonly IReadOnlyList _textRuns;