|
|
@@ -10,6 +10,7 @@ namespace Avalonia.Media.TextFormatting
|
|
|
private readonly double _paragraphWidth;
|
|
|
private readonly TextParagraphProperties _paragraphProperties;
|
|
|
private TextLineMetrics _textLineMetrics;
|
|
|
+ private TextLineBreak? _textLineBreak;
|
|
|
private readonly FlowDirection _resolvedFlowDirection;
|
|
|
|
|
|
public TextLineImpl(TextRun[] textRuns, int firstTextSourceIndex, int length, double paragraphWidth,
|
|
|
@@ -18,7 +19,7 @@ namespace Avalonia.Media.TextFormatting
|
|
|
{
|
|
|
FirstTextSourceIndex = firstTextSourceIndex;
|
|
|
Length = length;
|
|
|
- TextLineBreak = lineBreak;
|
|
|
+ _textLineBreak = lineBreak;
|
|
|
HasCollapsed = hasCollapsed;
|
|
|
|
|
|
_textRuns = textRuns;
|
|
|
@@ -38,7 +39,7 @@ namespace Avalonia.Media.TextFormatting
|
|
|
public override int Length { get; }
|
|
|
|
|
|
/// <inheritdoc/>
|
|
|
- public override TextLineBreak? TextLineBreak { get; }
|
|
|
+ public override TextLineBreak? TextLineBreak => _textLineBreak;
|
|
|
|
|
|
/// <inheritdoc/>
|
|
|
public override bool HasCollapsed { get; }
|
|
|
@@ -167,38 +168,54 @@ namespace Avalonia.Media.TextFormatting
|
|
|
{
|
|
|
if (_textRuns.Length == 0)
|
|
|
{
|
|
|
- return new CharacterHit();
|
|
|
+ return new CharacterHit(FirstTextSourceIndex);
|
|
|
}
|
|
|
|
|
|
distance -= Start;
|
|
|
|
|
|
+ var lastIndex = _textRuns.Length - 1;
|
|
|
+
|
|
|
+ if (_textRuns[lastIndex] is TextEndOfLine)
|
|
|
+ {
|
|
|
+ lastIndex--;
|
|
|
+ }
|
|
|
+
|
|
|
+ var currentPosition = FirstTextSourceIndex;
|
|
|
+
|
|
|
+ if (lastIndex < 0)
|
|
|
+ {
|
|
|
+ return new CharacterHit(currentPosition);
|
|
|
+ }
|
|
|
+
|
|
|
if (distance <= 0)
|
|
|
{
|
|
|
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)
|
|
|
{
|
|
|
- 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
|
|
|
var characterHit = new CharacterHit();
|
|
|
- var currentPosition = FirstTextSourceIndex;
|
|
|
var currentDistance = 0.0;
|
|
|
|
|
|
- for (var i = 0; i < _textRuns.Length; i++)
|
|
|
+ for (var i = 0; i <= lastIndex; i++)
|
|
|
{
|
|
|
var currentRun = _textRuns[i];
|
|
|
|
|
|
@@ -230,7 +247,7 @@ namespace Avalonia.Media.TextFormatting
|
|
|
|
|
|
currentRun = _textRuns[j];
|
|
|
|
|
|
- if(currentRun is not ShapedTextRun)
|
|
|
+ if (currentRun is not ShapedTextRun)
|
|
|
{
|
|
|
continue;
|
|
|
}
|
|
|
@@ -262,10 +279,6 @@ namespace Avalonia.Media.TextFormatting
|
|
|
continue;
|
|
|
}
|
|
|
}
|
|
|
- else
|
|
|
- {
|
|
|
- continue;
|
|
|
- }
|
|
|
|
|
|
break;
|
|
|
}
|
|
|
@@ -410,10 +423,10 @@ namespace Avalonia.Media.TextFormatting
|
|
|
{
|
|
|
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)
|
|
|
@@ -563,386 +576,505 @@ namespace Avalonia.Media.TextFormatting
|
|
|
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 lastDirection = FlowDirection.LeftToRight;
|
|
|
- var currentDirection = lastDirection;
|
|
|
+ var result = new List<TextBounds>();
|
|
|
|
|
|
var currentPosition = FirstTextSourceIndex;
|
|
|
var remainingLength = textLength;
|
|
|
|
|
|
- var startX = Start;
|
|
|
- double currentWidth = 0;
|
|
|
- var currentRect = default(Rect);
|
|
|
-
|
|
|
- TextRunBounds lastRunBounds = default;
|
|
|
-
|
|
|
- for (var index = 0; index < _textRuns.Length; index++)
|
|
|
+ static FlowDirection GetDirection(TextRun textRun, FlowDirection currentDirection)
|
|
|
{
|
|
|
- if (_textRuns[index] is not DrawableTextRun currentRun)
|
|
|
+ if (textRun is ShapedTextRun shapedTextRun)
|
|
|
{
|
|
|
- continue;
|
|
|
+ return shapedTextRun.ShapedBuffer.IsLeftToRight ?
|
|
|
+ FlowDirection.LeftToRight :
|
|
|
+ FlowDirection.RightToLeft;
|
|
|
}
|
|
|
|
|
|
- var characterLength = 0;
|
|
|
- var endX = startX;
|
|
|
-
|
|
|
- TextRunBounds currentRunBounds;
|
|
|
+ return currentDirection;
|
|
|
+ }
|
|
|
|
|
|
- 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];
|
|
|
|
|
|
- if (currentPosition + currentRun.Length <= firstTextSourceIndex)
|
|
|
+ var firstRunIndex = i;
|
|
|
+ var lastRunIndex = firstRunIndex;
|
|
|
+ var currentDirection = GetDirection(currentRun, FlowDirection.LeftToRight);
|
|
|
+ var directionalWidth = 0.0;
|
|
|
+
|
|
|
+ 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];
|
|
|
|
|
|
- double startOffset;
|
|
|
+ if (currentPosition + currentRun.Length > firstTextSourceIndex)
|
|
|
+ {
|
|
|
+ break;
|
|
|
+ }
|
|
|
|
|
|
- double endOffset;
|
|
|
+ currentPosition += currentRun.Length;
|
|
|
|
|
|
- startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
|
|
|
+ if (currentRun is DrawableTextRun drawableTextRun)
|
|
|
+ {
|
|
|
+ directionalWidth -= drawableTextRun.Size.Width;
|
|
|
+ currentX += drawableTextRun.Size.Width;
|
|
|
+ }
|
|
|
|
|
|
- endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
|
|
|
+ if(lastRunIndex - 1 < 0)
|
|
|
+ {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- startX += startOffset;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ default:
|
|
|
+ {
|
|
|
+ for (; firstRunIndex <= lastRunIndex; firstRunIndex++)
|
|
|
+ {
|
|
|
+ currentRun = _textRuns[firstRunIndex];
|
|
|
|
|
|
- endX += endOffset;
|
|
|
+ if (currentPosition + currentRun.Length > firstTextSourceIndex)
|
|
|
+ {
|
|
|
+ break;
|
|
|
+ }
|
|
|
|
|
|
- var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
|
|
|
+ currentPosition += currentRun.Length;
|
|
|
|
|
|
- var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
|
|
|
+ if (currentRun is DrawableTextRun drawableTextRun)
|
|
|
+ {
|
|
|
+ currentX += drawableTextRun.Size.Width;
|
|
|
+ directionalWidth -= drawableTextRun.Size.Width;
|
|
|
+ }
|
|
|
|
|
|
- characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength);
|
|
|
+ if(firstRunIndex + 1 == _textRuns.Length)
|
|
|
+ {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- currentDirection = FlowDirection.LeftToRight;
|
|
|
+ break;
|
|
|
+ }
|
|
|
}
|
|
|
- else
|
|
|
+
|
|
|
+ i = lastRunIndex;
|
|
|
+
|
|
|
+ if (directionalWidth == 0)
|
|
|
{
|
|
|
- var rightToLeftIndex = index;
|
|
|
- var rightToLeftWidth = currentShapedRun.Size.Width;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
|
|
|
- while (rightToLeftIndex + 1 <= _textRuns.Length - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextRun nextShapedRun)
|
|
|
- {
|
|
|
- if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight)
|
|
|
+ var coveredLength = 0;
|
|
|
+ TextBounds? textBounds = null;
|
|
|
+
|
|
|
+ switch (currentDirection)
|
|
|
+ {
|
|
|
+
|
|
|
+ case FlowDirection.RightToLeft:
|
|
|
{
|
|
|
+ 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);
|
|
|
|
|
|
- rightToLeftIndex++;
|
|
|
-
|
|
|
- rightToLeftWidth += nextShapedRun.Size.Width;
|
|
|
+ currentX = textBounds.Rectangle.Right;
|
|
|
|
|
|
- if (currentPosition + nextShapedRun.Length > firstTextSourceIndex + textLength)
|
|
|
- {
|
|
|
break;
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- currentShapedRun = nextShapedRun;
|
|
|
- }
|
|
|
+ if (coveredLength > 0)
|
|
|
+ {
|
|
|
+ result.Add(textBounds);
|
|
|
|
|
|
- startX += rightToLeftWidth;
|
|
|
+ remainingLength -= coveredLength;
|
|
|
+ }
|
|
|
|
|
|
- currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
|
|
|
+ if (remainingLength <= 0)
|
|
|
+ {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ var currentX = Start + WidthIncludingTrailingWhitespace;
|
|
|
|
|
|
- remainingLength -= currentRunBounds.Length;
|
|
|
- currentPosition = currentRunBounds.TextSourceCharacterIndex + currentRunBounds.Length;
|
|
|
- endX = currentRunBounds.Rectangle.Right;
|
|
|
- startX = currentRunBounds.Rectangle.Left;
|
|
|
+ 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;
|
|
|
|
|
|
- var rightToLeftRunBounds = new List<TextRunBounds> { currentRunBounds };
|
|
|
+ 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];
|
|
|
+
|
|
|
+ var previousDirection = GetDirection(previousRun, currentDirection);
|
|
|
+
|
|
|
+ if (currentDirection != previousDirection)
|
|
|
+ {
|
|
|
+ break;
|
|
|
+ }
|
|
|
|
|
|
- for (int i = rightToLeftIndex - 1; i >= index; i--)
|
|
|
+ if (currentRun is DrawableTextRun previousDrawable)
|
|
|
{
|
|
|
- if (_textRuns[i] is not ShapedTextRun shapedRun)
|
|
|
+ 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;
|
|
|
- startX = currentRunBounds.Rectangle.Left;
|
|
|
+ break;
|
|
|
+ }
|
|
|
|
|
|
- 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))
|
|
|
- {
|
|
|
- result.Add(new TextBounds(currentRect, currentDirection, rightToLeftRunBounds));
|
|
|
- }
|
|
|
+ break;
|
|
|
+ }
|
|
|
|
|
|
- startX = endX;
|
|
|
+ break;
|
|
|
+ }
|
|
|
}
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- if (currentPosition + currentRun.Length <= firstTextSourceIndex)
|
|
|
- {
|
|
|
- startX += currentRun.Size.Width;
|
|
|
|
|
|
- currentPosition += currentRun.Length;
|
|
|
+ i = firstRunIndex;
|
|
|
|
|
|
+ if (directionalWidth == 0)
|
|
|
+ {
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
- if (currentPosition < firstTextSourceIndex)
|
|
|
- {
|
|
|
- startX += currentRun.Size.Width;
|
|
|
- }
|
|
|
+ var coveredLength = 0;
|
|
|
+
|
|
|
+ TextBounds? textBounds = null;
|
|
|
|
|
|
- if (currentPosition + currentRun.Length <= characterIndex)
|
|
|
+ switch (currentDirection)
|
|
|
{
|
|
|
- endX += currentRun.Size.Width;
|
|
|
+ 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);
|
|
|
|
|
|
- characterLength = currentRun.Length;
|
|
|
+ currentX = textBounds.Rectangle.Left;
|
|
|
+
|
|
|
+ break;
|
|
|
+ }
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- if (endX < startX)
|
|
|
- {
|
|
|
- (endX, startX) = (startX, endX);
|
|
|
- }
|
|
|
+ //Visual order is always left to right so we need to insert
|
|
|
+ result.Insert(0, textBounds);
|
|
|
|
|
|
- //Lines that only contain a linebreak need to be covered here
|
|
|
- if (characterLength == 0)
|
|
|
- {
|
|
|
- characterLength = NewLineLength;
|
|
|
+ remainingLength -= coveredLength;
|
|
|
+
|
|
|
+ if (remainingLength <= 0)
|
|
|
+ {
|
|
|
+ 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 (result.Count > 0 && lastDirection == currentDirection && MathUtilities.AreClose(currentRect.Left, lastRunBounds.Rectangle.Right))
|
|
|
+ if (offset > 0)
|
|
|
{
|
|
|
- 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);
|
|
|
- }
|
|
|
- else
|
|
|
+ currentPosition += runBounds.Length + offset;
|
|
|
+
|
|
|
+ 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;
|
|
|
|
|
|
- currentWidth += combinedWidth;
|
|
|
+ coveredLength += currentRun.Length;
|
|
|
|
|
|
- if (remainingLength <= 0 || currentPosition >= characterIndex)
|
|
|
+ remainingLength -= currentRun.Length;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (remainingLength <= 0)
|
|
|
{
|
|
|
break;
|
|
|
}
|
|
|
-
|
|
|
- lastDirection = currentDirection;
|
|
|
}
|
|
|
|
|
|
- return result;
|
|
|
- }
|
|
|
+ newPosition = currentPosition;
|
|
|
|
|
|
- private IReadOnlyList<TextBounds> GetTextBoundsRightToLeft(int firstTextSourceIndex, int textLength)
|
|
|
- {
|
|
|
- var characterIndex = firstTextSourceIndex + textLength;
|
|
|
+ var runWidth = endX - startX;
|
|
|
|
|
|
- var result = new List<TextBounds>(_textRuns.Length);
|
|
|
- var lastDirection = FlowDirection.LeftToRight;
|
|
|
- var currentDirection = lastDirection;
|
|
|
+ var bounds = new Rect(startX, 0, runWidth, Height);
|
|
|
|
|
|
- var currentPosition = FirstTextSourceIndex;
|
|
|
- var remainingLength = textLength;
|
|
|
+ return new TextBounds(bounds, FlowDirection.RightToLeft, textRunBounds);
|
|
|
+ }
|
|
|
|
|
|
- var startX = WidthIncludingTrailingWhitespace;
|
|
|
- double currentWidth = 0;
|
|
|
- var currentRect = default(Rect);
|
|
|
+ private TextBounds GetTextBoundsLeftToRight(int firstRunIndex, int lastRunIndex, double startX,
|
|
|
+ int firstTextSourceIndex, int currentPosition, int remainingLength, out int coveredLength, out int newPosition)
|
|
|
+ {
|
|
|
+ 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)
|
|
|
- {
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- if (currentPosition + currentRun.Length < firstTextSourceIndex)
|
|
|
- {
|
|
|
- startX -= currentRun.Size.Width;
|
|
|
-
|
|
|
- currentPosition += currentRun.Length;
|
|
|
-
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- var characterLength = 0;
|
|
|
- var endX = startX;
|
|
|
+ var currentRun = _textRuns[i];
|
|
|
|
|
|
- if (currentRun is ShapedTextRun currentShapedRun)
|
|
|
+ if (currentRun is ShapedTextRun shapedTextRun)
|
|
|
{
|
|
|
- var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
|
|
|
-
|
|
|
- currentPosition += offset;
|
|
|
+ var runBounds = GetRunBoundsLeftToRight(shapedTextRun, endX, firstTextSourceIndex, remainingLength, currentPosition, out var offset);
|
|
|
|
|
|
- var startIndex = currentPosition;
|
|
|
- double startOffset;
|
|
|
- double endOffset;
|
|
|
+ textRunBounds.Add(runBounds);
|
|
|
|
|
|
- if (currentShapedRun.ShapedBuffer.IsLeftToRight)
|
|
|
+ if (offset > 0)
|
|
|
{
|
|
|
- if (currentPosition < startIndex)
|
|
|
- {
|
|
|
- startOffset = endOffset = 0;
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
|
|
|
+ startX = runBounds.Rectangle.Left;
|
|
|
|
|
|
- startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
|
|
|
- }
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
|
|
|
-
|
|
|
- startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
|
|
|
+ endX = startX;
|
|
|
}
|
|
|
|
|
|
- startX -= currentRun.Size.Width - startOffset;
|
|
|
- endX -= currentRun.Size.Width - endOffset;
|
|
|
+ currentPosition += runBounds.Length + offset;
|
|
|
|
|
|
- var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
|
|
|
- var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
|
|
|
+ endX += runBounds.Rectangle.Width;
|
|
|
|
|
|
- characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength);
|
|
|
+ coveredLength += runBounds.Length;
|
|
|
|
|
|
- currentDirection = currentShapedRun.ShapedBuffer.IsLeftToRight ?
|
|
|
- FlowDirection.LeftToRight :
|
|
|
- FlowDirection.RightToLeft;
|
|
|
+ remainingLength -= runBounds.Length;
|
|
|
}
|
|
|
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)
|
|
|
- {
|
|
|
- startX -= currentRun.Size.Width;
|
|
|
+ currentPosition += currentRun.Length;
|
|
|
|
|
|
- characterLength = currentRun.Length;
|
|
|
- }
|
|
|
- }
|
|
|
+ coveredLength += currentRun.Length;
|
|
|
|
|
|
- if (endX < startX)
|
|
|
- {
|
|
|
- (endX, startX) = (startX, endX);
|
|
|
+ remainingLength -= currentRun.Length;
|
|
|
}
|
|
|
|
|
|
- //Lines that only contain a linebreak need to be covered here
|
|
|
- if (characterLength == 0)
|
|
|
+ if (remainingLength <= 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)
|
|
|
- {
|
|
|
- if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, Start + startX))
|
|
|
- {
|
|
|
- currentRect = currentRect.WithWidth(currentWidth + runWidth);
|
|
|
+ var bounds = new Rect(startX, 0, runWidth, Height);
|
|
|
|
|
|
- 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);
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- currentRect = currentRunBounds.Rectangle;
|
|
|
+ offset = Math.Max(0, firstTextSourceIndex - currentPosition);
|
|
|
|
|
|
- result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds }));
|
|
|
- }
|
|
|
- }
|
|
|
+ var firstCluster = currentRun.GlyphRun.Metrics.FirstCluster;
|
|
|
|
|
|
- currentWidth += runWidth;
|
|
|
- currentPosition += characterLength;
|
|
|
+ if (currentPosition != firstCluster)
|
|
|
+ {
|
|
|
+ startIndex = firstCluster + offset;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ startIndex += offset;
|
|
|
+ }
|
|
|
|
|
|
- if (currentPosition > characterIndex)
|
|
|
- {
|
|
|
- break;
|
|
|
- }
|
|
|
+ var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
|
|
|
+ var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
|
|
|
|
|
|
- lastDirection = currentDirection;
|
|
|
- remainingLength -= characterLength;
|
|
|
+ var endX = startX + endOffset;
|
|
|
+ startX += startOffset;
|
|
|
|
|
|
- if (remainingLength <= 0)
|
|
|
- {
|
|
|
- break;
|
|
|
- }
|
|
|
+ var startHit = currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
|
|
|
+ var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
|
|
|
+
|
|
|
+ var characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength);
|
|
|
+
|
|
|
+ if (endX < startX)
|
|
|
+ {
|
|
|
+ (endX, startX) = (startX, endX);
|
|
|
}
|
|
|
|
|
|
- result.Reverse();
|
|
|
+ //Lines that only contain a linebreak need to be covered here
|
|
|
+ if (characterLength == 0)
|
|
|
+ {
|
|
|
+ characterLength = NewLineLength;
|
|
|
+ }
|
|
|
|
|
|
- return result;
|
|
|
+ var runWidth = endX - startX;
|
|
|
+
|
|
|
+ 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 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;
|
|
|
- double endOffset;
|
|
|
+ if (currentPosition != firstCluster)
|
|
|
+ {
|
|
|
+ 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;
|
|
|
endX -= currentRun.Size.Width - endOffset;
|
|
|
@@ -968,16 +1100,6 @@ namespace Avalonia.Media.TextFormatting
|
|
|
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()
|
|
|
{
|
|
|
for (int i = 0; i < _textRuns.Length; i++)
|
|
|
@@ -993,6 +1115,11 @@ namespace Avalonia.Media.TextFormatting
|
|
|
{
|
|
|
_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);
|
|
|
}
|
|
|
|
|
|
@@ -1285,13 +1412,11 @@ namespace Avalonia.Media.TextFormatting
|
|
|
{
|
|
|
case ShapedTextRun textRun:
|
|
|
{
|
|
|
- var properties = textRun.Properties;
|
|
|
- var textMetrics =
|
|
|
- new TextMetrics(properties.CachedGlyphTypeface, properties.FontRenderingEmSize);
|
|
|
+ var textMetrics = textRun.TextMetrics;
|
|
|
|
|
|
- if (fontRenderingEmSize < properties.FontRenderingEmSize)
|
|
|
+ if (fontRenderingEmSize < textMetrics.FontRenderingEmSize)
|
|
|
{
|
|
|
- fontRenderingEmSize = properties.FontRenderingEmSize;
|
|
|
+ fontRenderingEmSize = textMetrics.FontRenderingEmSize;
|
|
|
|
|
|
if (ascent > textMetrics.Ascent)
|
|
|
{
|
|
|
@@ -1318,7 +1443,7 @@ namespace Avalonia.Media.TextFormatting
|
|
|
{
|
|
|
width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width;
|
|
|
trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength;
|
|
|
- newLineLength = textRun.GlyphRun.Metrics.NewLineLength;
|
|
|
+ newLineLength += textRun.GlyphRun.Metrics.NewLineLength;
|
|
|
}
|
|
|
|
|
|
widthIncludingWhitespace += textRun.Size.Width;
|
|
|
@@ -1330,31 +1455,10 @@ namespace Avalonia.Media.TextFormatting
|
|
|
{
|
|
|
widthIncludingWhitespace += drawableTextRun.Size.Width;
|
|
|
|
|
|
- switch (_paragraphProperties.FlowDirection)
|
|
|
+ if (index == lastRunIndex)
|
|
|
{
|
|
|
- case FlowDirection.LeftToRight:
|
|
|
- {
|
|
|
- if (index == lastRunIndex)
|
|
|
- {
|
|
|
- width = widthIncludingWhitespace;
|
|
|
- trailingWhitespaceLength = 0;
|
|
|
- newLineLength = 0;
|
|
|
- }
|
|
|
-
|
|
|
- break;
|
|
|
- }
|
|
|
-
|
|
|
- case FlowDirection.RightToLeft:
|
|
|
- {
|
|
|
- if (index == lastRunIndex)
|
|
|
- {
|
|
|
- width = widthIncludingWhitespace;
|
|
|
- trailingWhitespaceLength = 0;
|
|
|
- newLineLength = 0;
|
|
|
- }
|
|
|
-
|
|
|
- break;
|
|
|
- }
|
|
|
+ width = widthIncludingWhitespace;
|
|
|
+ trailingWhitespaceLength = 0;
|
|
|
}
|
|
|
|
|
|
if (drawableTextRun.Size.Height > height)
|