Browse Source

[TextTrimming] Fixed some text trimming bugs (#17998)

* Trim text-runs in their logical order

* Revert breaking changes

* Added TODO12 comment

* Added test unit

* Use LogicalTextRunEnumerator

---------

Co-authored-by: Benedikt Stebner <Gillibald@users.noreply.github.com>
pull/18076/head
Compunet 1 year ago
committed by GitHub
parent
commit
84e62d67f9
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 75
      src/Avalonia.Base/Media/TextFormatting/LogicalTextRunEnumerator.cs
  2. 57
      src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs
  3. 169
      src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
  4. 7
      src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
  5. 39
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs

75
src/Avalonia.Base/Media/TextFormatting/LogicalTextRunEnumerator.cs

@ -0,0 +1,75 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace Avalonia.Media.TextFormatting;
internal ref struct LogicalTextRunEnumerator
{
private readonly IReadOnlyList<TextRun>? _textRuns;
private readonly IReadOnlyList<IndexedTextRun>? _indexedTextRuns;
private readonly int _step;
private readonly int _end;
private int _index;
public int Count { get; }
public LogicalTextRunEnumerator(TextLine line, bool backward = false)
{
var indexedTextRuns = (line as TextLineImpl)?._indexedTextRuns;
if (indexedTextRuns?.Count > 0)
{
_indexedTextRuns = indexedTextRuns;
Count = indexedTextRuns.Count;
}
else if (line.TextRuns.Count > 0)
{
_textRuns = line.TextRuns;
Count = _textRuns.Count;
}
if (backward)
{
_step = -1;
_end = -1;
_index = Count;
}
else
{
_step = 1;
_end = Count;
_index = -1;
}
}
public bool MoveNext([MaybeNullWhen(false)] out TextRun run)
{
_index += _step;
if (_index == _end)
{
run = null;
return false;
}
if (_indexedTextRuns != null)
{
run = _indexedTextRuns[_index].TextRun!;
}
else if (_textRuns != null)
{
run = _textRuns[0];
}
else
{
run = null;
return false;
}
return true;
}
}

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

@ -1,6 +1,4 @@
using System.Collections.Generic;
namespace Avalonia.Media.TextFormatting
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// Properties of text collapsing.
@ -28,6 +26,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="textLine">Text line to collapse.</param>
public abstract TextRun[]? Collapse(TextLine textLine);
// TODO12: Remove the flowDirection parameter
/// <summary>
/// Creates a list of runs for given collapsed length which includes specified symbol at the end.
/// </summary>
@ -36,44 +35,50 @@ namespace Avalonia.Media.TextFormatting
/// <param name="flowDirection">The flow direction.</param>
/// <param name="shapedSymbol">The symbol.</param>
/// <returns>List of remaining runs.</returns>
public static TextRun[] CreateCollapsedRuns(TextLine textLine, int collapsedLength,
FlowDirection flowDirection, TextRun shapedSymbol)
public 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;
return [shapedSymbol];
}
var objectPool = FormattingObjectPool.Instance;
var (preSplitRuns, postSplitRuns) = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength, objectPool);
FormattingObjectPool.RentedList<TextRun>? preSplitRuns = null;
FormattingObjectPool.RentedList<TextRun>? postSplitRuns = null;
var textRuns = objectPool.TextRunLists.Rent();
try
{
if (flowDirection == FlowDirection.RightToLeft)
{
var collapsedRuns = new TextRun[postSplitRuns!.Count + 1];
postSplitRuns.CopyTo(collapsedRuns, 1);
collapsedRuns[0] = shapedSymbol;
return collapsedRuns;
}
else
var textRunEnumerator = new LogicalTextRunEnumerator(textLine);
var textRunsLength = 0;
while (textRunEnumerator.MoveNext(out var textRun))
{
var collapsedRuns = new TextRun[preSplitRuns!.Count + 1];
preSplitRuns.CopyTo(collapsedRuns);
collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol;
return collapsedRuns;
if (textRunsLength >= collapsedLength)
{
break;
}
textRunsLength += textRun.Length;
textRuns.Add(textRun);
}
(preSplitRuns, postSplitRuns) = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength, objectPool);
var collapsedRuns = new TextRun[preSplitRuns!.Count + 1];
preSplitRuns.CopyTo(collapsedRuns);
collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol;
return collapsedRuns;
}
finally
{
objectPool.TextRunLists.Return(ref textRuns);
objectPool.TextRunLists.Return(ref preSplitRuns);
objectPool.TextRunLists.Return(ref postSplitRuns);
}

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

