A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

762 lines
27 KiB

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 = { ' ' };
/// <inheritdoc cref="TextFormatter.FormatLine"/>
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<DrawableTextRun> 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;
}
/// <summary>
/// Split a sequence of runs into two segments at specified length.
/// </summary>
/// <param name="textRuns">The text run's.</param>
/// <param name="length">The length to split at.</param>
/// <returns>The split text runs.</returns>
internal static SplitResult<List<DrawableTextRun>> SplitDrawableRuns(List<DrawableTextRun> 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<DrawableTextRun>(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<DrawableTextRun>(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<List<DrawableTextRun>>(first, second);
}
else
{
secondCount++;
var second = new List<DrawableTextRun>(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<List<DrawableTextRun>>(first, second);
}
}
return new SplitResult<List<DrawableTextRun>>(textRuns, null);
}
/// <summary>
/// Shape specified text runs with specified paragraph embedding.
/// </summary>
/// <param name="textRuns">The text runs to shape.</param>
/// <param name="paragraphProperties">The default paragraph properties.</param>
/// <param name="resolvedFlowDirection">The resolved flow direction.</param>
/// <returns>
/// A list of shaped text characters.
/// </returns>
private static List<DrawableTextRun> ShapeTextRuns(List<TextRun> textRuns, TextParagraphProperties paragraphProperties,
out FlowDirection resolvedFlowDirection)
{
var flowDirection = paragraphProperties.FlowDirection;
var drawableTextRuns = new List<DrawableTextRun>();
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<TextRun>(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<ShapeableTextCharacters>(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<char>(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, paragraphProperties.LetterSpacing);
drawableTextRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions));
break;
}
}
}
return drawableTextRuns;
}
private static IReadOnlyList<ShapedTextCharacters> ShapeTogether(
IReadOnlyList<ShapeableTextCharacters> textRuns, ReadOnlySlice<char> text, TextShaperOptions options)
{
var shapedRuns = new List<ShapedTextCharacters>(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;
}
/// <summary>
/// Coalesces ranges of the same bidi level to form <see cref="ShapeableTextCharacters"/>
/// </summary>
/// <param name="textCharacters">The text characters to form <see cref="ShapeableTextCharacters"/> from.</param>
/// <param name="levels">The bidi levels.</param>
/// <returns></returns>
private static IEnumerable<IReadOnlyList<TextRun>> CoalesceLevels(IReadOnlyList<TextRun> textCharacters, ArraySlice<sbyte> levels)
{
if (levels.Length == 0)
{
yield break;
}
var levelIndex = 0;
var runLevel = levels[0];
TextRunProperties? previousProperties = null;
TextCharacters? currentRun = null;
var runText = ReadOnlySlice<char>.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);
}
/// <summary>
/// Fetches text runs.
/// </summary>
/// <param name="textSource">The text source.</param>
/// <param name="firstTextSourceIndex">The first text source index.</param>
/// <param name="endOfLine"></param>
/// <param name="textSourceLength"></param>
/// <returns>
/// The formatted text runs.
/// </returns>
private static List<TextRun> FetchTextRuns(ITextSource textSource, int firstTextSourceIndex,
out TextEndOfLine? endOfLine, out int textSourceLength)
{
textSourceLength = 0;
endOfLine = null;
var textRuns = new List<TextRun>();
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<DrawableTextRun> textRuns, double paragraphWidth, out int measuredLength)
{
measuredLength = 0;
var currentWidth = 0.0;
foreach (var currentRun in textRuns)
{
switch (currentRun)
{
case ShapedTextCharacters shapedTextCharacters:
{
if(shapedTextCharacters.ShapedBuffer.Length > 0)
{
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;
}
/// <summary>
/// Creates an empty text line.
/// </summary>
/// <returns>The empty text line.</returns>
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<char>(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<DrawableTextRun> { new ShapedTextCharacters(shapedBuffer, properties) };
return new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection).FinalizeLine();
}
/// <summary>
/// Performs text wrapping returns a list of text lines.
/// </summary>
/// <param name="textRuns"></param>
/// <param name="firstTextSourceIndex">The first text source index.</param>
/// <param name="paragraphWidth">The paragraph width.</param>
/// <param name="paragraphProperties">The text paragraph properties.</param>
/// <param name="resolvedFlowDirection"></param>
/// <param name="currentLineBreak">The current line break if the line was explicitly broken.</param>
/// <returns>The wrapped text line.</returns>
private static TextLineImpl PerformTextWrapping(List<DrawableTextRun> 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.TextSourceLength;
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;
}
}
/// <summary>
/// Creates a shaped symbol.
/// </summary>
/// <param name="textRun">The symbol run to shape.</param>
/// <param name="flowDirection">The flow direction.</param>
/// <returns>
/// The shaped symbol.
/// </returns>
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);
}
}
}