From 7071c7a8d6e698e8d8684d0ed6453bf4bf08af67 Mon Sep 17 00:00:00 2001
From: Compunet <117437050+dme-compunet@users.noreply.github.com>
Date: Thu, 9 Jan 2025 15:58:16 +0200
Subject: [PATCH] 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
---
.../Media/TextFormatting/ShapedBuffer.cs | 4 +-
.../Media/TextFormatting/TextFormatterImpl.cs | 85 +++++++++++++++++++
.../TextFormatting/TextFormatterTests.cs | 81 ++++++++++++++++++
3 files changed, 169 insertions(+), 1 deletion(-)
diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs
index 3f26d081b0..65b89c1ed7 100644
--- a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs
@@ -49,7 +49,7 @@ namespace Avalonia.Media.TextFormatting
///
/// The buffer's bidi level.
///
- public sbyte BidiLevel { get; }
+ public sbyte BidiLevel { get; private set; }
///
/// The buffer's reading direction.
@@ -169,6 +169,8 @@ namespace Avalonia.Media.TextFormatting
return new SplitResult(first, second);
}
+ internal void ResetBidiLevel(sbyte paragraphEmbeddingLevel) => BidiLevel = paragraphEmbeddingLevel;
+
int IReadOnlyCollection.Count => _glyphInfos.Length;
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
index 8e2325fb14..784ee835b4 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
+++ b/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 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;
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
index 5ff0a45e39..59c9216aec 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
+++ b/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()
{