Browse Source

Merge pull request #10009 from Gillibald/fixes/textProcessing

Text processing fixes
pull/10411/head
Jumar Macato 3 years ago
committed by GitHub
parent
commit
a19b43d06f
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 32
      src/Avalonia.Base/Media/FormattedText.cs
  2. 4
      src/Avalonia.Base/Media/TextFormatting/ITextSource.cs
  3. 21
      src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
  4. 2
      src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs
  5. 18
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  6. 37
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  7. 684
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  8. 51
      src/Avalonia.Controls/TextBlock.cs
  9. BIN
      tests/Avalonia.RenderTests/Assets/NotoSansHebrew-Regular.ttf
  10. 2
      tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj
  11. 2
      tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj
  12. 10
      tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs
  13. 84
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
  14. 66
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
  15. 27
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

32
src/Avalonia.Base/Media/FormattedText.cs

@ -741,6 +741,11 @@ namespace Avalonia.Media
null // no previous line break null // no previous line break
); );
if(Current is null)
{
return false;
}
// check if this line fits the text height // check if this line fits the text height
if (_totalHeight + Current.Height > _that._maxTextHeight) if (_totalHeight + Current.Height > _that._maxTextHeight)
{ {
@ -779,7 +784,7 @@ namespace Avalonia.Media
// maybe there is no next line at all // maybe there is no next line at all
if (Position + Current.Length < _that._text.Length) if (Position + Current.Length < _that._text.Length)
{ {
bool nextLineFits; bool nextLineFits = false;
if (_lineCount + 1 >= _that._maxLineCount) if (_lineCount + 1 >= _that._maxLineCount)
{ {
@ -795,7 +800,10 @@ namespace Avalonia.Media
currentLineBreak currentLineBreak
); );
nextLineFits = (_totalHeight + Current.Height + _nextLine.Height <= _that._maxTextHeight); if(_nextLine != null)
{
nextLineFits = (_totalHeight + Current.Height + _nextLine.Height <= _that._maxTextHeight);
}
} }
if (!nextLineFits) if (!nextLineFits)
@ -819,16 +827,22 @@ namespace Avalonia.Media
_previousLineBreak _previousLineBreak
); );
currentLineBreak = Current.TextLineBreak; if(Current != null)
{
currentLineBreak = Current.TextLineBreak;
}
_that._defaultParaProps.SetTextWrapping(currentWrap); _that._defaultParaProps.SetTextWrapping(currentWrap);
} }
} }
} }
_previousHeight = Current.Height; if(Current != null)
{
_previousHeight = Current.Height;
Length = Current.Length; Length = Current.Length;
}
_previousLineBreak = currentLineBreak; _previousLineBreak = currentLineBreak;
@ -838,7 +852,7 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Wrapper of TextFormatter.FormatLine that auto-collapses the line if needed. /// Wrapper of TextFormatter.FormatLine that auto-collapses the line if needed.
/// </summary> /// </summary>
private TextLine FormatLine(ITextSource textSource, int textSourcePosition, double maxLineLength, TextParagraphProperties paraProps, TextLineBreak? lineBreak) private TextLine? FormatLine(ITextSource textSource, int textSourcePosition, double maxLineLength, TextParagraphProperties paraProps, TextLineBreak? lineBreak)
{ {
var line = _formatter.FormatLine( var line = _formatter.FormatLine(
textSource, textSource,
@ -848,7 +862,7 @@ namespace Avalonia.Media
lineBreak lineBreak
); );
if (_that._trimming != TextTrimming.None && line.HasOverflowed && line.Length > 0) if (line != null && _that._trimming != TextTrimming.None && line.HasOverflowed && line.Length > 0)
{ {
// what I really need here is the last displayed text run of the line // what I really need here is the last displayed text run of the line
// textSourcePosition + line.Length - 1 works except the end of paragraph case, // textSourcePosition + line.Length - 1 works except the end of paragraph case,
@ -1601,11 +1615,11 @@ namespace Avalonia.Media
} }
/// <inheritdoc/> /// <inheritdoc/>
public TextRun? GetTextRun(int textSourceCharacterIndex) public TextRun GetTextRun(int textSourceCharacterIndex)
{ {
if (textSourceCharacterIndex >= _that._text.Length) if (textSourceCharacterIndex >= _that._text.Length)
{ {
return null; return new TextEndOfParagraph();
} }
var thatFormatRider = new SpanRider(_that._formatRuns, _that._latestPosition, textSourceCharacterIndex); var thatFormatRider = new SpanRider(_that._formatRuns, _that._latestPosition, textSourceCharacterIndex);

4
src/Avalonia.Base/Media/TextFormatting/ITextSource.cs

@ -1,6 +1,4 @@
using Avalonia.Metadata; namespace Avalonia.Media.TextFormatting
namespace Avalonia.Media.TextFormatting
{ {
/// <summary> /// <summary>
/// Produces <see cref="TextRun"/> objects that are used by the <see cref="TextFormatter"/>. /// Produces <see cref="TextRun"/> objects that are used by the <see cref="TextFormatter"/>.

21
src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs

@ -82,24 +82,15 @@ namespace Avalonia.Media.TextFormatting
var previousGlyphTypeface = previousProperties?.CachedGlyphTypeface; var previousGlyphTypeface = previousProperties?.CachedGlyphTypeface;
var textSpan = text.Span; var textSpan = text.Span;
if (TryGetShapeableLength(textSpan, defaultGlyphTypeface, null, out var count, out var script)) if (TryGetShapeableLength(textSpan, defaultGlyphTypeface, null, out var count))
{ {
if (script == Script.Common && previousGlyphTypeface is not null)
{
if (TryGetShapeableLength(textSpan, previousGlyphTypeface, null, out var fallbackCount, out _))
{
return new UnshapedTextRun(text.Slice(0, fallbackCount),
defaultProperties.WithTypeface(previousTypeface!.Value), biDiLevel);
}
}
return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(defaultTypeface), return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(defaultTypeface),
biDiLevel); biDiLevel);
} }
if (previousGlyphTypeface is not null) if (previousGlyphTypeface is not null)
{ {
if (TryGetShapeableLength(textSpan, previousGlyphTypeface, defaultGlyphTypeface, out count, out _)) if (TryGetShapeableLength(textSpan, previousGlyphTypeface, defaultGlyphTypeface, out count))
{ {
return new UnshapedTextRun(text.Slice(0, count), return new UnshapedTextRun(text.Slice(0, count),
defaultProperties.WithTypeface(previousTypeface!.Value), biDiLevel); defaultProperties.WithTypeface(previousTypeface!.Value), biDiLevel);
@ -130,7 +121,7 @@ namespace Avalonia.Media.TextFormatting
var fallbackGlyphTypeface = fontManager.GetOrAddGlyphTypeface(fallbackTypeface); var fallbackGlyphTypeface = fontManager.GetOrAddGlyphTypeface(fallbackTypeface);
if (matchFound && TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count, out _)) if (matchFound && TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count))
{ {
//Fallback found //Fallback found
return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface), return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface),
@ -160,17 +151,15 @@ namespace Avalonia.Media.TextFormatting
/// <param name="glyphTypeface">The typeface that is used to find matching characters.</param> /// <param name="glyphTypeface">The typeface that is used to find matching characters.</param>
/// <param name="defaultGlyphTypeface">The default typeface.</param> /// <param name="defaultGlyphTypeface">The default typeface.</param>
/// <param name="length">The shapeable length.</param> /// <param name="length">The shapeable length.</param>
/// <param name="script"></param>
/// <returns></returns> /// <returns></returns>
internal static bool TryGetShapeableLength( internal static bool TryGetShapeableLength(
ReadOnlySpan<char> text, ReadOnlySpan<char> text,
IGlyphTypeface glyphTypeface, IGlyphTypeface glyphTypeface,
IGlyphTypeface? defaultGlyphTypeface, IGlyphTypeface? defaultGlyphTypeface,
out int length, out int length)
out Script script)
{ {
length = 0; length = 0;
script = Script.Unknown; var script = Script.Unknown;
if (text.IsEmpty) if (text.IsEmpty)
{ {

2
src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs

@ -38,7 +38,7 @@
/// <param name="previousLineBreak">A <see cref="TextLineBreak"/> value that specifies the text formatter state, /// <param name="previousLineBreak">A <see cref="TextLineBreak"/> value that specifies the text formatter state,
/// in terms of where the previous line in the paragraph was broken by the text formatting process.</param> /// in terms of where the previous line in the paragraph was broken by the text formatting process.</param>
/// <returns>The formatted line.</returns> /// <returns>The formatted line.</returns>
public abstract TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, public abstract TextLine? FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null); TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null);
} }
} }

18
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@ -18,7 +18,7 @@ namespace Avalonia.Media.TextFormatting
[ThreadStatic] private static BidiAlgorithm? t_bidiAlgorithm; [ThreadStatic] private static BidiAlgorithm? t_bidiAlgorithm;
/// <inheritdoc cref="TextFormatter.FormatLine"/> /// <inheritdoc cref="TextFormatter.FormatLine"/>
public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, public override TextLine? FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null) TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null)
{ {
TextLineBreak? nextLineBreak = null; TextLineBreak? nextLineBreak = null;
@ -41,6 +41,11 @@ namespace Avalonia.Media.TextFormatting
fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool, out var textEndOfLine, fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool, out var textEndOfLine,
out var textSourceLength); out var textSourceLength);
if (fetchedRuns.Count == 0)
{
return null;
}
shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, fontManager, shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, fontManager,
out var resolvedFlowDirection); out var resolvedFlowDirection);
@ -491,16 +496,7 @@ namespace Avalonia.Media.TextFormatting
while (textRunEnumerator.MoveNext()) while (textRunEnumerator.MoveNext())
{ {
var textRun = textRunEnumerator.Current; TextRun textRun = textRunEnumerator.Current!;
if (textRun == null)
{
textRuns.Add(new TextEndOfParagraph());
textSourceLength += TextRun.DefaultTextSourceLength;
break;
}
if (textRun is TextEndOfLine textEndOfLine) if (textRun is TextEndOfLine textEndOfLine)
{ {

37
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

@ -238,7 +238,7 @@ namespace Avalonia.Media.TextFormatting
foreach (var textLine in _textLines) foreach (var textLine in _textLines)
{ {
//Current line isn't covered. //Current line isn't covered.
if (textLine.FirstTextSourceIndex + textLine.Length < start) if (textLine.FirstTextSourceIndex + textLine.Length <= start)
{ {
currentY += textLine.Height; currentY += textLine.Height;
@ -348,14 +348,36 @@ namespace Avalonia.Media.TextFormatting
{ {
var (x, y) = point; var (x, y) = point;
var lastTrailingIndex = textLine.FirstTextSourceIndex + textLine.Length;
var isInside = x >= 0 && x <= textLine.Width && y >= 0 && y <= textLine.Height; var isInside = x >= 0 && x <= textLine.Width && y >= 0 && y <= textLine.Height;
if (x >= textLine.Width && textLine.Length > 0 && textLine.NewLineLength > 0) var lastTrailingIndex = 0;
if(_paragraphProperties.FlowDirection== FlowDirection.LeftToRight)
{ {
lastTrailingIndex -= textLine.NewLineLength; lastTrailingIndex = textLine.FirstTextSourceIndex + textLine.Length;
if (x >= textLine.Width && textLine.Length > 0 && textLine.NewLineLength > 0)
{
lastTrailingIndex -= textLine.NewLineLength;
}
if (textLine.TextLineBreak?.TextEndOfLine is TextEndOfLine textEndOfLine)
{
lastTrailingIndex -= textEndOfLine.Length;
}
} }
else
{
if (x <= textLine.WidthIncludingTrailingWhitespace - textLine.Width && textLine.Length > 0 && textLine.NewLineLength > 0)
{
lastTrailingIndex += textLine.NewLineLength;
}
if (textLine.TextLineBreak?.TextEndOfLine is TextEndOfLine textEndOfLine)
{
lastTrailingIndex += textEndOfLine.Length;
}
}
var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
@ -391,7 +413,7 @@ namespace Avalonia.Media.TextFormatting
/// <returns></returns> /// <returns></returns>
private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize, private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize,
IBrush? foreground, TextAlignment textAlignment, TextWrapping textWrapping, IBrush? foreground, TextAlignment textAlignment, TextWrapping textWrapping,
TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight, TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight,
double letterSpacing) double letterSpacing)
{ {
var textRunStyle = new GenericTextRunProperties(typeface, fontSize, textDecorations, foreground); var textRunStyle = new GenericTextRunProperties(typeface, fontSize, textDecorations, foreground);
@ -456,7 +478,7 @@ namespace Avalonia.Media.TextFormatting
var textLine = textFormatter.FormatLine(_textSource, _textSourceLength, MaxWidth, var textLine = textFormatter.FormatLine(_textSource, _textSourceLength, MaxWidth,
_paragraphProperties, previousLine?.TextLineBreak); _paragraphProperties, previousLine?.TextLineBreak);
if (textLine.Length == 0) if (textLine is null)
{ {
if (previousLine != null && previousLine.NewLineLength > 0) if (previousLine != null && previousLine.NewLineLength > 0)
{ {
@ -518,7 +540,6 @@ namespace Avalonia.Media.TextFormatting
} }
} }
//Make sure the TextLayout always contains at least on empty line
if (textLines.Count == 0) if (textLines.Count == 0)
{ {
var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties); var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties);

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

@ -10,6 +10,7 @@ namespace Avalonia.Media.TextFormatting
private readonly double _paragraphWidth; private readonly double _paragraphWidth;
private readonly TextParagraphProperties _paragraphProperties; private readonly TextParagraphProperties _paragraphProperties;
private TextLineMetrics _textLineMetrics; private TextLineMetrics _textLineMetrics;
private TextLineBreak? _textLineBreak;
private readonly FlowDirection _resolvedFlowDirection; private readonly FlowDirection _resolvedFlowDirection;
public TextLineImpl(TextRun[] textRuns, int firstTextSourceIndex, int length, double paragraphWidth, public TextLineImpl(TextRun[] textRuns, int firstTextSourceIndex, int length, double paragraphWidth,
@ -18,7 +19,7 @@ namespace Avalonia.Media.TextFormatting
{ {
FirstTextSourceIndex = firstTextSourceIndex; FirstTextSourceIndex = firstTextSourceIndex;
Length = length; Length = length;
TextLineBreak = lineBreak; _textLineBreak = lineBreak;
HasCollapsed = hasCollapsed; HasCollapsed = hasCollapsed;
_textRuns = textRuns; _textRuns = textRuns;
@ -38,7 +39,7 @@ namespace Avalonia.Media.TextFormatting
public override int Length { get; } public override int Length { get; }
/// <inheritdoc/> /// <inheritdoc/>
public override TextLineBreak? TextLineBreak { get; } public override TextLineBreak? TextLineBreak => _textLineBreak;
/// <inheritdoc/> /// <inheritdoc/>
public override bool HasCollapsed { get; } public override bool HasCollapsed { get; }
@ -167,50 +168,54 @@ namespace Avalonia.Media.TextFormatting
{ {
if (_textRuns.Length == 0) if (_textRuns.Length == 0)
{ {
return new CharacterHit(); return new CharacterHit(FirstTextSourceIndex);
} }
distance -= Start; distance -= Start;
var firstRunIndex = 0; var lastIndex = _textRuns.Length - 1;
if (_textRuns[firstRunIndex] is TextEndOfLine) if (_textRuns[lastIndex] is TextEndOfLine)
{ {
firstRunIndex++; lastIndex--;
} }
if(firstRunIndex >= _textRuns.Length) var currentPosition = FirstTextSourceIndex;
if (lastIndex < 0)
{ {
return new CharacterHit(FirstTextSourceIndex); return new CharacterHit(currentPosition);
} }
if (distance <= 0) if (distance <= 0)
{ {
var firstRun = _textRuns[firstRunIndex]; var firstRun = _textRuns[0];
return GetRunCharacterHit(firstRun, FirstTextSourceIndex, 0); if (_paragraphProperties.FlowDirection == FlowDirection.RightToLeft)
{
currentPosition = Length - firstRun.Length;
}
return GetRunCharacterHit(firstRun, currentPosition, 0);
} }
if (distance >= WidthIncludingTrailingWhitespace) if (distance >= WidthIncludingTrailingWhitespace)
{ {
var lastRun = _textRuns[_textRuns.Length - 1]; var lastRun = _textRuns[lastIndex];
var size = 0.0;
if (lastRun is DrawableTextRun drawableTextRun) if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight)
{ {
size = drawableTextRun.Size.Width; currentPosition = Length - lastRun.Length;
} }
return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.Length, size); return GetRunCharacterHit(lastRun, currentPosition, distance);
} }
// process hit that happens within the line // process hit that happens within the line
var characterHit = new CharacterHit(); var characterHit = new CharacterHit();
var currentPosition = FirstTextSourceIndex;
var currentDistance = 0.0; var currentDistance = 0.0;
for (var i = 0; i < _textRuns.Length; i++) for (var i = 0; i <= lastIndex; i++)
{ {
var currentRun = _textRuns[i]; var currentRun = _textRuns[i];
@ -242,7 +247,7 @@ namespace Avalonia.Media.TextFormatting
currentRun = _textRuns[j]; currentRun = _textRuns[j];
if(currentRun is not ShapedTextRun) if (currentRun is not ShapedTextRun)
{ {
continue; continue;
} }
@ -274,10 +279,6 @@ namespace Avalonia.Media.TextFormatting
continue; continue;
} }
} }
else
{
continue;
}
break; break;
} }
@ -422,10 +423,10 @@ namespace Avalonia.Media.TextFormatting
{ {
if (currentGlyphRun != null) if (currentGlyphRun != null)
{ {
distance = currentGlyphRun.Size.Width - distance; currentDistance -= currentGlyphRun.Size.Width;
} }
return Math.Max(0, currentDistance - distance); return currentDistance + distance;
} }
if (currentRun is DrawableTextRun drawableTextRun) if (currentRun is DrawableTextRun drawableTextRun)
@ -575,386 +576,505 @@ namespace Avalonia.Media.TextFormatting
return GetPreviousCaretCharacterHit(characterHit); return GetPreviousCaretCharacterHit(characterHit);
} }
private IReadOnlyList<TextBounds> GetTextBoundsLeftToRight(int firstTextSourceIndex, int textLength) public override IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceIndex, int textLength)
{ {
var characterIndex = firstTextSourceIndex + textLength; if (_textRuns.Length == 0)
{
return Array.Empty<TextBounds>();
}
var result = new List<TextBounds>(_textRuns.Length); var result = new List<TextBounds>();
var lastDirection = FlowDirection.LeftToRight;
var currentDirection = lastDirection;
var currentPosition = FirstTextSourceIndex; var currentPosition = FirstTextSourceIndex;
var remainingLength = textLength; var remainingLength = textLength;
var startX = Start; static FlowDirection GetDirection(TextRun textRun, FlowDirection currentDirection)
double currentWidth = 0;
var currentRect = default(Rect);
TextRunBounds lastRunBounds = default;
for (var index = 0; index < _textRuns.Length; index++)
{ {
if (_textRuns[index] is not DrawableTextRun currentRun) if (textRun is ShapedTextRun shapedTextRun)
{ {
continue; return shapedTextRun.ShapedBuffer.IsLeftToRight ?
FlowDirection.LeftToRight :
FlowDirection.RightToLeft;
} }
var characterLength = 0; return currentDirection;
var endX = startX; }
TextRunBounds currentRunBounds;
double combinedWidth; if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight)
{
var currentX = Start;
if (currentRun is ShapedTextRun currentShapedRun) for (int i = 0; i < _textRuns.Length; i++)
{ {
var firstCluster = currentShapedRun.GlyphRun.Metrics.FirstCluster; var currentRun = _textRuns[i];
var firstRunIndex = i;
var lastRunIndex = firstRunIndex;
var currentDirection = GetDirection(currentRun, FlowDirection.LeftToRight);
var directionalWidth = 0.0;
if (currentPosition + currentRun.Length <= firstTextSourceIndex) if (currentRun is DrawableTextRun currentDrawable)
{ {
startX += currentRun.Size.Width; directionalWidth = currentDrawable.Size.Width;
}
currentPosition += currentRun.Length; // Find consecutive runs of same direction
for (; lastRunIndex + 1 < _textRuns.Length; lastRunIndex++)
{
var nextRun = _textRuns[lastRunIndex + 1];
continue; var nextDirection = GetDirection(nextRun, currentDirection);
if (currentDirection != nextDirection)
{
break;
}
if (nextRun is DrawableTextRun nextDrawable)
{
directionalWidth += nextDrawable.Size.Width;
}
} }
if (currentShapedRun.ShapedBuffer.IsLeftToRight) //Skip runs that are not part of the hit test range
switch (currentDirection)
{ {
var startIndex = firstCluster + Math.Max(0, firstTextSourceIndex - currentPosition); case FlowDirection.RightToLeft:
{
for (; lastRunIndex >= firstRunIndex; lastRunIndex--)
{
currentRun = _textRuns[lastRunIndex];
if (currentPosition + currentRun.Length > firstTextSourceIndex)
{
break;
}
currentPosition += currentRun.Length;
double startOffset; if (currentRun is DrawableTextRun drawableTextRun)
{
directionalWidth -= drawableTextRun.Size.Width;
currentX += drawableTextRun.Size.Width;
}
double endOffset; if(lastRunIndex - 1 < 0)
{
break;
}
}
startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); break;
}
default:
{
for (; firstRunIndex <= lastRunIndex; firstRunIndex++)
{
currentRun = _textRuns[firstRunIndex];
endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); if (currentPosition + currentRun.Length > firstTextSourceIndex)
{
break;
}
startX += startOffset; currentPosition += currentRun.Length;
endX += endOffset; if (currentRun is DrawableTextRun drawableTextRun)
{
currentX += drawableTextRun.Size.Width;
directionalWidth -= drawableTextRun.Size.Width;
}
var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); if(firstRunIndex + 1 == _textRuns.Length)
{
break;
}
}
var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); break;
}
}
characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength); i = lastRunIndex;
currentDirection = FlowDirection.LeftToRight; if (directionalWidth == 0)
{
continue;
} }
else
var coveredLength = 0;
TextBounds? textBounds = null;
switch (currentDirection)
{ {
var rightToLeftIndex = index;
var rightToLeftWidth = currentShapedRun.Size.Width;
while (rightToLeftIndex + 1 <= _textRuns.Length - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextRun nextShapedRun) case FlowDirection.RightToLeft:
{
if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight)
{ {
textBounds = GetTextRunBoundsRightToLeft(firstRunIndex, lastRunIndex, currentX + directionalWidth, firstTextSourceIndex,
currentPosition, remainingLength, out coveredLength, out currentPosition);
currentX += directionalWidth;
break; break;
} }
default:
{
textBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex,
currentPosition, remainingLength, out coveredLength, out currentPosition);
rightToLeftIndex++; currentX = textBounds.Rectangle.Right;
rightToLeftWidth += nextShapedRun.Size.Width;
if (currentPosition + nextShapedRun.Length > firstTextSourceIndex + textLength)
{
break; break;
} }
}
currentShapedRun = nextShapedRun; if (coveredLength > 0)
} {
result.Add(textBounds);
remainingLength -= coveredLength;
}
if (remainingLength <= 0)
{
break;
}
}
}
else
{
var currentX = Start + WidthIncludingTrailingWhitespace;
startX += rightToLeftWidth; for (int i = _textRuns.Length - 1; i >= 0; i--)
{
var currentRun = _textRuns[i];
var firstRunIndex = i;
var lastRunIndex = firstRunIndex;
var currentDirection = GetDirection(currentRun, FlowDirection.RightToLeft);
var directionalWidth = 0.0;
currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength); if (currentRun is DrawableTextRun currentDrawable)
{
directionalWidth = currentDrawable.Size.Width;
}
remainingLength -= currentRunBounds.Length; // Find consecutive runs of same direction
currentPosition = currentRunBounds.TextSourceCharacterIndex + currentRunBounds.Length; for (; firstRunIndex - 1 > 0; firstRunIndex--)
endX = currentRunBounds.Rectangle.Right; {
startX = currentRunBounds.Rectangle.Left; var previousRun = _textRuns[firstRunIndex - 1];
var rightToLeftRunBounds = new List<TextRunBounds> { currentRunBounds }; var previousDirection = GetDirection(previousRun, currentDirection);
for (int i = rightToLeftIndex - 1; i >= index; i--) if (currentDirection != previousDirection)
{ {
if (_textRuns[i] is not ShapedTextRun shapedRun) break;
}
if (currentRun is DrawableTextRun previousDrawable)
{
directionalWidth += previousDrawable.Size.Width;
}
}
//Skip runs that are not part of the hit test range
switch (currentDirection)
{
case FlowDirection.RightToLeft:
{ {
continue; for (; lastRunIndex >= firstRunIndex; lastRunIndex--)
} {
currentRun = _textRuns[lastRunIndex];
currentShapedRun = shapedRun; if (currentPosition + currentRun.Length <= firstTextSourceIndex)
{
currentPosition += currentRun.Length;
currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength); if (currentRun is DrawableTextRun drawableTextRun)
{
currentX -= drawableTextRun.Size.Width;
directionalWidth -= drawableTextRun.Size.Width;
}
rightToLeftRunBounds.Insert(0, currentRunBounds); continue;
}
remainingLength -= currentRunBounds.Length; break;
startX = currentRunBounds.Rectangle.Left; }
currentPosition += currentRunBounds.Length; break;
} }
default:
{
for (; firstRunIndex <= lastRunIndex; firstRunIndex++)
{
currentRun = _textRuns[firstRunIndex];
combinedWidth = endX - startX; if (currentPosition + currentRun.Length <= firstTextSourceIndex)
{
currentPosition += currentRun.Length;
currentRect = new Rect(startX, 0, combinedWidth, Height); if (currentRun is DrawableTextRun drawableTextRun)
{
currentX += drawableTextRun.Size.Width;
directionalWidth -= drawableTextRun.Size.Width;
}
currentDirection = FlowDirection.RightToLeft; continue;
}
if (!MathUtilities.IsZero(combinedWidth)) break;
{ }
result.Add(new TextBounds(currentRect, currentDirection, rightToLeftRunBounds));
}
startX = endX; break;
}
} }
}
else
{
if (currentPosition + currentRun.Length <= firstTextSourceIndex)
{
startX += currentRun.Size.Width;
currentPosition += currentRun.Length; i = firstRunIndex;
if (directionalWidth == 0)
{
continue; continue;
} }
if (currentPosition < firstTextSourceIndex) var coveredLength = 0;
{
startX += currentRun.Size.Width;
}
if (currentPosition + currentRun.Length <= characterIndex) TextBounds? textBounds = null;
switch (currentDirection)
{ {
endX += currentRun.Size.Width; case FlowDirection.LeftToRight:
{
textBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX - directionalWidth, firstTextSourceIndex,
currentPosition, remainingLength, out coveredLength, out currentPosition);
currentX -= directionalWidth;
characterLength = currentRun.Length; break;
}
default:
{
textBounds = GetTextRunBoundsRightToLeft(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex,
currentPosition, remainingLength, out coveredLength, out currentPosition);
currentX = textBounds.Rectangle.Left;
break;
}
} }
}
if (endX < startX) //Visual order is always left to right so we need to insert
{ result.Insert(0, textBounds);
(endX, startX) = (startX, endX);
}
//Lines that only contain a linebreak need to be covered here remainingLength -= coveredLength;
if (characterLength == 0)
{ if (remainingLength <= 0)
characterLength = NewLineLength; {
break;
}
} }
}
combinedWidth = endX - startX; return result;
}
currentRunBounds = new TextRunBounds(new Rect(startX, 0, combinedWidth, Height), currentPosition, characterLength, currentRun); private TextBounds GetTextRunBoundsRightToLeft(int firstRunIndex, int lastRunIndex, double endX,
int firstTextSourceIndex, int currentPosition, int remainingLength, out int coveredLength, out int newPosition)
{
coveredLength = 0;
var textRunBounds = new List<TextRunBounds>();
var startX = endX;
currentPosition += characterLength; for (int i = lastRunIndex; i >= firstRunIndex; i--)
{
var currentRun = _textRuns[i];
remainingLength -= characterLength; if (currentRun is ShapedTextRun shapedTextRun)
{
var runBounds = GetRunBoundsRightToLeft(shapedTextRun, startX, firstTextSourceIndex, remainingLength, currentPosition, out var offset);
startX = endX; textRunBounds.Insert(0, runBounds);
if (currentRunBounds.TextRun != null && !MathUtilities.IsZero(combinedWidth) || NewLineLength > 0) if (offset > 0)
{
if (result.Count > 0 && lastDirection == currentDirection && MathUtilities.AreClose(currentRect.Left, lastRunBounds.Rectangle.Right))
{ {
currentRect = currentRect.WithWidth(currentWidth + combinedWidth); endX = runBounds.Rectangle.Right;
var textBounds = result[result.Count - 1]; startX = endX;
}
textBounds.Rectangle = currentRect; startX -= runBounds.Rectangle.Width;
textBounds.TextRunBounds.Add(currentRunBounds); currentPosition += runBounds.Length + offset;
}
else coveredLength += runBounds.Length;
remainingLength -= runBounds.Length;
}
else
{
if (currentRun is DrawableTextRun drawableTextRun)
{ {
currentRect = currentRunBounds.Rectangle; startX -= drawableTextRun.Size.Width;
result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds })); textRunBounds.Insert(0,
new TextRunBounds(
new Rect(startX, 0, drawableTextRun.Size.Width, Height), currentPosition, currentRun.Length, currentRun));
} }
}
lastRunBounds = currentRunBounds; currentPosition += currentRun.Length;
coveredLength += currentRun.Length;
currentWidth += combinedWidth; remainingLength -= currentRun.Length;
}
if (remainingLength <= 0 || currentPosition >= characterIndex) if (remainingLength <= 0)
{ {
break; break;
} }
lastDirection = currentDirection;
} }
return result; newPosition = currentPosition;
}
private IReadOnlyList<TextBounds> GetTextBoundsRightToLeft(int firstTextSourceIndex, int textLength) var runWidth = endX - startX;
{
var characterIndex = firstTextSourceIndex + textLength;
var result = new List<TextBounds>(_textRuns.Length); var bounds = new Rect(startX, 0, runWidth, Height);
var lastDirection = FlowDirection.LeftToRight;
var currentDirection = lastDirection;
var currentPosition = FirstTextSourceIndex; return new TextBounds(bounds, FlowDirection.RightToLeft, textRunBounds);
var remainingLength = textLength; }
var startX = WidthIncludingTrailingWhitespace; private TextBounds GetTextBoundsLeftToRight(int firstRunIndex, int lastRunIndex, double startX,
double currentWidth = 0; int firstTextSourceIndex, int currentPosition, int remainingLength, out int coveredLength, out int newPosition)
var currentRect = default(Rect); {
coveredLength = 0;
var textRunBounds = new List<TextRunBounds>();
var endX = startX;
for (var index = _textRuns.Length - 1; index >= 0; index--) for (int i = firstRunIndex; i <= lastRunIndex; i++)
{ {
if (_textRuns[index] is not DrawableTextRun currentRun) var currentRun = _textRuns[i];
{
continue;
}
if (currentPosition + currentRun.Length < firstTextSourceIndex)
{
startX -= currentRun.Size.Width;
currentPosition += currentRun.Length;
continue;
}
var characterLength = 0;
var endX = startX;
if (currentRun is ShapedTextRun currentShapedRun) if (currentRun is ShapedTextRun shapedTextRun)
{ {
var offset = Math.Max(0, firstTextSourceIndex - currentPosition); var runBounds = GetRunBoundsLeftToRight(shapedTextRun, endX, firstTextSourceIndex, remainingLength, currentPosition, out var offset);
currentPosition += offset;
var startIndex = currentPosition;
double startOffset;
double endOffset;
if (currentShapedRun.ShapedBuffer.IsLeftToRight) textRunBounds.Add(runBounds);
{
if (currentPosition < startIndex)
{
startOffset = endOffset = 0;
}
else
{
endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); if (offset > 0)
}
}
else
{ {
endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); startX = runBounds.Rectangle.Left;
startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); endX = startX;
} }
startX -= currentRun.Size.Width - startOffset; currentPosition += runBounds.Length + offset;
endX -= currentRun.Size.Width - endOffset;
var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); endX += runBounds.Rectangle.Width;
var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength); coveredLength += runBounds.Length;
currentDirection = currentShapedRun.ShapedBuffer.IsLeftToRight ? remainingLength -= runBounds.Length;
FlowDirection.LeftToRight :
FlowDirection.RightToLeft;
} }
else else
{ {
if (currentPosition + currentRun.Length <= characterIndex) if (currentRun is DrawableTextRun drawableTextRun)
{ {
endX -= currentRun.Size.Width; textRunBounds.Add(
new TextRunBounds(
new Rect(endX, 0, drawableTextRun.Size.Width, Height), currentPosition, currentRun.Length, currentRun));
endX += drawableTextRun.Size.Width;
} }
if (currentPosition < firstTextSourceIndex) currentPosition += currentRun.Length;
{
startX -= currentRun.Size.Width;
characterLength = currentRun.Length; coveredLength += currentRun.Length;
}
}
if (endX < startX) remainingLength -= currentRun.Length;
{
(endX, startX) = (startX, endX);
} }
//Lines that only contain a linebreak need to be covered here if (remainingLength <= 0)
if (characterLength == 0)
{ {
characterLength = NewLineLength; break;
} }
}
var runWidth = endX - startX; newPosition = currentPosition;
var currentRunBounds = new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); var runWidth = endX - startX;
if (!MathUtilities.IsZero(runWidth) || NewLineLength > 0) var bounds = new Rect(startX, 0, runWidth, Height);
{
if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, Start + startX))
{
currentRect = currentRect.WithWidth(currentWidth + runWidth);
var textBounds = result[result.Count - 1]; return new TextBounds(bounds, FlowDirection.LeftToRight, textRunBounds);
}
textBounds.Rectangle = currentRect; private TextRunBounds GetRunBoundsLeftToRight(ShapedTextRun currentRun, double startX,
int firstTextSourceIndex, int remainingLength, int currentPosition, out int offset)
{
var startIndex = currentPosition;
textBounds.TextRunBounds.Add(currentRunBounds); offset = Math.Max(0, firstTextSourceIndex - currentPosition);
}
else
{
currentRect = currentRunBounds.Rectangle;
result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds })); var firstCluster = currentRun.GlyphRun.Metrics.FirstCluster;
}
}
currentWidth += runWidth; if (currentPosition != firstCluster)
currentPosition += characterLength; {
startIndex = firstCluster + offset;
}
else
{
startIndex += offset;
}
if (currentPosition > characterIndex) var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
{ var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
break;
}
lastDirection = currentDirection; var endX = startX + endOffset;
remainingLength -= characterLength; startX += startOffset;
if (remainingLength <= 0) var startHit = currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
{ var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
break;
} var characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength);
if (endX < startX)
{
(endX, startX) = (startX, endX);
}
//Lines that only contain a linebreak need to be covered here
if (characterLength == 0)
{
characterLength = NewLineLength;
} }
result.Reverse(); var runWidth = endX - startX;
return result; return new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
} }
private TextRunBounds GetRightToLeftTextRunBounds(ShapedTextRun currentRun, double endX, int firstTextSourceIndex, int characterIndex, int currentPosition, int remainingLength) private TextRunBounds GetRunBoundsRightToLeft(ShapedTextRun currentRun, double endX,
int firstTextSourceIndex, int remainingLength, int currentPosition, out int offset)
{ {
var startX = endX; var startX = endX;
var offset = Math.Max(0, firstTextSourceIndex - currentPosition); var startIndex = currentPosition;
currentPosition += offset; offset = Math.Max(0, firstTextSourceIndex - currentPosition);
var startIndex = currentPosition; var firstCluster = currentRun.GlyphRun.Metrics.FirstCluster;
double startOffset; if (currentPosition != firstCluster)
double endOffset; {
startIndex = firstCluster + offset;
}
else
{
startIndex += offset;
}
endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
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;
endX -= currentRun.Size.Width - endOffset; endX -= currentRun.Size.Width - endOffset;
@ -980,16 +1100,6 @@ namespace Avalonia.Media.TextFormatting
return new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); return new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
} }
public override IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceIndex, int textLength)
{
if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight)
{
return GetTextBoundsLeftToRight(firstTextSourceIndex, textLength);
}
return GetTextBoundsRightToLeft(firstTextSourceIndex, textLength);
}
public override void Dispose() public override void Dispose()
{ {
for (int i = 0; i < _textRuns.Length; i++) for (int i = 0; i < _textRuns.Length; i++)
@ -1005,6 +1115,11 @@ namespace Avalonia.Media.TextFormatting
{ {
_textLineMetrics = CreateLineMetrics(); _textLineMetrics = CreateLineMetrics();
if (_textLineBreak is null && _textRuns.Length > 1 && _textRuns[_textRuns.Length - 1] is TextEndOfLine textEndOfLine)
{
_textLineBreak = new TextLineBreak(textEndOfLine);
}
BidiReorderer.Instance.BidiReorder(_textRuns, _resolvedFlowDirection); BidiReorderer.Instance.BidiReorder(_textRuns, _resolvedFlowDirection);
} }
@ -1328,7 +1443,7 @@ namespace Avalonia.Media.TextFormatting
{ {
width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width; width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width;
trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength; trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength;
newLineLength = textRun.GlyphRun.Metrics.NewLineLength; newLineLength += textRun.GlyphRun.Metrics.NewLineLength;
} }
widthIncludingWhitespace += textRun.Size.Width; widthIncludingWhitespace += textRun.Size.Width;
@ -1340,31 +1455,10 @@ namespace Avalonia.Media.TextFormatting
{ {
widthIncludingWhitespace += drawableTextRun.Size.Width; widthIncludingWhitespace += drawableTextRun.Size.Width;
switch (_paragraphProperties.FlowDirection) if (index == lastRunIndex)
{ {
case FlowDirection.LeftToRight: width = widthIncludingWhitespace;
{ trailingWhitespaceLength = 0;
if (index == lastRunIndex)
{
width = widthIncludingWhitespace;
trailingWhitespaceLength = 0;
newLineLength = 0;
}
break;
}
case FlowDirection.RightToLeft:
{
if (index == lastRunIndex)
{
width = widthIncludingWhitespace;
trailingWhitespaceLength = 0;
newLineLength = 0;
}
break;
}
} }
if (drawableTextRun.Size.Height > height) if (drawableTextRun.Size.Height > height)

51
src/Avalonia.Controls/TextBlock.cs

@ -720,6 +720,16 @@ namespace Avalonia.Controls
var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale); var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale);
if (HasComplexContent)
{
ArrangeComplexContent(TextLayout, padding);
}
if (MathUtilities.AreClose(_constraint.Inflate(padding).Width, finalSize.Width))
{
return finalSize;
}
_constraint = new Size(Math.Ceiling(finalSize.Deflate(padding).Width), double.PositiveInfinity); _constraint = new Size(Math.Ceiling(finalSize.Deflate(padding).Width), double.PositiveInfinity);
_textLayout?.Dispose(); _textLayout?.Dispose();
@ -727,31 +737,36 @@ namespace Avalonia.Controls
if (HasComplexContent) if (HasComplexContent)
{ {
var currentY = padding.Top; ArrangeComplexContent(TextLayout, padding);
}
foreach (var textLine in TextLayout.TextLines) return finalSize;
{ }
var currentX = padding.Left + textLine.Start;
foreach (var run in textLine.TextRuns) private static void ArrangeComplexContent(TextLayout textLayout, Thickness padding)
{
var currentY = padding.Top;
foreach (var textLine in textLayout.TextLines)
{
var currentX = padding.Left + textLine.Start;
foreach (var run in textLine.TextRuns)
{
if (run is DrawableTextRun drawable)
{ {
if (run is DrawableTextRun drawable) if (drawable is EmbeddedControlRun controlRun
&& controlRun.Control is Control control)
{ {
if (drawable is EmbeddedControlRun controlRun control.Arrange(new Rect(new Point(currentX, currentY), control.DesiredSize));
&& controlRun.Control is Control control)
{
control.Arrange(new Rect(new Point(currentX, currentY), control.DesiredSize));
}
currentX += drawable.Size.Width;
} }
}
currentY += textLine.Height; currentX += drawable.Size.Width;
}
} }
}
return finalSize; currentY += textLine.Height;
}
} }
protected override AutomationPeer OnCreateAutomationPeer() protected override AutomationPeer OnCreateAutomationPeer()
@ -892,7 +907,7 @@ namespace Avalonia.Controls
return textRun; return textRun;
} }
return null; return new TextEndOfParagraph();
} }
} }
} }

