Browse Source

More text rendering improvements

Benedikt Schroeder 5 years ago
parent
commit
0b1b3914b3

+ 22 - 4
src/Avalonia.Visuals/Media/FontManager.cs

@@ -23,6 +23,11 @@ namespace Avalonia.Media
 
             DefaultFontFamilyName = PlatformImpl.GetDefaultFontFamilyName();
 
+            if (string.IsNullOrEmpty(DefaultFontFamilyName))
+            {
+                throw new InvalidOperationException("Default font family name can't be null or empty.");
+            }
+
             _defaultFontFamily = new FontFamily(DefaultFontFamilyName);
         }
 
@@ -39,7 +44,8 @@ namespace Avalonia.Media
 
                 var fontManagerImpl = AvaloniaLocator.Current.GetService<IFontManagerImpl>();
 
-                if (fontManagerImpl == null) throw new InvalidOperationException("No font manager implementation was registered.");
+                if (fontManagerImpl == null)
+                    throw new InvalidOperationException("No font manager implementation was registered.");
 
                 current = new FontManager(fontManagerImpl);
 
@@ -87,7 +93,7 @@ namespace Avalonia.Media
                     fontFamily = _defaultFontFamily;
                 }
 
-                var key = new FontKey(fontFamily, fontWeight, fontStyle);
+                var key = new FontKey(fontFamily.Name, fontWeight, fontStyle);
 
                 if (_typefaceCache.TryGetValue(key, out var typeface))
                 {
@@ -126,9 +132,21 @@ namespace Avalonia.Media
             FontStyle fontStyle = FontStyle.Normal,
             FontFamily fontFamily = null, CultureInfo culture = null)
         {
-            return PlatformImpl.TryMatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture, out var key) ?
-                _typefaceCache.GetOrAdd(key, new Typeface(key.FontFamily, key.Weight, key.Style)) :
+            foreach (var cachedTypeface in _typefaceCache.Values)
+            {
+                // First try to find a cached typeface by style and weight to avoid redundant glyph index lookup.
+                if (cachedTypeface.Style == fontStyle && cachedTypeface.Weight == fontWeight
+                                                      && cachedTypeface.GlyphTypeface.GetGlyph((uint)codepoint) != 0)
+                {
+                    return cachedTypeface;
+                }
+            }
+
+            var matchedTypeface = PlatformImpl.TryMatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture, out var key) ?
+                _typefaceCache.GetOrAdd(key, new Typeface(key.FamilyName, key.Weight, key.Style)) :
                 null;
+
+            return matchedTypeface;
         }
     }
 }

+ 1 - 2
src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs

@@ -1,7 +1,6 @@
 using System;
 using System.Collections;
 using System.Collections.Generic;
-using System.Linq;
 using System.Text;
 using Avalonia.Utilities;
 
@@ -21,7 +20,7 @@ namespace Avalonia.Media.Fonts
                 throw new ArgumentNullException(nameof(familyNames));
             }
 
-            Names = familyNames.Split(',').Select(x => x.Trim()).ToArray();
+            Names = Array.ConvertAll(familyNames.Split(','), p => p.Trim());
 
             PrimaryFamilyName = Names[0];
 

+ 5 - 5
src/Avalonia.Visuals/Media/Fonts/FontKey.cs

@@ -4,20 +4,20 @@ namespace Avalonia.Media.Fonts
 {
     public readonly struct FontKey : IEquatable<FontKey>
     {
-        public readonly FontFamily FontFamily;
+        public readonly string FamilyName;
         public readonly FontStyle Style;
         public readonly FontWeight Weight;
 
-        public FontKey(FontFamily fontFamily, FontWeight weight, FontStyle style)
+        public FontKey(string familyName, FontWeight weight, FontStyle style)
         {
-            FontFamily = fontFamily;
+            FamilyName = familyName;
             Style = style;
             Weight = weight;
         }
 
         public override int GetHashCode()
         {
-            var hash = FontFamily.GetHashCode();
+            var hash = FamilyName.GetHashCode();
 
             hash = hash * 31 + (int)Style;
             hash = hash * 31 + (int)Weight;
@@ -32,7 +32,7 @@ namespace Avalonia.Media.Fonts
 
         public bool Equals(FontKey other)
         {
-            return FontFamily == other.FontFamily &&
+            return FamilyName == other.FamilyName &&
                 Style == other.Style &&
                    Weight == other.Weight;
         }

+ 1 - 1
src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs

@@ -66,7 +66,7 @@ namespace Avalonia.Media.TextFormatting
 
             //ToDo: Fix FontFamily fallback
             currentTypeface =
-                FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Weight, defaultTypeface.Style);
+                FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Weight, defaultTypeface.Style, defaultStyle.TextFormat.Typeface.FontFamily);
 
             if (currentTypeface != null && TryGetRunProperties(text, currentTypeface, defaultTypeface, out count))
             {

+ 1 - 1
src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs

@@ -2,7 +2,7 @@
 
 namespace Avalonia.Media.TextFormatting.Unicode
 {
-    internal ref struct CodepointEnumerator
+    public ref struct CodepointEnumerator
     {
         private ReadOnlySlice<char> _text;
 

+ 0 - 44
src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeGeneralCategory.cs

@@ -1,44 +0,0 @@
-namespace Avalonia.Media.TextFormatting.Unicode
-{
-    public enum UnicodeGeneralCategory : byte
-    {
-        Other, //C# Cc | Cf | Cn | Co | Cs
-        Control, //Cc
-        Format, //Cf
-        Unassigned, //Cn
-        PrivateUse, //Co
-        Surrogate, //Cs
-        Letter, //L# Ll | Lm | Lo | Lt | Lu
-        CasedLetter, //LC# Ll | Lt | Lu
-        LowercaseLetter, //Ll
-        ModifierLetter, //Lm
-        OtherLetter, //Lo
-        TitlecaseLetter, //Lt
-        UppercaseLetter, //Lu
-        Mark, //M
-        SpacingMark, //Mc
-        EnclosingMark, //Me
-        NonspacingMark, //Mn
-        Number, //N# Nd | Nl | No
-        DecimalNumber, //Nd
-        LetterNumber, //Nl
-        OtherNumber, //No
-        Punctuation, //P
-        ConnectorPunctuation, //Pc
-        DashPunctuation, //Pd
-        ClosePunctuation, //Pe
-        FinalPunctuation, //Pf
-        InitialPunctuation, //Pi
-        OtherPunctuation, //Po
-        OpenPunctuation, //Ps
-        Symbol, //S# Sc | Sk | Sm | So
-        CurrencySymbol, //Sc
-        ModifierSymbol, //Sk
-        MathSymbol, //Sm
-        OtherSymbol, //So
-        Separator, //Z# Zl | Zp | Zs
-        LineSeparator, //Zl
-        ParagraphSeparator, //Zp
-        SpaceSeparator, //Zs
-    }
-}

+ 45 - 22
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@@ -30,6 +30,10 @@ namespace Avalonia.Skia
         private Matrix _currentTransform;
         private GRContext _grContext;
         private bool _disposed;
+
+        private readonly SKPaint _strokePaint = new SKPaint();
+        private readonly SKPaint _fillPaint = new SKPaint();
+
         /// <summary>
         /// Context create info.
         /// </summary>
@@ -153,7 +157,7 @@ namespace Avalonia.Skia
         /// <inheritdoc />
         public void DrawLine(IPen pen, Point p1, Point p2)
         {
-            using (var paint = CreatePaint(pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y))))
+            using (var paint = CreatePaint(_strokePaint, pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y))))
             {
                 Canvas.DrawLine((float) p1.X, (float) p1.Y, (float) p2.X, (float) p2.Y, paint.Paint);
             }
@@ -165,8 +169,8 @@ namespace Avalonia.Skia
             var impl = (GeometryImpl) geometry;
             var size = geometry.Bounds.Size;
 
-            using (var fill = brush != null ? CreatePaint(brush, size) : default(PaintWrapper))
-            using (var stroke = pen?.Brush != null ? CreatePaint(pen, size) : default(PaintWrapper))
+            using (var fill = brush != null ? CreatePaint(_fillPaint, brush, size) : default(PaintWrapper))
+            using (var stroke = pen?.Brush != null ? CreatePaint(_strokePaint, pen, size) : default(PaintWrapper))
             {
                 if (fill.Paint != null)
                 {
@@ -188,7 +192,7 @@ namespace Avalonia.Skia
 
             if (brush != null)
             {
-                using (var paint = CreatePaint(brush, rect.Size))
+                using (var paint = CreatePaint(_fillPaint, brush, rect.Size))
                 {
                     if (isRounded)
                     {
@@ -204,7 +208,7 @@ namespace Avalonia.Skia
 
             if (pen?.Brush != null)
             {
-                using (var paint = CreatePaint(pen, rect.Size))
+                using (var paint = CreatePaint(_strokePaint, pen, rect.Size))
                 {
                     if (isRounded)
                     {
@@ -222,7 +226,7 @@ namespace Avalonia.Skia
         /// <inheritdoc />
         public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text)
         {
-            using (var paint = CreatePaint(foreground, text.Bounds.Size))
+            using (var paint = CreatePaint(_fillPaint, foreground, text.Bounds.Size))
             {
                 var textImpl = (FormattedTextImpl) text;
                 textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, _canTextUseLcdRendering);
@@ -232,14 +236,14 @@ namespace Avalonia.Skia
         /// <inheritdoc />
         public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin)
         {
-            using (var paint = CreatePaint(foreground, glyphRun.Bounds.Size))
+            using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Bounds.Size))
             {
                 var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl;
 
-                paint.ApplyTo(glyphRunImpl.Paint);
+                ConfigureTextRendering(paintWrapper);
 
                 Canvas.DrawText(glyphRunImpl.TextBlob, (float)baselineOrigin.X,
-                    (float)baselineOrigin.Y, glyphRunImpl.Paint);
+                    (float)baselineOrigin.Y, paintWrapper.Paint);
             }
         }
 
@@ -323,7 +327,7 @@ namespace Avalonia.Skia
             var paint = new SKPaint();
 
             Canvas.SaveLayer(paint);
-            _maskStack.Push(CreatePaint(mask, bounds.Size));
+            _maskStack.Push(CreatePaint(paint, mask, bounds.Size, true));
         }
 
         /// <inheritdoc />
@@ -364,6 +368,15 @@ namespace Avalonia.Skia
             }
         }
 
+        internal void ConfigureTextRendering(PaintWrapper wrapper)
+        {
+            var paint = wrapper.Paint;
+
+            paint.IsEmbeddedBitmapText = true;
+            paint.SubpixelText = true;
+            paint.LcdRenderText = _canTextUseLcdRendering;
+        }
+
         /// <summary>
         /// Configure paint wrapper for using gradient brush.
         /// </summary>
@@ -514,17 +527,16 @@ namespace Avalonia.Skia
         /// <summary>
         /// Creates paint wrapper for given brush.
         /// </summary>
+        /// <param name="paint">The paint to wrap.</param>
         /// <param name="brush">Source brush.</param>
         /// <param name="targetSize">Target size.</param>
+        /// <param name="disposePaint">Optional dispose of the supplied paint.</param>
         /// <returns>Paint wrapper for given brush.</returns>
-        internal PaintWrapper CreatePaint(IBrush brush, Size targetSize)
+        internal PaintWrapper CreatePaint(SKPaint paint, IBrush brush, Size targetSize, bool disposePaint = false)
         {
-            var paint = new SKPaint
-            {
-                IsAntialias = true
-            };
+            var paintWrapper = new PaintWrapper(paint, disposePaint);
 
-            var paintWrapper = new PaintWrapper(paint);
+            paint.IsAntialias = true;
 
             double opacity = brush.Opacity * _currentOpacity;
 
@@ -572,10 +584,12 @@ namespace Avalonia.Skia
         /// <summary>
         /// Creates paint wrapper for given pen.
         /// </summary>
+        /// <param name="paint">The paint to wrap.</param>
         /// <param name="pen">Source pen.</param>
         /// <param name="targetSize">Target size.</param>
+        /// <param name="disposePaint">Optional dispose of the supplied paint.</param>
         /// <returns></returns>
-        private PaintWrapper CreatePaint(IPen pen, Size targetSize)
+        private PaintWrapper CreatePaint(SKPaint paint, IPen pen, Size targetSize, bool disposePaint = false)
         {
             // In Skia 0 thickness means - use hairline rendering
             // and for us it means - there is nothing rendered.
@@ -584,8 +598,7 @@ namespace Avalonia.Skia
                 return default;
             }
 
-            var rv = CreatePaint(pen.Brush, targetSize);
-            var paint = rv.Paint;
+            var rv = CreatePaint(paint, pen.Brush, targetSize, disposePaint);
 
             paint.IsStroke = true;
             paint.StrokeWidth = (float) pen.Thickness;
@@ -668,7 +681,7 @@ namespace Avalonia.Skia
         /// <summary>
         /// Skia cached paint state.
         /// </summary>
-        private struct PaintState : IDisposable
+        private readonly struct PaintState : IDisposable
         {
             private readonly SKColor _color;
             private readonly SKShader _shader;
@@ -696,14 +709,16 @@ namespace Avalonia.Skia
         {
             //We are saving memory allocations there
             public readonly SKPaint Paint;
+            private readonly bool _disposePaint;
 
             private IDisposable _disposable1;
             private IDisposable _disposable2;
             private IDisposable _disposable3;
 
-            public PaintWrapper(SKPaint paint)
+            public PaintWrapper(SKPaint paint, bool disposePaint)
             {
                 Paint = paint;
+                _disposePaint = disposePaint;
 
                 _disposable1 = null;
                 _disposable2 = null;
@@ -751,7 +766,15 @@ namespace Avalonia.Skia
             /// <inheritdoc />
             public void Dispose()
             {
-                Paint?.Dispose();
+                if (_disposePaint)
+                {
+                    Paint?.Dispose();
+                }
+                else
+                {
+                    Paint?.Reset();
+                }
+
                 _disposable1?.Dispose();
                 _disposable2?.Dispose();
                 _disposable3?.Dispose();

+ 37 - 9
src/Skia/Avalonia.Skia/FontManagerImpl.cs

@@ -32,6 +32,27 @@ namespace Avalonia.Skia
         public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle,
             FontFamily fontFamily, CultureInfo culture, out FontKey fontKey)
         {
+            SKFontStyle skFontStyle;
+
+            switch (fontWeight)
+            {
+                case FontWeight.Normal when fontStyle == FontStyle.Normal:
+                    skFontStyle = SKFontStyle.Normal;
+                    break;
+                case FontWeight.Normal when fontStyle == FontStyle.Italic:
+                    skFontStyle = SKFontStyle.Italic;
+                    break;
+                case FontWeight.Bold when fontStyle == FontStyle.Normal:
+                    skFontStyle = SKFontStyle.Bold;
+                    break;
+                case FontWeight.Bold when fontStyle == FontStyle.Italic:
+                    skFontStyle = SKFontStyle.BoldItalic;
+                    break;
+                default:
+                    skFontStyle = new SKFontStyle((SKFontStyleWeight)fontWeight, SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle);
+                    break;
+            }
+
             if (culture == null)
             {
                 culture = CultureInfo.CurrentUICulture;
@@ -45,31 +66,32 @@ namespace Avalonia.Skia
             t_languageTagBuffer[0] = culture.TwoLetterISOLanguageName;
             t_languageTagBuffer[1] = culture.ThreeLetterISOLanguageName;
 
-            if (fontFamily != null)
+            if (fontFamily != null && fontFamily.FamilyNames.HasFallbacks)
             {
-                foreach (var familyName in fontFamily.FamilyNames)
+                var familyNames = fontFamily.FamilyNames;
+
+                for (var i = 1; i < familyNames.Count; i++)
                 {
-                    var skTypeface = _skFontManager.MatchCharacter(familyName, (SKFontStyleWeight)fontWeight,
-                        SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, t_languageTagBuffer, codepoint);
+                    var skTypeface =
+                        _skFontManager.MatchCharacter(familyNames[i], skFontStyle, t_languageTagBuffer, codepoint);
 
                     if (skTypeface == null)
                     {
                         continue;
                     }
 
-                    fontKey = new FontKey(new FontFamily(familyName), fontWeight, fontStyle);
+                    fontKey = new FontKey(skTypeface.FamilyName, fontWeight, fontStyle);
 
                     return true;
                 }
             }
             else
             {
-                var skTypeface = _skFontManager.MatchCharacter(null, (SKFontStyleWeight)fontWeight,
-                    SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, t_languageTagBuffer, codepoint);
+                var skTypeface = _skFontManager.MatchCharacter(null, skFontStyle, t_languageTagBuffer, codepoint);
 
                 if (skTypeface != null)
                 {
-                    fontKey = new FontKey(new FontFamily(skTypeface.FamilyName), fontWeight, fontStyle);
+                    fontKey = new FontKey(skTypeface.FamilyName, fontWeight, fontStyle);
 
                     return true;
                 }
@@ -82,7 +104,7 @@ namespace Avalonia.Skia
 
         public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
         {
-            var skTypeface = SKTypeface.Default;
+            SKTypeface skTypeface = null;
 
             if (typeface.FontFamily.Key == null)
             {
@@ -109,6 +131,12 @@ namespace Avalonia.Skia
                 skTypeface = fontCollection.Get(typeface);
             }
 
+            if (skTypeface == null)
+            {
+                throw new InvalidOperationException(
+                    $"Could not create glyph typeface for: {typeface.FontFamily.Name}.");
+            }
+
             return new GlyphTypefaceImpl(skTypeface);
         }
     }

+ 2 - 1
src/Skia/Avalonia.Skia/FormattedTextImpl.cs

@@ -266,7 +266,8 @@ namespace Avalonia.Skia
                                 if (fb != null)
                                 {
                                     //TODO: figure out how to get the brush size
-                                    currentWrapper = context.CreatePaint(fb, new Size());
+                                    currentWrapper = context.CreatePaint(new SKPaint { IsAntialias = true }, fb,
+                                        new Size());
                                 }
                                 else
                                 {

+ 1 - 8
src/Skia/Avalonia.Skia/GlyphRunImpl.cs

@@ -7,17 +7,11 @@ namespace Avalonia.Skia
     /// <inheritdoc />
     public class GlyphRunImpl : IGlyphRunImpl
     {
-        public GlyphRunImpl(SKPaint paint, SKTextBlob textBlob)
+        public GlyphRunImpl(SKTextBlob textBlob)
         {
-            Paint = paint;
             TextBlob = textBlob;
         }
 
-        /// <summary>
-        ///     Gets the paint to draw with.
-        /// </summary>
-        public SKPaint Paint { get; }
-
         /// <summary>
         ///     Gets the text blob to draw.
         /// </summary>
@@ -26,7 +20,6 @@ namespace Avalonia.Skia
         void IDisposable.Dispose()
         {
             TextBlob.Dispose();
-            Paint.Dispose();
         }
     }
 }

+ 59 - 57
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@@ -149,6 +149,16 @@ namespace Avalonia.Skia
             return new WriteableBitmapImpl(size, dpi, format);
         }
 
+        private static readonly SKPaint s_paint = new SKPaint
+        {
+            TextEncoding = SKTextEncoding.GlyphId,
+            IsAntialias = true,
+            IsStroke = false,
+            SubpixelText = true
+        };
+
+        private static readonly SKTextBlobBuilder s_textBlobBuilder = new SKTextBlobBuilder();
+
         /// <inheritdoc />
         public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
         {
@@ -158,92 +168,84 @@ namespace Avalonia.Skia
 
             var typeface = glyphTypeface.Typeface;
 
-            var paint = new SKPaint
-            {
-                TextSize = (float)glyphRun.FontRenderingEmSize,
-                Typeface = typeface,
-                TextEncoding = SKTextEncoding.GlyphId,
-                IsAntialias = true,
-                IsStroke = false,
-                SubpixelText = true
-            };
-
-            using (var textBlobBuilder = new SKTextBlobBuilder())
-            {
-                SKTextBlob textBlob;
+            s_paint.TextSize = (float)glyphRun.FontRenderingEmSize;
+            s_paint.Typeface = typeface;
 
-                width = 0;
 
-                var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight);
+            SKTextBlob textBlob;
 
-                if (glyphRun.GlyphOffsets.IsEmpty)
-                {
-                    if (glyphTypeface.IsFixedPitch)
-                    {
-                        textBlobBuilder.AddRun(paint, 0, 0, glyphRun.GlyphIndices.Buffer.Span);
+            width = 0;
 
-                        textBlob = textBlobBuilder.Build();
+            var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight);
 
-                        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++)
-                        {
-                            positions[i] = (float)width;
-
-                            if (glyphRun.GlyphAdvances.IsEmpty)
-                            {
-                                width += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
-                            }
-                            else
-                            {
-                                width += glyphRun.GlyphAdvances[i];
-                            }
-                        }
+            if (glyphRun.GlyphOffsets.IsEmpty)
+            {
+                if (glyphTypeface.IsFixedPitch)
+                {
+                    s_textBlobBuilder.AddRun(s_paint, 0, 0, glyphRun.GlyphIndices.Buffer.Span);
 
-                        buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span);
+                    textBlob = s_textBlobBuilder.Build();
 
-                        textBlob = textBlobBuilder.Build();
-                    }
+                    width = glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[0]) * scale * glyphRun.GlyphIndices.Length;
                 }
                 else
                 {
-                    var buffer = textBlobBuilder.AllocatePositionedRun(paint, count);
+                    var buffer = s_textBlobBuilder.AllocateHorizontalRun(s_paint, count, 0);
 
-                    var glyphPositions = buffer.GetPositionSpan();
-
-                    var currentX = 0.0;
+                    var positions = buffer.GetPositionSpan();
 
                     for (var i = 0; i < count; i++)
                     {
-                        var glyphOffset = glyphRun.GlyphOffsets[i];
-
-                        glyphPositions[i] = new SKPoint((float)(currentX + glyphOffset.X), (float)glyphOffset.Y);
+                        positions[i] = (float)width;
 
                         if (glyphRun.GlyphAdvances.IsEmpty)
                         {
-                            currentX += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
+                            width += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
                         }
                         else
                         {
-                            currentX += glyphRun.GlyphAdvances[i];
+                            width += glyphRun.GlyphAdvances[i];
                         }
                     }
 
                     buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span);
 
-                    width = currentX;
+                    textBlob = s_textBlobBuilder.Build();
+                }
+            }
+            else
+            {
+                var buffer = s_textBlobBuilder.AllocatePositionedRun(s_paint, count);
+
+                var glyphPositions = buffer.GetPositionSpan();
+
+                var currentX = 0.0;
+
+                for (var i = 0; i < count; i++)
+                {
+                    var glyphOffset = glyphRun.GlyphOffsets[i];
+
+                    glyphPositions[i] = new SKPoint((float)(currentX + glyphOffset.X), (float)glyphOffset.Y);
 
-                    textBlob = textBlobBuilder.Build();
+                    if (glyphRun.GlyphAdvances.IsEmpty)
+                    {
+                        currentX += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
+                    }
+                    else
+                    {
+                        currentX += glyphRun.GlyphAdvances[i];
+                    }
                 }
 
-                return new GlyphRunImpl(paint, textBlob);
+                buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span);
+
+                width = currentX;
+
+                textBlob = s_textBlobBuilder.Build();
             }
+
+            return new GlyphRunImpl(textBlob);
+
         }
     }
 }

