Browse Source

Rework TextLine GetTextBounds and GetDistanceFromCharacterHit

Fix TextAlignment.Right offset calculation
pull/11532/head
Benedikt Stebner 3 years ago
parent
commit
460ea0e093
  1. 1
      src/Avalonia.Base/Media/CharacterHit.cs
  2. 74
      src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs
  3. 10
      src/Avalonia.Base/Media/TextFormatting/IndexedTextRun.cs
  4. 2
      src/Avalonia.Base/Media/TextFormatting/TextBounds.cs
  5. 594
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  6. 49
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

1
src/Avalonia.Base/Media/CharacterHit.cs

@ -19,6 +19,7 @@ namespace Avalonia.Media
/// <param name="firstCharacterIndex">Index of the first character that got hit.</param> /// <param name="firstCharacterIndex">Index of the first character that got hit.</param>
/// <param name="trailingLength">In the case of a leading edge, this value is 0. In the case of a trailing edge, /// <param name="trailingLength">In the case of a leading edge, this value is 0. In the case of a trailing edge,
/// this value is the number of code points until the next valid caret position.</param> /// this value is the number of code points until the next valid caret position.</param>
[DebuggerStepThrough]
public CharacterHit(int firstCharacterIndex, int trailingLength = 0) public CharacterHit(int firstCharacterIndex, int trailingLength = 0)
{ {
FirstCharacterIndex = firstCharacterIndex; FirstCharacterIndex = firstCharacterIndex;

74
src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs

@ -18,14 +18,14 @@ namespace Avalonia.Media.TextFormatting
public static BidiReorderer Instance public static BidiReorderer Instance
=> t_instance ??= new(); => t_instance ??= new();
public void BidiReorder(Span<TextRun> textRuns, FlowDirection flowDirection) public IndexedTextRun[] BidiReorder(Span<TextRun> textRuns, FlowDirection flowDirection, int firstTextSourceIndex)
{ {
Debug.Assert(_runs.Length == 0); Debug.Assert(_runs.Length == 0);
Debug.Assert(_ranges.Length == 0); Debug.Assert(_ranges.Length == 0);
if (textRuns.IsEmpty) if (textRuns.IsEmpty)
{ {
return; return Array.Empty<IndexedTextRun>();
} }
try try
@ -46,6 +46,22 @@ namespace Avalonia.Media.TextFormatting
// Reorder them into visual order. // Reorder them into visual order.
var firstIndex = LinearReorder(); var firstIndex = LinearReorder();
var indexedTextRuns = new IndexedTextRun[textRuns.Length];
for (var i = 0; i < textRuns.Length; i++)
{
var currentRun = textRuns[i];
indexedTextRuns[i] = new IndexedTextRun
{
TextRun = currentRun,
TextSourceCharacterIndex = firstTextSourceIndex,
RunIndex = i,
NextRunIndex = i + 1
};
firstTextSourceIndex += currentRun.Length;
}
// Now perform a recursive reversal of each run. // Now perform a recursive reversal of each run.
// From the highest level found in the text to the lowest odd level on each line, including intermediate levels // From the highest level found in the text to the lowest odd level on each line, including intermediate levels
@ -76,7 +92,7 @@ namespace Avalonia.Media.TextFormatting
if (max == 0 || (min == max && (max & 1) == 0)) if (max == 0 || (min == max && (max & 1) == 0))
{ {
// Nothing to reverse. // Nothing to reverse.
return; return indexedTextRuns;
} }
// Now apply the reversal and replace the original contents. // Now apply the reversal and replace the original contents.
@ -107,13 +123,25 @@ namespace Avalonia.Media.TextFormatting
var index = 0; var index = 0;
currentIndex = firstIndex; currentIndex = firstIndex;
while (currentIndex >= 0) while (currentIndex >= 0)
{ {
ref var current = ref _runs[currentIndex]; ref var current = ref _runs[currentIndex];
textRuns[index++] = current.Run;
textRuns[index] = current.Run;
var indexedRun = indexedTextRuns[index];
indexedRun.RunIndex = current.RunIndex;
indexedRun.NextRunIndex = current.NextRunIndex;
index++;
currentIndex = current.NextRunIndex; currentIndex = current.NextRunIndex;
} }
return indexedTextRuns;
} }
finally finally
{ {
@ -227,25 +255,6 @@ namespace Avalonia.Media.TextFormatting
return previousIndex; return previousIndex;
} }
private struct OrderedBidiRun
{
public OrderedBidiRun(int runIndex, TextRun run, sbyte level)
{
RunIndex = runIndex;
Run = run;
Level = level;
NextRunIndex = -1;
}
public int RunIndex { get; }
public sbyte Level { get; }
public TextRun Run { get; }
public int NextRunIndex { get; set; } // -1 if none
}
private struct BidiRange private struct BidiRange
{ {
public BidiRange(sbyte level, int leftRunIndex, int rightRunIndex, int previousRangeIndex) public BidiRange(sbyte level, int leftRunIndex, int rightRunIndex, int previousRangeIndex)
@ -265,4 +274,23 @@ namespace Avalonia.Media.TextFormatting
public int PreviousRangeIndex { get; } // -1 if none public int PreviousRangeIndex { get; } // -1 if none
} }
} }
internal struct OrderedBidiRun
{
public OrderedBidiRun(int runIndex, TextRun run, sbyte level)
{
RunIndex = runIndex;
Run = run;
Level = level;
NextRunIndex = -1;
}
public int RunIndex { get; }
public sbyte Level { get; }
public TextRun Run { get; }
public int NextRunIndex { get; set; } // -1 if none
}
} }

