From 038fa34f3d1b93d9d2510ca63514c0dd06cb2287 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 2 Sep 2025 11:35:07 +0200 Subject: [PATCH] Rework TextLineImpl.GetTextBounds (#19576) * Rework TextLineImpl.GetTextBounds * Minor adjustments * Fix GlyphRun.GetDistanceFromCharacterHit in cluster hit --- src/Avalonia.Base/Media/GlyphRun.cs | 27 +- .../Media/TextFormatting/TextLineImpl.cs | 406 ++++++++---------- .../Media/GlyphRunTests.cs | 2 +- .../Assets/Manrope-Light.ttf | Bin 0 -> 96728 bytes .../Media/GlyphRunTests.cs | 99 +++++ .../Media/TextFormatting/TextLineTests.cs | 233 +++++++++- 6 files changed, 545 insertions(+), 222 deletions(-) create mode 100644 tests/Avalonia.RenderTests/Assets/Manrope-Light.ttf diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index f8e0eb88bd..db80590082 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -243,6 +243,7 @@ namespace Avalonia.Media public double GetDistanceFromCharacterHit(CharacterHit characterHit) { var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; + var isTrailingHit = characterHit.TrailingLength > 0; var distance = 0.0; @@ -260,10 +261,28 @@ namespace Avalonia.Media var glyphIndex = FindGlyphIndex(characterIndex); - var currentCluster = _glyphInfos[glyphIndex].GlyphCluster; + var glyphInfo = _glyphInfos[glyphIndex]; + + var currentCluster = glyphInfo.GlyphCluster; + + var inClusterHit = currentCluster < characterIndex; + + //For in cluster hits we need to move to the start of the next cluster. + if (inClusterHit) + { + for(; glyphIndex < _glyphInfos.Count; glyphIndex++) + { + if (_glyphInfos[glyphIndex].GlyphCluster > characterIndex) + { + break; + } + } + + isTrailingHit = false; + } //Move to the end of the glyph cluster - if (characterHit.TrailingLength > 0) + if (isTrailingHit) { while (glyphIndex + 1 < _glyphInfos.Count && _glyphInfos[glyphIndex + 1].GlyphCluster == currentCluster) { @@ -347,8 +366,8 @@ namespace Avalonia.Media characterIndex = glyphInfo.GlyphCluster; - if (distance > currentX && distance <= currentX + advance) - { + if (currentX + advance > distance) + { break; } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index bebbe5d190..2e434cd078 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Utilities; @@ -395,22 +396,22 @@ namespace Avalonia.Media.TextFormatting return currentDirection; } - IndexedTextRun FindIndexedRun() + IndexedTextRun FindIndexedRun(out int index) { - var i = 0; + index = 0; - IndexedTextRun currentIndexedRun = _indexedTextRuns[i]; + IndexedTextRun currentIndexedRun = _indexedTextRuns[index]; while (currentIndexedRun.TextSourceCharacterIndex != currentPosition) { - if (i + 1 == _indexedTextRuns.Count) + if (index + 1 == _indexedTextRuns.Count) { break; } - i++; + index++; - currentIndexedRun = _indexedTextRuns[i]; + currentIndexedRun = _indexedTextRuns[index]; } return currentIndexedRun; @@ -434,7 +435,8 @@ namespace Avalonia.Media.TextFormatting } TextRun? currentTextRun = null; - var currentIndexedRun = FindIndexedRun(); + + var currentIndexedRun = FindIndexedRun(out var indexedRunIndex); while (currentPosition < FirstTextSourceIndex + Length) { @@ -451,7 +453,7 @@ namespace Avalonia.Media.TextFormatting { currentPosition += currentTextRun.Length; - currentIndexedRun = FindIndexedRun(); + currentIndexedRun = FindIndexedRun(out indexedRunIndex); continue; } @@ -467,7 +469,6 @@ namespace Avalonia.Media.TextFormatting var directionalWidth = 0.0; var firstRunIndex = currentIndexedRun.RunIndex; - var lastRunIndex = firstRunIndex; var currentDirection = GetDirection(currentTextRun, _resolvedFlowDirection); @@ -478,51 +479,7 @@ namespace Avalonia.Media.TextFormatting directionalWidth = currentDrawable.Size.Width; } - if (currentTextRun is not TextEndOfLine) - { - if (currentDirection == FlowDirection.LeftToRight) - { - // Find consecutive runs of same direction - for (; lastRunIndex + 1 < _textRuns.Length; lastRunIndex++) - { - var nextRun = _textRuns[lastRunIndex + 1]; - - var nextDirection = GetDirection(nextRun, currentDirection); - - if (currentDirection != nextDirection) - { - break; - } - - if (nextRun is DrawableTextRun nextDrawable) - { - directionalWidth += nextDrawable.Size.Width; - } - } - } - else - { - // Find consecutive runs of same direction - for (; firstRunIndex - 1 > 0; firstRunIndex--) - { - var previousRun = _textRuns[firstRunIndex - 1]; - - var previousDirection = GetDirection(previousRun, currentDirection); - - if (currentDirection != previousDirection) - { - break; - } - - if (previousRun is DrawableTextRun previousDrawable) - { - directionalWidth += previousDrawable.Size.Width; - - currentX -= previousDrawable.Size.Width; - } - } - } - } + var lastRunIndex = GetLastDirectionalRunIndex(indexedRunIndex, currentDirection, ref directionalWidth); switch (currentDirection) { @@ -600,6 +557,71 @@ namespace Avalonia.Media.TextFormatting return GetPreviousCharacterHit(characterHit, true); } + private static FlowDirection GetRunDirection(TextRun? textRun, FlowDirection currentDirection) + { + if (textRun is ShapedTextRun shapedTextRun) + { + return shapedTextRun.ShapedBuffer.IsLeftToRight ? + FlowDirection.LeftToRight : + FlowDirection.RightToLeft; + } + + return currentDirection; + } + + /// + /// Get the last consecutive visual run index that shares the same direction as the current direction. + /// + /// The current logical run's index. + /// The current flow direction. + /// The current directional width. + /// + /// The last consecutive visual run index that shares the same direction as the current direction. + /// + private int GetLastDirectionalRunIndex(int indexedRunIndex, FlowDirection flowDirection, ref double directionalWidth) + { + if(_indexedTextRuns is null) + { + return -1; + } + + var lastRunIndex = _indexedTextRuns[indexedRunIndex].RunIndex; + + // Find consecutive runs of same direction + while (indexedRunIndex + 1 < _indexedTextRuns.Count) + { + var nextIndexedRun = _indexedTextRuns[++indexedRunIndex]; + + if (nextIndexedRun.RunIndex != lastRunIndex + 1) + { + break; + } + + var nextRun = nextIndexedRun.TextRun; + + if (nextRun is null) + { + break; + } + + var nextDirection = GetRunDirection(nextRun, flowDirection); + + if (nextDirection != flowDirection) + { + break; + } + + if (nextRun is DrawableTextRun nextDrawable) + { + directionalWidth += nextDrawable.Size.Width; + } + + lastRunIndex = nextIndexedRun.RunIndex; + } + + return lastRunIndex; + } + public override IReadOnlyList GetTextBounds(int firstTextSourceIndex, int textLength) { if(textLength == 0) @@ -619,7 +641,7 @@ namespace Avalonia.Media.TextFormatting if (firstTextSourceIndex + textLength < FirstTextSourceIndex) { var indexedTextRun = _indexedTextRuns[0]; - var currentDirection = GetDirection(indexedTextRun.TextRun, _resolvedFlowDirection); + var currentDirection = GetRunDirection(indexedTextRun.TextRun, _resolvedFlowDirection); return [new TextBounds(new Rect(0,0,0, Height), currentDirection, [])]; } @@ -628,7 +650,7 @@ namespace Avalonia.Media.TextFormatting if (firstTextSourceIndex >= FirstTextSourceIndex + Length) { var indexedTextRun = _indexedTextRuns[_indexedTextRuns.Count - 1]; - var currentDirection = GetDirection(indexedTextRun.TextRun, _resolvedFlowDirection); + var currentDirection = GetRunDirection(indexedTextRun.TextRun, _resolvedFlowDirection); return [new TextBounds(new Rect(WidthIncludingTrailingWhitespace, 0, 0, Height), currentDirection, [])]; } @@ -639,16 +661,13 @@ namespace Avalonia.Media.TextFormatting while (remainingLength > 0 && currentPosition < FirstTextSourceIndex + Length) { - var currentIndexedRun = FindIndexedRun(); + var currentIndexedRun = FindIndexedRun(out var indexedRunIndex); if (currentIndexedRun == null) { break; } - - var directionalWidth = 0.0; - var firstRunIndex = currentIndexedRun.RunIndex; - var lastRunIndex = firstRunIndex; + var currentTextRun = currentIndexedRun.TextRun; if (currentTextRun == null) @@ -656,7 +675,7 @@ namespace Avalonia.Media.TextFormatting break; } - var currentDirection = GetDirection(currentTextRun, _resolvedFlowDirection); + var currentDirection = GetRunDirection(currentTextRun, _resolvedFlowDirection); if (currentIndexedRun.TextSourceCharacterIndex + currentTextRun.Length <= firstTextSourceIndex) { @@ -666,11 +685,15 @@ namespace Avalonia.Media.TextFormatting } var currentX = Start + GetPreceedingDistance(currentIndexedRun.RunIndex); + var directionalWidth = 0.0; if (currentTextRun is DrawableTextRun currentDrawable) { directionalWidth = currentDrawable.Size.Width; } + + var firstRunIndex = currentIndexedRun.RunIndex; + var lastRunIndex = GetLastDirectionalRunIndex(indexedRunIndex, currentDirection, ref directionalWidth); TextBounds currentBounds; int coveredLength; @@ -686,7 +709,7 @@ namespace Avalonia.Media.TextFormatting } default: { - currentBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex, + currentBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex, currentPosition, remainingLength, out coveredLength, out currentPosition); break; @@ -718,34 +741,22 @@ namespace Avalonia.Media.TextFormatting return result; - static FlowDirection GetDirection(TextRun? textRun, FlowDirection currentDirection) + IndexedTextRun FindIndexedRun(out int index) { - if (textRun is ShapedTextRun shapedTextRun) - { - return shapedTextRun.ShapedBuffer.IsLeftToRight ? - FlowDirection.LeftToRight : - FlowDirection.RightToLeft; - } + index = 0; - return currentDirection; - } - - IndexedTextRun FindIndexedRun() - { - var i = 0; - - var currentIndexedRun = _indexedTextRuns[i]; + var currentIndexedRun = _indexedTextRuns[index]; while (currentIndexedRun.TextSourceCharacterIndex != currentPosition) { - if (i + 1 == _indexedTextRuns.Count) + if (index + 1 == _indexedTextRuns.Count) { break; } - i++; + index++; - currentIndexedRun = _indexedTextRuns[i]; + currentIndexedRun = _indexedTextRuns[index]; } return currentIndexedRun; @@ -905,14 +916,14 @@ namespace Avalonia.Media.TextFormatting if (currentRun is ShapedTextRun shapedTextRun) { - var runBounds = GetRunBoundsRightToLeft(shapedTextRun, startX, firstTextSourceIndex, remainingLength, currentPosition, out var offset); + var runBounds = GetRunBoundsRightToLeft(shapedTextRun, startX, firstTextSourceIndex, remainingLength, currentPosition); if (runBounds.TextSourceCharacterIndex < FirstTextSourceIndex + Length) { textRunBounds.Insert(0, runBounds); } - if (offset > 0) + if (i == lastRunIndex) { endX = runBounds.Rectangle.Right; @@ -921,7 +932,7 @@ namespace Avalonia.Media.TextFormatting startX -= runBounds.Rectangle.Width; - currentPosition += runBounds.Length + offset; + currentPosition = runBounds.TextSourceCharacterIndex + runBounds.Length; coveredLength += runBounds.Length; @@ -985,23 +996,21 @@ namespace Avalonia.Media.TextFormatting if (currentRun is ShapedTextRun shapedTextRun) { - var runBounds = GetRunBoundsLeftToRight(shapedTextRun, endX, firstTextSourceIndex, remainingLength, currentPosition, out var offset); + var runBounds = GetRunBoundsLeftToRight(shapedTextRun, endX, firstTextSourceIndex, remainingLength, currentPosition); if(runBounds.TextSourceCharacterIndex < FirstTextSourceIndex + Length) { textRunBounds.Add(runBounds); } - if (offset > 0) + currentPosition = runBounds.TextSourceCharacterIndex + runBounds.Length; + + if(i == firstRunIndex) { startX = runBounds.Rectangle.Left; - - endX = startX; } - currentPosition += runBounds.Length + offset; - - endX += runBounds.Rectangle.Width; + endX = runBounds.Rectangle.Right; coveredLength += runBounds.Length; @@ -1054,189 +1063,154 @@ namespace Avalonia.Media.TextFormatting } private TextRunBounds GetRunBoundsLeftToRight(ShapedTextRun currentRun, double startX, - int firstTextSourceIndex, int remainingLength, int currentPosition, out int offset) + int firstTextSourceIndex, int remainingLength, int currentPosition) { - var startIndex = currentPosition; - - offset = Math.Max(0, firstTextSourceIndex - currentPosition); - + //Determine the start of the first hit in local positions. + var runOffset = Math.Max(0, firstTextSourceIndex - currentPosition); + var firstCluster = currentRun.GlyphRun.Metrics.FirstCluster; - if (currentPosition != firstCluster) - { - startIndex = firstCluster + offset; - } - else - { - startIndex += offset; - } + //The start index needs to be relative to the first cluster + var startIndex = firstCluster + runOffset; + var endIndex = startIndex + remainingLength; - //Make sure we start the hit test at the start of the possible cluster. - var clusterStartHit = currentRun.GlyphRun.GetPreviousCaretCharacterHit(new CharacterHit(startIndex)); - var clusterEndHit = currentRun.GlyphRun.GetNextCaretCharacterHit(clusterStartHit); + //Current position is a text source index and first cluster is relative to the GlyphRun's buffer. + var textSourceOffset = currentPosition - firstCluster; + + Debug.Assert(textSourceOffset >= 0); var clusterOffset = 0; - if (startIndex > clusterStartHit.FirstCharacterIndex && startIndex < clusterEndHit.FirstCharacterIndex + clusterEndHit.TrailingLength) - { - clusterOffset = clusterEndHit.FirstCharacterIndex + clusterEndHit.TrailingLength - startIndex; + //Cluster boundary correction + if (runOffset > 0) + { + var characterHit = currentRun.GlyphRun.FindNearestCharacterHit(startIndex, out _); + + var clusterStart = characterHit.FirstCharacterIndex; + var clusterEnd = clusterStart + characterHit.TrailingLength; + + //Test against left and right edge + if (clusterStart < startIndex && clusterEnd > startIndex) + { + //Remember the cluster correction offset + clusterOffset = startIndex - clusterStart; - //We need to move the startIndex to the start of the cluster. - startIndex -= clusterOffset; + //Move to the start of the cluster + startIndex -= clusterOffset; + } } + //Find the visual start and end position we want to hit test against var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); - var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(endIndex)); + + // Preserve non-zero width for zero-advance ranges + if (startOffset == endOffset && startIndex != endIndex) + { + //We need to make sure a zero width text line is hit test at the end so we add some delta + endOffset += MathUtilities.DoubleEpsilon; + } var endX = startX + endOffset; startX += startOffset; + //Hit test against visual positions var startHit = currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); + var startHitIndex = startHit.FirstCharacterIndex + startHit.TrailingLength; + var endHitIndex = endHit.FirstCharacterIndex + endHit.TrailingLength; + //Adjust characterLength by the cluster offset to only cover the remaining length of the cluster. - var characterLength = Math.Max(0, Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - - endHit.FirstCharacterIndex - endHit.TrailingLength) - clusterOffset); - - remainingLength -= characterLength; - - var runOffset = startIndex - firstCluster; - - //Make sure we are properly dealing with zero width space runs - if (remainingLength > 0 && currentRun.Text.Length > 0 && runOffset + characterLength < currentRun.Text.Length) - { - var glyphInfos = currentRun.GlyphRun.GlyphInfos; - - for (int i = runOffset + characterLength; i < glyphInfos.Count; i++) - { - var glyphInfo = glyphInfos[i]; - - if(glyphInfo.GlyphAdvance > 0) - { - break; - } - - var graphemeEnumerator = new GraphemeEnumerator(currentRun.Text.Span.Slice(runOffset + characterLength)); - - if(!graphemeEnumerator.MoveNext(out var grapheme)) - { - break; - } - - characterLength += grapheme.Length - clusterOffset; - remainingLength -= grapheme.Length; - - if(remainingLength <= 0) - { - break; - } - } - } + var characterLength = Math.Max(0, Math.Abs(startHitIndex - endHitIndex) - clusterOffset); + // Normalize bounds if (endX < startX) { (endX, startX) = (startX, endX); } - //Lines that only contain a linebreak need to be covered here - if (characterLength == 0) - { - characterLength = NewLineLength; - } - var runWidth = endX - startX; - var textSourceIndex = startIndex + Math.Max(0, currentPosition - firstCluster) + clusterOffset; + //We need to adjust the local position to the text source + var textSourceIndex = textSourceOffset + startHitIndex + clusterOffset; return new TextRunBounds(new Rect(startX, 0, runWidth, Height), textSourceIndex, characterLength, currentRun); } - private TextRunBounds GetRunBoundsRightToLeft(ShapedTextRun currentRun, double endX, - int firstTextSourceIndex, int remainingLength, int currentPosition, out int offset) + private TextRunBounds GetRunBoundsRightToLeft(ShapedTextRun currentRun, double endX, int firstTextSourceIndex, int remainingLength, int currentPosition) { + // We start from the right edge of the run var startX = endX; - var startIndex = currentPosition; - - offset = Math.Max(0, firstTextSourceIndex - currentPosition); + //Determine the start of the first hit in local positions. + var runOffset = Math.Max(0, firstTextSourceIndex - currentPosition); var firstCluster = currentRun.GlyphRun.Metrics.FirstCluster; - if (currentPosition != firstCluster) - { - startIndex = firstCluster + offset; - } - else - { - startIndex += offset; - } - - var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); - var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); - - startX -= currentRun.Size.Width - startOffset; - endX -= currentRun.Size.Width - endOffset; - - var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); - var startHit = currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); + //The start index needs to be relative to the first cluster + var startIndex = firstCluster + runOffset; + var endIndex = startIndex + remainingLength; - //Make sure we start the hit test at the start of the possible cluster. - var clusterStartHit = currentRun.GlyphRun.GetNextCaretCharacterHit(new CharacterHit(startIndex)); - var clusterEndHit = currentRun.GlyphRun.GetPreviousCaretCharacterHit(startHit); + //Current position is a text source index and first cluster is relative to the GlyphRun's buffer. + var textSourceOffset = currentPosition - firstCluster; + Debug.Assert(textSourceOffset >= 0); var clusterOffset = 0; - if (startIndex > clusterStartHit.FirstCharacterIndex && startIndex < clusterEndHit.FirstCharacterIndex + clusterEndHit.TrailingLength) + //Cluster boundary correction + if (runOffset > 0) { - clusterOffset = clusterEndHit.FirstCharacterIndex + clusterEndHit.TrailingLength - startIndex; + var characterHit = currentRun.GlyphRun.FindNearestCharacterHit(startIndex, out _); - //We need to move the startIndex to the start of the cluster. - startIndex -= clusterOffset; - } - - var characterLength = Math.Max(0, Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - - endHit.FirstCharacterIndex - endHit.TrailingLength) - clusterOffset); - - var runOffset = startIndex - offset; - - if (characterLength == 0 && currentRun.Text.Length > 0 && runOffset < currentRun.Text.Length) - { - //Make sure we are properly dealing with zero width space runs - var codepointEnumerator = new CodepointEnumerator(currentRun.Text.Span.Slice(runOffset)); + var clusterStart = characterHit.FirstCharacterIndex; + var clusterEnd = clusterStart + characterHit.TrailingLength; - while (remainingLength > 0 && codepointEnumerator.MoveNext(out var codepoint)) + //Test against left and right edge + if (clusterStart < startIndex && clusterEnd > startIndex) { - if (codepoint.IsWhiteSpace) - { - characterLength++; - remainingLength--; - } - else - { - break; - } + //Remember the cluster correction offset + clusterOffset = startIndex - clusterStart; + + //Move to the start of the cluster + startIndex -= clusterOffset; } } - if (startHit.FirstCharacterIndex > endHit.FirstCharacterIndex) + //Find the visual start and end position we want to hit test against + var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); + var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(endIndex)); + + //We need the distance from right to left and GetDistanceFromCharacterHit returs a distance from left to right so we need to adjust the offsets + startX -= currentRun.Size.Width - startOffset; + endX -= currentRun.Size.Width - endOffset; + + // Preserve non-zero width for zero-advance ranges + if (startOffset == endOffset && startIndex != endIndex) { - startHit = endHit; + //We need to make sure a zero width text line is hit test at the end so we add some delta + endOffset += MathUtilities.DoubleEpsilon; } + //Hit test against visual positions + var startHit = currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); + var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); + + var startHitIndex = startHit.FirstCharacterIndex + startHit.TrailingLength; + var endHitIndex = endHit.FirstCharacterIndex + endHit.TrailingLength; + + var characterLength = Math.Max(0, Math.Abs(startHitIndex - endHitIndex) - clusterOffset); + + // Normalize bounds if (endX < startX) { (endX, startX) = (startX, endX); } - //Lines that only contain a linebreak need to be covered here - if (characterLength == 0) - { - characterLength = NewLineLength; - } - var runWidth = endX - startX; - var textSourceIndex = startIndex + Math.Max(0, currentPosition - firstCluster) + clusterOffset; + //We need to adjust the local position to the text source + var textSourceIndex = textSourceOffset + startHitIndex + clusterOffset; return new TextRunBounds(new Rect(startX, 0, runWidth, Height), textSourceIndex, characterLength, currentRun); } diff --git a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs index 229b63ae1a..ca573fae90 100644 --- a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs @@ -29,7 +29,7 @@ namespace Avalonia.Base.UnitTests.Media } [InlineData(new double[] { 30, 0, 0 }, new int[] { 0, 0, 0 }, 26.0, 0, 3, true)] - [InlineData(new double[] { 10, 10, 10 }, new int[] { 0, 1, 2 }, 20.0, 1, 1, true)] + [InlineData(new double[] { 10, 10, 10 }, new int[] { 0, 1, 2 }, 20.0, 2, 0, true)] [InlineData(new double[] { 10, 10, 10 }, new int[] { 0, 1, 2 }, 26.0, 2, 1, true)] [InlineData(new double[] { 10, 10, 10 }, new int[] { 0, 1, 2 }, 35.0, 2, 1, false)] [Theory] diff --git a/tests/Avalonia.RenderTests/Assets/Manrope-Light.ttf b/tests/Avalonia.RenderTests/Assets/Manrope-Light.ttf new file mode 100644 index 0000000000000000000000000000000000000000..494292489391cfc86082318f8517b12701450875 GIT binary patch literal 96728 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 F`wdleUETly literal 0 HcmV?d00001 diff --git a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs index 1cad489f0d..278bf698de 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Linq; using Avalonia.Media; using Avalonia.Media.TextFormatting; +using Avalonia.Media.TextFormatting.Unicode; using Avalonia.UnitTests; using Xunit; @@ -221,6 +222,30 @@ namespace Avalonia.Skia.UnitTests.Media } } + [Fact] + public void Should_CharacterHit_From_Distance_Zero_Width() + { + const string df7Font = "resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#DF7segHMI"; + const string text = "3,47-=?:#"; + + using (Start()) + { + var typeface = new Typeface(df7Font); + var options = new TextShaperOptions(typeface.GlyphTypeface, 14, 0); + var shapedBuffer = TextShaper.Current.ShapeText(text, options); + + Assert.NotEmpty(shapedBuffer); + + var firstGlyphInfo = shapedBuffer[0]; + + var glyphRun = CreateGlyphRun(shapedBuffer); + + var characterHit = glyphRun.GetCharacterHitFromDistance(firstGlyphInfo.GlyphAdvance, out _); + + Assert.Equal(2, characterHit.FirstCharacterIndex + characterHit.TrailingLength); + } + } + [Fact] public void Should_Get_Distance_From_CharacterHit_Zero_Width() { @@ -280,6 +305,80 @@ namespace Avalonia.Skia.UnitTests.Media } } + [Fact] + public void Should_Get_Distance_From_CharacterHit_Within_Cluster() + { + var text = "எடுத்துக்காட்டு வழி வினவல்"; + + using (Start()) + { + var cp = Codepoint.ReadAt(text, 0, out _); + + Assert.True(FontManager.Current.TryMatchCharacter(cp, FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, null, null, out var typeface)); + + var options = new TextShaperOptions(typeface.GlyphTypeface, 12); + + var shapedBuffer = TextShaper.Current.ShapeText(text, options); + + var glyphRun = CreateGlyphRun(shapedBuffer); + + var clusterWidth = new List(); + var distances = new List(); + var clusters = new List(); + var lastCluster = -1; + var currentDistance = 0.0; + var currentAdvance = 0.0; + + foreach (var glyphInfo in shapedBuffer) + { + if (lastCluster != glyphInfo.GlyphCluster) + { + clusterWidth.Add(currentAdvance); + distances.Add(currentDistance); + clusters.Add(glyphInfo.GlyphCluster); + + currentAdvance = 0; + } + + lastCluster = glyphInfo.GlyphCluster; + currentDistance += glyphInfo.GlyphAdvance; + currentAdvance += glyphInfo.GlyphAdvance; + } + + clusterWidth.RemoveAt(0); + + clusterWidth.Add(currentAdvance); + + var expectedLeftHit = new CharacterHit(11); + + var distance = glyphRun.GetDistanceFromCharacterHit(expectedLeftHit); + + var expectedLeft = distances[6]; + + Assert.Equal(expectedLeft, distance); + + var leftHit = glyphRun.GetCharacterHitFromDistance(expectedLeft, out _); + + Assert.Equal(11, leftHit.FirstCharacterIndex + leftHit.TrailingLength); + + var expectedRight = distances[7]; + + distance = glyphRun.GetDistanceFromCharacterHit(new CharacterHit(12)); + + Assert.Equal(expectedRight, distance); + + var expectedRightHit = new CharacterHit(13); + + distance = glyphRun.GetDistanceFromCharacterHit(expectedRightHit); + + Assert.Equal(expectedRight, distance); + + var rightHit = glyphRun.GetCharacterHitFromDistance(expectedRight, out _); + + Assert.Equal(13, rightHit.FirstCharacterIndex + rightHit.TrailingLength); + } + } + private static List BuildRects(GlyphRun glyphRun) { var height = glyphRun.Bounds.Height; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index 3df6c92928..7985c631fc 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -1639,7 +1639,6 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } - [Fact] public void Should_GetTextBounds_NotInfiniteLoop() { @@ -1868,6 +1867,238 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + [Fact] + public void Should_GetTextBounds_For_Multiple_TextRuns() + { + var text = "Test👩🏽‍🚒"; + + using (Start()) + { + var typeface = Typeface.Default; + + var defaultProperties = new GenericTextRunProperties(typeface, 12); + + var textSource = new SingleBufferTextSource(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 result = textLine.GetTextBounds(0, 11); + + Assert.Equal(1, result.Count); + + var firstBounds = result[0]; + + Assert.NotEmpty(firstBounds.TextRunBounds); + + Assert.Equal(textLine.WidthIncludingTrailingWhitespace, firstBounds.Rectangle.Width, 2); + } + } + + [Fact] + public void Should_GetTextBounds_Within_Cluster_2() + { + var text = "Test👩🏽‍🚒"; + + using (Start()) + { + var typeface = Typeface.Default; + + var defaultProperties = new GenericTextRunProperties(typeface, 12); + + var textSource = new SingleBufferTextSource(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 textPosition = 0; + + while(textPosition < text.Length) + { + var bounds = textLine.GetTextBounds(textPosition, 1); + + Assert.Equal(1, bounds.Count); + + var firstBounds = bounds[0]; + + Assert.Equal(1, firstBounds.TextRunBounds.Count); + + var firstRunBounds = firstBounds.TextRunBounds[0]; + + Assert.Equal(textPosition, firstRunBounds.TextSourceCharacterIndex); + + var expectedDistance = firstRunBounds.Rectangle.Left; + + var characterHit = new CharacterHit(textPosition); + + var distance = textLine.GetDistanceFromCharacterHit(characterHit); + + Assert.Equal(expectedDistance, distance, 2); + + var nextCharacterHit = textLine.GetNextCaretCharacterHit(characterHit); + + var expectedNextPosition = textPosition + firstRunBounds.Length; + + var nextPosition = nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength; + + Assert.Equal(expectedNextPosition, nextPosition); + + var previousCharacterHit = textLine.GetPreviousCaretCharacterHit(nextCharacterHit); + + Assert.Equal(characterHit, previousCharacterHit); + + textPosition += firstRunBounds.Length; + } + } + } + + [Fact] + public void Should_Get_TextBounds_With_Mixed_Runs_Within_Cluster() + { + using (Start()) + { + const string manropeFont = "resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#Manrope"; + + var typeface = new Typeface(manropeFont); + + var defaultProperties = new GenericTextRunProperties(typeface); + var text = "Fotografin"; + var shaperOption = new TextShaperOptions(typeface.GlyphTypeface); + + var firstRun = new ShapedTextRun(TextShaper.Current.ShapeText(text, shaperOption), defaultProperties); + + var textRuns = new List + { + new CustomDrawableRun(), + new CustomDrawableRun(), + firstRun, + new CustomDrawableRun(), + }; + + var textSource = new FixedRunsTextSource(textRuns); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + Assert.NotNull(textLine); + + var textBounds = textLine.GetTextBounds(10, 1); + + Assert.Equal(1, textBounds.Count); + + var firstBounds = textBounds[0]; + + Assert.NotEmpty(firstBounds.TextRunBounds); + + var firstRunBounds = firstBounds.TextRunBounds[0]; + + Assert.Equal(1, firstRunBounds.Length); + } + } + + [Fact] + public void Should_Get_TextBounds_Tamil() + { + var text = "எடுத்துக்காட்டு வழி வினவல்"; + + using (Start()) + { + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + var textSource = new SingleBufferTextSource(text, defaultProperties, true); + + 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); + + Assert.NotEmpty(textLine.TextRuns); + + var firstRun = textLine.TextRuns[0] as ShapedTextRun; + + Assert.NotNull(firstRun); + + var clusterWidth = new List(); + var distances = new List(); + var clusters = new List(); + var lastCluster = -1; + var currentDistance = 0.0; + var currentAdvance = 0.0; + + foreach (var glyphInfo in firstRun.ShapedBuffer) + { + if(lastCluster != glyphInfo.GlyphCluster) + { + clusterWidth.Add(currentAdvance); + distances.Add(currentDistance); + clusters.Add(glyphInfo.GlyphCluster); + + currentAdvance = 0; + } + + lastCluster = glyphInfo.GlyphCluster; + currentDistance += glyphInfo.GlyphAdvance; + currentAdvance += glyphInfo.GlyphAdvance; + } + + clusterWidth.RemoveAt(0); + + clusterWidth.Add(currentAdvance); + + for (var i = 6; i < clusters.Count; i++) + { + var cluster = clusters[i]; + var expectedDistance = distances[i]; + var expectedWidth = clusterWidth[i]; + + var actualDistance = textLine.GetDistanceFromCharacterHit(new CharacterHit(cluster)); + + Assert.Equal(expectedDistance, actualDistance, 2); + + var characterHit = textLine.GetCharacterHitFromDistance(expectedDistance); + + var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; + + Assert.Equal(cluster, textPosition); + + var bounds = textLine.GetTextBounds(cluster, 1); + + Assert.NotNull(bounds); + Assert.NotEmpty(bounds); + + var firstBounds = bounds[0]; + + Assert.NotEmpty(firstBounds.TextRunBounds); + + var firstRunBounds = firstBounds.TextRunBounds[0]; + + Assert.Equal(cluster, firstRunBounds.TextSourceCharacterIndex); + + var width = firstRunBounds.Rectangle.Width; + + Assert.Equal(expectedWidth, width, 2); + } + } + } + private class FixedRunsTextSource : ITextSource { private readonly IReadOnlyList _textRuns;