+ 2 - 2
src/Skia/Avalonia.Skia/SKTypefaceCollection.cs

@@ -19,7 +19,7 @@ namespace Avalonia.Skia
 
         public SKTypeface Get(Typeface typeface)
         {
-            var key = new FontKey(typeface.FontFamily, typeface.Weight, typeface.Style);
+            var key = new FontKey(typeface.FontFamily.Name, typeface.Weight, typeface.Style);
 
             return GetNearestMatch(_typefaces, key);
         }
@@ -49,7 +49,7 @@ namespace Avalonia.Skia
 
             if (keys.Length == 0)
             {
-                return SKTypeface.Default;
+                return null;
             }
 
             key = keys[0];

+ 1 - 1
src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs

@@ -54,7 +54,7 @@ namespace Avalonia.Skia
                     continue;
                 }
 
-                var key = new FontKey(fontFamily, (FontWeight)typeface.FontWeight, (FontStyle)typeface.FontSlant);
+                var key = new FontKey(fontFamily.Name, (FontWeight)typeface.FontWeight, (FontStyle)typeface.FontSlant);
 
                 typeFaceCollection.AddTypeface(key, typeface);
             }

+ 1 - 1
src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs

@@ -50,7 +50,7 @@ namespace Avalonia.Direct2D1.Media
 
                 var fontFamilyName = font.FontFamily.FamilyNames.GetString(0);
 
