Browse Source

Merge pull request #3690 from Gillibald/fixes/TextBlock

Some optimizations for text processing
Benedikt Stebner 5 years ago
parent
commit
4e3f50edb6

+ 12 - 22
src/Avalonia.Controls/TextBlock.cs

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

+ 28 - 18
src/Avalonia.Visuals/Media/GlyphRun.cs

@@ -274,27 +274,39 @@ namespace Avalonia.Media
             var currentX = 0.0;
             var index = 0;
 
-            for (; index < GlyphIndices.Length; index++)
+            if (GlyphTypeface.IsFixedPitch)
             {
-                double advance;
+                var glyph = GlyphIndices[index];
 
-                if (GlyphAdvances.IsEmpty)
-                {
-                    var glyph = GlyphIndices[index];
+                var advance = GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
 
-                    advance = GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
-                }
-                else
+                index = Math.Min(GlyphIndices.Length - 1,
+                    (int)Math.Round(distance / advance, MidpointRounding.AwayFromZero));
+            }
+            else
+            {
+                for (; index < GlyphIndices.Length; index++)
                 {
-                    advance = GlyphAdvances[index];
-                }
+                    double advance;
 
-                if (currentX + advance >= distance)
-                {
-                    break;
-                }
+                    if (GlyphAdvances.IsEmpty)
+                    {
+                        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);
@@ -473,9 +485,7 @@ namespace Avalonia.Media
         /// </returns>
         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;
 

+ 29 - 60
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' });
 
-        /// <summary>
-        /// 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>
+        /// <inheritdoc cref="TextFormatter.FormatLine"/>
         public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
             TextParagraphProperties paragraphProperties)
         {
@@ -61,17 +53,18 @@ namespace Avalonia.Media.TextFormatting
         /// </returns>
         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>();
 
             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)
@@ -79,29 +72,33 @@ namespace Avalonia.Media.TextFormatting
                     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)
-                {
-                    var shapableTextStyleRun = CreateShapableTextStyleRun(runText, textRun.Style);
+                        while (!runText.IsEmpty)
+                        {
+                            var shapableTextStyleRun = CreateShapableTextStyleRun(runText, textRun.Style);
 
-                    var shapedRun = new ShapedTextRun(runText.Take(shapableTextStyleRun.TextPointer.Length),
-                        shapableTextStyleRun.Style);
+                            var shapedRun = new ShapedTextRun(runText.Take(shapableTextStyleRun.TextPointer.Length),
+                                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;
         }
@@ -115,7 +112,7 @@ namespace Avalonia.Media.TextFormatting
         /// <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>
         /// <returns></returns>
-        private TextLine PerformTextTrimming(TextPointer text, IReadOnlyList<ShapedTextRun> textRuns,
+        private static TextLine PerformTextTrimming(TextPointer text, IReadOnlyList<ShapedTextRun> textRuns,
             double paragraphWidth, TextParagraphProperties paragraphProperties)
         {
             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="paragraphWidth"></param>
         /// <returns></returns>
-        private TextLine PerformTextWrapping(TextPointer text, IReadOnlyList<ShapedTextRun> textRuns,
+        private static TextLine PerformTextWrapping(TextPointer text, IReadOnlyList<ShapedTextRun> textRuns,
             double paragraphWidth, TextParagraphProperties paragraphProperties)
         {
             var availableWidth = paragraphWidth;
@@ -267,41 +264,13 @@ namespace Avalonia.Media.TextFormatting
         /// <param name="textRun">The text run.</param>
         /// <param name="availableWidth">The available width.</param>
         /// <returns></returns>
-        private int MeasureText(ShapedTextRun textRun, double availableWidth)
+        private static int MeasureText(ShapedTextRun textRun, double availableWidth)
         {
-            if (textRun.GlyphRun.Bounds.Width < availableWidth)
-            {
-                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 glyphRun = textRun.GlyphRun;
 
-            var characterHit = textRun.GlyphRun.FindNearestCharacterHit(cluster, out _);
+            var characterHit = glyphRun.GetCharacterHitFromDistance(availableWidth, out _);
 
-            return characterHit.FirstCharacterIndex - textRun.GlyphRun.Characters.Start +
-                   (textRun.GlyphRun.IsLeftToRight ? characterHit.TrailingLength : 0);
+            return characterHit.FirstCharacterIndex + characterHit.TrailingLength - textRun.Text.Start;
         }
 
         /// <summary>

+ 15 - 0
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>
         public ReadOnlySlice<T> AsSlice(int start, int length)
         {
+            if (IsEmpty)
+            {
+                return this;
+            }
+
             if (start < Start || start > End)
             {
                 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>
         public ReadOnlySlice<T> Take(int length)
         {
+            if (IsEmpty)
+            {
+                return this;
+            }
+
             if (length > 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>
         public ReadOnlySlice<T> Skip(int length)
         {
+            if (IsEmpty)
+            {
+                return this;
+            }
+
             if (length > Length)
             {
                 throw new ArgumentOutOfRangeException(nameof(length));

+ 19 - 7
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@@ -173,16 +173,26 @@ namespace Avalonia.Skia
 
             using (var textBlobBuilder = new SKTextBlobBuilder())
             {
+                SKTextBlob textBlob;
+
+                width = 0;
+
                 var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight);
 
                 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();
 
                         for (var i = 0; i < count; i++)
@@ -198,9 +208,11 @@ namespace Avalonia.Skia
                                 width += glyphRun.GlyphAdvances[i];
                             }
                         }
-                    }
 
-                    buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span);
+                        buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span);
+
+                        textBlob = textBlobBuilder.Build();
+                    }
                 }
                 else
                 {
@@ -229,9 +241,9 @@ namespace Avalonia.Skia
                     buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span);
 
                     width = currentX;
-                }
 
-                var textBlob = textBlobBuilder.Build();
+                    textBlob = textBlobBuilder.Build();
+                }
 
                 return new GlyphRunImpl(paint, textBlob);
             }

+ 52 - 20
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.Unicode;
 using Avalonia.Platform;
@@ -72,36 +73,32 @@ namespace Avalonia.Skia
 
                 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;
-                    // Depends on direction of layout
-                    //var advanceY = pos[i].YAdvance * textScale;
-
-                    glyphAdvances[i] = advanceX;
-
-                    var offsetX = pos[i].XOffset * textScale;
-                    var offsetY = pos[i].YOffset * textScale;
+                    if (!glyphTypeface.IsFixedPitch)
+                    {
+                        SetAdvance(glyphPositions, i, textScale, ref glyphAdvances);
+                    }
 
-                    glyphOffsets[i] = new Vector(offsetX, offsetY);
+                    SetOffset(glyphPositions, i, textScale, ref glyphOffsets);
                 }
 
                 return new GlyphRun(glyphTypeface, textFormat.FontRenderingEmSize,
@@ -112,5 +109,40 @@ namespace Avalonia.Skia
                     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


+ 4 - 1
tests/Avalonia.Skia.UnitTests/CustomFontManagerImpl.cs

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

+ 64 - 2
tests/Avalonia.Skia.UnitTests/SimpleTextFormatterTests.cs

@@ -1,6 +1,8 @@
 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;
@@ -240,7 +242,9 @@ namespace Avalonia.Skia.UnitTests
                     {
                         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));
 
@@ -280,7 +284,9 @@ namespace Avalonia.Skia.UnitTests
                     {
                         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);
 
@@ -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()
         {
             var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface