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