-                fontKey = new FontKey(new FontFamily(fontFamilyName), fontWeight, fontStyle);
+                fontKey = new FontKey(fontFamilyName, fontWeight, fontStyle);
 
                 return true;
             }

+ 38 - 10
tests/Avalonia.Skia.UnitTests/CustomFontManagerImpl.cs

@@ -11,10 +11,11 @@ namespace Avalonia.Skia.UnitTests
     public class CustomFontManagerImpl : IFontManagerImpl
     {
         private readonly Typeface[] _customTypefaces;
+        private readonly string _defaultFamilyName;
 
         private readonly Typeface _defaultTypeface =
             new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono");
-        private readonly Typeface _italicTypeface = 
+        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");
@@ -22,11 +23,12 @@ namespace Avalonia.Skia.UnitTests
         public CustomFontManagerImpl()
         {
             _customTypefaces = new[] { _emojiTypeface, _italicTypeface, _defaultTypeface };
+            _defaultFamilyName = _defaultTypeface.FontFamily.FamilyNames.PrimaryFamilyName;
         }
 
         public string GetDefaultFontFamilyName()
         {
-            return _defaultTypeface.FontFamily.ToString();
+            return _defaultFamilyName;
         }
 
         public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
@@ -34,39 +36,65 @@ namespace Avalonia.Skia.UnitTests
             return _customTypefaces.Select(x => x.FontFamily.Name);
         }
 
