|
|
|
@ -183,8 +183,47 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
var currentPosition = FirstTextSourceIndex; |
|
|
|
var currentDistance = 0.0; |
|
|
|
|
|
|
|
foreach (var currentRun in _textRuns) |
|
|
|
for (var i = 0; i < _textRuns.Count; i++) |
|
|
|
{ |
|
|
|
var currentRun = _textRuns[i]; |
|
|
|
|
|
|
|
if(currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight) |
|
|
|
{ |
|
|
|
var rightToLeftIndex = i; |
|
|
|
var rightToLeftDistance = shapedRun.Size.Width; |
|
|
|
|
|
|
|
while (rightToLeftIndex + 1 <= _textRuns.Count - 1) |
|
|
|
{ |
|
|
|
var nextShaped = _textRuns[rightToLeftIndex + 1] as ShapedTextCharacters; |
|
|
|
|
|
|
|
if (nextShaped == null || nextShaped.ShapedBuffer.IsLeftToRight) |
|
|
|
{ |
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
rightToLeftIndex++; |
|
|
|
|
|
|
|
rightToLeftDistance += nextShaped.Size.Width; |
|
|
|
} |
|
|
|
|
|
|
|
for (var j = rightToLeftIndex; rightToLeftIndex >= 0; j--) |
|
|
|
{ |
|
|
|
currentRun = _textRuns[j]; |
|
|
|
|
|
|
|
if(distance <= currentDistance + rightToLeftDistance - currentRun.Size.Width) |
|
|
|
{ |
|
|
|
currentPosition += currentRun.TextSourceLength; |
|
|
|
rightToLeftDistance -= currentRun.Size.Width; |
|
|
|
|
|
|
|
continue; |
|
|
|
} |
|
|
|
|
|
|
|
characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance); |
|
|
|
|
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (currentDistance + currentRun.Size.Width < distance) |
|
|
|
{ |
|
|
|
currentDistance += currentRun.Size.Width; |
|
|
|
@ -255,6 +294,37 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
{ |
|
|
|
var currentRun = _textRuns[index]; |
|
|
|
|
|
|
|
if (currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight) |
|
|
|
{ |
|
|
|
var i = index; |
|
|
|
|
|
|
|
while (i + 1 <= _textRuns.Count - 1) |
|
|
|
{ |
|
|
|
var nextRun = _textRuns[i + 1]; |
|
|
|
|
|
|
|
if (nextRun is ShapedTextCharacters nextShapedRun) |
|
|
|
{ |
|
|
|
if (nextShapedRun.ShapedBuffer.IsLeftToRight) |
|
|
|
{ |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
i++; |
|
|
|
} |
|
|
|
|
|
|
|
while (i > index) |
|
|
|
{ |
|
|
|
var rightToLeftRun = _textRuns[i]; |
|
|
|
|
|
|
|
currentPosition += rightToLeftRun.TextSourceLength; |
|
|
|
|
|
|
|
remainingLength -= rightToLeftRun.TextSourceLength; |
|
|
|
|
|
|
|
i--; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength, |
|
|
|
flowDirection, out var distance, out _)) |
|
|
|
{ |
|
|
|
@ -442,92 +512,130 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
continue; |
|
|
|
} |
|
|
|
|
|
|
|
if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex) |
|
|
|
{ |
|
|
|
startX += currentRun.Size.Width; |
|
|
|
|
|
|
|
currentPosition += currentRun.TextSourceLength; |
|
|
|
|
|
|
|
continue; |
|
|
|
} |
|
|
|
|
|
|
|
var characterLength = 0; |
|
|
|
var endX = startX; |
|
|
|
var runWidth = 0.0; |
|
|
|
TextRunBounds? currentRunBounds = null; |
|
|
|
|
|
|
|
if (currentRun is ShapedTextCharacters currentShapedRun) |
|
|
|
var currentShapedRun = currentRun as ShapedTextCharacters; |
|
|
|
|
|
|
|
if (currentShapedRun != null && !currentShapedRun.ShapedBuffer.IsLeftToRight) |
|
|
|
{ |
|
|
|
var offset = Math.Max(0, firstTextSourceIndex - currentPosition); |
|
|
|
var rightToLeftIndex = index; |
|
|
|
startX += currentShapedRun.Size.Width; |
|
|
|
|
|
|
|
currentPosition += offset; |
|
|
|
while (rightToLeftIndex + 1 <= _textRuns.Count - 1) |
|
|
|
{ |
|
|
|
var nextShapedRun = _textRuns[rightToLeftIndex + 1] as ShapedTextCharacters; |
|
|
|
|
|
|
|
var startIndex = currentRun.Text.Start + offset; |
|
|
|
if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight) |
|
|
|
{ |
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
double startOffset; |
|
|
|
double endOffset; |
|
|
|
startX += nextShapedRun.Size.Width; |
|
|
|
|
|
|
|
if (currentShapedRun.ShapedBuffer.IsLeftToRight) |
|
|
|
rightToLeftIndex++; |
|
|
|
} |
|
|
|
|
|
|
|
if (TryGetTextRunBoundsRightToLeft(startX, firstTextSourceIndex, characterIndex, rightToLeftIndex, ref currentPosition, ref remainingLength, out currentRunBounds)) |
|
|
|
{ |
|
|
|
startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); |
|
|
|
startX = currentRunBounds!.Rectangle.Left; |
|
|
|
endX = currentRunBounds.Rectangle.Right; |
|
|
|
|
|
|
|
endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); |
|
|
|
runWidth = currentRunBounds.Rectangle.Width; |
|
|
|
} |
|
|
|
else |
|
|
|
|
|
|
|
currentDirection = FlowDirection.RightToLeft; |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
if (currentShapedRun != null) |
|
|
|
{ |
|
|
|
endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); |
|
|
|
if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex) |
|
|
|
{ |
|
|
|
startX += currentRun.Size.Width; |
|
|
|
|
|
|
|
if (currentPosition < startIndex) |
|
|
|
currentPosition += currentRun.TextSourceLength; |
|
|
|
|
|
|
|
continue; |
|
|
|
} |
|
|
|
|
|
|
|
var offset = Math.Max(0, firstTextSourceIndex - currentPosition); |
|
|
|
|
|
|
|
currentPosition += offset; |
|
|
|
|
|
|
|
var startIndex = currentRun.Text.Start + offset; |
|
|
|
|
|
|
|
double startOffset; |
|
|
|
double endOffset; |
|
|
|
|
|
|
|
if (currentShapedRun.ShapedBuffer.IsLeftToRight) |
|
|
|
{ |
|
|
|
startOffset = endOffset; |
|
|
|
startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); |
|
|
|
|
|
|
|
endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); |
|
|
|
endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); |
|
|
|
|
|
|
|
if (currentPosition < startIndex) |
|
|
|
{ |
|
|
|
startOffset = endOffset; |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
startX += startOffset; |
|
|
|
startX += startOffset; |
|
|
|
|
|
|
|
endX += endOffset; |
|
|
|
endX += endOffset; |
|
|
|
|
|
|
|
var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); |
|
|
|
var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); |
|
|
|
var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); |
|
|
|
var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); |
|
|
|
|
|
|
|
characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength); |
|
|
|
characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength); |
|
|
|
|
|
|
|
currentDirection = currentShapedRun.ShapedBuffer.IsLeftToRight ? |
|
|
|
FlowDirection.LeftToRight : |
|
|
|
FlowDirection.RightToLeft; |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
if (currentPosition < firstTextSourceIndex) |
|
|
|
currentDirection = FlowDirection.LeftToRight; |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
startX += currentRun.Size.Width; |
|
|
|
if (currentPosition < firstTextSourceIndex) |
|
|
|
{ |
|
|
|
startX += currentRun.Size.Width; |
|
|
|
} |
|
|
|
|
|
|
|
if (currentPosition + currentRun.TextSourceLength <= characterIndex) |
|
|
|
{ |
|
|
|
endX += currentRun.Size.Width; |
|
|
|
|
|
|
|
characterLength = currentRun.TextSourceLength; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (currentPosition + currentRun.TextSourceLength <= characterIndex) |
|
|
|
if (endX < startX) |
|
|
|
{ |
|
|
|
endX += currentRun.Size.Width; |
|
|
|
(endX, startX) = (startX, endX); |
|
|
|
} |
|
|
|
|
|
|
|
characterLength = currentRun.TextSourceLength; |
|
|
|
//Lines that only contain a linebreak need to be covered here
|
|
|
|
if (characterLength == 0) |
|
|
|
{ |
|
|
|
characterLength = NewLineLength; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (endX < startX) |
|
|
|
{ |
|
|
|
(endX, startX) = (startX, endX); |
|
|
|
} |
|
|
|
runWidth = endX - startX; |
|
|
|
currentRunBounds = new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); |
|
|
|
|
|
|
|
//Lines that only contain a linebreak need to be covered here
|
|
|
|
if (characterLength == 0) |
|
|
|
{ |
|
|
|
characterLength = NewLineLength; |
|
|
|
} |
|
|
|
currentPosition += characterLength; |
|
|
|
|
|
|
|
var runWidth = endX - startX; |
|
|
|
var currentRunBounds = new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); |
|
|
|
remainingLength -= characterLength; |
|
|
|
} |
|
|
|
|
|
|
|
if (!MathUtilities.IsZero(runWidth) || NewLineLength > 0) |
|
|
|
if (currentRunBounds != null && !MathUtilities.IsZero(runWidth) || NewLineLength > 0) |
|
|
|
{ |
|
|
|
if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX)) |
|
|
|
{ |
|
|
|
@ -537,32 +645,26 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
|
|
|
|
textBounds.Rectangle = currentRect; |
|
|
|
|
|
|
|
textBounds.TextRunBounds.Add(currentRunBounds); |
|
|
|
textBounds.TextRunBounds.Add(currentRunBounds!); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
currentRect = currentRunBounds.Rectangle; |
|
|
|
currentRect = currentRunBounds!.Rectangle; |
|
|
|
|
|
|
|
result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds })); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
currentWidth += runWidth; |
|
|
|
currentPosition += characterLength; |
|
|
|
|
|
|
|
|
|
|
|
if (currentPosition > characterIndex) |
|
|
|
if (remainingLength <= 0 || currentPosition >= characterIndex) |
|
|
|
{ |
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
startX = endX; |
|
|
|
lastDirection = currentDirection; |
|
|
|
remainingLength -= characterLength; |
|
|
|
|
|
|
|
if (remainingLength <= 0) |
|
|
|
{ |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return result; |
|
|
|
@ -674,7 +776,7 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
|
|
|
|
var currentRunBounds = new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); |
|
|
|
|
|
|
|
if(!MathUtilities.IsZero(runWidth) || NewLineLength > 0) |
|
|
|
if (!MathUtilities.IsZero(runWidth) || NewLineLength > 0) |
|
|
|
{ |
|
|
|
if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, Start + startX)) |
|
|
|
{ |
|
|
|
@ -692,7 +794,7 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
|
|
|
|
result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds })); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
currentWidth += runWidth; |
|
|
|
currentPosition += characterLength; |
|
|
|
@ -716,6 +818,107 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
return result; |
|
|
|
} |
|
|
|
|
|
|
|
private bool TryGetTextRunBoundsRightToLeft(double startX, int firstTextSourceIndex, int characterIndex, int runIndex, ref int currentPosition, ref int remainingLength, out TextRunBounds? textRunBounds) |
|
|
|
{ |
|
|
|
textRunBounds = null; |
|
|
|
|
|
|
|
for (var index = runIndex; index >= 0; index--) |
|
|
|
{ |
|
|
|
if (TextRuns[index] is not DrawableTextRun currentRun) |
|
|
|
{ |
|
|
|
continue; |
|
|
|
} |
|
|
|
|
|
|
|
if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex) |
|
|
|
{ |
|
|
|
startX -= currentRun.Size.Width; |
|
|
|
|
|
|
|
currentPosition += currentRun.TextSourceLength; |
|
|
|
|
|
|
|
continue; |
|
|
|
} |
|
|
|
|
|
|
|
var characterLength = 0; |
|
|
|
var endX = startX; |
|
|
|
|
|
|
|
if (currentRun is ShapedTextCharacters currentShapedRun) |
|
|
|
{ |
|
|
|
var offset = Math.Max(0, firstTextSourceIndex - currentPosition); |
|
|
|
|
|
|
|
currentPosition += offset; |
|
|
|
|
|
|
|
var startIndex = currentRun.Text.Start + offset; |
|
|
|
double startOffset; |
|
|
|
double endOffset; |
|
|
|
|
|
|
|
if (currentShapedRun.ShapedBuffer.IsLeftToRight) |
|
|
|
{ |
|
|
|
if (currentPosition < startIndex) |
|
|
|
{ |
|
|
|
startOffset = endOffset = 0; |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); |
|
|
|
|
|
|
|
startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); |
|
|
|
} |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); |
|
|
|
|
|
|
|
startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); |
|
|
|
} |
|
|
|
|
|
|
|
startX -= currentRun.Size.Width - startOffset; |
|
|
|
endX -= currentRun.Size.Width - endOffset; |
|
|
|
|
|
|
|
var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); |
|
|
|
var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); |
|
|
|
|
|
|
|
characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
if (currentPosition + currentRun.TextSourceLength <= characterIndex) |
|
|
|
{ |
|
|
|
endX -= currentRun.Size.Width; |
|
|
|
} |
|
|
|
|
|
|
|
if (currentPosition < firstTextSourceIndex) |
|
|
|
{ |
|
|
|
startX -= currentRun.Size.Width; |
|
|
|
|
|
|
|
characterLength = currentRun.TextSourceLength; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (endX < startX) |
|
|
|
{ |
|
|
|
(endX, startX) = (startX, endX); |
|
|
|
} |
|
|
|
|
|
|
|
//Lines that only contain a linebreak need to be covered here
|
|
|
|
if (characterLength == 0) |
|
|
|
{ |
|
|
|
characterLength = NewLineLength; |
|
|
|
} |
|
|
|
|
|
|
|
var runWidth = endX - startX; |
|
|
|
|
|
|
|
remainingLength -= characterLength; |
|
|
|
|
|
|
|
currentPosition += characterLength; |
|
|
|
|
|
|
|
textRunBounds = new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); |
|
|
|
|
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
public override IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceIndex, int textLength) |
|
|
|
{ |
|
|
|
if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight) |
|
|
|
@ -1319,12 +1522,12 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
case TextAlignment.Center: |
|
|
|
var start = (_paragraphWidth - width) / 2; |
|
|
|
|
|
|
|
if(paragraphFlowDirection == FlowDirection.RightToLeft) |
|
|
|
if (paragraphFlowDirection == FlowDirection.RightToLeft) |
|
|
|
{ |
|
|
|
start -= (widthIncludingTrailingWhitespace - width); |
|
|
|
} |
|
|
|
|
|
|
|
return Math.Max(0, start); |
|
|
|
return Math.Max(0, start); |
|
|
|
case TextAlignment.Right: |
|
|
|
return Math.Max(0, _paragraphWidth - widthIncludingTrailingWhitespace); |
|
|
|
|
|
|
|
|