csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
373 lines
13 KiB
373 lines
13 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using Avalonia.Media;
|
|
using Avalonia.Media.TextFormatting;
|
|
using Avalonia.Media.TextFormatting.Unicode;
|
|
using Avalonia.UnitTests;
|
|
using Avalonia.Utility;
|
|
using Xunit;
|
|
|
|
namespace Avalonia.Skia.UnitTests
|
|
{
|
|
public class SimpleTextFormatterTests
|
|
{
|
|
[Fact]
|
|
public void Should_Format_TextRuns_With_Default_Style()
|
|
{
|
|
using (Start())
|
|
{
|
|
const string text = "0123456789";
|
|
|
|
var defaultTextRunStyle = new TextStyle(Typeface.Default, 12, Brushes.Black);
|
|
|
|
var textSource = new SimpleTextSource(text, defaultTextRunStyle);
|
|
|
|
var formatter = new SimpleTextFormatter();
|
|
|
|
var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new TextParagraphProperties());
|
|
|
|
Assert.Single(textLine.TextRuns);
|
|
|
|
var textRun = textLine.TextRuns[0];
|
|
|
|
Assert.Equal(defaultTextRunStyle.TextFormat, textRun.Style.TextFormat);
|
|
|
|
Assert.Equal(defaultTextRunStyle.Foreground, textRun.Style.Foreground);
|
|
|
|
Assert.Equal(text.Length, textRun.Text.Length);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Should_Format_TextRuns_With_Multiple_Buffers()
|
|
{
|
|
using (Start())
|
|
{
|
|
var defaultTextRunStyle = new TextStyle(Typeface.Default, 12, Brushes.Black);
|
|
|
|
var textSource = new MultiBufferTextSource(defaultTextRunStyle);
|
|
|
|
var formatter = new SimpleTextFormatter();
|
|
|
|
var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity,
|
|
new TextParagraphProperties(defaultTextRunStyle));
|
|
|
|
Assert.Equal(5, textLine.TextRuns.Count);
|
|
|
|
Assert.Equal(50, textLine.Text.Length);
|
|
}
|
|
}
|
|
|
|
private class MultiBufferTextSource : ITextSource
|
|
{
|
|
private readonly string[] _runTexts;
|
|
private readonly TextStyle _defaultStyle;
|
|
|
|
public MultiBufferTextSource(TextStyle defaultStyle)
|
|
{
|
|
_defaultStyle = defaultStyle;
|
|
|
|
_runTexts = new[] { "A123456789", "B123456789", "C123456789", "D123456789", "E123456789" };
|
|
}
|
|
|
|
public TextPointer TextPointer => new TextPointer(0, 50);
|
|
|
|
public TextRun GetTextRun(int textSourceIndex)
|
|
{
|
|
if (textSourceIndex == 50)
|
|
{
|
|
return new TextEndOfParagraph();
|
|
}
|
|
|
|
var index = textSourceIndex / 10;
|
|
|
|
var runText = _runTexts[index];
|
|
|
|
return new TextCharacters(
|
|
new ReadOnlySlice<char>(runText.AsMemory(), textSourceIndex, runText.Length), _defaultStyle);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Should_Format_TextRuns_With_TextRunStyles()
|
|
{
|
|
using (Start())
|
|
{
|
|
const string text = "0123456789";
|
|
|
|
var defaultStyle = new TextStyle(Typeface.Default, 12, Brushes.Black);
|
|
|
|
var textStyleRuns = new[]
|
|
{
|
|
new TextStyleRun(new TextPointer(0, 3), defaultStyle ),
|
|
new TextStyleRun(new TextPointer(3, 3), new TextStyle(Typeface.Default, 13, Brushes.Black) ),
|
|
new TextStyleRun(new TextPointer(6, 3), new TextStyle(Typeface.Default, 14, Brushes.Black) ),
|
|
new TextStyleRun(new TextPointer(9, 1), defaultStyle )
|
|
};
|
|
|
|
var textSource = new FormattableTextSource(text, defaultStyle, textStyleRuns);
|
|
|
|
var formatter = new SimpleTextFormatter();
|
|
|
|
var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new TextParagraphProperties());
|
|
|
|
Assert.Equal(text.Length, textLine.Text.Length);
|
|
|
|
for (var i = 0; i < textStyleRuns.Length; i++)
|
|
{
|
|
var textStyleRun = textStyleRuns[i];
|
|
|
|
var textRun = textLine.TextRuns[i];
|
|
|
|
Assert.Equal(textStyleRun.TextPointer.Length, textRun.Text.Length);
|
|
}
|
|
}
|
|
}
|
|
|
|
private class FormattableTextSource : ITextSource
|
|
{
|
|
private readonly ReadOnlySlice<char> _text;
|
|
private readonly TextStyle _defaultStyle;
|
|
private ReadOnlySlice<TextStyleRun> _textStyleRuns;
|
|
|
|
public FormattableTextSource(string text, TextStyle defaultStyle, ReadOnlySlice<TextStyleRun> textStyleRuns)
|
|
{
|
|
_text = text.AsMemory();
|
|
|
|
_defaultStyle = defaultStyle;
|
|
|
|
_textStyleRuns = textStyleRuns;
|
|
}
|
|
|
|
public TextRun GetTextRun(int textSourceIndex)
|
|
{
|
|
if (_textStyleRuns.IsEmpty)
|
|
{
|
|
return new TextEndOfParagraph();
|
|
}
|
|
|
|
var styleRun = _textStyleRuns[0];
|
|
|
|
_textStyleRuns = _textStyleRuns.Skip(1);
|
|
|
|
return new TextCharacters(_text.AsSlice(styleRun.TextPointer.Start, styleRun.TextPointer.Length),
|
|
_defaultStyle);
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("0123", 1)]
|
|
[InlineData("\r\n", 1)]
|
|
[InlineData("👍b", 2)]
|
|
[InlineData("a👍b", 3)]
|
|
[InlineData("a👍子b", 4)]
|
|
public void Should_Produce_Unique_Runs(string text, int numberOfRuns)
|
|
{
|
|
using (Start())
|
|
{
|
|
var textSource = new SimpleTextSource(text, new TextStyle(Typeface.Default));
|
|
|
|
var formatter = new SimpleTextFormatter();
|
|
|
|
var textLine =
|
|
formatter.FormatLine(textSource, 0, double.PositiveInfinity, new TextParagraphProperties());
|
|
|
|
Assert.Equal(numberOfRuns, textLine.TextRuns.Count);
|
|
}
|
|
}
|
|
|
|
private class SimpleTextSource : ITextSource
|
|
{
|
|
private readonly ReadOnlySlice<char> _text;
|
|
private readonly TextStyle _defaultTextStyle;
|
|
|
|
public SimpleTextSource(string text, TextStyle defaultText)
|
|
{
|
|
_text = text.AsMemory();
|
|
_defaultTextStyle = defaultText;
|
|
}
|
|
|
|
public TextRun GetTextRun(int textSourceIndex)
|
|
{
|
|
var runText = _text.Skip(textSourceIndex);
|
|
|
|
if (runText.IsEmpty)
|
|
{
|
|
return new TextEndOfParagraph();
|
|
}
|
|
|
|
return new TextCharacters(runText, _defaultTextStyle);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Should_Split_Run_On_Script()
|
|
{
|
|
using (Start())
|
|
{
|
|
const string text = "1234الدولي";
|
|
|
|
var textSource = new SimpleTextSource(text, new TextStyle(Typeface.Default));
|
|
|
|
var formatter = new SimpleTextFormatter();
|
|
|
|
var textLine =
|
|
formatter.FormatLine(textSource, 0, double.PositiveInfinity, new TextParagraphProperties());
|
|
|
|
Assert.Equal(4, textLine.TextRuns[0].Text.Length);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Should_Get_Distance_From_CharacterHit()
|
|
{
|
|
using (Start())
|
|
{
|
|
var textSource = new MultiBufferTextSource(new TextStyle(Typeface.Default));
|
|
|
|
var formatter = new SimpleTextFormatter();
|
|
|
|
var textLine =
|
|
formatter.FormatLine(textSource, 0, double.PositiveInfinity, new TextParagraphProperties());
|
|
|
|
var currentDistance = 0.0;
|
|
|
|
foreach (var run in textLine.TextRuns)
|
|
{
|
|
var textRun = (ShapedTextRun)run;
|
|
|
|
var glyphRun = textRun.GlyphRun;
|
|
|
|
for (var i = 0; i < glyphRun.GlyphClusters.Length; i++)
|
|
{
|
|
var cluster = glyphRun.GlyphClusters[i];
|
|
|
|
var glyph = glyphRun.GlyphIndices[i];
|
|
|
|
var advance = glyphRun.GlyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale;
|
|
|
|
var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(cluster));
|
|
|
|
Assert.Equal(currentDistance, distance);
|
|
|
|
currentDistance += advance;
|
|
}
|
|
}
|
|
|
|
Assert.Equal(currentDistance, textLine.GetDistanceFromCharacterHit(new CharacterHit(textSource.TextPointer.Length)));
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Should_Get_CharacterHit_From_Distance()
|
|
{
|
|
using (Start())
|
|
{
|
|
var textSource = new MultiBufferTextSource(new TextStyle(Typeface.Default));
|
|
|
|
var formatter = new SimpleTextFormatter();
|
|
|
|
var textLine =
|
|
formatter.FormatLine(textSource, 0, double.PositiveInfinity, new TextParagraphProperties());
|
|
|
|
var currentDistance = 0.0;
|
|
|
|
CharacterHit characterHit;
|
|
|
|
foreach (var run in textLine.TextRuns)
|
|
{
|
|
var textRun = (ShapedTextRun)run;
|
|
|
|
var glyphRun = textRun.GlyphRun;
|
|
|
|
for (var i = 0; i < glyphRun.GlyphClusters.Length; i++)
|
|
{
|
|
var cluster = glyphRun.GlyphClusters[i];
|
|
|
|
var glyph = glyphRun.GlyphIndices[i];
|
|
|
|
var advance = glyphRun.GlyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale;
|
|
|
|
characterHit = textLine.GetCharacterHitFromDistance(currentDistance);
|
|
|
|
Assert.Equal(cluster, characterHit.FirstCharacterIndex + characterHit.TrailingLength);
|
|
|
|
currentDistance += advance;
|
|
}
|
|
}
|
|
|
|
characterHit = textLine.GetCharacterHitFromDistance(textLine.LineMetrics.Size.Width);
|
|
|
|
Assert.Equal(textSource.TextPointer.End, characterHit.FirstCharacterIndex);
|
|
}
|
|
}
|
|
|
|
[InlineData("Whether to turn off HTTPS. This option only applies if Individual, " +
|
|
"IndividualB2C, SingleOrg, or MultiOrg aren't used for ‑‑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()
|
|
{
|
|
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
|
|
.With(renderInterface: new PlatformRenderInterface(null),
|
|
textShaperImpl: new TextShaperImpl()));
|
|
|
|
AvaloniaLocator.CurrentMutable
|
|
.Bind<FontManager>().ToConstant(new FontManager(new CustomFontManagerImpl()));
|
|
|
|
return disposable;
|
|
}
|
|
}
|
|
}
|
|
|