소스 검색

Fix font fallback
Fix TextPresenter measure

Benedikt Stebner 3 년 전
부모
커밋
3fdfdd64d2

+ 3 - 1
src/Avalonia.Controls/Presenters/TextPresenter.cs

@@ -515,9 +515,11 @@ namespace Avalonia.Controls.Presenters
         
         protected override Size MeasureOverride(Size availableSize)
         {
-            if (availableSize != Size.Infinity)
+            if (!double.IsInfinity(availableSize.Width) && availableSize != _constraint)
             {
                 _constraint = availableSize;
+                
+                InvalidateTextLayout();
             }
 
             return TextLayout.Size;

+ 10 - 0
src/Avalonia.Visuals/Media/GlyphRun.cs

@@ -669,6 +669,15 @@ namespace Avalonia.Media
 
                     var codepointIndex = IsLeftToRight ? cluster - _characters.Start : _characters.End - cluster;
 
+                    if (codepointIndex < 0)
+                    {
+                        trailingWhitespaceLength = _characters.Length;
+                        
+                        glyphCount = GlyphClusters.Count;
+                        
+                        break;
+                    }
+                    
                     var codepoint = Codepoint.ReadAt(_characters, codepointIndex, out _);
 
                     if (!codepoint.IsWhiteSpace)
@@ -682,6 +691,7 @@ namespace Avalonia.Media
                     }
 
                     trailingWhitespaceLength++;
+                    
                     glyphCount++;
                 }
             }

+ 0 - 2
src/Avalonia.Visuals/Media/GlyphTypeface.cs

@@ -5,8 +5,6 @@ namespace Avalonia.Media
 {
     public sealed class GlyphTypeface : IDisposable
     {
-        public const int InvisibleGlyph = 3;
-
         public GlyphTypeface(Typeface typeface) 
             : this(FontManager.Current.PlatformImpl.CreateGlyphTypeface(typeface)) 
         {

+ 9 - 0
src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs

@@ -152,6 +152,15 @@ namespace Avalonia.Media.TextFormatting
 
             var first = new ShapedTextCharacters(splitBuffer.First, Properties);
 
+            #if DEBUG
+
+            if (first.Text.Length != length)
+            {
+                throw new InvalidOperationException("Split length mismatch.");
+            }
+            
+            #endif
+
             var second = new ShapedTextCharacters(splitBuffer.Second!, Properties);
 
             return new SplitResult<ShapedTextCharacters>(first, second);

+ 38 - 38
src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs

@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
 using Avalonia.Media.TextFormatting.Unicode;
 using Avalonia.Utilities;
 
@@ -37,17 +38,20 @@ namespace Avalonia.Media.TextFormatting
         /// Gets a list of <see cref="ShapeableTextCharacters"/>.
         /// </summary>
         /// <returns>The shapeable text characters.</returns>
-        internal IList<ShapeableTextCharacters> GetShapeableCharacters(ReadOnlySlice<char> runText, sbyte biDiLevel)
+        internal IList<ShapeableTextCharacters> GetShapeableCharacters(ReadOnlySlice<char> runText, sbyte biDiLevel, 
+            ref TextRunProperties? previousProperties)
         {
             var shapeableCharacters = new List<ShapeableTextCharacters>(2);
 
             while (!runText.IsEmpty)
             {
-                var shapeableRun = CreateShapeableRun(runText, Properties, biDiLevel);
+                var shapeableRun = CreateShapeableRun(runText, Properties, biDiLevel, ref previousProperties);
 
                 shapeableCharacters.Add(shapeableRun);
 
                 runText = runText.Skip(shapeableRun.Text.Length);
+
+                previousProperties = shapeableRun.Properties;
             }
 
             return shapeableCharacters;
@@ -59,15 +63,29 @@ namespace Avalonia.Media.TextFormatting
         /// <param name="text">The text to create text runs from.</param>
         /// <param name="defaultProperties">The default text run properties.</param>
         /// <param name="biDiLevel">The bidi level of the run.</param>
+        /// <param name="previousProperties"></param>
         /// <returns>A list of shapeable text runs.</returns>
-        private ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice<char> text, TextRunProperties defaultProperties, sbyte biDiLevel)
+        private static ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice<char> text, 
+            TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties? previousProperties)
         {
             var defaultTypeface = defaultProperties.Typeface;
 
             var currentTypeface = defaultTypeface;
 
-            if (TryGetShapeableLength(text, currentTypeface, defaultTypeface, out var count))
+            if (TryGetShapeableLength(text, currentTypeface, out var count, out var script))
             {
+                var previousTypeface = previousProperties?.Typeface;
+                
+                if (script == Script.Common && previousTypeface is not null)
+                {
+                    if(TryGetShapeableLength(text, previousTypeface.Value, out var fallbackCount, out _))
+                    {
+                        return new ShapeableTextCharacters(text.Take(fallbackCount),
+                            new GenericTextRunProperties(previousTypeface.Value, defaultProperties.FontRenderingEmSize,
+                                defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel);
+                    }
+                }
+
                 return new ShapeableTextCharacters(text.Take(count),
                     new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize,
                         defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel);
@@ -94,7 +112,7 @@ namespace Avalonia.Media.TextFormatting
                 FontManager.Current.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight,
                     defaultTypeface.FontFamily, defaultProperties.CultureInfo, out currentTypeface);
 
-            if (matchFound && TextCharacters.TryGetShapeableLength(text, currentTypeface, defaultTypeface, out count))
+            if (matchFound && TryGetShapeableLength(text, currentTypeface, out count, out _))
             {
                 //Fallback found
                 return new ShapeableTextCharacters(text.Take(count),
@@ -127,30 +145,26 @@ namespace Avalonia.Media.TextFormatting
         }
 
         /// <summary>
-        /// Tries to get run properties.
+        /// Tries to get a shapeable length that is supported by the specified typeface.
         /// </summary>
-        /// <param name="defaultTypeface"></param>
-        /// <param name="text"></param>
+        /// <param name="text">The text.</param>
         /// <param name="typeface">The typeface that is used to find matching characters.</param>
-        /// <param name="length"></param>
+        /// <param name="length">The shapeable length.</param>
+        /// <param name="script"></param>
         /// <returns></returns>
-        protected static bool TryGetShapeableLength(ReadOnlySlice<char> text, Typeface typeface, Typeface defaultTypeface,
-            out int length)
+        protected static bool TryGetShapeableLength(ReadOnlySlice<char> text, Typeface typeface, out int length,
+            out Script script)
         {
+            length = 0;
+            script = Script.Unknown;
+
             if (text.Length == 0)
             {
-                length = 0;
                 return false;
             }
 
-            var isFallback = typeface != defaultTypeface;
-
-            length = 0;
-            var script = Script.Unknown;
-
             var font = typeface.GlyphTypeface;
-            var defaultFont = defaultTypeface.GlyphTypeface;
-            
+
             var enumerator = new GraphemeEnumerator(text);
 
             while (enumerator.MoveNext())
@@ -161,7 +175,8 @@ namespace Avalonia.Media.TextFormatting
 
                 if (currentScript != script)
                 {
-                    if (script is Script.Unknown || currentScript != Script.Common && (script is Script.Common || script is Script.Inherited))
+                    if (script is Script.Unknown || currentScript != Script.Common &&
+                        (script is Script.Common || script is Script.Inherited))
                     {
                         script = currentScript;
                     }
@@ -174,23 +189,8 @@ namespace Avalonia.Media.TextFormatting
                     }
                 }
 
-                //Only handle non whitespace here
-                if(!currentGrapheme.FirstCodepoint.IsWhiteSpace)
-                {
-                    //Stop at the first glyph that is present in the default typeface.
-                    if (isFallback && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
-                    {
-                        break;
-                    }
-
-                    //Stop at the first missing glyph
-                    if (!font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
-                    {
-                        break;
-                    }
-                }
-
-                if (!currentGrapheme.FirstCodepoint.IsWhiteSpace && !font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
+                //Stop at the first missing glyph
+                if (!font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
                 {
                     break;
                 }

+ 4 - 3
src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs

@@ -208,6 +208,7 @@ namespace Avalonia.Media.TextFormatting
             var levelIndex = 0;
             var runLevel = levels[0];
 
+            TextRunProperties? previousProperties = null;
             TextCharacters? currentRun = null;
             var runText = ReadOnlySlice<char>.Empty;
 
@@ -231,7 +232,7 @@ namespace Avalonia.Media.TextFormatting
 
                     if (j == runText.Length)
                     {
-                        yield return currentRun.GetShapeableCharacters(runText.Take(j), runLevel);
+                        yield return currentRun.GetShapeableCharacters(runText.Take(j), runLevel, ref previousProperties);
 
                         runLevel = levels[levelIndex];
 
@@ -244,7 +245,7 @@ namespace Avalonia.Media.TextFormatting
                     }
 
                     // End of this run
-                    yield return currentRun.GetShapeableCharacters(runText.Take(j), runLevel);
+                    yield return currentRun.GetShapeableCharacters(runText.Take(j), runLevel, ref previousProperties);
 
                     runText = runText.Skip(j);
 
@@ -260,7 +261,7 @@ namespace Avalonia.Media.TextFormatting
                 yield break;
             }
 
-            yield return currentRun.GetShapeableCharacters(runText, runLevel);
+            yield return currentRun.GetShapeableCharacters(runText, runLevel, ref previousProperties);
         }
 
         /// <summary>

+ 2 - 0
src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs

@@ -64,6 +64,8 @@ namespace Avalonia.Skia
 
         public SKTypeface Typeface { get; }
 
+        public int ReplacementCodepoint { get; }
+        
         /// <inheritdoc cref="IGlyphTypefaceImpl"/>
         public short DesignEmHeight { get; }
 

+ 32 - 0
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs

@@ -461,6 +461,38 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 Assert.Equal(expectedOffset, textLine.Start);
             }
         }
+        
+        [Fact]
+        public void Should_Wrap_Syriac()
+        {
+            using (Start())
+            {
+                const string text =
+                    "܀ ܁ ܂ ܃ ܄ ܅ ܆ ܇ ܈ ܉ ܊ ܋ ܌ ܍ ܏ ܐ ܑ ܒ ܓ ܔ ܕ ܖ ܗ ܘ ܙ ܚ ܛ ܜ ܝ ܞ ܟ ܠ ܡ ܢ ܣ ܤ ܥ ܦ ܧ ܨ ܩ ܪ ܫ ܬ ܰ ܱ ܲ ܳ ܴ ܵ ܶ ܷ ܸ ܹ ܺ ܻ ܼ ܽ ܾ ܿ ݀ ݁ ݂ ݃ ݄ ݅ ݆ ݇ ݈ ݉ ݊";
+                var defaultProperties = new GenericTextRunProperties(Typeface.Default);
+
+                var paragraphProperties =
+                    new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap);
+
+                var textSource = new SingleBufferTextSource(text, defaultProperties);
+                var formatter = new TextFormatterImpl();
+
+                var textPosition = 87;
+                TextLineBreak lastBreak = null;
+
+                while (textPosition < text.Length)
+                {
+                    var textLine =
+                        formatter.FormatLine(textSource, textPosition, 50, paragraphProperties, lastBreak);
+
+                    Assert.Equal(textLine.TextRange.Length, textLine.TextRuns.Sum(x => x.TextSourceLength));
+                    
+                    textPosition += textLine.TextRange.Length;
+
+                    lastBreak = textLine.TextLineBreak;
+                }
+            }
+        }
 
         [Fact]
         public void Should_FormatLine_With_Emergency_Breaks()