|
|
@@ -1,49 +1,41 @@
|
|
|
-using System.Collections.Generic;
|
|
|
+using System;
|
|
|
+using System.Collections.Generic;
|
|
|
using Avalonia.Media.TextFormatting.Unicode;
|
|
|
-using Avalonia.Platform;
|
|
|
-using Avalonia.Utilities;
|
|
|
|
|
|
namespace Avalonia.Media.TextFormatting
|
|
|
{
|
|
|
internal class TextFormatterImpl : TextFormatter
|
|
|
{
|
|
|
- private static readonly ReadOnlySlice<char> s_ellipsis = new ReadOnlySlice<char>(new[] { '\u2026' });
|
|
|
-
|
|
|
/// <inheritdoc cref="TextFormatter.FormatLine"/>
|
|
|
public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
|
|
|
TextParagraphProperties paragraphProperties, TextLineBreak previousLineBreak = null)
|
|
|
{
|
|
|
- var textTrimming = paragraphProperties.TextTrimming;
|
|
|
var textWrapping = paragraphProperties.TextWrapping;
|
|
|
- TextLine textLine = null;
|
|
|
|
|
|
var textRuns = FetchTextRuns(textSource, firstTextSourceIndex, previousLineBreak, out var nextLineBreak);
|
|
|
|
|
|
var textRange = GetTextRange(textRuns);
|
|
|
|
|
|
- if (textTrimming != TextTrimming.None)
|
|
|
- {
|
|
|
- textLine = PerformTextTrimming(textRuns, textRange, paragraphWidth, paragraphProperties);
|
|
|
- }
|
|
|
- else
|
|
|
+ TextLine textLine;
|
|
|
+
|
|
|
+ switch (textWrapping)
|
|
|
{
|
|
|
- switch (textWrapping)
|
|
|
- {
|
|
|
- case TextWrapping.NoWrap:
|
|
|
- {
|
|
|
- var textLineMetrics =
|
|
|
- TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties);
|
|
|
+ case TextWrapping.NoWrap:
|
|
|
+ {
|
|
|
+ var textLineMetrics =
|
|
|
+ TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties);
|
|
|
|
|
|
- textLine = new TextLineImpl(textRuns, textLineMetrics, nextLineBreak);
|
|
|
- break;
|
|
|
- }
|
|
|
- case TextWrapping.WrapWithOverflow:
|
|
|
- case TextWrapping.Wrap:
|
|
|
- {
|
|
|
- textLine = PerformTextWrapping(textRuns, textRange, paragraphWidth, paragraphProperties);
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
+ textLine = new TextLineImpl(textRuns, textLineMetrics, nextLineBreak);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ case TextWrapping.WrapWithOverflow:
|
|
|
+ case TextWrapping.Wrap:
|
|
|
+ {
|
|
|
+ textLine = PerformTextWrapping(textRuns, textRange, paragraphWidth, paragraphProperties);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ default:
|
|
|
+ throw new ArgumentOutOfRangeException();
|
|
|
}
|
|
|
|
|
|
return textLine;
|
|
|
@@ -174,87 +166,6 @@ namespace Avalonia.Media.TextFormatting
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Performs text trimming and returns a trimmed line.
|
|
|
- /// </summary>
|
|
|
- /// <param name="textRuns">The text runs to perform the trimming on.</param>
|
|
|
- /// <param name="textRange">The text range that is covered by the text runs.</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></returns>
|
|
|
- private static TextLine PerformTextTrimming(IReadOnlyList<ShapedTextCharacters> textRuns, TextRange textRange,
|
|
|
- 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.Properties);
|
|
|
-
|
|
|
- var measuredLength = MeasureText(currentRun, availableWidth - ellipsisRun.GlyphRun.Bounds.Width);
|
|
|
-
|
|
|
- if (textTrimming == TextTrimming.WordEllipsis)
|
|
|
- {
|
|
|
- if (measuredLength < textRange.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<ShapedTextCharacters>(splitResult.First.Count + 1);
|
|
|
-
|
|
|
- trimmedRuns.AddRange(splitResult.First);
|
|
|
-
|
|
|
- trimmedRuns.Add(ellipsisRun);
|
|
|
-
|
|
|
- var textLineMetrics =
|
|
|
- TextLineMetrics.Create(trimmedRuns, textRange, paragraphWidth, paragraphProperties);
|
|
|
-
|
|
|
- return new TextLineImpl(trimmedRuns, textLineMetrics);
|
|
|
- }
|
|
|
-
|
|
|
- availableWidth -= currentRun.GlyphRun.Bounds.Width;
|
|
|
-
|
|
|
- runIndex++;
|
|
|
- }
|
|
|
-
|
|
|
- return new TextLineImpl(textRuns,
|
|
|
- TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties));
|
|
|
- }
|
|
|
-
|
|
|
/// <summary>
|
|
|
/// Performs text wrapping returns a list of text lines.
|
|
|
/// </summary>
|
|
|
@@ -269,7 +180,7 @@ namespace Avalonia.Media.TextFormatting
|
|
|
var availableWidth = paragraphWidth;
|
|
|
var currentWidth = 0.0;
|
|
|
var runIndex = 0;
|
|
|
- var length = 0;
|
|
|
+ var currentLength = 0;
|
|
|
|
|
|
while (runIndex < textRuns.Count)
|
|
|
{
|
|
|
@@ -279,58 +190,53 @@ namespace Avalonia.Media.TextFormatting
|
|
|
{
|
|
|
var measuredLength = MeasureText(currentRun, paragraphWidth - currentWidth);
|
|
|
|
|
|
+ var breakFound = false;
|
|
|
+
|
|
|
+ var currentBreakPosition = 0;
|
|
|
+
|
|
|
if (measuredLength < currentRun.Text.Length)
|
|
|
{
|
|
|
- if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow)
|
|
|
- {
|
|
|
- var lineBreaker = new LineBreakEnumerator(currentRun.Text.Skip(measuredLength));
|
|
|
+ var lineBreaker = new LineBreakEnumerator(currentRun.Text);
|
|
|
|
|
|
- if (lineBreaker.MoveNext())
|
|
|
- {
|
|
|
- measuredLength += lineBreaker.Current.PositionWrap;
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- measuredLength = currentRun.Text.Length;
|
|
|
- }
|
|
|
- }
|
|
|
- else
|
|
|
+ while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
|
|
|
{
|
|
|
- var currentBreakPosition = -1;
|
|
|
+ var nextBreakPosition = lineBreaker.Current.PositionWrap;
|
|
|
|
|
|
- var lineBreaker = new LineBreakEnumerator(currentRun.Text);
|
|
|
-
|
|
|
- while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
|
|
|
+ if (nextBreakPosition == 0 || nextBreakPosition > measuredLength)
|
|
|
{
|
|
|
- var nextBreakPosition = lineBreaker.Current.PositionWrap;
|
|
|
+ break;
|
|
|
+ }
|
|
|
|
|
|
- if (nextBreakPosition == 0)
|
|
|
- {
|
|
|
- break;
|
|
|
- }
|
|
|
+ breakFound = lineBreaker.Current.Required ||
|
|
|
+ lineBreaker.Current.PositionWrap != currentRun.Text.Length;
|
|
|
|
|
|
- if (nextBreakPosition > measuredLength)
|
|
|
- {
|
|
|
- break;
|
|
|
- }
|
|
|
+ currentBreakPosition = nextBreakPosition;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- currentBreakPosition = nextBreakPosition;
|
|
|
- }
|
|
|
+ if (breakFound)
|
|
|
+ {
|
|
|
+ measuredLength = currentBreakPosition;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow)
|
|
|
+ {
|
|
|
+ var lineBreaker = new LineBreakEnumerator(currentRun.Text.Skip(currentBreakPosition));
|
|
|
|
|
|
- if (currentBreakPosition != -1)
|
|
|
+ if (lineBreaker.MoveNext())
|
|
|
{
|
|
|
- measuredLength = currentBreakPosition;
|
|
|
+ measuredLength = currentBreakPosition + lineBreaker.Current.PositionWrap;
|
|
|
}
|
|
|
-
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- length += measuredLength;
|
|
|
+ currentLength += measuredLength;
|
|
|
|
|
|
- var splitResult = SplitTextRuns(textRuns, length);
|
|
|
+ var splitResult = SplitTextRuns(textRuns, currentLength);
|
|
|
|
|
|
var textLineMetrics = TextLineMetrics.Create(splitResult.First,
|
|
|
- new TextRange(textRange.Start, length), paragraphWidth, paragraphProperties);
|
|
|
+ new TextRange(textRange.Start, currentLength), paragraphWidth, paragraphProperties);
|
|
|
|
|
|
var lineBreak = splitResult.Second != null && splitResult.Second.Count > 0 ?
|
|
|
new TextLineBreak(splitResult.Second) :
|
|
|
@@ -341,7 +247,7 @@ namespace Avalonia.Media.TextFormatting
|
|
|
|
|
|
currentWidth += currentRun.GlyphRun.Bounds.Width;
|
|
|
|
|
|
- length += currentRun.GlyphRun.Characters.Length;
|
|
|
+ currentLength += currentRun.GlyphRun.Characters.Length;
|
|
|
|
|
|
runIndex++;
|
|
|
}
|
|
|
@@ -356,7 +262,7 @@ namespace Avalonia.Media.TextFormatting
|
|
|
/// <param name="textCharacters">The text run.</param>
|
|
|
/// <param name="availableWidth">The available width.</param>
|
|
|
/// <returns></returns>
|
|
|
- private static int MeasureText(ShapedTextCharacters textCharacters, double availableWidth)
|
|
|
+ internal static int MeasureText(ShapedTextCharacters textCharacters, double availableWidth)
|
|
|
{
|
|
|
var glyphRun = textCharacters.GlyphRun;
|
|
|
|
|
|
@@ -391,10 +297,8 @@ namespace Avalonia.Media.TextFormatting
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
- for (var i = 0; i < glyphRun.GlyphAdvances.Length; i++)
|
|
|
+ foreach (var advance in glyphRun.GlyphAdvances)
|
|
|
{
|
|
|
- var advance = glyphRun.GlyphAdvances[i];
|
|
|
-
|
|
|
if (currentWidth + advance > availableWidth)
|
|
|
{
|
|
|
break;
|
|
|
@@ -423,21 +327,6 @@ namespace Avalonia.Media.TextFormatting
|
|
|
return lastCluster - firstCluster;
|
|
|
}
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Creates an ellipsis.
|
|
|
- /// </summary>
|
|
|
- /// <param name="properties">The text run properties.</param>
|
|
|
- /// <returns></returns>
|
|
|
- private static ShapedTextCharacters CreateEllipsisRun(TextRunProperties properties)
|
|
|
- {
|
|
|
- var formatterImpl = AvaloniaLocator.Current.GetService<ITextShaperImpl>();
|
|
|
-
|
|
|
- var glyphRun = formatterImpl.ShapeText(s_ellipsis, properties.Typeface, properties.FontRenderingEmSize,
|
|
|
- properties.CultureInfo);
|
|
|
-
|
|
|
- return new ShapedTextCharacters(glyphRun, properties);
|
|
|
- }
|
|
|
-
|
|
|
/// <summary>
|
|
|
/// Gets the text range that is covered by the text runs.
|
|
|
/// </summary>
|
|
|
@@ -470,7 +359,7 @@ namespace Avalonia.Media.TextFormatting
|
|
|
/// <param name="textRuns">The text run's.</param>
|
|
|
/// <param name="length">The length to split at.</param>
|
|
|
/// <returns>The split text runs.</returns>
|
|
|
- private static SplitTextRunsResult SplitTextRuns(IReadOnlyList<ShapedTextCharacters> textRuns, int length)
|
|
|
+ internal static SplitTextRunsResult SplitTextRuns(IReadOnlyList<ShapedTextCharacters> textRuns, int length)
|
|
|
{
|
|
|
var currentLength = 0;
|
|
|
|
|
|
@@ -543,7 +432,7 @@ namespace Avalonia.Media.TextFormatting
|
|
|
return new SplitTextRunsResult(textRuns, null);
|
|
|
}
|
|
|
|
|
|
- private readonly struct SplitTextRunsResult
|
|
|
+ internal readonly struct SplitTextRunsResult
|
|
|
{
|
|
|
public SplitTextRunsResult(IReadOnlyList<ShapedTextCharacters> first, IReadOnlyList<ShapedTextCharacters> second)
|
|
|
{
|