Skip to content

Commit

Permalink
Reset bidi levels of trailing whitespace after text wrapping (Accordi…
Browse files Browse the repository at this point in the history
…ng to TR9 guidelines) (#17924)

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

* Added unit tests
  • Loading branch information
dme-compunet authored Jan 9, 2025
1 parent a647c5c commit 7071c7a
Show file tree
Hide file tree
Showing 3 changed files with 169 additions and 1 deletion.
4 changes: 3 additions & 1 deletion src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ internal ShapedBuffer(ReadOnlyMemory<char> text, ArraySlice<GlyphInfo> glyphInfo
/// <summary>
/// The buffer's bidi level.
/// </summary>
public sbyte BidiLevel { get; }
public sbyte BidiLevel { get; private set; }

/// <summary>
/// The buffer's reading direction.
Expand Down Expand Up @@ -169,6 +169,8 @@ internal SplitResult<ShapedBuffer> Split(int length)
return new SplitResult<ShapedBuffer>(first, second);
}

internal void ResetBidiLevel(sbyte paragraphEmbeddingLevel) => BidiLevel = paragraphEmbeddingLevel;

int IReadOnlyCollection<GlyphInfo>.Count => _glyphInfos.Length;

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
Expand Down
85 changes: 85 additions & 0 deletions src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -908,6 +908,11 @@ private static TextLineImpl PerformTextWrapping(List<TextRun> textRuns, bool can
textLineBreak = null;
}

if (postSplitRuns?.Count > 0)
{
ResetTrailingWhitespaceBidiLevels(preSplitRuns, paragraphProperties.FlowDirection, objectPool);
}

var textLine = new TextLineImpl(preSplitRuns.ToArray(), firstTextSourceIndex, measuredLength,
paragraphWidth, paragraphProperties, resolvedFlowDirection,
textLineBreak);
Expand All @@ -923,6 +928,86 @@ private static TextLineImpl PerformTextWrapping(List<TextRun> textRuns, bool can
}
}

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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,87 @@ public void Should_Format_TextLine_With_Non_Text_TextRuns_RightToLeft()
}
}

[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()
{
Expand Down

0 comments on commit 7071c7a

Please sign in to comment.