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>
release/11.2.4
Compunet 2 years ago
committed by Max Katz
parent
commit
6734108322
  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> /// <summary>
/// Properties of text collapsing. /// Properties of text collapsing.
@ -28,6 +26,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="textLine">Text line to collapse.</param> /// <param name="textLine">Text line to collapse.</param>
public abstract TextRun[]? Collapse(TextLine textLine); public abstract TextRun[]? Collapse(TextLine textLine);
// TODO12: Remove the flowDirection parameter
/// <summary> /// <summary>
/// Creates a list of runs for given collapsed length which includes specified symbol at the end. /// Creates a list of runs for given collapsed length which includes specified symbol at the end.
/// </summary> /// </summary>
@ -36,44 +35,50 @@ namespace Avalonia.Media.TextFormatting
/// <param name="flowDirection">The flow direction.</param> /// <param name="flowDirection">The flow direction.</param>
/// <param name="shapedSymbol">The symbol.</param> /// <param name="shapedSymbol">The symbol.</param>
/// <returns>List of remaining runs.</returns> /// <returns>List of remaining runs.</returns>
public static TextRun[] CreateCollapsedRuns(TextLine textLine, int collapsedLength, public static TextRun[] CreateCollapsedRuns(TextLine textLine, int collapsedLength, FlowDirection flowDirection, TextRun shapedSymbol)
FlowDirection flowDirection, TextRun shapedSymbol)
{ {
var textRuns = textLine.TextRuns;
if (collapsedLength <= 0) if (collapsedLength <= 0)
{ {
return new[] { shapedSymbol }; return [shapedSymbol];
}
if (flowDirection == FlowDirection.RightToLeft)
{
collapsedLength = textLine.Length - collapsedLength;
} }
var objectPool = FormattingObjectPool.Instance; 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 try
{ {
if (flowDirection == FlowDirection.RightToLeft) var textRunEnumerator = new LogicalTextRunEnumerator(textLine);
{
var collapsedRuns = new TextRun[postSplitRuns!.Count + 1]; var textRunsLength = 0;
postSplitRuns.CopyTo(collapsedRuns, 1);
collapsedRuns[0] = shapedSymbol; while (textRunEnumerator.MoveNext(out var textRun))
return collapsedRuns;
}
else
{ {
var collapsedRuns = new TextRun[preSplitRuns!.Count + 1]; if (textRunsLength >= collapsedLength)
preSplitRuns.CopyTo(collapsedRuns); {
collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol; break;
return collapsedRuns; }
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 finally
{ {
objectPool.TextRunLists.Return(ref textRuns);
objectPool.TextRunLists.Return(ref preSplitRuns); objectPool.TextRunLists.Return(ref preSplitRuns);
objectPool.TextRunLists.Return(ref postSplitRuns); 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) 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; return null;
} }
var runIndex = 0; var shapedSymbol = TextFormatter.CreateSymbol(properties.Symbol, properties.FlowDirection);
var currentWidth = 0.0;
var collapsedLength = 0;
var shapedSymbol = TextFormatter.CreateSymbol(properties.Symbol, FlowDirection.LeftToRight);
if (properties.Width < shapedSymbol.GlyphRun.Bounds.Width) if (properties.Width < shapedSymbol.GlyphRun.Bounds.Width)
{ {
@ -26,153 +23,79 @@ namespace Avalonia.Media.TextFormatting
var availableWidth = properties.Width - shapedSymbol.Size.Width; 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) if (textRunWidth > availableWidth)
{
case ShapedTextRun shapedRun:
{ {
currentWidth += shapedRun.Size.Width; if (shapedRun.IsReversed)
if (currentWidth > availableWidth)
{ {
if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength)) shapedRun.Reverse();
{
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);
} }
availableWidth -= shapedRun.Size.Width; if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength))
break;
}
case DrawableTextRun drawableRun:
{
//The whole run needs to fit into available space
if (currentWidth + drawableRun.Size.Width > availableWidth)
{ {
return TextCollapsingProperties.CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.LeftToRight, shapedSymbol); if (isWordEllipsis && measuredLength < textLine.Length)
} {
var currentBreakPosition = 0;
availableWidth -= drawableRun.Size.Width;
break;
}
}
collapsedLength += currentRun.Length;
runIndex++;
}
}
else
{
runIndex = textRuns.Count - 1;
while (runIndex >= 0)
{
var currentRun = textRuns[runIndex];
switch (currentRun) var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span);
{
case ShapedTextRun shapedRun:
{
currentWidth += shapedRun.Size.Width;
if (currentWidth > availableWidth) while (currentBreakPosition < measuredLength && lineBreaker.MoveNext(out var lineBreak))
{
if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength))
{
if (isWordEllipsis && measuredLength < textLine.Length)
{ {
var currentBreakPosition = 0; var nextBreakPosition = lineBreak.PositionMeasure;
var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span);
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext(out var lineBreak)) if (nextBreakPosition == 0)
{ {
var nextBreakPosition = lineBreak.PositionMeasure; break;
}
if (nextBreakPosition == 0)
{
break;
}
if (nextBreakPosition >= measuredLength)
{
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: availableWidth -= textRunWidth;
{
//The whole run needs to fit into available space
if (currentWidth + drawableRun.Size.Width > availableWidth)
{
return TextCollapsingProperties.CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.RightToLeft, shapedSymbol);
}
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; return null;
} }
} }

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

@ -51,14 +51,9 @@ namespace Avalonia.Media.TextFormatting
{ {
var textRuns = textLine.TextRuns; var textRuns = textLine.TextRuns;
if (textRuns.Count == 0)
{
return null;
}
var runIndex = 0; var runIndex = 0;
var currentWidth = 0.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) if (Width < shapedSymbol.GlyphRun.Bounds.Width)
{ {

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

@ -328,6 +328,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, " + [InlineData("Whether to turn off HTTPS. This option only applies if Individual, " +
"IndividualB2C, SingleOrg, or MultiOrg aren't used for &#8209;&#8209;auth." "IndividualB2C, SingleOrg, or MultiOrg aren't used for &#8209;&#8209;auth."
, "Noto Sans", 40)] , "Noto Sans", 40)]

Loading…
Cancel
Save