+        private readonly string[] _bcp47 = { CultureInfo.CurrentCulture.ThreeLetterISOLanguageName, CultureInfo.CurrentCulture.TwoLetterISOLanguageName };
+
         public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily,
             CultureInfo culture, out FontKey fontKey)
         {
             foreach (var customTypeface in _customTypefaces)
             {
                 if (customTypeface.GlyphTypeface.GetGlyph((uint)codepoint) == 0)
+                {
                     continue;
-                fontKey = new FontKey(customTypeface.FontFamily, fontWeight, fontStyle);
+                }
+
+                fontKey = new FontKey(customTypeface.FontFamily.Name, fontWeight, fontStyle);
 
                 return true;
             }
 
-            var fallback = SKFontManager.Default.MatchCharacter(codepoint);
+            var fallback = SKFontManager.Default.MatchCharacter(fontFamily?.Name, (SKFontStyleWeight)fontWeight,
+                SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, _bcp47, codepoint);
 
-            fontKey = new FontKey(fallback?.FamilyName ?? SKTypeface.Default.FamilyName, fontWeight, fontStyle);
+            fontKey = new FontKey(fallback?.FamilyName ?? _defaultFamilyName, fontWeight, fontStyle);
 
             return true;
         }
 
         public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
         {
+            SKTypeface skTypeface;
+
             switch (typeface.FontFamily.Name)
             {
                 case "Twitter Color Emoji":
+                    {
+                        var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_emojiTypeface.FontFamily);
+                        skTypeface = typefaceCollection.Get(typeface);
+                        break;
+                    }
+
                 case "Noto Sans":
+                    {
+                        var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_italicTypeface.FontFamily);
+                        skTypeface = typefaceCollection.Get(typeface);
+                        break;
+                    }
                 case "Noto Mono":
