@ -4,8 +4,12 @@ using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
internal sealed class TextLineImpl : TextLine
internal class TextLineImpl : TextLine
{
internal static Comparer < TextBounds > TextBoundsComparer { get ; } =
Comparer < TextBounds > . Create ( ( x , y ) = > x . Rectangle . Left . CompareTo ( y . Rectangle . Left ) ) ;
private IReadOnlyList < IndexedTextRun > ? _ indexedTextRuns ;
private readonly TextRun [ ] _ textRuns ;
private readonly double _ paragraphWidth ;
private readonly TextParagraphProperties _ paragraphProperties ;
@ -338,184 +342,169 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/>
public override double GetDistanceFromCharacterHit ( CharacterHit characterHit )
{
var flowDirection = _ paragraphProperties . FlowDirection ;
var characterIndex = characterHit . FirstCharacterIndex + characterHit . TrailingLength ;
var currentPosition = FirstTextSourceIndex ;
var remainingLength = characterIndex - FirstTextSourceIndex ;
var currentDistance = Start ;
if ( flowDirection = = FlowDirection . LeftToRight )
if ( _ indexedTextRuns is null | | _ indexedTextRuns . Count = = 0 )
{
for ( var index = 0 ; index < _ textRuns . Length ; index + + )
{
var currentRun = _ textRuns [ index ] ;
if ( currentRun is ShapedTextRun shapedRun & & ! shapedRun . ShapedBuffer . IsLeftToRight )
{
var i = index ;
var rightToLeftWidth = shapedRun . Size . Width ;
while ( i + 1 < = _ textRuns . Length - 1 )
{
var nextRun = _ textRuns [ i + 1 ] ;
if ( nextRun is ShapedTextRun nextShapedRun & & ! nextShapedRun . ShapedBuffer . IsLeftToRight )
{
i + + ;
return Start ;
}
rightToLeftWidth + = nextShapedRun . Size . Width ;
var characterIndex = Math . Min (
characterHit . FirstCharacterIndex + characterHit . TrailingLength ,
FirstTextSourceIndex + Length ) ;
continue ;
}
var currentPosition = FirstTextSourceIndex ;
break ;
}
static FlowDirection GetDirection ( TextRun textRun , FlowDirection currentDirection )
{
if ( textRun is ShapedTextRun shapedTextRun )
{
return shapedTextRun . ShapedBuffer . IsLeftToRight ?
FlowDirection . LeftToRight :
FlowDirection . RightToLeft ;
}
if ( i > index )
{
while ( i > = index )
{
currentRun = _ textRuns [ i ] ;
return currentDirection ;
}
if ( currentRun is DrawableTextRun drawable )
{
rightToLeftWidth - = drawable . Size . Width ;
}
IndexedTextRun FindIndexedRun ( )
{
var i = 0 ;
if ( currentPosition + currentRun . Length > = characterIndex )
{
break ;
}
IndexedTextRun currentIndexedRun = _ indexedTextRuns [ i ] ;
currentPosition + = currentRun . Length ;
while ( currentIndexedRun . TextSourceCharacterIndex ! = currentPosition )
{
if ( i + 1 < _ indexedTextRuns . Count )
{
i + + ;
remainingLength - = currentRun . Length ;
currentIndexedRun = _ indexedTextRuns [ i ] ;
}
}
i - - ;
}
return currentIndexedRun ;
}
currentDistance + = rightToLeftWidth ;
}
}
double GetPreceedingDistance ( int firstIndex )
{
var distance = 0.0 ;
if ( currentPosition + currentRun . Length > = characterIndex & &
TryGetDistanceFromCharacterHit ( currentRun , characterHit , currentPosition , remainingLength , flowDirection , out var distance , out _ ) )
{
return Math . Max ( 0 , currentDistance + distance ) ;
}
for ( var i = 0 ; i < firstIndex ; i + + )
{
var currentRun = _ textRuns [ i ] ;
if ( currentRun is DrawableTextRun drawableTextRun )
{
currentD istance + = drawableTextRun . Size . Width ;
d istance + = drawableTextRun . Size . Width ;
}
//No hit hit found so we add the full width
currentPosition + = currentRun . Length ;
remainingLength - = currentRun . Length ;
}
return distance ;
}
else
TextRun ? currentTextRun = null ;
var currentIndexedRun = FindIndexedRun ( ) ;
while ( currentPosition < FirstTextSourceIndex + Length )
{
currentDistance + = WidthIncludingTrailingWhitespace ;
currentTextRun = currentIndexedRun . TextRun ;
for ( var index = _ textRuns . Length - 1 ; index > = 0 ; index - - )
if ( currentTextRun = = null )
{
var currentRun = _ textRuns [ index ] ;
break ;
}
if ( TryGetDistanceFromCharacterHit ( currentRun , characterHit , currentPosition , remainingLength ,
flowDirection , out var distance , out var currentGlyphRun ) )
if ( currentIndexedRun . TextSourceCharacterIndex + currentTextRun . Length < = characterHit . FirstCharacterIndex )
{
if ( currentPosition + currentTextRun . Length < FirstTextSourceIndex + Length )
{
if ( currentGlyphRun ! = null )
{
currentDistance - = currentGlyphRun . Bounds . Width ;
}
currentPosition + = currentTextRun . Length ;
return currentDistance + distance ;
}
currentIndexedRun = FindIndexedRun ( ) ;
if ( currentRun is DrawableTextRun drawableTextRun )
{
currentDistance - = drawableTextRun . Size . Width ;
continue ;
}
//No hit hit found so we add the full width
currentPosition + = currentRun . Length ;
remainingLength - = currentRun . Length ;
}
break ;
}
return Math . Max ( 0 , currentDistance ) ;
}
if ( currentTextRun = = null )
{
return 0 ;
}
private static bool TryGetDistanceFromCharacterHit (
TextRun currentRun ,
CharacterHit characterHit ,
int currentPosition ,
int remainingLength ,
FlowDirection flowDirection ,
out double distance ,
out GlyphRun ? currentGlyphRun )
{
var characterIndex = characterHit . FirstCharacterIndex + characterHit . TrailingLength ;
var isTrailingHit = characterHit . TrailingLength > 0 ;
var directionalWidth = 0.0 ;
var firstRunIndex = currentIndexedRun . RunIndex ;
var lastRunIndex = firstRunIndex ;
distance = 0 ;
currentGlyphRun = null ;
var currentDirection = GetDirection ( currentTextRun , _ resolvedFlowDirection ) ;
switch ( currentRun )
var currentX = Start + GetPreceedingDistance ( currentIndexedRun . RunIndex ) ;
if ( currentTextRun is DrawableTextRun currentDrawable )
{
case ShapedTextRun shapedTextCharacters :
{
currentGlyphRun = shapedTextCharacters . GlyphRun ;
directionalWidth = currentDrawable . Size . Width ;
}
if ( currentPosition + remainingLength < = currentPosition + currentRun . Length )
{
characterHit = new CharacterHit ( currentPosition + remainingLength ) ;
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 ] ;
distance = currentGlyphRun . GetDistanceFromCharacterHit ( characterHit ) ;
var nextDirection = GetDirection ( nextRun , currentDirection ) ;
return true ;
if ( currentDirection ! = nextDirection )
{
break ;
}
if ( currentPosition + remainingLength = = currentPosition + currentRun . Length & & isTrailingHit )
if ( nextRun is DrawableTextRun nextDrawable )
{
if ( currentGlyphRun . IsLeftToRight | | flowDirection = = FlowDirection . RightToLeft )
{
distance = currentGlyphRun . Bounds . Width ;
}
return true ;
directionalWidth + = nextDrawable . Size . Width ;
}
break ;
}
case DrawableTextRun drawableTextRun :
}
else
{
// Find consecutive runs of same direction
for ( ; firstRunIndex - 1 > 0 ; firstRunIndex - - )
{
if ( characterIndex = = currentPosition )
var previousRun = _ textRuns [ firstRunIndex - 1 ] ;
var previousDirection = GetDirection ( previousRun , currentDirection ) ;
if ( currentDirection ! = previousDirection )
{
return true ;
b reak ;
}
if ( characterIndex = = currentPosition + currentRun . Length )
if ( previousRun is DrawableTextRun previousDrawable )
{
distance = drawableTextRun . Size . Width ;
return true ;
directionalWidth + = previousDrawable . Size . Width ;
currentX - = previousDrawable . Size . Width ;
}
}
}
}
break ;
switch ( currentDirection )
{
case FlowDirection . RightToLeft :
{
return GetTextRunBoundsRightToLeft ( firstRunIndex , lastRunIndex , currentX + directionalWidth , characterIndex ,
currentPosition , 1 , out _ , out _ ) . Rectangle . Right ;
}
default :
{
return false ;
return GetTextBoundsLeftToRight ( firstRunIndex , lastRunIndex , currentX , characterIndex ,
currentPosition , 1 , out _ , out _ ) . Rectangle . Left ;
}
}
return false ;
}
/// <inheritdoc/>
@ -585,7 +574,7 @@ namespace Avalonia.Media.TextFormatting
public override IReadOnlyList < TextBounds > GetTextBounds ( int firstTextSourceIndex , int textLength )
{
if ( _ textRuns . Length = = 0 )
if ( _ indexedTex tRuns is null | | _ indexedT extRuns. Count = = 0 )
{
return Array . Empty < TextBounds > ( ) ;
}
@ -607,303 +596,154 @@ namespace Avalonia.Media.TextFormatting
return currentDirection ;
}
if ( _ paragraphProperties . FlowDirection = = FlowDirection . LeftToRight )
IndexedTextRun FindIndexedRun ( )
{
var currentX = Start ;
var i = 0 ;
for ( int i = 0 ; i < _ textRuns . Length ; i + + )
{
var currentRun = _ textRuns [ i ] ;
IndexedTextRun currentIndexedRun = _ indexedTextRuns [ i ] ;
var firstRunIndex = i ;
var lastRunIndex = firstRunIndex ;
var currentDirection = GetDirection ( currentRun , FlowDirection . LeftToRight ) ;
var directionalWidth = 0.0 ;
if ( currentRun is DrawableTextRun currentDrawable )
while ( currentIndexedRun . TextSourceCharacterIndex ! = currentPosition )
{
if ( i + 1 < _ indexedTextRuns . Count )
{
directionalWidth = currentDrawable . Size . Width ;
}
i + + ;
// Find consecutive runs of same direction
for ( ; lastRunIndex + 1 < _ textRuns . Length ; lastRunIndex + + )
{
var nextRun = _ textRuns [ lastRunIndex + 1 ] ;
currentIndexedRun = _ indexedTextRuns [ i ] ;
}
}
var nextDirection = GetDirection ( nextRun , currentDirection ) ;
return currentIndexedRun ;
}
if ( currentDirection ! = nextDirection )
{
break ;
}
double GetPreceedingDistance ( int firstIndex )
{
var distance = 0.0 ;
if ( nextRun is DrawableTextRun nextDrawable )
{
directionalWidth + = nextDrawable . Size . Width ;
}
}
for ( var i = 0 ; i < firstIndex ; i + + )
{
var currentRun = _ textRuns [ i ] ;
//Skip runs that are not part of the hit test range
switch ( currentDirection )
if ( currentRun is DrawableTextRun drawableTextRun )
{
case FlowDirection . RightToLeft :
{
for ( ; lastRunIndex > = firstRunIndex ; lastRunIndex - - )
{
currentRun = _ textRuns [ lastRunIndex ] ;
distance + = drawableTextRun . Size . Width ;
}
}
if ( currentPosition + currentRun . Length > firstTextSourceIndex )
{
break ;
}
return distance ;
}
currentPosition + = currentRun . Length ;
while ( remainingLength > 0 & & currentPosition < FirstTextSourceIndex + Length )
{
var currentIndexedRun = FindIndexedRun ( ) ;
if ( currentRun is DrawableTextRun drawableTextRun )
{
directionalWidth - = drawableTextRun . Size . Width ;
currentX + = drawableTextRun . Size . Width ;
}
if ( currentIndexedRun = = null )
{
break ;
}
if ( lastRunIndex - 1 < 0 )
{
break ;
}
}
var directionalWidth = 0.0 ;
var firstRunIndex = currentIndexedRun . RunIndex ;
var lastRunIndex = firstRunIndex ;
var currentTextRun = currentIndexedRun . TextRun ;
break ;
}
default :
{
for ( ; firstRunIndex < = lastRunIndex ; firstRunIndex + + )
{
currentRun = _ textRuns [ firstRunIndex ] ;
if ( currentPosition + currentRun . Length > firstTextSourceIndex )
{
break ;
}
if ( currentTextRun = = null )
{
break ;
}
currentPosition + = currentRun . Length ;
var currentDirection = GetDirection ( currentTextRun , _ resolvedFlowDirection ) ;
if ( currentRun is DrawableTextRun drawableTextRun )
{
currentX + = drawableTextRun . Size . Width ;
directionalWidth - = drawableTextRun . Size . Width ;
}
if ( currentIndexedRun . TextSourceCharacterIndex + currentTextRun . Length < = firstTextSourceIndex )
{
currentPosition + = currentTextRun . Length ;
if ( firstRunIndex + 1 = = _ textRuns . Length )
{
break ;
}
}
continue ;
}
break ;
}
}
var currentX = Start + GetPreceedingDistance ( currentIndexedRun . RunIndex ) ;
i = lastRunIndex ;
if ( currentTextRun is DrawableTextRun currentDrawable )
{
directionalWidth = currentDrawable . Size . Width ;
}
//Possible overlap at runs of different direction
if ( directionalWidth = = 0 & & i < _ textRuns . Length - 1 )
if ( currentTextRun is not TextEndOfLine )
{
if ( currentDirection = = FlowDirection . LeftToRight )
{
//In case a run only contains a linebreak we don't want to skip it.
if ( currentRun is ShapedTextRun shaped )
{
if ( currentRun . Length - shaped . GlyphRun . Metrics . NewLineLength > 0 )
{
continue ;
}
}
else
// Find consecutive runs of same direction
for ( ; lastRunIndex + 1 < _ textRuns . Length ; lastRunIndex + + )
{
continue ;
}
}
var nextRun = _ textRuns [ lastRunIndex + 1 ] ;
int coveredLength ;
TextBounds ? textBounds ;
var nextDirection = GetDirection ( nextRun , currentDirection ) ;
switch ( currentDirection )
{
case FlowDirection . RightToLeft :
if ( currentDirection ! = nextDirection )
{
textBounds = GetTextRunBoundsRightToLeft ( firstRunIndex , lastRunIndex , currentX + directionalWidth , firstTextSourceIndex ,
currentPosition , remainingLength , out coveredLength , out currentPosition ) ;
currentX + = directionalWidth ;
break ;
}
default :
{
textBounds = GetTextBoundsLeftToRight ( firstRunIndex , lastRunIndex , currentX , firstTextSourceIndex ,
currentPosition , remainingLength , out coveredLength , out currentPosition ) ;
currentX = textBounds . Rectangle . Right ;
break ;
if ( nextRun is DrawableTextRun nextDrawable )
{
directionalWidth + = nextDrawable . Size . Width ;
}
}
}
if ( coveredLength > 0 )
{
result . Add ( textBounds ) ;
remainingLength - = coveredLength ;
}
if ( remainingLength < = 0 )
{
break ;
}
}
}
else
{
var currentX = Start + WidthIncludingTrailingWhitespace ;
for ( int i = _ textRuns . Length - 1 ; i > = 0 ; i - - )
{
var currentRun = _ textRuns [ i ] ;
var firstRunIndex = i ;
var lastRunIndex = firstRunIndex ;
var currentDirection = GetDirection ( currentRun , FlowDirection . RightToLeft ) ;
var directionalWidth = 0.0 ;
if ( currentRun is DrawableTextRun currentDrawable )
{
directionalWidth = currentDrawable . Size . Width ;
}
// Find consecutive runs of same direction
for ( ; firstRunIndex - 1 > 0 ; firstRunIndex - - )
else
{
var previousRun = _ textRuns [ firstRunIndex - 1 ] ;
var previousDirection = GetDirection ( previousRun , currentDirection ) ;
if ( currentDirection ! = previousDirection )
// Find consecutive runs of same direction
for ( ; firstRunIndex - 1 > 0 ; firstRunIndex - - )
{
break ;
}
var previousRun = _ textRuns [ firstRunIndex - 1 ] ;
if ( currentRun is DrawableTextRun previousDrawable )
{
directionalWidth + = previousDrawable . Size . Width ;
}
}
var previousDirection = GetDirection ( previousRun , currentDirection ) ;
//Skip runs that are not part of the hit test range
switch ( currentDirection )
{
case FlowDirection . RightToLeft :
if ( currentDirection ! = previousDirection )
{
for ( ; lastRunIndex > = firstRunIndex ; lastRunIndex - - )
{
currentRun = _ textRuns [ lastRunIndex ] ;
if ( currentPosition + currentRun . Length < = firstTextSourceIndex )
{
currentPosition + = currentRun . Length ;
if ( currentRun is DrawableTextRun drawableTextRun )
{
currentX - = drawableTextRun . Size . Width ;
directionalWidth - = drawableTextRun . Size . Width ;
}
continue ;
}
break ;
}
break ;
}
default :
{
for ( ; firstRunIndex < = lastRunIndex ; firstRunIndex + + )
{
currentRun = _ textRuns [ firstRunIndex ] ;
if ( currentPosition + currentRun . Length < = firstTextSourceIndex )
{
currentPosition + = currentRun . Length ;
if ( currentRun is DrawableTextRun drawableTextRun )
{
currentX + = drawableTextRun . Size . Width ;
directionalWidth - = drawableTextRun . Size . Width ;
}
continue ;
}
break ;
}
if ( previousRun is DrawableTextRun previousDrawable )
{
directionalWidth + = previousDrawable . Size . Width ;
break ;
currentX - = previousDrawable . Size . Width ;
}
}
}
}
i = firstRunIndex ;
int coveredLength ;
TextBounds ? textBounds ;
//Possible overlap at runs of different direction
if ( directionalWidth = = 0 & & i > 0 )
{
//In case a run only contains a linebreak we don't want to skip it.
if ( currentRun is ShapedTextRun shaped )
switch ( currentDirection )
{
case FlowDirection . RightToLeft :
{
if ( currentRun . Length - shaped . GlyphRun . Metrics . NewLineLength > 0 )
{
continue ;
}
textBounds = GetTextRunBoundsRightToLeft ( firstRunIndex , lastRunIndex , currentX + directionalWidth , firstTextSourceIndex ,
currentPosition , remainingLength , out coveredLength , out currentPosition ) ;
break ;
}
else
default :
{
continue ;
}
}
int coveredLength ;
TextBounds ? textBounds ;
switch ( currentDirection )
{
case FlowDirection . LeftToRight :
{
textBounds = GetTextBoundsLeftToRight ( firstRunIndex , lastRunIndex , currentX - directionalWidth , firstTextSourceIndex ,
currentPosition , remainingLength , out coveredLength , out currentPosition ) ;
currentX - = directionalWidth ;
break ;
}
default :
{
textBounds = GetTextRunBoundsRightToLeft ( firstRunIndex , lastRunIndex , currentX , firstTextSourceIndex ,
currentPosition , remainingLength , out coveredLength , out currentPosition ) ;
currentX = textBounds . Rectangle . Left ;
textBounds = GetTextBoundsLeftToRight ( firstRunIndex , lastRunIndex , currentX , firstTextSourceIndex ,
currentPosition , remainingLength , out coveredLength , out currentPosition ) ;
break ;
}
}
break ;
}
}
//Visual order is always left to right so we need to insert
result . Insert ( 0 , textBounds ) ;
if ( coveredLength > 0 )
{
result . Add ( textBounds ) ;
remainingLength - = coveredLength ;
if ( remainingLength < = 0 )
{
break ;
}
}
}
result . Sort ( TextBoundsComparer ) ;
return result ;
}
@ -1164,7 +1004,7 @@ namespace Avalonia.Media.TextFormatting
_ textLineBreak = new TextLineBreak ( textEndOfLine ) ;
}
BidiReorderer . Instance . BidiReorder ( _ textRuns , _ resolvedFlowDirection ) ;
_ indexedTextRuns = BidiReorderer . Instance . BidiReorder ( _ textRuns , _ paragraphProperties . FlowDirection , FirstTextSourceIndex ) ;
}
/// <summary>
@ -1211,13 +1051,6 @@ namespace Avalonia.Media.TextFormatting
return true ;
}
//var characterIndex = codepointIndex - shapedRun.Text.Start;
//if (characterIndex < 0 && shapedRun.ShapedBuffer.IsLeftToRight)
//{
// foundCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex);
//}
nextCharacterHit = isAtEnd | | characterHit . TrailingLength ! = 0 ?
foundCharacterHit :
new CharacterHit ( foundCharacterHit . FirstCharacterIndex + foundCharacterHit . TrailingLength ) ;
@ -1556,8 +1389,8 @@ namespace Avalonia.Media.TextFormatting
TrailingWhitespaceLength = trailingWhitespaceLength ,
Width = width ,
WidthIncludingTrailingWhitespace = widthIncludingWhitespace ,
OverhangLeading = overhangLeading ,
OverhangTrailing = overhangTrailing ,
OverhangLeading = overhangLeading ,
OverhangTrailing = overhangTrailing ,
OverhangAfter = overhangAfter
} ;
}
@ -1615,8 +1448,7 @@ namespace Avalonia.Media.TextFormatting
return Math . Max ( 0 , start ) ;
case TextAlignment . Right :
return Math . Max ( 0 , _ paragraphWidth - width ) ;
return Math . Max ( 0 , _ paragraphWidth - widthIncludingTrailingWhitespace ) ;
default :
return 0 ;
}