Browse Source

Merge pull request #9765 from Gillibald/characterBufferReference

Allow TextFormatter to process non text runs
pull/9949/head
Max Katz 4 years ago
committed by GitHub
parent
commit
b6eafdf660
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs
  2. 2
      src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs
  3. 23
      src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
  4. 218
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  5. 7
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  6. 133
      src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
  7. 4
      src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs
  8. 106
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  9. 4
      src/Avalonia.Base/Media/TextFormatting/TextRun.cs
  10. 2
      src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
  11. 2
      src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs
  12. 5
      src/Avalonia.Controls/Documents/Run.cs
  13. 4
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs
  14. 63
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
  15. 4
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

11
src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs

@ -140,7 +140,7 @@ namespace Avalonia.Media.TextFormatting
throw new ArgumentOutOfRangeException(nameof(index)); throw new ArgumentOutOfRangeException(nameof(index));
} }
#endif #endif
return CharacterBufferReference.CharacterBuffer.Span[CharacterBufferReference.OffsetToFirstChar + index]; return CharacterBuffer.Span[CharacterBufferReference.OffsetToFirstChar + index];
} }
} }
@ -157,8 +157,7 @@ namespace Avalonia.Media.TextFormatting
/// <summary> /// <summary>
/// Gets a span from the character buffer range /// Gets a span from the character buffer range
/// </summary> /// </summary>
public ReadOnlySpan<char> Span => public ReadOnlySpan<char> Span => CharacterBuffer.Span.Slice(OffsetToFirstChar, Length);
CharacterBufferReference.CharacterBuffer.Span.Slice(CharacterBufferReference.OffsetToFirstChar, Length);
/// <summary> /// <summary>
/// Gets the character memory buffer /// Gets the character memory buffer
@ -174,7 +173,7 @@ namespace Avalonia.Media.TextFormatting
/// <summary> /// <summary>
/// Indicate whether the character buffer range is empty /// Indicate whether the character buffer range is empty
/// </summary> /// </summary>
internal bool IsEmpty => CharacterBufferReference.CharacterBuffer.Length == 0 || Length <= 0; internal bool IsEmpty => CharacterBuffer.Length == 0 || Length <= 0;
internal CharacterBufferRange Take(int length) internal CharacterBufferRange Take(int length)
{ {
@ -208,9 +207,7 @@ namespace Avalonia.Media.TextFormatting
return new CharacterBufferRange(new CharacterBufferReference(), 0); return new CharacterBufferRange(new CharacterBufferReference(), 0);
} }
var characterBufferReference = new CharacterBufferReference( var characterBufferReference = new CharacterBufferReference(CharacterBuffer, OffsetToFirstChar + length);
CharacterBufferReference.CharacterBuffer,
CharacterBufferReference.OffsetToFirstChar + length);
return new CharacterBufferRange(characterBufferReference, Length - length); return new CharacterBufferRange(characterBufferReference, Length - length);
} }

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

@ -21,6 +21,6 @@ namespace Avalonia.Media.TextFormatting
/// Collapses given text line. /// Collapses given text line.
/// </summary> /// </summary>
/// <param name="textLine">Text line to collapse.</param> /// <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 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; return null;
} }
@ -20,7 +22,7 @@ namespace Avalonia.Media.TextFormatting
if (properties.Width < shapedSymbol.GlyphRun.Size.Width) if (properties.Width < shapedSymbol.GlyphRun.Size.Width)
{ {
//Not enough space to fit in the symbol //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; var availableWidth = properties.Width - shapedSymbol.Size.Width;
@ -70,11 +72,11 @@ namespace Avalonia.Media.TextFormatting
collapsedLength += measuredLength; collapsedLength += measuredLength;
var collapsedRuns = new List<DrawableTextRun>(textRuns.Count); var collapsedRuns = new List<TextRun>(textRuns.Count);
if (collapsedLength > 0) if (collapsedLength > 0)
{ {
var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength); var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength);
collapsedRuns.AddRange(splitResult.First); collapsedRuns.AddRange(splitResult.First);
} }
@ -84,22 +86,21 @@ namespace Avalonia.Media.TextFormatting
return collapsedRuns; return collapsedRuns;
} }
availableWidth -= currentRun.Size.Width; availableWidth -= shapedRun.Size.Width;
break; break;
} }
case { } drawableRun: case DrawableTextRun drawableRun:
{ {
//The whole run needs to fit into available space //The whole run needs to fit into available space
if (currentWidth + drawableRun.Size.Width > availableWidth) if (currentWidth + drawableRun.Size.Width > availableWidth)
{ {
var collapsedRuns = new List<DrawableTextRun>(textRuns.Count); var collapsedRuns = new List<TextRun>(textRuns.Count);
if (collapsedLength > 0) if (collapsedLength > 0)
{ {
var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength); var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength);
collapsedRuns.AddRange(splitResult.First); collapsedRuns.AddRange(splitResult.First);
} }
@ -109,6 +110,8 @@ namespace Avalonia.Media.TextFormatting
return collapsedRuns; return collapsedRuns;
} }
availableWidth -= drawableRun.Size.Width;
break; break;
} }
} }

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

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities; using Avalonia.Utilities;
@ -17,20 +16,20 @@ namespace Avalonia.Media.TextFormatting
var textWrapping = paragraphProperties.TextWrapping; var textWrapping = paragraphProperties.TextWrapping;
FlowDirection resolvedFlowDirection; FlowDirection resolvedFlowDirection;
TextLineBreak? nextLineBreak = null; 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); out var textEndOfLine, out var textSourceLength);
if (previousLineBreak?.RemainingRuns != null) if (previousLineBreak?.RemainingRuns != null)
{ {
resolvedFlowDirection = previousLineBreak.FlowDirection; resolvedFlowDirection = previousLineBreak.FlowDirection;
drawableTextRuns = previousLineBreak.RemainingRuns.ToList(); textRuns = previousLineBreak.RemainingRuns;
nextLineBreak = previousLineBreak; nextLineBreak = previousLineBreak;
} }
else else
{ {
drawableTextRuns = ShapeTextRuns(textRuns, paragraphProperties, out resolvedFlowDirection); textRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, out resolvedFlowDirection);
if (nextLineBreak == null && textEndOfLine != null) if (nextLineBreak == null && textEndOfLine != null)
{ {
@ -44,7 +43,7 @@ namespace Avalonia.Media.TextFormatting
{ {
case TextWrapping.NoWrap: case TextWrapping.NoWrap:
{ {
textLine = new TextLineImpl(drawableTextRuns, firstTextSourceIndex, textSourceLength, textLine = new TextLineImpl(textRuns, firstTextSourceIndex, textSourceLength,
paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak); paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak);
textLine.FinalizeLine(); textLine.FinalizeLine();
@ -54,7 +53,7 @@ namespace Avalonia.Media.TextFormatting
case TextWrapping.WrapWithOverflow: case TextWrapping.WrapWithOverflow:
case TextWrapping.Wrap: case TextWrapping.Wrap:
{ {
textLine = PerformTextWrapping(drawableTextRuns, firstTextSourceIndex, paragraphWidth, paragraphProperties, textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth, paragraphProperties,
resolvedFlowDirection, nextLineBreak); resolvedFlowDirection, nextLineBreak);
break; break;
} }
@ -71,7 +70,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="textRuns">The text run's.</param> /// <param name="textRuns">The text run's.</param>
/// <param name="length">The length to split at.</param> /// <param name="length">The length to split at.</param>
/// <returns>The split text runs.</returns> /// <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; var currentLength = 0;
@ -88,7 +87,7 @@ namespace Avalonia.Media.TextFormatting
var firstCount = currentRun.Length >= 1 ? i + 1 : i; var firstCount = currentRun.Length >= 1 ? i + 1 : i;
var first = new List<DrawableTextRun>(firstCount); var first = new List<TextRun>(firstCount);
if (firstCount > 1) if (firstCount > 1)
{ {
@ -102,7 +101,7 @@ namespace Avalonia.Media.TextFormatting
if (currentLength + currentRun.Length == length) 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) if (second != null)
{ {
@ -116,13 +115,13 @@ namespace Avalonia.Media.TextFormatting
first.Add(currentRun); first.Add(currentRun);
return new SplitResult<List<DrawableTextRun>>(first, second); return new SplitResult<IReadOnlyList<TextRun>>(first, second);
} }
else else
{ {
secondCount++; secondCount++;
var second = new List<DrawableTextRun>(secondCount); var second = new List<TextRun>(secondCount);
if (currentRun is ShapedTextRun shapedTextCharacters) if (currentRun is ShapedTextRun shapedTextCharacters)
{ {
@ -131,18 +130,18 @@ namespace Avalonia.Media.TextFormatting
first.Add(split.First); first.Add(split.First);
second.Add(split.Second!); second.Add(split.Second!);
} }
for (var j = 1; j < secondCount; j++) for (var j = 1; j < secondCount; j++)
{ {
second.Add(textRuns[i + j]); 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> /// <summary>
@ -154,11 +153,11 @@ namespace Avalonia.Media.TextFormatting
/// <returns> /// <returns>
/// A list of shaped text characters. /// A list of shaped text characters.
/// </returns> /// </returns>
private static List<DrawableTextRun> ShapeTextRuns(List<TextRun> textRuns, TextParagraphProperties paragraphProperties, private static List<TextRun> ShapeTextRuns(List<TextRun> textRuns, TextParagraphProperties paragraphProperties,
out FlowDirection resolvedFlowDirection) out FlowDirection resolvedFlowDirection)
{ {
var flowDirection = paragraphProperties.FlowDirection; var flowDirection = paragraphProperties.FlowDirection;
var drawableTextRuns = new List<DrawableTextRun>(); var shapedRuns = new List<TextRun>();
var biDiData = new BidiData((sbyte)flowDirection); var biDiData = new BidiData((sbyte)flowDirection);
foreach (var textRun in textRuns) foreach (var textRun in textRuns)
@ -199,13 +198,6 @@ namespace Avalonia.Media.TextFormatting
switch (currentRun) switch (currentRun)
{ {
case DrawableTextRun drawableRun:
{
drawableTextRuns.Add(drawableRun);
break;
}
case UnshapedTextRun shapeableRun: case UnshapedTextRun shapeableRun:
{ {
var groupedRuns = new List<UnshapedTextRun>(2) { shapeableRun }; var groupedRuns = new List<UnshapedTextRun>(2) { shapeableRun };
@ -245,17 +237,23 @@ namespace Avalonia.Media.TextFormatting
var shaperOptions = new TextShaperOptions(currentRun.Properties!.Typeface.GlyphTypeface, var shaperOptions = new TextShaperOptions(currentRun.Properties!.Typeface.GlyphTypeface,
currentRun.Properties.FontRenderingEmSize, currentRun.Properties.FontRenderingEmSize,
shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, shapeableRun.BidiLevel, currentRun.Properties.CultureInfo,
paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing); paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing);
drawableTextRuns.AddRange(ShapeTogether(groupedRuns, characterBufferReference, length, shaperOptions)); shapedRuns.AddRange(ShapeTogether(groupedRuns, characterBufferReference, length, shaperOptions));
break;
}
default:
{
shapedRuns.Add(currentRun);
break; break;
} }
} }
} }
return drawableTextRuns; return shapedRuns;
} }
private static IReadOnlyList<ShapedTextRun> ShapeTogether( private static IReadOnlyList<ShapedTextRun> ShapeTogether(
@ -390,6 +388,10 @@ namespace Avalonia.Media.TextFormatting
if (textRun == null) if (textRun == null)
{ {
textRuns.Add(new TextEndOfParagraph());
textSourceLength += TextRun.DefaultTextSourceLength;
break; break;
} }
@ -465,7 +467,7 @@ namespace Avalonia.Media.TextFormatting
return false; 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; measuredLength = 0;
var currentWidth = 0.0; var currentWidth = 0.0;
@ -476,7 +478,7 @@ namespace Avalonia.Media.TextFormatting
{ {
case ShapedTextRun shapedTextCharacters: case ShapedTextRun shapedTextCharacters:
{ {
if(shapedTextCharacters.ShapedBuffer.Length > 0) if (shapedTextCharacters.ShapedBuffer.Length > 0)
{ {
var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphInfos[0].GlyphCluster; var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphInfos[0].GlyphCluster;
var lastCluster = firstCluster; var lastCluster = firstCluster;
@ -497,12 +499,12 @@ namespace Avalonia.Media.TextFormatting
} }
measuredLength += currentRun.Length; measuredLength += currentRun.Length;
} }
break; break;
} }
case { } drawableTextRun: case DrawableTextRun drawableTextRun:
{ {
if (currentWidth + drawableTextRun.Size.Width >= paragraphWidth) if (currentWidth + drawableTextRun.Size.Width >= paragraphWidth)
{ {
@ -510,14 +512,20 @@ namespace Avalonia.Media.TextFormatting
} }
measuredLength += currentRun.Length; measuredLength += currentRun.Length;
currentWidth += currentRun.Size.Width; currentWidth += drawableTextRun.Size.Width;
break;
}
default:
{
measuredLength += currentRun.Length;
break; break;
} }
} }
} }
found: found:
return measuredLength != 0; return measuredLength != 0;
} }
@ -553,13 +561,13 @@ namespace Avalonia.Media.TextFormatting
/// <param name="resolvedFlowDirection"></param> /// <param name="resolvedFlowDirection"></param>
/// <param name="currentLineBreak">The current line break if the line was explicitly broken.</param> /// <param name="currentLineBreak">The current line break if the line was explicitly broken.</param>
/// <returns>The wrapped text line.</returns> /// <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, double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection,
TextLineBreak? currentLineBreak) TextLineBreak? currentLineBreak)
{ {
if(textRuns.Count == 0) if (textRuns.Count == 0)
{ {
return CreateEmptyTextLine(firstTextSourceIndex,paragraphWidth, paragraphProperties); return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties);
} }
if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength)) if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength))
@ -575,46 +583,24 @@ namespace Avalonia.Media.TextFormatting
for (var index = 0; index < textRuns.Count; index++) for (var index = 0; index < textRuns.Count; index++)
{ {
var currentRun = textRuns[index];
var runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
var lineBreaker = new LineBreakEnumerator(runText);
var breakFound = false; var breakFound = false;
while (lineBreaker.MoveNext()) var currentRun = textRuns[index];
{
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) switch (currentRun)
{ {
if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow) case ShapedTextRun:
{ {
if (lastWrapPosition > 0) var runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
{
currentPosition = lastWrapPosition;
breakFound = true; var lineBreaker = new LineBreakEnumerator(runText);
break; while (lineBreaker.MoveNext())
}
//Find next possible wrap position (overflow)
if (index < textRuns.Count - 1)
{ {
if (lineBreaker.Current.PositionWrap != currentRun.Length) if (lineBreaker.Current.Required &&
currentLength + lineBreaker.Current.PositionMeasure <= measuredLength)
{ {
//We already found the next possible wrap position. //Explicit break found
breakFound = true; breakFound = true;
currentPosition = currentLength + lineBreaker.Current.PositionWrap; currentPosition = currentLength + lineBreaker.Current.PositionWrap;
@ -622,51 +608,81 @@ namespace Avalonia.Media.TextFormatting
break; break;
} }
while (lineBreaker.MoveNext() && index < textRuns.Count) if (currentLength + lineBreaker.Current.PositionMeasure > measuredLength)
{ {
currentPosition += lineBreaker.Current.PositionWrap; if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow)
if (lineBreaker.Current.PositionWrap != currentRun.Length)
{ {
break; if (lastWrapPosition > 0)
} {
currentPosition = lastWrapPosition;
index++; breakFound = true;
break;
}
//Find next possible wrap position (overflow)
if (index < textRuns.Count - 1)
{
if (lineBreaker.Current.PositionWrap != currentRun.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.Length)
{
break;
}
index++;
if (index >= textRuns.Count)
{
break;
}
currentRun = textRuns[index];
runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
lineBreaker = new LineBreakEnumerator(runText);
}
}
else
{
currentPosition = currentLength + lineBreaker.Current.PositionWrap;
}
breakFound = true;
if (index >= textRuns.Count)
{
break; break;
} }
currentRun = textRuns[index]; //We overflowed so we use the last available wrap position.
currentPosition = lastWrapPosition == 0 ? measuredLength : lastWrapPosition;
runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length); breakFound = true;
lineBreaker = new LineBreakEnumerator(runText); break;
} }
}
else
{
currentPosition = currentLength + lineBreaker.Current.PositionWrap;
}
breakFound = true; if (lineBreaker.Current.PositionMeasure != lineBreaker.Current.PositionWrap)
{
lastWrapPosition = currentLength + lineBreaker.Current.PositionWrap;
}
}
break; 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) if (!breakFound)
@ -681,12 +697,12 @@ namespace Avalonia.Media.TextFormatting
break; break;
} }
var splitResult = SplitDrawableRuns(textRuns, measuredLength); var splitResult = SplitTextRuns(textRuns, measuredLength);
var remainingCharacters = splitResult.Second; var remainingCharacters = splitResult.Second;
var lineBreak = remainingCharacters?.Count > 0 ? var lineBreak = remainingCharacters?.Count > 0 ?
new TextLineBreak(currentLineBreak?.TextEndOfLine, resolvedFlowDirection, remainingCharacters) : new TextLineBreak(null, resolvedFlowDirection, remainingCharacters) :
null; null;
if (lineBreak is null && currentLineBreak?.TextEndOfLine != null) if (lineBreak is null && currentLineBreak?.TextEndOfLine != null)

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