@ -6,17 +6,14 @@ namespace Avalonia.Media.TextFormatting
{
public static TextRun[]? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis)
{
var textRuns = textLine.TextRuns;
var textRunsEnumerator = new LogicalTextRunEnumerator(textLine);
if (textRuns.Count == 0)
if (textRunsEnumerator.Count == 0)
{
return null;
}
var runIndex = 0;
var currentWidth = 0.0;
var collapsedLength = 0;
var shapedSymbol = TextFormatter.CreateSymbol(properties.Symbol, FlowDirection.LeftToRight);
var shapedSymbol = TextFormatter.CreateSymbol(properties.Symbol, properties.FlowDirection);
if (properties.Width < shapedSymbol.GlyphRun.Bounds.Width)
{
@ -26,153 +23,79 @@ namespace Avalonia.Media.TextFormatting
var availableWidth = properties.Width - shapedSymbol.Size.Width;
if(properties.FlowDirection== FlowDirection.LeftToRight)
var collapsedLength = 0;
while (textRunsEnumerator.MoveNext(out var currentRun))
{
while (runIndex < textRuns.Count)
switch (currentRun)
{
var currentRun = textRuns[runIndex];
case ShapedTextRun shapedRun:
{
var textRunWidth = shapedRun.Size.Width;
switch (currentRun)
{
case ShapedTextRun shapedRun:
if (textRunWidth > availableWidth)
{
currentWidth += shapedRun.Size.Width;
if (currentWidth > availableWidth)
if (shapedRun.IsReversed)
{
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 TextCollapsingProperties.CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.LeftToRight, shapedSymbol);
shapedRun.Reverse();
}
availableWidth -= shapedRun.Size.Width;
break;
}
case DrawableTextRun drawableRun:
{
//The whole run needs to fit into available space
if (currentWidth + drawableRun.Size.Width > availableWidth)
if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength))
{
return TextCollapsingProperties.CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.LeftToRight, shapedSymbol);
}
availableWidth -= drawableRun.Size.Width;
break;
}
}
collapsedLength += currentRun.Length;
runIndex++;
}
}
else
{
runIndex = textRuns.Count - 1;
while (runIndex >= 0)
{
var currentRun = textRuns[runIndex];
if (isWordEllipsis && measuredLength < textLine.Length)
{
var currentBreakPosition = 0;
switch (currentRun)
{
case ShapedTextRun shapedRun:
{
currentWidth += shapedRun.Size.Width;
var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span);
if (currentWidth > availableWidth)
{
if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength))
{
if (isWordEllipsis && measuredLength < textLine.Length)
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext(out var lineBreak))
{
var currentBreakPosition = 0;
var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span);
var nextBreakPosition = lineBreak.PositionMeasure;
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext(out var lineBreak))
if (nextBreakPosition == 0)
{
var nextBreakPosition = lineBreak.PositionMeasure;
if (nextBreakPosition == 0)
{
break;
}
if (nextBreakPosition >= measuredLength)
{
break;
}
break;
}
currentBreakPosition = nextBreakPosition;
if (nextBreakPosition >= measuredLength)
{
break;
}
measuredLength = currentBreakPosition;
currentBreakPosition = nextBreakPosition;
}
}
collapsedLength += measuredLength;
return TextCollapsingProperties.CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.RightToLeft, shapedSymbol);
measuredLength = currentBreakPosition;
}
}
availableWidth -= shapedRun.Size.Width;
collapsedLength += measuredLength;
break;
return TextCollapsingProperties.CreateCollapsedRuns(textLine, collapsedLength, properties.FlowDirection, shapedSymbol);
}
case DrawableTextRun drawableRun:
{
//The whole run needs to fit into available space
if (currentWidth + drawableRun.Size.Width > availableWidth)
{
return TextCollapsingProperties.CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.RightToLeft, shapedSymbol);
}
availableWidth -= textRunWidth;
availableWidth -= drawableRun.Size.Width;
break;
}
break;
case DrawableTextRun drawableRun:
{
//The whole run needs to fit into available space
if (drawableRun.Size.Width > availableWidth)
{
return TextCollapsingProperties.CreateCollapsedRuns(textLine, collapsedLength, properties.FlowDirection, shapedSymbol);
}
}
collapsedLength += currentRun.Length;
availableWidth -= drawableRun.Size.Width;
runIndex--;
break;
}
}
collapsedLength += currentRun.Length;
}
return null;
}
}

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

@ -51,14 +51,9 @@ namespace Avalonia.Media.TextFormatting
{
var textRuns = textLine.TextRuns;
if (textRuns.Count == 0)
{
return null;
}
var runIndex = 0;
var currentWidth = 0.0;
var shapedSymbol = TextFormatterImpl.CreateSymbol(Symbol, FlowDirection.LeftToRight);
var shapedSymbol = TextFormatter.CreateSymbol(Symbol, FlowDirection.LeftToRight);
if (Width < shapedSymbol.GlyphRun.Bounds.Width)
{

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

@ -409,6 +409,45 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
[Theory]
[InlineData("one שתיים three ארבע", "one שתיים thr…", FlowDirection.LeftToRight, false)]
[InlineData("one שתיים three ארבע", "…thrשתיים one", FlowDirection.RightToLeft, false)]
[InlineData("one שתיים three ארבע", "one שתיים…", FlowDirection.LeftToRight, true)]
[InlineData("one שתיים three ארבע", "…שתיים one", FlowDirection.RightToLeft, true)]
public void TextTrimming_Should_Trim_Correctly(string text, string trimmed, FlowDirection direction, bool wordEllipsis)
{
const double Width = 160.0;
const double EmSize = 20.0;
using (Start())
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default, EmSize);
var paragraphProperties = new GenericTextParagraphProperties(direction, TextAlignment.Start, true,
true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0);
var textSource = new SimpleTextSource(text, defaultProperties);
var formatter = new TextFormatterImpl();
var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProperties);
Assert.NotNull(textLine);
var textTrimming = wordEllipsis ? TextTrimming.WordEllipsis : TextTrimming.CharacterEllipsis;
var collapsingProperties = textTrimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(Width, defaultProperties, direction));
var collapsedLine = textLine.Collapse(collapsingProperties);
Assert.NotNull(collapsedLine);
var trimmedResult = string.Concat(collapsedLine.TextRuns.Select(x => x.Text));
Assert.Equal(trimmed, trimmedResult);
}
}
[InlineData("Whether to turn off HTTPS. This option only applies if Individual, " +
"IndividualB2C, SingleOrg, or MultiOrg aren't used for &#8209;&#8209;auth."
, "Noto Sans", 40)]

Loading…
Cancel
Save