Browse Source

Rework TextLineImpl.GetTextBounds (#19576)

* Rework TextLineImpl.GetTextBounds

* Minor adjustments

* Fix GlyphRun.GetDistanceFromCharacterHit in cluster hit
pull/19583/head
Benedikt Stebner 5 months ago
committed by GitHub
parent
commit
27859b9e54
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 27
      src/Avalonia.Base/Media/GlyphRun.cs
  2. 406
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  3. 2
      tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs
  4. BIN
      tests/Avalonia.RenderTests/Assets/Manrope-Light.ttf
  5. 99
      tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs
  6. 233
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

27
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;
}

406
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;
}
/// <summary>
/// Get the last consecutive visual run index that shares the same direction as the current direction.
/// </summary>
/// <param name="indexedRunIndex">The current logical run's index.</param>
/// <param name="flowDirection">The current flow direction.</param>
/// <param name="directionalWidth">The current directional width.</param>
/// <returns>
/// The last consecutive visual run index that shares the same direction as the current direction.
/// </returns>
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<TextBounds> 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);
}

2
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]

BIN
tests/Avalonia.RenderTests/Assets/Manrope-Light.ttf

Binary file not shown.

99
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<double>();
var distances = new List<double>();
var clusters = new List<int>();
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<Rect> BuildRects(GlyphRun glyphRun)
{
var height = glyphRun.Bounds.Height;

233
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<TextRun>
{
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<double>();
var distances = new List<double>();
var clusters = new List<int>();
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<TextRun> _textRuns;

Loading…
Cancel
Save