using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
internal class TextFormatterImpl : TextFormatter
{
private static readonly char[] s_empty = { ' ' };
///
public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null)
{
var textWrapping = paragraphProperties.TextWrapping;
FlowDirection resolvedFlowDirection;
TextLineBreak? nextLineBreak = null;
List drawableTextRuns;
var textRuns = FetchTextRuns(textSource, firstTextSourceIndex,
out var textEndOfLine, out var textSourceLength);
if (previousLineBreak?.RemainingRuns != null)
{
resolvedFlowDirection = previousLineBreak.FlowDirection;
drawableTextRuns = previousLineBreak.RemainingRuns.ToList();
nextLineBreak = previousLineBreak;
}
else
{
drawableTextRuns = ShapeTextRuns(textRuns, paragraphProperties, out resolvedFlowDirection);
if (nextLineBreak == null && textEndOfLine != null)
{
nextLineBreak = new TextLineBreak(textEndOfLine, resolvedFlowDirection);
}
}
TextLineImpl textLine;
switch (textWrapping)
{
case TextWrapping.NoWrap:
{
textLine = new TextLineImpl(drawableTextRuns, firstTextSourceIndex, textSourceLength,
paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak);
textLine.FinalizeLine();
break;
}
case TextWrapping.WrapWithOverflow:
case TextWrapping.Wrap:
{
textLine = PerformTextWrapping(drawableTextRuns, firstTextSourceIndex, paragraphWidth, paragraphProperties,
resolvedFlowDirection, nextLineBreak);
break;
}
default:
throw new ArgumentOutOfRangeException(nameof(textWrapping));
}
return textLine;
}
///
/// Split a sequence of runs into two segments at specified length.
///
/// The text run's.
/// The length to split at.
/// The split text runs.
internal static SplitResult> SplitDrawableRuns(List textRuns, int length)
{
var currentLength = 0;
for (var i = 0; i < textRuns.Count; i++)
{
var currentRun = textRuns[i];
if (currentLength + currentRun.TextSourceLength < length)
{
currentLength += currentRun.TextSourceLength;
continue;
}
var firstCount = currentRun.TextSourceLength >= 1 ? i + 1 : i;
var first = new List(firstCount);
if (firstCount > 1)
{
for (var j = 0; j < i; j++)
{
first.Add(textRuns[j]);
}
}
var secondCount = textRuns.Count - firstCount;
if (currentLength + currentRun.TextSourceLength == length)
{
var second = secondCount > 0 ? new List(secondCount) : null;
if (second != null)
{
var offset = currentRun.TextSourceLength >= 1 ? 1 : 0;
for (var j = 0; j < secondCount; j++)
{
second.Add(textRuns[i + j + offset]);
}
}
first.Add(currentRun);
return new SplitResult>(first, second);
}
else
{
secondCount++;
var second = new List(secondCount);
if (currentRun is ShapedTextCharacters shapedTextCharacters)
{
var split = shapedTextCharacters.Split(length - currentLength);
first.Add(split.First);
second.Add(split.Second!);
}
for (var j = 1; j < secondCount; j++)
{
second.Add(textRuns[i + j]);
}
return new SplitResult>(first, second);
}
}
return new SplitResult>(textRuns, null);
}
///
/// Shape specified text runs with specified paragraph embedding.
///
/// The text runs to shape.
/// The default paragraph properties.
/// The resolved flow direction.
///
/// A list of shaped text characters.
///
private static List ShapeTextRuns(List textRuns, TextParagraphProperties paragraphProperties,
out FlowDirection resolvedFlowDirection)
{
var flowDirection = paragraphProperties.FlowDirection;
var drawableTextRuns = new List();
var biDiData = new BidiData((sbyte)flowDirection);
foreach (var textRun in textRuns)
{
if (textRun.Text.IsEmpty)
{
var text = new char[textRun.TextSourceLength];
biDiData.Append(text);
}
else
{
biDiData.Append(textRun.Text);
}
}
var biDi = new BidiAlgorithm();
biDi.Process(biDiData);
var resolvedEmbeddingLevel = biDi.ResolveEmbeddingLevel(biDiData.Classes);
resolvedFlowDirection =
(resolvedEmbeddingLevel & 1) == 0 ? FlowDirection.LeftToRight : FlowDirection.RightToLeft;
var processedRuns = new List(textRuns.Count);
foreach (var coalescedRuns in CoalesceLevels(textRuns, biDi.ResolvedLevels))
{
processedRuns.AddRange(coalescedRuns);
}
for (var index = 0; index < processedRuns.Count; index++)
{
var currentRun = processedRuns[index];
switch (currentRun)
{
case DrawableTextRun drawableRun:
{
drawableTextRuns.Add(drawableRun);
break;
}
case ShapeableTextCharacters shapeableRun:
{
var groupedRuns = new List(2) { shapeableRun };
var text = currentRun.Text;
var start = currentRun.Text.Start;
var length = currentRun.Text.Length;
var bufferOffset = currentRun.Text.BufferOffset;
while (index + 1 < processedRuns.Count)
{
if (processedRuns[index + 1] is not ShapeableTextCharacters nextRun)
{
break;
}
if (shapeableRun.CanShapeTogether(nextRun))
{
groupedRuns.Add(nextRun);
length += nextRun.Text.Length;
if (start > nextRun.Text.Start)
{
start = nextRun.Text.Start;
}
if (bufferOffset > nextRun.Text.BufferOffset)
{
bufferOffset = nextRun.Text.BufferOffset;
}
text = new ReadOnlySlice(text.Buffer, start, length, bufferOffset);
index++;
shapeableRun = nextRun;
continue;
}
break;
}
var shaperOptions = new TextShaperOptions(currentRun.Properties!.Typeface.GlyphTypeface,
currentRun.Properties.FontRenderingEmSize,
shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, paragraphProperties.DefaultIncrementalTab);
drawableTextRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions));
break;
}
}
}
return drawableTextRuns;
}
private static IReadOnlyList ShapeTogether(
IReadOnlyList textRuns, ReadOnlySlice text, TextShaperOptions options)
{
var shapedRuns = new List(textRuns.Count);
var shapedBuffer = TextShaper.Current.ShapeText(text, options);
for (var i = 0; i < textRuns.Count; i++)
{
var currentRun = textRuns[i];
var splitResult = shapedBuffer.Split(currentRun.Text.Length);
shapedRuns.Add(new ShapedTextCharacters(splitResult.First, currentRun.Properties));
shapedBuffer = splitResult.Second!;
}
return shapedRuns;
}
///
/// Coalesces ranges of the same bidi level to form
///
/// The text characters to form from.
/// The bidi levels.
///
private static IEnumerable> CoalesceLevels(IReadOnlyList textCharacters, ArraySlice levels)
{
if (levels.Length == 0)
{
yield break;
}
var levelIndex = 0;
var runLevel = levels[0];
TextRunProperties? previousProperties = null;
TextCharacters? currentRun = null;
var runText = ReadOnlySlice.Empty;
for (var i = 0; i < textCharacters.Count; i++)
{
var j = 0;
currentRun = textCharacters[i] as TextCharacters;
if (currentRun == null)
{
var drawableRun = textCharacters[i];
yield return new[] { drawableRun };
levelIndex += drawableRun.TextSourceLength;
continue;
}
runText = currentRun.Text;
for (; j < runText.Length;)
{
Codepoint.ReadAt(runText, j, out var count);
if (levelIndex + 1 == levels.Length)
{
break;
}
levelIndex++;
j += count;
if (j == runText.Length)
{
yield return currentRun.GetShapeableCharacters(runText.Take(j), runLevel, ref previousProperties);
runLevel = levels[levelIndex];
continue;
}
if (levels[levelIndex] == runLevel)
{
continue;
}
// End of this run
yield return currentRun.GetShapeableCharacters(runText.Take(j), runLevel, ref previousProperties);
runText = runText.Skip(j);
j = 0;
// Move to next run
runLevel = levels[levelIndex];
}
}
if (currentRun is null || runText.IsEmpty)
{
yield break;
}
yield return currentRun.GetShapeableCharacters(runText, runLevel, ref previousProperties);
}
///
/// Fetches text runs.
///
/// The text source.
/// The first text source index.
///
///
///
/// The formatted text runs.
///
private static List FetchTextRuns(ITextSource textSource, int firstTextSourceIndex,
out TextEndOfLine? endOfLine, out int textSourceLength)
{
textSourceLength = 0;
endOfLine = null;
var textRuns = new List();
var textRunEnumerator = new TextRunEnumerator(textSource, firstTextSourceIndex);
while (textRunEnumerator.MoveNext())
{
var textRun = textRunEnumerator.Current;
if (textRun == null)
{
break;
}
if (textRun is TextEndOfLine textEndOfLine)
{
endOfLine = textEndOfLine;
textSourceLength += textEndOfLine.TextSourceLength;
textRuns.Add(textRun);
break;
}
switch (textRun)
{
case TextCharacters textCharacters:
{
if (TryGetLineBreak(textCharacters, out var runLineBreak))
{
var splitResult = new TextCharacters(textCharacters.Text.Take(runLineBreak.PositionWrap),
textCharacters.Properties);
textRuns.Add(splitResult);
textSourceLength += runLineBreak.PositionWrap;
return textRuns;
}
textRuns.Add(textCharacters);
break;
}
default:
{
textRuns.Add(textRun);
break;
}
}
textSourceLength += textRun.TextSourceLength;
}
return textRuns;
}
private static bool TryGetLineBreak(TextRun textRun, out LineBreak lineBreak)
{
lineBreak = default;
if (textRun.Text.IsEmpty)
{
return false;
}
var lineBreakEnumerator = new LineBreakEnumerator(textRun.Text);
while (lineBreakEnumerator.MoveNext())
{
if (!lineBreakEnumerator.Current.Required)
{
continue;
}
lineBreak = lineBreakEnumerator.Current;
return lineBreak.PositionWrap >= textRun.Text.Length || true;
}
return false;
}
private static bool TryMeasureLength(IReadOnlyList textRuns, double paragraphWidth, out int measuredLength)
{
measuredLength = 0;
var currentWidth = 0.0;
foreach (var currentRun in textRuns)
{
switch (currentRun)
{
case ShapedTextCharacters shapedTextCharacters:
{
var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphClusters[0];
var lastCluster = firstCluster;
for (var i = 0; i < shapedTextCharacters.ShapedBuffer.Length; i++)
{
var glyphInfo = shapedTextCharacters.ShapedBuffer[i];
if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth)
{
measuredLength += Math.Max(0, lastCluster - firstCluster);
goto found;
}
lastCluster = glyphInfo.GlyphCluster;
currentWidth += glyphInfo.GlyphAdvance;
}
measuredLength += currentRun.TextSourceLength;
break;
}
case { } drawableTextRun:
{
if (currentWidth + drawableTextRun.Size.Width > paragraphWidth)
{
goto found;
}
measuredLength += currentRun.TextSourceLength;
currentWidth += currentRun.Size.Width;
break;
}
}
}
found:
return measuredLength != 0;
}
///
/// Creates an empty text line.
///
/// The empty text line.
public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties)
{
var flowDirection = paragraphProperties.FlowDirection;
var properties = paragraphProperties.DefaultTextRunProperties;
var glyphTypeface = properties.Typeface.GlyphTypeface;
var text = new ReadOnlySlice(s_empty, firstTextSourceIndex, 1);
var glyph = glyphTypeface.GetGlyph(s_empty[0]);
var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) };
var shapedBuffer = new ShapedBuffer(text, glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
(sbyte)flowDirection);
var textRuns = new List { new ShapedTextCharacters(shapedBuffer, properties) };
return new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection).FinalizeLine();
}
///
/// Performs text wrapping returns a list of text lines.
///
///
/// The first text source index.
/// The paragraph width.
/// The text paragraph properties.
///
/// The current line break if the line was explicitly broken.
/// The wrapped text line.
private static TextLineImpl PerformTextWrapping(List textRuns, int firstTextSourceIndex,
double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection,
TextLineBreak? currentLineBreak)
{
if(textRuns.Count == 0)
{
return CreateEmptyTextLine(firstTextSourceIndex,paragraphWidth, paragraphProperties);
}
if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength))
{
measuredLength = 1;
}
var currentLength = 0;
var lastWrapPosition = 0;
var currentPosition = 0;
for (var index = 0; index < textRuns.Count; index++)
{
var currentRun = textRuns[index];
var lineBreaker = new LineBreakEnumerator(currentRun.Text);
var breakFound = false;
while (lineBreaker.MoveNext())
{
if (lineBreaker.Current.Required &&
currentLength + lineBreaker.Current.PositionMeasure <= measuredLength)
{
//Explicit break found
breakFound = true;
currentPosition = currentLength + lineBreaker.Current.PositionWrap;
break;
}
if (currentLength + lineBreaker.Current.PositionMeasure > measuredLength)
{
if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow)
{
if (lastWrapPosition > 0)
{
currentPosition = lastWrapPosition;
breakFound = true;
break;
}
//Find next possible wrap position (overflow)
if (index < textRuns.Count - 1)
{
if (lineBreaker.Current.PositionWrap != currentRun.Text.Length)
{
//We already found the next possible wrap position.
breakFound = true;
currentPosition = currentLength + lineBreaker.Current.PositionWrap;
break;
}
while (lineBreaker.MoveNext() && index < textRuns.Count)
{
currentPosition += lineBreaker.Current.PositionWrap;
if (lineBreaker.Current.PositionWrap != currentRun.Text.Length)
{
break;
}
index++;
if (index >= textRuns.Count)
{
break;
}
currentRun = textRuns[index];
lineBreaker = new LineBreakEnumerator(currentRun.Text);
}
}
else
{
currentPosition = currentLength + lineBreaker.Current.PositionWrap;
}
breakFound = true;
break;
}
//We overflowed so we use the last available wrap position.
currentPosition = lastWrapPosition == 0 ? measuredLength : lastWrapPosition;
breakFound = true;
break;
}
if (lineBreaker.Current.PositionMeasure != lineBreaker.Current.PositionWrap)
{
lastWrapPosition = currentLength + lineBreaker.Current.PositionWrap;
}
}
if (!breakFound)
{
currentLength += currentRun.Text.Length;
continue;
}
measuredLength = currentPosition;
break;
}
var splitResult = SplitDrawableRuns(textRuns, measuredLength);
var remainingCharacters = splitResult.Second;
var lineBreak = remainingCharacters?.Count > 0 ?
new TextLineBreak(currentLineBreak?.TextEndOfLine, resolvedFlowDirection, remainingCharacters) :
null;
if (lineBreak is null && currentLineBreak?.TextEndOfLine != null)
{
lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection);
}
var textLine = new TextLineImpl(splitResult.First, firstTextSourceIndex, measuredLength,
paragraphWidth, paragraphProperties, resolvedFlowDirection,
lineBreak);
return textLine.FinalizeLine();
}
private struct TextRunEnumerator
{
private readonly ITextSource _textSource;
private int _pos;
public TextRunEnumerator(ITextSource textSource, int firstTextSourceIndex)
{
_textSource = textSource;
_pos = firstTextSourceIndex;
Current = null;
}
// ReSharper disable once MemberHidesStaticFromOuterClass
public TextRun? Current { get; private set; }
public bool MoveNext()
{
Current = _textSource.GetTextRun(_pos);
if (Current is null)
{
return false;
}
if (Current.TextSourceLength == 0)
{
return false;
}
_pos += Current.TextSourceLength;
return true;
}
}
///
/// Creates a shaped symbol.
///
/// The symbol run to shape.
/// The flow direction.
///
/// The shaped symbol.
///
internal static ShapedTextCharacters CreateSymbol(TextRun textRun, FlowDirection flowDirection)
{
var textShaper = TextShaper.Current;
var glyphTypeface = textRun.Properties!.Typeface.GlyphTypeface;
var fontRenderingEmSize = textRun.Properties.FontRenderingEmSize;
var cultureInfo = textRun.Properties.CultureInfo;
var shaperOptions = new TextShaperOptions(glyphTypeface, fontRenderingEmSize, (sbyte)flowDirection, cultureInfo);
var shapedBuffer = textShaper.ShapeText(textRun.Text, shaperOptions);
return new ShapedTextCharacters(shapedBuffer, textRun.Properties);
}
}
}