Browse Source

Remove trailing whitespace for right aligned text

Fix justification for non wrapped text
Fix text trimming for RTL flow direction
pull/10408/head
Benedikt Stebner 3 years ago
parent
commit
1ecdf32520
  1. 2
      src/Avalonia.Base/Media/FormattedText.cs
  2. 4
      src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs
  3. 13
      src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs
  4. 21
      src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs
  5. 9
      src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs
  6. 191
      src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
  7. 25
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  8. 7
      src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
  9. 47
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  10. 7
      src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
  11. 7
      src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs
  12. 2
      src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs
  13. 4
      src/Avalonia.Base/Media/TextTrailingTrimming.cs
  14. 2
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
  15. 2
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

2
src/Avalonia.Base/Media/FormattedText.cs

@ -877,7 +877,7 @@ namespace Avalonia.Media
var lastRunProps = (GenericTextRunProperties)thatFormatRider.CurrentElement!;
TextCollapsingProperties collapsingProperties = _that._trimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(maxLineLength, lastRunProps));
TextCollapsingProperties collapsingProperties = _that._trimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(maxLineLength, lastRunProps, paraProps.FlowDirection));
var collapsedLine = line.Collapse(collapsingProperties);

4
src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs

@ -6,11 +6,13 @@ namespace Avalonia.Media
{
public readonly double Width;
public readonly TextRunProperties TextRunProperties;
public readonly FlowDirection FlowDirection;
public TextCollapsingCreateInfo(double width, TextRunProperties textRunProperties)
public TextCollapsingCreateInfo(double width, TextRunProperties textRunProperties, FlowDirection flowDirection)
{
Width = width;
TextRunProperties = textRunProperties;
FlowDirection = flowDirection;
}
}
}

13
src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs

@ -27,16 +27,6 @@ namespace Avalonia.Media.TextFormatting
return;
}
if (lineImpl.NewLineLength > 0)
{
return;
}
if (lineImpl.TextLineBreak is { TextEndOfLine: not null, IsSplit: false })
{
return;
}
var breakOportunities = new Queue<int>();
var currentPosition = textLine.FirstTextSourceIndex;
@ -97,7 +87,8 @@ namespace Avalonia.Media.TextFormatting
continue;
}
var glyphIndex = glyphRun.FindGlyphIndex(characterIndex);
var offset = Math.Max(0, currentPosition - glyphRun.Metrics.FirstCluster);
var glyphIndex = glyphRun.FindGlyphIndex(characterIndex - offset);
var glyphInfo = shapedBuffer.GlyphInfos[glyphIndex];
shapedBuffer.GlyphInfos[glyphIndex] = new GlyphInfo(glyphInfo.GlyphIndex,

21
src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs

@ -148,33 +148,38 @@ namespace Avalonia.Media.TextFormatting
internal SplitResult<ShapedTextRun> Split(int length)
{
if (IsReversed)
var isReversed = IsReversed;
if (isReversed)
{
Reverse();
}
length = Length - length;
}
#if DEBUG
if(length == 0)
if (length == 0)
{
throw new ArgumentOutOfRangeException(nameof(length), "length must be greater than zero.");
}
#endif
#endif
var splitBuffer = ShapedBuffer.Split(length);
var first = new ShapedTextRun(splitBuffer.First, Properties);
#if DEBUG
#if DEBUG
if (first.Length != length)
{
throw new InvalidOperationException("Split length mismatch.");
}
#endif
var second = new ShapedTextRun(splitBuffer.Second!, Properties);
if (isReversed)
{
return new SplitResult<ShapedTextRun>(second, first);
}
return new SplitResult<ShapedTextRun>(first, second);
}

9
src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs

@ -1,4 +1,6 @@
namespace Avalonia.Media.TextFormatting
using System.Collections.Generic;
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// Properties of text collapsing.
@ -15,6 +17,11 @@
/// </summary>
public abstract TextRun Symbol { get; }
/// <summary>
/// Gets the flow direction that is used for collapsing.
/// </summary>
public abstract FlowDirection FlowDirection { get; }
/// <summary>
/// Collapses given text line.
/// </summary>

191
src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using Avalonia.Media.TextFormatting.Unicode;
namespace Avalonia.Media.TextFormatting
@ -28,97 +27,191 @@ namespace Avalonia.Media.TextFormatting
var availableWidth = properties.Width - shapedSymbol.Size.Width;
while (runIndex < textRuns.Count)
if(properties.FlowDirection== FlowDirection.LeftToRight)
{
var currentRun = textRuns[runIndex];
switch (currentRun)
while (runIndex < textRuns.Count)
{
case ShapedTextRun shapedRun:
{
currentWidth += shapedRun.Size.Width;
var currentRun = textRuns[runIndex];
if (currentWidth > availableWidth)
switch (currentRun)
{
case ShapedTextRun shapedRun:
{
if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength))
currentWidth += shapedRun.Size.Width;
if (currentWidth > availableWidth)
{
if (isWordEllipsis && measuredLength < textLine.Length)
if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength))
{
var currentBreakPosition = 0;
var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span);
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext(out var lineBreak))
if (isWordEllipsis && measuredLength < textLine.Length)
{
var nextBreakPosition = lineBreak.PositionMeasure;
var currentBreakPosition = 0;
if (nextBreakPosition == 0)
{
break;
}
var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span);
if (nextBreakPosition >= measuredLength)
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext(out var lineBreak))
{
break;
var nextBreakPosition = lineBreak.PositionMeasure;
if (nextBreakPosition == 0)
{
break;
}
if (nextBreakPosition >= measuredLength)
{
break;
}
currentBreakPosition = nextBreakPosition;
}
currentBreakPosition = nextBreakPosition;
measuredLength = currentBreakPosition;
}
measuredLength = currentBreakPosition;
}
collapsedLength += measuredLength;
return CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.LeftToRight, shapedSymbol);
}
collapsedLength += measuredLength;
availableWidth -= shapedRun.Size.Width;
return CreateCollapsedRuns(textRuns, collapsedLength, shapedSymbol);
break;
}
availableWidth -= shapedRun.Size.Width;
case DrawableTextRun drawableRun:
{
//The whole run needs to fit into available space
if (currentWidth + drawableRun.Size.Width > availableWidth)
{
return CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.LeftToRight, shapedSymbol);
}
break;
}
availableWidth -= drawableRun.Size.Width;
case DrawableTextRun drawableRun:
{
//The whole run needs to fit into available space
if (currentWidth + drawableRun.Size.Width > availableWidth)
{
return CreateCollapsedRuns(textRuns, collapsedLength, shapedSymbol);
break;
}
}
availableWidth -= drawableRun.Size.Width;
collapsedLength += currentRun.Length;
break;
}
runIndex++;
}
}
else
{
runIndex = textRuns.Count - 1;
while (runIndex >= 0)
{
var currentRun = textRuns[runIndex];
collapsedLength += currentRun.Length;
switch (currentRun)
{
case ShapedTextRun shapedRun:
{
currentWidth += shapedRun.Size.Width;
runIndex++;
}
if (currentWidth > availableWidth)
{
if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength))
{
if (isWordEllipsis && measuredLength < textLine.Length)
{
var currentBreakPosition = 0;
var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span);
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext(out var lineBreak))
{
var nextBreakPosition = lineBreak.PositionMeasure;
if (nextBreakPosition == 0)
{
break;
}
if (nextBreakPosition >= measuredLength)
{
break;
}
currentBreakPosition = nextBreakPosition;
}
measuredLength = currentBreakPosition;
}
}
collapsedLength += measuredLength;
return CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.RightToLeft, shapedSymbol);
}
availableWidth -= shapedRun.Size.Width;
break;
}
case DrawableTextRun drawableRun:
{
//The whole run needs to fit into available space
if (currentWidth + drawableRun.Size.Width > availableWidth)
{
return CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.RightToLeft, shapedSymbol);
}
availableWidth -= drawableRun.Size.Width;
break;
}
}
collapsedLength += currentRun.Length;
runIndex--;
}
}
return null;
}
private static TextRun[] CreateCollapsedRuns(IReadOnlyList<TextRun> textRuns, int collapsedLength,
TextRun shapedSymbol)
private static TextRun[] CreateCollapsedRuns(TextLine textLine, int collapsedLength,
FlowDirection flowDirection, TextRun shapedSymbol)
{
var textRuns = textLine.TextRuns;
if (collapsedLength <= 0)
{
return new[] { shapedSymbol };
}
if(flowDirection == FlowDirection.RightToLeft)
{
collapsedLength = textLine.Length - collapsedLength;
}
var objectPool = FormattingObjectPool.Instance;
var (preSplitRuns, postSplitRuns) = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength, objectPool);
try
{
var collapsedRuns = new TextRun[preSplitRuns.Count + 1];
preSplitRuns.CopyTo(collapsedRuns);
collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol;
return collapsedRuns;
if (flowDirection == FlowDirection.RightToLeft)
{
var collapsedRuns = new TextRun[postSplitRuns!.Count + 1];
postSplitRuns.CopyTo(collapsedRuns, 1);
collapsedRuns[0] = shapedSymbol;
return collapsedRuns;
}
else
{
var collapsedRuns = new TextRun[preSplitRuns!.Count + 1];
preSplitRuns.CopyTo(collapsedRuns);
collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol;
return collapsedRuns;
}
}
finally
{

25
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

@ -352,7 +352,7 @@ namespace Avalonia.Media.TextFormatting
var lastTrailingIndex = 0;
if(_paragraphProperties.FlowDirection== FlowDirection.LeftToRight)
if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight)
{
lastTrailingIndex = textLine.FirstTextSourceIndex + textLine.Length;
@ -377,7 +377,7 @@ namespace Avalonia.Media.TextFormatting
{
lastTrailingIndex += textEndOfLine.Length;
}
}
}
var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
@ -553,26 +553,18 @@ namespace Avalonia.Media.TextFormatting
if (_paragraphProperties.TextAlignment == TextAlignment.Justify)
{
var whitespaceWidth = 0d;
var justificationWidth = MaxWidth;
for (var i = 0; i < textLines.Count; i++)
if (_paragraphProperties.TextWrapping != TextWrapping.NoWrap)
{
var line = textLines[i];
var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace;
if (lineWhitespaceWidth > whitespaceWidth)
{
whitespaceWidth = lineWhitespaceWidth;
}
justificationWidth = width;
}
var justificationWidth = width - whitespaceWidth;
if (justificationWidth > 0)
{
var justificationProperties = new InterWordJustification(justificationWidth);
for (var i = 0; i < textLines.Count - 1; i++)
for (var i = 0; i < textLines.Count; i++)
{
var line = textLines[i];
@ -597,12 +589,13 @@ namespace Avalonia.Media.TextFormatting
/// <returns>The <see cref="TextCollapsingProperties"/>.</returns>
private TextCollapsingProperties? GetCollapsingProperties(double width)
{
if(_textTrimming == TextTrimming.None)
if (_textTrimming == TextTrimming.None)
{
return null;
}
return _textTrimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(width, _paragraphProperties.DefaultTextRunProperties));
return _textTrimming.CreateCollapsingProperties(
new TextCollapsingCreateInfo(width, _paragraphProperties.DefaultTextRunProperties, _paragraphProperties.FlowDirection));
}
public void Dispose()

7
src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs

@ -19,11 +19,13 @@ namespace Avalonia.Media.TextFormatting
/// <param name="prefixLength">Length of leading prefix.</param>
/// <param name="width">width in which collapsing is constrained to</param>
/// <param name="textRunProperties">text run properties of ellipsis symbol</param>
/// <param name="flowDirection">the flow direction of the collapes line.</param>
public TextLeadingPrefixCharacterEllipsis(
string ellipsis,
int prefixLength,
double width,
TextRunProperties textRunProperties)
TextRunProperties textRunProperties,
FlowDirection flowDirection)
{
if (_prefixLength < 0)
{
@ -33,6 +35,7 @@ namespace Avalonia.Media.TextFormatting
_prefixLength = prefixLength;
Width = width;
Symbol = new TextCharacters(ellipsis, textRunProperties);
FlowDirection = flowDirection;
}
/// <inheritdoc/>
@ -41,6 +44,8 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/>
public override TextRun Symbol { get; }
public override FlowDirection FlowDirection { get; }
/// <inheritdoc />
public override TextRun[]? Collapse(TextLine textLine)
{

47
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@ -658,7 +658,7 @@ namespace Avalonia.Media.TextFormatting
currentX += drawableTextRun.Size.Width;
}
if(lastRunIndex - 1 < 0)
if (lastRunIndex - 1 < 0)
{
break;
}
@ -685,7 +685,7 @@ namespace Avalonia.Media.TextFormatting
directionalWidth -= drawableTextRun.Size.Width;
}
if(firstRunIndex + 1 == _textRuns.Length)
if (firstRunIndex + 1 == _textRuns.Length)
{
break;
}
@ -1097,7 +1097,7 @@ namespace Avalonia.Media.TextFormatting
var runWidth = endX - startX;
return new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
return new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
}
public override void Dispose()
@ -1439,13 +1439,6 @@ namespace Avalonia.Media.TextFormatting
}
}
if (index == lastRunIndex)
{
width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width;
trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength;
newLineLength += textRun.GlyphRun.Metrics.NewLineLength;
}
widthIncludingWhitespace += textRun.Size.Width;
break;
@ -1455,12 +1448,6 @@ namespace Avalonia.Media.TextFormatting
{
widthIncludingWhitespace += drawableTextRun.Size.Width;
if (index == lastRunIndex)
{
width = widthIncludingWhitespace;
trailingWhitespaceLength = 0;
}
if (drawableTextRun.Size.Height > height)
{
height = drawableTextRun.Size.Height;
@ -1476,6 +1463,32 @@ namespace Avalonia.Media.TextFormatting
}
}
width = widthIncludingWhitespace;
for (var i = _textRuns.Length - 1; i >= 0; i--)
{
var currentRun = _textRuns[i];
if(currentRun is ShapedTextRun shapedText)
{
var glyphRun = shapedText.GlyphRun;
var glyphRunMetrics = glyphRun.Metrics;
newLineLength += glyphRunMetrics.NewLineLength;
if (glyphRunMetrics.TrailingWhitespaceLength == 0)
{
break;
}
trailingWhitespaceLength += glyphRunMetrics.TrailingWhitespaceLength;
var whitespaceWidth = glyphRun.Size.Width - glyphRunMetrics.Width;
width -= whitespaceWidth;
}
}
var start = GetParagraphOffsetX(width, widthIncludingWhitespace);
if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight))
@ -1543,7 +1556,7 @@ namespace Avalonia.Media.TextFormatting
return Math.Max(0, start);
case TextAlignment.Right:
return Math.Max(0, _paragraphWidth - widthIncludingTrailingWhitespace);
return Math.Max(0, _paragraphWidth - width);
default:
return 0;

