|
|
@@ -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);
|
|
|
}
|