Browse Source

Merge branch 'master' into refactor-win32-window

pull/3705/head
Dariusz Komosiński 7 years ago
committed by GitHub
parent
commit
5fb351200d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 34
      src/Avalonia.Controls/TextBlock.cs
  2. 46
      src/Avalonia.Visuals/Media/GlyphRun.cs
  3. 89
      src/Avalonia.Visuals/Media/TextFormatting/SimpleTextFormatter.cs
  4. 15
      src/Avalonia.Visuals/Utility/ReadOnlySlice.cs
  5. 26
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  6. 72
      src/Skia/Avalonia.Skia/TextShaperImpl.cs
  7. BIN
      tests/Avalonia.RenderTests/Assets/NotoSans-Italic.ttf
  8. 5
      tests/Avalonia.Skia.UnitTests/CustomFontManagerImpl.cs
  9. 66
      tests/Avalonia.Skia.UnitTests/SimpleTextFormatterTests.cs

34
src/Avalonia.Controls/TextBlock.cs

@ -117,28 +117,18 @@ namespace Avalonia.Controls
{ {
ClipToBoundsProperty.OverrideDefaultValue<TextBlock>(true); ClipToBoundsProperty.OverrideDefaultValue<TextBlock>(true);
AffectsRender<TextBlock>( AffectsRender<TextBlock>(BackgroundProperty, ForegroundProperty,
BackgroundProperty, ForegroundProperty, FontSizeProperty, TextAlignmentProperty, TextDecorationsProperty);
FontWeightProperty, FontStyleProperty, TextWrappingProperty,
TextTrimmingProperty, TextAlignmentProperty, FontFamilyProperty, AffectsMeasure<TextBlock>(FontSizeProperty, FontWeightProperty,
TextDecorationsProperty, TextProperty, PaddingProperty); FontStyleProperty, TextWrappingProperty, FontFamilyProperty,
TextTrimmingProperty, TextProperty, PaddingProperty);
AffectsMeasure<TextBlock>(
FontSizeProperty, FontWeightProperty, FontStyleProperty, Observable.Merge(TextProperty.Changed, ForegroundProperty.Changed,
FontFamilyProperty, TextTrimmingProperty, TextProperty, TextAlignmentProperty.Changed, TextWrappingProperty.Changed,
PaddingProperty); TextTrimmingProperty.Changed, FontSizeProperty.Changed,
FontStyleProperty.Changed, FontWeightProperty.Changed,
Observable.Merge( FontFamilyProperty.Changed, TextDecorationsProperty.Changed,
TextProperty.Changed,
ForegroundProperty.Changed,
TextAlignmentProperty.Changed,
TextWrappingProperty.Changed,
TextTrimmingProperty.Changed,
FontSizeProperty.Changed,
FontStyleProperty.Changed,
FontWeightProperty.Changed,
FontFamilyProperty.Changed,
TextDecorationsProperty.Changed,
PaddingProperty.Changed PaddingProperty.Changed
).AddClassHandler<TextBlock>((x, _) => x.InvalidateTextLayout()); ).AddClassHandler<TextBlock>((x, _) => x.InvalidateTextLayout());
} }

46
src/Avalonia.Visuals/Media/GlyphRun.cs

