6 changed files with 289 additions and 240 deletions
@ -0,0 +1,263 @@ |
|||||
|
using System; |
||||
|
using System.Diagnostics; |
||||
|
using Avalonia.Utilities; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Reorders text runs according to their bidi level.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>To avoid allocations, this class is designed to be reused.</remarks>
|
||||
|
internal sealed class BidiReorderer |
||||
|
{ |
||||
|
private ArrayBuilder<OrderedBidiRun> _runs; |
||||
|
private ArrayBuilder<BidiRange> _ranges; |
||||
|
|
||||
|
public void BidiReorder(Span<TextRun> 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; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Reorders the runs from logical to visual order.
|
||||
|
/// <see href="https://github.com/fribidi/linear-reorder/blob/f2f872257d4d8b8e137fcf831f254d6d4db79d3c/linear-reorder.c"/>
|
||||
|
/// </summary>
|
||||
|
/// <returns>The first run index in visual order.</returns>
|
||||
|
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
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue