Browse Source

Allow to process non text TextRuns

Add TextEndOfLine to a TextLine's TextRuns
pull/9765/head
Benedikt Stebner 3 years ago
parent
commit
9f309415c1
  1. 2
      src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs
  2. 23
      src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
  3. 68
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  4. 7
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  5. 133
      src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
  6. 4
      src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs
  7. 106
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  8. 2
      src/Avalonia.Base/Media/TextFormatting/TextRun.cs
  9. 2
      src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
  10. 2
      src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs
  11. 5
      src/Avalonia.Controls/Documents/Run.cs
  12. 4
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs
  13. 63
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
  14. 4
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

2
src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs

@ -21,6 +21,6 @@ namespace Avalonia.Media.TextFormatting
/// Collapses given text line.
/// </summary>
/// <param name="textLine">Text line to collapse.</param>
public abstract List<DrawableTextRun>? Collapse(TextLine textLine);
public abstract List<TextRun>? Collapse(TextLine textLine);
}
}

23
src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs

@ -5,9 +5,11 @@ namespace Avalonia.Media.TextFormatting
{
internal static class TextEllipsisHelper
{
public static List<DrawableTextRun>? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis)
public static List<TextRun>? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis)
{
if (textLine.TextRuns is not List<DrawableTextRun> textRuns || textRuns.Count == 0)
var textRuns = textLine.TextRuns;
if (textRuns == null || textRuns.Count == 0)
{
return null;
}
@ -20,7 +22,7 @@ namespace Avalonia.Media.TextFormatting
if (properties.Width < shapedSymbol.GlyphRun.Size.Width)
{
//Not enough space to fit in the symbol
return new List<DrawableTextRun>(0);
return new List<TextRun>(0);
}
var availableWidth = properties.Width - shapedSymbol.Size.Width;
@ -70,11 +72,11 @@ namespace Avalonia.Media.TextFormatting
collapsedLength += measuredLength;
var collapsedRuns = new List<DrawableTextRun>(textRuns.Count);
var collapsedRuns = new List<TextRun>(textRuns.Count);
if (collapsedLength > 0)
{
var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength);
var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength);
collapsedRuns.AddRange(splitResult.First);
}
@ -84,22 +86,21 @@ namespace Avalonia.Media.TextFormatting
return collapsedRuns;
}
availableWidth -= currentRun.Size.Width;
availableWidth -= shapedRun.Size.Width;
break;
}
case { } drawableRun:
case DrawableTextRun drawableRun:
{
//The whole run needs to fit into available space
if (currentWidth + drawableRun.Size.Width > availableWidth)
{
var collapsedRuns = new List<DrawableTextRun>(textRuns.Count);
var collapsedRuns = new List<TextRun>(textRuns.Count);
if (collapsedLength > 0)
{
var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength);
var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength);
collapsedRuns.AddRange(splitResult.First);
}
@ -109,6 +110,8 @@ namespace Avalonia.Media.TextFormatting
return collapsedRuns;
}
availableWidth -= drawableRun.Size.Width;
break;
}
}

