浏览代码

Fix TextLineImp GetTextBounds
Fix Tab character handling

Benedikt Stebner 2 年之前
父节点
当前提交
f307b7364a

+ 3 - 14
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@@ -697,13 +697,8 @@ namespace Avalonia.Media.TextFormatting
 
                     i = lastRunIndex;
 
-                    if (directionalWidth == 0)
-                    {
-                        continue;
-                    }
-
-                    var coveredLength = 0;
-                    TextBounds? textBounds = null;
+                    int coveredLength;
+                    TextBounds? textBounds;
 
                     switch (currentDirection)
                     {
@@ -831,14 +826,8 @@ namespace Avalonia.Media.TextFormatting
 
                     i = firstRunIndex;
 
-                    if (directionalWidth == 0)
-                    {
-                        continue;
-                    }
-
-                    var coveredLength = 0;
-
                     TextBounds? textBounds = null;
+                    int coveredLength;
 
                     switch (currentDirection)
                     {

+ 3 - 3
src/Skia/Avalonia.Skia/TextShaperImpl.cs

@@ -26,8 +26,8 @@ namespace Avalonia.Skia
             using (var buffer = new Buffer())
             {
                 // HarfBuzz needs the surrounding characters to correctly shape the text
-                var containingText = GetContainingMemory(text, out var start, out var length);
-                buffer.AddUtf16(containingText.Span, start, length);
+                var containingText = GetContainingMemory(text, out var start, out var length).Span;
+                buffer.AddUtf16(containingText, start, length);
 
                 MergeBreakPair(buffer);
 
@@ -72,7 +72,7 @@ namespace Avalonia.Skia
 
                     var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);
 
-                    if (i < textSpan.Length && textSpan[i] == '\t')
+                    if (glyphCluster < containingText.Length && containingText[glyphCluster] == '\t')
                     {
                         glyphIndex = typeface.GetGlyph(' ');
 

+ 9 - 4
src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Buffers;
+using System.Collections.Concurrent;
 using System.Globalization;
 using System.Runtime.InteropServices;
 using Avalonia.Media.TextFormatting;
@@ -13,6 +14,8 @@ namespace Avalonia.Direct2D1.Media
 {
     internal class TextShaperImpl : ITextShaperImpl
     {
+        private static readonly ConcurrentDictionary<int, Language> s_cachedLanguage = new();
+
         public ShapedBuffer ShapeText(ReadOnlyMemory<char> text, TextShaperOptions options)
         {
             var textSpan = text.Span;
@@ -24,8 +27,8 @@ namespace Avalonia.Direct2D1.Media
             using (var buffer = new Buffer())
             {
                 // HarfBuzz needs the surrounding characters to correctly shape the text
-                var containingText = GetContainingMemory(text, out var start, out var length);
-                buffer.AddUtf16(containingText.Span, start, length);
+                var containingText = GetContainingMemory(text, out var start, out var length).Span;
+                buffer.AddUtf16(containingText, start, length);
 
                 MergeBreakPair(buffer);
 
@@ -33,7 +36,9 @@ namespace Avalonia.Direct2D1.Media
 
                 buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft;
 
-                buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture);
+                var usedCulture = culture ?? CultureInfo.CurrentCulture;
+
+                buffer.Language = s_cachedLanguage.GetOrAdd(usedCulture.LCID, _ => new Language(usedCulture));
 
                 var font = ((GlyphTypefaceImpl)typeface).Font;
 
@@ -68,7 +73,7 @@ namespace Avalonia.Direct2D1.Media
 
                     var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);
 
-                    if (i < textSpan.Length && textSpan[i] == '\t')
+                    if (glyphCluster < containingText.Length && containingText[glyphCluster] == '\t')
                     {
                         glyphIndex = typeface.GetGlyph(' ');
 

+ 25 - 1
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs

@@ -1051,7 +1051,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
         [InlineData("שנב🧐שנב", 2, 4, FlowDirection.LeftToRight, "11.268,38.208")]
         [InlineData("שנב🧐שנב", 2, 4, FlowDirection.RightToLeft, "11.268,38.208")]
         [Theory]
-        public void Should_HitTextTextRangeBetweenRuns(string text, int start, int length, 
+        public void Should_HitTestTextRangeBetweenRuns(string text, int start, int length, 
             FlowDirection flowDirection, string expected)
         {
             using (Start())
@@ -1087,6 +1087,30 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
             }
         }
 
+        [Fact]
+        public void Should_HitTestTextRangeWithLineBreaks()
+        {
+            using (Start())
+            {
+                var beforeLinebreak = "Line before linebreak";
+                var afterLinebreak = "Line after linebreak";
+                var text = beforeLinebreak + Environment.NewLine + "" + Environment.NewLine + afterLinebreak;
+
+                var textLayout = new TextLayout(text, Typeface.Default, 12, Brushes.Black);
+
+                var end = text.Length - afterLinebreak.Length + 1;
+
+                var rects = textLayout.HitTestTextRange(0, end).ToArray();
+
+                Assert.Equal(3, rects.Length);
+
+                var endX = textLayout.TextLines[2].GetDistanceFromCharacterHit(new CharacterHit(end));
+
+                //First character should be covered
+                Assert.Equal(7.201171875, endX, 2);
+            }
+        }
+
 
         private static IDisposable Start()
         {

+ 24 - 0
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

@@ -622,6 +622,30 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
             }
         }
 
+        [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));
+
+                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()
         {

+ 3 - 3
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs

@@ -31,11 +31,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
         {
             using (Start())
             {
-                var text = "\t";
+                var text = "012345\t";
                 var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 12, 0, CultureInfo.CurrentCulture, 100);
-                var shapedBuffer = TextShaper.Current.ShapeText(text, options);
+                var shapedBuffer = TextShaper.Current.ShapeText(text.AsMemory().Slice(6), options);
 
-                Assert.Equal(shapedBuffer.Length, text.Length);
+                Assert.Equal(1, shapedBuffer.Length);
                 Assert.Equal(100, shapedBuffer[0].GlyphAdvance);
             }
         }

+ 8 - 4
tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Buffers;
+using System.Collections.Concurrent;
 using System.Globalization;
 using System.Runtime.InteropServices;
 using Avalonia.Media.TextFormatting;
@@ -13,6 +14,7 @@ namespace Avalonia.UnitTests
 {
     internal class HarfBuzzTextShaperImpl : ITextShaperImpl
     {
+        private static readonly ConcurrentDictionary<int, Language> s_cachedLanguage = new();
         public ShapedBuffer ShapeText(ReadOnlyMemory<char> text, TextShaperOptions options)
         {
             var textSpan = text.Span;
@@ -24,8 +26,8 @@ namespace Avalonia.UnitTests
             using (var buffer = new Buffer())
             {
                 // HarfBuzz needs the surrounding characters to correctly shape the text
-                var containingText = GetContainingMemory(text, out var start, out var length);
-                buffer.AddUtf16(containingText.Span, start, length);
+                var containingText = GetContainingMemory(text, out var start, out var length).Span;
+                buffer.AddUtf16(containingText, start, length);
 
                 MergeBreakPair(buffer);
 
@@ -33,7 +35,9 @@ namespace Avalonia.UnitTests
 
                 buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft;
 
-                buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture);
+                var usedCulture = culture ?? CultureInfo.CurrentCulture;
+
+                buffer.Language = s_cachedLanguage.GetOrAdd(usedCulture.LCID, _ => new Language(usedCulture));
 
                 var font = ((HarfBuzzGlyphTypefaceImpl)typeface).Font;
 
@@ -68,7 +72,7 @@ namespace Avalonia.UnitTests
 
                     var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);
 
-                    if (textSpan[i] == '\t')
+                    if (glyphCluster < containingText.Length && containingText[glyphCluster] == '\t')
                     {
                         glyphIndex = typeface.GetGlyph(' ');