Browse Source

Reset bidi levels of trailing whitespace after text wrapping (According to TR9 guidelines) (#17924)

* Reset bidi levels of trailing whitespaces to paragraph embedding level after text wrapping

* Added unit tests
pull/17948/head
Compunet 1 year ago
committed by GitHub
parent
commit
7071c7a8d6
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs
  2. 85
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  3. 81
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs

4
src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs

@ -49,7 +49,7 @@ namespace Avalonia.Media.TextFormatting
/// <summary>
/// The buffer's bidi level.
/// </summary>
public sbyte BidiLevel { get; }
public sbyte BidiLevel { get; private set; }
/// <summary>
/// The buffer's reading direction.
@ -169,6 +169,8 @@ namespace Avalonia.Media.TextFormatting
return new SplitResult<ShapedBuffer>(first, second);
}
internal void ResetBidiLevel(sbyte paragraphEmbeddingLevel) => BidiLevel = paragraphEmbeddingLevel;
int IReadOnlyCollection<GlyphInfo>.Count => _glyphInfos.Length;
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

85
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@ -908,6 +908,11 @@ namespace Avalonia.Media.TextFormatting
textLineBreak = null;
}
if (postSplitRuns?.Count > 0)
{
ResetTrailingWhitespaceBidiLevels(preSplitRuns, paragraphProperties.FlowDirection, objectPool);
}
var textLine = new TextLineImpl(preSplitRuns.ToArray(), firstTextSourceIndex, measuredLength,
paragraphWidth, paragraphProperties, resolvedFlowDirection,
textLineBreak);
@ -923,6 +928,86 @@ namespace Avalonia.Media.TextFormatting
}
}
private static void ResetTrailingWhitespaceBidiLevels(RentedList<TextRun> lineTextRuns, FlowDirection paragraphFlowDirection, FormattingObjectPool objectPool)
{
if (lineTextRuns.Count == 0)
{
return;
}
var lastTextRunIndex = lineTextRuns.Count - 1;
var lastTextRun = lineTextRuns[lastTextRunIndex];
if (lastTextRun is not ShapedTextRun shapedText)
{
return;
}
var paragraphEmbeddingLevel = (sbyte)paragraphFlowDirection;
if (shapedText.BidiLevel == paragraphEmbeddingLevel)
{
return;
}
var textSpan = shapedText.Text.Span;
if (textSpan.IsEmpty)
{
return;
}
var whitespaceCharactersCount = 0;
for (var i = textSpan.Length - 1; i >= 0; i--)
{
var isWhitespace = Codepoint.ReadAt(textSpan, i, out _).IsWhiteSpace;
if (isWhitespace)
{
whitespaceCharactersCount++;
}
else
{
break;
}
}
if (whitespaceCharactersCount == 0)
{
return;
}
var splitIndex = shapedText.Length - whitespaceCharactersCount;
var (textRuns, trailingWhitespaceRuns) = SplitTextRuns([shapedText], splitIndex, objectPool);
try
{
if (trailingWhitespaceRuns != null)
{
for (var i = 0; i < trailingWhitespaceRuns.Count; i++)
{
if (trailingWhitespaceRuns[i] is ShapedTextRun shapedTextRun)
{
shapedTextRun.ShapedBuffer.ResetBidiLevel(paragraphEmbeddingLevel);
}
}
lineTextRuns.RemoveAt(lastTextRunIndex);
lineTextRuns.AddRange(textRuns);
lineTextRuns.AddRange(trailingWhitespaceRuns);
}
}
finally
{
objectPool.TextRunLists.Return(ref textRuns);
objectPool.TextRunLists.Return(ref trailingWhitespaceRuns);
}
}
private struct TextRunEnumerator
{
private readonly ITextSource _textSource;

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

@ -168,6 +168,87 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
[Fact]
public void Should_Reset_Bidi_Levels_Of_Trailing_Whitespaces_After_TextWrapping()
{
using (Start())
{
const string text = "aaa bbb";
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var paragraphProperties = new GenericTextParagraphProperties(FlowDirection.RightToLeft, TextAlignment.Right, true,
true, defaultProperties, TextWrapping.Wrap, 0, 0, 0);
var textSource = new SimpleTextSource(text, defaultProperties);
var formatter = new TextFormatterImpl();
var firstLine = formatter.FormatLine(textSource, 0, 50, paragraphProperties);
Assert.NotNull(firstLine);
Assert.Equal(2, firstLine.TextRuns.Count);
var first = firstLine.TextRuns[0] as ShapedTextRun;
var second = firstLine.TextRuns[1] as ShapedTextRun;
Assert.NotNull(first);
Assert.NotNull(second);
Assert.Equal(" ", first.Text.ToString());
Assert.Equal("aaa", second.Text.ToString());
Assert.Equal(1, first.BidiLevel);
Assert.Equal(2, second.BidiLevel);
}
}
[Fact]
public void Should_Reset_Bidi_Levels_Of_Trailing_Whitespaces_After_TextWrapping_2()
{
using (Start())
{
const string text = "אאא בבב";
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var paragraphProperties = new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, true,
true, defaultProperties, TextWrapping.Wrap, 0, 0, 0);
var textSource = new SimpleTextSource(text, defaultProperties);
var formatter = new TextFormatterImpl();
var firstLine = formatter.FormatLine(textSource, 0, 40, paragraphProperties);
Assert.NotNull(firstLine);
Assert.Equal(2, firstLine.TextRuns.Count);
var first = firstLine.TextRuns[0] as ShapedTextRun;
var second = firstLine.TextRuns[1] as ShapedTextRun;
Assert.NotNull(first);
Assert.NotNull(second);
Assert.Equal("אאא", first.Text.ToString());
Assert.Equal(" ", second.Text.ToString());
Assert.Equal(1, first.BidiLevel);
Assert.Equal(0, second.BidiLevel);
}
}
[Fact]
public void Should_Format_TextRuns_With_TextRunStyles()
{

Loading…
Cancel
Save