Browse Source

Merge branch 'master' into treeviewitem-styledproperty

pull/10137/head
Max Katz 4 years ago
committed by GitHub
parent
commit
32a3ae3ed1
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 16
      src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs
  2. 120
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  3. 17
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  4. 15
      src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs
  5. 8
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  6. 30
      src/Avalonia.Base/Media/TextFormatting/WrappingTextLineBreak.cs
  7. 2
      tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs
  8. 5
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs

16
src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs

@ -15,9 +15,7 @@ namespace Avalonia.Media.TextFormatting
public override void Justify(TextLine textLine) public override void Justify(TextLine textLine)
{ {
var lineImpl = textLine as TextLineImpl; if (textLine is not TextLineImpl lineImpl)
if(lineImpl is null)
{ {
return; return;
} }
@ -34,14 +32,9 @@ namespace Avalonia.Media.TextFormatting
return; return;
} }
var textLineBreak = lineImpl.TextLineBreak; if (lineImpl.TextLineBreak is { TextEndOfLine: not null, IsSplit: false })
if (textLineBreak is not null && textLineBreak.TextEndOfLine is not null)
{ {
if (textLineBreak.RemainingRuns is null || textLineBreak.RemainingRuns.Count == 0) return;
{
return;
}
} }
var breakOportunities = new Queue<int>(); var breakOportunities = new Queue<int>();
@ -107,7 +100,8 @@ namespace Avalonia.Media.TextFormatting
var glyphIndex = glyphRun.FindGlyphIndex(characterIndex); var glyphIndex = glyphRun.FindGlyphIndex(characterIndex);
var glyphInfo = shapedBuffer.GlyphInfos[glyphIndex]; var glyphInfo = shapedBuffer.GlyphInfos[glyphIndex];
shapedBuffer.GlyphInfos[glyphIndex] = new GlyphInfo(glyphInfo.GlyphIndex, glyphInfo.GlyphCluster, glyphInfo.GlyphAdvance + spacing); shapedBuffer.GlyphInfos[glyphIndex] = new GlyphInfo(glyphInfo.GlyphIndex,
glyphInfo.GlyphCluster, glyphInfo.GlyphAdvance + spacing);
} }
glyphRun.GlyphInfos = shapedBuffer.GlyphInfos; glyphRun.GlyphInfos = shapedBuffer.GlyphInfos;

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

