#nullable enable using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using Avalonia.Headless; using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.UnitTests; using Xunit; using static System.Net.Mime.MediaTypeNames; namespace Avalonia.Skia.UnitTests.Media.TextFormatting { public class TextLineTests { private const string s_multiLineText = "012345678\r\r0123456789"; [Fact] public void Should_Get_First_CharacterHit() { using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var textSource = new SingleBufferTextSource(s_multiLineText, defaultProperties); var formatter = new TextFormatterImpl(); var currentIndex = 0; while (currentIndex < s_multiLineText.Length) { var textLine = formatter.FormatLine(textSource, currentIndex, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); Assert.NotNull(textLine); var firstCharacterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(int.MinValue)); Assert.Equal(textLine.FirstTextSourceIndex, firstCharacterHit.FirstCharacterIndex); currentIndex += textLine.Length; } } } [Fact] public void Should_Get_Last_CharacterHit() { using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var textSource = new SingleBufferTextSource(s_multiLineText, defaultProperties); var formatter = new TextFormatterImpl(); var currentIndex = 0; while (currentIndex < s_multiLineText.Length) { var textLine = formatter.FormatLine(textSource, currentIndex, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); Assert.NotNull(textLine); var lastCharacterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(int.MaxValue)); Assert.Equal(textLine.FirstTextSourceIndex + textLine.Length, lastCharacterHit.FirstCharacterIndex + lastCharacterHit.TrailingLength); currentIndex += textLine.Length; } } } [Fact] public void Should_Get_Next_Caret_CharacterHit_Bidi() { const string text = "אבג 1 ABC"; using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var textSource = new SingleBufferTextSource(text, defaultProperties); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); Assert.NotNull(textLine); var clusters = new List(); foreach (var textRun in textLine.TextRuns.OrderBy(x => TextTestHelper.GetStartCharIndex(x.Text))) { var shapedRun = (ShapedTextRun)textRun; var runClusters = shapedRun.ShapedBuffer.Select(glyph => glyph.GlyphCluster); clusters.AddRange(shapedRun.IsReversed ? runClusters.Reverse() : runClusters); } var nextCharacterHit = new CharacterHit(0, clusters[1] - clusters[0]); foreach (var cluster in clusters) { Assert.Equal(cluster, nextCharacterHit.FirstCharacterIndex); nextCharacterHit = textLine.GetNextCaretCharacterHit(nextCharacterHit); } var lastCharacterHit = nextCharacterHit; nextCharacterHit = textLine.GetNextCaretCharacterHit(lastCharacterHit); Assert.Equal(lastCharacterHit.FirstCharacterIndex, nextCharacterHit.FirstCharacterIndex); Assert.Equal(lastCharacterHit.TrailingLength, nextCharacterHit.TrailingLength); } } [Fact] public void Should_Get_Previous_Caret_CharacterHit_Bidi() { const string text = "אבג 1 ABC"; using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var textSource = new SingleBufferTextSource(text, defaultProperties); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); Assert.NotNull(textLine); var clusters = new List(); foreach (var textRun in textLine.TextRuns.OrderBy(x => TextTestHelper.GetStartCharIndex(x.Text))) { var shapedRun = (ShapedTextRun)textRun; var runClusters = shapedRun.ShapedBuffer.Select(glyph => glyph.GlyphCluster); clusters.AddRange(shapedRun.IsReversed ? runClusters.Reverse() : runClusters); } clusters.Reverse(); var nextCharacterHit = new CharacterHit(text.Length - 1); foreach (var cluster in clusters) { var currentCaretIndex = nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength; Assert.Equal(cluster, currentCaretIndex); nextCharacterHit = textLine.GetPreviousCaretCharacterHit(nextCharacterHit); } var lastCharacterHit = nextCharacterHit; nextCharacterHit = textLine.GetPreviousCaretCharacterHit(lastCharacterHit); Assert.Equal(lastCharacterHit.FirstCharacterIndex, nextCharacterHit.FirstCharacterIndex); Assert.Equal(lastCharacterHit.TrailingLength, nextCharacterHit.TrailingLength); } } [InlineData("𐐷𐐷𐐷𐐷𐐷")] [InlineData("01234567🎉\n")] [InlineData("𐐷1234")] [Theory] public void Should_Get_Next_Caret_CharacterHit(string text) { using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var textSource = new SingleBufferTextSource(text, defaultProperties); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); Assert.NotNull(textLine); var clusters = BuildGlyphClusters(textLine); var nextCharacterHit = new CharacterHit(0); for (var i = 0; i < clusters.Count; i++) { var expectedCluster = clusters[i]; var actualCluster = nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength; Assert.Equal(expectedCluster, actualCluster); nextCharacterHit = textLine.GetNextCaretCharacterHit(nextCharacterHit); } var lastCharacterHit = nextCharacterHit; nextCharacterHit = textLine.GetNextCaretCharacterHit(lastCharacterHit); Assert.Equal(lastCharacterHit.FirstCharacterIndex, nextCharacterHit.FirstCharacterIndex); Assert.Equal(lastCharacterHit.TrailingLength, nextCharacterHit.TrailingLength); nextCharacterHit = new CharacterHit(0, clusters[1] - clusters[0]); foreach (var cluster in clusters) { Assert.Equal(cluster, nextCharacterHit.FirstCharacterIndex); nextCharacterHit = textLine.GetNextCaretCharacterHit(nextCharacterHit); } lastCharacterHit = nextCharacterHit; nextCharacterHit = textLine.GetNextCaretCharacterHit(lastCharacterHit); Assert.Equal(lastCharacterHit.FirstCharacterIndex, nextCharacterHit.FirstCharacterIndex); Assert.Equal(lastCharacterHit.TrailingLength, nextCharacterHit.TrailingLength); } } [InlineData("𐐷𐐷𐐷𐐷𐐷")] [InlineData("01234567🎉\n")] [InlineData("𐐷1234")] [Theory] public void Should_Get_Previous_Caret_CharacterHit(string text) { using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var textSource = new SingleBufferTextSource(text, defaultProperties); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); Assert.NotNull(textLine); var clusters = textLine.TextRuns .Cast() .SelectMany(x => x.ShapedBuffer, (_, glyph) => glyph.GlyphCluster) .ToArray(); var previousCharacterHit = new CharacterHit(text.Length); for (var i = clusters.Length - 1; i >= 0; i--) { previousCharacterHit = textLine.GetPreviousCaretCharacterHit(previousCharacterHit); Assert.Equal(clusters[i], previousCharacterHit.FirstCharacterIndex + previousCharacterHit.TrailingLength); } var firstCharacterHit = previousCharacterHit; previousCharacterHit = textLine.GetPreviousCaretCharacterHit(firstCharacterHit); Assert.Equal(firstCharacterHit.FirstCharacterIndex, previousCharacterHit.FirstCharacterIndex); Assert.Equal(0, previousCharacterHit.TrailingLength); previousCharacterHit = new CharacterHit(clusters[^1], text.Length - clusters[^1]); for (var i = clusters.Length - 1; i > 0; i--) { previousCharacterHit = textLine.GetPreviousCaretCharacterHit(previousCharacterHit); Assert.Equal(clusters[i], previousCharacterHit.FirstCharacterIndex + previousCharacterHit.TrailingLength); } } } [Fact] public void Should_Get_Distance_From_CharacterHit() { using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var textSource = new SingleBufferTextSource(s_multiLineText, defaultProperties); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); Assert.NotNull(textLine); var currentDistance = 0.0; foreach (var run in textLine.TextRuns) { var textRun = (ShapedTextRun)run; var glyphRun = textRun.GlyphRun; for (var i = 0; i < glyphRun.GlyphInfos.Count; i++) { var cluster = glyphRun.GlyphInfos[i].GlyphCluster; var advance = glyphRun.GlyphInfos[i].GlyphAdvance; var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(cluster)); Assert.Equal(currentDistance, distance); currentDistance += advance; } } var actualDistance = textLine.GetDistanceFromCharacterHit(new CharacterHit(s_multiLineText.Length)); Assert.Equal(currentDistance, actualDistance); } } [InlineData("ABC012345")] //LeftToRight [InlineData("זה כיף סתם לשמוע איך תנצח קרפד עץ טוב בגן")] //RightToLeft [Theory] public void Should_Get_CharacterHit_From_Distance(string text) { using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var textSource = new SingleBufferTextSource(text, defaultProperties); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); Assert.NotNull(textLine); var isRightToLeft = IsRightToLeft(textLine); var rects = BuildRects(textLine); var glyphClusters = BuildGlyphClusters(textLine); for (var i = 0; i < rects.Count; i++) { var cluster = glyphClusters[i]; var rect = rects[i]; var characterHit = textLine.GetCharacterHitFromDistance(rect.Left); Assert.Equal(isRightToLeft ? cluster + 1 : cluster, characterHit.FirstCharacterIndex + characterHit.TrailingLength); } } } public static IEnumerable CollapsingData { get { yield return CreateData("01234 01234 01234", 120, TextTrimming.PrefixCharacterEllipsis, "01234 01\u20264 01234"); yield return CreateData("01234 01234", 58, TextTrimming.CharacterEllipsis, "01234 0\u2026"); yield return CreateData("01234 01234", 58, TextTrimming.WordEllipsis, "01234\u2026"); yield return CreateData("01234", 9, TextTrimming.CharacterEllipsis, "\u2026"); yield return CreateData("01234", 2, TextTrimming.CharacterEllipsis, ""); object[] CreateData(string text, double width, TextTrimming mode, string expected) { return new object[] { text, width, mode, expected }; } } } [MemberData(nameof(CollapsingData))] [Theory] public void Should_Collapse_Line(string text, double width, TextTrimming trimming, string expected) { using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var textSource = new SingleBufferTextSource(text, defaultProperties); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); Assert.NotNull(textLine); Assert.False(textLine.HasCollapsed); TextCollapsingProperties collapsingProperties = trimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(width, defaultProperties, FlowDirection.LeftToRight)); var collapsedLine = textLine.Collapse(collapsingProperties); Assert.True(collapsedLine.HasCollapsed); var trimmedText = collapsedLine.TextRuns.SelectMany(x => x.Text.ToString()).ToArray(); Assert.Equal(expected.Length, trimmedText.Length); for (var i = 0; i < expected.Length; i++) { Assert.Equal(expected[i], trimmedText[i]); } } } [Fact] public void Should_Get_Next_CharacterHit_For_Drawable_Runs() { using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var textSource = new DrawableRunTextSource(); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); Assert.NotNull(textLine); Assert.Equal(4, textLine.TextRuns.Count); var currentHit = textLine.GetNextCaretCharacterHit(new CharacterHit(0)); Assert.Equal(1, currentHit.FirstCharacterIndex); Assert.Equal(0, currentHit.TrailingLength); currentHit = textLine.GetNextCaretCharacterHit(currentHit); Assert.Equal(2, currentHit.FirstCharacterIndex); Assert.Equal(0, currentHit.TrailingLength); currentHit = textLine.GetNextCaretCharacterHit(currentHit); Assert.Equal(3, currentHit.FirstCharacterIndex); Assert.Equal(0, currentHit.TrailingLength); currentHit = textLine.GetNextCaretCharacterHit(currentHit); Assert.Equal(4, currentHit.FirstCharacterIndex); Assert.Equal(0, currentHit.TrailingLength); } } [Fact] public void Should_Get_Previous_CharacterHit_For_Drawable_Runs() { using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var textSource = new DrawableRunTextSource(); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); Assert.NotNull(textLine); Assert.Equal(4, textLine.TextRuns.Count); var currentHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(3, 1)); Assert.Equal(3, currentHit.FirstCharacterIndex); Assert.Equal(0, currentHit.TrailingLength); currentHit = textLine.GetPreviousCaretCharacterHit(currentHit); Assert.Equal(2, currentHit.FirstCharacterIndex); Assert.Equal(0, currentHit.TrailingLength); currentHit = textLine.GetPreviousCaretCharacterHit(currentHit); Assert.Equal(1, currentHit.FirstCharacterIndex); Assert.Equal(0, currentHit.TrailingLength); currentHit = textLine.GetPreviousCaretCharacterHit(currentHit); Assert.Equal(0, currentHit.FirstCharacterIndex); Assert.Equal(0, currentHit.TrailingLength); } } [Fact] public void Should_Get_CharacterHit_From_Distance_For_Drawable_Runs() { using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var textSource = new DrawableRunTextSource(); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); Assert.NotNull(textLine); var characterHit = textLine.GetCharacterHitFromDistance(50); Assert.Equal(5, characterHit.FirstCharacterIndex); Assert.Equal(1, characterHit.TrailingLength); characterHit = textLine.GetCharacterHitFromDistance(32); Assert.Equal(3, characterHit.FirstCharacterIndex); Assert.Equal(0, characterHit.TrailingLength); } } [Fact] public void Should_Get_Distance_From_CharacterHit_Drawable_Runs() { using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var textSource = new DrawableRunTextSource(); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); Assert.NotNull(textLine); var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(1)); Assert.Equal(14, distance); distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(2)); Assert.True(distance > 14); } } [Fact] public void Should_Get_Distance_From_CharacterHit_Mixed_TextBuffer() { using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var textSource = new MixedTextBufferTextSource(); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); Assert.NotNull(textLine); var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(10)); Assert.Equal(72.01171875, distance); distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(20)); Assert.Equal(144.0234375, distance); distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(30)); Assert.Equal(216.03515625, distance); distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(40)); Assert.Equal(textLine.WidthIncludingTrailingWhitespace, distance); } } [Fact] public void Should_Get_TextBounds_From_Mixed_TextBuffer() { using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var textSource = new MixedTextBufferTextSource(); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); Assert.NotNull(textLine); var textBounds = textLine.GetTextBounds(0, 10); Assert.Equal(1, textBounds.Count); Assert.Equal(72.01171875, textBounds[0].Rectangle.Width); textBounds = textLine.GetTextBounds(0, 20); Assert.Equal(1, textBounds.Count); Assert.Equal(144.0234375, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(0, 30); Assert.Equal(1, textBounds.Count); Assert.Equal(216.03515625, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(0, 40); Assert.Equal(1, textBounds.Count); Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); } } [Fact] public void Should_Get_TextBounds_For_LineBreak() { using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var textSource = new SingleBufferTextSource(Environment.NewLine, defaultProperties); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); Assert.NotNull(textLine); var textBounds = textLine.GetTextBounds(0, Environment.NewLine.Length); Assert.Equal(1, textBounds.Count); Assert.Equal(1, textBounds[0].TextRunBounds.Count); Assert.Equal(Environment.NewLine.Length, textBounds[0].TextRunBounds[0].Length); } } [Fact] public void Should_GetTextRange() { var text = "שדגככעיחדגכAישדגשדגחייטYDASYWIWחיחלדשSAטויליHUHIUHUIDWKLאא'ק'קחליק/'וקןגגגלךשף'/קפוכדגכשדגשיח'/קטאגשד"; using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var textSource = new SingleBufferTextSource(text, defaultProperties); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); Assert.NotNull(textLine); var textRuns = textLine.TextRuns.Cast().ToList(); var lineWidth = textLine.WidthIncludingTrailingWhitespace; var textBounds = textLine.GetTextBounds(0, text.Length); TextBounds? lastBounds = null; var runBounds = textBounds.SelectMany(x => x.TextRunBounds).ToList(); Assert.Equal(textRuns.Count, runBounds.Count); for (var i = 0; i < textRuns.Count; i++) { var run = textRuns[i]; var bounds = runBounds[i]; Assert.Equal(TextTestHelper.GetStartCharIndex(run.Text), bounds.TextSourceCharacterIndex); Assert.Equal(run, bounds.TextRun); Assert.Equal(run.Size.Width, bounds.Rectangle.Width, 2); } for (var i = 0; i < textBounds.Count; i++) { var currentBounds = textBounds[i]; if (lastBounds != null) { Assert.Equal(lastBounds.Rectangle.Right, currentBounds.Rectangle.Left, 2); } var sumOfRunWidth = currentBounds.TextRunBounds.Sum(x => x.Rectangle.Width); Assert.Equal(sumOfRunWidth, currentBounds.Rectangle.Width, 2); lastBounds = currentBounds; } var sumOfBoundsWidth = textBounds.Sum(x => x.Rectangle.Width); Assert.Equal(lineWidth, sumOfBoundsWidth, 2); } } [Fact] public void Should_Get_CharacterHit_For_Distance_With_TextEndOfLine() { using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var textSource = new SingleBufferTextSource("Hello World", defaultProperties, true); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, 1000, new GenericTextParagraphProperties(defaultProperties)); Assert.NotNull(textLine); var characterHit = textLine.GetCharacterHitFromDistance(1000); Assert.Equal(10, characterHit.FirstCharacterIndex); Assert.Equal(1, characterHit.TrailingLength); } } [Fact] public void Should_GetNextCaretCharacterHit_From_Mixed_TextBuffer() { using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var textSource = new MixedTextBufferTextSource(); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); Assert.NotNull(textLine); var characterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(9, 1)); Assert.Equal(10, characterHit.FirstCharacterIndex); Assert.Equal(1, characterHit.TrailingLength); characterHit = textLine.GetNextCaretCharacterHit(characterHit); Assert.Equal(11, characterHit.FirstCharacterIndex); Assert.Equal(1, characterHit.TrailingLength); characterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(19, 1)); Assert.Equal(20, characterHit.FirstCharacterIndex); Assert.Equal(1, characterHit.TrailingLength); characterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(10)); Assert.Equal(11, characterHit.FirstCharacterIndex); Assert.Equal(0, characterHit.TrailingLength); characterHit = textLine.GetNextCaretCharacterHit(characterHit); Assert.Equal(12, characterHit.FirstCharacterIndex); Assert.Equal(0, characterHit.TrailingLength); characterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(20)); Assert.Equal(21, characterHit.FirstCharacterIndex); Assert.Equal(0, characterHit.TrailingLength); } } [Fact] public void Should_GetPreviousCaretCharacterHit_From_Mixed_TextBuffer() { using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var textSource = new MixedTextBufferTextSource(); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); Assert.NotNull(textLine); var characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(20, 1)); Assert.Equal(20, characterHit.FirstCharacterIndex); Assert.Equal(0, characterHit.TrailingLength); characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(10, 1)); Assert.Equal(10, characterHit.FirstCharacterIndex); Assert.Equal(0, characterHit.TrailingLength); characterHit = textLine.GetPreviousCaretCharacterHit(characterHit); Assert.Equal(9, characterHit.FirstCharacterIndex); Assert.Equal(0, characterHit.TrailingLength); characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(21)); Assert.Equal(20, characterHit.FirstCharacterIndex); Assert.Equal(0, characterHit.TrailingLength); characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(11)); Assert.Equal(10, characterHit.FirstCharacterIndex); Assert.Equal(0, characterHit.TrailingLength); characterHit = textLine.GetPreviousCaretCharacterHit(characterHit); Assert.Equal(9, characterHit.FirstCharacterIndex); Assert.Equal(0, characterHit.TrailingLength); } } [Fact] public void Should_GetCharacterHitFromDistance_From_Mixed_TextBuffer() { using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var textSource = new MixedTextBufferTextSource(); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 20, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); Assert.NotNull(textLine); var characterHit = textLine.GetCharacterHitFromDistance(double.PositiveInfinity); Assert.Equal(40, characterHit.FirstCharacterIndex + characterHit.TrailingLength); } } [Fact] public void Should_Throw_ArgumentOutOfRangeException_For_Zero_TextLength() { using (Start()) { var typeface = Typeface.Default; var defaultProperties = new GenericTextRunProperties(typeface); var textSource = new CustomTextBufferTextSource(new TextCharacters("1234", defaultProperties)); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); Assert.NotNull(textLine); Assert.Throws(() => textLine.GetTextBounds(0, 0)); } } [Fact] public void Should_GetTextBounds_For_Negative_TextLength() { using (Start()) { var typeface = Typeface.Default; var defaultProperties = new GenericTextRunProperties(typeface); var textSource = new CustomTextBufferTextSource(new TextCharacters("1234", defaultProperties)); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); Assert.NotNull(textLine); var textBounds = textLine.GetTextBounds(0, -1); Assert.NotNull(textBounds); Assert.NotEmpty(textBounds); var firstBounds = textBounds[0]; Assert.Empty(firstBounds.TextRunBounds); Assert.Equal(0, firstBounds.Rectangle.Width); Assert.Equal(0, firstBounds.Rectangle.Left); } } [Fact] public void Should_GetTextBounds_For_Exceeding_TextLength() { using (Start()) { var typeface = Typeface.Default; var defaultProperties = new GenericTextRunProperties(typeface); var textSource = new CustomTextBufferTextSource(new TextCharacters("1234", defaultProperties)); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); Assert.NotNull(textLine); var textBounds = textLine.GetTextBounds(10, 1); Assert.NotNull(textBounds); Assert.NotEmpty(textBounds); var firstBounds = textBounds[0]; Assert.Empty(firstBounds.TextRunBounds); Assert.Equal(0, firstBounds.Rectangle.Width); Assert.Equal(textLine.WidthIncludingTrailingWhitespace, firstBounds.Rectangle.Right); } } [Fact] public void Should_GetTextBounds_For_Mixed_Hidden_Runs_With_Ligature() { using (Start()) { var typeface = new Typeface(FontFamily.Parse("resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#Manrope")); var defaultProperties = new GenericTextRunProperties(typeface); var textSource = new CustomTextBufferTextSource( new TextHidden(1), new TextCharacters("Authenti", defaultProperties), new TextHidden(1), new TextHidden(1), new TextCharacters("ff", defaultProperties), new TextHidden(1), new TextHidden(1)); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); Assert.NotNull(textLine); var textBounds = textLine.GetTextBounds(12, 1); Assert.NotEmpty(textBounds); var firstBounds = textBounds[0]; Assert.NotNull(firstBounds.TextRunBounds); Assert.NotEmpty(firstBounds.TextRunBounds); var firstRun = firstBounds.TextRunBounds[0]; Assert.NotNull(firstRun); Assert.Equal(12, firstRun.TextSourceCharacterIndex); } } [Fact] public void Should_GetTextBounds_For_Mixed_Hidden_Runs() { using (Start()) { var typeface = new Typeface(FontFamily.Parse("resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#Manrope")); var defaultProperties = new GenericTextRunProperties(typeface); var textSource = new CustomTextBufferTextSource( new TextHidden(1), new TextCharacters("Authenti", defaultProperties), new TextHidden(1), new TextHidden(1), new TextEndOfParagraph(1)); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); Assert.NotNull(textLine); var textBounds = textLine.GetTextBounds(8, 1); Assert.NotEmpty(textBounds); var firstBounds = textBounds[0]; Assert.NotNull(firstBounds.TextRunBounds); Assert.NotEmpty(firstBounds.TextRunBounds); var firstRun = firstBounds.TextRunBounds[0]; Assert.NotNull(firstRun); Assert.Equal(8, firstRun.TextSourceCharacterIndex); } } [Win32Fact("Windows font")] public void Should_GetTextBounds_Within_Cluster() { using (Start()) { var typeface = new Typeface("Segoe UI Emoji"); var defaultProperties = new GenericTextRunProperties(typeface); var textSource = new CustomTextBufferTextSource(new TextCharacters("🙈", defaultProperties)); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); Assert.NotNull(textLine); var textBounds = textLine.GetTextBounds(0, 1); Assert.NotEmpty(textBounds); var runBounds = textBounds[0].TextRunBounds[0]; Assert.Equal(0, runBounds.TextSourceCharacterIndex); textBounds = textLine.GetTextBounds(1, 1); Assert.NotEmpty(textBounds); runBounds = textBounds[0].TextRunBounds[0]; Assert.Equal(1, runBounds.TextSourceCharacterIndex); textBounds = textLine.GetTextBounds(2, 1); Assert.NotEmpty(textBounds); Assert.NotNull(textBounds[0].TextRunBounds); Assert.Empty(textBounds[0].TextRunBounds); } } [Win32Fact("Windows font")] public void Should_GetTextBounds_After_Last_Index() { using (Start()) { var typeface = new Typeface("Segoe UI Emoji"); var defaultProperties = new GenericTextRunProperties(typeface); var textSource = new CustomTextBufferTextSource(new TextCharacters("🙈", defaultProperties)); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); Assert.NotNull(textLine); var textBounds = textLine.GetTextBounds(2, 1); Assert.NotEmpty(textBounds); var firstBounds = textBounds[0]; Assert.Equal(textLine.Width, firstBounds.Rectangle.Right); Assert.NotNull(firstBounds.TextRunBounds); Assert.Empty(firstBounds.TextRunBounds); } } [Fact] public void Should_Get_Run_Bounds() { using (Start()) { var typeface = new Typeface(FontFamily.Parse("resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#Manrope")); var defaultProperties = new GenericTextRunProperties(typeface); var textSource = new CustomTextBufferTextSource( new TextCharacters("He", defaultProperties), new TextCharacters("Wo", defaultProperties), new TextCharacters("ff", defaultProperties)); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); Assert.NotNull(textLine); var textBounds = textLine.GetTextBounds(1, 1); Assert.NotEmpty(textBounds); textBounds = textLine.GetTextBounds(2, 1); Assert.NotEmpty(textBounds); textBounds = textLine.GetTextBounds(4, 1); Assert.NotEmpty(textBounds); } } [Fact] public void Should_Handle_NewLine_In_RTL_Text() { using (Start()) { var typeface = Typeface.Default; var defaultProperties = new GenericTextRunProperties(typeface); var textSource = new SingleBufferTextSource("test\r\n", defaultProperties); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(FlowDirection.RightToLeft, TextAlignment.Right, true, true, defaultProperties, TextWrapping.Wrap, 0, 0, 0)); Assert.NotNull(textLine); Assert.NotEqual(textLine.NewLineLength, 0); } } [Theory] [InlineData("hello\r\nworld")] [InlineData("مرحباً\r\nبالعالم")] [InlineData("hello مرحباً\r\nworld بالعالم")] [InlineData("مرحباً hello\r\nبالعالم nworld")] public void Should_Set_NewLineLength_For_CRLF_In_RTL_Text(string text) { using (Start()) { var typeface = Typeface.Default; var defaultProperties = new GenericTextRunProperties(typeface); var textSource = new SingleBufferTextSource(text, defaultProperties); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(FlowDirection.RightToLeft, TextAlignment.Right, true, true, defaultProperties, TextWrapping.Wrap, 0, 0, 0)); Assert.NotNull(textLine); Assert.NotEqual(0, textLine.NewLineLength); } } [Fact] public void Should_Get_TextBounds_With_Trailing_Zero_Advance() { const string df7Font = "resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#DF7segHMI"; using (Start()) { var typeface = new Typeface(df7Font); var defaultProperties = new GenericTextRunProperties(typeface); var textSource = new SingleBufferTextSource("3,47-=?:#", defaultProperties); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); Assert.NotNull(textLine); var textBounds = textLine.GetTextBounds(0, 2); Assert.NotEmpty(textBounds); var textRunBounds = textBounds.First().TextRunBounds; Assert.NotEmpty(textBounds); var first = textRunBounds.First(); Assert.Equal(0, first.TextSourceCharacterIndex); Assert.Equal(2, first.Length); } } private class TextHidden : TextRun { public TextHidden(int length) { Length = length; } public override int Length { get; } } private class CustomTextBufferTextSource : ITextSource { private IReadOnlyList _textRuns; public CustomTextBufferTextSource(params TextRun[] textRuns) { _textRuns = textRuns; } public TextRun? GetTextRun(int textSourceIndex) { var pos = 0; for(var i = 0; i < _textRuns.Count; i++) { var currentRun = _textRuns[i]; if(pos + currentRun.Length > textSourceIndex) { return currentRun; } pos += currentRun.Length; } return null; } } private class MixedTextBufferTextSource : ITextSource { public TextRun? GetTextRun(int textSourceIndex) { switch (textSourceIndex) { case 0: return new TextCharacters("aaaaaaaaaa", new GenericTextRunProperties(Typeface.Default)); case 10: return new TextCharacters("bbbbbbbbbb", new GenericTextRunProperties(Typeface.Default)); case 20: return new TextCharacters("cccccccccc", new GenericTextRunProperties(Typeface.Default)); case 30: return new TextCharacters("dddddddddd", new GenericTextRunProperties(Typeface.Default)); default: return null; } } } private class DrawableRunTextSource : ITextSource { private const string Text = "_A_A"; public TextRun? GetTextRun(int textSourceIndex) { switch (textSourceIndex) { case 0: return new CustomDrawableRun(); case 1: return new TextCharacters(Text, new GenericTextRunProperties(Typeface.Default)); case 5: return new CustomDrawableRun(); case 6: return new TextCharacters(Text, new GenericTextRunProperties(Typeface.Default)); default: return null; } } } private class CustomDrawableRun : DrawableTextRun { public override Size Size => new(14, 14); public override double Baseline => 14; public override void Draw(DrawingContext drawingContext, Point origin) { } } private static bool IsRightToLeft(TextLine textLine) { return textLine.TextRuns.Cast().Any(x => !x.ShapedBuffer.IsLeftToRight); } private static List BuildGlyphClusters(TextLine textLine) { var glyphClusters = new List(); var shapedTextRuns = textLine.TextRuns.Cast().ToList(); var lastCluster = -1; foreach (var textRun in shapedTextRuns) { var shapedBuffer = textRun.ShapedBuffer; var currentClusters = shapedBuffer.Select(glyph => glyph.GlyphCluster).ToList(); foreach (var currentCluster in currentClusters) { if (lastCluster == currentCluster) { continue; } glyphClusters.Add(currentCluster); lastCluster = currentCluster; } } return glyphClusters; } private static List BuildRects(TextLine textLine) { var rects = new List(); var height = textLine.Height; var currentX = 0d; var lastCluster = -1; var shapedTextRuns = textLine.TextRuns.Cast().ToList(); foreach (var textRun in shapedTextRuns) { var shapedBuffer = textRun.ShapedBuffer; for (var index = 0; index < shapedBuffer.Length; index++) { var currentCluster = shapedBuffer[index].GlyphCluster; var advance = shapedBuffer[index].GlyphAdvance; if (lastCluster != currentCluster) { rects.Add(new Rect(currentX, 0, advance, height)); } else { var rect = rects[index - 1]; rects.Remove(rect); rect = rect.WithWidth(rect.Width + advance); rects.Add(rect); } currentX += advance; lastCluster = currentCluster; } } return rects; } [Fact] public void Should_Get_TextBounds_Mixed() { using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var text = "0123"; var shaperOption = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, 0, CultureInfo.CurrentCulture); var firstRun = new ShapedTextRun(TextShaper.Current.ShapeText(text, shaperOption), defaultProperties); var textRuns = new List { new CustomDrawableRun(), firstRun, new CustomDrawableRun(), new ShapedTextRun(TextShaper.Current.ShapeText(text, shaperOption), defaultProperties), new CustomDrawableRun(), new ShapedTextRun(TextShaper.Current.ShapeText(text, shaperOption), defaultProperties) }; var textSource = new FixedRunsTextSource(textRuns); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); Assert.NotNull(textLine); var textBounds = textLine.GetTextBounds(0, textLine.Length); Assert.Equal(1, textBounds.Count); Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(0, 1); Assert.Equal(1, textBounds.Count); Assert.Equal(14, textBounds[0].Rectangle.Width); textBounds = textLine.GetTextBounds(0, firstRun.Length + 1); Assert.Equal(1, textBounds.Count); Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(1, firstRun.Length); Assert.Equal(1, textBounds.Count); Assert.Equal(firstRun.Size.Width, textBounds[0].Rectangle.Width); textBounds = textLine.GetTextBounds(0, 1 + firstRun.Length); Assert.Equal(1, textBounds.Count); Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width)); } } [Fact] public void Should_Get_TextBounds_BiDi_LeftToRight() { using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var text = "אאא AAA"; var textSource = new SingleBufferTextSource(text, defaultProperties); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, 200, new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0)); Assert.NotNull(textLine); var textBounds = textLine.GetTextBounds(0, 3); var firstRun = Assert.IsType(textLine.TextRuns[0]); Assert.Equal(1, textBounds.Count); Assert.Equal(firstRun.Size.Width, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(3, 4); var secondRun = Assert.IsType(textLine.TextRuns[1]); Assert.Equal(1, textBounds.Count); Assert.Equal(secondRun.Size.Width, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(0, 4); Assert.Equal(2, textBounds.Count); Assert.Equal(firstRun.Size.Width, textBounds[0].Rectangle.Width); Assert.Equal(7.201171875, textBounds[1].Rectangle.Width); Assert.Equal(firstRun.Size.Width, textBounds[1].Rectangle.Left); textBounds = textLine.GetTextBounds(0, text.Length); Assert.Equal(2, textBounds.Count); Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); } } [Fact] public void Should_Get_TextBounds_BiDi_RightToLeft() { using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var text = "אאא AAA"; var textSource = new SingleBufferTextSource(text, defaultProperties); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, 200, new GenericTextParagraphProperties(FlowDirection.RightToLeft, TextAlignment.Left, true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0)); Assert.NotNull(textLine); var textBounds = textLine.GetTextBounds(0, 4); var secondRun = Assert.IsType(textLine.TextRuns[1]); Assert.Equal(1, textBounds.Count); Assert.Equal(secondRun.Size.Width, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(4, 3); var firstRun = Assert.IsType(textLine.TextRuns[0]); Assert.Equal(1, textBounds.Count); Assert.Equal(3, textBounds[0].TextRunBounds.Sum(x => x.Length)); Assert.Equal(firstRun.Size.Width, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(0, 5); Assert.Equal(2, textBounds.Count); Assert.Equal(5, textBounds.Sum(x => x.TextRunBounds.Sum(x => x.Length))); Assert.Equal(secondRun.Size.Width, textBounds[1].Rectangle.Width); Assert.Equal(7.201171875, textBounds[0].Rectangle.Width); Assert.Equal(textLine.Start + 7.201171875, textBounds[0].Rectangle.Right, 2); Assert.Equal(textLine.Start + firstRun.Size.Width, textBounds[1].Rectangle.Left, 2); textBounds = textLine.GetTextBounds(0, text.Length); Assert.Equal(2, textBounds.Count); Assert.Equal(7, textBounds.Sum(x => x.TextRunBounds.Sum(x => x.Length))); Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width), 2); } } [Fact] public void Should_GetTextBounds_With_EndOfParagraph_RightToLeft() { var text = "لوحة المفاتيح العربية"; using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var textSource = new SingleBufferTextSource(text, defaultProperties, true); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0)); Assert.NotNull(textLine); var textBounds = textLine.GetTextBounds(0, 1); Assert.Equal(1, textBounds.Count); var firstBounds = textBounds.First(); Assert.True(firstBounds.TextRunBounds.Count > 0); } } [Fact] public void Should_GetTextBounds_With_EndOfParagraph() { var text = "abc"; using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var textSource = new SingleBufferTextSource(text, defaultProperties, true); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0)); Assert.NotNull(textLine); var textBounds = textLine.GetTextBounds(3, 1); Assert.Equal(1, textBounds.Count); var firstBounds = textBounds.First(); Assert.True(firstBounds.TextRunBounds.Count > 0); } } [Fact] public void Should_GetTextBounds_NotInfiniteLoop() { using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var shaperOption = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, 0, CultureInfo.CurrentCulture); var shaperOption2 = new TextShaperOptions(Typeface.Default.GlyphTypeface, 11, 0, CultureInfo.CurrentCulture); var textRuns = new List { new ShapedTextRun(TextShaper.Current.ShapeText("قرأ ", shaperOption), defaultProperties), new ShapedTextRun(TextShaper.Current.ShapeText("Wikipedia\u2122", shaperOption), defaultProperties), new ShapedTextRun(TextShaper.Current.ShapeText("\u200e ", shaperOption2), defaultProperties), new ShapedTextRun(TextShaper.Current.ShapeText("طوال اليوم", shaperOption), defaultProperties), new ShapedTextRun(TextShaper.Current.ShapeText(".", shaperOption), defaultProperties) }; var textSource = new FixedRunsTextSource(textRuns); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0)); Assert.NotNull(textLine); textLine.GetTextBounds(4, 11); } } [Fact] public void Should_GetTextBounds_Bidi() { var text = "אבגדה 12345 ABCDEF אבגדה"; using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var textSource = new SingleBufferTextSource(text, defaultProperties, true); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0)); Assert.NotNull(textLine); var bounds = textLine.GetTextBounds(6, 1); Assert.Equal(1, bounds.Count); Assert.Equal(0, bounds[0].Rectangle.Left); bounds = textLine.GetTextBounds(5, 1); Assert.Equal(1, bounds.Count); Assert.Equal(36.005859374999993, bounds[0].Rectangle.Left); bounds = textLine.GetTextBounds(0, 1); Assert.Equal(1, bounds.Count); Assert.Equal(71.165859375, bounds[0].Rectangle.Right); bounds = textLine.GetTextBounds(11, 1); Assert.Equal(1, bounds.Count); Assert.Equal(71.165859375, bounds[0].Rectangle.Left); bounds = textLine.GetTextBounds(0, 25); Assert.Equal(4, bounds.Count); Assert.Equal(textLine.WidthIncludingTrailingWhitespace, bounds.Last().Rectangle.Right); } } [Fact] public void Should_GetTextBounds_Bidi_2() { var text = "אבג ABC אבג 123"; using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var textSource = new SingleBufferTextSource(text, defaultProperties, true); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0)); Assert.NotNull(textLine); var bounds = textLine.GetTextBounds(0, text.Length); Assert.Equal(4, bounds.Count); var right = bounds.Last().Rectangle.Right; Assert.Equal(textLine.WidthIncludingTrailingWhitespace, right); } } [Fact] public void Should_GetPreviousCharacterHit_Non_Trailing() { var text = "123.45.67.•"; using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var textSource = new SingleBufferTextSource(text, defaultProperties, true); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0)); Assert.NotNull(textLine); var characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(10, 1)); } } [Theory] [InlineData("\0", 0.0)] [InlineData("\0\0\0", 0.0)] [InlineData("\0A\0\0", 7.201171875)] [InlineData("\0AA\0AA\0", 28.8046875)] public void Should_Ignore_Null_Terminator(string text, double width) { using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var textSource = new SingleBufferTextSource(text, defaultProperties, true); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0)); Assert.NotNull(textLine); Assert.Equal(width, textLine.Width); } } [Fact] public void Should_GetTextBounds_For_Clustered_Zero_Width_Characters() { const string text = "\r\n"; using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var textSource = new TextFormatterTests.ListTextSource(new TextHidden(1) ,new TextCharacters(text, defaultProperties)); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0)); Assert.NotNull(textLine); var textBounds = textLine.GetTextBounds(2, 1); Assert.NotEmpty(textBounds); var firstBounds = textBounds[0]; Assert.NotEmpty(firstBounds.TextRunBounds); var firstRunBounds = firstBounds.TextRunBounds[0]; Assert.Equal(2, firstRunBounds.TextSourceCharacterIndex); Assert.Equal(1, firstRunBounds.Length); } } [InlineData("y", -8, -1.304, -5.44)] [InlineData("f", -12, -11.824, -4.44)] [InlineData("a", 1, -0.232, -20.44)] [Win32Theory("Values depend on the Skia platform backend")] public void Should_Produce_Overhang(string text, double leading, double trailing, double after) { const string symbolsFont = "resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Source Serif"; using (Start()) { var typeface = new Typeface(FontFamily.Parse(symbolsFont)); var defaultProperties = new GenericTextRunProperties(typeface, 64); var textSource = new SingleBufferTextSource(text, defaultProperties); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0)); Assert.NotNull(textLine); Assert.Equal(leading, textLine.OverhangLeading, 2); Assert.Equal(trailing, textLine.OverhangTrailing, 2); Assert.Equal(after, textLine.OverhangAfter, 2); } } [Fact] public void Should_GetTextBounds_For_Multiple_TextRuns() { var text = "Test👩🏽‍🚒"; using (Start()) { var typeface = Typeface.Default; var defaultProperties = new GenericTextRunProperties(typeface, 12); var textSource = new SingleBufferTextSource(text, defaultProperties); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0)); Assert.NotNull(textLine); var result = textLine.GetTextBounds(0, 11); Assert.Equal(1, result.Count); var firstBounds = result[0]; Assert.NotEmpty(firstBounds.TextRunBounds); Assert.Equal(textLine.WidthIncludingTrailingWhitespace, firstBounds.Rectangle.Width, 2); } } [Fact] public void Should_GetTextBounds_Within_Cluster_2() { var text = "Test👩🏽‍🚒"; using (Start()) { var typeface = Typeface.Default; var defaultProperties = new GenericTextRunProperties(typeface, 12); var textSource = new SingleBufferTextSource(text, defaultProperties); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0)); Assert.NotNull(textLine); var textPosition = 0; while(textPosition < text.Length) { var bounds = textLine.GetTextBounds(textPosition, 1); Assert.Equal(1, bounds.Count); var firstBounds = bounds[0]; Assert.Equal(1, firstBounds.TextRunBounds.Count); var firstRunBounds = firstBounds.TextRunBounds[0]; Assert.Equal(textPosition, firstRunBounds.TextSourceCharacterIndex); var expectedDistance = firstRunBounds.Rectangle.Left; var characterHit = new CharacterHit(textPosition); var distance = textLine.GetDistanceFromCharacterHit(characterHit); Assert.Equal(expectedDistance, distance, 2); var nextCharacterHit = textLine.GetNextCaretCharacterHit(characterHit); var expectedNextPosition = textPosition + firstRunBounds.Length; var nextPosition = nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength; Assert.Equal(expectedNextPosition, nextPosition); var previousCharacterHit = textLine.GetPreviousCaretCharacterHit(nextCharacterHit); Assert.Equal(characterHit, previousCharacterHit); textPosition += firstRunBounds.Length; } } } [Fact] public void Should_Get_TextBounds_With_Mixed_Runs_Within_Cluster() { using (Start()) { const string manropeFont = "resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#Manrope"; var typeface = new Typeface(manropeFont); var defaultProperties = new GenericTextRunProperties(typeface); var text = "Fotografin"; var shaperOption = new TextShaperOptions(typeface.GlyphTypeface); var firstRun = new ShapedTextRun(TextShaper.Current.ShapeText(text, shaperOption), defaultProperties); var textRuns = new List { new CustomDrawableRun(), new CustomDrawableRun(), firstRun, new CustomDrawableRun(), }; var textSource = new FixedRunsTextSource(textRuns); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); Assert.NotNull(textLine); var textBounds = textLine.GetTextBounds(10, 1); Assert.Equal(1, textBounds.Count); var firstBounds = textBounds[0]; Assert.NotEmpty(firstBounds.TextRunBounds); var firstRunBounds = firstBounds.TextRunBounds[0]; Assert.Equal(1, firstRunBounds.Length); } } [Fact] public void Should_Get_TextBounds_Tamil() { var text = "எடுத்துக்காட்டு வழி வினவல்"; using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var textSource = new SingleBufferTextSource(text, defaultProperties, true); var formatter = new TextFormatterImpl(); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0)); Assert.NotNull(textLine); Assert.NotEmpty(textLine.TextRuns); var firstRun = textLine.TextRuns[0] as ShapedTextRun; Assert.NotNull(firstRun); var clusterWidth = new List(); var distances = new List(); var clusters = new List(); var lastCluster = -1; var currentDistance = 0.0; var currentAdvance = 0.0; foreach (var glyphInfo in firstRun.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); for (var i = 6; i < clusters.Count; i++) { var cluster = clusters[i]; var expectedDistance = distances[i]; var expectedWidth = clusterWidth[i]; var actualDistance = textLine.GetDistanceFromCharacterHit(new CharacterHit(cluster)); Assert.Equal(expectedDistance, actualDistance, 2); var characterHit = textLine.GetCharacterHitFromDistance(expectedDistance); var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; Assert.Equal(cluster, textPosition); var bounds = textLine.GetTextBounds(cluster, 1); Assert.NotNull(bounds); Assert.NotEmpty(bounds); var firstBounds = bounds[0]; Assert.NotEmpty(firstBounds.TextRunBounds); var firstRunBounds = firstBounds.TextRunBounds[0]; Assert.Equal(cluster, firstRunBounds.TextSourceCharacterIndex); var width = firstRunBounds.Rectangle.Width; Assert.Equal(expectedWidth, width, 2); } } } private class FixedRunsTextSource : ITextSource { private readonly IReadOnlyList _textRuns; public FixedRunsTextSource(IReadOnlyList textRuns) { _textRuns = textRuns; } public TextRun? GetTextRun(int textSourceIndex) { var currentPosition = 0; foreach (var textRun in _textRuns) { if (currentPosition == textSourceIndex) { return textRun; } currentPosition += textRun.Length; } return null; } } private static IDisposable Start() { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface .With(renderInterface: new PlatformRenderInterface(null), textShaperImpl: new TextShaperImpl(), fontManagerImpl: new CustomFontManagerImpl())); return disposable; } } }