@ -274,27 +274,39 @@ namespace Avalonia.Media
var currentX = 0.0; var currentX = 0.0;
var index = 0; var index = 0;
for (; index < GlyphIndices.Length; index++) if (GlyphTypeface.IsFixedPitch)
{ {
double advance; var glyph = GlyphIndices[index];
if (GlyphAdvances.IsEmpty) var advance = GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
{
var glyph = GlyphIndices[index];
advance = GlyphTypeface.GetGlyphAdvance(glyph) * Scale; index = Math.Min(GlyphIndices.Length - 1,
} (int)Math.Round(distance / advance, MidpointRounding.AwayFromZero));
else }
else
{
for (; index < GlyphIndices.Length; index++)
{ {
advance = GlyphAdvances[index]; double advance;
}
if (currentX + advance >= distance) if (GlyphAdvances.IsEmpty)
{ {
break; var glyph = GlyphIndices[index];
}
advance = GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
}
else
{
advance = GlyphAdvances[index];
}
currentX += advance; if (currentX + advance >= distance)
{
break;
}
currentX += advance;
}
} }
var characterHit = FindNearestCharacterHit(GlyphClusters[index], out var width); var characterHit = FindNearestCharacterHit(GlyphClusters[index], out var width);
@ -473,9 +485,7 @@ namespace Avalonia.Media
/// </returns> /// </returns>
private Rect CalculateBounds() private Rect CalculateBounds()
{ {
var scale = FontRenderingEmSize / GlyphTypeface.DesignEmHeight; var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale;
var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * scale;
var width = 0.0; var width = 0.0;

89
src/Avalonia.Visuals/Media/TextFormatting/SimpleTextFormatter.cs

@ -10,15 +10,7 @@ namespace Avalonia.Media.TextFormatting
{ {
private static readonly ReadOnlySlice<char> s_ellipsis = new ReadOnlySlice<char>(new[] { '\u2026' }); private static readonly ReadOnlySlice<char> s_ellipsis = new ReadOnlySlice<char>(new[] { '\u2026' });
/// <summary> /// <inheritdoc cref="TextFormatter.FormatLine"/>
/// Formats a text line.
/// </summary>
/// <param name="textSource">The text source.</param>
/// <param name="firstTextSourceIndex">The first character index to start the text line from.</param>
/// <param name="paragraphWidth">A <see cref="double"/> value that specifies the width of the paragraph that the line fills.</param>
/// <param name="paragraphProperties">A <see cref="TextParagraphProperties"/> value that represents paragraph properties,
/// such as TextWrapping, TextAlignment, or TextStyle.</param>
/// <returns>The formatted line.</returns>
public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
TextParagraphProperties paragraphProperties) TextParagraphProperties paragraphProperties)
{ {
@ -61,17 +53,18 @@ namespace Avalonia.Media.TextFormatting
/// </returns> /// </returns>
private List<ShapedTextRun> FormatTextRuns(ITextSource textSource, int firstTextSourceIndex, out TextPointer textPointer) private List<ShapedTextRun> FormatTextRuns(ITextSource textSource, int firstTextSourceIndex, out TextPointer textPointer)
{ {
var start = firstTextSourceIndex; var start = -1;
var length = 0;
var textRuns = new List<ShapedTextRun>(); var textRuns = new List<ShapedTextRun>();
while (true) while (true)
{ {
var textRun = textSource.GetTextRun(firstTextSourceIndex); var textRun = textSource.GetTextRun(firstTextSourceIndex + length);
if (textRun.Text.IsEmpty) if (start == -1)
{ {
break; start = textRun.Text.Start;
} }
if (textRun is TextEndOfLine) if (textRun is TextEndOfLine)
@ -79,29 +72,33 @@ namespace Avalonia.Media.TextFormatting
break; break;
} }
if (!(textRun is TextCharacters)) switch (textRun)
{ {
throw new NotSupportedException("Run type not supported by the formatter."); case TextCharacters textCharacters:
}
var runText = textRun.Text; var runText = textCharacters.Text;
while (!runText.IsEmpty) while (!runText.IsEmpty)
{ {
var shapableTextStyleRun = CreateShapableTextStyleRun(runText, textRun.Style); var shapableTextStyleRun = CreateShapableTextStyleRun(runText, textRun.Style);
var shapedRun = new ShapedTextRun(runText.Take(shapableTextStyleRun.TextPointer.Length), var shapedRun = new ShapedTextRun(runText.Take(shapableTextStyleRun.TextPointer.Length),
shapableTextStyleRun.Style); shapableTextStyleRun.Style);
textRuns.Add(shapedRun); textRuns.Add(shapedRun);
runText = runText.Skip(shapedRun.Text.Length); runText = runText.Skip(shapedRun.Text.Length);
}
break;
default:
throw new NotSupportedException("Run type not supported by the formatter.");
} }
firstTextSourceIndex += textRun.Text.Length; length += textRun.Text.Length;
} }
textPointer = new TextPointer(start, firstTextSourceIndex - start); textPointer = new TextPointer(start, length);
return textRuns; return textRuns;
} }
@ -115,7 +112,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="textRuns">The text runs to perform the trimming on.</param> /// <param name="textRuns">The text runs to perform the trimming on.</param>
/// <param name="text">The text that was used to construct the text runs.</param> /// <param name="text">The text that was used to construct the text runs.</param>
/// <returns></returns> /// <returns></returns>
private TextLine PerformTextTrimming(TextPointer text, IReadOnlyList<ShapedTextRun> textRuns, private static TextLine PerformTextTrimming(TextPointer text, IReadOnlyList<ShapedTextRun> textRuns,
double paragraphWidth, TextParagraphProperties paragraphProperties) double paragraphWidth, TextParagraphProperties paragraphProperties)
{ {
var textTrimming = paragraphProperties.TextTrimming; var textTrimming = paragraphProperties.TextTrimming;
@ -195,7 +192,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="text">The text to analyze for break opportunities.</param> /// <param name="text">The text to analyze for break opportunities.</param>
/// <param name="paragraphWidth"></param> /// <param name="paragraphWidth"></param>
/// <returns></returns> /// <returns></returns>
private TextLine PerformTextWrapping(TextPointer text, IReadOnlyList<ShapedTextRun> textRuns, private static TextLine PerformTextWrapping(TextPointer text, IReadOnlyList<ShapedTextRun> textRuns,
double paragraphWidth, TextParagraphProperties paragraphProperties) double paragraphWidth, TextParagraphProperties paragraphProperties)
{ {
var availableWidth = paragraphWidth; var availableWidth = paragraphWidth;
@ -267,41 +264,13 @@ namespace Avalonia.Media.TextFormatting
/// <param name="textRun">The text run.</param> /// <param name="textRun">The text run.</param>
/// <param name="availableWidth">The available width.</param> /// <param name="availableWidth">The available width.</param>
/// <returns></returns> /// <returns></returns>
private int MeasureText(ShapedTextRun textRun, double availableWidth) private static int MeasureText(ShapedTextRun textRun, double availableWidth)
{ {
if (textRun.GlyphRun.Bounds.Width < availableWidth) var glyphRun = textRun.GlyphRun;
{
return textRun.Text.Length;
}
var measuredWidth = 0.0;
var index = 0;
for (; index < textRun.GlyphRun.GlyphAdvances.Length; index++)
{
var advance = textRun.GlyphRun.GlyphAdvances[index];
if (measuredWidth + advance > availableWidth)
{
index--;
break;
}
measuredWidth += advance;
}
if(index < 0)
{
return 0;
}
var cluster = textRun.GlyphRun.GlyphClusters[index];
var characterHit = textRun.GlyphRun.FindNearestCharacterHit(cluster, out _); var characterHit = glyphRun.GetCharacterHitFromDistance(availableWidth, out _);
return characterHit.FirstCharacterIndex - textRun.GlyphRun.Characters.Start + return characterHit.FirstCharacterIndex + characterHit.TrailingLength - textRun.Text.Start;
(textRun.GlyphRun.IsLeftToRight ? characterHit.TrailingLength : 0);
} }
/// <summary> /// <summary>

15
src/Avalonia.Visuals/Utility/ReadOnlySlice.cs

@ -69,6 +69,11 @@ namespace Avalonia.Utility
/// <returns>A <see cref="ReadOnlySlice{T}"/> that contains the specified number of elements from the specified start.</returns> /// <returns>A <see cref="ReadOnlySlice{T}"/> that contains the specified number of elements from the specified start.</returns>
public ReadOnlySlice<T> AsSlice(int start, int length) public ReadOnlySlice<T> AsSlice(int start, int length)
{ {
if (IsEmpty)
{
return this;
}
if (start < Start || start > End) if (start < Start || start > End)
{ {
throw new ArgumentOutOfRangeException(nameof(start)); throw new ArgumentOutOfRangeException(nameof(start));
@ -91,6 +96,11 @@ namespace Avalonia.Utility
/// <returns>A <see cref="ReadOnlySlice{T}"/> that contains the specified number of elements from the start of this slice.</returns> /// <returns>A <see cref="ReadOnlySlice{T}"/> that contains the specified number of elements from the start of this slice.</returns>
public ReadOnlySlice<T> Take(int length) public ReadOnlySlice<T> Take(int length)
{ {
if (IsEmpty)
{
return this;
}
if (length > Length) if (length > Length)
{ {
throw new ArgumentOutOfRangeException(nameof(length)); throw new ArgumentOutOfRangeException(nameof(length));
@ -106,6 +116,11 @@ namespace Avalonia.Utility
/// <returns>A <see cref="ReadOnlySlice{T}"/> that contains the elements that occur after the specified index in this slice.</returns> /// <returns>A <see cref="ReadOnlySlice{T}"/> that contains the elements that occur after the specified index in this slice.</returns>
public ReadOnlySlice<T> Skip(int length) public ReadOnlySlice<T> Skip(int length)
{ {
if (IsEmpty)
{
return this;
}
if (length > Length) if (length > Length)
{ {
throw new ArgumentOutOfRangeException(nameof(length)); throw new ArgumentOutOfRangeException(nameof(length));

26
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@ -173,16 +173,26 @@ namespace Avalonia.Skia
using (var textBlobBuilder = new SKTextBlobBuilder()) using (var textBlobBuilder = new SKTextBlobBuilder())
{ {
SKTextBlob textBlob;
width = 0;
var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight); var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight);
if (glyphRun.GlyphOffsets.IsEmpty) if (glyphRun.GlyphOffsets.IsEmpty)
{ {
width = 0; if (glyphTypeface.IsFixedPitch)
{
textBlobBuilder.AddRun(paint, 0, 0, glyphRun.GlyphIndices.Buffer.Span);
var buffer = textBlobBuilder.AllocateHorizontalRun(paint, count, 0); textBlob = textBlobBuilder.Build();
if (!glyphTypeface.IsFixedPitch) width = glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[0]) * scale * glyphRun.GlyphIndices.Length;
}
else
{ {
var buffer = textBlobBuilder.AllocateHorizontalRun(paint, count, 0);
var positions = buffer.GetPositionSpan(); var positions = buffer.GetPositionSpan();
for (var i = 0; i < count; i++) for (var i = 0; i < count; i++)
@ -198,9 +208,11 @@ namespace Avalonia.Skia
width += glyphRun.GlyphAdvances[i]; width += glyphRun.GlyphAdvances[i];
} }
} }
}
buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span); buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span);
textBlob = textBlobBuilder.Build();
}
} }
else else
{ {
@ -229,9 +241,9 @@ namespace Avalonia.Skia
buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span); buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span);
width = currentX; width = currentX;
}
var textBlob = textBlobBuilder.Build(); textBlob = textBlobBuilder.Build();
}
return new GlyphRunImpl(paint, textBlob); return new GlyphRunImpl(paint, textBlob);
} }

72
src/Skia/Avalonia.Skia/TextShaperImpl.cs

@ -1,4 +1,5 @@
using Avalonia.Media; using System;
using Avalonia.Media;
using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform; using Avalonia.Platform;
@ -72,36 +73,32 @@ namespace Avalonia.Skia
var textScale = textFormat.FontRenderingEmSize / scaleX; var textScale = textFormat.FontRenderingEmSize / scaleX;
var len = buffer.Length; var bufferLength = buffer.Length;
var info = buffer.GetGlyphInfoSpan(); var glyphInfos = buffer.GetGlyphInfoSpan();
var pos = buffer.GetGlyphPositionSpan(); var glyphPositions = buffer.GetGlyphPositionSpan();
var glyphIndices = new ushort[len]; var glyphIndices = new ushort[bufferLength];
var clusters = new ushort[len]; var clusters = new ushort[bufferLength];
var glyphAdvances = new double[len]; double[] glyphAdvances = null;
var glyphOffsets = new Vector[len]; Vector[] glyphOffsets = null;
for (var i = 0; i < len; i++) for (var i = 0; i < bufferLength; i++)
{ {
glyphIndices[i] = (ushort)info[i].Codepoint; glyphIndices[i] = (ushort)glyphInfos[i].Codepoint;
clusters[i] = (ushort)(text.Start + info[i].Cluster); clusters[i] = (ushort)(text.Start + glyphInfos[i].Cluster);
var advanceX = pos[i].XAdvance * textScale; if (!glyphTypeface.IsFixedPitch)
// Depends on direction of layout {
//var advanceY = pos[i].YAdvance * textScale; SetAdvance(glyphPositions, i, textScale, ref glyphAdvances);
}
glyphAdvances[i] = advanceX;
var offsetX = pos[i].XOffset * textScale;
var offsetY = pos[i].YOffset * textScale;
glyphOffsets[i] = new Vector(offsetX, offsetY); SetOffset(glyphPositions, i, textScale, ref glyphOffsets);
} }
return new GlyphRun(glyphTypeface, textFormat.FontRenderingEmSize, return new GlyphRun(glyphTypeface, textFormat.FontRenderingEmSize,
@ -112,5 +109,40 @@ namespace Avalonia.Skia
new ReadOnlySlice<ushort>(clusters)); new ReadOnlySlice<ushort>(clusters));
} }
} }
private static void SetOffset(ReadOnlySpan<GlyphPosition> glyphPositions, int index, double textScale,
ref Vector[] offsetBuffer)
{
var position = glyphPositions[index];
if (position.XOffset == 0 && position.YOffset == 0)
{
return;
}
if (offsetBuffer == null)
{
offsetBuffer = new Vector[glyphPositions.Length];
}
var offsetX = position.XOffset * textScale;
var offsetY = position.YOffset * textScale;
offsetBuffer[index] = new Vector(offsetX, offsetY);
}
private static void SetAdvance(ReadOnlySpan<GlyphPosition> glyphPositions, int index, double textScale,
ref double[] advanceBuffer)
{
if (advanceBuffer == null)
{
advanceBuffer = new double[glyphPositions.Length];
}
// Depends on direction of layout
// advanceBuffer[index] = buffer.GlyphPositions[index].YAdvance * textScale;
advanceBuffer[index] = glyphPositions[index].XAdvance * textScale;
}
} }
} }