@ -448,7 +448,7 @@ namespace Avalonia.Media.TextFormatting
var textLine = TextFormatter.Current.FormatLine(_textSource, _textSourceLength, MaxWidth, var textLine = TextFormatter.Current.FormatLine(_textSource, _textSourceLength, MaxWidth,
_paragraphProperties, previousLine?.TextLineBreak); _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) if (previousLine != null && previousLine.NewLineLength > 0)
{ {
@ -501,6 +501,11 @@ namespace Avalonia.Media.TextFormatting
break; break;
} }
if (textLine.TextLineBreak?.TextEndOfLine is TextEndOfParagraph)
{
break;
}
} }
//Make sure the TextLayout always contains at least on empty line //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/> /// <inheritdoc/>
public override TextRun Symbol { get; } 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; return null;
} }
@ -52,7 +54,7 @@ namespace Avalonia.Media.TextFormatting
if (Width < shapedSymbol.GlyphRun.Size.Width) if (Width < shapedSymbol.GlyphRun.Size.Width)
{ {
return new List<DrawableTextRun>(0); return new List<TextRun>(0);
} }
// Overview of ellipsis structure // Overview of ellipsis structure
@ -66,92 +68,101 @@ namespace Avalonia.Media.TextFormatting
switch (currentRun) switch (currentRun)
{ {
case ShapedTextRun shapedRun: case ShapedTextRun shapedRun:
{
currentWidth += currentRun.Size.Width;
if (currentWidth > availableWidth)
{ {
shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength); currentWidth += shapedRun.Size.Width;
var collapsedRuns = new List<DrawableTextRun>(textRuns.Count);
if (measuredLength > 0) if (currentWidth > availableWidth)
{ {
List<DrawableTextRun>? preSplitRuns = null; shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength);
List<DrawableTextRun>? postSplitRuns;
if (_prefixLength > 0)
{
var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns,
Math.Min(_prefixLength, measuredLength));
collapsedRuns.AddRange(splitResult.First); var collapsedRuns = new List<TextRun>(textRuns.Count);
preSplitRuns = splitResult.First; if (measuredLength > 0)
postSplitRuns = splitResult.Second;
}
else
{ {
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) collapsedRuns.AddRange(splitResult.First);
{
return collapsedRuns;
}
var availableSuffixWidth = availableWidth; preSplitRuns = splitResult.First;
postSplitRuns = splitResult.Second;
}
else
{
postSplitRuns = textRuns;
}
if (preSplitRuns is not null) collapsedRuns.Add(shapedSymbol);
{
foreach (var run in preSplitRuns) if (measuredLength <= _prefixLength || postSplitRuns is null)
{ {
availableSuffixWidth -= run.Size.Width; return collapsedRuns;
} }
}
for (var i = postSplitRuns.Count - 1; i >= 0; i--) var availableSuffixWidth = availableWidth;
{
var run = postSplitRuns[i];
switch (run) if (preSplitRuns is not null)
{ {
case ShapedTextRun endShapedRun: foreach (var run in preSplitRuns)
{ {
if (endShapedRun.TryMeasureCharactersBackwards(availableSuffixWidth, if (run is DrawableTextRun drawableTextRun)
out var suffixCount, out var suffixWidth))
{ {
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 ShapedTextRun endShapedRun:
{ {
var splitSuffix = if (endShapedRun.TryMeasureCharactersBackwards(availableSuffixWidth,
endShapedRun.Split(run.Length - suffixCount); 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
else {
{ collapsedRuns.Add(shapedSymbol);
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++; runIndex++;
} }

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

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

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

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

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

@ -26,7 +26,7 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/> /// <inheritdoc/>
public override TextRun Symbol { get; } 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); return TextEllipsisHelper.Collapse(textLine, this, false);
} }

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

@ -31,7 +31,7 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/> /// <inheritdoc/>
public override TextRun Symbol { get; } 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); return TextEllipsisHelper.Collapse(textLine, this, true);
} }

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

@ -54,6 +54,11 @@ namespace Avalonia.Controls.Documents
{ {
var text = Text ?? ""; var text = Text ?? "";
if (string.IsNullOrEmpty(text))
{
return;
}
var textRunProperties = CreateTextRunProperties(); var textRunProperties = CreateTextRunProperties();
var textCharacters = new TextCharacters(text, textRunProperties); 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.Media.TextFormatting;
using Avalonia.Utilities;
namespace Avalonia.Skia.UnitTests.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] [Fact]
public void Should_Format_TextRuns_With_TextRunStyles() 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