@ -1,4 +1,5 @@
using System ;
using System.Buffers ;
using System.Collections.Generic ;
using Avalonia.Media.TextFormatting.Unicode ;
@ -44,54 +45,7 @@ namespace Avalonia.Media.TextFormatting
var breakOportunities = new Queue < int > ( ) ;
var currentPosition = textLine . FirstTextSourceIndex ;
// Note: trailing whitespace needs no special handling here. The LineBreakEnumerator does
// not emit a non-required break inside trailing whitespace (LB07 forbids breaking before
// a space, LB06 before a hard break), and the run-final break is excluded by
// PositionWrap != textRun.Length below - so no break opportunity ever targets a glyph in
// the trailing whitespace. Verified for ASCII and ideographic (U+3000) spaces by
// Justify_Does_Not_Space_Trailing_Whitespace.
for ( var i = 0 ; i < lineImpl . TextRuns . Count ; + + i )
{
var textRun = lineImpl . TextRuns [ i ] ;
var text = textRun . Text ;
if ( text . IsEmpty )
{
continue ;
}
var lineBreakEnumerator = new LineBreakEnumerator ( text . Span ) ;
while ( lineBreakEnumerator . MoveNext ( out var currentBreak ) )
{
if ( ! currentBreak . Required & & currentBreak . PositionWrap ! = textRun . Length )
{
// The extra advance must land on the glyph that ENDS at the break
// boundary (the last glyph before the break), so the widened gap sits on
// the break itself. For whitespace breaks GetLineBreak has already pulled
// PositionMeasure back onto the trailing whitespace glyph
// (PositionMeasure < PositionWrap), so that position is the target as-is.
// For zero-width breaks - CJK/Korean ideograph boundaries, hyphens and
// other break-after punctuation - PositionMeasure == PositionWrap and
// points one glyph PAST the boundary; step back one so we widen the gap the
// break represents rather than the following gap. This also keeps the last
// visible glyph of a CJK/Korean line unstretched: its only inbound break is
// the run-final break, already excluded by PositionWrap != textRun.Length.
var target = currentPosition + currentBreak . PositionMeasure ;
if ( currentBreak . PositionMeasure = = currentBreak . PositionWrap )
{
target - = 1 ;
}
breakOportunities . Enqueue ( target ) ;
}
}
currentPosition + = textRun . Length ;
}
CollectBreakOpportunities ( lineImpl , breakOportunities ) ;
if ( breakOportunities . Count = = 0 )
{
@ -107,7 +61,7 @@ namespace Avalonia.Media.TextFormatting
var remainingSpace = Math . Max ( 0 , paragraphWidth - lineImpl . Width ) ;
var spacing = remainingSpace / breakOportunities . Count ;
currentPosition = textLine . FirstTextSourceIndex ;
var currentPosition = textLine . FirstTextSourceIndex ;
for ( var runIndex = 0 ; runIndex < lineImpl . TextRuns . Count ; runIndex + + )
{
@ -166,5 +120,117 @@ namespace Avalonia.Media.TextFormatting
currentPosition + = runLength ;
}
}
/// <summary>
/// Collects the break opportunities of the line, in ascending text source position order.
/// </summary>
/// <remarks>
/// <para>
/// The opportunities are searched in the line's text rather than in each run's text: a run
/// boundary is not a text boundary - a font fallback or a style change splits a run in the
/// middle of a sentence - while the line breaker reports a break at the end of whatever text
/// it is given. Searching run by run therefore invents a break at every run boundary, and
/// filtering those artefacts out drops the real break whenever a word gap falls there.
/// </para>
/// <para>
/// Trailing whitespace needs no special handling. The LineBreakEnumerator does not emit a
/// non-required break inside trailing whitespace (LB07 forbids breaking before a space, LB06
/// before a hard break), and the final break is excluded below - so no break opportunity ever
/// targets a glyph in the trailing whitespace. Verified for ASCII and ideographic (U+3000)
/// spaces by Justify_Does_Not_Space_Trailing_Whitespace.
/// </para>
/// </remarks>
private static void CollectBreakOpportunities ( TextLineImpl lineImpl , Queue < int > breakOportunities )
{
var textRuns = lineImpl . TextRuns ;
var currentPosition = lineImpl . FirstTextSourceIndex ;
var runIndex = 0 ;
while ( runIndex < textRuns . Count )
{
// Runs that carry no text (an embedded object, an end of line) interrupt the text, so
// each stretch of text runs between them is searched on its own.
var segmentStart = currentPosition ;
var segmentLength = 0 ;
var segmentEnd = runIndex ;
while ( segmentEnd < textRuns . Count )
{
var textRun = textRuns [ segmentEnd ] ;
// A run whose length doesn't match its text can't be mapped back to text source
// positions by offset, so it ends the segment instead of shifting everything
// after it.
if ( textRun . Text . IsEmpty | | textRun . Text . Length ! = textRun . Length )
{
break ;
}
segmentLength + = textRun . Length ;
+ + segmentEnd ;
}
if ( segmentLength = = 0 )
{
currentPosition + = textRuns [ runIndex ] . Length ;
+ + runIndex ;
continue ;
}
var buffer = ArrayPool < char > . Shared . Rent ( segmentLength ) ;
try
{
var offset = 0 ;
for ( var i = runIndex ; i < segmentEnd ; i + + )
{
var runText = textRuns [ i ] . Text ;
runText . Span . CopyTo ( buffer . AsSpan ( offset , runText . Length ) ) ;
offset + = runText . Length ;
}
var lineBreakEnumerator = new LineBreakEnumerator ( buffer . AsSpan ( 0 , segmentLength ) ) ;
while ( lineBreakEnumerator . MoveNext ( out var currentBreak ) )
{
if ( currentBreak . Required | | currentBreak . PositionWrap = = segmentLength )
{
continue ;
}
// The extra advance must land on the glyph that ENDS at the break boundary
// (the last glyph before the break), so the widened gap sits on the break
// itself. For whitespace breaks GetLineBreak has already pulled
// PositionMeasure back onto the trailing whitespace glyph
// (PositionMeasure < PositionWrap), so that position is the target as-is.
// For zero-width breaks - CJK/Korean ideograph boundaries, hyphens and other
// break-after punctuation - PositionMeasure == PositionWrap and points one
// glyph PAST the boundary; step back one so we widen the gap the break
// represents rather than the following gap. This also keeps the last visible
// glyph of a CJK/Korean line unstretched: its only inbound break is the
// segment-final one, already excluded above.
var target = segmentStart + currentBreak . PositionMeasure ;
if ( currentBreak . PositionMeasure = = currentBreak . PositionWrap )
{
target - = 1 ;
}
breakOportunities . Enqueue ( target ) ;
}
}
finally
{
ArrayPool < char > . Shared . Return ( buffer ) ;
}
currentPosition = segmentStart + segmentLength ;
runIndex = segmentEnd ;
}
}
}
}