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.
 
 
 

417 lines
15 KiB

using System;
using System.Collections.Generic;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
using Avalonia.Utility;
namespace Avalonia.Media.TextFormatting
{
internal class SimpleTextFormatter : TextFormatter
{
private static readonly ReadOnlySlice<char> s_ellipsis = new ReadOnlySlice<char>(new[] { '\u2026' });
/// <summary>
/// Formats a text line.
/// </summary>
/// <param name="textSource">The text source.</param>
/// <param name="firstTextSourceIndex">The first character index to start the text line from.</param>
/// <param name="paragraphWidth">A <see cref="double"/> value that specifies the width of the paragraph that the line fills.</param>
/// <param name="paragraphProperties">A <see cref="TextParagraphProperties"/> value that represents paragraph properties,
/// such as TextWrapping, TextAlignment, or TextStyle.</param>
/// <returns>The formatted line.</returns>
public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
TextParagraphProperties paragraphProperties)
{
var textTrimming = paragraphProperties.TextTrimming;
var textWrapping = paragraphProperties.TextWrapping;
TextLine textLine;
var textRuns = FormatTextRuns(textSource, firstTextSourceIndex, out var textPointer);
if (textTrimming != TextTrimming.None)
{
textLine = PerformTextTrimming(textPointer, textRuns, paragraphWidth, paragraphProperties);
}
else
{
if (textWrapping == TextWrapping.Wrap)
{
textLine = PerformTextWrapping(textPointer, textRuns, paragraphWidth, paragraphProperties);
}
else
{
var textLineMetrics =
TextLineMetrics.Create(textRuns, paragraphWidth, paragraphProperties.TextAlignment);
textLine = new SimpleTextLine(textPointer, textRuns, textLineMetrics);
}
}
return textLine;
}
/// <summary>
/// Formats text runs with optional text style overrides.
/// </summary>
/// <param name="textSource">The text source.</param>
/// <param name="firstTextSourceIndex">The first text source index.</param>
/// <param name="textPointer">The text pointer that covers the formatted text runs.</param>
/// <returns>
/// The formatted text runs.
/// </returns>
private List<ShapedTextRun> FormatTextRuns(ITextSource textSource, int firstTextSourceIndex, out TextPointer textPointer)
{
var start = firstTextSourceIndex;
var textRuns = new List<ShapedTextRun>();
while (true)
{
var textRun = textSource.GetTextRun(firstTextSourceIndex);
if (textRun.Text.IsEmpty)
{
break;
}
if (textRun is TextEndOfLine)
{
break;
}
if (!(textRun is TextCharacters))
{
throw new NotSupportedException("Run type not supported by the formatter.");
}
var runText = textRun.Text;
while (!runText.IsEmpty)
{
var shapableTextStyleRun = CreateShapableTextStyleRun(runText, textRun.Style);
var shapedRun = new ShapedTextRun(runText.Take(shapableTextStyleRun.TextPointer.Length),
shapableTextStyleRun.Style);
textRuns.Add(shapedRun);
runText = runText.Skip(shapedRun.Text.Length);
}
firstTextSourceIndex += textRun.Text.Length;
}
textPointer = new TextPointer(start, firstTextSourceIndex - start);
return textRuns;
}
/// <summary>
/// Performs text trimming and returns a trimmed line.
/// </summary>
/// <param name="paragraphWidth">A <see cref="double"/> value that specifies the width of the paragraph that the line fills.</param>
/// <param name="paragraphProperties">A <see cref="TextParagraphProperties"/> value that represents paragraph properties,
/// such as TextWrapping, TextAlignment, or TextStyle.</param>
/// <param name="textRuns">The text runs to perform the trimming on.</param>
/// <param name="text">The text that was used to construct the text runs.</param>
/// <returns></returns>
private TextLine PerformTextTrimming(TextPointer text, IReadOnlyList<ShapedTextRun> textRuns,
double paragraphWidth, TextParagraphProperties paragraphProperties)
{
var textTrimming = paragraphProperties.TextTrimming;
var availableWidth = paragraphWidth;
var currentWidth = 0.0;
var runIndex = 0;
while (runIndex < textRuns.Count)
{
var currentRun = textRuns[runIndex];
currentWidth += currentRun.GlyphRun.Bounds.Width;
if (currentWidth > availableWidth)
{
var ellipsisRun = CreateEllipsisRun(currentRun.Style);
var measuredLength = MeasureText(currentRun, availableWidth - ellipsisRun.GlyphRun.Bounds.Width);
if (textTrimming == TextTrimming.WordEllipsis)
{
if (measuredLength < text.End)
{
var currentBreakPosition = 0;
var lineBreaker = new LineBreakEnumerator(currentRun.Text);
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
{
var nextBreakPosition = lineBreaker.Current.PositionWrap;
if (nextBreakPosition == 0)
{
break;
}
if (nextBreakPosition > measuredLength)
{
break;
}
currentBreakPosition = nextBreakPosition;
}
measuredLength = currentBreakPosition;
}
}
var splitResult = SplitTextRuns(textRuns, measuredLength);
var trimmedRuns = new List<ShapedTextRun>(splitResult.First.Count + 1);
trimmedRuns.AddRange(splitResult.First);
trimmedRuns.Add(ellipsisRun);
var textLineMetrics =
TextLineMetrics.Create(trimmedRuns, paragraphWidth, paragraphProperties.TextAlignment);
return new SimpleTextLine(text.Take(measuredLength), trimmedRuns, textLineMetrics);
}
availableWidth -= currentRun.GlyphRun.Bounds.Width;
runIndex++;
}
return new SimpleTextLine(text, textRuns,
TextLineMetrics.Create(textRuns, paragraphWidth, paragraphProperties.TextAlignment));
}
/// <summary>
/// Performs text wrapping returns a list of text lines.
/// </summary>
/// <param name="paragraphProperties">The text paragraph properties.</param>
/// <param name="textRuns">The text run'S.</param>
/// <param name="text">The text to analyze for break opportunities.</param>
/// <param name="paragraphWidth"></param>
/// <returns></returns>
private TextLine PerformTextWrapping(TextPointer text, IReadOnlyList<ShapedTextRun> textRuns,
double paragraphWidth, TextParagraphProperties paragraphProperties)
{
var availableWidth = paragraphWidth;
var currentWidth = 0.0;
var runIndex = 0;
while (runIndex < textRuns.Count)
{
var currentRun = textRuns[runIndex];
currentWidth += currentRun.GlyphRun.Bounds.Width;
if (currentWidth > availableWidth)
{
var measuredLength = MeasureText(currentRun, paragraphWidth);
if (measuredLength < text.End)
{
var currentBreakPosition = -1;
var lineBreaker = new LineBreakEnumerator(currentRun.Text);
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
{
var nextBreakPosition = lineBreaker.Current.PositionWrap;
if (nextBreakPosition == 0)
{
break;
}
if (nextBreakPosition > measuredLength)
{
break;
}
currentBreakPosition = nextBreakPosition;
}
if (currentBreakPosition != -1)
{
measuredLength = currentBreakPosition;
}
}
var splitResult = SplitTextRuns(textRuns, measuredLength);
var textLineMetrics =
TextLineMetrics.Create(splitResult.First, paragraphWidth, paragraphProperties.TextAlignment);
return new SimpleTextLine(text.Take(measuredLength), splitResult.First, textLineMetrics);
}
availableWidth -= currentRun.GlyphRun.Bounds.Width;
runIndex++;
}
return new SimpleTextLine(text, textRuns,
TextLineMetrics.Create(textRuns, paragraphWidth, paragraphProperties.TextAlignment));
}
/// <summary>
/// Measures the number of characters that fits into available width.
/// </summary>
/// <param name="textRun">The text run.</param>
/// <param name="availableWidth">The available width.</param>
/// <returns></returns>
private int MeasureText(ShapedTextRun textRun, double availableWidth)
{
if (textRun.GlyphRun.Bounds.Width < availableWidth)
{
return textRun.Text.Length;
}
var measuredWidth = 0.0;
var index = 0;
for (; index < textRun.GlyphRun.GlyphAdvances.Length; index++)
{
var advance = textRun.GlyphRun.GlyphAdvances[index];
if (measuredWidth + advance > availableWidth)
{
break;
}
measuredWidth += advance;
}
var cluster = textRun.GlyphRun.GlyphClusters[index];
var characterHit = textRun.GlyphRun.FindNearestCharacterHit(cluster, out _);
return characterHit.FirstCharacterIndex - textRun.GlyphRun.Characters.Start +
(textRun.GlyphRun.IsLeftToRight ? characterHit.TrailingLength : 0);
}
/// <summary>
/// Creates an ellipsis.
/// </summary>
/// <param name="textStyle">The text style.</param>
/// <returns></returns>
private static ShapedTextRun CreateEllipsisRun(TextStyle textStyle)
{
var formatterImpl = AvaloniaLocator.Current.GetService<ITextShaperImpl>();
var glyphRun = formatterImpl.ShapeText(s_ellipsis, textStyle.TextFormat);
return new ShapedTextRun(glyphRun, textStyle);
}
private readonly struct SplitTextRunsResult
{
public SplitTextRunsResult(IReadOnlyList<ShapedTextRun> first, IReadOnlyList<ShapedTextRun> second)
{
First = first;
Second = second;
}
/// <summary>
/// Gets the first text runs.
/// </summary>
/// <value>
/// The first text runs.
/// </value>
public IReadOnlyList<ShapedTextRun> First { get; }
/// <summary>
/// Gets the second text runs.
/// </summary>
/// <value>
/// The second text runs.
/// </value>
public IReadOnlyList<ShapedTextRun> Second { get; }
}
/// <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></returns>
private static SplitTextRunsResult SplitTextRuns(IReadOnlyList<ShapedTextRun> textRuns, int length)
{
var currentLength = 0;
for (var i = 0; i < textRuns.Count; i++)
{
var currentRun = textRuns[i];
if (currentLength + currentRun.GlyphRun.Characters.Length < length)
{
currentLength += currentRun.GlyphRun.Characters.Length;
continue;
}
var firstCount = currentRun.GlyphRun.Characters.Length > 1 ? i + 1 : i;
var first = new ShapedTextRun[firstCount];
if (firstCount > 1)
{
for (var j = 0; j < i; j++)
{
first[j] = textRuns[j];
}
}
var secondCount = textRuns.Count - firstCount;
if (currentLength + currentRun.GlyphRun.Characters.Length == length)
{
var second = new ShapedTextRun[secondCount];
var offset = currentRun.GlyphRun.Characters.Length > 1 ? 1 : 0;
if (secondCount > 0)
{
for (var j = 0; j < secondCount; j++)
{
second[j] = textRuns[i + j + offset];
}
}
first[i] = currentRun;
return new SplitTextRunsResult(first, second);
}
else
{
secondCount++;
var second = new ShapedTextRun[secondCount];
if (secondCount > 0)
{
for (var j = 1; j < secondCount; j++)
{
second[j] = textRuns[i + j];
}
}
var split = currentRun.Split(length - currentLength);
first[i] = split.First;
second[0] = split.Second;
return new SplitTextRunsResult(first, second);
}
}
return new SplitTextRunsResult(textRuns, null);
}
}
}