diff --git a/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs b/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs
new file mode 100644
index 0000000000..3fcb7bf420
--- /dev/null
+++ b/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs
@@ -0,0 +1,263 @@
+using System;
+using System.Diagnostics;
+using Avalonia.Utilities;
+
+namespace Avalonia.Media.TextFormatting
+{
+ ///
+ /// Reorders text runs according to their bidi level.
+ ///
+ /// To avoid allocations, this class is designed to be reused.
+ internal sealed class BidiReorderer
+ {
+ private ArrayBuilder _runs;
+ private ArrayBuilder _ranges;
+
+ public void BidiReorder(Span textRuns, FlowDirection flowDirection)
+ {
+ Debug.Assert(_runs.Length == 0);
+ Debug.Assert(_ranges.Length == 0);
+
+ if (textRuns.IsEmpty)
+ {
+ return;
+ }
+
+ try
+ {
+ _runs.Add(textRuns.Length);
+
+ // Build up the collection of ordered runs.
+ for (var i = 0; i < textRuns.Length; i++)
+ {
+ var textRun = textRuns[i];
+ _runs[i] = new OrderedBidiRun(i, textRun, GetRunBidiLevel(textRun, flowDirection));
+
+ if (i > 0)
+ {
+ _runs[i - 1].NextRunIndex = i;
+ }
+ }
+
+ // Reorder them into visual order.
+ var firstIndex = LinearReorder();
+
+ // Now perform a recursive reversal of each run.
+ // From the highest level found in the text to the lowest odd level on each line, including intermediate levels
+ // not actually present in the text, reverse any contiguous sequence of characters that are at that level or higher.
+ // https://unicode.org/reports/tr9/#L2
+ sbyte max = 0;
+ var min = sbyte.MaxValue;
+
+ for (var i = 0; i < textRuns.Length; i++)
+ {
+ var level = GetRunBidiLevel(textRuns[i], flowDirection);
+ if (level > max)
+ {
+ max = level;
+ }
+
+ if ((level & 1) != 0 && level < min)
+ {
+ min = level;
+ }
+ }
+
+ if (min > max)
+ {
+ min = max;
+ }
+
+ if (max == 0 || (min == max && (max & 1) == 0))
+ {
+ // Nothing to reverse.
+ return;
+ }
+
+ // Now apply the reversal and replace the original contents.
+ var minLevelToReverse = max;
+ int currentIndex;
+
+ while (minLevelToReverse >= min)
+ {
+ currentIndex = firstIndex;
+
+ while (currentIndex >= 0)
+ {
+ ref var current = ref _runs[currentIndex];
+ if (current.Level >= minLevelToReverse && current.Level % 2 != 0)
+ {
+ if (current.Run is ShapedTextRun { IsReversed: false } shapedTextCharacters)
+ {
+ shapedTextCharacters.Reverse();
+ }
+ }
+
+ currentIndex = current.NextRunIndex;
+ }
+
+ minLevelToReverse--;
+ }
+
+ var index = 0;
+
+ currentIndex = firstIndex;
+ while (currentIndex >= 0)
+ {
+ ref var current = ref _runs[currentIndex];
+ textRuns[index++] = current.Run;
+
+ currentIndex = current.NextRunIndex;
+ }
+ }
+ finally
+ {
+ _runs.Clear();
+ _ranges.Clear();
+ }
+ }
+
+ private static sbyte GetRunBidiLevel(TextRun run, FlowDirection flowDirection)
+ {
+ if (run is ShapedTextRun shapedTextRun)
+ {
+ return shapedTextRun.BidiLevel;
+ }
+
+ var defaultLevel = flowDirection == FlowDirection.LeftToRight ? 0 : 1;
+ return (sbyte)defaultLevel;
+ }
+
+ ///
+ /// Reorders the runs from logical to visual order.
+ ///
+ ///
+ /// The first run index in visual order.
+ private int LinearReorder()
+ {
+ var runIndex = 0;
+ var rangeIndex = -1;
+
+ while (runIndex >= 0)
+ {
+ ref var run = ref _runs[runIndex];
+ var nextRunIndex = run.NextRunIndex;
+
+ while (rangeIndex >= 0
+ && _ranges[rangeIndex].Level > run.Level
+ && _ranges[rangeIndex].PreviousRangeIndex >= 0
+ && _ranges[_ranges[rangeIndex].PreviousRangeIndex].Level >= run.Level)
+ {
+
+ rangeIndex = MergeRangeWithPrevious(rangeIndex);
+ }
+
+ if (rangeIndex >= 0 && _ranges[rangeIndex].Level >= run.Level)
+ {
+ // Attach run to the range.
+ if ((run.Level & 1) != 0)
+ {
+ // Odd, range goes to the right of run.
+ run.NextRunIndex = _ranges[rangeIndex].LeftRunIndex;
+ _ranges[rangeIndex].LeftRunIndex = runIndex;
+ }
+ else
+ {
+ // Even, range goes to the left of run.
+ _runs[_ranges[rangeIndex].RightRunIndex].NextRunIndex = runIndex;
+ _ranges[rangeIndex].RightRunIndex = runIndex;
+ }
+
+ _ranges[rangeIndex].Level = run.Level;
+ }
+ else
+ {
+ var r = new BidiRange(run.Level, runIndex, runIndex, previousRangeIndex: rangeIndex);
+ _ranges.AddItem(r);
+ rangeIndex = _ranges.Length - 1;
+ }
+
+ runIndex = nextRunIndex;
+ }
+
+ while (rangeIndex >= 0 && _ranges[rangeIndex].PreviousRangeIndex >= 0)
+ {
+ rangeIndex = MergeRangeWithPrevious(rangeIndex);
+ }
+
+ // Terminate.
+ _runs[_ranges[rangeIndex].RightRunIndex].NextRunIndex = -1;
+
+ return _runs[_ranges[rangeIndex].LeftRunIndex].RunIndex;
+ }
+
+ private int MergeRangeWithPrevious(int index)
+ {
+ var previousIndex = _ranges[index].PreviousRangeIndex;
+ ref var previous = ref _ranges[previousIndex];
+
+ int leftIndex;
+ int rightIndex;
+
+ if ((previous.Level & 1) != 0)
+ {
+ // Odd, previous goes to the right of range.
+ leftIndex = index;
+ rightIndex = previousIndex;
+ }
+ else
+ {
+ // Even, previous goes to the left of range.
+ leftIndex = previousIndex;
+ rightIndex = index;
+ }
+
+ // Stitch them
+ ref var left = ref _ranges[leftIndex];
+ ref var right = ref _ranges[rightIndex];
+ _runs[left.RightRunIndex].NextRunIndex = _runs[right.LeftRunIndex].RunIndex;
+ previous.LeftRunIndex = left.LeftRunIndex;
+ previous.RightRunIndex = right.RightRunIndex;
+
+ return previousIndex;
+ }
+
+ private struct OrderedBidiRun
+ {
+ public OrderedBidiRun(int runIndex, TextRun run, sbyte level)
+ {
+ RunIndex = runIndex;
+ Run = run;
+ Level = level;
+ NextRunIndex = -1;
+ }
+
+ public int RunIndex { get; }
+
+ public sbyte Level { get; }
+
+ public TextRun Run { get; }
+
+ public int NextRunIndex { get; set; } // -1 if none
+ }
+
+ private struct BidiRange
+ {
+ public BidiRange(sbyte level, int leftRunIndex, int rightRunIndex, int previousRangeIndex)
+ {
+ Level = level;
+ LeftRunIndex = leftRunIndex;
+ RightRunIndex = rightRunIndex;
+ PreviousRangeIndex = previousRangeIndex;
+ }
+
+ public sbyte Level { get; set; }
+
+ public int LeftRunIndex { get; set; }
+
+ public int RightRunIndex { get; set; }
+
+ public int PreviousRangeIndex { get; } // -1 if none
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
index 7614c8e3dc..f3cc0a714e 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
@@ -173,9 +173,9 @@ namespace Avalonia.Media.TextFormatting
}
- var biDiData = t_bidiData ??= new BidiData();
- biDiData.Reset();
- biDiData.ParagraphEmbeddingLevel = (sbyte)flowDirection;
+ var bidiData = t_bidiData ??= new BidiData();
+ bidiData.Reset();
+ bidiData.ParagraphEmbeddingLevel = (sbyte)flowDirection;
foreach (var textRun in textRuns)
{
@@ -187,21 +187,21 @@ namespace Avalonia.Media.TextFormatting
else
text = new char[textRun.Length];
- biDiData.Append(text);
+ bidiData.Append(text);
}
- var biDi = t_bidiAlgorithm ??= new BidiAlgorithm();
+ var bidiAlgorithm = t_bidiAlgorithm ??= new BidiAlgorithm();
- biDi.Process(biDiData);
+ bidiAlgorithm.Process(bidiData);
- var resolvedEmbeddingLevel = biDi.ResolveEmbeddingLevel(biDiData.Classes);
+ var resolvedEmbeddingLevel = bidiAlgorithm.ResolveEmbeddingLevel(bidiData.Classes);
resolvedFlowDirection =
(resolvedEmbeddingLevel & 1) == 0 ? FlowDirection.LeftToRight : FlowDirection.RightToLeft;
var processedRuns = new List(textRuns.Count);
- CoalesceLevels(textRuns, biDi.ResolvedLevels, processedRuns);
+ CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels, processedRuns);
for (var index = 0; index < processedRuns.Count; index++)
{
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
index ae6df3a232..7fa9155b02 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
@@ -1,12 +1,15 @@
using System;
using System.Collections.Generic;
+using System.Threading;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
internal sealed class TextLineImpl : TextLine
{
- private TextRun[] _textRuns;
+ private static readonly ThreadLocal s_bidiReorderer = new(() => new BidiReorderer());
+
+ private readonly TextRun[] _textRuns;
private readonly double _paragraphWidth;
private readonly TextParagraphProperties _paragraphProperties;
private TextLineMetrics _textLineMetrics;
@@ -993,185 +996,12 @@ namespace Avalonia.Media.TextFormatting
{
_textLineMetrics = CreateLineMetrics();
- BidiReorder();
+ var bidiReorderer = s_bidiReorderer.Value!;
+ bidiReorderer.BidiReorder(_textRuns, _resolvedFlowDirection);
return this;
}
- private static sbyte GetRunBidiLevel(TextRun run, FlowDirection flowDirection)
- {
- if (run is ShapedTextRun shapedTextCharacters)
- {
- return shapedTextCharacters.BidiLevel;
- }
-
- var defaultLevel = flowDirection == FlowDirection.LeftToRight ? 0 : 1;
-
- return (sbyte)defaultLevel;
- }
-
- private void BidiReorder()
- {
- if (_textRuns.Length == 0)
- {
- return;
- }
-
- // Build up the collection of ordered runs.
- var run = _textRuns[0];
-
- OrderedBidiRun orderedRun = new(run, GetRunBidiLevel(run, _resolvedFlowDirection));
-
- var current = orderedRun;
-
- for (var i = 1; i < _textRuns.Length; i++)
- {
- run = _textRuns[i];
-
- current.Next = new OrderedBidiRun(run, GetRunBidiLevel(run, _resolvedFlowDirection));
-
- current = current.Next;
- }
-
- // Reorder them into visual order.
- orderedRun = LinearReOrder(orderedRun);
-
- // Now perform a recursive reversal of each run.
- // From the highest level found in the text to the lowest odd level on each line, including intermediate levels
- // not actually present in the text, reverse any contiguous sequence of characters that are at that level or higher.
- // https://unicode.org/reports/tr9/#L2
- sbyte max = 0;
- var min = sbyte.MaxValue;
-
- for (var i = 0; i < _textRuns.Length; i++)
- {
- var currentRun = _textRuns[i];
-
- var level = GetRunBidiLevel(currentRun, _resolvedFlowDirection);
-
- if (level > max)
- {
- max = level;
- }
-
- if ((level & 1) != 0 && level < min)
- {
- min = level;
- }
- }
-
- if (min > max)
- {
- min = max;
- }
-
- if (max == 0 || (min == max && (max & 1) == 0))
- {
- // Nothing to reverse.
- return;
- }
-
- // Now apply the reversal and replace the original contents.
- var minLevelToReverse = max;
-
- while (minLevelToReverse >= min)
- {
- current = orderedRun;
-
- while (current != null)
- {
- if (current.Level >= minLevelToReverse && current.Level % 2 != 0)
- {
- if (current.Run is ShapedTextRun { IsReversed: false } shapedTextCharacters)
- {
- shapedTextCharacters.Reverse();
- }
- }
-
- current = current.Next;
- }
-
- minLevelToReverse--;
- }
-
- var textRuns = new TextRun[_textRuns.Length];
- var index = 0;
-
- current = orderedRun;
-
- while (current != null)
- {
- textRuns[index++] = current.Run;
-
- current = current.Next;
- }
-
- _textRuns = textRuns;
- }
-
- ///
- /// Reorders a series of runs from logical to visual order, returning the left most run.
- ///
- ///
- /// The ordered bidi run.
- /// The .
- private static OrderedBidiRun LinearReOrder(OrderedBidiRun? run)
- {
- BidiRange? range = null;
-
- while (run != null)
- {
- var next = run.Next;
-
- while (range != null && range.Level > run.Level
- && range.Previous != null && range.Previous.Level >= run.Level)
- {
- range = BidiRange.MergeWithPrevious(range);
- }
-
- if (range != null && range.Level >= run.Level)
- {
- // Attach run to the range.
- if ((run.Level & 1) != 0)
- {
- // Odd, range goes to the right of run.
- run.Next = range.Left;
- range.Left = run;
- }
- else
- {
- // Even, range goes to the left of run.
- range.Right!.Next = run;
- range.Right = run;
- }
-
- range.Level = run.Level;
- }
- else
- {
- var r = new BidiRange();
-
- r.Left = r.Right = run;
- r.Level = run.Level;
- r.Previous = range;
-
- range = r;
- }
-
- run = next;
- }
-
- while (range?.Previous != null)
- {
- range = BidiRange.MergeWithPrevious(range);
- }
-
- // Terminate.
- range!.Right!.Next = null;
-
- return range.Left!;
- }
-
///
/// Tries to find the next character hit.
///
@@ -1620,59 +1450,5 @@ namespace Avalonia.Media.TextFormatting
return 0;
}
}
-
- private sealed class OrderedBidiRun
- {
- public OrderedBidiRun(TextRun run, sbyte level)
- {
- Run = run;
- Level = level;
- }
-
- public sbyte Level { get; }
-
- public TextRun Run { get; }
-
- public OrderedBidiRun? Next { get; set; }
- }
-
- private sealed class BidiRange
- {
- public int Level { get; set; }
-
- public OrderedBidiRun? Left { get; set; }
-
- public OrderedBidiRun? Right { get; set; }
-
- public BidiRange? Previous { get; set; }
-
- public static BidiRange MergeWithPrevious(BidiRange range)
- {
- var previous = range.Previous;
-
- BidiRange left;
- BidiRange right;
-
- if ((previous!.Level & 1) != 0)
- {
- // Odd, previous goes to the right of range.
- left = range;
- right = previous;
- }
- else
- {
- // Even, previous goes to the left of range.
- left = previous;
- right = range;
- }
-
- // Stitch them
- left.Right!.Next = right.Left;
- previous.Left = left.Left;
- previous.Right = right.Right;
-
- return previous;
- }
- }
}
}
diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs
index e770ba9e91..e960a510a9 100644
--- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs
@@ -5,7 +5,6 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
-using System.Threading;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting.Unicode
diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs
index 106079de8e..226e5ad6bd 100644
--- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs
@@ -11,6 +11,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// Represents a unicode string and all associated attributes
/// for each character required for the bidirectional Unicode algorithm
///
+ /// To avoid allocations, this class is designed to be reused.
internal sealed class BidiData
{
private ArrayBuilder _classes;
diff --git a/src/Avalonia.Base/Utilities/ArrayBuilder.cs b/src/Avalonia.Base/Utilities/ArrayBuilder.cs
index 1c11966a7d..3a22fc7b9c 100644
--- a/src/Avalonia.Base/Utilities/ArrayBuilder.cs
+++ b/src/Avalonia.Base/Utilities/ArrayBuilder.cs
@@ -18,7 +18,7 @@ namespace Avalonia.Utilities
private const int MaxCoreClrArrayLength = 0x7FeFFFFF;
// Starts out null, initialized on first Add.
- private T[] _data;
+ private T[]? _data;
private int _size;
///
@@ -115,6 +115,16 @@ namespace Avalonia.Utilities
return slice;
}
+ ///
+ /// Appends an item.
+ ///
+ /// The item to append.
+ public void AddItem(T value)
+ {
+ var index = Length++;
+ _data![index] = value;
+ }
+
///
/// Clears the array.
/// Allocated memory is left intact for future usage.