Browse Source

Multiple TextLine.GetTextRunBounds fixes (#18749)

* Adjust TextLineImpl.GetTextRunBounds so it properly handles substitutions
Adjust TextLineImpl.GetTextRunBounds so it properly reports out of text range bounds
Adjust TextLineImpl.GetTextRunBounds so it properly reports text source run indices

* Remove redundant comments

* Add requested changes
pull/18852/head
Benedikt Stebner 9 months ago
parent
commit
b752c32481
  1. 298
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  2. 316
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

298
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@ -602,101 +602,40 @@ namespace Avalonia.Media.TextFormatting
public override IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceIndex, int textLength) public override IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceIndex, int textLength)
{ {
if (_indexedTextRuns is null || _indexedTextRuns.Count == 0) if(textLength == 0)
{ {
return Array.Empty<TextBounds>(); throw new ArgumentOutOfRangeException(nameof(textLength), textLength, $"{nameof(textLength)} ('0') must be a non-zero value. ");
} }
var result = new List<TextBounds>(); if (_indexedTextRuns is null || _indexedTextRuns.Count == 0)
{
return [];
}
var currentPosition = FirstTextSourceIndex; var currentPosition = FirstTextSourceIndex;
var remainingLength = textLength; var remainingLength = textLength;
TextBounds? lastBounds = null; //We can return early if the requested text range is before the line's text range.
if (firstTextSourceIndex + textLength < FirstTextSourceIndex)
static FlowDirection GetDirection(TextRun textRun, FlowDirection currentDirection)
{ {
if (textRun is ShapedTextRun shapedTextRun) var indexedTextRun = _indexedTextRuns[0];
{ var currentDirection = GetDirection(indexedTextRun.TextRun, _resolvedFlowDirection);
return shapedTextRun.ShapedBuffer.IsLeftToRight ?
FlowDirection.LeftToRight :
FlowDirection.RightToLeft;
}
return currentDirection; return [new TextBounds(new Rect(0,0,0, Height), currentDirection, [])];
} }
IndexedTextRun FindIndexedRun() //We can return early if the requested text range is after the line's text range.
if (firstTextSourceIndex >= FirstTextSourceIndex + Length)
{ {
var i = 0; var indexedTextRun = _indexedTextRuns[_indexedTextRuns.Count - 1];
var currentDirection = GetDirection(indexedTextRun.TextRun, _resolvedFlowDirection);
IndexedTextRun currentIndexedRun = _indexedTextRuns[i]; return [new TextBounds(new Rect(WidthIncludingTrailingWhitespace, 0, 0, Height), currentDirection, [])];
while (currentIndexedRun.TextSourceCharacterIndex != currentPosition)
{
if (i + 1 == _indexedTextRuns.Count)
{
break;
}
i++;
currentIndexedRun = _indexedTextRuns[i];
}
return currentIndexedRun;
}
double GetPreceedingDistance(int firstIndex)
{
var distance = 0.0;
for (var i = 0; i < firstIndex; i++)
{
var currentRun = _textRuns[i];
if (currentRun is DrawableTextRun drawableTextRun)
{
distance += drawableTextRun.Size.Width;
}
}
return distance;
} }
bool TryMergeWithLastBounds(TextBounds currentBounds, TextBounds lastBounds) var result = new List<TextBounds>();
{
if (currentBounds.FlowDirection != lastBounds.FlowDirection)
{
return false;
}
if (currentBounds.Rectangle.Left == lastBounds.Rectangle.Right)
{
foreach (var runBounds in currentBounds.TextRunBounds)
{
lastBounds.TextRunBounds.Add(runBounds);
}
lastBounds.Rectangle = lastBounds.Rectangle.Union(currentBounds.Rectangle);
return true;
}
if (currentBounds.Rectangle.Right == lastBounds.Rectangle.Left)
{
for (int i = 0; i < currentBounds.TextRunBounds.Count; i++)
{
lastBounds.TextRunBounds.Insert(i, currentBounds.TextRunBounds[i]);
}
lastBounds.Rectangle = lastBounds.Rectangle.Union(currentBounds.Rectangle);
return true;
}
return false; TextBounds? lastBounds = null;
}
while (remainingLength > 0 && currentPosition < FirstTextSourceIndex + Length) while (remainingLength > 0 && currentPosition < FirstTextSourceIndex + Length)
{ {
@ -733,8 +672,8 @@ namespace Avalonia.Media.TextFormatting
directionalWidth = currentDrawable.Size.Width; directionalWidth = currentDrawable.Size.Width;
} }
TextBounds currentBounds;
int coveredLength; int coveredLength;
TextBounds? currentBounds;
switch (currentDirection) switch (currentDirection)
{ {
@ -754,12 +693,6 @@ namespace Avalonia.Media.TextFormatting
} }
} }
if (coveredLength == 0)
{
//This should never happen
break;
}
if (lastBounds != null && TryMergeWithLastBounds(currentBounds, lastBounds)) if (lastBounds != null && TryMergeWithLastBounds(currentBounds, lastBounds))
{ {
currentBounds = lastBounds; currentBounds = lastBounds;
@ -779,6 +712,90 @@ namespace Avalonia.Media.TextFormatting
result.Sort(TextBoundsComparer); result.Sort(TextBoundsComparer);
return result; return result;
static FlowDirection GetDirection(TextRun? textRun, FlowDirection currentDirection)
{
if (textRun is ShapedTextRun shapedTextRun)
{
return shapedTextRun.ShapedBuffer.IsLeftToRight ?
FlowDirection.LeftToRight :
FlowDirection.RightToLeft;
}
return currentDirection;
}
IndexedTextRun FindIndexedRun()
{
var i = 0;
var currentIndexedRun = _indexedTextRuns[i];
while (currentIndexedRun.TextSourceCharacterIndex != currentPosition)
{
if (i + 1 == _indexedTextRuns.Count)
{
break;
}
i++;
currentIndexedRun = _indexedTextRuns[i];
}
return currentIndexedRun;
}
double GetPreceedingDistance(int firstIndex)
{
var distance = 0.0;
for (var i = 0; i < firstIndex; i++)
{
var currentRun = _textRuns[i];
if (currentRun is DrawableTextRun drawableTextRun)
{
distance += drawableTextRun.Size.Width;
}
}
return distance;
}
bool TryMergeWithLastBounds(TextBounds currentBounds, TextBounds lastBounds)
{
if (currentBounds.FlowDirection != lastBounds.FlowDirection)
{
return false;
}
if (currentBounds.Rectangle.Left == lastBounds.Rectangle.Right)
{
foreach (var runBounds in currentBounds.TextRunBounds)
{
lastBounds.TextRunBounds.Add(runBounds);
}
lastBounds.Rectangle = lastBounds.Rectangle.Union(currentBounds.Rectangle);
return true;
}
if (currentBounds.Rectangle.Right == lastBounds.Rectangle.Left)
{
for (int i = 0; i < currentBounds.TextRunBounds.Count; i++)
{
lastBounds.TextRunBounds.Insert(i, currentBounds.TextRunBounds[i]);
}
lastBounds.Rectangle = lastBounds.Rectangle.Union(currentBounds.Rectangle);
return true;
}
return false;
}
} }
private CharacterHit GetPreviousCharacterHit(CharacterHit characterHit, bool useGraphemeBoundaries) private CharacterHit GetPreviousCharacterHit(CharacterHit characterHit, bool useGraphemeBoundaries)
@ -885,7 +902,10 @@ namespace Avalonia.Media.TextFormatting
{ {
var runBounds = GetRunBoundsRightToLeft(shapedTextRun, startX, firstTextSourceIndex, remainingLength, currentPosition, out var offset); var runBounds = GetRunBoundsRightToLeft(shapedTextRun, startX, firstTextSourceIndex, remainingLength, currentPosition, out var offset);
textRunBounds.Insert(0, runBounds); if (runBounds.TextSourceCharacterIndex < FirstTextSourceIndex + Length)
{
textRunBounds.Insert(0, runBounds);
}
if (offset > 0) if (offset > 0)
{ {
@ -904,20 +924,25 @@ namespace Avalonia.Media.TextFormatting
} }
else else
{ {
if (currentRun is DrawableTextRun drawableTextRun) if (currentPosition < FirstTextSourceIndex + Length)
{ {
startX -= drawableTextRun.Size.Width; if (currentRun is DrawableTextRun drawableTextRun)
{
startX -= drawableTextRun.Size.Width;
textRunBounds.Insert(0, var runBounds = new TextRunBounds(
new TextRunBounds( new Rect(startX, 0, drawableTextRun.Size.Width, Height), currentPosition, currentRun.Length, currentRun);
new Rect(startX, 0, drawableTextRun.Size.Width, Height), currentPosition, currentRun.Length, currentRun));
} textRunBounds.Insert(0, runBounds);
else }
{ else
//Add potential TextEndOfParagraph {
textRunBounds.Add( //Add potential TextEndOfParagraph
new TextRunBounds( var runBounds = new TextRunBounds(
new Rect(endX, 0, 0, Height), currentPosition, currentRun.Length, currentRun)); new Rect(endX, 0, 0, Height), currentPosition, currentRun.Length, currentRun);
textRunBounds.Add(runBounds);
}
} }
currentPosition += currentRun.Length; currentPosition += currentRun.Length;
@ -946,7 +971,7 @@ namespace Avalonia.Media.TextFormatting
int firstTextSourceIndex, int currentPosition, int remainingLength, out int coveredLength, out int newPosition) int firstTextSourceIndex, int currentPosition, int remainingLength, out int coveredLength, out int newPosition)
{ {
coveredLength = 0; coveredLength = 0;
var textRunBounds = new List<TextRunBounds>(); var textRunBounds = new List<TextRunBounds>(1);
var endX = startX; var endX = startX;
for (int i = firstRunIndex; i <= lastRunIndex; i++) for (int i = firstRunIndex; i <= lastRunIndex; i++)
@ -957,7 +982,10 @@ namespace Avalonia.Media.TextFormatting
{ {
var runBounds = GetRunBoundsLeftToRight(shapedTextRun, endX, firstTextSourceIndex, remainingLength, currentPosition, out var offset); var runBounds = GetRunBoundsLeftToRight(shapedTextRun, endX, firstTextSourceIndex, remainingLength, currentPosition, out var offset);
textRunBounds.Add(runBounds); if(runBounds.TextSourceCharacterIndex < FirstTextSourceIndex + Length)
{
textRunBounds.Add(runBounds);
}
if (offset > 0) if (offset > 0)
{ {
@ -976,20 +1004,26 @@ namespace Avalonia.Media.TextFormatting
} }
else else
{ {
if (currentRun is DrawableTextRun drawableTextRun) if (currentPosition < FirstTextSourceIndex + Length)
{ {
textRunBounds.Add( if (currentRun is DrawableTextRun drawableTextRun)
new TextRunBounds( {
new Rect(endX, 0, drawableTextRun.Size.Width, Height), currentPosition, currentRun.Length, currentRun)); var runBounds = new TextRunBounds(
new Rect(endX, 0, drawableTextRun.Size.Width, Height), currentPosition, currentRun.Length, currentRun);
endX += drawableTextRun.Size.Width; textRunBounds.Add(runBounds);
}
else
{ endX += drawableTextRun.Size.Width;
//Add potential TextEndOfParagraph }
textRunBounds.Add( else
new TextRunBounds( {
new Rect(endX, 0, 0, Height), currentPosition, currentRun.Length, currentRun)); //Add potential TextEndOfParagraph
var runBounds = new TextRunBounds(
new Rect(endX, 0, 0, Height), currentPosition, currentRun.Length, currentRun);
textRunBounds.Add(runBounds);
}
} }
currentPosition += currentRun.Length; currentPosition += currentRun.Length;
@ -1032,6 +1066,20 @@ namespace Avalonia.Media.TextFormatting
startIndex += offset; startIndex += offset;
} }
//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);
var clusterOffset = 0;
if (startIndex > clusterStartHit.FirstCharacterIndex && startIndex < clusterEndHit.FirstCharacterIndex + clusterEndHit.TrailingLength)
{
clusterOffset = clusterEndHit.FirstCharacterIndex + clusterEndHit.TrailingLength - startIndex;
//We need to move the startIndex to the start of the cluster.
startIndex -= clusterOffset;
}
var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
@ -1041,7 +1089,8 @@ namespace Avalonia.Media.TextFormatting
var startHit = currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); var startHit = currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
var characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength); //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;
if (characterLength == 0 && currentRun.Text.Length > 0 && startIndex < currentRun.Text.Length) if (characterLength == 0 && currentRun.Text.Length > 0 && startIndex < currentRun.Text.Length)
{ {
@ -1075,7 +1124,7 @@ namespace Avalonia.Media.TextFormatting
var runWidth = endX - startX; var runWidth = endX - startX;
var textSourceIndex = offset + startHit.FirstCharacterIndex; var textSourceIndex = startIndex + Math.Max(0, currentPosition - firstCluster) + clusterOffset;
return new TextRunBounds(new Rect(startX, 0, runWidth, Height), textSourceIndex, characterLength, currentRun); return new TextRunBounds(new Rect(startX, 0, runWidth, Height), textSourceIndex, characterLength, currentRun);
} }
@ -1101,7 +1150,6 @@ namespace Avalonia.Media.TextFormatting
} }
var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
startX -= currentRun.Size.Width - startOffset; startX -= currentRun.Size.Width - startOffset;
@ -1110,7 +1158,21 @@ namespace Avalonia.Media.TextFormatting
var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
var startHit = currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); var startHit = currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
var characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength); //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);
var clusterOffset = 0;
if (startIndex > clusterStartHit.FirstCharacterIndex && startIndex < clusterEndHit.FirstCharacterIndex + clusterEndHit.TrailingLength)
{
clusterOffset = clusterEndHit.FirstCharacterIndex + clusterEndHit.TrailingLength - startIndex;
//We need to move the startIndex to the start of the cluster.
startIndex -= clusterOffset;
}
var characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength) - clusterOffset;
if (characterLength == 0 && currentRun.Text.Length > 0 && startIndex < currentRun.Text.Length) if (characterLength == 0 && currentRun.Text.Length > 0 && startIndex < currentRun.Text.Length)
{ {
@ -1149,7 +1211,7 @@ namespace Avalonia.Media.TextFormatting
var runWidth = endX - startX; var runWidth = endX - startX;
var textSourceIndex = offset + startHit.FirstCharacterIndex; var textSourceIndex = startIndex + Math.Max(0, currentPosition - firstCluster) + clusterOffset;
return new TextRunBounds(new Rect(startX, 0, runWidth, Height), textSourceIndex, characterLength, currentRun); return new TextRunBounds(new Rect(startX, 0, runWidth, Height), textSourceIndex, characterLength, currentRun);
} }