-                    var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily);
-                    var skTypeface = typefaceCollection.Get(typeface);
-                    return new GlyphTypefaceImpl(skTypeface);
+                    {
+                        var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_defaultTypeface.FontFamily);
+                        skTypeface = typefaceCollection.Get(typeface);
+                        break;
+                    }
                 default:
-                    return new GlyphTypefaceImpl(SKTypeface.FromFamilyName(typeface.FontFamily.Name,
-                        (SKFontStyleWeight)typeface.Weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style));
+                    {
+                        skTypeface = SKTypeface.FromFamilyName(typeface.FontFamily.Name,
+                            (SKFontStyleWeight)typeface.Weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style);
+                        break;
+                    }
             }
+
+            return new GlyphTypefaceImpl(skTypeface);
         }
     }
 }

+ 13 - 0
tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs

@@ -95,5 +95,18 @@ namespace Avalonia.Skia.UnitTests
                 Assert.Equal("Noto Mono", skTypeface.FamilyName);
             }
         }
+
+        [Fact]
+        public void Should_Throw_For_Invalid_Custom_Font()
+        {
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
+            {
+                var fontManager = new FontManagerImpl();
+
+                Assert.Throws<InvalidOperationException>(() =>
+                    fontManager.CreateGlyphTypeface(
+                        new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Unknown")));
+            }
+        }
     }
 }