BIN
tests/Avalonia.RenderTests/Assets/NotoSans-Italic.ttf

Binary file not shown.

5
tests/Avalonia.Skia.UnitTests/CustomFontManagerImpl.cs

@ -14,12 +14,14 @@ namespace Avalonia.Skia.UnitTests
private readonly Typeface _defaultTypeface = private readonly Typeface _defaultTypeface =
new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono"); new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono");
private readonly Typeface _italicTypeface =
new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans");
private readonly Typeface _emojiTypeface = private readonly Typeface _emojiTypeface =
new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Twitter Color Emoji"); new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Twitter Color Emoji");
public CustomFontManagerImpl() public CustomFontManagerImpl()
{ {
_customTypefaces = new[] { _emojiTypeface, _defaultTypeface }; _customTypefaces = new[] { _emojiTypeface, _italicTypeface, _defaultTypeface };
} }
public string GetDefaultFontFamilyName() public string GetDefaultFontFamilyName()
@ -56,6 +58,7 @@ namespace Avalonia.Skia.UnitTests
switch (typeface.FontFamily.Name) switch (typeface.FontFamily.Name)
{ {
case "Twitter Color Emoji": case "Twitter Color Emoji":
case "Noto Sans":
case "Noto Mono": case "Noto Mono":
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily); var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily);
var skTypeface = typefaceCollection.Get(typeface); var skTypeface = typefaceCollection.Get(typeface);

