A cross-platform UI framework for .NET
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.
 
 
 

482 lines
17 KiB

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Skia.UnitTests.Media
{
public class GlyphRunTests
{
[InlineData("ABC012345", 0)] //LeftToRight
[InlineData("זה כיף סתם לשמוע איך תנצח קרפד עץ טוב בגן", 1)] //RightToLeft
[Theory]
public void Should_Get_Next_CharacterHit(string text, sbyte direction)
{
using (Start())
{
var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, direction, CultureInfo.CurrentCulture);
var shapedBuffer =
TextShaper.Current.ShapeText(text, options);
var glyphRun = CreateGlyphRun(shapedBuffer);
var characterHit = new CharacterHit(0);
var rects = BuildRects(glyphRun);
if (glyphRun.IsLeftToRight)
{
foreach (var rect in rects)
{
characterHit = glyphRun.GetNextCaretCharacterHit(characterHit);
var distance = glyphRun.GetDistanceFromCharacterHit(characterHit);
Assert.Equal(rect.Right, distance);
}
}
else
{
foreach (var rect in rects)
{
characterHit = glyphRun.GetNextCaretCharacterHit(characterHit);
var distance = glyphRun.GetDistanceFromCharacterHit(characterHit);
Assert.Equal(rect.Left, distance);
}
}
}
}
[InlineData("ABC012345", 0)] //LeftToRight
[InlineData("זה כיף סתם לשמוע איך תנצח קרפד עץ טוב בגן", 1)] //RightToLeft
[Theory]
public void Should_Get_Previous_CharacterHit(string text, sbyte direction)
{
using (Start())
{
var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, direction, CultureInfo.CurrentCulture);
var shapedBuffer =
TextShaper.Current.ShapeText(text, options);
var glyphRun = CreateGlyphRun(shapedBuffer);
var characterHit = new CharacterHit(text.Length);
var rects = BuildRects(glyphRun);
rects.Reverse();
if (glyphRun.IsLeftToRight)
{
foreach (var rect in rects)
{
characterHit = glyphRun.GetPreviousCaretCharacterHit(characterHit);
var distance = glyphRun.GetDistanceFromCharacterHit(characterHit);
Assert.Equal(rect.Left, distance);
}
}
else
{
foreach (var rect in rects)
{
characterHit = glyphRun.GetPreviousCaretCharacterHit(characterHit);
var distance = glyphRun.GetDistanceFromCharacterHit(characterHit);
Assert.Equal(rect.Right, distance);
}
}
}
}
[InlineData("ABC012345", 0)] //LeftToRight
[InlineData("זה כיף סתם לשמוע איך תנצח קרפד עץ טוב בגן", 1)] //RightToLeft
[Theory]
public void Should_Get_CharacterHit_From_Distance(string text, sbyte direction)
{
using (Start())
{
var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, direction, CultureInfo.CurrentCulture);
var shapedBuffer =
TextShaper.Current.ShapeText(text, options);
var glyphRun = CreateGlyphRun(shapedBuffer);
if (glyphRun.IsLeftToRight)
{
var characterHit =
glyphRun.GetCharacterHitFromDistance(glyphRun.Bounds.Width, out _);
Assert.Equal(glyphRun.Characters.Length, characterHit.FirstCharacterIndex + characterHit.TrailingLength);
}
else
{
var characterHit =
glyphRun.GetCharacterHitFromDistance(0, out _);
Assert.Equal(glyphRun.Characters.Length, characterHit.FirstCharacterIndex + characterHit.TrailingLength);
}
var rects = BuildRects(glyphRun);
var lastCluster = -1;
var index = 0;
if (!glyphRun.IsLeftToRight)
{
rects.Reverse();
}
foreach (var rect in rects)
{
var currentCluster = glyphRun.GlyphInfos[index].GlyphCluster;
while (currentCluster == lastCluster && index + 1 < glyphRun.GlyphInfos.Count)
{
currentCluster = glyphRun.GlyphInfos[++index].GlyphCluster;
}
//Non trailing edge
var distance = glyphRun.IsLeftToRight ? rect.Left : rect.Right;
var characterHit = glyphRun.GetCharacterHitFromDistance(distance, out _);
Assert.Equal(currentCluster, characterHit.FirstCharacterIndex + characterHit.TrailingLength);
lastCluster = currentCluster;
index++;
}
}
}
// https://github.com/AvaloniaUI/Avalonia/issues/12676
[Fact]
public void Similar_Runs_Have_Same_InkBounds_After_Blob_Creation()
{
using (Start())
{
var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Inter");
var options = new TextShaperOptions(typeface.GlyphTypeface, 14);
var shapedBuffer = TextShaper.Current.ShapeText("F", options);
var glyphRun1 = CreateGlyphRun(shapedBuffer);
var bounds1 = glyphRun1.InkBounds;
((GlyphRunImpl)glyphRun1.PlatformImpl.Item).GetTextBlob(new TextOptions { TextRenderingMode = TextRenderingMode.SubpixelAntialias }, default);
var bounds2 = CreateGlyphRun(shapedBuffer).InkBounds;
Assert.Equal(bounds1, bounds2);
}
}
[Fact]
public void GlyphRun_With_Leading_Space_Has_Correct_InkBounds()
{
using (Start())
{
var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Inter");
var options = new TextShaperOptions(typeface.GlyphTypeface, 14);
var shapedBuffer = TextShaper.Current.ShapeText(" I", options);
var glyphRun1 = CreateGlyphRun(shapedBuffer);
var bounds = glyphRun1.InkBounds;
Assert.True(bounds.Left > 0);
}
}
[Fact]
public void Should_Get_Distance_From_CharacterHit_Non_Trailing_RightToLeft()
{
const string text = "נִקּוּד";
using (Start())
{
var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Inter");
var options = new TextShaperOptions(typeface.GlyphTypeface, 14, 1);
var shapedBuffer = TextShaper.Current.ShapeText(text, options);
var glyphRun = CreateGlyphRun(shapedBuffer);
//Get the distance to the left side of the first glyph
var actualDistance = glyphRun.GetDistanceFromCharacterHit(new CharacterHit(text.Length));
var expectedDistance = 0.0;
Assert.Equal(expectedDistance, actualDistance, 2);
//Get the distance to the right side of the first glyph
actualDistance = glyphRun.GetDistanceFromCharacterHit(new CharacterHit(text.Length - 1));
expectedDistance = shapedBuffer[0].GlyphAdvance;
Assert.Equal(expectedDistance, actualDistance, 2);
}
}
[Fact]
public void Should_CharacterHit_From_Distance_Zero_Width()
{
const string df7Font = "resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#DF7segHMI";
const string text = "3,47-=?:#";
using (Start())
{
var typeface = new Typeface(df7Font);
var options = new TextShaperOptions(typeface.GlyphTypeface, 14, 0);
var shapedBuffer = TextShaper.Current.ShapeText(text, options);
Assert.NotEmpty(shapedBuffer);
var firstGlyphInfo = shapedBuffer[0];
var glyphRun = CreateGlyphRun(shapedBuffer);
var characterHit = glyphRun.GetCharacterHitFromDistance(firstGlyphInfo.GlyphAdvance, out _);
Assert.Equal(2, characterHit.FirstCharacterIndex + characterHit.TrailingLength);
}
}
[Fact]
public void Should_Get_Distance_From_CharacterHit_Zero_Width()
{
const string text = "נִקּוּד";
using (Start())
{
var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Inter");
var options = new TextShaperOptions(typeface.GlyphTypeface, 14, 1);
var shapedBuffer = TextShaper.Current.ShapeText(text, options);
var glyphRun = CreateGlyphRun(shapedBuffer);
var clusters = glyphRun.GlyphInfos.GroupBy(x => x.GlyphCluster);
var rightSideDistances = new List<double>();
var leftSideDistances = new List<double>();
var currentX = 0.0;
foreach (var cluster in clusters)
{
leftSideDistances.Add(currentX);
currentX += cluster.Sum(x => x.GlyphAdvance);
rightSideDistances.Add(currentX);
}
var characterIndices = clusters.Select(x => x.First().GlyphCluster).ToList();
var characterHit = new CharacterHit(text.Length);
for (var i = 0; i < characterIndices.Count; i++)
{
var characterIndex = characterIndices[i];
var leftSideDistance = leftSideDistances[i];
var leftSideCharacterHit = glyphRun.GetCharacterHitFromDistance(leftSideDistance, out _);
var distance = glyphRun.GetDistanceFromCharacterHit(leftSideCharacterHit);
Assert.Equal(leftSideDistance, distance, 2);
var previousCharacterHit = glyphRun.GetPreviousCaretCharacterHit(characterHit);
distance = glyphRun.GetDistanceFromCharacterHit(new CharacterHit(characterIndex));
var rightSideDistance = rightSideDistances[i];
Assert.Equal(rightSideDistance, distance, 2);
characterHit = previousCharacterHit;
}
}
}
[Fact]
public void Should_Get_Distance_From_CharacterHit_Within_Cluster()
{
var text = "எடுத்துக்காட்டு வழி வினவல்";
using (Start())
{
var cp = Codepoint.ReadAt(text, 0, out _);
Assert.True(FontManager.Current.TryMatchCharacter(cp, FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, null, null, out var typeface));
var options = new TextShaperOptions(typeface.GlyphTypeface, 12);
var shapedBuffer = TextShaper.Current.ShapeText(text, options);
var glyphRun = CreateGlyphRun(shapedBuffer);
var clusterWidth = new List<double>();
var distances = new List<double>();
var clusters = new List<int>();
var lastCluster = -1;
var currentDistance = 0.0;
var currentAdvance = 0.0;
foreach (var glyphInfo in shapedBuffer)
{
if (lastCluster != glyphInfo.GlyphCluster)
{
clusterWidth.Add(currentAdvance);
distances.Add(currentDistance);
clusters.Add(glyphInfo.GlyphCluster);
currentAdvance = 0;
}
lastCluster = glyphInfo.GlyphCluster;
currentDistance += glyphInfo.GlyphAdvance;
currentAdvance += glyphInfo.GlyphAdvance;
}
clusterWidth.RemoveAt(0);
clusterWidth.Add(currentAdvance);
var expectedLeftHit = new CharacterHit(11);
var distance = glyphRun.GetDistanceFromCharacterHit(expectedLeftHit);
var expectedLeft = distances[6];
Assert.Equal(expectedLeft, distance);
var leftHit = glyphRun.GetCharacterHitFromDistance(expectedLeft, out _);
Assert.Equal(11, leftHit.FirstCharacterIndex + leftHit.TrailingLength);
var expectedRight = distances[7];
distance = glyphRun.GetDistanceFromCharacterHit(new CharacterHit(12));
Assert.Equal(expectedRight, distance);
var expectedRightHit = new CharacterHit(13);
distance = glyphRun.GetDistanceFromCharacterHit(expectedRightHit);
Assert.Equal(expectedRight, distance);
var rightHit = glyphRun.GetCharacterHitFromDistance(expectedRight, out _);
Assert.Equal(13, rightHit.FirstCharacterIndex + rightHit.TrailingLength);
}
}
[Fact]
public void Should_Add_Half_LineGap_To_Baseline()
{
using (Start())
{
var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#Inter");
var options = new TextShaperOptions(typeface.GlyphTypeface, 14);
var shapedBuffer = TextShaper.Current.ShapeText("F", options);
var textMetrics = new TextMetrics(shapedBuffer.GlyphTypeface, 14);
var glyphRun = CreateGlyphRun(shapedBuffer);
var expectedBaseline = -textMetrics.Ascent + textMetrics.LineGap / 2;
Assert.Equal(expectedBaseline, glyphRun.Metrics.Baseline);
}
}
private static List<Rect> BuildRects(GlyphRun glyphRun)
{
var height = glyphRun.Bounds.Height;
var currentX = glyphRun.IsLeftToRight ? 0d : glyphRun.Bounds.Width;
var rects = new List<Rect>(glyphRun.GlyphInfos!.Count);
var lastCluster = -1;
for (var index = 0; index < glyphRun.GlyphInfos.Count; index++)
{
var currentCluster = glyphRun.GlyphInfos[index].GlyphCluster;
var advance = glyphRun.GlyphInfos[index].GlyphAdvance;
if (lastCluster != currentCluster)
{
if (glyphRun.IsLeftToRight)
{
rects.Add(new Rect(currentX, 0, advance, height));
}
else
{
rects.Add(new Rect(currentX - advance, 0, advance, height));
}
}
else
{
var rect = rects[index - 1];
rects.Remove(rect);
rect = glyphRun.IsLeftToRight ?
rect.WithWidth(rect.Width + advance) :
new Rect(rect.X - advance, 0, rect.Width + advance, height);
rects.Add(rect);
}
if (glyphRun.IsLeftToRight)
{
currentX += advance;
}
else
{
currentX -= advance;
}
lastCluster = currentCluster;
}
return rects;
}
private static GlyphRun CreateGlyphRun(ShapedBuffer shapedBuffer)
{
var glyphRun = new GlyphRun(
shapedBuffer.GlyphTypeface,
shapedBuffer.FontRenderingEmSize,
shapedBuffer.Text,
shapedBuffer,
biDiLevel: shapedBuffer.BidiLevel);
if (shapedBuffer.BidiLevel == 1)
{
shapedBuffer.Reverse();
}
return glyphRun;
}
private static IDisposable Start()
{
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
.With(renderInterface: new PlatformRenderInterface(),
fontManagerImpl: new CustomFontManagerImpl()));
return disposable;
}
}
}