Browse Source

Merge pull request #11532 from Gillibald/textAlignmentFixes

[Text] Fixes
pull/11559/head
Max Katz 3 years ago
committed by GitHub
parent
commit
23d96c7366
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  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="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>
[DebuggerStepThrough]
public CharacterHit(int firstCharacterIndex, int trailingLength = 0)
{
FirstCharacterIndex = firstCharacterIndex;

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

@ -18,14 +18,14 @@ namespace Avalonia.Media.TextFormatting
public static BidiReorderer Instance
=> 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(_ranges.Length == 0);
if (textRuns.IsEmpty)
{
return;
return Array.Empty<IndexedTextRun>();
}
try
@ -46,6 +46,22 @@ namespace Avalonia.Media.TextFormatting
// Reorder them into visual order.
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.
// 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))
{
// Nothing to reverse.
return;
return indexedTextRuns;
}
// Now apply the reversal and replace the original contents.
@ -107,13 +123,25 @@ namespace Avalonia.Media.TextFormatting
var index = 0;
currentIndex = firstIndex;
while (currentIndex >= 0)
{
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;
}
return indexedTextRuns;
}
finally
{
@ -227,25 +255,6 @@ namespace Avalonia.Media.TextFormatting
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
{
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
}
}
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.Diagnostics;
namespace Avalonia.Media.TextFormatting
{
@ -10,6 +11,7 @@ namespace Avalonia.Media.TextFormatting
/// <summary>
/// Constructing TextBounds object
/// </summary>
[DebuggerStepThrough]
internal TextBounds(Rect bounds, FlowDirection flowDirection, IList<TextRunBounds> runBounds)
{
Rectangle = bounds;

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

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

Loading…
Cancel
Save