|
@@ -1,5 +1,6 @@
|
|
|
using System;
|
|
|
using System.Collections.Generic;
|
|
|
+using System.Diagnostics;
|
|
|
using Avalonia.Media.TextFormatting.Unicode;
|
|
|
using Avalonia.Utilities;
|
|
|
|
|
@@ -395,22 +396,22 @@ namespace Avalonia.Media.TextFormatting
|
|
|
return currentDirection;
|
|
|
}
|
|
|
|
|
|
- IndexedTextRun FindIndexedRun()
|
|
|
+ IndexedTextRun FindIndexedRun(out int index)
|
|
|
{
|
|
|
- var i = 0;
|
|
|
+ index = 0;
|
|
|
|
|
|
- IndexedTextRun currentIndexedRun = _indexedTextRuns[i];
|
|
|
+ IndexedTextRun currentIndexedRun = _indexedTextRuns[index];
|
|
|
|
|
|
while (currentIndexedRun.TextSourceCharacterIndex != currentPosition)
|
|
|
{
|
|
|
- if (i + 1 == _indexedTextRuns.Count)
|
|
|
+ if (index + 1 == _indexedTextRuns.Count)
|
|
|
{
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
- i++;
|
|
|
+ index++;
|
|
|
|
|
|
- currentIndexedRun = _indexedTextRuns[i];
|
|
|
+ currentIndexedRun = _indexedTextRuns[index];
|
|
|
}
|
|
|
|
|
|
return currentIndexedRun;
|
|
@@ -434,7 +435,8 @@ namespace Avalonia.Media.TextFormatting
|
|
|
}
|
|
|
|
|
|
TextRun? currentTextRun = null;
|
|
|
- var currentIndexedRun = FindIndexedRun();
|
|
|
+
|
|
|
+ var currentIndexedRun = FindIndexedRun(out var indexedRunIndex);
|
|
|
|
|
|
while (currentPosition < FirstTextSourceIndex + Length)
|
|
|
{
|
|
@@ -451,7 +453,7 @@ namespace Avalonia.Media.TextFormatting
|
|
|
{
|
|
|
currentPosition += currentTextRun.Length;
|
|
|
|
|
|
- currentIndexedRun = FindIndexedRun();
|
|
|
+ currentIndexedRun = FindIndexedRun(out indexedRunIndex);
|
|
|
|
|
|
continue;
|
|
|
}
|
|
@@ -467,7 +469,6 @@ namespace Avalonia.Media.TextFormatting
|
|
|
|
|
|
var directionalWidth = 0.0;
|
|
|
var firstRunIndex = currentIndexedRun.RunIndex;
|
|
|
- var lastRunIndex = firstRunIndex;
|
|
|
|
|
|
var currentDirection = GetDirection(currentTextRun, _resolvedFlowDirection);
|
|
|
|
|
@@ -478,51 +479,7 @@ namespace Avalonia.Media.TextFormatting
|
|
|
directionalWidth = currentDrawable.Size.Width;
|
|
|
}
|
|
|
|
|
|
- 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];
|
|
|
-
|
|
|
- var nextDirection = GetDirection(nextRun, currentDirection);
|
|
|
-
|
|
|
- if (currentDirection != nextDirection)
|
|
|
- {
|
|
|
- break;
|
|
|
- }
|
|
|
-
|
|
|
- if (nextRun is DrawableTextRun nextDrawable)
|
|
|
- {
|
|
|
- directionalWidth += nextDrawable.Size.Width;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- // 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;
|
|
|
- }
|
|
|
-
|
|
|
- if (previousRun is DrawableTextRun previousDrawable)
|
|
|
- {
|
|
|
- directionalWidth += previousDrawable.Size.Width;
|
|
|
-
|
|
|
- currentX -= previousDrawable.Size.Width;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ var lastRunIndex = GetLastDirectionalRunIndex(indexedRunIndex, currentDirection, ref directionalWidth);
|
|
|
|
|
|
switch (currentDirection)
|
|
|
{
|
|
@@ -600,6 +557,71 @@ namespace Avalonia.Media.TextFormatting
|
|
|
return GetPreviousCharacterHit(characterHit, true);
|
|
|
}
|
|
|
|
|
|
+ private static FlowDirection GetRunDirection(TextRun? textRun, FlowDirection currentDirection)
|
|
|
+ {
|
|
|
+ if (textRun is ShapedTextRun shapedTextRun)
|
|
|
+ {
|
|
|
+ return shapedTextRun.ShapedBuffer.IsLeftToRight ?
|
|
|
+ FlowDirection.LeftToRight :
|
|
|
+ FlowDirection.RightToLeft;
|
|
|
+ }
|
|
|
+
|
|
|
+ return currentDirection;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Get the last consecutive visual run index that shares the same direction as the current direction.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="indexedRunIndex">The current logical run's index.</param>
|
|
|
+ /// <param name="flowDirection">The current flow direction.</param>
|
|
|
+ /// <param name="directionalWidth">The current directional width.</param>
|
|
|
+ /// <returns>
|
|
|
+ /// The last consecutive visual run index that shares the same direction as the current direction.
|
|
|
+ /// </returns>
|
|
|
+ private int GetLastDirectionalRunIndex(int indexedRunIndex, FlowDirection flowDirection, ref double directionalWidth)
|
|
|
+ {
|
|
|
+ if(_indexedTextRuns is null)
|
|
|
+ {
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+
|
|
|
+ var lastRunIndex = _indexedTextRuns[indexedRunIndex].RunIndex;
|
|
|
+
|
|
|
+ // Find consecutive runs of same direction
|
|
|
+ while (indexedRunIndex + 1 < _indexedTextRuns.Count)
|
|
|
+ {
|
|
|
+ var nextIndexedRun = _indexedTextRuns[++indexedRunIndex];
|
|
|
+
|
|
|
+ if (nextIndexedRun.RunIndex != lastRunIndex + 1)
|
|
|
+ {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ var nextRun = nextIndexedRun.TextRun;
|
|
|
+
|
|
|
+ if (nextRun is null)
|
|
|
+ {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ var nextDirection = GetRunDirection(nextRun, flowDirection);
|
|
|
+
|
|
|
+ if (nextDirection != flowDirection)
|
|
|
+ {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (nextRun is DrawableTextRun nextDrawable)
|
|
|
+ {
|
|
|
+ directionalWidth += nextDrawable.Size.Width;
|
|
|
+ }
|
|
|
+
|
|
|
+ lastRunIndex = nextIndexedRun.RunIndex;
|
|
|
+ }
|
|
|
+
|
|
|
+ return lastRunIndex;
|
|
|
+ }
|
|
|
+
|
|
|
public override IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceIndex, int textLength)
|
|
|
{
|
|
|
if(textLength == 0)
|
|
@@ -619,7 +641,7 @@ namespace Avalonia.Media.TextFormatting
|
|
|
if (firstTextSourceIndex + textLength < FirstTextSourceIndex)
|
|
|
{
|
|
|
var indexedTextRun = _indexedTextRuns[0];
|
|
|
- var currentDirection = GetDirection(indexedTextRun.TextRun, _resolvedFlowDirection);
|
|
|
+ var currentDirection = GetRunDirection(indexedTextRun.TextRun, _resolvedFlowDirection);
|
|
|
|
|
|
return [new TextBounds(new Rect(0,0,0, Height), currentDirection, [])];
|
|
|
}
|
|
@@ -628,7 +650,7 @@ namespace Avalonia.Media.TextFormatting
|
|
|
if (firstTextSourceIndex >= FirstTextSourceIndex + Length)
|
|
|
{
|
|
|
var indexedTextRun = _indexedTextRuns[_indexedTextRuns.Count - 1];
|
|
|
- var currentDirection = GetDirection(indexedTextRun.TextRun, _resolvedFlowDirection);
|
|
|
+ var currentDirection = GetRunDirection(indexedTextRun.TextRun, _resolvedFlowDirection);
|
|
|
|
|
|
return [new TextBounds(new Rect(WidthIncludingTrailingWhitespace, 0, 0, Height), currentDirection, [])];
|
|
|
}
|
|
@@ -639,16 +661,13 @@ namespace Avalonia.Media.TextFormatting
|
|
|
|
|
|
while (remainingLength > 0 && currentPosition < FirstTextSourceIndex + Length)
|
|
|
{
|
|
|
- var currentIndexedRun = FindIndexedRun();
|
|
|
+ var currentIndexedRun = FindIndexedRun(out var indexedRunIndex);
|
|
|
|
|
|
if (currentIndexedRun == null)
|
|
|
{
|
|
|
break;
|
|
|
}
|
|
|
-
|
|
|
- var directionalWidth = 0.0;
|
|
|
- var firstRunIndex = currentIndexedRun.RunIndex;
|
|
|
- var lastRunIndex = firstRunIndex;
|
|
|
+
|
|
|
var currentTextRun = currentIndexedRun.TextRun;
|
|
|
|
|
|
if (currentTextRun == null)
|
|
@@ -656,7 +675,7 @@ namespace Avalonia.Media.TextFormatting
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
- var currentDirection = GetDirection(currentTextRun, _resolvedFlowDirection);
|
|
|
+ var currentDirection = GetRunDirection(currentTextRun, _resolvedFlowDirection);
|
|
|
|
|
|
if (currentIndexedRun.TextSourceCharacterIndex + currentTextRun.Length <= firstTextSourceIndex)
|
|
|
{
|
|
@@ -666,11 +685,15 @@ namespace Avalonia.Media.TextFormatting
|
|
|
}
|
|
|
|
|
|
var currentX = Start + GetPreceedingDistance(currentIndexedRun.RunIndex);
|
|
|
+ var directionalWidth = 0.0;
|
|
|
|
|
|
if (currentTextRun is DrawableTextRun currentDrawable)
|
|
|
{
|
|
|
directionalWidth = currentDrawable.Size.Width;
|
|
|
}
|
|
|
+
|
|
|
+ var firstRunIndex = currentIndexedRun.RunIndex;
|
|
|
+ var lastRunIndex = GetLastDirectionalRunIndex(indexedRunIndex, currentDirection, ref directionalWidth);
|
|
|
|
|
|
TextBounds currentBounds;
|
|
|
int coveredLength;
|
|
@@ -686,7 +709,7 @@ namespace Avalonia.Media.TextFormatting
|
|
|
}
|
|
|
default:
|
|
|
{
|
|
|
- currentBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex,
|
|
|
+ currentBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex,
|
|
|
currentPosition, remainingLength, out coveredLength, out currentPosition);
|
|
|
|
|
|
break;
|
|
@@ -718,34 +741,22 @@ namespace Avalonia.Media.TextFormatting
|
|
|
|
|
|
return result;
|
|
|
|
|
|
- static FlowDirection GetDirection(TextRun? textRun, FlowDirection currentDirection)
|
|
|
+ IndexedTextRun FindIndexedRun(out int index)
|
|
|
{
|
|
|
- if (textRun is ShapedTextRun shapedTextRun)
|
|
|
- {
|
|
|
- return shapedTextRun.ShapedBuffer.IsLeftToRight ?
|
|
|
- FlowDirection.LeftToRight :
|
|
|
- FlowDirection.RightToLeft;
|
|
|
- }
|
|
|
+ index = 0;
|
|
|
|
|
|
- return currentDirection;
|
|
|
- }
|
|
|
-
|
|
|
- IndexedTextRun FindIndexedRun()
|
|
|
- {
|
|
|
- var i = 0;
|
|
|
-
|
|
|
- var currentIndexedRun = _indexedTextRuns[i];
|
|
|
+ var currentIndexedRun = _indexedTextRuns[index];
|
|
|
|
|
|
while (currentIndexedRun.TextSourceCharacterIndex != currentPosition)
|
|
|
{
|
|
|
- if (i + 1 == _indexedTextRuns.Count)
|
|
|
+ if (index + 1 == _indexedTextRuns.Count)
|
|
|
{
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
- i++;
|
|
|
+ index++;
|
|
|
|
|
|
- currentIndexedRun = _indexedTextRuns[i];
|
|
|
+ currentIndexedRun = _indexedTextRuns[index];
|
|
|
}
|
|
|
|
|
|
return currentIndexedRun;
|
|
@@ -905,14 +916,14 @@ namespace Avalonia.Media.TextFormatting
|
|
|
|
|
|
if (currentRun is ShapedTextRun shapedTextRun)
|
|
|
{
|
|
|
- var runBounds = GetRunBoundsRightToLeft(shapedTextRun, startX, firstTextSourceIndex, remainingLength, currentPosition, out var offset);
|
|
|
+ var runBounds = GetRunBoundsRightToLeft(shapedTextRun, startX, firstTextSourceIndex, remainingLength, currentPosition);
|
|
|
|
|
|
if (runBounds.TextSourceCharacterIndex < FirstTextSourceIndex + Length)
|
|
|
{
|
|
|
textRunBounds.Insert(0, runBounds);
|
|
|
}
|
|
|
|
|
|
- if (offset > 0)
|
|
|
+ if (i == lastRunIndex)
|
|
|
{
|
|
|
endX = runBounds.Rectangle.Right;
|
|
|
|
|
@@ -921,7 +932,7 @@ namespace Avalonia.Media.TextFormatting
|
|
|
|
|
|
startX -= runBounds.Rectangle.Width;
|
|
|
|
|
|
- currentPosition += runBounds.Length + offset;
|
|
|
+ currentPosition = runBounds.TextSourceCharacterIndex + runBounds.Length;
|
|
|
|
|
|
coveredLength += runBounds.Length;
|
|
|
|
|
@@ -985,23 +996,21 @@ namespace Avalonia.Media.TextFormatting
|
|
|
|
|
|
if (currentRun is ShapedTextRun shapedTextRun)
|
|
|
{
|
|
|
- var runBounds = GetRunBoundsLeftToRight(shapedTextRun, endX, firstTextSourceIndex, remainingLength, currentPosition, out var offset);
|
|
|
+ var runBounds = GetRunBoundsLeftToRight(shapedTextRun, endX, firstTextSourceIndex, remainingLength, currentPosition);
|
|
|
|
|
|
if(runBounds.TextSourceCharacterIndex < FirstTextSourceIndex + Length)
|
|
|
{
|
|
|
textRunBounds.Add(runBounds);
|
|
|
}
|
|
|
|
|
|
- if (offset > 0)
|
|
|
+ currentPosition = runBounds.TextSourceCharacterIndex + runBounds.Length;
|
|
|
+
|
|
|
+ if(i == firstRunIndex)
|
|
|
{
|
|
|
startX = runBounds.Rectangle.Left;
|
|
|
-
|
|
|
- endX = startX;
|
|
|
}
|
|
|
|
|
|
- currentPosition += runBounds.Length + offset;
|
|
|
-
|
|
|
- endX += runBounds.Rectangle.Width;
|
|
|
+ endX = runBounds.Rectangle.Right;
|
|
|
|
|
|
coveredLength += runBounds.Length;
|
|
|
|
|
@@ -1054,189 +1063,154 @@ namespace Avalonia.Media.TextFormatting
|
|
|
}
|
|
|
|
|
|
private TextRunBounds GetRunBoundsLeftToRight(ShapedTextRun currentRun, double startX,
|
|
|
- int firstTextSourceIndex, int remainingLength, int currentPosition, out int offset)
|
|
|
+ int firstTextSourceIndex, int remainingLength, int currentPosition)
|
|
|
{
|
|
|
- var startIndex = currentPosition;
|
|
|
-
|
|
|
- offset = Math.Max(0, firstTextSourceIndex - currentPosition);
|
|
|
-
|
|
|
+ //Determine the start of the first hit in local positions.
|
|
|
+ var runOffset = Math.Max(0, firstTextSourceIndex - currentPosition);
|
|
|
+
|
|
|
var firstCluster = currentRun.GlyphRun.Metrics.FirstCluster;
|
|
|
|
|
|
- if (currentPosition != firstCluster)
|
|
|
- {
|
|
|
- startIndex = firstCluster + offset;
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- startIndex += offset;
|
|
|
- }
|
|
|
+ //The start index needs to be relative to the first cluster
|
|
|
+ var startIndex = firstCluster + runOffset;
|
|
|
+ var endIndex = startIndex + remainingLength;
|
|
|
|
|
|
- //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);
|
|
|
+ //Current position is a text source index and first cluster is relative to the GlyphRun's buffer.
|
|
|
+ var textSourceOffset = currentPosition - firstCluster;
|
|
|
+
|
|
|
+ Debug.Assert(textSourceOffset >= 0);
|
|
|
|
|
|
var clusterOffset = 0;
|
|
|
|
|
|
- if (startIndex > clusterStartHit.FirstCharacterIndex && startIndex < clusterEndHit.FirstCharacterIndex + clusterEndHit.TrailingLength)
|
|
|
- {
|
|
|
- clusterOffset = clusterEndHit.FirstCharacterIndex + clusterEndHit.TrailingLength - startIndex;
|
|
|
+ //Cluster boundary correction
|
|
|
+ if (runOffset > 0)
|
|
|
+ {
|
|
|
+ var characterHit = currentRun.GlyphRun.FindNearestCharacterHit(startIndex, out _);
|
|
|
+
|
|
|
+ var clusterStart = characterHit.FirstCharacterIndex;
|
|
|
+ var clusterEnd = clusterStart + characterHit.TrailingLength;
|
|
|
+
|
|
|
+ //Test against left and right edge
|
|
|
+ if (clusterStart < startIndex && clusterEnd > startIndex)
|
|
|
+ {
|
|
|
+ //Remember the cluster correction offset
|
|
|
+ clusterOffset = startIndex - clusterStart;
|
|
|
|
|
|
- //We need to move the startIndex to the start of the cluster.
|
|
|
- startIndex -= clusterOffset;
|
|
|
+ //Move to the start of the cluster
|
|
|
+ startIndex -= clusterOffset;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
+ //Find the visual start and end position we want to hit test against
|
|
|
var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
|
|
|
- var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
|
|
|
+ var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(endIndex));
|
|
|
+
|
|
|
+ // Preserve non-zero width for zero-advance ranges
|
|
|
+ if (startOffset == endOffset && startIndex != endIndex)
|
|
|
+ {
|
|
|
+ //We need to make sure a zero width text line is hit test at the end so we add some delta
|
|
|
+ endOffset += MathUtilities.DoubleEpsilon;
|
|
|
+ }
|
|
|
|
|
|
var endX = startX + endOffset;
|
|
|
startX += startOffset;
|
|
|
|
|
|
+ //Hit test against visual positions
|
|
|
var startHit = currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
|
|
|
var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
|
|
|
|
|
|
+ var startHitIndex = startHit.FirstCharacterIndex + startHit.TrailingLength;
|
|
|
+ var endHitIndex = endHit.FirstCharacterIndex + endHit.TrailingLength;
|
|
|
+
|
|
|
//Adjust characterLength by the cluster offset to only cover the remaining length of the cluster.
|
|
|
- var characterLength = Math.Max(0, Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength -
|
|
|
- endHit.FirstCharacterIndex - endHit.TrailingLength) - clusterOffset);
|
|
|
-
|
|
|
- remainingLength -= characterLength;
|
|
|
-
|
|
|
- var runOffset = startIndex - firstCluster;
|
|
|
-
|
|
|
- //Make sure we are properly dealing with zero width space runs
|
|
|
- if (remainingLength > 0 && currentRun.Text.Length > 0 && runOffset + characterLength < currentRun.Text.Length)
|
|
|
- {
|
|
|
- var glyphInfos = currentRun.GlyphRun.GlyphInfos;
|
|
|
-
|
|
|
- for (int i = runOffset + characterLength; i < glyphInfos.Count; i++)
|
|
|
- {
|
|
|
- var glyphInfo = glyphInfos[i];
|
|
|
-
|
|
|
- if(glyphInfo.GlyphAdvance > 0)
|
|
|
- {
|
|
|
- break;
|
|
|
- }
|
|
|
-
|
|
|
- var graphemeEnumerator = new GraphemeEnumerator(currentRun.Text.Span.Slice(runOffset + characterLength));
|
|
|
-
|
|
|
- if(!graphemeEnumerator.MoveNext(out var grapheme))
|
|
|
- {
|
|
|
- break;
|
|
|
- }
|
|
|
-
|
|
|
- characterLength += grapheme.Length - clusterOffset;
|
|
|
- remainingLength -= grapheme.Length;
|
|
|
-
|
|
|
- if(remainingLength <= 0)
|
|
|
- {
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ var characterLength = Math.Max(0, Math.Abs(startHitIndex - endHitIndex) - clusterOffset);
|
|
|
|
|
|
+ // Normalize bounds
|
|
|
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;
|
|
|
|
|
|
- var textSourceIndex = startIndex + Math.Max(0, currentPosition - firstCluster) + clusterOffset;
|
|
|
+ //We need to adjust the local position to the text source
|
|
|
+ var textSourceIndex = textSourceOffset + startHitIndex + clusterOffset;
|
|
|
|
|
|
return new TextRunBounds(new Rect(startX, 0, runWidth, Height), textSourceIndex, characterLength, currentRun);
|
|
|
}
|
|
|
|
|
|
- private TextRunBounds GetRunBoundsRightToLeft(ShapedTextRun currentRun, double endX,
|
|
|
- int firstTextSourceIndex, int remainingLength, int currentPosition, out int offset)
|
|
|
+ private TextRunBounds GetRunBoundsRightToLeft(ShapedTextRun currentRun, double endX, int firstTextSourceIndex, int remainingLength, int currentPosition)
|
|
|
{
|
|
|
+ // We start from the right edge of the run
|
|
|
var startX = endX;
|
|
|
|
|
|
- var startIndex = currentPosition;
|
|
|
-
|
|
|
- offset = Math.Max(0, firstTextSourceIndex - currentPosition);
|
|
|
+ //Determine the start of the first hit in local positions.
|
|
|
+ var runOffset = Math.Max(0, firstTextSourceIndex - currentPosition);
|
|
|
|
|
|
var firstCluster = currentRun.GlyphRun.Metrics.FirstCluster;
|
|
|
|
|
|
- if (currentPosition != firstCluster)
|
|
|
- {
|
|
|
- startIndex = firstCluster + offset;
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- startIndex += offset;
|
|
|
- }
|
|
|
-
|
|
|
- var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
|
|
|
- var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
|
|
|
-
|
|
|
- startX -= currentRun.Size.Width - startOffset;
|
|
|
- endX -= currentRun.Size.Width - endOffset;
|
|
|
-
|
|
|
- var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
|
|
|
- var startHit = currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
|
|
|
+ //The start index needs to be relative to the first cluster
|
|
|
+ var startIndex = firstCluster + runOffset;
|
|
|
+ var endIndex = startIndex + remainingLength;
|
|
|
|
|
|
- //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);
|
|
|
+ //Current position is a text source index and first cluster is relative to the GlyphRun's buffer.
|
|
|
+ var textSourceOffset = currentPosition - firstCluster;
|
|
|
+ Debug.Assert(textSourceOffset >= 0);
|
|
|
|
|
|
var clusterOffset = 0;
|
|
|
|
|
|
- if (startIndex > clusterStartHit.FirstCharacterIndex && startIndex < clusterEndHit.FirstCharacterIndex + clusterEndHit.TrailingLength)
|
|
|
+ //Cluster boundary correction
|
|
|
+ if (runOffset > 0)
|
|
|
{
|
|
|
- clusterOffset = clusterEndHit.FirstCharacterIndex + clusterEndHit.TrailingLength - startIndex;
|
|
|
+ var characterHit = currentRun.GlyphRun.FindNearestCharacterHit(startIndex, out _);
|
|
|
|
|
|
- //We need to move the startIndex to the start of the cluster.
|
|
|
- startIndex -= clusterOffset;
|
|
|
- }
|
|
|
-
|
|
|
- var characterLength = Math.Max(0, Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength -
|
|
|
- endHit.FirstCharacterIndex - endHit.TrailingLength) - clusterOffset);
|
|
|
-
|
|
|
- var runOffset = startIndex - offset;
|
|
|
-
|
|
|
- if (characterLength == 0 && currentRun.Text.Length > 0 && runOffset < currentRun.Text.Length)
|
|
|
- {
|
|
|
- //Make sure we are properly dealing with zero width space runs
|
|
|
- var codepointEnumerator = new CodepointEnumerator(currentRun.Text.Span.Slice(runOffset));
|
|
|
+ var clusterStart = characterHit.FirstCharacterIndex;
|
|
|
+ var clusterEnd = clusterStart + characterHit.TrailingLength;
|
|
|
|
|
|
- while (remainingLength > 0 && codepointEnumerator.MoveNext(out var codepoint))
|
|
|
+ //Test against left and right edge
|
|
|
+ if (clusterStart < startIndex && clusterEnd > startIndex)
|
|
|
{
|
|
|
- if (codepoint.IsWhiteSpace)
|
|
|
- {
|
|
|
- characterLength++;
|
|
|
- remainingLength--;
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- break;
|
|
|
- }
|
|
|
+ //Remember the cluster correction offset
|
|
|
+ clusterOffset = startIndex - clusterStart;
|
|
|
+
|
|
|
+ //Move to the start of the cluster
|
|
|
+ startIndex -= clusterOffset;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- if (startHit.FirstCharacterIndex > endHit.FirstCharacterIndex)
|
|
|
+ //Find the visual start and end position we want to hit test against
|
|
|
+ var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
|
|
|
+ var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(endIndex));
|
|
|
+
|
|
|
+ //We need the distance from right to left and GetDistanceFromCharacterHit returs a distance from left to right so we need to adjust the offsets
|
|
|
+ startX -= currentRun.Size.Width - startOffset;
|
|
|
+ endX -= currentRun.Size.Width - endOffset;
|
|
|
+
|
|
|
+ // Preserve non-zero width for zero-advance ranges
|
|
|
+ if (startOffset == endOffset && startIndex != endIndex)
|
|
|
{
|
|
|
- startHit = endHit;
|
|
|
+ //We need to make sure a zero width text line is hit test at the end so we add some delta
|
|
|
+ endOffset += MathUtilities.DoubleEpsilon;
|
|
|
}
|
|
|
|
|
|
+ //Hit test against visual positions
|
|
|
+ var startHit = currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
|
|
|
+ var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
|
|
|
+
|
|
|
+ var startHitIndex = startHit.FirstCharacterIndex + startHit.TrailingLength;
|
|
|
+ var endHitIndex = endHit.FirstCharacterIndex + endHit.TrailingLength;
|
|
|
+
|
|
|
+ var characterLength = Math.Max(0, Math.Abs(startHitIndex - endHitIndex) - clusterOffset);
|
|
|
+
|
|
|
+ // Normalize bounds
|
|
|
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;
|
|
|
|
|
|
- var textSourceIndex = startIndex + Math.Max(0, currentPosition - firstCluster) + clusterOffset;
|
|
|
+ //We need to adjust the local position to the text source
|
|
|
+ var textSourceIndex = textSourceOffset + startHitIndex + clusterOffset;
|
|
|
|
|
|
return new TextRunBounds(new Rect(startX, 0, runWidth, Height), textSourceIndex, characterLength, currentRun);
|
|
|
}
|