+ 22 - 2
tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs

@@ -332,7 +332,7 @@ namespace Avalonia.Skia.UnitTests
                     Typeface.Default,
                     12.0f,
                     Brushes.Black.ToImmutable(),
-                    maxWidth : 200, 
+                    maxWidth : 200,
                     maxHeight : 125,
                     textStyleOverrides: spans);
 
@@ -506,10 +506,30 @@ namespace Avalonia.Skia.UnitTests
             }
         }
 
+        private const string Text = "日本でTest一番読まれている英字新聞・ジャパンタイムズが発信する国内外ニュースと、様々なジャンルの特集記事。";
+
+        [Fact]
+        public void Should_Wrap()
+        {
+            using (Start())
+            {
+                for (var i = 0; i < 2000; i++)
+                {
+                    var layout = new TextLayout(
+                        Text,
+                        Typeface.Default,
+                        12,
+                        Brushes.Black,
+                        textWrapping: TextWrapping.Wrap,
+                        maxWidth: 50);
+                }
+            }
+        }
+
         public static IDisposable Start()
         {
             var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
-                .With(renderInterface: new PlatformRenderInterface(null), 
+                .With(renderInterface: new PlatformRenderInterface(null),
                     textShaperImpl: new TextShaperImpl(),
                     fontManagerImpl : new CustomFontManagerImpl()));
 

+ 9 - 2
tests/Avalonia.UnitTests/MockFontManagerImpl.cs

@@ -8,14 +8,21 @@ namespace Avalonia.UnitTests
 {
     public class MockFontManagerImpl : IFontManagerImpl
     {
+        private readonly string _defaultFamilyName;
+
+        public MockFontManagerImpl(string defaultFamilyName = "Default")
+        {
+            _defaultFamilyName = defaultFamilyName;
+        }
+
         public string GetDefaultFontFamilyName()
         {
-            return "Default";
+            return _defaultFamilyName;
         }
 
         public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
         {
-            return new[] { "Default" };
+            return new[] { _defaultFamilyName };
         }
 
         public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily,

+ 11 - 1
tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs

@@ -1,4 +1,5 @@
-using Avalonia.Media;
+using System;
+using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.UnitTests;
 using Xunit;
@@ -19,5 +20,14 @@ namespace Avalonia.Visuals.UnitTests.Media
                 Assert.Same(typeface, FontManager.Current.GetOrAddTypeface(fontFamily));
             }
         }
+
+        [Fact]
+        public void Should_Throw_When_Default_FamilyName_Is_Null()
+        {
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new MockFontManagerImpl(null))))
+            {
+                Assert.Throws<InvalidOperationException>(() => FontManager.Current);
+            }
+        }
     }
 }