|
|
|
@ -602,101 +602,40 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
|
|
|
|
public override IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceIndex, int textLength) |
|
|
|
{ |
|
|
|
if (_indexedTextRuns is null || _indexedTextRuns.Count == 0) |
|
|
|
if(textLength == 0) |
|
|
|
{ |
|
|
|
return Array.Empty<TextBounds>(); |
|
|
|
throw new ArgumentOutOfRangeException(nameof(textLength), textLength, $"{nameof(textLength)} ('0') must be a non-zero value. "); |
|
|
|
} |
|
|
|
|
|
|
|
var result = new List<TextBounds>(); |
|
|
|
if (_indexedTextRuns is null || _indexedTextRuns.Count == 0) |
|
|
|
{ |
|
|
|
return []; |
|
|
|
} |
|
|
|
|
|
|
|
var currentPosition = FirstTextSourceIndex; |
|
|
|
var remainingLength = textLength; |
|
|
|
|
|
|
|
TextBounds? lastBounds = null; |
|
|
|
|
|
|
|
static FlowDirection GetDirection(TextRun textRun, FlowDirection currentDirection) |
|
|
|
//We can return early if the requested text range is before the line's text range.
|
|
|
|
if (firstTextSourceIndex + textLength < FirstTextSourceIndex) |
|
|
|
{ |
|
|
|
if (textRun is ShapedTextRun shapedTextRun) |
|
|
|
{ |
|
|
|
return shapedTextRun.ShapedBuffer.IsLeftToRight ? |
|
|
|
FlowDirection.LeftToRight : |
|
|
|
FlowDirection.RightToLeft; |
|
|
|
} |
|
|
|
var indexedTextRun = _indexedTextRuns[0]; |
|
|
|
var currentDirection = GetDirection(indexedTextRun.TextRun, _resolvedFlowDirection); |
|
|
|
|
|
|
|
return currentDirection; |
|
|
|
return [new TextBounds(new Rect(0,0,0, Height), currentDirection, [])]; |
|
|
|
} |
|
|
|
|
|
|
|
IndexedTextRun FindIndexedRun() |
|
|
|
//We can return early if the requested text range is after the line's text range.
|
|
|
|
if (firstTextSourceIndex >= FirstTextSourceIndex + Length) |
|
|
|
{ |
|
|
|
var i = 0; |
|
|
|
var indexedTextRun = _indexedTextRuns[_indexedTextRuns.Count - 1]; |
|
|
|
var currentDirection = GetDirection(indexedTextRun.TextRun, _resolvedFlowDirection); |
|
|
|
|
|
|
|
IndexedTextRun currentIndexedRun = _indexedTextRuns[i]; |
|
|
|
|
|
|
|
while (currentIndexedRun.TextSourceCharacterIndex != currentPosition) |
|
|
|
{ |
|
|
|
if (i + 1 == _indexedTextRuns.Count) |
|
|
|
{ |
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
i++; |
|
|
|
|
|
|
|
currentIndexedRun = _indexedTextRuns[i]; |
|
|
|
} |
|
|
|
|
|
|
|
return currentIndexedRun; |
|
|
|
} |
|
|
|
|
|
|
|
double GetPreceedingDistance(int firstIndex) |
|
|
|
{ |
|
|
|
var distance = 0.0; |
|
|
|
|
|
|
|
for (var i = 0; i < firstIndex; i++) |
|
|
|
{ |
|
|
|
var currentRun = _textRuns[i]; |
|
|
|
|
|
|
|
if (currentRun is DrawableTextRun drawableTextRun) |
|
|
|
{ |
|
|
|
distance += drawableTextRun.Size.Width; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return distance; |
|
|
|
return [new TextBounds(new Rect(WidthIncludingTrailingWhitespace, 0, 0, Height), currentDirection, [])]; |
|
|
|
} |
|
|
|
|
|
|
|
bool TryMergeWithLastBounds(TextBounds currentBounds, TextBounds lastBounds) |
|
|
|
{ |
|
|
|
if (currentBounds.FlowDirection != lastBounds.FlowDirection) |
|
|
|
{ |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
if (currentBounds.Rectangle.Left == lastBounds.Rectangle.Right) |
|
|
|
{ |
|
|
|
foreach (var runBounds in currentBounds.TextRunBounds) |
|
|
|
{ |
|
|
|
lastBounds.TextRunBounds.Add(runBounds); |
|
|
|
} |
|
|
|
|
|
|
|
lastBounds.Rectangle = lastBounds.Rectangle.Union(currentBounds.Rectangle); |
|
|
|
|
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
if (currentBounds.Rectangle.Right == lastBounds.Rectangle.Left) |
|
|
|
{ |
|
|
|
for (int i = 0; i < currentBounds.TextRunBounds.Count; i++) |
|
|
|
{ |
|
|
|
lastBounds.TextRunBounds.Insert(i, currentBounds.TextRunBounds[i]); |
|
|
|
} |
|
|
|
|
|
|
|
lastBounds.Rectangle = lastBounds.Rectangle.Union(currentBounds.Rectangle); |
|
|
|
|
|
|
|
return true; |
|
|
|
} |
|
|
|
var result = new List<TextBounds>(); |
|
|
|
|
|
|
|
return false; |
|
|
|
} |
|
|
|
TextBounds? lastBounds = null; |
|
|
|
|
|
|
|
while (remainingLength > 0 && currentPosition < FirstTextSourceIndex + Length) |
|
|
|
{ |
|
|
|
@ -733,8 +672,8 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
directionalWidth = currentDrawable.Size.Width; |
|
|
|
} |
|
|
|
|
|
|
|
TextBounds currentBounds; |
|
|
|
int coveredLength; |
|
|
|
TextBounds? currentBounds; |
|
|
|
|
|
|
|
switch (currentDirection) |
|
|
|
{ |
|
|
|
@ -754,12 +693,6 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (coveredLength == 0) |
|
|
|
{ |
|
|
|
//This should never happen
|
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
if (lastBounds != null && TryMergeWithLastBounds(currentBounds, lastBounds)) |
|
|
|
{ |
|
|
|
currentBounds = lastBounds; |
|
|
|
@ -779,6 +712,90 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
result.Sort(TextBoundsComparer); |
|
|
|
|
|
|
|
return result; |
|
|
|
|
|
|
|
static FlowDirection GetDirection(TextRun? textRun, FlowDirection currentDirection) |
|
|
|
{ |
|
|
|
if (textRun is ShapedTextRun shapedTextRun) |
|
|
|
{ |
|
|
|
return shapedTextRun.ShapedBuffer.IsLeftToRight ? |
|
|
|
FlowDirection.LeftToRight : |
|
|
|
FlowDirection.RightToLeft; |
|
|
|
} |
|
|
|
|
|
|
|
return currentDirection; |
|
|
|
} |
|
|
|
|
|
|
|
IndexedTextRun FindIndexedRun() |
|
|
|
{ |
|
|
|
var i = 0; |
|
|
|
|
|
|
|
var currentIndexedRun = _indexedTextRuns[i]; |
|
|
|
|
|
|
|
while (currentIndexedRun.TextSourceCharacterIndex != currentPosition) |
|
|
|
{ |
|
|
|
if (i + 1 == _indexedTextRuns.Count) |
|
|
|
{ |
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
i++; |
|
|
|
|
|
|
|
currentIndexedRun = _indexedTextRuns[i]; |
|
|
|
} |
|
|
|
|
|
|
|
return currentIndexedRun; |
|
|
|
} |
|
|
|
|
|
|
|
double GetPreceedingDistance(int firstIndex) |
|
|
|
{ |
|
|
|
var distance = 0.0; |
|
|
|
|
|
|
|
for (var i = 0; i < firstIndex; i++) |
|
|
|
{ |
|
|
|
var currentRun = _textRuns[i]; |
|
|
|
|
|
|
|
if (currentRun is DrawableTextRun drawableTextRun) |
|
|
|
{ |
|
|
|
distance += drawableTextRun.Size.Width; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return distance; |
|
|
|
} |
|
|
|
|
|
|
|
bool TryMergeWithLastBounds(TextBounds currentBounds, TextBounds lastBounds) |
|
|
|
{ |
|
|
|
if (currentBounds.FlowDirection != lastBounds.FlowDirection) |
|
|
|
{ |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
if (currentBounds.Rectangle.Left == lastBounds.Rectangle.Right) |
|
|
|
{ |
|
|
|
foreach (var runBounds in currentBounds.TextRunBounds) |
|
|
|
{ |
|
|
|
lastBounds.TextRunBounds.Add(runBounds); |
|
|
|
} |
|
|
|
|
|
|
|
lastBounds.Rectangle = lastBounds.Rectangle.Union(currentBounds.Rectangle); |
|
|
|
|
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
if (currentBounds.Rectangle.Right == lastBounds.Rectangle.Left) |
|
|
|
{ |
|
|
|
for (int i = 0; i < currentBounds.TextRunBounds.Count; i++) |
|
|
|
{ |
|
|
|
lastBounds.TextRunBounds.Insert(i, currentBounds.TextRunBounds[i]); |
|
|
|
} |
|
|
|
|
|
|
|
lastBounds.Rectangle = lastBounds.Rectangle.Union(currentBounds.Rectangle); |
|
|
|
|
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private CharacterHit GetPreviousCharacterHit(CharacterHit characterHit, bool useGraphemeBoundaries) |
|
|
|
@ -885,7 +902,10 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
{ |
|
|
|
var runBounds = GetRunBoundsRightToLeft(shapedTextRun, startX, firstTextSourceIndex, remainingLength, currentPosition, out var offset); |
|
|
|
|
|
|
|
textRunBounds.Insert(0, runBounds); |
|
|
|
if (runBounds.TextSourceCharacterIndex < FirstTextSourceIndex + Length) |
|
|
|
{ |
|
|
|
textRunBounds.Insert(0, runBounds); |
|
|
|
} |
|
|
|
|
|
|
|
if (offset > 0) |
|
|
|
{ |
|
|
|
@ -904,20 +924,25 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
if (currentRun is DrawableTextRun drawableTextRun) |
|
|
|
if (currentPosition < FirstTextSourceIndex + Length) |
|
|
|
{ |
|
|
|
startX -= drawableTextRun.Size.Width; |
|
|
|
if (currentRun is DrawableTextRun drawableTextRun) |
|
|
|
{ |
|
|
|
startX -= drawableTextRun.Size.Width; |
|
|
|
|
|
|
|
textRunBounds.Insert(0, |
|
|
|
new TextRunBounds( |
|
|
|
new Rect(startX, 0, drawableTextRun.Size.Width, Height), currentPosition, currentRun.Length, currentRun)); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
//Add potential TextEndOfParagraph
|
|
|
|
textRunBounds.Add( |
|
|
|
new TextRunBounds( |
|
|
|
new Rect(endX, 0, 0, Height), currentPosition, currentRun.Length, currentRun)); |
|
|
|
var runBounds = new TextRunBounds( |
|
|
|
new Rect(startX, 0, drawableTextRun.Size.Width, Height), currentPosition, currentRun.Length, currentRun); |
|
|
|
|
|
|
|
textRunBounds.Insert(0, runBounds); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
//Add potential TextEndOfParagraph
|
|
|
|
var runBounds = new TextRunBounds( |
|
|
|
new Rect(endX, 0, 0, Height), currentPosition, currentRun.Length, currentRun); |
|
|
|
|
|
|
|
textRunBounds.Add(runBounds); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
currentPosition += currentRun.Length; |
|
|
|
@ -946,7 +971,7 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
int firstTextSourceIndex, int currentPosition, int remainingLength, out int coveredLength, out int newPosition) |
|
|
|
{ |
|
|
|
coveredLength = 0; |
|
|
|
var textRunBounds = new List<TextRunBounds>(); |
|
|
|
var textRunBounds = new List<TextRunBounds>(1); |
|
|
|
var endX = startX; |
|
|
|
|
|
|
|
for (int i = firstRunIndex; i <= lastRunIndex; i++) |
|
|
|
@ -957,7 +982,10 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
{ |
|
|
|
var runBounds = GetRunBoundsLeftToRight(shapedTextRun, endX, firstTextSourceIndex, remainingLength, currentPosition, out var offset); |
|
|
|
|
|
|
|
textRunBounds.Add(runBounds); |
|
|
|
if(runBounds.TextSourceCharacterIndex < FirstTextSourceIndex + Length) |
|
|
|
{ |
|
|
|
textRunBounds.Add(runBounds); |
|
|
|
} |
|
|
|
|
|
|
|
if (offset > 0) |
|
|
|
{ |
|
|
|
@ -976,20 +1004,26 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
if (currentRun is DrawableTextRun drawableTextRun) |
|
|
|
if (currentPosition < FirstTextSourceIndex + Length) |
|
|
|
{ |
|
|
|
textRunBounds.Add( |
|
|
|
new TextRunBounds( |
|
|
|
new Rect(endX, 0, drawableTextRun.Size.Width, Height), currentPosition, currentRun.Length, currentRun)); |
|
|
|
if (currentRun is DrawableTextRun drawableTextRun) |
|
|
|
{ |
|
|
|
var runBounds = new TextRunBounds( |
|
|
|
new Rect(endX, 0, drawableTextRun.Size.Width, Height), currentPosition, currentRun.Length, currentRun); |
|
|
|
|
|
|
|
endX += drawableTextRun.Size.Width; |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
//Add potential TextEndOfParagraph
|
|
|
|
textRunBounds.Add( |
|
|
|
new TextRunBounds( |
|
|
|
new Rect(endX, 0, 0, Height), currentPosition, currentRun.Length, currentRun)); |
|
|
|
textRunBounds.Add(runBounds); |
|
|
|
|
|
|
|
|
|
|
|
endX += drawableTextRun.Size.Width; |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
//Add potential TextEndOfParagraph
|
|
|
|
var runBounds = new TextRunBounds( |
|
|
|
new Rect(endX, 0, 0, Height), currentPosition, currentRun.Length, currentRun); |
|
|
|
|
|
|
|
textRunBounds.Add(runBounds); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
currentPosition += currentRun.Length; |
|
|
|
@ -1032,6 +1066,20 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
startIndex += offset; |
|
|
|
} |
|
|
|
|
|
|
|
//Make sure we start the hit test at the start of the possible cluster.
|
|
|
|
var clusterStartHit = currentRun.GlyphRun.GetPreviousCaretCharacterHit(new CharacterHit(startIndex)); |
|
|
|
var clusterEndHit = currentRun.GlyphRun.GetNextCaretCharacterHit(clusterStartHit); |
|
|
|
|
|
|
|
var clusterOffset = 0; |
|
|
|
|
|
|
|
if (startIndex > clusterStartHit.FirstCharacterIndex && startIndex < clusterEndHit.FirstCharacterIndex + clusterEndHit.TrailingLength) |
|
|
|
{ |
|
|
|
clusterOffset = clusterEndHit.FirstCharacterIndex + clusterEndHit.TrailingLength - startIndex; |
|
|
|
|
|
|
|
//We need to move the startIndex to the start of the cluster.
|
|
|
|
startIndex -= clusterOffset; |
|
|
|
} |
|
|
|
|
|
|
|
var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); |
|
|
|
var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); |
|
|
|
|
|
|
|
@ -1041,7 +1089,8 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
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); |
|
|
|
//Adjust characterLength by the cluster offset to only cover the remaining length of the cluster.
|
|
|
|
var characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength) - clusterOffset; |
|
|
|
|
|
|
|
if (characterLength == 0 && currentRun.Text.Length > 0 && startIndex < currentRun.Text.Length) |
|
|
|
{ |
|
|
|
@ -1075,7 +1124,7 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
|
|
|
|
var runWidth = endX - startX; |
|
|
|
|
|
|
|
var textSourceIndex = offset + startHit.FirstCharacterIndex; |
|
|
|
var textSourceIndex = startIndex + Math.Max(0, currentPosition - firstCluster) + clusterOffset; |
|
|
|
|
|
|
|
return new TextRunBounds(new Rect(startX, 0, runWidth, Height), textSourceIndex, characterLength, currentRun); |
|
|
|
} |
|
|
|
@ -1101,7 +1150,6 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
} |
|
|
|
|
|
|
|
var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); |
|
|
|
|
|
|
|
var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); |
|
|
|
|
|
|
|
startX -= currentRun.Size.Width - startOffset; |
|
|
|
@ -1110,7 +1158,21 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); |
|
|
|
var startHit = currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); |
|
|
|
|
|
|
|
var characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength); |
|
|
|
//Make sure we start the hit test at the start of the possible cluster.
|
|
|
|
var clusterStartHit = currentRun.GlyphRun.GetNextCaretCharacterHit(new CharacterHit(startIndex)); |
|
|
|
var clusterEndHit = currentRun.GlyphRun.GetPreviousCaretCharacterHit(startHit); |
|
|
|
|
|
|
|
var clusterOffset = 0; |
|
|
|
|
|
|
|
if (startIndex > clusterStartHit.FirstCharacterIndex && startIndex < clusterEndHit.FirstCharacterIndex + clusterEndHit.TrailingLength) |
|
|
|
{ |
|
|
|
clusterOffset = clusterEndHit.FirstCharacterIndex + clusterEndHit.TrailingLength - startIndex; |
|
|
|
|
|
|
|
//We need to move the startIndex to the start of the cluster.
|
|
|
|
startIndex -= clusterOffset; |
|
|
|
} |
|
|
|
|
|
|
|
var characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength) - clusterOffset; |
|
|
|
|
|
|
|
if (characterLength == 0 && currentRun.Text.Length > 0 && startIndex < currentRun.Text.Length) |
|
|
|
{ |
|
|
|
@ -1149,7 +1211,7 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
|
|
|
|
var runWidth = endX - startX; |
|
|
|
|
|
|
|
var textSourceIndex = offset + startHit.FirstCharacterIndex; |
|
|
|
var textSourceIndex = startIndex + Math.Max(0, currentPosition - firstCluster) + clusterOffset; |
|
|
|
|
|
|
|
return new TextRunBounds(new Rect(startX, 0, runWidth, Height), textSourceIndex, characterLength, currentRun); |
|
|
|
} |
|
|
|
|