BIN
tests/Avalonia.RenderTests/Assets/NotoSansHebrew-Regular.ttf

Binary file not shown.

2
tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj

@ -8,7 +8,7 @@
<Compile Include="..\Avalonia.RenderTests\**\*.cs" /> <Compile Include="..\Avalonia.RenderTests\**\*.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="..\Avalonia.RenderTests\**\*.ttf" /> <EmbeddedResource Include="..\Avalonia.RenderTests\*\*.ttf" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" /> <ProjectReference Include="..\..\src\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />

2
tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj

@ -9,7 +9,7 @@
<Import Project="..\..\build\Microsoft.Reactive.Testing.props" /> <Import Project="..\..\build\Microsoft.Reactive.Testing.props" />
<Import Project="..\..\build\SharedVersion.props" /> <Import Project="..\..\build\SharedVersion.props" />
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="..\Avalonia.RenderTests\**\*.ttf" /> <EmbeddedResource Include="..\Avalonia.RenderTests\*\*.ttf" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj" /> <ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj" />

10
tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs

@ -17,6 +17,8 @@ namespace Avalonia.Skia.UnitTests.Media
new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono"); new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono");
private readonly Typeface _arabicTypeface = private readonly Typeface _arabicTypeface =
new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans Arabic"); new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans Arabic");
private readonly Typeface _hebrewTypeface =
new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans Hebrew");
private readonly Typeface _italicTypeface = private readonly Typeface _italicTypeface =
new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans", FontStyle.Italic); new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans", FontStyle.Italic);
private readonly Typeface _emojiTypeface = private readonly Typeface _emojiTypeface =
@ -24,7 +26,7 @@ namespace Avalonia.Skia.UnitTests.Media
public CustomFontManagerImpl() public CustomFontManagerImpl()
{ {
_customTypefaces = new[] { _emojiTypeface, _italicTypeface, _arabicTypeface, _defaultTypeface }; _customTypefaces = new[] { _emojiTypeface, _italicTypeface, _arabicTypeface, _hebrewTypeface, _defaultTypeface };
_defaultFamilyName = _defaultTypeface.FontFamily.FamilyNames.PrimaryFamilyName; _defaultFamilyName = _defaultTypeface.FontFamily.FamilyNames.PrimaryFamilyName;
} }
@ -88,6 +90,12 @@ namespace Avalonia.Skia.UnitTests.Media
skTypeface = typefaceCollection.Get(typeface); skTypeface = typefaceCollection.Get(typeface);
break; break;
} }
case "Noto Sans Hebrew":
{
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_hebrewTypeface.FontFamily);
skTypeface = typefaceCollection.Get(typeface);
break;
}
case FontFamily.DefaultFontFamilyName: case FontFamily.DefaultFontFamilyName:
case "Noto Mono": case "Noto Mono":
{ {

84
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs

@ -660,6 +660,90 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
} }
} }
[Fact]
public void Should_Return_Null_For_Empty_TextSource()
{
using (Start())
{
var defaultRunProperties = new GenericTextRunProperties(Typeface.Default);
var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties);
var textSource = new EmptyTextSource();
var textLine = TextFormatter.Current.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProperties);
Assert.Null(textLine);
}
}
[Fact]
public void Should_Retain_TextEndOfParagraph_With_TextWrapping()
{
using (Start())
{
var defaultRunProperties = new GenericTextRunProperties(Typeface.Default);
var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties, textWrap: TextWrapping.Wrap);
var text = "Hello World";
var textSource = new SimpleTextSource(text, defaultRunProperties);
var pos = 0;
TextLineBreak previousLineBreak = null;
TextLine textLine = null;
while (pos < text.Length)
{
textLine = TextFormatter.Current.FormatLine(textSource, pos, 30, paragraphProperties, previousLineBreak);
pos += textLine.Length;
previousLineBreak = textLine.TextLineBreak;
}
Assert.NotNull(textLine);
Assert.NotNull(textLine.TextLineBreak.TextEndOfLine);
}
}
protected readonly record struct SimpleTextSource : ITextSource
{
private readonly string _text;
private readonly TextRunProperties _defaultProperties;
public SimpleTextSource(string text, TextRunProperties defaultProperties)
{
_text = text;
_defaultProperties = defaultProperties;
}
public TextRun? GetTextRun(int textSourceIndex)
{
if (textSourceIndex > _text.Length)
{
return new TextEndOfParagraph();
}
var runText = _text.AsMemory(textSourceIndex);
if (runText.IsEmpty)
{
return new TextEndOfParagraph();
}
return new TextCharacters(runText, _defaultProperties);
}
}
private class EmptyTextSource : ITextSource
{
public TextRun GetTextRun(int textSourceIndex)
{
return null;
}
}
private class EndOfLineTextSource : ITextSource private class EndOfLineTextSource : ITextSource
{ {
public TextRun GetTextRun(int textSourceIndex) public TextRun GetTextRun(int textSourceIndex)

66
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs

@ -9,7 +9,6 @@ using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Avalonia.Utilities; using Avalonia.Utilities;
using Xunit; using Xunit;
namespace Avalonia.Skia.UnitTests.Media.TextFormatting namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{ {
public class TextLayoutTests public class TextLayoutTests
@ -725,7 +724,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var selectedRect = rects[0]; var selectedRect = rects[0];
Assert.Equal(selectedText.Bounds.Width, selectedRect.Width); Assert.Equal(selectedText.Bounds.Width, selectedRect.Width, 2);
} }
} }
@ -886,7 +885,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var distance = hitRange.First().Left; var distance = hitRange.First().Left;
Assert.Equal(currentX, distance); Assert.Equal(currentX, distance, 2);
currentX += advance; currentX += advance;
} }
@ -916,7 +915,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var distance = hitRange.First().Left + 0.5; var distance = hitRange.First().Left + 0.5;
Assert.Equal(currentX, distance); Assert.Equal(currentX, distance, 2);
currentX += advance; currentX += advance;
} }
@ -1028,6 +1027,65 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
} }
} }
[InlineData("mgfg🧐df f sdf", "g🧐d", 20, 40)]
[InlineData("وه. وقد تعرض لانتقادات", "دات", 5, 30)]
[InlineData("وه. وقد تعرض لانتقادات", "تعرض", 20, 50)]
[InlineData(" علمية 😱ومضللة ،", " علمية 😱ومضللة ،", 40, 100)]
[InlineData("في عام 2018 ، رفعت ل", "في عام 2018 ، رفعت ل", 100, 120)]
[Theory]
public void HitTestTextRange_Range_ValidLength(string text, string textToSelect, double minWidth, double maxWidth)
{
using (Start())
{
var layout = new TextLayout(text, Typeface.Default, 12, Brushes.Black);
var start = text.IndexOf(textToSelect);
var selectionRectangles = layout.HitTestTextRange(start, textToSelect.Length);
Assert.Equal(1, selectionRectangles.Count());
var rect = selectionRectangles.First();
Assert.InRange(rect.Width, minWidth, maxWidth);
}
}
[InlineData("012🧐210", 2, 4, FlowDirection.LeftToRight, "14.40234375,40.8046875")]
[InlineData("210🧐012", 2, 4, FlowDirection.RightToLeft, "0,7.201171875;21.603515625,33.603515625;48.005859375,55.20703125")]
[InlineData("שנב🧐שנב", 2, 4, FlowDirection.LeftToRight, "11.268,38.208")]
[InlineData("שנב🧐שנב", 2, 4, FlowDirection.RightToLeft, "11.268,38.208")]
[Theory]
public void Should_HitTextTextRangeBetweenRuns(string text, int start, int length,
FlowDirection flowDirection, string expected)
{
using (Start())
{
var expectedRects = expected.Split(';').Select(x =>
{
var startEnd = x.Split(',');
var start = double.Parse(startEnd[0], CultureInfo.InvariantCulture);
var end = double.Parse(startEnd[1], CultureInfo.InvariantCulture);
return new Rect(start, 0, end - start, 0);
}).ToArray();
var textLayout = new TextLayout(text, Typeface.Default, 12, Brushes.Black, flowDirection: flowDirection);
var rects = textLayout.HitTestTextRange(start, length).ToArray();
Assert.Equal(expectedRects.Length, rects.Length);
var endX = textLayout.TextLines[0].GetDistanceFromCharacterHit(new CharacterHit(2));
var startX = textLayout.TextLines[0].GetDistanceFromCharacterHit(new CharacterHit(5, 1));
for (int i = 0; i < expectedRects.Length; i++)
{
var expectedRect = expectedRects[i];
Assert.Equal(expectedRect.Left, rects[i].Left, 2);
Assert.Equal(expectedRect.Right, rects[i].Right, 2);
}
}
}
private static IDisposable Start() private static IDisposable Start()

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