10
src/Avalonia.Base/Media/TextFormatting/IndexedTextRun.cs

@ -0,0 +1,10 @@
namespace Avalonia.Media.TextFormatting
{
internal class IndexedTextRun
{
public int TextSourceCharacterIndex { get; init; }
public int RunIndex { get; set; }
public int NextRunIndex { get; set; }
public TextRun? TextRun { get; init; }
}
}

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

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
namespace Avalonia.Media.TextFormatting namespace Avalonia.Media.TextFormatting
{ {
@ -10,6 +11,7 @@ namespace Avalonia.Media.TextFormatting
/// <summary> /// <summary>
/// Constructing TextBounds object /// Constructing TextBounds object
/// </summary> /// </summary>
[DebuggerStepThrough]
internal TextBounds(Rect bounds, FlowDirection flowDirection, IList<TextRunBounds> runBounds) internal TextBounds(Rect bounds, FlowDirection flowDirection, IList<TextRunBounds> runBounds)
{ {
Rectangle = bounds; Rectangle = bounds;

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

@ -4,8 +4,12 @@ using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting namespace Avalonia.Media.TextFormatting
{ {
internal sealed class TextLineImpl : TextLine internal class TextLineImpl : TextLine
{ {
internal static Comparer<TextBounds> TextBoundsComparer { get; } =
Comparer<TextBounds>.Create((x, y) => x.Rectangle.Left.CompareTo(y.Rectangle.Left));
private IReadOnlyList<IndexedTextRun>? _indexedTextRuns;
private readonly TextRun[] _textRuns; private readonly TextRun[] _textRuns;
private readonly double _paragraphWidth; private readonly double _paragraphWidth;
private readonly TextParagraphProperties _paragraphProperties; private readonly TextParagraphProperties _paragraphProperties;
@ -338,184 +342,171 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/> /// <inheritdoc/>
public override double GetDistanceFromCharacterHit(CharacterHit characterHit) public override double GetDistanceFromCharacterHit(CharacterHit characterHit)
{ {
var flowDirection = _paragraphProperties.FlowDirection; if (_indexedTextRuns is null || _indexedTextRuns.Count == 0)
var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
var currentPosition = FirstTextSourceIndex;
var remainingLength = characterIndex - FirstTextSourceIndex;
var currentDistance = Start;
if (flowDirection == FlowDirection.LeftToRight)
{ {
for (var index = 0; index < _textRuns.Length; index++) return Start;
{ }
var currentRun = _textRuns[index];
if (currentRun is ShapedTextRun shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight)
{
var i = index;
var rightToLeftWidth = shapedRun.Size.Width;
while (i + 1 <= _textRuns.Length - 1)
{
var nextRun = _textRuns[i + 1];
if (nextRun is ShapedTextRun nextShapedRun && !nextShapedRun.ShapedBuffer.IsLeftToRight) var characterIndex = Math.Min(
{ characterHit.FirstCharacterIndex + characterHit.TrailingLength,
i++; FirstTextSourceIndex + Length);
rightToLeftWidth += nextShapedRun.Size.Width; var currentPosition = FirstTextSourceIndex;
continue; static FlowDirection GetDirection(TextRun textRun, FlowDirection currentDirection)
} {
if (textRun is ShapedTextRun shapedTextRun)
{
return shapedTextRun.ShapedBuffer.IsLeftToRight ?
FlowDirection.LeftToRight :
FlowDirection.RightToLeft;
}
break; return currentDirection;
} }
if (i > index) IndexedTextRun FindIndexedRun()
{ {
while (i >= index) var i = 0;
{
currentRun = _textRuns[i];
if (currentRun is DrawableTextRun drawable) IndexedTextRun currentIndexedRun = _indexedTextRuns[i];
{
rightToLeftWidth -= drawable.Size.Width;
}
if (currentPosition + currentRun.Length >= characterIndex) while(currentIndexedRun.TextSourceCharacterIndex != currentPosition)
{ {
break; if(i + 1 < _indexedTextRuns.Count)
} {
i++;
currentPosition += currentRun.Length; currentIndexedRun = _indexedTextRuns[i];
}
remainingLength -= currentRun.Length; break;
}
i--; return currentIndexedRun;
} }
currentDistance += rightToLeftWidth; double GetPreceedingDistance(int firstIndex)
} {
} var distance = 0.0;
if (currentPosition + currentRun.Length >= characterIndex && for (var i = 0; i < firstIndex; i++)
TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength, flowDirection, out var distance, out _)) {
{ var currentRun = _textRuns[i];
return Math.Max(0, currentDistance + distance);
}
if (currentRun is DrawableTextRun drawableTextRun) if (currentRun is DrawableTextRun drawableTextRun)
{ {
currentDistance += drawableTextRun.Size.Width; distance += drawableTextRun.Size.Width;
} }
//No hit hit found so we add the full width
currentPosition += currentRun.Length;
remainingLength -= currentRun.Length;
} }
return distance;
} }
else
TextRun? currentTextRun = null;
var currentIndexedRun = FindIndexedRun();
while (currentPosition < FirstTextSourceIndex + Length)
{ {
currentDistance += WidthIncludingTrailingWhitespace; currentTextRun = currentIndexedRun.TextRun;
for (var index = _textRuns.Length - 1; index >= 0; index--) if (currentTextRun == null)
{ {
var currentRun = _textRuns[index]; break;
}
if (TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength, if (currentIndexedRun.TextSourceCharacterIndex + currentTextRun.Length <= characterHit.FirstCharacterIndex)
flowDirection, out var distance, out var currentGlyphRun)) {
if (currentPosition + currentTextRun.Length < FirstTextSourceIndex + Length)
{ {
if (currentGlyphRun != null) currentPosition += currentTextRun.Length;
{
currentDistance -= currentGlyphRun.Bounds.Width;
}
return currentDistance + distance; currentIndexedRun = FindIndexedRun();
}
if (currentRun is DrawableTextRun drawableTextRun) continue;
{
currentDistance -= drawableTextRun.Size.Width;
} }
//No hit hit found so we add the full width
currentPosition += currentRun.Length;
remainingLength -= currentRun.Length;
} }
break;
} }
return Math.Max(0, currentDistance); if (currentTextRun == null)
} {
return 0;
}
private static bool TryGetDistanceFromCharacterHit( var directionalWidth = 0.0;
TextRun currentRun, var firstRunIndex = currentIndexedRun.RunIndex;
CharacterHit characterHit, var lastRunIndex = firstRunIndex;
int currentPosition,
int remainingLength,
FlowDirection flowDirection,
out double distance,
out GlyphRun? currentGlyphRun)
{
var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
var isTrailingHit = characterHit.TrailingLength > 0;
distance = 0; var currentDirection = GetDirection(currentTextRun, _resolvedFlowDirection);
currentGlyphRun = null;
switch (currentRun) var currentX = Start + GetPreceedingDistance(currentIndexedRun.RunIndex);
if (currentTextRun is DrawableTextRun currentDrawable)
{ {
case ShapedTextRun shapedTextCharacters: directionalWidth = currentDrawable.Size.Width;
{ }
currentGlyphRun = shapedTextCharacters.GlyphRun;
if (currentPosition + remainingLength <= currentPosition + currentRun.Length) if (currentTextRun is not TextEndOfLine)
{ {
characterHit = new CharacterHit(currentPosition + remainingLength); if (currentDirection == FlowDirection.LeftToRight)
{
// Find consecutive runs of same direction
for (; lastRunIndex + 1 < _textRuns.Length; lastRunIndex++)
{
var nextRun = _textRuns[lastRunIndex + 1];
distance = currentGlyphRun.GetDistanceFromCharacterHit(characterHit); var nextDirection = GetDirection(nextRun, currentDirection);
return true; if (currentDirection != nextDirection)
{
break;
} }
if (currentPosition + remainingLength == currentPosition + currentRun.Length && isTrailingHit) if (nextRun is DrawableTextRun nextDrawable)
{ {
if (currentGlyphRun.IsLeftToRight || flowDirection == FlowDirection.RightToLeft) directionalWidth += nextDrawable.Size.Width;
{
distance = currentGlyphRun.Bounds.Width;
}
return true;
} }
break;
} }
case DrawableTextRun drawableTextRun: }
else
{
// Find consecutive runs of same direction
for (; firstRunIndex - 1 > 0; firstRunIndex--)
{ {
if (characterIndex == currentPosition) var previousRun = _textRuns[firstRunIndex - 1];
var previousDirection = GetDirection(previousRun, currentDirection);
if (currentDirection != previousDirection)
{ {
return true; break;
} }
if (characterIndex == currentPosition + currentRun.Length) if (previousRun is DrawableTextRun previousDrawable)
{ {
distance = drawableTextRun.Size.Width; directionalWidth += previousDrawable.Size.Width;
return true;
currentX -= previousDrawable.Size.Width;
} }
}
}
}
break; switch (currentDirection)
{
case FlowDirection.RightToLeft:
{
return GetTextRunBoundsRightToLeft(firstRunIndex, lastRunIndex, currentX + directionalWidth, characterIndex,
currentPosition, 1, out _, out _).Rectangle.Right;
} }
default: default:
{ {
return false; return GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX, characterIndex,
currentPosition, 1, out _, out _).Rectangle.Left;
} }
} }
return false;
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -585,7 +576,7 @@ namespace Avalonia.Media.TextFormatting
public override IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceIndex, int textLength) public override IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceIndex, int textLength)
{ {
if (_textRuns.Length == 0) if (_indexedTextRuns is null || _indexedTextRuns.Count == 0)
{ {
return Array.Empty<TextBounds>(); return Array.Empty<TextBounds>();
} }
@ -607,303 +598,156 @@ namespace Avalonia.Media.TextFormatting
return currentDirection; return currentDirection;
} }
if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight) IndexedTextRun FindIndexedRun()
{ {
var currentX = Start; var i = 0;
for (int i = 0; i < _textRuns.Length; i++) IndexedTextRun currentIndexedRun = _indexedTextRuns[i];
{
var currentRun = _textRuns[i];
var firstRunIndex = i; while (currentIndexedRun.TextSourceCharacterIndex != currentPosition)
var lastRunIndex = firstRunIndex; {
var currentDirection = GetDirection(currentRun, FlowDirection.LeftToRight); if (i + 1 < _indexedTextRuns.Count)
var directionalWidth = 0.0;
if (currentRun is DrawableTextRun currentDrawable)
{ {
directionalWidth = currentDrawable.Size.Width; i++;
currentIndexedRun = _indexedTextRuns[i];
} }
// Find consecutive runs of same direction break;
for (; lastRunIndex + 1 < _textRuns.Length; lastRunIndex++) }
{
var nextRun = _textRuns[lastRunIndex + 1];
var nextDirection = GetDirection(nextRun, currentDirection); return currentIndexedRun;
}
if (currentDirection != nextDirection) double GetPreceedingDistance(int firstIndex)
{ {
break; var distance = 0.0;
}
if (nextRun is DrawableTextRun nextDrawable) for (var i = 0; i < firstIndex; i++)
{ {
directionalWidth += nextDrawable.Size.Width; var currentRun = _textRuns[i];
}
}
//Skip runs that are not part of the hit test range if (currentRun is DrawableTextRun drawableTextRun)
switch (currentDirection)
{ {
case FlowDirection.RightToLeft: distance += drawableTextRun.Size.Width;
{ }
for (; lastRunIndex >= firstRunIndex; lastRunIndex--) }
{
currentRun = _textRuns[lastRunIndex];
if (currentPosition + currentRun.Length > firstTextSourceIndex) return distance;
{ }
break;
}
currentPosition += currentRun.Length; while (remainingLength > 0 && currentPosition < FirstTextSourceIndex + Length)
{
var currentIndexedRun = FindIndexedRun();
if (currentRun is DrawableTextRun drawableTextRun) if (currentIndexedRun == null)
{ {
directionalWidth -= drawableTextRun.Size.Width; break;
currentX += drawableTextRun.Size.Width; }
}
if (lastRunIndex - 1 < 0) var directionalWidth = 0.0;
{ var firstRunIndex = currentIndexedRun.RunIndex;
break; var lastRunIndex = firstRunIndex;
} var currentTextRun = currentIndexedRun.TextRun;
}
break; if (currentTextRun == null)
} {
default: break;
{ }
for (; firstRunIndex <= lastRunIndex; firstRunIndex++)
{
currentRun = _textRuns[firstRunIndex];
if (currentPosition + currentRun.Length > firstTextSourceIndex)
{
break;
}
currentPosition += currentRun.Length; var currentDirection = GetDirection(currentTextRun, _resolvedFlowDirection);
if (currentRun is DrawableTextRun drawableTextRun) if (currentIndexedRun.TextSourceCharacterIndex + currentTextRun.Length <= firstTextSourceIndex)
{ {
currentX += drawableTextRun.Size.Width; currentPosition += currentTextRun.Length;
directionalWidth -= drawableTextRun.Size.Width;
}
if (firstRunIndex + 1 == _textRuns.Length) continue;
{ }
break;
}
}
break; var currentX = Start + GetPreceedingDistance(currentIndexedRun.RunIndex);
}
}
i = lastRunIndex; if (currentTextRun is DrawableTextRun currentDrawable)
{
directionalWidth = currentDrawable.Size.Width;
}
//Possible overlap at runs of different direction if (currentTextRun is not TextEndOfLine)
if (directionalWidth == 0 && i < _textRuns.Length - 1) {
if (currentDirection == FlowDirection.LeftToRight)
{ {
//In case a run only contains a linebreak we don't want to skip it. // Find consecutive runs of same direction
if (currentRun is ShapedTextRun shaped) for (; lastRunIndex + 1 < _textRuns.Length; lastRunIndex++)
{
if (currentRun.Length - shaped.GlyphRun.Metrics.NewLineLength > 0)
{
continue;
}
}
else
{ {
continue; var nextRun = _textRuns[lastRunIndex + 1];
}
}
int coveredLength; var nextDirection = GetDirection(nextRun, currentDirection);
TextBounds? textBounds;
switch (currentDirection) if (currentDirection != nextDirection)
{
case FlowDirection.RightToLeft:
{ {
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);
currentX = textBounds.Rectangle.Right; if (nextRun is DrawableTextRun nextDrawable)
{
break; directionalWidth += nextDrawable.Size.Width;
} }
}
} }
else
if (coveredLength > 0)
{
result.Add(textBounds);
remainingLength -= coveredLength;
}
if (remainingLength <= 0)
{
break;
}
}
}
else
{
var currentX = Start + WidthIncludingTrailingWhitespace;
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;
if (currentRun is DrawableTextRun currentDrawable)
{
directionalWidth = currentDrawable.Size.Width;
}
// Find consecutive runs of same direction
for (; firstRunIndex - 1 > 0; firstRunIndex--)
{ {
var previousRun = _textRuns[firstRunIndex - 1]; // Find consecutive runs of same direction
for (; firstRunIndex - 1 > 0; firstRunIndex--)
var previousDirection = GetDirection(previousRun, currentDirection);
if (currentDirection != previousDirection)
{ {
break; var previousRun = _textRuns[firstRunIndex - 1];
}
if (currentRun is DrawableTextRun previousDrawable) var previousDirection = GetDirection(previousRun, currentDirection);
{
directionalWidth += previousDrawable.Size.Width;
}
}
//Skip runs that are not part of the hit test range if (currentDirection != previousDirection)
switch (currentDirection)
{
case FlowDirection.RightToLeft:
{ {
for (; lastRunIndex >= firstRunIndex; lastRunIndex--)
{
currentRun = _textRuns[lastRunIndex];
if (currentPosition + currentRun.Length <= firstTextSourceIndex)
{
currentPosition += currentRun.Length;
if (currentRun is DrawableTextRun drawableTextRun)
{
currentX -= drawableTextRun.Size.Width;
directionalWidth -= drawableTextRun.Size.Width;
}
continue;
}
break;
}
break; break;
} }
default:
{
for (; firstRunIndex <= lastRunIndex; firstRunIndex++)
{
currentRun = _textRuns[firstRunIndex];
if (currentPosition + currentRun.Length <= firstTextSourceIndex)
{
currentPosition += currentRun.Length;
if (currentRun is DrawableTextRun drawableTextRun)
{
currentX += drawableTextRun.Size.Width;
directionalWidth -= drawableTextRun.Size.Width;
}
continue; if (previousRun is DrawableTextRun previousDrawable)
} {
directionalWidth += previousDrawable.Size.Width;
break;
}
break; currentX -= previousDrawable.Size.Width;
} }
}
} }
}
i = firstRunIndex; int coveredLength;
TextBounds? textBounds;
//Possible overlap at runs of different direction switch (currentDirection)
if (directionalWidth == 0 && i > 0) {
{ case FlowDirection.RightToLeft:
//In case a run only contains a linebreak we don't want to skip it.
if (currentRun is ShapedTextRun shaped)
{ {
if (currentRun.Length - shaped.GlyphRun.Metrics.NewLineLength > 0) textBounds = GetTextRunBoundsRightToLeft(firstRunIndex, lastRunIndex, currentX + directionalWidth, firstTextSourceIndex,
{ currentPosition, remainingLength, out coveredLength, out currentPosition);
continue;
} break;
} }
else default:
{ {
continue; textBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex,
} currentPosition, remainingLength, out coveredLength, out currentPosition);
}
int coveredLength;
TextBounds? textBounds;
switch (currentDirection)
{
case FlowDirection.LeftToRight:
{
textBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX - directionalWidth, firstTextSourceIndex,
currentPosition, remainingLength, out coveredLength, out currentPosition);
currentX -= directionalWidth;
break;
}
default:
{
textBounds = GetTextRunBoundsRightToLeft(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex,
currentPosition, remainingLength, out coveredLength, out currentPosition);
currentX = textBounds.Rectangle.Left;
break; break;
} }
} }
//Visual order is always left to right so we need to insert if (coveredLength > 0)
result.Insert(0, textBounds); {
result.Add(textBounds);
remainingLength -= coveredLength; remainingLength -= coveredLength;
if (remainingLength <= 0)
{
break;
}
} }
} }
result.Sort(TextBoundsComparer);
return result; return result;
} }
@ -1164,7 +1008,7 @@ namespace Avalonia.Media.TextFormatting
_textLineBreak = new TextLineBreak(textEndOfLine); _textLineBreak = new TextLineBreak(textEndOfLine);
} }
BidiReorderer.Instance.BidiReorder(_textRuns, _resolvedFlowDirection); _indexedTextRuns = BidiReorderer.Instance.BidiReorder(_textRuns, _paragraphProperties.FlowDirection, FirstTextSourceIndex);
} }
/// <summary> /// <summary>
@ -1211,13 +1055,6 @@ namespace Avalonia.Media.TextFormatting
return true; return true;
} }
//var characterIndex = codepointIndex - shapedRun.Text.Start;
//if (characterIndex < 0 && shapedRun.ShapedBuffer.IsLeftToRight)
//{
// foundCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex);
//}
nextCharacterHit = isAtEnd || characterHit.TrailingLength != 0 ? nextCharacterHit = isAtEnd || characterHit.TrailingLength != 0 ?
foundCharacterHit : foundCharacterHit :
new CharacterHit(foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength); new CharacterHit(foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength);
@ -1556,8 +1393,8 @@ namespace Avalonia.Media.TextFormatting
TrailingWhitespaceLength = trailingWhitespaceLength, TrailingWhitespaceLength = trailingWhitespaceLength,
Width = width, Width = width,
WidthIncludingTrailingWhitespace = widthIncludingWhitespace, WidthIncludingTrailingWhitespace = widthIncludingWhitespace,
OverhangLeading= overhangLeading, OverhangLeading = overhangLeading,
OverhangTrailing= overhangTrailing, OverhangTrailing = overhangTrailing,
OverhangAfter = overhangAfter OverhangAfter = overhangAfter
}; };
} }
@ -1615,8 +1452,7 @@ namespace Avalonia.Media.TextFormatting
return Math.Max(0, start); return Math.Max(0, start);
case TextAlignment.Right: case TextAlignment.Right:
return Math.Max(0, _paragraphWidth - width); return Math.Max(0, _paragraphWidth - widthIncludingTrailingWhitespace);
default: default:
return 0; return 0;
} }

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