68
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities;
@ -17,20 +16,20 @@ namespace Avalonia.Media.TextFormatting
var textWrapping = paragraphProperties.TextWrapping;
FlowDirection resolvedFlowDirection;
TextLineBreak? nextLineBreak = null;
List<DrawableTextRun> drawableTextRuns;
IReadOnlyList<TextRun> textRuns;
var textRuns = FetchTextRuns(textSource, firstTextSourceIndex,
var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex,
out var textEndOfLine, out var textSourceLength);
if (previousLineBreak?.RemainingRuns != null)
{
resolvedFlowDirection = previousLineBreak.FlowDirection;
drawableTextRuns = previousLineBreak.RemainingRuns.ToList();
textRuns = previousLineBreak.RemainingRuns;
nextLineBreak = previousLineBreak;
}
else
{
drawableTextRuns = ShapeTextRuns(textRuns, paragraphProperties, out resolvedFlowDirection);
textRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, out resolvedFlowDirection);
if (nextLineBreak == null && textEndOfLine != null)
{
@ -44,7 +43,7 @@ namespace Avalonia.Media.TextFormatting
{
case TextWrapping.NoWrap:
{
textLine = new TextLineImpl(drawableTextRuns, firstTextSourceIndex, textSourceLength,
textLine = new TextLineImpl(textRuns, firstTextSourceIndex, textSourceLength,
paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak);
textLine.FinalizeLine();
@ -54,7 +53,7 @@ namespace Avalonia.Media.TextFormatting
case TextWrapping.WrapWithOverflow:
case TextWrapping.Wrap:
{
textLine = PerformTextWrapping(drawableTextRuns, firstTextSourceIndex, paragraphWidth, paragraphProperties,
textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth, paragraphProperties,
resolvedFlowDirection, nextLineBreak);
break;
}
@ -71,7 +70,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>
internal static SplitResult<List<DrawableTextRun>> SplitDrawableRuns(List<DrawableTextRun> textRuns, int length)
internal static SplitResult<IReadOnlyList<TextRun>> SplitTextRuns(IReadOnlyList<TextRun> textRuns, int length)
{
var currentLength = 0;
@ -88,7 +87,7 @@ namespace Avalonia.Media.TextFormatting
var firstCount = currentRun.Length >= 1 ? i + 1 : i;
var first = new List<DrawableTextRun>(firstCount);
var first = new List<TextRun>(firstCount);
if (firstCount > 1)
{
@ -102,7 +101,7 @@ namespace Avalonia.Media.TextFormatting
if (currentLength + currentRun.Length == length)
{
var second = secondCount > 0 ? new List<DrawableTextRun>(secondCount) : null;
var second = secondCount > 0 ? new List<TextRun>(secondCount) : null;
if (second != null)
{
@ -116,13 +115,13 @@ namespace Avalonia.Media.TextFormatting
first.Add(currentRun);
return new SplitResult<List<DrawableTextRun>>(first, second);
return new SplitResult<IReadOnlyList<TextRun>>(first, second);
}
else
{
secondCount++;
var second = new List<DrawableTextRun>(secondCount);
var second = new List<TextRun>(secondCount);
if (currentRun is ShapedTextCharacters shapedTextCharacters)
{
@ -138,11 +137,11 @@ namespace Avalonia.Media.TextFormatting
second.Add(textRuns[i + j]);
}
return new SplitResult<List<DrawableTextRun>>(first, second);
return new SplitResult<IReadOnlyList<TextRun>>(first, second);
}
}
return new SplitResult<List<DrawableTextRun>>(textRuns, null);
return new SplitResult<IReadOnlyList<TextRun>>(textRuns, null);
}
/// <summary>
@ -154,11 +153,11 @@ namespace Avalonia.Media.TextFormatting
/// <returns>
/// A list of shaped text characters.
/// </returns>
private static List<DrawableTextRun> ShapeTextRuns(List<TextRun> textRuns, TextParagraphProperties paragraphProperties,
private static List<TextRun> ShapeTextRuns(List<TextRun> textRuns, TextParagraphProperties paragraphProperties,
out FlowDirection resolvedFlowDirection)
{
var flowDirection = paragraphProperties.FlowDirection;
var drawableTextRuns = new List<DrawableTextRun>();
var shapedRuns = new List<TextRun>();
var biDiData = new BidiData((sbyte)flowDirection);
foreach (var textRun in textRuns)
@ -199,13 +198,6 @@ namespace Avalonia.Media.TextFormatting
switch (currentRun)
{
case DrawableTextRun drawableRun:
{
drawableTextRuns.Add(drawableRun);
break;
}
case ShapeableTextCharacters shapeableRun:
{
var groupedRuns = new List<ShapeableTextCharacters>(2) { shapeableRun };
@ -248,14 +240,20 @@ namespace Avalonia.Media.TextFormatting
shapeableRun.BidiLevel, currentRun.Properties.CultureInfo,
paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing);
drawableTextRuns.AddRange(ShapeTogether(groupedRuns, characterBufferReference, length, shaperOptions));
shapedRuns.AddRange(ShapeTogether(groupedRuns, characterBufferReference, length, shaperOptions));
break;
}
default:
{
shapedRuns.Add(currentRun);
break;
}
}
}
return drawableTextRuns;
return shapedRuns;
}
private static IReadOnlyList<ShapedTextCharacters> ShapeTogether(
@ -390,6 +388,10 @@ namespace Avalonia.Media.TextFormatting
if (textRun == null)
{
textRuns.Add(new TextEndOfParagraph());
textSourceLength += TextRun.DefaultTextSourceLength;
break;
}
@ -465,7 +467,7 @@ namespace Avalonia.Media.TextFormatting
return false;
}
private static bool TryMeasureLength(IReadOnlyList<DrawableTextRun> textRuns, double paragraphWidth, out int measuredLength)
private static bool TryMeasureLength(IReadOnlyList<TextRun> textRuns, double paragraphWidth, out int measuredLength)
{
measuredLength = 0;
var currentWidth = 0.0;
@ -502,7 +504,7 @@ namespace Avalonia.Media.TextFormatting
break;
}
case { } drawableTextRun:
case DrawableTextRun drawableTextRun:
{
if (currentWidth + drawableTextRun.Size.Width >= paragraphWidth)
{
@ -510,7 +512,13 @@ namespace Avalonia.Media.TextFormatting
}
measuredLength += currentRun.Length;
currentWidth += currentRun.Size.Width;
currentWidth += drawableTextRun.Size.Width;
break;
}
default:
{
measuredLength += currentRun.Length;
break;
}
@ -553,7 +561,7 @@ namespace Avalonia.Media.TextFormatting
/// <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,
private static TextLineImpl PerformTextWrapping(IReadOnlyList<TextRun> textRuns, int firstTextSourceIndex,
double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection,
TextLineBreak? currentLineBreak)
{
@ -681,7 +689,7 @@ namespace Avalonia.Media.TextFormatting
break;
}
var splitResult = SplitDrawableRuns(textRuns, measuredLength);
var splitResult = SplitTextRuns(textRuns, measuredLength);
var remainingCharacters = splitResult.Second;

7
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

@ -448,7 +448,7 @@ namespace Avalonia.Media.TextFormatting
var textLine = TextFormatter.Current.FormatLine(_textSource, _textSourceLength, MaxWidth,
_paragraphProperties, previousLine?.TextLineBreak);
if (textLine == null || textLine.Length == 0 || textLine.TextRuns.Count == 0 && textLine.TextLineBreak?.TextEndOfLine is TextEndOfParagraph)
if(textLine == null || textLine.Length == 0)
{
if (previousLine != null && previousLine.NewLineLength > 0)
{
@ -501,6 +501,11 @@ namespace Avalonia.Media.TextFormatting
break;
}
if (textLine.TextLineBreak?.TextEndOfLine is TextEndOfParagraph)
{
break;
}
}
//Make sure the TextLayout always contains at least on empty line

133
src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs

@ -39,9 +39,11 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/>
public override TextRun Symbol { get; }
public override List<DrawableTextRun>? Collapse(TextLine textLine)
public override List<TextRun>? Collapse(TextLine textLine)
{
if (textLine.TextRuns is not List<DrawableTextRun> textRuns || textRuns.Count == 0)
var textRuns = textLine.TextRuns;
if (textRuns == null || textRuns.Count == 0)
{
return null;
}
@ -52,7 +54,7 @@ namespace Avalonia.Media.TextFormatting
if (Width < shapedSymbol.GlyphRun.Size.Width)
{
return new List<DrawableTextRun>(0);
return new List<TextRun>(0);
}
// Overview of ellipsis structure
@ -66,92 +68,101 @@ namespace Avalonia.Media.TextFormatting
switch (currentRun)
{
case ShapedTextCharacters shapedRun:
{
currentWidth += currentRun.Size.Width;
if (currentWidth > availableWidth)
{
shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength);
var collapsedRuns = new List<DrawableTextRun>(textRuns.Count);
currentWidth += shapedRun.Size.Width;
if (measuredLength > 0)
if (currentWidth > availableWidth)
{
List<DrawableTextRun>? preSplitRuns = null;
List<DrawableTextRun>? postSplitRuns;
if (_prefixLength > 0)
{
var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns,
Math.Min(_prefixLength, measuredLength));
shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength);
collapsedRuns.AddRange(splitResult.First);
var collapsedRuns = new List<TextRun>(textRuns.Count);
preSplitRuns = splitResult.First;
postSplitRuns = splitResult.Second;
}
else
if (measuredLength > 0)
{
postSplitRuns = textRuns;
}
IReadOnlyList<TextRun>? preSplitRuns = null;
IReadOnlyList<TextRun>? postSplitRuns;
collapsedRuns.Add(shapedSymbol);
if (_prefixLength > 0)
{
var splitResult = TextFormatterImpl.SplitTextRuns(textRuns,
Math.Min(_prefixLength, measuredLength));
if (measuredLength <= _prefixLength || postSplitRuns is null)
{
return collapsedRuns;
}
collapsedRuns.AddRange(splitResult.First);
var availableSuffixWidth = availableWidth;
preSplitRuns = splitResult.First;
postSplitRuns = splitResult.Second;
}
else
{
postSplitRuns = textRuns;
}
if (preSplitRuns is not null)
{
foreach (var run in preSplitRuns)
collapsedRuns.Add(shapedSymbol);
if (measuredLength <= _prefixLength || postSplitRuns is null)
{
availableSuffixWidth -= run.Size.Width;
return collapsedRuns;
}
}
for (var i = postSplitRuns.Count - 1; i >= 0; i--)
{
var run = postSplitRuns[i];
var availableSuffixWidth = availableWidth;
switch (run)
if (preSplitRuns is not null)
{
case ShapedTextCharacters endShapedRun:
foreach (var run in preSplitRuns)
{
if (endShapedRun.TryMeasureCharactersBackwards(availableSuffixWidth,
out var suffixCount, out var suffixWidth))
if (run is DrawableTextRun drawableTextRun)
{
availableSuffixWidth -= suffixWidth;
availableSuffixWidth -= drawableTextRun.Size.Width;
}
}
}
if (suffixCount > 0)
for (var i = postSplitRuns.Count - 1; i >= 0; i--)
{
var run = postSplitRuns[i];
switch (run)
{
case ShapedTextCharacters endShapedRun:
{
var splitSuffix =
endShapedRun.Split(run.Length - suffixCount);
if (endShapedRun.TryMeasureCharactersBackwards(availableSuffixWidth,
out var suffixCount, out var suffixWidth))
{
availableSuffixWidth -= suffixWidth;
collapsedRuns.Add(splitSuffix.Second!);
}
}
if (suffixCount > 0)
{
var splitSuffix =
endShapedRun.Split(run.Length - suffixCount);
collapsedRuns.Add(splitSuffix.Second!);
}
}
break;
break;
}
}
}
}
}
else
{
collapsedRuns.Add(shapedSymbol);
else
{
collapsedRuns.Add(shapedSymbol);
}
return collapsedRuns;
}
return collapsedRuns;
}
availableWidth -= shapedRun.Size.Width;
break;
}
}
break;
}
case DrawableTextRun drawableTextRun:
{
availableWidth -= drawableTextRun.Size.Width;
availableWidth -= currentRun.Size.Width;
break;
}
}
runIndex++;
}