@ -604,19 +604,19 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
textBounds = textLine.GetTextBounds(0, 20); textBounds = textLine.GetTextBounds(0, 20);
Assert.Equal(2, textBounds.Count); Assert.Equal(1, textBounds.Count);
Assert.Equal(144.0234375, textBounds.Sum(x => x.Rectangle.Width)); Assert.Equal(144.0234375, textBounds.Sum(x => x.Rectangle.Width));
textBounds = textLine.GetTextBounds(0, 30); textBounds = textLine.GetTextBounds(0, 30);
Assert.Equal(3, textBounds.Count); Assert.Equal(1, textBounds.Count);
Assert.Equal(216.03515625, textBounds.Sum(x => x.Rectangle.Width)); Assert.Equal(216.03515625, textBounds.Sum(x => x.Rectangle.Width));
textBounds = textLine.GetTextBounds(0, 40); textBounds = textLine.GetTextBounds(0, 40);
Assert.Equal(4, textBounds.Count); Assert.Equal(1, textBounds.Count);
Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width));
} }
@ -658,7 +658,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(TextTestHelper.GetStartCharIndex(run.Text), bounds.TextSourceCharacterIndex); Assert.Equal(TextTestHelper.GetStartCharIndex(run.Text), bounds.TextSourceCharacterIndex);
Assert.Equal(run, bounds.TextRun); Assert.Equal(run, bounds.TextRun);
Assert.Equal(run.Size.Width, bounds.Rectangle.Width); Assert.Equal(run.Size.Width, bounds.Rectangle.Width, 2);
} }
for (var i = 0; i < textBounds.Count; i++) for (var i = 0; i < textBounds.Count; i++)
@ -667,19 +667,19 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
if (lastBounds != null) if (lastBounds != null)
{ {
Assert.Equal(lastBounds.Rectangle.Right, currentBounds.Rectangle.Left); Assert.Equal(lastBounds.Rectangle.Right, currentBounds.Rectangle.Left, 2);
} }
var sumOfRunWidth = currentBounds.TextRunBounds.Sum(x => x.Rectangle.Width); var sumOfRunWidth = currentBounds.TextRunBounds.Sum(x => x.Rectangle.Width);
Assert.Equal(sumOfRunWidth, currentBounds.Rectangle.Width); Assert.Equal(sumOfRunWidth, currentBounds.Rectangle.Width, 2);
lastBounds = currentBounds; lastBounds = currentBounds;
} }
var sumOfBoundsWidth = textBounds.Sum(x => x.Rectangle.Width); var sumOfBoundsWidth = textBounds.Sum(x => x.Rectangle.Width);
Assert.Equal(lineWidth, sumOfBoundsWidth); Assert.Equal(lineWidth, sumOfBoundsWidth, 2);
} }
} }
@ -847,7 +847,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textBounds = textLine.GetTextBounds(0, textLine.Length); var textBounds = textLine.GetTextBounds(0, textLine.Length);
Assert.Equal(6, textBounds.Count); Assert.Equal(1, textBounds.Count);
Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width));
textBounds = textLine.GetTextBounds(0, 1); textBounds = textLine.GetTextBounds(0, 1);
@ -857,7 +857,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
textBounds = textLine.GetTextBounds(0, firstRun.Length + 1); textBounds = textLine.GetTextBounds(0, firstRun.Length + 1);
Assert.Equal(2, textBounds.Count); Assert.Equal(1, textBounds.Count);
Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width)); Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width));
textBounds = textLine.GetTextBounds(1, firstRun.Length); textBounds = textLine.GetTextBounds(1, firstRun.Length);
@ -867,7 +867,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
textBounds = textLine.GetTextBounds(0, 1 + firstRun.Length); textBounds = textLine.GetTextBounds(0, 1 + firstRun.Length);
Assert.Equal(2, textBounds.Count); Assert.Equal(1, textBounds.Count);
Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width)); Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width));
} }
} }
@ -958,14 +958,15 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(secondRun.Size.Width, textBounds[1].Rectangle.Width); Assert.Equal(secondRun.Size.Width, textBounds[1].Rectangle.Width);
Assert.Equal(7.201171875, textBounds[0].Rectangle.Width); Assert.Equal(7.201171875, textBounds[0].Rectangle.Width);
Assert.Equal(textLine.Start + 7.201171875, textBounds[0].Rectangle.Right);
Assert.Equal(textLine.Start + firstRun.Size.Width, textBounds[1].Rectangle.Left); Assert.Equal(textLine.Start + 7.201171875, textBounds[0].Rectangle.Right, 2);
Assert.Equal(textLine.Start + firstRun.Size.Width, textBounds[1].Rectangle.Left, 2);
textBounds = textLine.GetTextBounds(0, text.Length); textBounds = textLine.GetTextBounds(0, text.Length);
Assert.Equal(2, textBounds.Count); Assert.Equal(2, textBounds.Count);
Assert.Equal(7, textBounds.Sum(x => x.TextRunBounds.Sum(x => x.Length))); Assert.Equal(7, textBounds.Sum(x => x.TextRunBounds.Sum(x => x.Length)));
Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width), 2);
} }
} }

Loading…
Cancel
Save