66
tests/Avalonia.Skia.UnitTests/SimpleTextFormatterTests.cs

@ -1,6 +1,8 @@
using System; using System;
using System.Collections.Generic;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Avalonia.Utility; using Avalonia.Utility;
using Xunit; using Xunit;
@ -240,7 +242,9 @@ namespace Avalonia.Skia.UnitTests
{ {
var cluster = glyphRun.GlyphClusters[i]; var cluster = glyphRun.GlyphClusters[i];
var advance = glyphRun.GlyphAdvances[i]; var glyph = glyphRun.GlyphIndices[i];
var advance = glyphRun.GlyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale;
var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(cluster)); var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(cluster));
@ -280,7 +284,9 @@ namespace Avalonia.Skia.UnitTests
{ {
var cluster = glyphRun.GlyphClusters[i]; var cluster = glyphRun.GlyphClusters[i];
var advance = glyphRun.GlyphAdvances[i]; var glyph = glyphRun.GlyphIndices[i];
var advance = glyphRun.GlyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale;
characterHit = textLine.GetCharacterHitFromDistance(currentDistance); characterHit = textLine.GetCharacterHitFromDistance(currentDistance);
@ -296,6 +302,62 @@ namespace Avalonia.Skia.UnitTests
} }
} }
[InlineData("Whether to turn off HTTPS. This option only applies if Individual, " +
"IndividualB2C, SingleOrg, or MultiOrg aren't used for &#8209;&#8209;auth."
, "Noto Sans", 40)]
[InlineData("01234 56789 01234 56789", "Noto Mono", 7)]
[Theory]
public void Should_Wrap_Text(string text, string familyName, int numberOfCharactersPerLine)
{
using (Start())
{
var lineBreaker = new LineBreakEnumerator(text.AsMemory());
var expected = new List<int>();
while (lineBreaker.MoveNext())
{
expected.Add(lineBreaker.Current.PositionWrap - 1);
}
var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#" +
familyName);
var defaultStyle = new TextStyle(typeface);
var textSource = new SimpleTextSource(text, defaultStyle);
var formatter = new SimpleTextFormatter();
var glyph = typeface.GlyphTypeface.GetGlyph('a');
var advance = typeface.GlyphTypeface.GetGlyphAdvance(glyph) *
(12.0 / typeface.GlyphTypeface.DesignEmHeight);
var paragraphWidth = advance * numberOfCharactersPerLine;
var currentPosition = 0;
while (currentPosition < text.Length)
{
var textLine =
formatter.FormatLine(textSource, currentPosition, paragraphWidth,
new TextParagraphProperties(defaultStyle, textWrapping: TextWrapping.Wrap));
Assert.True(expected.Contains(textLine.Text.End));
var index = expected.IndexOf(textLine.Text.End);
for (var i = 0; i <= index; i++)
{
expected.RemoveAt(0);
}
currentPosition += textLine.Text.Length;
}
}
}
public static IDisposable Start() public static IDisposable Start()
{ {
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface

Loading…
Cancel
Save