@ -1071,6 +1071,55 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
} }
} }
[Fact]
public void Should_GetTextBounds_BiDi()
{
var text = "אבגדה 12345 ABCDEF אבגדה";
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));
var bounds = textLine.GetTextBounds(6, 1);
Assert.Equal(1, bounds.Count);
Assert.Equal(0, bounds[0].Rectangle.Left);
bounds = textLine.GetTextBounds(5, 1);
Assert.Equal(1, bounds.Count);
Assert.Equal(36.005859374999993, bounds[0].Rectangle.Left);
bounds = textLine.GetTextBounds(0, 1);
Assert.Equal(1, bounds.Count);
Assert.Equal(71.165859375, bounds[0].Rectangle.Right);
bounds = textLine.GetTextBounds(11, 1);
Assert.Equal(1, bounds.Count);
Assert.Equal(71.165859375, bounds[0].Rectangle.Left);
bounds = textLine.GetTextBounds(0, 25);
Assert.Equal(5, bounds.Count);
Assert.Equal(textLine.WidthIncludingTrailingWhitespace, bounds.Last().Rectangle.Right);
}
}
private class FixedRunsTextSource : ITextSource private class FixedRunsTextSource : ITextSource
{ {
private readonly IReadOnlyList<TextRun> _textRuns; private readonly IReadOnlyList<TextRun> _textRuns;

Loading…
Cancel
Save