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.
 
 
 

595 lines
20 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
{
/// <inheritdoc cref="TextFormatter.FormatLine"/>
public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null)
{
var textWrapping = paragraphProperties.TextWrapping;
FlowDirection flowDirection;
TextLineBreak? nextLineBreak = null;
List<ShapedTextCharacters> shapedRuns;
var textRuns = FetchTextRuns(textSource, firstTextSourceIndex,
out var textEndOfLine, out var textRange);
if (previousLineBreak?.RemainingCharacters != null)
{
flowDirection = previousLineBreak.FlowDirection;
shapedRuns = previousLineBreak.RemainingCharacters.ToList();
nextLineBreak = previousLineBreak;
}
else
{
shapedRuns = ShapeTextRuns(textRuns, paragraphProperties.FlowDirection,out flowDirection);
if(nextLineBreak == null && textEndOfLine != null)
{
nextLineBreak = new TextLineBreak(textEndOfLine, flowDirection);
}
}
TextLineImpl textLine;
switch (textWrapping)
{
case TextWrapping.NoWrap:
{
TextLineImpl.SortRuns(shapedRuns);
textLine = new TextLineImpl(shapedRuns, textRange, paragraphWidth, paragraphProperties,
flowDirection, nextLineBreak);
textLine.FinalizeLine();
break;
}
case TextWrapping.WrapWithOverflow:
case TextWrapping.Wrap:
{
textLine = PerformTextWrapping(shapedRuns, textRange, paragraphWidth, paragraphProperties,
flowDirection, 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<ShapedTextCharacters>> SplitShapedRuns(List<ShapedTextCharacters> textRuns, int length)
{
var currentLength = 0;
for (var i = 0; i < textRuns.Count; i++)
{
var currentRun = textRuns[i];
if (currentLength + currentRun.Text.Length < length)
{
currentLength += currentRun.Text.Length;
continue;
}
var firstCount = currentRun.Text.Length >= 1 ? i + 1 : i;
var first = new List<ShapedTextCharacters>(firstCount);
if (firstCount > 1)
{
for (var j = 0; j < i; j++)
{
first.Add(textRuns[j]);
}
}
var secondCount = textRuns.Count - firstCount;
if (currentLength + currentRun.Text.Length == length)
{
var second = secondCount > 0 ? new List<ShapedTextCharacters>(secondCount) : null;
if (second != null)
{
var offset = currentRun.Text.Length >= 1 ? 1 : 0;
for (var j = 0; j < secondCount; j++)
{
second.Add(textRuns[i + j + offset]);
}
}
first.Add(currentRun);
return new SplitResult<List<ShapedTextCharacters>>(first, second);
}
else
{
secondCount++;
var second = new List<ShapedTextCharacters>(secondCount);
var split = currentRun.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<ShapedTextCharacters>>(first, second);
}
}
return new SplitResult<List<ShapedTextCharacters>>(textRuns, null);
}
/// <summary>
/// Shape specified text runs with specified paragraph embedding.
/// </summary>
/// <param name="textRuns">The text runs to shape.</param>
/// <param name="flowDirection">The paragraph embedding level.</param>
/// <param name="resolvedFlowDirection">The resolved flow direction.</param>
/// <returns>
/// A list of shaped text characters.
/// </returns>
private static List<ShapedTextCharacters> ShapeTextRuns(List<TextCharacters> textRuns,
FlowDirection flowDirection, out FlowDirection resolvedFlowDirection)
{
var shapedTextCharacters = new List<ShapedTextCharacters>();
var biDiData = new BidiData((sbyte)flowDirection);
foreach (var textRun in textRuns)
{
biDiData.Append(textRun.Text);
}
var biDi = BidiAlgorithm.Instance.Value!;
biDi.Process(biDiData);
var resolvedEmbeddingLevel = biDi.ResolveEmbeddingLevel(biDiData.Classes);
resolvedFlowDirection =
(resolvedEmbeddingLevel & 1) == 0 ? FlowDirection.LeftToRight : FlowDirection.RightToLeft;
foreach (var shapeableRuns in CoalesceLevels(textRuns, biDi.ResolvedLevels))
{
for (var index = 0; index < shapeableRuns.Count; index++)
{
var currentRun = shapeableRuns[index];
var shapedBuffer = TextShaper.Current.ShapeText(currentRun.Text, currentRun.Properties.Typeface.GlyphTypeface,
currentRun.Properties.FontRenderingEmSize, currentRun.Properties.CultureInfo, currentRun.BidiLevel);
var shapedCharacters = new ShapedTextCharacters(shapedBuffer, currentRun.Properties);
shapedTextCharacters.Add(shapedCharacters);
}
}
return shapedTextCharacters;
}
/// <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<IList<ShapeableTextCharacters>> CoalesceLevels(
IReadOnlyList<TextCharacters> textCharacters,
ReadOnlySlice<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];
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="textRange"></param>
/// <returns>
/// The formatted text runs.
/// </returns>
private static List<TextCharacters> FetchTextRuns(ITextSource textSource, int firstTextSourceIndex,
out TextEndOfLine? endOfLine, out TextRange textRange)
{
var length = 0;
endOfLine = null;
var textRuns = new List<TextCharacters>();
var textRunEnumerator = new TextRunEnumerator(textSource, firstTextSourceIndex);
while (textRunEnumerator.MoveNext())
{
var textRun = textRunEnumerator.Current;
if(textRun == null)
{
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);
length += runLineBreak.PositionWrap;
textRange = new TextRange(firstTextSourceIndex, length);
return textRuns;
}
textRuns.Add(textCharacters);
break;
}
case TextEndOfLine textEndOfLine:
endOfLine = textEndOfLine;
break;
}
length += textRun.Text.Length;
}
textRange = new TextRange(firstTextSourceIndex, length);
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 int MeasureLength(IReadOnlyList<ShapedTextCharacters> textRuns, TextRange textRange,
double paragraphWidth)
{
var currentWidth = 0.0;
var lastCluster = textRange.Start;
foreach (var currentRun in textRuns)
{
for (var i = 0; i < currentRun.ShapedBuffer.Length; i++)
{
var glyphInfo = currentRun.ShapedBuffer[i];
if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth)
{
return lastCluster - textRange.Start;
}
lastCluster = glyphInfo.GlyphCluster;
currentWidth += glyphInfo.GlyphAdvance;
}
}
return textRange.Length;
}
/// <summary>
/// Performs text wrapping returns a list of text lines.
/// </summary>
/// <param name="textRuns"></param>
/// <param name="textRange">The text range that is covered by the text runs.</param>
/// <param name="paragraphWidth">The paragraph width.</param>
/// <param name="paragraphProperties">The text paragraph properties.</param>
/// <param name="flowDirection"></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<ShapedTextCharacters> textRuns, TextRange textRange,
double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection flowDirection,
TextLineBreak? currentLineBreak)
{
var measuredLength = MeasureLength(textRuns, textRange, paragraphWidth);
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;
}
if (measuredLength == 0)
{
measuredLength = 1;
}
var splitResult = SplitShapedRuns(textRuns, measuredLength);
textRange = new TextRange(textRange.Start, measuredLength);
var remainingCharacters = splitResult.Second;
var lineBreak = remainingCharacters?.Count > 0 ?
new TextLineBreak(currentLineBreak?.TextEndOfLine, flowDirection, remainingCharacters) :
null;
if (lineBreak is null && currentLineBreak?.TextEndOfLine != null)
{
lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, flowDirection);
}
TextLineImpl.SortRuns(splitResult.First);
var textLine = new TextLineImpl(splitResult.First, textRange, paragraphWidth, paragraphProperties, flowDirection,
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 shapedBuffer = textShaper.ShapeText(textRun.Text, glyphTypeface, fontRenderingEmSize, cultureInfo, (sbyte)flowDirection);
return new ShapedTextCharacters(shapedBuffer, textRun.Properties);
}
}
}