316
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

@ -886,6 +886,322 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
} }
} }
[Fact]
public void Should_Throw_ArgumentOutOfRangeException_For_Zero_TextLength()
{
using (Start())
{
var typeface = Typeface.Default;
var defaultProperties = new GenericTextRunProperties(typeface);
var textSource = new CustomTextBufferTextSource(new TextCharacters("1234", defaultProperties));
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
Assert.NotNull(textLine);
Assert.Throws<ArgumentOutOfRangeException>(() => textLine.GetTextBounds(0, 0));
}
}
[Fact]
public void Should_GetTextBounds_For_Negative_TextLength()
{
using (Start())
{
var typeface = Typeface.Default;
var defaultProperties = new GenericTextRunProperties(typeface);
var textSource = new CustomTextBufferTextSource(new TextCharacters("1234", defaultProperties));
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
Assert.NotNull(textLine);
var textBounds = textLine.GetTextBounds(0, -1);
Assert.NotNull(textBounds);
Assert.NotEmpty(textBounds);
var firstBounds = textBounds[0];
Assert.Empty(firstBounds.TextRunBounds);
Assert.Equal(0, firstBounds.Rectangle.Width);
Assert.Equal(0, firstBounds.Rectangle.Left);
}
}
[Fact]
public void Should_GetTextBounds_For_Exceeding_TextLength()
{
using (Start())
{
var typeface = Typeface.Default;
var defaultProperties = new GenericTextRunProperties(typeface);
var textSource = new CustomTextBufferTextSource(new TextCharacters("1234", defaultProperties));
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.NotNull(textBounds);
Assert.NotEmpty(textBounds);
var firstBounds = textBounds[0];
Assert.Empty(firstBounds.TextRunBounds);
Assert.Equal(0, firstBounds.Rectangle.Width);
Assert.Equal(textLine.WidthIncludingTrailingWhitespace, firstBounds.Rectangle.Right);
}
}
[Fact]
public void Should_GetTextBounds_For_Mixed_Hidden_Runs_With_Ligature()
{
using (Start())
{
var typeface = new Typeface(FontFamily.Parse("resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#Manrope"));
var defaultProperties = new GenericTextRunProperties(typeface);
var textSource = new CustomTextBufferTextSource(
new TextHidden(1),
new TextCharacters("Authenti", defaultProperties),
new TextHidden(1),
new TextHidden(1),
new TextCharacters("ff", defaultProperties),
new TextHidden(1),
new TextHidden(1));
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
Assert.NotNull(textLine);
var textBounds = textLine.GetTextBounds(12, 1);
Assert.NotEmpty(textBounds);
var firstBounds = textBounds[0];
Assert.NotNull(firstBounds.TextRunBounds);
Assert.NotEmpty(firstBounds.TextRunBounds);
var firstRun = firstBounds.TextRunBounds[0];
Assert.NotNull(firstRun);
Assert.Equal(12, firstRun.TextSourceCharacterIndex);
}
}
[Fact]
public void Should_GetTextBounds_For_Mixed_Hidden_Runs()
{
using (Start())
{
var typeface = new Typeface(FontFamily.Parse("resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#Manrope"));
var defaultProperties = new GenericTextRunProperties(typeface);
var textSource = new CustomTextBufferTextSource(
new TextHidden(1),
new TextCharacters("Authenti", defaultProperties),
new TextHidden(1),
new TextHidden(1),
new TextEndOfParagraph(1));
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
Assert.NotNull(textLine);
var textBounds = textLine.GetTextBounds(8, 1);
Assert.NotEmpty(textBounds);
var firstBounds = textBounds[0];
Assert.NotNull(firstBounds.TextRunBounds);
Assert.NotEmpty(firstBounds.TextRunBounds);
var firstRun = firstBounds.TextRunBounds[0];
Assert.NotNull(firstRun);
Assert.Equal(8, firstRun.TextSourceCharacterIndex);
}
}
[Win32Fact("Windows font")]
public void Should_GetTextBounds_Within_Cluster()
{
using (Start())
{
var typeface = new Typeface("Segoe UI Emoji");
var defaultProperties = new GenericTextRunProperties(typeface);
var textSource = new CustomTextBufferTextSource(new TextCharacters("🙈", defaultProperties));
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
Assert.NotNull(textLine);
var textBounds = textLine.GetTextBounds(0, 1);
Assert.NotEmpty(textBounds);
var runBounds = textBounds[0].TextRunBounds[0];
Assert.Equal(0, runBounds.TextSourceCharacterIndex);
textBounds = textLine.GetTextBounds(1, 1);
Assert.NotEmpty(textBounds);
runBounds = textBounds[0].TextRunBounds[0];
Assert.Equal(1, runBounds.TextSourceCharacterIndex);
textBounds = textLine.GetTextBounds(2, 1);
Assert.NotEmpty(textBounds);
Assert.NotNull(textBounds[0].TextRunBounds);
Assert.Empty(textBounds[0].TextRunBounds);
}
}
[Win32Fact("Windows font")]
public void Should_GetTextBounds_After_Last_Index()
{
using (Start())
{
var typeface = new Typeface("Segoe UI Emoji");
var defaultProperties = new GenericTextRunProperties(typeface);
var textSource = new CustomTextBufferTextSource(new TextCharacters("🙈", defaultProperties));
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
Assert.NotNull(textLine);
var textBounds = textLine.GetTextBounds(2, 1);
Assert.NotEmpty(textBounds);
var firstBounds = textBounds[0];
Assert.Equal(textLine.Width, firstBounds.Rectangle.Right);
Assert.NotNull(firstBounds.TextRunBounds);
Assert.Empty(firstBounds.TextRunBounds);
}
}
[Fact]
public void Should_Get_Run_Bounds()
{
using (Start())
{
var typeface = new Typeface(FontFamily.Parse("resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#Manrope"));
var defaultProperties = new GenericTextRunProperties(typeface);
var textSource = new CustomTextBufferTextSource(
new TextCharacters("He", defaultProperties),
new TextCharacters("Wo", defaultProperties),
new TextCharacters("ff", defaultProperties));
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
Assert.NotNull(textLine);
var textBounds = textLine.GetTextBounds(1, 1);
Assert.NotEmpty(textBounds);
textBounds = textLine.GetTextBounds(2, 1);
Assert.NotEmpty(textBounds);
textBounds = textLine.GetTextBounds(4, 1);
Assert.NotEmpty(textBounds);
}
}
private class TextHidden : TextRun
{
public TextHidden(int length)
{
Length = length;
}
public override int Length { get; }
}
private class CustomTextBufferTextSource : ITextSource
{
private IReadOnlyList<TextRun> _textRuns;
public CustomTextBufferTextSource(params TextRun[] textRuns)
{
_textRuns = textRuns;
}
public TextRun? GetTextRun(int textSourceIndex)
{
var pos = 0;
for(var i = 0; i < _textRuns.Count; i++)
{
var currentRun = _textRuns[i];
if(pos + currentRun.Length > textSourceIndex)
{
return currentRun;
}
pos += currentRun.Length;
}
return null;
}
}
private class MixedTextBufferTextSource : ITextSource private class MixedTextBufferTextSource : ITextSource
{ {
public TextRun? GetTextRun(int textSourceIndex) public TextRun? GetTextRun(int textSourceIndex)

Loading…
Cancel
Save