@ -2,7 +2,6 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities; using Avalonia.Utilities;
@ -22,68 +21,55 @@ namespace Avalonia.Media.TextFormatting
public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null) TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null)
{ {
var textWrapping = paragraphProperties.TextWrapping;
FlowDirection resolvedFlowDirection;
TextLineBreak? nextLineBreak = null; TextLineBreak? nextLineBreak = null;
IReadOnlyList<TextRun>? textRuns;
var objectPool = FormattingObjectPool.Instance; var objectPool = FormattingObjectPool.Instance;
var fontManager = FontManager.Current; var fontManager = FontManager.Current;
var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool, // we've wrapped the previous line and need to continue wrapping: ignore the textSource and do that instead
out var textEndOfLine, out var textSourceLength); if (previousLineBreak is WrappingTextLineBreak wrappingTextLineBreak
&& wrappingTextLineBreak.AcquireRemainingRuns() is { } remainingRuns
&& paragraphProperties.TextWrapping != TextWrapping.NoWrap)
{
return PerformTextWrapping(remainingRuns, true, firstTextSourceIndex, paragraphWidth,
paragraphProperties, previousLineBreak.FlowDirection, previousLineBreak, objectPool);
}
RentedList<TextRun>? fetchedRuns = null;
RentedList<TextRun>? shapedTextRuns = null; RentedList<TextRun>? shapedTextRuns = null;
try try
{ {
if (previousLineBreak?.RemainingRuns is { } remainingRuns) fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool, out var textEndOfLine,
{ out var textSourceLength);
resolvedFlowDirection = previousLineBreak.FlowDirection;
textRuns = remainingRuns;
nextLineBreak = previousLineBreak;
shapedTextRuns = null;
}
else
{
shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, fontManager,
out resolvedFlowDirection);
textRuns = shapedTextRuns;
if (nextLineBreak == null && textEndOfLine != null) shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, fontManager,
{ out var resolvedFlowDirection);
nextLineBreak = new TextLineBreak(textEndOfLine, resolvedFlowDirection);
}
}
TextLineImpl textLine; if (nextLineBreak == null && textEndOfLine != null)
{
nextLineBreak = new TextLineBreak(textEndOfLine, resolvedFlowDirection);
}
switch (textWrapping) switch (paragraphProperties.TextWrapping)
{ {
case TextWrapping.NoWrap: case TextWrapping.NoWrap:
{ {
// perf note: if textRuns comes from remainingRuns above, it's very likely coming from this class var textLine = new TextLineImpl(shapedTextRuns.ToArray(), firstTextSourceIndex,
// which already uses an array: ToArray() won't ever be called in this case textSourceLength,
var textRunArray = textRuns as TextRun[] ?? textRuns.ToArray();
textLine = new TextLineImpl(textRunArray, firstTextSourceIndex, textSourceLength,
paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak); paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak);
textLine.FinalizeLine(); textLine.FinalizeLine();
break; return textLine;
} }
case TextWrapping.WrapWithOverflow: case TextWrapping.WrapWithOverflow:
case TextWrapping.Wrap: case TextWrapping.Wrap:
{ {
textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth, return PerformTextWrapping(shapedTextRuns, false, firstTextSourceIndex, paragraphWidth,
paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool, fontManager); paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool);
break;
} }
default: default:
throw new ArgumentOutOfRangeException(nameof(textWrapping)); throw new ArgumentOutOfRangeException(nameof(paragraphProperties.TextWrapping));
} }
return textLine;
} }
finally finally
{ {
@ -108,15 +94,16 @@ namespace Avalonia.Media.TextFormatting
for (var i = 0; i < textRuns.Count; i++) for (var i = 0; i < textRuns.Count; i++)
{ {
var currentRun = textRuns[i]; var currentRun = textRuns[i];
var currentRunLength = currentRun.Length;
if (currentLength + currentRun.Length < length) if (currentLength + currentRunLength < length)
{ {
currentLength += currentRun.Length; currentLength += currentRunLength;
continue; continue;
} }
var firstCount = currentRun.Length >= 1 ? i + 1 : i; var firstCount = currentRunLength >= 1 ? i + 1 : i;
if (firstCount > 1) if (firstCount > 1)
{ {
@ -128,13 +115,13 @@ namespace Avalonia.Media.TextFormatting
var secondCount = textRuns.Count - firstCount; var secondCount = textRuns.Count - firstCount;
if (currentLength + currentRun.Length == length) if (currentLength + currentRunLength == length)
{ {
var second = secondCount > 0 ? objectPool.TextRunLists.Rent() : null; var second = secondCount > 0 ? objectPool.TextRunLists.Rent() : null;
if (second != null) if (second != null)
{ {
var offset = currentRun.Length >= 1 ? 1 : 0; var offset = currentRunLength >= 1 ? 1 : 0;
for (var j = 0; j < secondCount; j++) for (var j = 0; j < secondCount; j++)
{ {
@ -653,7 +640,7 @@ namespace Avalonia.Media.TextFormatting
/// </summary> /// </summary>
/// <returns>The empty text line.</returns> /// <returns>The empty text line.</returns>
public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth, public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth,
TextParagraphProperties paragraphProperties, FontManager fontManager) TextParagraphProperties paragraphProperties)
{ {
var flowDirection = paragraphProperties.FlowDirection; var flowDirection = paragraphProperties.FlowDirection;
var properties = paragraphProperties.DefaultTextRunProperties; var properties = paragraphProperties.DefaultTextRunProperties;
@ -675,21 +662,21 @@ namespace Avalonia.Media.TextFormatting
/// Performs text wrapping returns a list of text lines. /// Performs text wrapping returns a list of text lines.
/// </summary> /// </summary>
/// <param name="textRuns"></param> /// <param name="textRuns"></param>
/// <param name="canReuseTextRunList">Whether <see cref="textRuns"/> can be reused to store the split runs.</param>
/// <param name="firstTextSourceIndex">The first text source index.</param> /// <param name="firstTextSourceIndex">The first text source index.</param>
/// <param name="paragraphWidth">The paragraph width.</param> /// <param name="paragraphWidth">The paragraph width.</param>
/// <param name="paragraphProperties">The text paragraph properties.</param> /// <param name="paragraphProperties">The text paragraph properties.</param>
/// <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>
/// <param name="objectPool">A pool used to get reusable formatting objects.</param> /// <param name="objectPool">A pool used to get reusable formatting objects.</param>
/// <param name="fontManager">The font manager to use.</param>
/// <returns>The wrapped text line.</returns> /// <returns>The wrapped text line.</returns>
private static TextLineImpl PerformTextWrapping(IReadOnlyList<TextRun> textRuns, int firstTextSourceIndex, private static TextLineImpl PerformTextWrapping(List<TextRun> textRuns, bool canReuseTextRunList,
double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection, int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties,
TextLineBreak? currentLineBreak, FormattingObjectPool objectPool, FontManager fontManager) FlowDirection resolvedFlowDirection, TextLineBreak? currentLineBreak, FormattingObjectPool objectPool)
{ {
if (textRuns.Count == 0) if (textRuns.Count == 0)
{ {
return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties, fontManager); return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties);
} }
if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength)) if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength))
@ -819,13 +806,37 @@ namespace Avalonia.Media.TextFormatting
try try
{ {
var textLineBreak = postSplitRuns?.Count > 0 ? TextLineBreak? textLineBreak;
new TextLineBreak(null, resolvedFlowDirection, postSplitRuns.ToArray()) : if (postSplitRuns?.Count > 0)
null; {
List<TextRun> remainingRuns;
// reuse the list as much as possible:
// if canReuseTextRunList == true it's coming from previous remaining runs
if (canReuseTextRunList)
{
remainingRuns = textRuns;
remainingRuns.Clear();
}
else
{
remainingRuns = new List<TextRun>();
}
if (textLineBreak is null && currentLineBreak?.TextEndOfLine != null) for (var i = 0; i < postSplitRuns.Count; ++i)
{
remainingRuns.Add(postSplitRuns[i]);
}
textLineBreak = new WrappingTextLineBreak(null, resolvedFlowDirection, remainingRuns);
}
else if (currentLineBreak?.TextEndOfLine is { } textEndOfLine)
{ {
textLineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection); textLineBreak = new TextLineBreak(textEndOfLine, resolvedFlowDirection);
}
else
{
textLineBreak = null;
} }
var textLine = new TextLineImpl(preSplitRuns.ToArray(), firstTextSourceIndex, measuredLength, var textLine = new TextLineImpl(preSplitRuns.ToArray(), firstTextSourceIndex, measuredLength,
@ -833,6 +844,7 @@ namespace Avalonia.Media.TextFormatting
textLineBreak); textLineBreak);
textLine.FinalizeLine(); textLine.FinalizeLine();
return textLine; return textLine;
} }
finally finally

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

@ -416,9 +416,11 @@ namespace Avalonia.Media.TextFormatting
width = lineWidth; width = lineWidth;
} }
if (left > textLine.Start) var start = textLine.Start;
if (left > start)
{ {
left = textLine.Start; left = start;
} }
height += textLine.Height; height += textLine.Height;
@ -427,12 +429,10 @@ namespace Avalonia.Media.TextFormatting
private TextLine[] CreateTextLines() private TextLine[] CreateTextLines()
{ {
var objectPool = FormattingObjectPool.Instance; var objectPool = FormattingObjectPool.Instance;
var fontManager = FontManager.Current;
if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight)) if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight))
{ {
var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties, var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties);
fontManager);
Bounds = new Rect(0, 0, 0, textLine.Height); Bounds = new Rect(0, 0, 0, textLine.Height);
@ -461,7 +461,7 @@ namespace Avalonia.Media.TextFormatting
if (previousLine != null && previousLine.NewLineLength > 0) if (previousLine != null && previousLine.NewLineLength > 0)
{ {
var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth, var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth,
_paragraphProperties, fontManager); _paragraphProperties);
textLines.Add(emptyTextLine); textLines.Add(emptyTextLine);
@ -504,7 +504,7 @@ namespace Avalonia.Media.TextFormatting
//Fulfill max lines constraint //Fulfill max lines constraint
if (MaxLines > 0 && textLines.Count >= MaxLines) if (MaxLines > 0 && textLines.Count >= MaxLines)
{ {
if (textLine.TextLineBreak?.RemainingRuns is not null) if (textLine.TextLineBreak is { IsSplit: true })
{ {
textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(width)); textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(width));
} }
@ -521,8 +521,7 @@ namespace Avalonia.Media.TextFormatting
//Make sure the TextLayout always contains at least on empty line //Make sure the TextLayout always contains at least on empty line
if (textLines.Count == 0) if (textLines.Count == 0)
{ {
var textLine = var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties);
TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties, fontManager);
textLines.Add(textLine); textLines.Add(textLine);

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

@ -1,15 +1,13 @@
using System.Collections.Generic; namespace Avalonia.Media.TextFormatting
namespace Avalonia.Media.TextFormatting
{ {
public class TextLineBreak public class TextLineBreak
{ {
public TextLineBreak(TextEndOfLine? textEndOfLine = null, FlowDirection flowDirection = FlowDirection.LeftToRight, public TextLineBreak(TextEndOfLine? textEndOfLine = null,
IReadOnlyList<TextRun>? remainingRuns = null) FlowDirection flowDirection = FlowDirection.LeftToRight, bool isSplit = false)
{ {
TextEndOfLine = textEndOfLine; TextEndOfLine = textEndOfLine;
FlowDirection = flowDirection; FlowDirection = flowDirection;
RemainingRuns = remainingRuns; IsSplit = isSplit;
} }
/// <summary> /// <summary>
@ -23,8 +21,9 @@ namespace Avalonia.Media.TextFormatting
public FlowDirection FlowDirection { get; } public FlowDirection FlowDirection { get; }
/// <summary> /// <summary>
/// Get the remaining runs that were split up by the <see cref="TextFormatter"/> during the formatting process. /// Gets whether there were remaining runs after this line break,
/// that were split up by the <see cref="TextFormatter"/> during the formatting process.
/// </summary> /// </summary>
public IReadOnlyList<TextRun>? RemainingRuns { get; } public bool IsSplit { get; }
} }
} }

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

@ -1285,13 +1285,11 @@ namespace Avalonia.Media.TextFormatting
{ {
case ShapedTextRun textRun: case ShapedTextRun textRun:
{ {
var properties = textRun.Properties; var textMetrics = textRun.TextMetrics;
var textMetrics =
new TextMetrics(properties.CachedGlyphTypeface, properties.FontRenderingEmSize);
if (fontRenderingEmSize < properties.FontRenderingEmSize) if (fontRenderingEmSize < textMetrics.FontRenderingEmSize)
{ {
fontRenderingEmSize = properties.FontRenderingEmSize; fontRenderingEmSize = textMetrics.FontRenderingEmSize;
if (ascent > textMetrics.Ascent) if (ascent > textMetrics.Ascent)
{ {

30
src/Avalonia.Base/Media/TextFormatting/WrappingTextLineBreak.cs

@ -0,0 +1,30 @@
using System.Collections.Generic;
using System.Diagnostics;
namespace Avalonia.Media.TextFormatting
{
/// <summary>Represents a line break that occurred due to wrapping.</summary>
internal sealed class WrappingTextLineBreak : TextLineBreak
{
private List<TextRun>? _remainingRuns;
public WrappingTextLineBreak(TextEndOfLine? textEndOfLine, FlowDirection flowDirection,
List<TextRun> remainingRuns)
: base(textEndOfLine, flowDirection, isSplit: true)
{
Debug.Assert(remainingRuns.Count > 0);
_remainingRuns = remainingRuns;
}
/// <summary>
/// Gets the remaining runs from this line break, and clears them from this line break.
/// </summary>
/// <returns>A list of text runs.</returns>
public List<TextRun>? AcquireRemainingRuns()
{
var remainingRuns = _remainingRuns;
_remainingRuns = null;
return remainingRuns;
}
}
}

2
tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs

@ -15,7 +15,7 @@ namespace Avalonia.Benchmarks.Text;
public class HugeTextLayout : IDisposable public class HugeTextLayout : IDisposable
{ {
private static readonly Random s_rand = new(); private static readonly Random s_rand = new();
private static readonly bool s_useSkia = true; private static readonly bool s_useSkia = false;
private readonly IDisposable _app; private readonly IDisposable _app;
private readonly string[] _manySmallStrings; private readonly string[] _manySmallStrings;

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

@ -558,7 +558,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textLine = var textLine =
formatter.FormatLine(textSource, 0, 33, paragraphProperties); formatter.FormatLine(textSource, 0, 33, paragraphProperties);
Assert.NotNull(textLine.TextLineBreak?.RemainingRuns); var remainingRunsLineBreak = Assert.IsType<WrappingTextLineBreak>(textLine.TextLineBreak);
var remainingRuns = remainingRunsLineBreak.AcquireRemainingRuns();
Assert.NotNull(remainingRuns);
Assert.NotEmpty(remainingRuns);
} }
} }

Loading…
Cancel
Save