7
src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs

@ -12,10 +12,13 @@
/// <param name="ellipsis">Text used as collapsing symbol.</param>
/// <param name="width">Width in which collapsing is constrained to.</param>
/// <param name="textRunProperties">Text run properties of ellipsis symbol.</param>
public TextTrailingCharacterEllipsis(string ellipsis, double width, TextRunProperties textRunProperties)
/// <param name="flowDirection">The flow direction of the collapsed line.</param>
public TextTrailingCharacterEllipsis(string ellipsis, double width,
TextRunProperties textRunProperties, FlowDirection flowDirection)
{
Width = width;
Symbol = new TextCharacters(ellipsis, textRunProperties);
FlowDirection = flowDirection;
}
/// <inheritdoc/>
@ -24,6 +27,8 @@
/// <inheritdoc/>
public override TextRun Symbol { get; }
public override FlowDirection FlowDirection { get; }
/// <inheritdoc />
public override TextRun[]? Collapse(TextLine textLine)
{

7
src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs

@ -12,14 +12,17 @@
/// <param name="ellipsis">Text used as collapsing symbol.</param>
/// <param name="width">width in which collapsing is constrained to.</param>
/// <param name="textRunProperties">text run properties of ellipsis symbol.</param>
/// <param name="flowDirection">flow direction of the collapsed line.</param>
public TextTrailingWordEllipsis(
string ellipsis,
double width,
TextRunProperties textRunProperties
TextRunProperties textRunProperties,
FlowDirection flowDirection
)
{
Width = width;
Symbol = new TextCharacters(ellipsis, textRunProperties);
FlowDirection = flowDirection;
}
/// <inheritdoc/>
@ -28,6 +31,8 @@
/// <inheritdoc/>
public override TextRun Symbol { get; }
public override FlowDirection FlowDirection { get; }
/// <inheritdoc />
public override TextRun[]? Collapse(TextLine textLine)
{

2
src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs

@ -15,7 +15,7 @@ namespace Avalonia.Media
public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo)
{
return new TextLeadingPrefixCharacterEllipsis(_ellipsis, _prefixLength, createInfo.Width, createInfo.TextRunProperties);
return new TextLeadingPrefixCharacterEllipsis(_ellipsis, _prefixLength, createInfo.Width, createInfo.TextRunProperties, createInfo.FlowDirection);
}
public override string ToString()

4
src/Avalonia.Base/Media/TextTrailingTrimming.cs

@ -17,10 +17,10 @@ namespace Avalonia.Media
{
if (_isWordBased)
{
return new TextTrailingWordEllipsis(_ellipsis, createInfo.Width, createInfo.TextRunProperties);
return new TextTrailingWordEllipsis(_ellipsis, createInfo.Width, createInfo.TextRunProperties, createInfo.FlowDirection);
}
return new TextTrailingCharacterEllipsis(_ellipsis, createInfo.Width, createInfo.TextRunProperties);
return new TextTrailingCharacterEllipsis(_ellipsis, createInfo.Width, createInfo.TextRunProperties, createInfo.FlowDirection);
}
public override string ToString()

2
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs

@ -457,7 +457,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
if (textLine.Width > 300 || currentHeight + textLine.Height > 240)
{
textLine = textLine.Collapse(new TextTrailingWordEllipsis(TextTrimming.DefaultEllipsisChar, 300, defaultProperties));
textLine = textLine.Collapse(new TextTrailingWordEllipsis(TextTrimming.DefaultEllipsisChar, 300, defaultProperties, FlowDirection.LeftToRight));
}
currentHeight += textLine.Height;

2
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

@ -407,7 +407,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.False(textLine.HasCollapsed);
TextCollapsingProperties collapsingProperties = trimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(width, defaultProperties));
TextCollapsingProperties collapsingProperties = trimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(width, defaultProperties, FlowDirection.LeftToRight));
var collapsedLine = textLine.Collapse(collapsingProperties);

Loading…
Cancel
Save