diff --git a/src/Avalonia.Base/Media/CharacterHit.cs b/src/Avalonia.Base/Media/CharacterHit.cs index 6bbbff4f5b..27cf3a42dc 100644 --- a/src/Avalonia.Base/Media/CharacterHit.cs +++ b/src/Avalonia.Base/Media/CharacterHit.cs @@ -19,6 +19,7 @@ namespace Avalonia.Media /// Index of the first character that got hit. /// 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. + [DebuggerStepThrough] public CharacterHit(int firstCharacterIndex, int trailingLength = 0) { FirstCharacterIndex = firstCharacterIndex; diff --git a/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs b/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs index 4db55fae6d..39ef8cce48 100644 --- a/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs +++ b/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 textRuns, FlowDirection flowDirection) + public IndexedTextRun[] BidiReorder(Span textRuns, FlowDirection flowDirection, int firstTextSourceIndex) { Debug.Assert(_runs.Length == 0); Debug.Assert(_ranges.Length == 0); if (textRuns.IsEmpty) { - return; + return Array.Empty(); } 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 + } } diff --git a/src/Avalonia.Base/Media/TextFormatting/IndexedTextRun.cs b/src/Avalonia.Base/Media/TextFormatting/IndexedTextRun.cs new file mode 100644 index 0000000000..0eb98533d2 --- /dev/null +++ b/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; } + } +} diff --git a/src/Avalonia.Base/Media/TextFormatting/TextBounds.cs b/src/Avalonia.Base/Media/TextFormatting/TextBounds.cs index 93edf68348..946c2e6931 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextBounds.cs +++ b/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 /// /// Constructing TextBounds object /// + [DebuggerStepThrough] internal TextBounds(Rect bounds, FlowDirection flowDirection, IList runBounds) { Rectangle = bounds; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index a0d7cabefd..c2ec78e187 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/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 TextBoundsComparer { get; } = + Comparer.Create((x, y) => x.Rectangle.Left.CompareTo(y.Rectangle.Left)); + + private IReadOnlyList? _indexedTextRuns; private readonly TextRun[] _textRuns; private readonly double _paragraphWidth; private readonly TextParagraphProperties _paragraphProperties; @@ -338,184 +342,169 @@ namespace Avalonia.Media.TextFormatting /// 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; } /// @@ -585,7 +574,7 @@ namespace Avalonia.Media.TextFormatting public override IReadOnlyList GetTextBounds(int firstTextSourceIndex, int textLength) { - if (_textRuns.Length == 0) + if (_indexedTextRuns is null || _indexedTextRuns.Count == 0) { return Array.Empty(); } @@ -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); } /// @@ -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; } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index aa5d707d0f..1d07e780e6 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/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 _textRuns;