4
src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs

@ -5,7 +5,7 @@ namespace Avalonia.Media.TextFormatting
public class TextLineBreak
{
public TextLineBreak(TextEndOfLine? textEndOfLine = null, FlowDirection flowDirection = FlowDirection.LeftToRight,
IReadOnlyList<DrawableTextRun>? remainingRuns = null)
IReadOnlyList<TextRun>? remainingRuns = null)
{
TextEndOfLine = textEndOfLine;
FlowDirection = flowDirection;
@ -25,6 +25,6 @@ namespace Avalonia.Media.TextFormatting
/// <summary>
/// Get the remaining runs that were split up by the <see cref="TextFormatter"/> during the formatting process.
/// </summary>
public IReadOnlyList<DrawableTextRun>? RemainingRuns { get; }
public IReadOnlyList<TextRun>? RemainingRuns { get; }
}
}

106
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@ -6,13 +6,13 @@ namespace Avalonia.Media.TextFormatting
{
internal class TextLineImpl : TextLine
{
private readonly List<DrawableTextRun> _textRuns;
private IReadOnlyList<TextRun> _textRuns;
private readonly double _paragraphWidth;
private readonly TextParagraphProperties _paragraphProperties;
private TextLineMetrics _textLineMetrics;
private readonly FlowDirection _resolvedFlowDirection;
public TextLineImpl(List<DrawableTextRun> textRuns, int firstTextSourceIndex, int length, double paragraphWidth,
public TextLineImpl(IReadOnlyList<TextRun> textRuns, int firstTextSourceIndex, int length, double paragraphWidth,
TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection = FlowDirection.LeftToRight,
TextLineBreak? lineBreak = null, bool hasCollapsed = false)
{
@ -86,11 +86,14 @@ namespace Avalonia.Media.TextFormatting
foreach (var textRun in _textRuns)
{
var offsetY = GetBaselineOffset(this, textRun);
if (textRun is DrawableTextRun drawable)
{
var offsetY = GetBaselineOffset(this, drawable);
textRun.Draw(drawingContext, new Point(currentX, currentY + offsetY));
drawable.Draw(drawingContext, new Point(currentX, currentY + offsetY));
currentX += textRun.Size.Width;
currentX += drawable.Size.Width;
}
}
}
@ -180,7 +183,14 @@ namespace Avalonia.Media.TextFormatting
{
var lastRun = _textRuns[_textRuns.Count - 1];
return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.Length, lastRun.Size.Width);
var size = 0.0;
if (lastRun is DrawableTextRun drawableTextRun)
{
size = drawableTextRun.Size.Width;
}
return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.Length, size);
}
// process hit that happens within the line
@ -220,9 +230,16 @@ namespace Avalonia.Media.TextFormatting
currentRun = _textRuns[j];
if (currentDistance + currentRun.Size.Width <= distance)
if(currentRun is not ShapedTextCharacters)
{
continue;
}
shapedRun = (ShapedTextCharacters)currentRun;
if (currentDistance + shapedRun.Size.Width <= distance)
{
currentDistance += currentRun.Size.Width;
currentDistance += shapedRun.Size.Width;
currentPosition -= currentRun.Length;
continue;
@ -234,12 +251,19 @@ namespace Avalonia.Media.TextFormatting
characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance);
if (i < _textRuns.Count - 1 && currentDistance + currentRun.Size.Width < distance)
if (currentRun is DrawableTextRun drawableTextRun)
{
currentDistance += currentRun.Size.Width;
if (i < _textRuns.Count - 1 && currentDistance + drawableTextRun.Size.Width < distance)
{
currentDistance += drawableTextRun.Size.Width;
currentPosition += currentRun.Length;
currentPosition += currentRun.Length;
continue;
}
}
else
{
continue;
}
@ -249,7 +273,7 @@ namespace Avalonia.Media.TextFormatting
return characterHit;
}
private static CharacterHit GetRunCharacterHit(DrawableTextRun run, int currentPosition, double distance)
private static CharacterHit GetRunCharacterHit(TextRun run, int currentPosition, double distance)
{
CharacterHit characterHit;
@ -270,9 +294,9 @@ namespace Avalonia.Media.TextFormatting
break;
}
default:
case DrawableTextRun drawableTextRun:
{
if (distance < run.Size.Width / 2)
if (distance < drawableTextRun.Size.Width / 2)
{
characterHit = new CharacterHit(currentPosition);
}
@ -282,6 +306,10 @@ namespace Avalonia.Media.TextFormatting
}
break;
}
default:
characterHit = new CharacterHit(currentPosition, run.Length);
break;
}
return characterHit;
@ -307,7 +335,7 @@ namespace Avalonia.Media.TextFormatting
{
var i = index;
var rightToLeftWidth = currentRun.Size.Width;
var rightToLeftWidth = shapedRun.Size.Width;
while (i + 1 <= _textRuns.Count - 1)
{
@ -317,7 +345,7 @@ namespace Avalonia.Media.TextFormatting
{
i++;
rightToLeftWidth += nextRun.Size.Width;
rightToLeftWidth += nextShapedRun.Size.Width;
continue;
}
@ -331,7 +359,10 @@ namespace Avalonia.Media.TextFormatting
{
currentRun = _textRuns[i];
rightToLeftWidth -= currentRun.Size.Width;
if (currentRun is DrawableTextRun drawable)
{
rightToLeftWidth -= drawable.Size.Width;
}
if (currentPosition + currentRun.Length >= characterIndex)
{
@ -355,8 +386,13 @@ namespace Avalonia.Media.TextFormatting
return Math.Max(0, currentDistance + distance);
}
if (currentRun is DrawableTextRun drawableTextRun)
{
currentDistance += drawableTextRun.Size.Width;
}
//No hit hit found so we add the full width
currentDistance += currentRun.Size.Width;
currentPosition += currentRun.Length;
remainingLength -= currentRun.Length;
}
@ -380,8 +416,12 @@ namespace Avalonia.Media.TextFormatting
return Math.Max(0, currentDistance - distance);
}
if (currentRun is DrawableTextRun drawableTextRun)
{
currentDistance -= drawableTextRun.Size.Width;
}
//No hit hit found so we add the full width
currentDistance -= currentRun.Size.Width;
currentPosition += currentRun.Length;
remainingLength -= currentRun.Length;
}
@ -391,7 +431,7 @@ namespace Avalonia.Media.TextFormatting
}
private static bool TryGetDistanceFromCharacterHit(
DrawableTextRun currentRun,
TextRun currentRun,
CharacterHit characterHit,
int currentPosition,
int remainingLength,
@ -432,7 +472,7 @@ namespace Avalonia.Media.TextFormatting
break;
}
default:
case DrawableTextRun drawableTextRun:
{
if (characterIndex == currentPosition)
{
@ -441,7 +481,7 @@ namespace Avalonia.Media.TextFormatting
if (characterIndex == currentPosition + currentRun.Length)
{
distance = currentRun.Size.Width;
distance = drawableTextRun.Size.Width;
return true;
@ -449,6 +489,10 @@ namespace Avalonia.Media.TextFormatting
break;
}
default:
{
return false;
}
}
return false;
@ -943,7 +987,7 @@ namespace Avalonia.Media.TextFormatting
return this;
}
private static sbyte GetRunBidiLevel(DrawableTextRun run, FlowDirection flowDirection)
private static sbyte GetRunBidiLevel(TextRun run, FlowDirection flowDirection)
{
if (run is ShapedTextCharacters shapedTextCharacters)
{
@ -1039,16 +1083,18 @@ namespace Avalonia.Media.TextFormatting
minLevelToReverse--;
}
_textRuns.Clear();
var textRuns = new List<TextRun>(_textRuns.Count);
current = orderedRun;
while (current != null)
{
_textRuns.Add(current.Run);
textRuns.Add(current.Run);
current = current.Next;
}
_textRuns = textRuns;
}
/// <summary>
@ -1286,7 +1332,7 @@ namespace Avalonia.Media.TextFormatting
{
var runIndex = 0;
textPosition = FirstTextSourceIndex;
DrawableTextRun? previousRun = null;
TextRun? previousRun = null;
while (runIndex < _textRuns.Count)
{
@ -1346,7 +1392,6 @@ namespace Avalonia.Media.TextFormatting
break;
}
default:
{
if (codepointIndex == textPosition)
@ -1363,6 +1408,7 @@ namespace Avalonia.Media.TextFormatting
break;
}
}
runIndex++;
@ -1436,7 +1482,7 @@ namespace Avalonia.Media.TextFormatting
break;
}
case { } drawableTextRun:
case DrawableTextRun drawableTextRun:
{
widthIncludingWhitespace += drawableTextRun.Size.Width;
@ -1558,7 +1604,7 @@ namespace Avalonia.Media.TextFormatting
private sealed class OrderedBidiRun
{
public OrderedBidiRun(DrawableTextRun run, sbyte level)
public OrderedBidiRun(TextRun run, sbyte level)
{
Run = run;
Level = level;
@ -1566,7 +1612,7 @@ namespace Avalonia.Media.TextFormatting
public sbyte Level { get; }
public DrawableTextRun Run { get; }
public TextRun Run { get; }
public OrderedBidiRun? Next { get; set; }
}

2
src/Avalonia.Base/Media/TextFormatting/TextRun.cs

@ -40,7 +40,7 @@ namespace Avalonia.Media.TextFormatting
{
unsafe
{
var characterBuffer = _textRun.CharacterBufferReference.CharacterBuffer;
var characterBuffer = new CharacterBufferRange(_textRun.CharacterBufferReference, _textRun.Length);
fixed (char* charsPtr = characterBuffer.Span)
{

2
src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs

@ -26,7 +26,7 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/>
public override TextRun Symbol { get; }
public override List<DrawableTextRun>? Collapse(TextLine textLine)
public override List<TextRun>? Collapse(TextLine textLine)
{
return TextEllipsisHelper.Collapse(textLine, this, false);
}

2
src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs

@ -31,7 +31,7 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/>
public override TextRun Symbol { get; }
public override List<DrawableTextRun>? Collapse(TextLine textLine)
public override List<TextRun>? Collapse(TextLine textLine)
{
return TextEllipsisHelper.Collapse(textLine, this, true);
}

5
src/Avalonia.Controls/Documents/Run.cs

@ -54,6 +54,11 @@ namespace Avalonia.Controls.Documents
{
var text = Text ?? "";
if (string.IsNullOrEmpty(text))
{
return;
}
var textRunProperties = CreateTextRunProperties();
var textCharacters = new TextCharacters(text, textRunProperties);

4
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs

@ -1,6 +1,4 @@
using System;
using Avalonia.Media.TextFormatting;
using Avalonia.Utilities;
using Avalonia.Media.TextFormatting;
namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{

63
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs

@ -62,6 +62,69 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
private class TextSourceWithDummyRuns : ITextSource
{
private readonly TextRunProperties _properties;
private readonly List<ValueSpan<TextRun>> _textRuns;
public TextSourceWithDummyRuns(TextRunProperties properties)
{
_properties = properties;
_textRuns = new List<ValueSpan<TextRun>>
{
new ValueSpan<TextRun>(0, 5, new TextCharacters("Hello", _properties)),
new ValueSpan<TextRun>(5, 1, new DummyRun()),
new ValueSpan<TextRun>(6, 1, new DummyRun()),
new ValueSpan<TextRun>(7, 6, new TextCharacters(" World", _properties))
};
}
public TextRun GetTextRun(int textSourceIndex)
{
foreach (var run in _textRuns)
{
if (textSourceIndex < run.Start + run.Length)
{
return run.Value;
}
}
return new TextEndOfParagraph();
}
private class DummyRun : TextRun
{
public DummyRun()
{
Length = DefaultTextSourceLength;
}
public override int Length { get; }
}
}
[Fact]
public void Should_Format_TextLine_With_Non_Text_TextRuns()
{
using (Start())
{
var defaultProperties =
new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Black);
var textSource = new TextSourceWithDummyRuns(defaultProperties);
var formatter = new TextFormatterImpl();
var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
Assert.Equal(5, textLine.TextRuns.Count);
Assert.Equal(14, textLine.Length);
}
}
[Fact]
public void Should_Format_TextRuns_With_TextRunStyles()
{

4
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

@ -326,7 +326,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
Assert.Equal(currentDistance, textLine.GetDistanceFromCharacterHit(new CharacterHit(s_multiLineText.Length)));
var actualDistance = textLine.GetDistanceFromCharacterHit(new CharacterHit(s_multiLineText.Length));
Assert.Equal(currentDistance, actualDistance);
}
}

Loading…
Cancel
Save